Files
rustfs-console/components/replication/new-form.vue
T
2025-10-30 21:22:58 +08:00

365 lines
12 KiB
Vue

<template>
<Modal v-model="visible" :title="t('Add Replication Rule') + ` (${t('Bucket')}: ${bucketName})`" size="xl" :close-on-backdrop="false">
<div class="space-y-6">
<div class="space-y-4">
<div class="grid gap-3 md:grid-cols-2">
<Field>
<FieldLabel>{{ t('Priority') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.level" type="number" min="1" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Mode') }}</FieldLabel>
<FieldContent>
<Selector v-model="formData.modeType" :options="modeOptions" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Endpoint') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.endpoint" :placeholder="t('Please enter endpoint')" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Bucket') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.bucket" :placeholder="t('Please enter bucket')" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Access Key') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Secret Key') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.secrretkey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Region') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.region" :placeholder="t('Please enter region')" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Storage Class') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.storageType" :placeholder="t('Please enter storage class')" />
</FieldContent>
</Field>
</div>
<Field>
<FieldLabel>{{ t('Prefix') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
</FieldContent>
</Field>
<div class="space-y-3">
<div class="flex items-center justify-between">
<FieldLabel class="text-sm font-medium">{{ t('Tags') }}</FieldLabel>
<Button variant="outline" size="sm" @click="addTag">
<Icon name="ri:add-line" class="size-4" />
{{ t('Add Tag') }}
</Button>
</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 p-3 md:grid-cols-2 md:items-center md:gap-4">
<Input v-model="tag.key" :placeholder="t('Tag Name')" />
<div class="flex items-center gap-2">
<Input v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
<Button variant="ghost" size="sm" class="text-destructive" :disabled="formData.tags.length === 1" @click="removeTag(index)">
<Icon name="ri:delete-bin-line" class="size-4" />
</Button>
</div>
</div>
</div>
</div>
</div>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium">{{ t('Use TLS') }}</p>
<p class="text-xs text-muted-foreground">{{ t('Enable secure transport when connecting to endpoint.') }}</p>
</div>
<Switch v-model:checked="formData.tls" />
</div>
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium">{{ t('Replicate Existing Objects') }}</p>
<p class="text-xs text-muted-foreground">{{ t('Include objects that already exist in the source bucket.') }}</p>
</div>
<Switch v-model:checked="formData.existingObject" />
</div>
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium">{{ t('Replicate Delete Markers') }}</p>
<p class="text-xs text-muted-foreground">{{ t('Sync delete markers to destination bucket.') }}</p>
</div>
<Switch v-model:checked="formData.expiredDeleteMark" />
</div>
<div class="space-y-3" v-if="formData.modeType === 'async'">
<Field>
<FieldLabel>{{ t('Health Check Interval (seconds)') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.timecheck" type="number" min="1" class="w-32" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Bandwidth Limit') }}</FieldLabel>
<FieldContent>
<div class="flex items-center gap-2">
<Input v-model="formData.bandwidth" type="number" min="0" class="w-32" />
<Selector v-model="formData.unit" :options="unitOptions" class="w-28" />
</div>
</FieldContent>
</Field>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<Button variant="outline" @click="handleCancel">{{ t('Cancel') }}</Button>
<Button variant="default" :loading="submitting" @click="handleSave">{{ t('Save') }}</Button>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Icon } from '#components'
import { Field, FieldContent, FieldLabel } from '@/components/ui/field'
import { Switch } from '@/components/ui/switch'
import { getBytes } from '@/utils/functions'
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Modal from '~/components/modal.vue'
import Selector from '~/components/selector.vue'
const { t } = useI18n()
const message = useMessage()
const { setRemoteReplicationTarget, putBucketReplication, getBucketReplication } = useBucket({})
const props = defineProps<{
bucketName: string
}>()
const emit = defineEmits<{
(e: 'success'): void
}>()
const visible = ref(false)
const submitting = ref(false)
const formData = reactive({
level: '1',
endpoint: '',
tls: false,
accesskey: '',
secrretkey: '',
bucket: '',
region: 'us-east-1',
modeType: 'async',
timecheck: '60',
unit: 'Gi',
bandwidth: 100,
storageType: 'STANDARD',
prefix: '',
tags: [{ key: '', value: '' }],
existingObject: true,
expiredDeleteMark: true,
})
const modeOptions = computed(() => [
{ label: t('Asynchronous'), value: 'async' },
{ label: t('Synchronous'), value: 'sync' },
])
const unitOptions = computed(() => [
{ label: 'KiB/s', value: 'Ki' },
{ label: 'MiB/s', value: 'Mi' },
{ label: 'GiB/s', value: 'Gi' },
])
const addTag = () => {
formData.tags.push({ key: '', value: '' })
}
const removeTag = (index: number) => {
if (formData.tags.length === 1) return
formData.tags.splice(index, 1)
}
const resetForm = () => {
formData.level = '1'
formData.endpoint = ''
formData.tls = false
formData.accesskey = ''
formData.secrretkey = ''
formData.bucket = ''
formData.region = 'us-east-1'
formData.modeType = 'async'
formData.timecheck = '60'
formData.unit = 'Gi'
formData.bandwidth = 100
formData.storageType = 'STANDARD'
formData.prefix = ''
formData.tags = [{ key: '', value: '' }]
formData.existingObject = true
formData.expiredDeleteMark = true
}
const validate = () => {
if (!formData.endpoint) {
message.error(t('Please enter endpoint'))
return false
}
if (!formData.bucket) {
message.error(t('Please enter bucket'))
return false
}
if (!formData.accesskey || !formData.secrretkey) {
message.error(t('Please provide credentials'))
return false
}
if (formData.modeType === 'async' && Number(formData.timecheck) < 1) {
message.error(t('Please enter valid health check interval'))
return false
}
return true
}
const handleSave = async () => {
if (!validate()) return
submitting.value = true
try {
const config: any = {
sourcebucket: props.bucketName,
endpoint: formData.endpoint,
credentials: {
accessKey: formData.accesskey,
secretKey: formData.secrretkey,
expiration: '0001-01-01T00:00:00Z',
},
targetbucket: formData.bucket,
secure: formData.tls,
region: formData.region,
path: 'auto',
api: 's3v4',
type: 'replication',
replicationSync: formData.modeType === 'sync',
healthCheckDuration: Number(formData.timecheck) || 60,
disableProxy: false,
resetBeforeDate: '0001-01-01T00:00:00Z',
totalDowntime: 0,
lastOnline: '0001-01-01T00:00:00Z',
isOnline: false,
latency: { curr: 0, avg: 0, max: 0 },
edge: false,
edgeSyncBeforeExpiry: false,
}
if (formData.modeType === 'async') {
config.bandwidth = Number(getBytes(String(formData.bandwidth), formData.unit, true)) || 0
}
const targetResponse = await setRemoteReplicationTarget(props.bucketName, config)
if (!targetResponse) {
throw new Error('Failed to create replication target')
}
let oldConfig: any = null
try {
oldConfig = await getBucketReplication(props.bucketName)
} catch (error) {
oldConfig = null
}
const newRule: any = {
ID: `replication-rule-${Date.now()}`,
Status: 'Enabled',
Priority: Number.parseInt(formData.level) || 1,
SourceSelectionCriteria: {
SseKmsEncryptedObjects: { Status: 'Enabled' },
},
ExistingObjectReplication: {
Status: formData.existingObject ? 'Enabled' : 'Disabled',
},
DeleteMarkerReplication: {
Status: formData.expiredDeleteMark ? 'Enabled' : 'Disabled',
},
Destination: {
Bucket: targetResponse,
StorageClass: formData.storageType || 'STANDARD',
},
}
const validTags = formData.tags.filter(tag => tag.key && tag.value)
const filter: any = {}
if (formData.prefix) {
filter.Prefix = formData.prefix
}
if (validTags.length === 1) {
const [singleTag] = validTags
if (singleTag) {
filter.Tag = { Key: singleTag.key, Value: singleTag.value }
}
} else if (validTags.length > 1) {
filter.And = {
...(formData.prefix ? { Prefix: formData.prefix } : {}),
Tags: validTags.map(tag => ({ Key: tag.key, Value: tag.value })),
}
delete filter.Prefix
}
if (Object.keys(filter).length > 0) {
newRule.Filter = filter
}
const existingRules = oldConfig?.ReplicationConfiguration?.Rules ?? []
const payload = {
Role: targetResponse,
Rules: [...existingRules.filter((rule: any) => rule.Destination?.Bucket !== targetResponse), newRule],
}
await putBucketReplication(props.bucketName, payload)
message.success(t('Create Success'))
emit('success')
handleCancel()
} catch (error: any) {
console.error(error)
message.error(error?.message || t('Save failed'))
} finally {
submitting.value = false
}
}
const handleCancel = () => {
visible.value = false
submitting.value = false
resetForm()
}
defineExpose({
open: () => {
resetForm()
visible.value = true
},
})
</script>