Files
rustfs-console/components/buckets/info.vue
T
2025-10-31 10:19:27 +08:00

718 lines
23 KiB
Vue

<template>
<Drawer v-model="visible" :title="drawerTitle" size="xl">
<div class="space-y-6">
<ItemGroup class="space-y-4">
<Item variant="outline" class="flex-col items-stretch gap-4">
<ItemHeader>
<ItemTitle>{{ t('Access Policy') }}</ItemTitle>
<ItemActions>
<Button variant="outline" size="sm" class="shrink-0" @click="editPolicy">
<Icon name="ri:edit-2-line" class="mr-2 size-4" />
<span>{{ t('Edit') }}</span>
</Button>
</ItemActions>
</ItemHeader>
<ItemContent>
<ItemDescription class="text-sm">{{ currentPolicyLabel }}</ItemDescription>
</ItemContent>
</Item>
<Item variant="outline" class="flex-col items-stretch gap-4">
<ItemHeader class="items-start">
<div class="flex flex-col gap-1">
<ItemTitle>{{ t('Encryption') }}</ItemTitle>
<ItemDescription class="text-xs text-muted-foreground">
{{ encryptionLabel }}
</ItemDescription>
</div>
<ItemActions>
<Button variant="outline" size="sm" class="shrink-0" @click="editEncrypt">
<Icon name="ri:edit-2-line" class="mr-2 size-4" />
<span>{{ t('Edit') }}</span>
</Button>
</ItemActions>
</ItemHeader>
</Item>
<Item variant="outline" class="flex-col items-stretch gap-4">
<ItemHeader>
<ItemTitle>{{ t('Tag') }}</ItemTitle>
<ItemActions>
<Button variant="outline" size="sm" class="shrink-0" @click="addTag">
<Icon name="ri:add-line" class="mr-2 size-4" />
<span>{{ t('Add') }}</span>
</Button>
</ItemActions>
</ItemHeader>
<ItemContent>
<div v-if="tags.length" class="flex flex-wrap gap-2">
<div
v-for="(tag, index) in tags"
:key="`${tag.Key}-${index}`"
class="flex items-center gap-2 rounded-full border bg-muted/40 px-3 py-1 text-xs"
>
<button type="button" class="text-left" @click="editTag(index)">{{ tag.Key }}:{{ tag.Value }}</button>
<Button variant="ghost" size="sm" class="h-6 w-6 p-0" @click.stop="handleDeleteTag(index)">
<Icon name="ri:close-line" class="size-3.5" />
</Button>
</div>
</div>
<ItemDescription v-else class="text-sm">
{{ t('No Data') }}
</ItemDescription>
</ItemContent>
</Item>
<Item variant="outline" class="flex-col items-stretch gap-4">
<ItemHeader>
<ItemTitle>{{ t('Object Lock') }}</ItemTitle>
<ItemActions>
<Spinner v-if="objectLockLoading" class="size-3 text-muted-foreground" />
</ItemActions>
</ItemHeader>
<ItemContent class="flex flex-col gap-3">
<div class="flex items-center justify-between gap-3">
<p class="text-sm font-medium text-foreground">{{ t('Object Lock') }}</p>
<Switch v-model="lockStatus" disabled />
</div>
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-2">
<p class="text-sm font-medium text-foreground">{{ t('Version Control') }}</p>
<Spinner v-if="statusLoading" class="size-3 text-muted-foreground" />
</div>
<Switch
:modelValue="versioningStatus === 'Enabled'"
:disabled="lockStatus || statusLoading"
@update:modelValue="handleVersionToggle"
/>
</div>
</ItemContent>
</Item>
<Item variant="outline" class="flex-col items-stretch gap-4">
<ItemHeader class="items-start">
<div>
<ItemTitle>{{ t('Retention') }}</ItemTitle>
<ItemDescription class="text-xs text-muted-foreground">
{{ retentionEnabled ? t('Enabled') : t('Disabled') }}
</ItemDescription>
</div>
<ItemActions>
<Button variant="outline" size="sm" class="shrink-0" @click="editRetention">
<span>{{ t('Edit') }}</span>
</Button>
</ItemActions>
</ItemHeader>
<ItemContent>
<div class="grid gap-3 sm:grid-cols-2">
<div>
<p class="text-xs text-muted-foreground">{{ t('Retention Mode') }}</p>
<p class="text-sm text-foreground">
{{ retentionFormValue.retentionMode ? t(retentionFormValue.retentionMode) : '-' }}
</p>
</div>
<div>
<p class="text-xs text-muted-foreground">{{ t('Retention Unit') }}</p>
<p class="text-sm text-foreground">
{{ retentionFormValue.retentionUnit ? t(retentionFormValue.retentionUnit) : '-' }}
</p>
</div>
<div>
<p class="text-xs text-muted-foreground">{{ t('Retention Period') }}</p>
<p class="text-sm text-foreground">
{{ retentionFormValue.retentionPeriod ?? '-' }}
</p>
</div>
</div>
</ItemContent>
</Item>
</ItemGroup>
</div>
</Drawer>
<Modal v-model="showPolicyModal" :title="t('Set Policy')" size="xl">
<div class="space-y-4">
<Field>
<FieldLabel>{{ t('Policy') }}</FieldLabel>
<FieldContent>
<Selector
v-model="policyFormValue.policy"
:options="policyOptions"
:placeholder="t('Please select policy')"
/>
</FieldContent>
</Field>
<Field v-if="policyFormValue.policy === 'custom'">
<FieldLabel>{{ t('Policy Content') }}</FieldLabel>
<FieldContent>
<div class="max-h-[60vh] overflow-y-auto rounded-md border p-2">
<json-editor v-model="policyFormValue.content" />
</div>
</FieldContent>
</Field>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<Button variant="outline" @click="showPolicyModal = false">
{{ t('Cancel') }}
</Button>
<Button variant="default" @click="submitPolicyForm">
{{ t('Confirm') }}
</Button>
</div>
</template>
</Modal>
<Modal v-model="showTagModal" :title="t('Set Tag')" size="md">
<div class="space-y-4">
<Field>
<FieldLabel>{{ t('Tag Key') }}</FieldLabel>
<FieldContent>
<Input v-model="tagFormValue.name" :placeholder="t('Tag Key Placeholder')" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Tag Value') }}</FieldLabel>
<FieldContent>
<Input v-model="tagFormValue.value" :placeholder="t('Please enter tag value')" />
</FieldContent>
</Field>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<Button variant="outline" @click="showTagModal = false">
{{ t('Cancel') }}
</Button>
<Button variant="default" @click="submitTagForm">
{{ t('Confirm') }}
</Button>
</div>
</template>
</Modal>
<Modal v-model="showEncryptModal" :title="t('Enable Storage Encryption')" size="md">
<div class="space-y-4">
<Field>
<FieldLabel>{{ t('Encryption Type') }}</FieldLabel>
<FieldContent>
<Selector
v-model="encryptFormValue.encrypt"
:options="encryptionOptions"
:placeholder="t('Please select encryption type')"
/>
</FieldContent>
</Field>
<Field v-if="encryptFormValue.encrypt === 'SSE-KMS'">
<FieldLabel>KMS Key ID</FieldLabel>
<FieldContent>
<Selector
v-model="encryptFormValue.kmsKeyId"
:options="kmsKeyOptions"
:placeholder="t('Please select KMS key')"
/>
</FieldContent>
</Field>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<Button variant="outline" @click="showEncryptModal = false">
{{ t('Cancel') }}
</Button>
<Button variant="default" @click="submitEncryptForm">
{{ t('Confirm') }}
</Button>
</div>
</template>
</Modal>
<Modal v-model="showRetentionModal" :title="t('Set Retention')" size="md">
<div class="space-y-4">
<Field>
<FieldLabel>{{ t('Retention Mode') }}</FieldLabel>
<FieldContent>
<RadioGroup v-model="retentionFormValue.retentionMode" class="grid gap-2 sm:grid-cols-2">
<label
v-for="option in retentionModeOptions"
:key="option.value"
class="flex items-start gap-3 rounded-md border border-border/50 p-3"
>
<RadioGroupItem :value="option.value" class="mt-0.5" />
<span class="text-sm font-medium">{{ option.label }}</span>
</label>
</RadioGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Retention Unit') }}</FieldLabel>
<FieldContent>
<RadioGroup v-model="retentionFormValue.retentionUnit" class="grid gap-2 sm:grid-cols-2">
<label
v-for="option in retentionUnitOptions"
:key="option.value"
class="flex items-start gap-3 rounded-md border border-border/50 p-3"
>
<RadioGroupItem :value="option.value" class="mt-0.5" />
<span class="text-sm font-medium">{{ option.label }}</span>
</label>
</RadioGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Retention Period') }}</FieldLabel>
<FieldContent>
<Input v-model="retentionPeriodInput" type="number" />
</FieldContent>
</Field>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<Button variant="outline" @click="showRetentionModal = false">
{{ t('Cancel') }}
</Button>
<Button variant="default" @click="submitRetentionForm">
{{ t('Confirm') }}
</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 { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemHeader, ItemTitle } from '@/components/ui/item'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Spinner } from '@/components/ui/spinner'
import { Switch } from '@/components/ui/switch'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import Drawer from '~/components/drawer.vue'
import Modal from '~/components/modal.vue'
import Selector from '~/components/selector.vue'
import type { BucketPolicyType } from '~/utils/bucket-policy'
import { getBucketPolicy as detectBucketPolicy, setBucketPolicy } from '~/utils/bucket-policy'
const { t } = useI18n()
const message = useMessage()
const dialog = useDialog()
const visible = ref(false)
const bucketName = ref('')
const drawerTitle = computed(() => `${t('Bucket Configuration')}(${bucketName.value || '-'})`)
const openDrawer = (bucket: string) => {
visible.value = true
bucketName.value = bucket
getData()
}
defineExpose({ openDrawer })
const {
getBucketTagging,
putBucketTagging,
putBucketVersioning,
getBucketVersioning,
getBucketPolicy,
putBucketPolicy,
getObjectLockConfiguration,
putObjectLockConfiguration,
getBucketEncryption,
putBucketEncryption,
deleteBucketEncryption,
} = useBucket({})
const { getKeyList } = useSSE()
const lockStatus = ref(false)
const objectLockLoading = ref(false)
const retentionEnabled = ref(false)
const retentionFormValue = ref<{
retentionMode: string | null
retentionPeriod: number | null
retentionUnit: string | null
}>({
retentionMode: null,
retentionPeriod: null,
retentionUnit: null,
})
const policyFormValue = ref({
policy: 'private',
content: '{}',
})
const bucketPolicy = ref('public')
const showPolicyModal = ref(false)
const encryptFormValue = ref({
encrypt: 'disabled',
kmsKeyId: '',
})
const showEncryptModal = ref(false)
const kmsKeyOptions = ref<{ label: string; value: string }[]>([])
const versioningStatus = ref<'Enabled' | 'Suspended' | undefined>('Suspended')
const statusLoading = ref(false)
interface Tag {
Key: string
Value: string
}
const tags = ref<Tag[]>([])
const showTagModal = ref(false)
const tagFormValue = ref({
name: '',
value: '',
})
const editingTagIndex = ref(-1)
const showRetentionModal = ref(false)
const retentionPeriodInput = computed({
get: () => (retentionFormValue.value.retentionPeriod ?? '').toString(),
set: value => {
if (!value) {
retentionFormValue.value.retentionPeriod = null
return
}
const numeric = Number.parseInt(value, 10)
retentionFormValue.value.retentionPeriod = Number.isFinite(numeric) ? numeric : null
},
})
const policyOptions = computed(() => [
{ label: t('Public'), value: 'public' },
{ label: t('Private'), value: 'private' },
{ label: t('Custom'), value: 'custom' },
])
const encryptionOptions = computed(() => [
{ label: t('Disabled'), value: 'disabled' },
{ label: 'SSE-KMS', value: 'SSE-KMS' },
{ label: 'SSE-S3', value: 'SSE-S3' },
])
const retentionModeOptions = computed(() => [
{ label: t('COMPLIANCE'), value: 'COMPLIANCE' },
{ label: t('GOVERNANCE'), value: 'GOVERNANCE' },
])
const retentionUnitOptions = computed(() => [
{ label: t('DAYS'), value: 'Days' },
{ label: t('YEARS'), value: 'Years' },
])
const currentPolicyLabel = computed(() => {
const option = policyOptions.value.find(option => option.value === bucketPolicy.value)
if (option) return option.label
if (bucketPolicy.value === 'custom') return t('Custom')
return bucketPolicy.value
})
const encryptionLabel = computed(() => {
if (encryptFormValue.value.encrypt === 'SSE-KMS') return 'SSE-KMS'
if (encryptFormValue.value.encrypt === 'SSE-S3') return 'SSE-S3'
return t('Disabled')
})
const getData = async () => {
await Promise.allSettled([
getTags(),
getVersioningStatus(),
getObjectLockConfig(),
getBucketEncryptionInfo(),
loadBucketPolicy(),
])
}
const getObjectLockConfig = async () => {
objectLockLoading.value = true
try {
const res = await getObjectLockConfiguration(bucketName.value)
if (res.ObjectLockConfiguration?.ObjectLockEnabled === 'Enabled') {
lockStatus.value = true
const rule = res.ObjectLockConfiguration.Rule?.DefaultRetention
if (rule) {
retentionEnabled.value = true
retentionFormValue.value.retentionMode = rule.Mode ?? null
retentionFormValue.value.retentionPeriod = rule.Days ?? rule.Years ?? null
retentionFormValue.value.retentionUnit = rule.Years ? 'Years' : rule.Days ? 'Days' : null
} else {
retentionEnabled.value = false
retentionFormValue.value.retentionMode = null
retentionFormValue.value.retentionPeriod = null
retentionFormValue.value.retentionUnit = null
}
} else {
lockStatus.value = false
retentionEnabled.value = false
retentionFormValue.value.retentionMode = null
retentionFormValue.value.retentionPeriod = null
retentionFormValue.value.retentionUnit = null
}
} finally {
objectLockLoading.value = false
}
}
const loadBucketPolicy = async () => {
try {
const res = await getBucketPolicy(bucketName.value)
if (res.Policy) {
policyFormValue.value.content = res.Policy
const parsed = detectBucketPolicy(JSON.parse(res.Policy).Statement, bucketName.value, '')
bucketPolicy.value = parsed === 'none' ? 'custom' : parsed
policyFormValue.value.policy = bucketPolicy.value
} else {
bucketPolicy.value = 'public'
policyFormValue.value.policy = 'public'
}
} catch (error: any) {
console.error('Error fetching bucket policy:', error)
bucketPolicy.value = 'private'
policyFormValue.value.policy = 'private'
policyFormValue.value.content = '{}'
}
}
const editPolicy = () => {
showPolicyModal.value = true
}
const submitPolicyForm = async () => {
try {
if (policyFormValue.value.policy === 'custom') {
const parsed = JSON.stringify(JSON.parse(policyFormValue.value.content))
await putBucketPolicy(bucketName.value, parsed)
} else {
const statements = setBucketPolicy([], policyFormValue.value.policy as BucketPolicyType, bucketName.value, '')
await putBucketPolicy(bucketName.value, JSON.stringify({ Version: '2012-10-17', Statement: statements }))
}
message.success(t('Edit Success'))
showPolicyModal.value = false
await loadBucketPolicy()
} catch (error: any) {
message.error(`${t('Edit Failed')}: ${error.message}`)
}
}
const editEncrypt = () => {
showEncryptModal.value = true
if (encryptFormValue.value.encrypt === 'SSE-KMS') {
fetchKMSKeys()
}
}
const getBucketEncryptionInfo = async () => {
try {
const res = await getBucketEncryption(bucketName.value)
const rule = res.ServerSideEncryptionConfiguration?.Rules?.[0]
const defaultRule = rule?.ApplyServerSideEncryptionByDefault
if (defaultRule?.SSEAlgorithm === 'aws:kms') {
encryptFormValue.value.encrypt = 'SSE-KMS'
encryptFormValue.value.kmsKeyId = defaultRule.KMSMasterKeyID ?? ''
} else if (defaultRule?.SSEAlgorithm === 'AES256') {
encryptFormValue.value.encrypt = 'SSE-S3'
encryptFormValue.value.kmsKeyId = ''
} else {
encryptFormValue.value.encrypt = 'disabled'
encryptFormValue.value.kmsKeyId = ''
}
} catch (error: any) {
if (error?.status === 404) {
encryptFormValue.value.encrypt = 'disabled'
encryptFormValue.value.kmsKeyId = ''
} else {
console.error('Failed to get bucket encryption:', error)
}
}
}
const fetchKMSKeys = async () => {
try {
const response = await getKeyList()
kmsKeyOptions.value = (response?.keys || []).map((key: any) => {
const label = key.tags?.name || key.description || `Key-${key.key_id?.substring(0, 8)}`
return { label, value: key.key_id }
})
} catch (error) {
console.error('Failed to fetch KMS keys:', error)
message.error(t('Failed to fetch KMS keys'))
}
}
watch(
() => encryptFormValue.value.encrypt,
value => {
if (value === 'SSE-KMS') {
fetchKMSKeys()
}
}
)
const submitEncryptForm = async () => {
try {
if (encryptFormValue.value.encrypt === 'disabled') {
await deleteBucketEncryption(bucketName.value)
} else if (encryptFormValue.value.encrypt === 'SSE-KMS') {
if (!encryptFormValue.value.kmsKeyId) {
message.error(t('Please select a KMS key for SSE-KMS encryption'))
return
}
await putBucketEncryption(bucketName.value, {
Rules: [
{
ApplyServerSideEncryptionByDefault: {
SSEAlgorithm: 'aws:kms',
KMSMasterKeyID: encryptFormValue.value.kmsKeyId,
},
},
],
})
} else {
await putBucketEncryption(bucketName.value, {
Rules: [
{
ApplyServerSideEncryptionByDefault: {
SSEAlgorithm: 'AES256',
},
},
],
})
}
message.success(t('Edit Success'))
showEncryptModal.value = false
await getBucketEncryptionInfo()
} catch (error: any) {
message.error(`${t('Edit Failed')}: ${error.message}`)
}
}
const getVersioningStatus = async () => {
try {
const resp = await getBucketVersioning(bucketName.value)
versioningStatus.value = resp.Status as 'Enabled' | 'Suspended' | undefined
} catch (error) {
console.error('get version fail:', error)
}
}
const handleVersionToggle = async (value: boolean) => {
if (statusLoading.value) return
const target = value ? 'Enabled' : 'Suspended'
const previous = versioningStatus.value
statusLoading.value = true
try {
await putBucketVersioning(bucketName.value, target)
versioningStatus.value = target
message.success(t('Edit Success'))
} catch (error: any) {
versioningStatus.value = previous
message.error(`${t('Edit Failed')}: ${error.message}`)
} finally {
statusLoading.value = false
}
}
const getTags = async () => {
const resp: any = await getBucketTagging(bucketName.value)
tags.value = resp.TagSet || []
}
const addTag = () => {
editingTagIndex.value = -1
tagFormValue.value = { name: '', value: '' }
showTagModal.value = true
}
const editTag = (index: number) => {
editingTagIndex.value = index
const nowTag = tags.value[index]
if (!nowTag) return
tagFormValue.value = { name: nowTag.Key, value: nowTag.Value }
showTagModal.value = true
}
const handleDeleteTag = (index: number) => {
dialog.error({
title: t('Warning'),
content: t('Delete Tag Confirm'),
positiveText: t('Confirm'),
negativeText: t('Cancel'),
onPositiveClick: async () => {
tags.value.splice(index, 1)
try {
await putBucketTagging(bucketName.value, { TagSet: tags.value })
message.success(t('Tag Update Success'))
} catch (error: any) {
message.error(`${t('Tag Delete Failed')}: ${error.message}`)
}
},
})
}
const submitTagForm = async () => {
if (!tagFormValue.value.name || !tagFormValue.value.value) {
message.error(t('Please fill in complete tag information'))
return
}
if (editingTagIndex.value === -1) {
tags.value.push({ Key: tagFormValue.value.name, Value: tagFormValue.value.value })
} else {
tags.value[editingTagIndex.value] = {
Key: tagFormValue.value.name,
Value: tagFormValue.value.value,
}
}
try {
await putBucketTagging(bucketName.value, { TagSet: tags.value })
message.success(t('Tag Update Success'))
showTagModal.value = false
} catch (error: any) {
message.error(`${t('Tag Update Failed')}: ${error.message}`)
}
}
const editRetention = () => {
if (!lockStatus.value) {
message.error(t('Object lock is not enabled, cannot set retention'))
return
}
showRetentionModal.value = true
}
const submitRetentionForm = async () => {
const { retentionMode, retentionPeriod, retentionUnit } = retentionFormValue.value
if (!retentionMode || !retentionPeriod || !retentionUnit) {
message.error(t('Please fill in complete retention information'))
return
}
try {
await putObjectLockConfiguration(bucketName.value, {
ObjectLockEnabled: 'Enabled',
Rule: {
DefaultRetention: {
Mode: retentionMode,
Days: retentionUnit === 'Days' ? retentionPeriod : null,
Years: retentionUnit === 'Years' ? retentionPeriod : null,
},
},
})
message.success(t('Edit Success'))
showRetentionModal.value = false
await getObjectLockConfig()
} catch (error: any) {
message.error(`${t('Edit Failed')}: ${error.message}`)
}
}
</script>