mirror of
https://github.com/rustfs/console.git
synced 2026-09-21 06:06:37 +08:00
feat: 添加对象保留功能及相关界面优化
This commit is contained in:
+100
-7
@@ -17,14 +17,10 @@
|
||||
<span>{{ t("Tags") }}</span>
|
||||
</n-button>
|
||||
|
||||
<!-- <n-button v-if="lockStatus" @click="() => true == true">
|
||||
<n-button v-if="lockStatus" @click="() => (showRetentionView = true)">
|
||||
<Icon name="ri:save-2-line" class="mr-2" />
|
||||
<span>{{ t("Retention") }}</span>
|
||||
</n-button>
|
||||
<n-button v-if="lockStatus" @click="() => true == true">
|
||||
<Icon name="ri:auction-line" class="mr-2" />
|
||||
<span>{{ t("Legal Hold") }}</span>
|
||||
</n-button> -->
|
||||
|
||||
<n-button id="copyTag" ref="copyRef" @click="copySignUrl">
|
||||
<Icon name="ri:file-copy-line" class="mr-2" />
|
||||
@@ -64,7 +60,14 @@
|
||||
</n-descriptions-item>
|
||||
|
||||
<!-- 保留 -->
|
||||
<n-descriptions-item :label="t('Retention') + t('Policy')">compliance</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Retention') + t('Policy')">
|
||||
<n-space>
|
||||
<span>{{ t("Retention Type") + ": " + retentionMode }}</span>
|
||||
</n-space>
|
||||
<n-space>
|
||||
<span>{{ t("Retention RetainUntilDate") + ": " + retainUntilDate }}</span>
|
||||
</n-space>
|
||||
</n-descriptions-item>
|
||||
|
||||
<!-- <n-descriptions-item label="版本ID">{{ object?.VersionId }}</n-descriptions-item>
|
||||
<n-descriptions-item label="存储类型">{{ object?.StorageClass }}</n-descriptions-item> -->
|
||||
@@ -105,6 +108,42 @@
|
||||
</n-form>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showRetentionView"
|
||||
preset="card"
|
||||
:title="t('Retention') + t('Policy')"
|
||||
class="max-w-screen-md">
|
||||
<n-card class="flex-col justify-center items-center">
|
||||
<n-form
|
||||
ref="retentionFormRef"
|
||||
:label-width="200"
|
||||
label-placement="left"
|
||||
label-align="left"
|
||||
class="w-[500px]">
|
||||
<n-form-item :label="t('Retention Mode')" path="retentionMode" class="flex-auto">
|
||||
<n-radio-group v-model:value="retentionMode">
|
||||
<n-radio value="COMPLIANCE">Compliance</n-radio>
|
||||
<n-radio value="GOVERNANCE">Governance</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="t('Retention RetainUntilDate')" path="retainUntilDate" class="flex-auto">
|
||||
<n-date-picker
|
||||
v-model:formatted-value="retainUntilDate"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
type="datetime"
|
||||
clearable />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="submitRetention">{{ t("Confirm") }}</n-button>
|
||||
<n-button class="mx-4" @click="resetRetention">{{ t("Reset") }}</n-button>
|
||||
<n-button @click="showRetentionView = false">{{ t("Cancel") }}</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</n-card>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
@@ -112,6 +151,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { t } = useI18n();
|
||||
const visibel = ref(false);
|
||||
@@ -178,6 +218,7 @@ const getObjectLockConfig = async () => {
|
||||
// 如果桶开启了object lock,则获取合法保留状态
|
||||
if (lockStatus.value) {
|
||||
getObjectLegalHoldFn();
|
||||
getObjectRetentionFn();
|
||||
}
|
||||
} else {
|
||||
lockStatus.value = false;
|
||||
@@ -211,7 +252,59 @@ const handleChangeLegalStatus = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/****************************************************************** */
|
||||
/**======================================================= */
|
||||
// 保留相关
|
||||
const retentionFormRef = ref();
|
||||
|
||||
const retentionMode = ref<string | null>(null);
|
||||
const retainUntilDate = ref<string | null>(null);
|
||||
console.log("🚀 ~ retainUntilDate:", retainUntilDate.value);
|
||||
|
||||
const retentionLoading = ref(false);
|
||||
const showRetentionView = ref(false);
|
||||
// 获取保留状态
|
||||
const getObjectRetentionFn = () => {
|
||||
const { getObjectRetention } = useObject({
|
||||
bucket: bucketName.value,
|
||||
});
|
||||
getObjectRetention(key.value).then((res) => {
|
||||
if (res.Retention) {
|
||||
retentionMode.value = res.Retention.Mode || "";
|
||||
retainUntilDate.value = res.Retention.RetainUntilDate
|
||||
? dayjs(res.Retention.RetainUntilDate).format("YYYY-MM-DD HH:mm:ss")
|
||||
: null;
|
||||
} else {
|
||||
retentionMode.value = "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const submitRetention = () => {
|
||||
retentionLoading.value = true;
|
||||
|
||||
const { putObjectRetention } = useObject({
|
||||
bucket: bucketName.value,
|
||||
});
|
||||
putObjectRetention(key.value, {
|
||||
Mode: retentionMode.value,
|
||||
RetainUntilDate: retainUntilDate.value ? new Date(retainUntilDate.value) : null,
|
||||
})
|
||||
.then(() => {
|
||||
message.success(t("Retention Update Success"));
|
||||
getObjectRetentionFn();
|
||||
})
|
||||
.finally(() => {
|
||||
retentionLoading.value = false;
|
||||
showRetentionView.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const resetRetention = () => {
|
||||
retentionMode.value = null;
|
||||
retainUntilDate.value = null;
|
||||
};
|
||||
|
||||
/**======================================================= */
|
||||
|
||||
// 删除标签
|
||||
const handledeleteTag = async (index: number) => {
|
||||
|
||||
+70
-41
@@ -38,10 +38,24 @@
|
||||
</div>
|
||||
</template>
|
||||
</n-page-header>
|
||||
<n-data-table class="border dark:border-neutral-700 rounded overflow-hidden" :columns="columns" :data="filteredObjects" :row-key="rowKey" @update:checked-row-keys="handleCheck"
|
||||
:pagination="false" :bordered="false" />
|
||||
<object-upload-picker :show="uploadPickerVisible" @update:show="(val: any) => (uploadPickerVisible = val && refresh())" :bucketName="bucketName" :prefix="prefix" />
|
||||
<object-new-form :show="newObjectFormVisible" :asPrefix="newObjectAsPrefix" @update:show="(val: any) => (newObjectFormVisible = val && refresh())" :bucketName="bucketName"
|
||||
<n-data-table
|
||||
class="border dark:border-neutral-700 rounded overflow-hidden"
|
||||
:columns="columns"
|
||||
:data="filteredObjects"
|
||||
:row-key="rowKey"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
:pagination="false"
|
||||
:bordered="false" />
|
||||
<object-upload-picker
|
||||
:show="uploadPickerVisible"
|
||||
@update:show="(val: any) => (uploadPickerVisible = val && refresh())"
|
||||
:bucketName="bucketName"
|
||||
:prefix="prefix" />
|
||||
<object-new-form
|
||||
:show="newObjectFormVisible"
|
||||
:asPrefix="newObjectAsPrefix"
|
||||
@update:show="(val: any) => (newObjectFormVisible = val && refresh())"
|
||||
:bucketName="bucketName"
|
||||
:prefix="prefix" />
|
||||
<n-button-group class="ml-auto">
|
||||
<n-button @click="goToPreviousPage" :disabled="!continuationToken">
|
||||
@@ -58,15 +72,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
const { $s3Client } = useNuxtApp();
|
||||
import { useAsyncData, useRoute, useRouter } from "#app"
|
||||
import { NuxtLink } from "#components"
|
||||
import { ListObjectsV2Command, type _Object, type CommonPrefix } from "@aws-sdk/client-s3"
|
||||
import dayjs from "dayjs"
|
||||
import type { DataTableColumns, DataTableRowKey } from 'naive-ui'
|
||||
import { joinRelativeURL } from "ufo"
|
||||
import { computed, ref, watch, type VNode } from "vue"
|
||||
import { useDeleteTaskManagerStore } from "~/store/delete-tasks"
|
||||
import { useUploadTaskManagerStore } from "~/store/upload-tasks"
|
||||
import { useAsyncData, useRoute, useRouter } from "#app";
|
||||
import { NuxtLink } from "#components";
|
||||
import { ListObjectsV2Command, type _Object, type CommonPrefix } from "@aws-sdk/client-s3";
|
||||
import dayjs from "dayjs";
|
||||
import type { DataTableColumns, DataTableRowKey } from "naive-ui";
|
||||
import { joinRelativeURL } from "ufo";
|
||||
import { computed, ref, watch, type VNode } from "vue";
|
||||
import { useDeleteTaskManagerStore } from "~/store/delete-tasks";
|
||||
import { useUploadTaskManagerStore } from "~/store/upload-tasks";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -77,7 +91,7 @@ const props = defineProps<{ bucket: string; path: string }>();
|
||||
const uploadPickerVisible = ref(false);
|
||||
const newObjectFormVisible = ref(false);
|
||||
const newObjectAsPrefix = ref(false);
|
||||
const searchTerm = ref('');
|
||||
const searchTerm = ref("");
|
||||
|
||||
// Add debounce function for search
|
||||
const debounce = (fn: Function, delay: number) => {
|
||||
@@ -107,8 +121,16 @@ const deleteTaskStore = useDeleteTaskManagerStore();
|
||||
const deleteTasks = computed(() => deleteTaskStore.tasks);
|
||||
|
||||
// 当任务变化时,刷新数据
|
||||
watch(() => uploadTasks, () => setTimeout(refresh, 500), { deep: true });
|
||||
watch(() => deleteTasks, () => setTimeout(refresh, 500), { deep: true });
|
||||
watch(
|
||||
() => uploadTasks,
|
||||
() => setTimeout(refresh, 500),
|
||||
{ deep: true }
|
||||
);
|
||||
watch(
|
||||
() => deleteTasks,
|
||||
() => setTimeout(refresh, 500),
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// bucketName
|
||||
const bucketName = computed(() => props.bucket as string);
|
||||
@@ -139,7 +161,6 @@ interface RowData {
|
||||
type: "prefix" | "object";
|
||||
Size: number;
|
||||
LastModified: string;
|
||||
|
||||
}
|
||||
|
||||
const infoRef = ref();
|
||||
@@ -159,13 +180,19 @@ const columns: DataTableColumns<RowData> = [
|
||||
}
|
||||
|
||||
const keyInUri = row.Key;
|
||||
return h(NuxtLink, {
|
||||
href: row.type === "prefix" ? bucketPath(keyInUri) : '', class: "block text-cyan-400 cursor-pointer", onClick: (e: MouseEvent) => {
|
||||
if (row.type === "prefix") return
|
||||
infoRef.value.openDrawer(bucketName.value, row.Key)
|
||||
return
|
||||
}
|
||||
}, () => label);
|
||||
return h(
|
||||
NuxtLink,
|
||||
{
|
||||
href: row.type === "prefix" ? bucketPath(keyInUri) : "",
|
||||
class: "block text-cyan-400 cursor-pointer",
|
||||
onClick: (e: MouseEvent) => {
|
||||
if (row.type === "prefix") return;
|
||||
infoRef.value.openDrawer(bucketName.value, row.Key);
|
||||
return;
|
||||
},
|
||||
},
|
||||
() => label
|
||||
);
|
||||
},
|
||||
},
|
||||
{ key: "Size", title: "大小", render: (row: { Size: number }) => (row.Size ? formatBytes(row.Size) : "") },
|
||||
@@ -173,7 +200,7 @@ const columns: DataTableColumns<RowData> = [
|
||||
key: "LastModified",
|
||||
title: "更新时间",
|
||||
render: (row: { LastModified: string }) => {
|
||||
return row.LastModified ? dayjs(row.LastModified).format('YYYY-MM-DD HH:mm:ss') : "";
|
||||
return row.LastModified ? dayjs(row.LastModified).format("YYYY-MM-DD HH:mm:ss") : "";
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -185,14 +212,14 @@ interface ListObjectsResponse {
|
||||
isTruncated: boolean;
|
||||
}
|
||||
const randomString = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < 8; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const salt = ref(randomString())
|
||||
const salt = ref(randomString());
|
||||
// 在服务端获取数据
|
||||
const { data, refresh } = await useAsyncData<ListObjectsResponse>(
|
||||
`objectsData-${salt.value}&${prefix.value}&${pageSize.value}&${continuationToken.value}`,
|
||||
@@ -256,7 +283,7 @@ const filteredObjects = computed(() => {
|
||||
}
|
||||
|
||||
const term = searchTerm.value.toLowerCase();
|
||||
return objects.value.filter(obj => {
|
||||
return objects.value.filter((obj) => {
|
||||
const displayKey = prefix.value ? obj.Key?.substring(prefix.value.length) : obj.Key;
|
||||
return displayKey?.toLowerCase().includes(term);
|
||||
});
|
||||
@@ -287,20 +314,22 @@ function handleBatchDelete() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Promise.all(checkedKeys.value.map(async (item) => {
|
||||
const findOne = objects.value.find((obj) => obj.Key === item);
|
||||
// 目录删除
|
||||
// 递归查询目录下的所有文件,然后删除
|
||||
if (findOne?.type === "prefix" && findOne?.Key) {
|
||||
return await objectApi.mapAllFiles(bucketName.value, findOne.Key, (fileKey: string) => {
|
||||
deleteTaskStore.addKeys([fileKey], bucketName.value);
|
||||
});
|
||||
}
|
||||
await Promise.all(
|
||||
checkedKeys.value.map(async (item) => {
|
||||
const findOne = objects.value.find((obj) => obj.Key === item);
|
||||
// 目录删除
|
||||
// 递归查询目录下的所有文件,然后删除
|
||||
if (findOne?.type === "prefix" && findOne?.Key) {
|
||||
return await objectApi.mapAllFiles(bucketName.value, findOne.Key, (fileKey: string) => {
|
||||
deleteTaskStore.addKeys([fileKey], bucketName.value);
|
||||
});
|
||||
}
|
||||
|
||||
return deleteTaskStore.addKeys([String(item)], bucketName.value);
|
||||
}));
|
||||
return deleteTaskStore.addKeys([String(item)], bucketName.value);
|
||||
})
|
||||
);
|
||||
message.success("删除中");
|
||||
salt.value = randomString()
|
||||
salt.value = randomString();
|
||||
refresh();
|
||||
} catch (error) {
|
||||
message.error("删除失败");
|
||||
|
||||
Reference in New Issue
Block a user