mirror of
https://github.com/rustfs/console.git
synced 2026-08-28 19:47:21 +08:00
refactor: migrate lifecycle management to shadcn components
This commit is contained in:
+407
-389
@@ -1,425 +1,443 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="visible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
:title="t('Add Lifecycle Rule') + ` (${t('Bucket')}: ${bucketName})`"
|
||||
class="max-w-screen-md"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}"
|
||||
<AppModal
|
||||
v-model="visible"
|
||||
:title="`${t('Add Lifecycle Rule')} (${t('Bucket')}: ${bucketName})`"
|
||||
size="lg"
|
||||
:close-on-backdrop="false"
|
||||
>
|
||||
<n-card>
|
||||
<n-form class="my-4" ref="formRef" :model="formData" :rules="rules">
|
||||
<n-tabs default-value="expire" justify-content="space-evenly" type="line" @update:value="handleUpdateValue">
|
||||
<n-tab-pane name="expire" :tab="t('Expiration')">
|
||||
<n-form-item :label="t('Object Version')" path="versionType" v-if="versioningStatus">
|
||||
<n-select v-model:value="formData.versionType" :options="versionOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Time Cycle')" path="days">
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<n-input-number
|
||||
class="flex-auto"
|
||||
v-model:value="formData.days"
|
||||
:min="1"
|
||||
<div class="space-y-6">
|
||||
<Tabs v-model="activeTab" class="flex flex-col gap-4">
|
||||
<TabsList class="w-full justify-start overflow-x-auto">
|
||||
<TabsTrigger value="expire">{{ t('Expiration') }}</TabsTrigger>
|
||||
<TabsTrigger value="transition">{{ t('Transition') }}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="expire" class="mt-0 space-y-6">
|
||||
<AppCard padded class="space-y-4">
|
||||
<div v-if="versioningStatus" class="grid gap-2">
|
||||
<Label>{{ t('Object Version') }}</Label>
|
||||
<AppSelect v-model="formData.versionType" :options="versionOptions" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Time Cycle') }}</Label>
|
||||
<div class="flex items-center gap-3">
|
||||
<AppInput
|
||||
v-model="formData.days"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-32"
|
||||
:placeholder="t('Days')"
|
||||
style="width: 100px"
|
||||
/>
|
||||
<span class="ms-4">{{ t('Days After') }}</span>
|
||||
<span class="text-sm text-muted-foreground">{{ t('Days After') }}</span>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<!-- 生命周期 -->
|
||||
<n-card>
|
||||
<n-collapse>
|
||||
<n-collapse-item :title="t('More Configurations')" name="advanced">
|
||||
<n-form-item :label="t('Prefix')">
|
||||
<n-input v-model:value="formData.prefix" :placeholder="t('Please enter prefix')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Tags')">
|
||||
<n-dynamic-input
|
||||
v-model:value="formData.tags"
|
||||
preset="pair"
|
||||
:key-placeholder="t('Tag Name')"
|
||||
:value-placeholder="t('Tag Value')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-card>
|
||||
<!-- 高级设置 -->
|
||||
<n-card class="my-4" v-if="formData.versionType == 'current'">
|
||||
<n-collapse>
|
||||
<n-collapse-item :title="t('Advanced Settings')" name="advanced">
|
||||
<n-form-item :label="t('Delete Marker Handling')">
|
||||
<n-space>
|
||||
<n-switch v-model:value="formData.expiredDeleteMark" :round="false" />
|
||||
<span class="ml-4 text-gray-500">{{
|
||||
t('If no versions remain, delete references to this object')
|
||||
}}</span>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<!-- <n-form-item label="版本清理">
|
||||
<n-space>
|
||||
<n-switch v-model:value="formData.deleteAllExpired" :round="false" />
|
||||
<span class="ml-4 text-gray-500">删除所有过期版本</span>
|
||||
</n-space>
|
||||
</n-form-item> -->
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-card>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="transition" :tab="t('Transition')">
|
||||
<n-form-item :label="t('Object Version')" path="versionType" v-if="versioningStatus">
|
||||
<n-select v-model:value="formData.versionType" :options="versionOptions" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Time Cycle')" path="days">
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<n-input-number
|
||||
class="flex-auto"
|
||||
v-model:value="formData.days"
|
||||
:min="1"
|
||||
<AppCard padded class="space-y-4">
|
||||
<details>
|
||||
<summary class="cursor-pointer text-sm font-medium text-primary">{{ t('More Configurations') }}</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Prefix') }}</Label>
|
||||
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label class="text-sm font-medium">{{ t('Tags') }}</Label>
|
||||
<AppButton variant="outline" size="sm" @click="addTag">
|
||||
<Icon name="ri:add-line" class="size-4" />
|
||||
{{ t('Add Tag') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
<div v-if="formData.tags.length" class="space-y-3">
|
||||
<div
|
||||
v-for="(tag, index) in formData.tags"
|
||||
:key="index"
|
||||
class="grid gap-2 rounded-md border border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
|
||||
>
|
||||
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
|
||||
<div class="flex items-center gap-2">
|
||||
<AppInput v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-destructive"
|
||||
:disabled="formData.tags.length === 1"
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
<Icon name="ri:delete-bin-line" class="size-4" />
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</AppCard>
|
||||
|
||||
<AppCard v-if="formData.versionType === 'current'" padded>
|
||||
<details>
|
||||
<summary class="cursor-pointer text-sm font-medium text-primary">{{ t('Advanced Settings') }}</summary>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium">{{ t('Delete Marker Handling') }}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('If no versions remain, delete references to this object') }}
|
||||
</p>
|
||||
</div>
|
||||
<AppSwitch v-model="formData.expiredDeleteMark" />
|
||||
</div>
|
||||
</details>
|
||||
</AppCard>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="transition" class="mt-0 space-y-6">
|
||||
<AppCard padded class="space-y-4">
|
||||
<div v-if="versioningStatus" class="grid gap-2">
|
||||
<Label>{{ t('Object Version') }}</Label>
|
||||
<AppSelect v-model="formData.versionType" :options="versionOptions" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Time Cycle') }}</Label>
|
||||
<div class="flex items-center gap-3">
|
||||
<AppInput
|
||||
v-model="formData.days"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-32"
|
||||
:placeholder="t('Days')"
|
||||
style="width: 100px"
|
||||
/>
|
||||
<span class="ms-4">{{ t('Days After') }}</span>
|
||||
<span class="text-sm text-muted-foreground">{{ t('Days After') }}</span>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Storage Type')" path="storageType">
|
||||
<n-select v-model:value="formData.storageType" :options="tiers" />
|
||||
</n-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 生命周期 -->
|
||||
<n-card>
|
||||
<n-collapse>
|
||||
<n-collapse-item :title="t('More Configurations')" name="advanced">
|
||||
<n-form-item :label="t('Prefix')">
|
||||
<n-input v-model:value="formData.prefix" :placeholder="t('Please enter prefix')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Tags')">
|
||||
<n-dynamic-input
|
||||
v-model:value="formData.tags"
|
||||
preset="pair"
|
||||
:key-placeholder="t('Tag Name')"
|
||||
:value-placeholder="t('Tag Value')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-card>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-form>
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Storage Type') }}</Label>
|
||||
<AppSelect
|
||||
v-model="formData.storageType"
|
||||
:options="tiers"
|
||||
:placeholder="t('Please select storage type')"
|
||||
/>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<n-space justify="center">
|
||||
<n-button @click="handleCancel">{{ t('Cancel') }}</n-button>
|
||||
<n-button type="primary" @click="handleSave">{{ t('Save') }}</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<AppCard padded class="space-y-4">
|
||||
<details>
|
||||
<summary class="cursor-pointer text-sm font-medium text-primary">{{ t('More Configurations') }}</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Prefix') }}</Label>
|
||||
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label class="text-sm font-medium">{{ t('Tags') }}</Label>
|
||||
<AppButton variant="outline" size="sm" @click="addTag">
|
||||
<Icon name="ri:add-line" class="size-4" />
|
||||
{{ t('Add Tag') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
<div v-if="formData.tags.length" class="space-y-3">
|
||||
<div
|
||||
v-for="(tag, index) in formData.tags"
|
||||
:key="index"
|
||||
class="grid gap-2 rounded-md border border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
|
||||
>
|
||||
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
|
||||
<div class="flex items-center gap-2">
|
||||
<AppInput v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-destructive"
|
||||
:disabled="formData.tags.length === 1"
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
<Icon name="ri:delete-bin-line" class="size-4" />
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</AppCard>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="handleCancel">{{ t('Cancel') }}</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="handleSave">{{ t('Save') }}</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NButton,
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
NDynamicInput,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NSelect,
|
||||
} from 'naive-ui';
|
||||
// 使用 Web Crypto API 或 Node.js crypto 模块
|
||||
const randomUUID = () => {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
// 兜底方案
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { putBucketLifecycleConfiguration, getBucketVersioning, getBucketLifecycleConfiguration } = useBucket({});
|
||||
const { listTiers } = useTiers();
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
// 定义 tiers 数据结构的接口
|
||||
interface TierItem {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppInput, AppModal, AppSelect, AppSwitch } from '@/components/app'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const emit = defineEmits(['search']);
|
||||
const formRef = ref();
|
||||
const formData = ref({
|
||||
ruleName: '',
|
||||
type: null,
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const { putBucketLifecycleConfiguration, getBucketVersioning, getBucketLifecycleConfiguration } = useBucket({})
|
||||
const { listTiers } = useTiers()
|
||||
|
||||
const props = defineProps<{
|
||||
bucketName: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search'): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const activeTab = ref<'expire' | 'transition'>('expire')
|
||||
const submitting = ref(false)
|
||||
const versioningStatus = ref(false)
|
||||
|
||||
const versionOptions = computed(() => [
|
||||
{ label: t('Current Version'), value: 'current' },
|
||||
{ label: t('Non-current Version'), value: 'non-current' },
|
||||
])
|
||||
|
||||
const formData = reactive({
|
||||
versionType: 'current',
|
||||
days: null,
|
||||
action: 'expire',
|
||||
days: 1,
|
||||
storageType: '',
|
||||
prefix: '',
|
||||
expiredDeleteMark: false,
|
||||
deleteAllExpired: false,
|
||||
storageType: '',
|
||||
tags: [
|
||||
{
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
] as Array<{ key: string; value: string }>,
|
||||
})
|
||||
|
||||
const versionOptions = ref([
|
||||
{ label: t('Current Version'), value: 'current' },
|
||||
{ label: t('Non-current Version'), value: 'non-current' },
|
||||
]);
|
||||
watch(
|
||||
() => activeTab.value,
|
||||
value => {
|
||||
formState.action = value === 'expire' ? 'expire' : 'transition'
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
days: {
|
||||
required: true,
|
||||
validator: (rule: any, value: any) => {
|
||||
if (value === null || value === undefined || value < 1) {
|
||||
return new Error(t('Please enter valid days'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
trigger: ['blur', 'input', 'change'],
|
||||
},
|
||||
storageType: {
|
||||
validator: (rule: any, value: any) => {
|
||||
// 只在 transition 模式下验证 storageType
|
||||
if (formData.value.action === 'transition' && (!value || value === '')) {
|
||||
return new Error(t('Please select storage type'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
};
|
||||
const formState = reactive({
|
||||
action: 'expire' as 'expire' | 'transition',
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
bucketName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// 已经存在的生命周期
|
||||
lifecycleList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
const tiers = ref<Array<{ label: string; value: string }>>([])
|
||||
|
||||
const loadTiers = async () => {
|
||||
const res = await listTiers()
|
||||
if (res) {
|
||||
tiers.value = res.map((item: any) => ({
|
||||
label: item[item.type].name,
|
||||
value: item[item.type].name,
|
||||
}))
|
||||
}
|
||||
if (!formData.storageType && tiers.value.length > 0) {
|
||||
formData.storageType = tiers.value[0].value
|
||||
}
|
||||
}
|
||||
|
||||
const loadVersioningStatus = async () => {
|
||||
if (!props.bucketName) return
|
||||
try {
|
||||
const resp = await getBucketVersioning(props.bucketName)
|
||||
versioningStatus.value = resp.Status === 'Enabled'
|
||||
} catch (error) {
|
||||
versioningStatus.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.bucketName,
|
||||
newVal => {
|
||||
// 获取是都开启版本管理
|
||||
getVersioningStatus();
|
||||
() => {
|
||||
loadVersioningStatus()
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const visible = ref(false);
|
||||
const open = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
const addTag = () => {
|
||||
formData.tags.push({ key: '', value: '' })
|
||||
}
|
||||
|
||||
const removeTag = (index: number) => {
|
||||
if (formData.tags.length === 1) return
|
||||
formData.tags.splice(index, 1)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
activeTab.value = 'expire'
|
||||
formState.action = 'expire'
|
||||
formData.versionType = 'current'
|
||||
formData.days = 1
|
||||
formData.storageType = tiers.value[0]?.value ?? ''
|
||||
formData.prefix = ''
|
||||
formData.expiredDeleteMark = false
|
||||
formData.tags = [{ key: '', value: '' }]
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
if (!formData.days || Number(formData.days) < 1) {
|
||||
message.error(t('Please enter valid days'))
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
formState.action === 'transition' &&
|
||||
(!formData.storageType || formData.storageType === '')
|
||||
) {
|
||||
message.error(t('Please select storage type'))
|
||||
return false
|
||||
}
|
||||
|
||||
if (!formData.tags.some(tag => tag.key && tag.value)) {
|
||||
// allow but no error
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const randomUUID = () => {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = (Math.random() * 16) | 0
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
const buildFilter = () => {
|
||||
const validTags = formData.tags.filter(tag => tag.key && tag.value)
|
||||
if (!formData.prefix && validTags.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const filter: any = {}
|
||||
|
||||
if (validTags.length === 1 && !formData.prefix) {
|
||||
filter.Tag = {
|
||||
Key: validTags[0].key,
|
||||
Value: validTags[0].value,
|
||||
}
|
||||
} else if (validTags.length > 0) {
|
||||
filter.And = {
|
||||
Tags: validTags.map(tag => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value,
|
||||
})),
|
||||
}
|
||||
if (formData.prefix) {
|
||||
filter.And.Prefix = formData.prefix
|
||||
}
|
||||
} else if (formData.prefix) {
|
||||
filter.Prefix = formData.prefix
|
||||
}
|
||||
|
||||
if (formData.prefix && validTags.length === 1 && !filter.And) {
|
||||
filter.Prefix = formData.prefix
|
||||
filter.Tag = {
|
||||
Key: validTags[0].key,
|
||||
Value: validTags[0].value,
|
||||
}
|
||||
} else if (formData.prefix && validTags.length === 0) {
|
||||
filter.Prefix = formData.prefix
|
||||
}
|
||||
|
||||
return Object.keys(filter).length ? filter : undefined
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validate()) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const currentConfig = await getBucketLifecycleConfiguration(props.bucketName)
|
||||
const newRule: any = {
|
||||
ID: randomUUID(),
|
||||
Status: 'Enabled',
|
||||
Filter: buildFilter(),
|
||||
}
|
||||
|
||||
const daysValue = Number(formData.days)
|
||||
|
||||
if (formState.action === 'expire') {
|
||||
if (formData.versionType === 'non-current') {
|
||||
newRule.NoncurrentVersionExpiration = {
|
||||
NoncurrentDays: daysValue,
|
||||
}
|
||||
if (formData.expiredDeleteMark) {
|
||||
newRule.ExpiredObjectDeleteMarker = true
|
||||
}
|
||||
} else {
|
||||
newRule.Expiration = {
|
||||
Days: daysValue,
|
||||
}
|
||||
if (formData.expiredDeleteMark) {
|
||||
newRule.Expiration.ExpiredObjectDeleteMarker = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (formData.versionType === 'non-current') {
|
||||
newRule.NoncurrentVersionTransitions = [
|
||||
{
|
||||
NoncurrentDays: daysValue,
|
||||
StorageClass: formData.storageType,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
newRule.Transitions = [
|
||||
{
|
||||
Days: daysValue,
|
||||
StorageClass: formData.storageType,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const existingRules = currentConfig.Rules || []
|
||||
const payload = {
|
||||
Rules: [...existingRules, newRule],
|
||||
}
|
||||
|
||||
await putBucketLifecycleConfiguration(props.bucketName, payload)
|
||||
message.success(t('Create Success'))
|
||||
emit('search')
|
||||
handleCancel()
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || t('Failed to create rule'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
|
||||
const handleUpdateValue = (value: string) => {
|
||||
if (value === 'expire') {
|
||||
formData.value.action = 'expire';
|
||||
} else {
|
||||
formData.value.action = 'transition';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
formRef.value?.validate((errors: any) => {
|
||||
if (!errors) {
|
||||
// 额外的参数验证(与表单验证保持一致)
|
||||
if (formData.value.days === null || formData.value.days === undefined || formData.value.days < 1) {
|
||||
message.error(t('Please enter valid days'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
formData.value.action === 'transition' &&
|
||||
(!formData.value.storageType || formData.value.storageType === '')
|
||||
) {
|
||||
message.error(t('Please select storage type'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 先获取当前的生命周期配置
|
||||
getBucketLifecycleConfiguration(props.bucketName)
|
||||
.then(currentConfig => {
|
||||
// 创建新的规则
|
||||
const newRule: any = {
|
||||
ID: randomUUID(),
|
||||
Status: 'Enabled',
|
||||
};
|
||||
|
||||
// 设置 Filter 或 Prefix(不能同时使用)
|
||||
console.log('formData.value.prefix:', formData.value.prefix);
|
||||
console.log('formData.value.tags:', formData.value.tags);
|
||||
console.log('formData.value.expiredDeleteMark:', formData.value.expiredDeleteMark);
|
||||
|
||||
// 检查是否有有效的标签
|
||||
const validTags = formData.value.tags.filter((item: any) => item.key && item.value);
|
||||
const hasValidTags = validTags.length > 0;
|
||||
|
||||
if (formData.value.prefix || hasValidTags) {
|
||||
newRule.Filter = {};
|
||||
|
||||
if (hasValidTags) {
|
||||
if (validTags.length === 1) {
|
||||
newRule.Filter.Tag = {
|
||||
Key: validTags[0].key,
|
||||
Value: validTags[0].value,
|
||||
};
|
||||
if (formData.value.prefix) {
|
||||
newRule.Filter.Prefix = formData.value.prefix;
|
||||
}
|
||||
} else {
|
||||
newRule.Filter.And = {
|
||||
Tags: validTags.map((item: any) => ({
|
||||
Key: item.key,
|
||||
Value: item.value,
|
||||
})),
|
||||
};
|
||||
if (formData.value.prefix) {
|
||||
newRule.Filter.And.Prefix = formData.value.prefix;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 只有前缀时,直接添加到 Filter
|
||||
newRule.Filter.Prefix = formData.value.prefix;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据操作类型添加相应的配置
|
||||
if (formData.value.action === 'expire') {
|
||||
if (formData.value.versionType === 'non-current') {
|
||||
newRule.NoncurrentVersionExpiration = {
|
||||
NoncurrentDays: formData.value.days,
|
||||
};
|
||||
|
||||
// 如果启用了删除标记处理
|
||||
if (formData.value.expiredDeleteMark) {
|
||||
newRule.ExpiredObjectDeleteMarker = true;
|
||||
}
|
||||
} else {
|
||||
newRule.Expiration = {
|
||||
Days: formData.value.days,
|
||||
};
|
||||
|
||||
// 如果启用了删除标记处理(当前版本也支持)
|
||||
if (formData.value.expiredDeleteMark) {
|
||||
newRule.Expiration.ExpiredObjectDeleteMarker = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// transition 操作
|
||||
if (formData.value.versionType === 'non-current') {
|
||||
newRule.NoncurrentVersionTransitions = [
|
||||
{
|
||||
NoncurrentDays: formData.value.days,
|
||||
StorageClass: formData.value.storageType,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
newRule.Transitions = [
|
||||
{
|
||||
Days: formData.value.days,
|
||||
StorageClass: formData.value.storageType,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 合并现有规则和新规则
|
||||
const existingRules = currentConfig.Rules || [];
|
||||
const updatedRules = [...existingRules, newRule];
|
||||
|
||||
// 调用保存接口
|
||||
const params = {
|
||||
Rules: updatedRules,
|
||||
};
|
||||
|
||||
return putBucketLifecycleConfiguration(props.bucketName, params);
|
||||
})
|
||||
.then(res => {
|
||||
visible.value = false;
|
||||
emit('search');
|
||||
message.success(t('Create Success'));
|
||||
formData.value = {
|
||||
ruleName: '',
|
||||
type: null,
|
||||
versionType: 'current',
|
||||
days: null,
|
||||
action: 'expire',
|
||||
expiredDeleteMark: false,
|
||||
deleteAllExpired: false,
|
||||
storageType: '',
|
||||
prefix: '',
|
||||
tags: [
|
||||
{
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 分层列表
|
||||
const tiers = ref<TierItem[]>([]);
|
||||
const getTiers = async () => {
|
||||
const res = await listTiers();
|
||||
if (res) {
|
||||
tiers.value = res.map((item: any) => {
|
||||
return {
|
||||
label: item[item.type].name,
|
||||
value: item[item.type].name,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (tiers.value.length) {
|
||||
formData.value.storageType = tiers.value[0].value;
|
||||
}
|
||||
};
|
||||
getTiers();
|
||||
|
||||
// 获取版本控制状态
|
||||
const versioningStatus: any = ref(false);
|
||||
const getVersioningStatus = async () => {
|
||||
try {
|
||||
const resp = await getBucketVersioning(props.bucketName);
|
||||
versioningStatus.value = resp.Status == 'Enabled';
|
||||
} catch (error) {
|
||||
console.error('获取版本控制状态失败:', error);
|
||||
}
|
||||
};
|
||||
open: async () => {
|
||||
await Promise.all([loadTiers(), loadVersioningStatus()])
|
||||
resetForm()
|
||||
visible.value = true
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getVersioningStatus();
|
||||
});
|
||||
const handleCancel = () => {
|
||||
// 取消逻辑
|
||||
visible.value = false;
|
||||
};
|
||||
loadTiers()
|
||||
loadVersioningStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
+175
-188
@@ -1,248 +1,235 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<page-header>
|
||||
<template #title>
|
||||
<h1 class="text-2xl font-bold">{{ t('Lifecycle') }}</h1>
|
||||
</template>
|
||||
</page-header>
|
||||
<page-content class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div style="width: 300px">
|
||||
<n-form-item :label="t('Bucket')" path="" class="flex-auto" label-placement="left">
|
||||
<n-select
|
||||
filterable
|
||||
v-model:value="bucketName"
|
||||
:placeholder="t('Please select bucket')"
|
||||
:options="bucketList"
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<n-button @click="() => handleNew()">
|
||||
<Icon name="ri:add-line" class="mr-2" />
|
||||
<page-content class="flex flex-col gap-4">
|
||||
<div
|
||||
class="flex flex-col gap-4 rounded-lg border border-border/60 bg-background/80 p-4 shadow-sm md:flex-row md:items-end md:justify-between"
|
||||
>
|
||||
<div class="w-full max-w-sm">
|
||||
<Label class="mb-2 block text-sm font-medium text-muted-foreground">{{ t('Bucket') }}</Label>
|
||||
<AppSelect
|
||||
v-model="bucketName"
|
||||
:options="bucketList"
|
||||
:placeholder="t('Please select bucket')"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<AppButton variant="secondary" @click="handleNew">
|
||||
<Icon name="ri:add-line" class="size-4" />
|
||||
<span>{{ t('Add Lifecycle Rule') }}</span>
|
||||
</n-button>
|
||||
<n-button @click="refresh">
|
||||
<Icon name="ri:refresh-line" class="mr-2" />
|
||||
</AppButton>
|
||||
<AppButton variant="outline" @click="refresh">
|
||||
<Icon name="ri:refresh-line" class="size-4" />
|
||||
<span>{{ t('Refresh') }}</span>
|
||||
</n-button>
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-data-table
|
||||
class="border dark:border-neutral-700 rounded overflow-hidden"
|
||||
v-if="pageData.length > 0"
|
||||
:columns="columns"
|
||||
:data="pageData"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
/>
|
||||
<AppCard padded class="border border-border/60">
|
||||
<AppDataTable
|
||||
:table="table"
|
||||
:is-loading="loading"
|
||||
:empty-title="t('No Data')"
|
||||
:empty-description="t('Create lifecycle rules to automate object transitions and expiration.')"
|
||||
/>
|
||||
</AppCard>
|
||||
|
||||
<n-card class="flex flex-center" style="height: 400px" v-else>
|
||||
<n-empty :description="t('No Data')"></n-empty>
|
||||
</n-card>
|
||||
<!-- <n-data-table class="border dark:border-neutral-700 rounded overflow-hidden" :columns="columns" :data="pageData" :pagination="false" :bordered="false" /> -->
|
||||
<lifecycle-new-form ref="newRef" :bucketName="bucketName" @search="refresh"></lifecycle-new-form>
|
||||
<lifecycle-new-form ref="newRef" :bucketName="bucketName" @search="refresh" />
|
||||
</page-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '#components';
|
||||
import { NButton, NSpace, NPopconfirm, type DataTableColumns } from 'naive-ui';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Icon } from '#components'
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import { AppButton, AppCard, AppSelect, AppTag } from '@/components/app'
|
||||
import { AppDataTable, useDataTable } from '@/components/app/data-table'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { h, computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const { listBuckets, getBucketLifecycleConfiguration, deleteBucketLifecycle, putBucketLifecycleConfiguration } =
|
||||
useBucket({});
|
||||
const formVisible = ref(false);
|
||||
useBucket({})
|
||||
|
||||
interface RowData {
|
||||
Status?: string;
|
||||
ID?: string;
|
||||
NoncurrentVersionExpiration?: {
|
||||
NoncurrentDays?: number;
|
||||
};
|
||||
Expiration?: {
|
||||
StorageClass?: string;
|
||||
Days?: number;
|
||||
Date?: string;
|
||||
ExpiredObjectDeleteMarker?: boolean;
|
||||
};
|
||||
interface LifecycleRule {
|
||||
ID?: string
|
||||
Status?: string
|
||||
Filter?: {
|
||||
Prefix?: string;
|
||||
};
|
||||
Transitions?: {
|
||||
Days?: number;
|
||||
StorageClass?: string;
|
||||
}[];
|
||||
Prefix?: string
|
||||
Tag?: { Key: string; Value: string }
|
||||
And?: {
|
||||
Prefix?: string
|
||||
Tags?: Array<{ Key: string; Value: string }>
|
||||
}
|
||||
}
|
||||
Expiration?: {
|
||||
Days?: number
|
||||
Date?: string
|
||||
StorageClass?: string
|
||||
ExpiredObjectDeleteMarker?: boolean
|
||||
}
|
||||
NoncurrentVersionExpiration?: {
|
||||
NoncurrentDays?: number
|
||||
}
|
||||
Transitions?: Array<{ Days?: number; StorageClass?: string }>
|
||||
NoncurrentVersionTransitions?: Array<{ NoncurrentDays?: number; StorageClass?: string }>
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<RowData> = [
|
||||
const columns: ColumnDef<LifecycleRule>[] = [
|
||||
{
|
||||
title: t('Type'),
|
||||
key: '',
|
||||
render: row => {
|
||||
return h(NSpace, {}, () => {
|
||||
console.log(row);
|
||||
if (row?.Transitions) {
|
||||
return t('Transition');
|
||||
} else {
|
||||
return t('Expire');
|
||||
}
|
||||
});
|
||||
},
|
||||
header: () => t('Type'),
|
||||
accessorFn: row => (row.Transitions || row.NoncurrentVersionTransitions ? 'Transition' : 'Expire'),
|
||||
},
|
||||
{
|
||||
title: t('Version'),
|
||||
key: 'Transition',
|
||||
render: row => {
|
||||
return h(NSpace, {}, row?.NoncurrentVersionExpiration ? t('Non-current Version') : t('Current Version'));
|
||||
},
|
||||
header: () => t('Version'),
|
||||
accessorFn: row => (row.NoncurrentVersionExpiration || row.NoncurrentVersionTransitions ? t('Non-current Version') : t('Current Version')),
|
||||
},
|
||||
{
|
||||
title: t('Expiration Delete Mark'),
|
||||
key: '',
|
||||
render: row => {
|
||||
return h(NSpace, {}, row?.Expiration?.ExpiredObjectDeleteMarker ? t('On') : t('Off'));
|
||||
},
|
||||
header: () => t('Expiration Delete Mark'),
|
||||
accessorFn: row => (row.Expiration?.ExpiredObjectDeleteMarker ? t('On') : t('Off')),
|
||||
},
|
||||
{
|
||||
title: t('Tier'),
|
||||
key: '',
|
||||
render: row => {
|
||||
return h(NSpace, {}, row?.Transitions?.[0]?.StorageClass || '--');
|
||||
},
|
||||
header: () => t('Tier'),
|
||||
accessorFn: row => row.Transitions?.[0]?.StorageClass || row.NoncurrentVersionTransitions?.[0]?.StorageClass || '--',
|
||||
},
|
||||
{
|
||||
title: t('Prefix'),
|
||||
key: 'Filter',
|
||||
render: row => {
|
||||
return h(NSpace, {}, row?.Filter?.Prefix || '');
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: t('Time Cycle') + '(' + t('Days') + ')',
|
||||
key: 'NoncurrentVersionExpiration',
|
||||
render: row => {
|
||||
return h(
|
||||
NSpace,
|
||||
{},
|
||||
row?.Expiration?.Days || row?.NoncurrentVersionExpiration?.NoncurrentDays || row?.Transitions?.[0]?.Days || ''
|
||||
);
|
||||
},
|
||||
header: () => t('Prefix'),
|
||||
accessorFn: row => row.Filter?.Prefix || row.Filter?.And?.Prefix || '',
|
||||
},
|
||||
{
|
||||
title: t('Status'),
|
||||
key: 'Status',
|
||||
header: () => `${t('Time Cycle')} (${t('Days')})`,
|
||||
accessorFn: row =>
|
||||
row.Expiration?.Days ||
|
||||
row.NoncurrentVersionExpiration?.NoncurrentDays ||
|
||||
row.Transitions?.[0]?.Days ||
|
||||
row.NoncurrentVersionTransitions?.[0]?.NoncurrentDays ||
|
||||
'',
|
||||
},
|
||||
{
|
||||
title: t('Actions'),
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: (row: RowData) => {
|
||||
return h(
|
||||
NSpace,
|
||||
header: () => t('Status'),
|
||||
accessorFn: row => row.Status || '-',
|
||||
cell: ({ row }) =>
|
||||
h(
|
||||
AppTag,
|
||||
{
|
||||
justify: 'center',
|
||||
tone: row.original.Status === 'Enabled' ? 'success' : 'danger',
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => handleRowDelete(row) },
|
||||
{
|
||||
default: () => t('Confirm Delete'),
|
||||
trigger: () =>
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', secondary: true },
|
||||
{
|
||||
default: () => '',
|
||||
icon: () => h(Icon, { name: 'ri:delete-bin-5-line' }),
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
() => row.original.Status || '-'
|
||||
),
|
||||
},
|
||||
];
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => t('Actions'),
|
||||
enableSorting: false,
|
||||
cell: ({ row }) =>
|
||||
h('div', { class: 'flex justify-center gap-2' }, [
|
||||
h(
|
||||
AppButton,
|
||||
{
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
onClick: () => confirmDelete(row.original),
|
||||
},
|
||||
() => [h(Icon, { name: 'ri:delete-bin-5-line', class: 'size-4' }), h('span', t('Delete'))]
|
||||
),
|
||||
]),
|
||||
},
|
||||
]
|
||||
|
||||
// 获取桶列表
|
||||
const { data } = await useAsyncData(
|
||||
'buckets',
|
||||
const bucketOptions = await useAsyncData(
|
||||
'lifecycle-buckets',
|
||||
async () => {
|
||||
const response = await listBuckets();
|
||||
const response = await listBuckets()
|
||||
return (
|
||||
response.Buckets?.sort((a: any, b: any) => {
|
||||
return a.Name.localeCompare(b.Name);
|
||||
return a.Name.localeCompare(b.Name)
|
||||
}) || []
|
||||
);
|
||||
)
|
||||
},
|
||||
{ default: () => [] }
|
||||
);
|
||||
)
|
||||
|
||||
const bucketList = computed(() => {
|
||||
return data.value.map(bucket => ({
|
||||
const bucketList = computed(() =>
|
||||
bucketOptions.data.value.map(bucket => ({
|
||||
label: bucket.Name,
|
||||
value: bucket.Name,
|
||||
}));
|
||||
});
|
||||
}))
|
||||
)
|
||||
|
||||
const bucketName = ref<string>(bucketList.value.length > 0 ? bucketList.value[0]?.value ?? '' : '')
|
||||
const pageData = ref<LifecycleRule[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const { table } = useDataTable<LifecycleRule>({
|
||||
data: pageData,
|
||||
columns,
|
||||
getRowId: row => row.ID ?? JSON.stringify(row),
|
||||
})
|
||||
|
||||
const fetchLifecycle = async () => {
|
||||
if (!bucketName.value) {
|
||||
pageData.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getBucketLifecycleConfiguration(bucketName.value)
|
||||
pageData.value = response.Rules?.sort((a: any, b: any) => (a.ID || '').localeCompare(b.ID || '')) ?? []
|
||||
} catch (error) {
|
||||
pageData.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = () => fetchLifecycle()
|
||||
|
||||
const bucketName = ref<string>(bucketList.value.length > 0 ? (bucketList.value[0]?.value ?? '') : '');
|
||||
const pageData = ref<any[]>([]);
|
||||
const loading = ref<boolean>(false);
|
||||
watch(
|
||||
() => bucketName.value,
|
||||
async newVal => {
|
||||
if (!newVal) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
refresh();
|
||||
} catch (error) {
|
||||
pageData.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
() => {
|
||||
fetchLifecycle()
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
)
|
||||
|
||||
const handleRowDelete = (row: RowData) => {
|
||||
const params = pageData.value.filter(item => item.ID !== row.ID);
|
||||
console.log(params);
|
||||
if (params.length === 0) {
|
||||
deleteBucketLifecycle(bucketName.value).then(async res => {
|
||||
message.success(t('Delete Success'));
|
||||
refresh();
|
||||
});
|
||||
} else {
|
||||
putBucketLifecycleConfiguration(bucketName.value, { Rules: params })
|
||||
.then(async res => {
|
||||
message.success(t('Delete Success'));
|
||||
refresh();
|
||||
})
|
||||
.catch(e => {
|
||||
message.error(e.message);
|
||||
});
|
||||
const confirmDelete = (row: LifecycleRule) => {
|
||||
dialog.error({
|
||||
title: t('Warning'),
|
||||
content: t('Are you sure you want to delete this rule?'),
|
||||
positiveText: t('Confirm'),
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: () => handleRowDelete(row),
|
||||
})
|
||||
}
|
||||
|
||||
const handleRowDelete = async (row: LifecycleRule) => {
|
||||
const remaining = pageData.value.filter(item => item.ID !== row.ID)
|
||||
|
||||
try {
|
||||
if (remaining.length === 0) {
|
||||
await deleteBucketLifecycle(bucketName.value)
|
||||
} else {
|
||||
await putBucketLifecycleConfiguration(bucketName.value, { Rules: remaining })
|
||||
}
|
||||
message.success(t('Delete Success'))
|
||||
fetchLifecycle()
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || t('Delete Failed'))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
const response = await getBucketLifecycleConfiguration(bucketName.value);
|
||||
pageData.value = response.Rules?.sort((a: any, b: any) => a.ID.localeCompare(b.ID)) || [];
|
||||
};
|
||||
|
||||
const newRef = ref();
|
||||
const newRef = ref<{ open: () => void }>()
|
||||
const handleNew = () => {
|
||||
newRef.value.open();
|
||||
};
|
||||
newRef.value?.open()
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user