Merge branch 'refactor/shadcn-vue'

This commit is contained in:
马登山
2025-11-05 10:52:40 +08:00
28 changed files with 254 additions and 143 deletions
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="70" height="30" fill="none" viewBox="0 0 70 30" style="width:204px;height:30px"><g clip-path="url(#a)"><path fill="#FBAD41" d="M52.688 13.028c-.22 0-.437.008-.654.015a.297.297 0 0 0-.102.024.365.365 0 0 0-.236.255l-.93 3.249c-.401 1.397-.252 2.687.422 3.634.618.876 1.646 1.39 2.894 1.45l5.045.306c.15.008.28.08.359.199a.492.492 0 0 1 .051.434.64.64 0 0 1-.547.426l-5.242.306c-2.848.132-5.912 2.456-6.987 5.29l-.378 1a.28.28 0 0 0 .248.382h18.054a.48.48 0 0 0 .464-.35 13.12 13.12 0 0 0 .48-3.54c0-7.22-5.789-13.072-12.933-13.072"></path><path fill="#F6821F" d="m44.808 29.578.334-1.175c.402-1.397.253-2.687-.42-3.634-.62-.876-1.647-1.39-2.896-1.45l-23.665-.306a.467.467 0 0 1-.374-.199.492.492 0 0 1-.052-.434.64.64 0 0 1 .552-.426l23.886-.306c2.836-.131 5.9-2.456 6.975-5.29l1.362-3.6a.914.914 0 0 0 .04-.477C48.998 5.259 42.79 0 35.368 0c-6.842 0-12.647 4.462-14.73 10.665a6.92 6.92 0 0 0-4.911-1.374c-3.28.33-5.92 3.002-6.246 6.318a7.148 7.148 0 0 0 .18 2.472c-5.36.16-9.66 4.598-9.66 10.052 0 .493.035.979.106 1.453a.46.46 0 0 0 .457.402h43.704a.57.57 0 0 0 .54-.418"></path></g><defs><clipPath id="a"><path fill="#FFF" d="M0 0h204v30H0z"></path></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+16 -3
View File
@@ -34,9 +34,22 @@ const visibleColumnCount = computed(() => props.table.getVisibleLeafColumns().le
const hasRows = computed(() => props.table.getRowModel().rows.length > 0)
const getColumnStyles = (column: Column<TData, unknown>) => {
const maxWidth = column.columnDef.meta?.maxWidth
if (maxWidth == null) return undefined
return { maxWidth: typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth }
const meta = column.columnDef.meta
if (!meta) return undefined
const styles: Record<string, string> = {}
if (meta.width != null) {
styles.width = typeof meta.width === 'number' ? `${meta.width}px` : meta.width
}
if (meta.minWidth != null) {
styles.minWidth = typeof meta.minWidth === 'number' ? `${meta.minWidth}px` : meta.minWidth
}
if (meta.maxWidth != null) {
styles.maxWidth = typeof meta.maxWidth === 'number' ? `${meta.maxWidth}px` : meta.maxWidth
}
return Object.keys(styles).length > 0 ? styles : undefined
}
</script>
+2 -2
View File
@@ -74,11 +74,11 @@ const handleEscape = (event: Event) => {
</slot>
</DialogHeader>
<div class="max-h-[80vh]">
<div class="max-h-[80vh] overflow-scroll">
<slot />
</div>
<DialogFooter v-if="showFooter && $slots.footer">
<DialogFooter v-if="showFooter && $slots.footer" class="border-t pt-4">
<slot name="footer" />
</DialogFooter>
</DialogContent>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="sticky top-0 z-10 flex flex-col justify-between gap-2 md:flex-row">
<div class="sticky bg-background top-0 z-10 flex flex-col justify-between gap-2 md:flex-row">
<div class="space-y-2">
<slot />
<slot name="description"></slot>
+78 -28
View File
@@ -52,24 +52,40 @@
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('Access Key') }}</FieldLabel>
<FieldContent>
<Input v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
</FieldContent>
</Field>
<template v-if="isGCS">
<Field>
<FieldLabel>{{ t('Credentials') }} (JSON)</FieldLabel>
<FieldContent>
<Textarea
v-model="formData.creds"
:placeholder="t('Please enter GCS credentials JSON')"
autocomplete="off"
:rows="6"
/>
</FieldContent>
</Field>
</template>
<Field>
<FieldLabel>{{ t('Secret Key') }}</FieldLabel>
<FieldContent>
<Input
v-model="formData.secretkey"
type="password"
autocomplete="off"
:placeholder="t('Please enter Secret Key')"
/>
</FieldContent>
</Field>
<template v-else>
<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.secretkey"
type="password"
autocomplete="off"
:placeholder="t('Please enter Secret Key')"
/>
</FieldContent>
</Field>
</template>
<Field>
<FieldLabel>{{ t('Bucket') }}</FieldLabel>
@@ -116,13 +132,20 @@
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Field, FieldContent, FieldDescription, FieldLabel } from '@/components/ui/field'
import { reactive, ref } from 'vue'
import { reactive, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import RustfsIcon from '~/assets/logo.svg'
import AWSIcon from '~/assets/svg/aws.svg'
import MinioIcon from '~/assets/svg/minio.svg'
import AliyunIcon from '~/assets/svg/aliyun.svg'
import TencentIcon from '~/assets/svg/tenxunyun.svg'
import HuaweiIcon from '~/assets/svg/huaweiyun.svg'
import AzureIcon from '~/assets/svg/azure.svg'
import GoogleIcon from '~/assets/svg/google.svg'
import CloudflareIcon from '~/assets/svg/cloudflare.svg'
import Modal from '~/components/modal.vue'
const { t } = useI18n()
@@ -133,6 +156,22 @@ const typeOptions = [
{ label: t('RustFS'), value: 'rustfs', iconUrl: RustfsIcon, description: t('RustFS built-in cold storage') },
{ label: t('Minio'), value: 'minio', iconUrl: MinioIcon, description: t('External MinIO tier') },
{ label: t('AWS S3'), value: 's3', iconUrl: AWSIcon, description: t('Standard AWS S3 tier') },
{
label: t('Aliyun OSS'),
value: 'aliyun',
iconUrl: AliyunIcon,
description: t('Alibaba Cloud Object Storage Service'),
},
{ label: t('Tencent COS'), value: 'tencent', iconUrl: TencentIcon, description: t('Tencent Cloud Object Storage') },
{
label: t('Huawei OBS'),
value: 'huaweicloud',
iconUrl: HuaweiIcon,
description: t('Huawei Cloud Object Storage Service'),
},
{ label: t('Azure Blob'), value: 'azure', iconUrl: AzureIcon, description: t('Microsoft Azure Blob Storage') },
{ label: t('Google Cloud Storage'), value: 'gcs', iconUrl: GoogleIcon, description: t('Google Cloud Storage') },
{ label: t('Cloudflare R2'), value: 'r2', iconUrl: CloudflareIcon, description: t('Cloudflare R2 Storage') },
]
const visible = ref(false)
@@ -145,12 +184,15 @@ const formData = reactive({
endpoint: '',
accesskey: '',
secretkey: '',
creds: '',
bucket: '',
prefix: '',
region: '',
storageclass: 'STANDARD',
})
const isGCS = computed(() => formData.type === 'gcs')
const errors = reactive({
name: '',
})
@@ -175,6 +217,7 @@ const resetForm = () => {
formData.endpoint = ''
formData.accesskey = ''
formData.secretkey = ''
formData.creds = ''
formData.bucket = ''
formData.prefix = ''
formData.region = ''
@@ -203,18 +246,25 @@ const handleSave = async () => {
if (!validate()) return
submitting.value = true
try {
const config: any = {
name: formData.name,
endpoint: formData.endpoint,
bucket: formData.bucket,
prefix: formData.prefix,
region: formData.region,
storageClass: formData.storageclass,
}
if (isGCS.value) {
config.creds = formData.creds
} else {
config.accessKey = formData.accesskey
config.secretKey = formData.secretkey
}
const payload = {
type: formData.type,
[formData.type]: {
name: formData.name,
endpoint: formData.endpoint,
bucket: formData.bucket,
prefix: formData.prefix,
region: formData.region,
accessKey: formData.accesskey,
secretKey: formData.secretkey,
storageClass: formData.storageclass,
},
[formData.type]: config,
}
await usetier.addTiers(payload)
+3 -2
View File
@@ -1,14 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { HTMLAttributes, CSSProperties } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
style?: CSSProperties | string
}>()
</script>
<template>
<td :class="cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', props.class)">
<td :class="cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', props.class)" :style="props.style">
<slot />
</td>
</template>
+3 -1
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { HTMLAttributes, CSSProperties } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
style?: CSSProperties | string
}>()
</script>
@@ -15,6 +16,7 @@ const props = defineProps<{
props.class
)
"
:style="props.style"
>
<slot />
</th>
+2 -2
View File
@@ -13,10 +13,10 @@
</TabsList>
<TabsContent value="users" class="mt-0">
<users-group-members :group="group" @search="getGroupData(group.name)" />
<user-group-members :group="group" @search="getGroupData(group.name)" />
</TabsContent>
<TabsContent value="policy" class="mt-0">
<users-group-policies :group="group" @search="getGroupData(group.name)" />
<user-group-policies :group="group" @search="getGroupData(group.name)" />
</TabsContent>
</Tabs>
</div>
+8 -3
View File
@@ -19,12 +19,17 @@
:placeholder="t('Select user group members')"
/>
</div>
<div class="flex items-center gap-2 sm:self-start">
<div class="flex items-center gap-2 sm:self-end">
<Button type="button" variant="outline" @click="changeMembers">{{ t('Submit') }}</Button>
</div>
</div>
</CardContent>
</Card>
<div v-if="editStatus && members.length" class="flex flex-wrap gap-2">
<Badge v-for="value in members" :key="value" variant="secondary">
{{ value }}
</Badge>
</div>
<DataTable :table="table" />
</div>
@@ -124,11 +129,11 @@ const changeMembers = async () => {
groupStatus: 'enabled',
})
message.success('修改成功')
message.success(t('Edit Success'))
editStatus.value = false
emit('search')
} catch (error) {
message.error('修改失败')
message.error(t('Edit Failed'))
}
}
</script>
+4 -5
View File
@@ -52,12 +52,9 @@
</Command>
</PopoverContent>
</Popover>
<div v-if="selectedPolicies.length" class="flex flex-wrap gap-2">
<Badge v-for="value in selectedPolicies" :key="value" variant="secondary">{{ value }}</Badge>
</div>
</FieldContent>
</Field>
<div class="flex items-center gap-2 sm:self-start">
<div class="flex items-center gap-2 sm:self-end">
<Button variant="ghost" @click="cancelEditing">
{{ t('Cancel') }}
</Button>
@@ -66,7 +63,9 @@
</Button>
</div>
</div>
<div v-if="editStatus && selectedPolicies.length" class="flex flex-wrap gap-2">
<Badge v-for="value in selectedPolicies" :key="value" variant="secondary">{{ value }}</Badge>
</div>
<DataTable :table="table" />
</div>
</template>
+5 -5
View File
@@ -139,11 +139,11 @@ defineExpose({
</Command>
</PopoverContent>
</Popover>
<div v-if="showBadges && modelValue.length" class="flex flex-wrap gap-2">
<Badge v-for="value in modelValue" :key="value" variant="secondary">
{{ userLabelMap[value] ?? value }}
</Badge>
</div>
</FieldContent>
</Field>
<!-- <div v-if="showBadges && modelValue.length" class="flex flex-wrap gap-2">
<Badge v-for="value in modelValue" :key="value" variant="secondary">
{{ userLabelMap[value] ?? value }}
</Badge>
</div> -->
</template>
+1 -1
View File
@@ -75,7 +75,7 @@ const refreshUser = async () => {
user.value = {
accessKey: user.value.accessKey,
memberOf: latest.memberOf ?? [],
policy: latest.policy ?? [],
policy: latest.policyName?.split(',') ?? [],
status: latest.status ?? 'enabled',
}
}
+4 -4
View File
@@ -52,12 +52,9 @@
</Command>
</PopoverContent>
</Popover>
<div v-if="selectedGroups.length" class="flex flex-wrap gap-2">
<Badge v-for="value in selectedGroups" :key="value" variant="secondary">{{ value }}</Badge>
</div>
</FieldContent>
</Field>
<div class="flex items-center gap-2 sm:self-start">
<div class="flex items-center gap-2 sm:self-end">
<Button variant="ghost" @click="cancelEditing">
{{ t('Cancel') }}
</Button>
@@ -66,6 +63,9 @@
</Button>
</div>
</div>
<div v-if="selectedGroups.length" class="flex flex-wrap gap-2">
<Badge v-for="value in selectedGroups" :key="value" variant="secondary">{{ value }}</Badge>
</div>
<DataTable :table="table" />
</div>
+4 -4
View File
@@ -52,12 +52,9 @@
</Command>
</PopoverContent>
</Popover>
<div v-if="selectedPolicies.length" class="flex flex-wrap gap-2">
<Badge v-for="value in selectedPolicies" :key="value" variant="secondary">{{ value }}</Badge>
</div>
</FieldContent>
</Field>
<div class="flex items-center gap-2 sm:self-start">
<div class="flex items-center gap-2 sm:self-end">
<Button variant="ghost" @click="cancelEditing">
{{ t('Cancel') }}
</Button>
@@ -66,6 +63,9 @@
</Button>
</div>
</div>
<div v-if="selectedPolicies.length" class="flex flex-wrap gap-2">
<Badge v-for="value in selectedPolicies" :key="value" variant="secondary">{{ value }}</Badge>
</div>
<DataTable :table="table" />
</div>
+3 -1
View File
@@ -848,5 +848,7 @@
"username length cannot be less than 8 characters and greater than 16 characters": "username length cannot be less than 8 characters and greater than 16 characters",
"Validation failed": "Validation failed",
"API request failed": "API request failed",
"Operation failed": "Operation failed"
"Operation failed": "Operation failed",
"Create a user to get started": "Create a user to get started",
"Get Notification Config Failed": "Get Notification Config Failed"
}
+3 -1
View File
@@ -848,5 +848,7 @@
"username length cannot be less than 8 characters and greater than 16 characters": "username length cannot be less than 8 characters and greater than 16 characters",
"Validation failed": "Doğrulama başarısız",
"API request failed": "API isteği başarısız",
"Operation failed": "İşlem başarısız"
"Operation failed": "İşlem başarısız",
"Create a user to get started": "Başlamak için bir kullanıcı oluşturun",
"Get Notification Config Failed": "Bildirim Yapılandırması Alınamadı"
}
+3 -1
View File
@@ -848,5 +848,7 @@
"username length cannot be less than 8 characters and greater than 16 characters": "用户名长度不能少于8个字符且不能大于16个字符",
"Validation failed": "验证失败",
"API request failed": "API 请求失败",
"Operation failed": "操作失败"
"Operation failed": "操作失败",
"Create a user to get started": "开始创建一个用户",
"Get Notification Config Failed": "获取通知配置失败"
}
+3
View File
@@ -118,6 +118,9 @@ const columns: ColumnDef<RowData>[] = [
header: () => t('Actions'),
enableSorting: false,
enableHiding: false,
meta: {
width: 200,
},
cell: ({ row }) =>
h('div', { class: 'flex justify-center gap-2' }, [
h(
+3 -3
View File
@@ -29,14 +29,14 @@ const handleLogin = async () => {
const credentials = method.value === 'accessKeyAndSecretKey' ? accessKeyAndSecretKey.value : sts.value
try {
// 使用配置管理器获取配置(优先使用localStorage中的配置)
// use configManager to get config .use localStorage first, then use the config to login
const { configManager } = await import('~/utils/config')
const currentConfig = await configManager.loadConfig()
await auth.login(credentials, currentConfig)
message.success(t('Login Success'))
// 重新加载页面以确保新的配置生效
// reload the page to ensure the new config is applied
window.location.reload()
} catch (error) {
message.error(t('Login Failed'))
@@ -56,7 +56,7 @@ const handleLogin = async () => {
<div
class="w-full lg:w-1/2 flex flex-col justify-center items-center bg-white dark:bg-neutral-900 dark:border-neutral-700 relative"
>
<!-- 表单容器右上角的配置按钮 -->
<!-- config button in the top right corner of the form container -->
<div class="absolute top-4 right-4 z-10">
<NuxtLink
to="/config"
+3
View File
@@ -130,6 +130,9 @@ const columns: ColumnDef<BucketRow>[] = [
id: 'actions',
header: () => t('Actions'),
enableSorting: false,
meta: {
width: 200,
},
cell: ({ row }) =>
h('div', { class: 'flex items-center gap-2' }, [
h(
+75 -73
View File
@@ -95,82 +95,84 @@ onMounted(() => {
<template>
<page>
<img src="~/assets/backgrounds/scillate.svg" class="absolute inset-0 z-0 opacity-45" alt="" />
<div
class="flex-1 flex w-full z-10 max-w-7xl lg:max-h-[85vh] shadow-lg rounded-lg overflow-hidden mx-auto dark:bg-neutral-800 dark:border-neutral-700"
>
<div class="hidden lg:block w-1/2">
<auth-heros-static></auth-heros-static>
</div>
<div class="lg:p-20 flex flex-col items-center justify-center min-h-screen bg-gray-100 dark:bg-neutral-800">
<img src="~/assets/backgrounds/scillate.svg" class="absolute inset-0 z-0 opacity-45" alt="" />
<div
class="w-full lg:w-1/2 flex flex-col justify-center items-center bg-white dark:bg-neutral-900 dark:border-neutral-700 relative"
class="flex-1 flex w-full z-10 max-w-7xl lg:max-h-[85vh] shadow-lg rounded-lg overflow-hidden mx-auto dark:bg-neutral-800 dark:border-neutral-700"
>
<div class="max-w-sm w-full p-4 sm:p-7">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="py-6">
<h1 class="block text-2xl font-bold text-gray-800 dark:text-white">
{{ t('Server Configuration') }}
</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
{{ t('Please configure your RustFS server address') }}
</p>
</div>
<div class="mt-5 space-y-4">
<!-- Form -->
<form @submit.prevent="validateAndSave" autocomplete="off">
<div class="grid gap-y-6">
<Field>
<FieldLabel for="serverHost">{{ t('Server Address') }}</FieldLabel>
<FieldDescription>
{{ t('Leave empty to use current host as default') }}
</FieldDescription>
<FieldContent>
<Input
id="serverHost"
v-model="serverHost"
type="text"
:placeholder="t('Please enter server address (e.g., http://localhost:9000)')"
/>
</FieldContent>
<FieldDescription>
{{ t('Example: http://localhost:9000 or https://your-domain.com') }}
</FieldDescription>
</Field>
<div class="flex gap-3">
<Button type="submit" class="flex-1">
{{ t('Save Configuration') }}
</Button>
<Button type="button" variant="outline" @click="resetToCurrentHost">
{{ t('Reset') }}
</Button>
<Button type="button" variant="outline" @click="skipConfig">
{{ t('Skip') }}
</Button>
</div>
</div>
</form>
<!-- End Form -->
</div>
<div class="my-8">
<p class="text-sm text-gray-600 dark:text-neutral-400">
{{ t('Need help?') }}
<NuxtLink to="https://docs.rustfs.com" class="text-blue-600 hover:underline">{{
t('View Documentation')
}}</NuxtLink>
</p>
</div>
<div class="flex items-center justify-around gap-4 w-1/2 mx-auto">
<div class="inline-flex">
<theme-switcher />
<div class="hidden lg:block w-1/2">
<auth-heros-static></auth-heros-static>
</div>
<div
class="w-full lg:w-1/2 flex flex-col justify-center items-center bg-white dark:bg-neutral-900 dark:border-neutral-700 relative"
>
<div class="max-w-sm w-full p-4 sm:p-7">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="py-6">
<h1 class="block text-2xl font-bold text-gray-800 dark:text-white">
{{ t('Server Configuration') }}
</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
{{ t('Please configure your RustFS server address') }}
</p>
</div>
<div class="inline-flex">
<language-switcher />
<div class="mt-5 space-y-4">
<!-- Form -->
<form @submit.prevent="validateAndSave" autocomplete="off">
<div class="grid gap-y-6">
<Field>
<FieldLabel for="serverHost">{{ t('Server Address') }}</FieldLabel>
<FieldDescription>
{{ t('Leave empty to use current host as default') }}
</FieldDescription>
<FieldContent>
<Input
id="serverHost"
v-model="serverHost"
type="text"
:placeholder="t('Please enter server address (e.g., http://localhost:9000)')"
/>
</FieldContent>
<FieldDescription>
{{ t('Example: http://localhost:9000 or https://your-domain.com') }}
</FieldDescription>
</Field>
<div class="flex gap-3">
<Button type="submit" class="flex-1">
{{ t('Save Configuration') }}
</Button>
<Button type="button" variant="outline" @click="resetToCurrentHost">
{{ t('Reset') }}
</Button>
<Button type="button" variant="outline" @click="skipConfig">
{{ t('Skip') }}
</Button>
</div>
</div>
</form>
<!-- End Form -->
</div>
<div class="my-8">
<p class="text-sm text-gray-600 dark:text-neutral-400">
{{ t('Need help?') }}
<NuxtLink to="https://docs.rustfs.com" class="text-blue-600 hover:underline">{{
t('View Documentation')
}}</NuxtLink>
</p>
</div>
<div class="flex items-center justify-around gap-4 w-1/2 mx-auto">
<div class="inline-flex">
<theme-switcher />
</div>
<div class="inline-flex">
<language-switcher />
</div>
</div>
</div>
</div>
+3
View File
@@ -81,6 +81,9 @@ const columns: ColumnDef<RowData>[] = [
header: () => t('Actions'),
enableSorting: false,
enableHiding: false,
meta: {
width: 90,
},
cell: ({ row }) =>
h('div', { class: 'flex items-center gap-2' }, [
h(
+2 -2
View File
@@ -212,7 +212,7 @@ const handleRowDelete = async (row: NotificationItem, event?: Event) => {
message.success(t('Delete Success'))
await refresh()
} catch (error: any) {
console.error('删除通知配置失败:', error)
console.error(t('Delete Failed'), error)
message.error(`${t('Delete Failed')}: ${error.message || error}`)
} finally {
loading.value = false
@@ -294,7 +294,7 @@ const refresh = async () => {
pageData.value = notifications
} catch (error) {
console.error('获取通知配置失败:', error)
console.error(t('Get Notification Config Failed'), error)
pageData.value = []
} finally {
loading.value = false
+3
View File
@@ -72,6 +72,9 @@ const columns: ColumnDef<PolicyRow>[] = [
header: () => t('Actions'),
enableSorting: false,
enableHiding: false,
meta: {
width: 200,
},
cell: ({ row }) =>
h('div', { class: 'flex items-center gap-2' }, [
h(
+12
View File
@@ -62,6 +62,18 @@ const getConfig = (row: TierRow) => {
return row.minio
case 's3':
return row.s3
case 'aliyun':
return row.aliyun
case 'tencent':
return row.tencent
case 'huaweicloud':
return row.huaweicloud
case 'azure':
return row.azure
case 'gcs':
return row.gcs
case 'r2':
return row.r2
default:
return undefined
}
+3
View File
@@ -80,6 +80,9 @@ const columns: ColumnDef<GroupRow>[] = [
header: () => t('Actions'),
enableSorting: false,
enableHiding: false,
meta: {
width: 200,
},
cell: ({ row }) =>
h('div', { class: 'flex items-center gap-2' }, [
h(
+4 -1
View File
@@ -24,7 +24,7 @@
:table="table"
:is-loading="loading"
:empty-title="t('No Data')"
:empty-description="t('Create a user to get started.')"
:empty-description="t('Create a user to get started')"
table-class="min-w-full"
/>
<DataTablePagination :table="table" />
@@ -122,6 +122,9 @@ const columns: ColumnDef<UserRow>[] = [
header: () => t('Actions'),
enableSorting: false,
enableHiding: false,
meta: {
width: 200,
},
cell: ({ row }) =>
h('div', { class: 'flex items-center gap-2' }, [
h(
+2
View File
@@ -2,6 +2,8 @@ import type { RowData } from '@tanstack/vue-table'
declare module '@tanstack/vue-table' {
interface ColumnMeta<TData extends RowData, TValue> {
width?: string | number
minWidth?: string | number
maxWidth?: string | number
}
}