mirror of
https://github.com/rustfs/console.git
synced 2026-09-24 16:30:56 +08:00
refactor: tailwindcss
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import Spinner from '@/components/ui/spinner/Spinner.vue'
|
||||
|
||||
import { AppCard, AppModal } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -106,7 +110,7 @@ async function submitForm() {
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="password-current">{{ t('Current Password') }}</Label>
|
||||
<AppInput
|
||||
<Input
|
||||
id="password-current"
|
||||
v-model="formModel.current_secret_key"
|
||||
type="password"
|
||||
@@ -119,7 +123,7 @@ async function submitForm() {
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password-new">{{ t('New Password') }}</Label>
|
||||
<AppInput
|
||||
<Input
|
||||
id="password-new"
|
||||
v-model="formModel.new_secret_key"
|
||||
type="password"
|
||||
@@ -132,7 +136,7 @@ async function submitForm() {
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password-new-confirm">{{ t('Confirm New Password') }}</Label>
|
||||
<AppInput
|
||||
<Input
|
||||
id="password-new-confirm"
|
||||
v-model="formModel.re_new_secret_key"
|
||||
type="password"
|
||||
@@ -147,12 +151,13 @@ async function submitForm() {
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">
|
||||
<Button variant="outline" @click="closeModal()">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
<Button variant="default" :disabled="submitting" @click="submitForm">
|
||||
<Spinner v-if="submitting" class="size-4" />
|
||||
<span>{{ t('Submit') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppInput, AppModal, AppSwitch, AppTextarea } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import Spinner from '@/components/ui/spinner/Spinner.vue'
|
||||
|
||||
import { AppCard, AppModal, AppSwitch, AppTextarea } from '@/components/app'
|
||||
import AppDateTimePicker from '@/components/app/AppDateTimePicker.vue'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -96,7 +100,7 @@ async function submitForm() {
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Access Key') }}</Label>
|
||||
<AppInput v-model="formModel.accesskey" disabled />
|
||||
<Input v-model="formModel.accesskey" disabled />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
@@ -111,7 +115,7 @@ async function submitForm() {
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Name') }}</Label>
|
||||
<AppInput v-model="formModel.name" />
|
||||
<Input v-model="formModel.name" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
@@ -119,7 +123,7 @@ async function submitForm() {
|
||||
<AppTextarea v-model="formModel.description" :rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between rounded-md border p-3">
|
||||
<span class="text-sm font-medium">{{ t('Status') }}</span>
|
||||
<AppSwitch v-model="statusBoolean" />
|
||||
</div>
|
||||
@@ -127,12 +131,13 @@ async function submitForm() {
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">
|
||||
<Button variant="outline" @click="closeModal()">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
<Button variant="default" :disabled="submitting" @click="submitForm">
|
||||
<Spinner v-if="submitting" class="size-4" />
|
||||
<span>{{ t('Submit') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="create-access-key">{{ t('Access Key') }}</Label>
|
||||
<AppInput id="create-access-key" v-model="formModel.accessKey" autocomplete="off" />
|
||||
<Input id="create-access-key" v-model="formModel.accessKey" autocomplete="off" />
|
||||
<p v-if="errors.accessKey" class="text-sm text-destructive">{{ errors.accessKey }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="create-secret-key">{{ t('Secret Key') }}</Label>
|
||||
<AppInput id="create-secret-key" v-model="formModel.secretKey" type="password" autocomplete="off" />
|
||||
<Input id="create-secret-key" v-model="formModel.secretKey" type="password" autocomplete="off" />
|
||||
<p v-if="errors.secretKey" class="text-sm text-destructive">{{ errors.secretKey }}</p>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="create-name">{{ t('Name') }}</Label>
|
||||
<AppInput id="create-name" v-model="formModel.name" autocomplete="off" />
|
||||
<Input id="create-name" v-model="formModel.name" autocomplete="off" />
|
||||
<p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<AppTextarea id="create-description" v-model="formModel.description" :rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-start justify-between gap-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-start justify-between gap-3 rounded-md border p-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium">{{ t('Use main account policy') }}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
@@ -58,19 +58,24 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">
|
||||
<Button variant="outline" @click="closeModal()">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
<Button variant="default" :disabled="submitting" @click="submitForm">
|
||||
<Spinner v-if="submitting" class="size-4" />
|
||||
<span>{{ t('Submit') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppInput, AppModal, AppSwitch, AppTextarea } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import Spinner from '@/components/ui/spinner/Spinner.vue'
|
||||
|
||||
import { AppCard, AppModal, AppSwitch, AppTextarea } from '@/components/app'
|
||||
import AppDateTimePicker from '@/components/app/AppDateTimePicker.vue'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { ButtonVariants } from '@/components/ui/button'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import Spinner from '@/components/ui/spinner/Spinner.vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
type AppButtonVariant =
|
||||
| 'default'
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'outline'
|
||||
| 'ghost'
|
||||
| 'link'
|
||||
| 'danger'
|
||||
| 'destructive'
|
||||
type AppButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
const variantMap: Record<AppButtonVariant, ButtonVariants['variant']> = {
|
||||
default: 'secondary',
|
||||
primary: 'default',
|
||||
secondary: 'secondary',
|
||||
outline: 'outline',
|
||||
ghost: 'ghost',
|
||||
link: 'link',
|
||||
danger: 'destructive',
|
||||
destructive: 'destructive',
|
||||
}
|
||||
|
||||
const sizeMap: Record<AppButtonSize, ButtonVariants['size']> = {
|
||||
xs: 'sm',
|
||||
sm: 'sm',
|
||||
md: 'default',
|
||||
lg: 'lg',
|
||||
xl: 'lg',
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
variant?: AppButtonVariant
|
||||
size?: AppButtonSize
|
||||
loading?: boolean
|
||||
block?: boolean
|
||||
disabled?: boolean
|
||||
type?: 'button' | 'submit' | 'reset'
|
||||
class?: HTMLAttributes['class']
|
||||
}>(),
|
||||
{
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
loading: false,
|
||||
block: false,
|
||||
disabled: false,
|
||||
type: 'button',
|
||||
class: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'click', event: MouseEvent): void
|
||||
}>()
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (props.loading || props.disabled) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
emits('click', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:type="type"
|
||||
:variant="variantMap[variant]"
|
||||
:size="sizeMap[size]"
|
||||
:disabled="disabled || loading"
|
||||
:aria-busy="loading ? 'true' : undefined"
|
||||
:class="cn('gap-2', block && 'w-full', loading && 'cursor-wait', props.class)"
|
||||
@click="handleClick"
|
||||
>
|
||||
<Spinner v-if="loading" class="size-4" />
|
||||
<slot />
|
||||
</Button>
|
||||
</template>
|
||||
@@ -31,7 +31,7 @@ const props = withDefaults(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :class="cn('border-border/60', props.class)">
|
||||
<Card :class="props.class">
|
||||
<CardHeader v-if="title || $slots.header">
|
||||
<slot name="header">
|
||||
<CardTitle v-if="title">{{ title }}</CardTitle>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
type InputSize = 'sm' | 'md' | 'lg'
|
||||
|
||||
const sizeClass: Record<InputSize, string> = {
|
||||
sm: 'h-9 px-3 text-sm',
|
||||
md: 'h-10 px-3',
|
||||
lg: 'h-11 px-4 text-base',
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: HTMLInputElement['type']
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
readonly?: boolean
|
||||
size?: InputSize
|
||||
class?: HTMLAttributes['class']
|
||||
autocomplete?: string
|
||||
}>(),
|
||||
{
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
size: 'md',
|
||||
class: undefined,
|
||||
autocomplete: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
const modelValue = defineModel<string | number | null>({ default: '' })
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
modelValue.value = target.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Input
|
||||
:type="type"
|
||||
:value="modelValue ?? ''"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:autocomplete="autocomplete"
|
||||
:class="cn(sizeClass[size], props.class)"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '#components'
|
||||
import LanguageSwitcher from '@/components/language-switcher.vue'
|
||||
import ThemeSwitcher from '@/components/theme-switcher.vue'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -17,14 +21,10 @@ import {
|
||||
SidebarSeparator,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import LanguageSwitcher from '@/components/language-switcher.vue'
|
||||
import ThemeSwitcher from '@/components/theme-switcher.vue'
|
||||
import UserDropdown from '@/components/user-dropdown.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { AppConfig, NavItem } from '~/types/app-config'
|
||||
|
||||
const appConfig = useAppConfig() as unknown as AppConfig
|
||||
@@ -80,78 +80,46 @@ const getLabel = (item: NavItem) => t(item.label)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sidebar collapsible="icon" class="bg-sidebar text-sidebar-foreground">
|
||||
<SidebarHeader class="border-b border-sidebar-border px-4 py-4">
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<NuxtLink to="/" class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex size-10 items-center justify-center rounded-lg bg-primary text-lg font-semibold text-primary-foreground"
|
||||
>
|
||||
<span v-if="isCollapsed">{{ brandInitial }}</span>
|
||||
<img v-else src="~/assets/logo.svg" alt="RustFS" class="h-8" />
|
||||
<div v-if="isCollapsed" class="flex size-8 items-center justify-center rounded-lg bg-primary text-md font-semibold text-primary-foreground">
|
||||
<span>{{ brandInitial }}</span>
|
||||
</div>
|
||||
<div v-if="!isCollapsed" class="flex min-w-0 flex-col">
|
||||
<span class="truncate text-sm font-semibold leading-tight">{{ appConfig.name }}</span>
|
||||
<span class="truncate text-xs text-muted-foreground">{{ appConfig.description }}</span>
|
||||
<div v-if="!isCollapsed" class="flex min-w-0 flex-col px-3 py-4">
|
||||
<img src="~/assets/logo.svg" alt="RustFS" class="h-6" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent class="px-2">
|
||||
<SidebarContent>
|
||||
<ScrollArea class="flex-1 pr-1">
|
||||
<div class="flex flex-col gap-4">
|
||||
<SidebarGroup v-for="(group, groupIndex) in navGroups" :key="groupIndex" class="gap-1">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<template v-for="item in group" :key="item.label">
|
||||
<Collapsible
|
||||
v-if="hasChildren(item)"
|
||||
as-child
|
||||
:default-open="isRouteActive(item)"
|
||||
class="group/collapsible"
|
||||
>
|
||||
<Collapsible v-if="hasChildren(item)" as-child :default-open="isRouteActive(item)" class="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton
|
||||
:is-active="isRouteActive(item)"
|
||||
:tooltip="getLabel(item)"
|
||||
>
|
||||
<SidebarMenuButton :is-active="isRouteActive(item)" :tooltip="getLabel(item)">
|
||||
<Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" />
|
||||
<span class="flex-1 truncate">{{ getLabel(item) }}</span>
|
||||
<Icon
|
||||
name="ri:arrow-right-s-line"
|
||||
class="size-4 shrink-0 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
<Icon name="ri:arrow-right-s-line" class="size-4 shrink-0 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem v-for="child in item.children" :key="child.label">
|
||||
<SidebarMenuSubButton
|
||||
v-if="isExternal(child)"
|
||||
as-child
|
||||
size="sm"
|
||||
>
|
||||
<a
|
||||
:href="normalizedTo(child)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex w-full items-center gap-2"
|
||||
>
|
||||
<SidebarMenuSubButton v-if="isExternal(child)" as-child size="sm">
|
||||
<a :href="normalizedTo(child)" target="_blank" rel="noopener noreferrer" class="flex w-full items-center gap-2">
|
||||
<Icon v-if="child.icon" :name="child.icon" class="size-3.5 shrink-0" />
|
||||
<span class="truncate">{{ getLabel(child) }}</span>
|
||||
<Icon name="ri:external-link-line" class="ml-auto size-3 text-muted-foreground" />
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
<SidebarMenuSubButton
|
||||
v-else
|
||||
as-child
|
||||
size="sm"
|
||||
:is-active="isRouteActive(child)"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="normalizedTo(child)"
|
||||
class="flex w-full items-center gap-2"
|
||||
>
|
||||
<SidebarMenuSubButton v-else as-child size="sm" :is-active="isRouteActive(child)">
|
||||
<NuxtLink :to="normalizedTo(child)" class="flex w-full items-center gap-2">
|
||||
<Icon v-if="child.icon" :name="child.icon" class="size-3.5 shrink-0" />
|
||||
<span class="truncate">{{ getLabel(child) }}</span>
|
||||
</NuxtLink>
|
||||
@@ -162,32 +130,15 @@ const getLabel = (item: NavItem) => t(item.label)
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
<SidebarMenuItem v-else>
|
||||
<SidebarMenuButton
|
||||
v-if="isExternal(item)"
|
||||
as-child
|
||||
:tooltip="getLabel(item)"
|
||||
>
|
||||
<a
|
||||
:href="normalizedTo(item)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex w-full items-center gap-3"
|
||||
>
|
||||
<SidebarMenuButton v-if="isExternal(item)" as-child :tooltip="getLabel(item)">
|
||||
<a :href="normalizedTo(item)" target="_blank" rel="noopener noreferrer" class="flex w-full items-center gap-3">
|
||||
<Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" />
|
||||
<span class="flex-1 truncate">{{ getLabel(item) }}</span>
|
||||
<Icon name="ri:external-link-line" class="size-3.5 text-muted-foreground" />
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton
|
||||
v-else
|
||||
as-child
|
||||
:is-active="isRouteActive(item)"
|
||||
:tooltip="getLabel(item)"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="normalizedTo(item)"
|
||||
class="flex w-full items-center gap-3"
|
||||
>
|
||||
<SidebarMenuButton v-else as-child :is-active="isRouteActive(item)" :tooltip="getLabel(item)">
|
||||
<NuxtLink :to="normalizedTo(item)" class="flex w-full items-center gap-3">
|
||||
<Icon v-if="item.icon" :name="item.icon" class="size-4 shrink-0" />
|
||||
<span class="flex-1 truncate">{{ getLabel(item) }}</span>
|
||||
</NuxtLink>
|
||||
@@ -203,7 +154,7 @@ const getLabel = (item: NavItem) => t(item.label)
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter class="mt-auto flex flex-col gap-3 px-2 pb-2">
|
||||
<div class="border-t border-sidebar-border pt-3">
|
||||
<div class="pt-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
<LanguageSwitcher />
|
||||
<ThemeSwitcher />
|
||||
|
||||
@@ -16,7 +16,7 @@ const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const value = defineModel<string>({ default: '' })
|
||||
const value = defineModel<string>()
|
||||
|
||||
watch(
|
||||
() => props.tabs,
|
||||
|
||||
@@ -21,7 +21,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', file: File | null): void
|
||||
}>
|
||||
}>()
|
||||
|
||||
const hovering = ref(false)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts" generic="TData">
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
import AppButton from '@/components/app/AppButton.vue'
|
||||
import AppSelect from '@/components/app/AppSelect.vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { computed } from 'vue'
|
||||
@@ -54,23 +55,23 @@ const handlePageSizeChange = (value: number | string | boolean | null) => {
|
||||
Page {{ pageCount === 0 ? 0 : currentPage }} of {{ pageCount }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<AppButton variant="outline" size="sm" :disabled="!canPrevious" @click="props.table.setPageIndex(0)">
|
||||
<Button variant="outline" size="sm" :disabled="!canPrevious" @click="props.table.setPageIndex(0)">
|
||||
First
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" :disabled="!canPrevious" @click="props.table.previousPage()">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" :disabled="!canPrevious" @click="props.table.previousPage()">
|
||||
Prev
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" :disabled="!canNext" @click="props.table.nextPage()">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" :disabled="!canNext" @click="props.table.nextPage()">
|
||||
Next
|
||||
</AppButton>
|
||||
<AppButton
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!canNext"
|
||||
@click="props.table.setPageIndex(Math.max(pageCount - 1, 0))"
|
||||
>
|
||||
Last
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
export { default as AppDataTable } from './AppDataTable.vue'
|
||||
export { default as AppDataTablePagination } from './AppDataTablePagination.vue'
|
||||
export { useDataTable } from './useDataTable'
|
||||
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
useVueTable,
|
||||
} from '@tanstack/vue-table'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
import { computed, reactive, ref, toValue, unref, watch, type Ref } from 'vue'
|
||||
import { computed, reactive, ref, toValue, watch, type Ref } from 'vue'
|
||||
|
||||
export interface UseDataTableOptions<TData extends RowData> {
|
||||
data: MaybeRefOrGetter<TData[]>
|
||||
columns: ColumnDef<TData, any>[]
|
||||
columns: MaybeRefOrGetter<ColumnDef<TData, any>[]>
|
||||
pageSize?: number
|
||||
manualPagination?: boolean
|
||||
manualSorting?: boolean
|
||||
@@ -58,10 +58,11 @@ export function useDataTable<TData extends RowData>(
|
||||
})
|
||||
|
||||
const dataRef = computed(() => toValue(options.data))
|
||||
const columnsRef = computed(() => toValue(options.columns))
|
||||
|
||||
const table = useVueTable({
|
||||
data: dataRef,
|
||||
columns: options.columns,
|
||||
data: dataRef.value,
|
||||
columns: columnsRef.value,
|
||||
state: reactive({
|
||||
get sorting() {
|
||||
return sorting.value
|
||||
@@ -98,6 +99,20 @@ export function useDataTable<TData extends RowData>(
|
||||
getPaginationRowModel: options.manualPagination ? undefined : getPaginationRowModel(),
|
||||
})
|
||||
|
||||
watch(dataRef, value => {
|
||||
table.setOptions(prev => ({
|
||||
...prev,
|
||||
data: value,
|
||||
}))
|
||||
})
|
||||
|
||||
watch(columnsRef, value => {
|
||||
table.setOptions(prev => ({
|
||||
...prev,
|
||||
columns: value,
|
||||
}))
|
||||
})
|
||||
|
||||
return {
|
||||
table,
|
||||
sorting,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
export { default as AppButton } from './AppButton.vue'
|
||||
export { default as AppCard } from './AppCard.vue'
|
||||
export { default as AppBadge } from './AppBadge.vue'
|
||||
export { default as AppTag } from './AppTag.vue'
|
||||
export { default as AppInput } from './AppInput.vue'
|
||||
export { default as AppTextarea } from './AppTextarea.vue'
|
||||
export { default as AppSelect } from './AppSelect.vue'
|
||||
export { default as AppSwitch } from './AppSwitch.vue'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
>
|
||||
<div class="max-w-7xl flex flex-col z-10">
|
||||
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
|
||||
<div class="text-4xl my-6 font-semibold !text-primary px-0">
|
||||
<div class="text-4xl my-6 font-semibold text-primary! px-0">
|
||||
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
|
||||
<FlipWords
|
||||
:words="[
|
||||
@@ -15,7 +15,7 @@
|
||||
t('S3 Compatible'),
|
||||
]"
|
||||
:duration="3000"
|
||||
class="text-4xl font-semibold !text-primary px-0"
|
||||
class="text-4xl font-semibold text-primary! px-0"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-2">
|
||||
{{ t('Reliable distributed file system') }}
|
||||
@@ -31,8 +31,8 @@
|
||||
</a>
|
||||
<div class="h-full inset-0 absolute z-0">
|
||||
<Ripple
|
||||
class="bg-white/5 -mb-[100vh] h-full w-full -mr-[50vw] [mask-image:linear-gradient(to_bottom,white,transparent)]"
|
||||
circle-class="border-[hsl(var(--primary))] bg-[#0000]/25 dark:bg-[#fff]/25 rounded-full"
|
||||
class="bg-white/5 -mb-[100vh] h-full w-full -mr-[50vw] mask-[linear-gradient(to_bottom,white,transparent)]"
|
||||
circle-class="border-[hsl(var(--primary))] bg-[#0000]/25 dark:bg-white/25 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
>
|
||||
<div class="max-w-7xl flex flex-col z-10">
|
||||
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
|
||||
<div class="text-4xl my-6 font-semibold !text-primary px-0">
|
||||
<div class="text-4xl my-6 font-semibold text-primary! px-0">
|
||||
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
|
||||
<FlipWords
|
||||
:words="[
|
||||
@@ -15,7 +15,7 @@
|
||||
t('S3 Compatible'),
|
||||
]"
|
||||
:duration="3000"
|
||||
class="text-4xl font-semibold !text-primary px-0"
|
||||
class="text-4xl font-semibold text-primary! px-0"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-2">
|
||||
{{ t('Reliable distributed file system') }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="w-full bg-gray-50 dark:bg-black h-full p-16 flex flex-col justify-center gap-8">
|
||||
<div class="max-w-7xl flex flex-col">
|
||||
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
|
||||
<div class="text-4xl my-6 font-semibold !text-primary px-0">
|
||||
<div class="text-4xl my-6 font-semibold text-primary! px-0">
|
||||
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
|
||||
<FlipWords
|
||||
:words="[
|
||||
@@ -13,7 +13,7 @@
|
||||
t('S3 Compatible'),
|
||||
]"
|
||||
:duration="3000"
|
||||
class="text-4xl font-semibold !text-primary px-0"
|
||||
class="text-4xl font-semibold text-primary! px-0"
|
||||
/>
|
||||
<div class="text-muted-foreground mt-2">
|
||||
{{ t('Reliable distributed file system') }}
|
||||
|
||||
+36
-33
@@ -6,10 +6,10 @@
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium text-foreground">{{ t('Access Policy') }}</p>
|
||||
</div>
|
||||
<AppButton variant="outline" size="sm" class="shrink-0" @click="editPolicy">
|
||||
<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>
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">{{ currentPolicyLabel }}</p>
|
||||
</AppCard>
|
||||
@@ -20,38 +20,38 @@
|
||||
<p class="text-sm font-medium text-foreground">{{ t('Encryption') }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ encryptionLabel }}</p>
|
||||
</div>
|
||||
<AppButton variant="outline" size="sm" class="shrink-0" @click="editEncrypt">
|
||||
<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>
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<AppCard padded class="space-y-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<p class="text-sm font-medium text-foreground">{{ t('Tag') }}</p>
|
||||
<AppButton variant="outline" size="sm" class="shrink-0" @click="addTag">
|
||||
<Button variant="outline" size="sm" class="shrink-0" @click="addTag">
|
||||
<Icon name="ri:add-line" class="mr-2 size-4" />
|
||||
<span>{{ t('Add') }}</span>
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
<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 border-border/60 bg-muted/40 px-3 py-1 text-xs"
|
||||
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>
|
||||
<AppButton
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
size="sm"
|
||||
class="h-6 w-6 p-0"
|
||||
@click.stop="handleDeleteTag(index)"
|
||||
>
|
||||
<Icon name="ri:close-line" class="size-3.5" />
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm text-muted-foreground">
|
||||
@@ -88,9 +88,9 @@
|
||||
{{ retentionEnabled ? t('Enabled') : t('Disabled') }}
|
||||
</p>
|
||||
</div>
|
||||
<AppButton variant="outline" size="sm" class="shrink-0" @click="editRetention">
|
||||
<Button variant="outline" size="sm" class="shrink-0" @click="editRetention">
|
||||
<span>{{ t('Edit') }}</span>
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
@@ -128,19 +128,19 @@
|
||||
</div>
|
||||
<div v-if="policyFormValue.policy === 'custom'" class="space-y-2">
|
||||
<Label>{{ t('Policy Content') }}</Label>
|
||||
<div class="max-h-[60vh] overflow-y-auto rounded-md border border-border/60 p-2">
|
||||
<div class="max-h-[60vh] overflow-y-auto rounded-md border p-2">
|
||||
<json-editor v-model="policyFormValue.content" />
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="showPolicyModal = false">
|
||||
<Button variant="outline" @click="showPolicyModal = false">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" @click="submitPolicyForm">
|
||||
</Button>
|
||||
<Button variant="default" @click="submitPolicyForm">
|
||||
{{ t('Confirm') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
@@ -149,21 +149,21 @@
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Tag Key') }}</Label>
|
||||
<AppInput v-model="tagFormValue.name" :placeholder="t('Tag Key Placeholder')" />
|
||||
<Input v-model="tagFormValue.name" :placeholder="t('Tag Key Placeholder')" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Tag Value') }}</Label>
|
||||
<AppInput v-model="tagFormValue.value" :placeholder="t('Please enter tag value')" />
|
||||
<Input v-model="tagFormValue.value" :placeholder="t('Please enter tag value')" />
|
||||
</div>
|
||||
</AppCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="showTagModal = false">
|
||||
<Button variant="outline" @click="showTagModal = false">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" @click="submitTagForm">
|
||||
</Button>
|
||||
<Button variant="default" @click="submitTagForm">
|
||||
{{ t('Confirm') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
@@ -189,12 +189,12 @@
|
||||
</AppCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="showEncryptModal = false">
|
||||
<Button variant="outline" @click="showEncryptModal = false">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" @click="submitEncryptForm">
|
||||
</Button>
|
||||
<Button variant="default" @click="submitEncryptForm">
|
||||
{{ t('Confirm') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
@@ -219,24 +219,27 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Retention Period') }}</Label>
|
||||
<AppInput v-model="retentionPeriodInput" type="number" />
|
||||
<Input v-model="retentionPeriodInput" type="number" />
|
||||
</div>
|
||||
</AppCard>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="showRetentionModal = false">
|
||||
<Button variant="outline" @click="showRetentionModal = false">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" @click="submitRetentionForm">
|
||||
</Button>
|
||||
<Button variant="default" @click="submitRetentionForm">
|
||||
{{ t('Confirm') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppDrawer, AppInput, AppModal, AppRadioGroup, AppSelect, AppSwitch, AppSpinner } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard, AppDrawer, AppModal, AppRadioGroup, AppSelect, AppSwitch, AppSpinner } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Icon } from '#components'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<AppCard padded class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<Label for="bucket-name">{{ t('Please enter name') }}</Label>
|
||||
<AppInput
|
||||
<Input
|
||||
id="bucket-name"
|
||||
v-model="objectKey"
|
||||
autocomplete="off"
|
||||
@@ -16,19 +16,19 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Version') }}</Label>
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between rounded-md border p-3">
|
||||
<AppSwitch v-model="version" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Object Lock') }}</Label>
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between rounded-md border p-3">
|
||||
<AppSwitch v-model="objectLock" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="objectLock" class="space-y-4 rounded-lg border border-border/60 p-4">
|
||||
<div v-if="objectLock" class="space-y-4 rounded-lg border p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{{ t('Retention') }}</p>
|
||||
@@ -50,7 +50,7 @@
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Validity') }}</Label>
|
||||
<div class="flex flex-col gap-2 sm:flex-row">
|
||||
<AppInput
|
||||
<Input
|
||||
v-model="retentionPeriod"
|
||||
type="number"
|
||||
class="sm:w-32"
|
||||
@@ -68,24 +68,27 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal">
|
||||
<Button variant="outline" @click="closeModal">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
variant="primary"
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
:loading="creating"
|
||||
:disabled="isSubmitDisabled"
|
||||
@click="handleCreateBucket"
|
||||
>
|
||||
{{ t('Create') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppInput, AppModal, AppRadioGroup, AppSelect, AppSwitch } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard, AppModal, AppRadioGroup, AppSelect, AppSwitch } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { Icon } from '#components'
|
||||
import ClipboardJS from 'clipboard'
|
||||
import AppButton from '@/components/app/AppButton.vue'
|
||||
import AppInput from '@/components/app/AppInput.vue'
|
||||
import { useMessage } from '@/lib/ui/message'
|
||||
import { onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -52,21 +53,21 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="flex h-full items-center gap-2">
|
||||
<AppInput
|
||||
<Input
|
||||
v-model="model"
|
||||
:readonly="props.readonly"
|
||||
:id="props.id"
|
||||
class="flex-1"
|
||||
/>
|
||||
<AppButton
|
||||
<Button
|
||||
v-if="!props.copyIcon"
|
||||
:id="`${props.id}-btn`"
|
||||
:data-clipboard-target="`#${props.id}`"
|
||||
variant="primary"
|
||||
variant="default"
|
||||
>
|
||||
{{ t('Copy') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
:id="`${props.id}-btn-icon`"
|
||||
variant="ghost"
|
||||
@@ -77,6 +78,6 @@ onUnmounted(() => {
|
||||
>
|
||||
<Icon :size="18" name="ri:file-copy-line" />
|
||||
<span class="sr-only">{{ t('Copy') }}</span>
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<AppCard
|
||||
class="flex cursor-pointer items-center gap-3 border border-border/60 transition hover:border-primary"
|
||||
class="flex cursor-pointer items-center gap-3 border transition hover:border-primary"
|
||||
@click="resetType"
|
||||
>
|
||||
<img :src="iconUrl" class="h-10 w-10" alt="" />
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="grid gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="target-name">{{ t('Name') }} (A-Z,0-9,_)</Label>
|
||||
<AppInput
|
||||
<Input
|
||||
id="target-name"
|
||||
v-model="formData.name"
|
||||
:placeholder="t('Please enter name')"
|
||||
@@ -54,13 +54,13 @@
|
||||
class="grid gap-2"
|
||||
>
|
||||
<Label :for="`config-${config.name}`">{{ config.label }}</Label>
|
||||
<AppInput
|
||||
<Input
|
||||
v-if="config.type === 'text'"
|
||||
:id="`config-${config.name}`"
|
||||
v-model="formData.config[config.name]"
|
||||
:placeholder="`${t('Please enter')} ${config.label.toLowerCase()}`"
|
||||
/>
|
||||
<AppInput
|
||||
<Input
|
||||
v-else-if="config.type === 'password'"
|
||||
:id="`config-${config.name}`"
|
||||
v-model="formData.config[config.name]"
|
||||
@@ -68,7 +68,7 @@
|
||||
autocomplete="off"
|
||||
:placeholder="`${t('Please enter')} ${config.label.toLowerCase()}`"
|
||||
/>
|
||||
<AppInput
|
||||
<Input
|
||||
v-else-if="config.type === 'number'"
|
||||
:id="`config-${config.name}`"
|
||||
v-model="formData.config[config.name]"
|
||||
@@ -82,20 +82,23 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="handleCancel">{{ t('Cancel') }}</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" :disabled="!formData.type" @click="handleSave">
|
||||
<Button variant="outline" @click="handleCancel">{{ t('Cancel') }}</Button>
|
||||
<Button variant="default" :loading="submitting" :disabled="!formData.type" @click="handleSave">
|
||||
{{ t('Save') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import MqttIcon from '~/assets/svg/mqtt.svg'
|
||||
import WebhooksIcon from '~/assets/svg/webhooks.svg'
|
||||
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
|
||||
import { AppCard, AppModal } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useEventTarget } from '#imports'
|
||||
@@ -258,4 +261,4 @@ defineExpose({
|
||||
resetType()
|
||||
},
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
@@ -72,7 +72,7 @@
|
||||
<Checkbox
|
||||
:checked="formData.events.includes(event.value)"
|
||||
class="mt-1"
|
||||
@update:checked="value => toggleEvent(event.value, value)"
|
||||
@update:checked="handleEventChecked(event.value, $event)"
|
||||
/>
|
||||
<span>{{ t(event.labelKey) }}</span>
|
||||
</label>
|
||||
@@ -213,6 +213,10 @@ const toggleEvent = (event: string, value: boolean | 'indeterminate') => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEventChecked = (eventValue: string, value: boolean | 'indeterminate') => {
|
||||
toggleEvent(eventValue, value)
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
errors.resourceName = formData.value.resourceName ? '' : t('Please select resource name')
|
||||
errors.events = formData.value.events.length ? '' : t('Please select at least one event')
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Time Cycle') }}</Label>
|
||||
<div class="flex items-center gap-3">
|
||||
<AppInput
|
||||
<Input
|
||||
v-model="formData.days"
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -40,27 +40,27 @@
|
||||
<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')" />
|
||||
<Input 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">
|
||||
<Button variant="outline" size="sm" @click="addTag">
|
||||
<Icon name="ri:add-line" class="size-4" />
|
||||
{{ t('Add Tag') }}
|
||||
</AppButton>
|
||||
</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 border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
|
||||
class="grid gap-2 rounded-md border p-3 md:grid-cols-2 md:items-center md:gap-4"
|
||||
>
|
||||
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
|
||||
<Input 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
|
||||
<Input v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-destructive"
|
||||
@@ -68,7 +68,7 @@
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
<Icon name="ri:delete-bin-line" class="size-4" />
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Time Cycle') }}</Label>
|
||||
<div class="flex items-center gap-3">
|
||||
<AppInput
|
||||
<Input
|
||||
v-model="formData.days"
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -130,27 +130,27 @@
|
||||
<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')" />
|
||||
<Input 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">
|
||||
<Button variant="outline" size="sm" @click="addTag">
|
||||
<Icon name="ri:add-line" class="size-4" />
|
||||
{{ t('Add Tag') }}
|
||||
</AppButton>
|
||||
</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 border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
|
||||
class="grid gap-2 rounded-md border p-3 md:grid-cols-2 md:items-center md:gap-4"
|
||||
>
|
||||
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
|
||||
<Input 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
|
||||
<Input v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-destructive"
|
||||
@@ -158,7 +158,7 @@
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
<Icon name="ri:delete-bin-line" class="size-4" />
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,16 +172,19 @@
|
||||
|
||||
<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>
|
||||
<Button variant="outline" @click="handleCancel">{{ t('Cancel') }}</Button>
|
||||
<Button variant="default" :loading="submitting" @click="handleSave">{{ t('Save') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppInput, AppModal, AppSelect, AppSwitch } from '@/components/app'
|
||||
import { AppCard, 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'
|
||||
@@ -247,7 +250,7 @@ const loadTiers = async () => {
|
||||
}))
|
||||
}
|
||||
if (!formData.storageType && tiers.value.length > 0) {
|
||||
formData.storageType = tiers.value[0].value
|
||||
formData.storageType = tiers.value[0]?.value ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,11 +330,12 @@ const buildFilter = () => {
|
||||
}
|
||||
|
||||
const filter: any = {}
|
||||
const [firstTag] = validTags
|
||||
|
||||
if (validTags.length === 1 && !formData.prefix) {
|
||||
if (firstTag && validTags.length === 1 && !formData.prefix) {
|
||||
filter.Tag = {
|
||||
Key: validTags[0].key,
|
||||
Value: validTags[0].value,
|
||||
Key: firstTag.key,
|
||||
Value: firstTag.value,
|
||||
}
|
||||
} else if (validTags.length > 0) {
|
||||
filter.And = {
|
||||
@@ -347,11 +351,11 @@ const buildFilter = () => {
|
||||
filter.Prefix = formData.prefix
|
||||
}
|
||||
|
||||
if (formData.prefix && validTags.length === 1 && !filter.And) {
|
||||
if (formData.prefix && firstTag && validTags.length === 1 && !filter.And) {
|
||||
filter.Prefix = formData.prefix
|
||||
filter.Tag = {
|
||||
Key: validTags[0].key,
|
||||
Value: validTags[0].value,
|
||||
Key: firstTag.key,
|
||||
Value: firstTag.value,
|
||||
}
|
||||
} else if (formData.prefix && validTags.length === 0) {
|
||||
filter.Prefix = formData.prefix
|
||||
@@ -440,4 +444,4 @@ onMounted(() => {
|
||||
loadTiers()
|
||||
loadVersioningStatus()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
@@ -2,9 +2,9 @@
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="truncate text-sm font-medium text-foreground">{{ task.key }}</div>
|
||||
<AppButton variant="ghost" size="xs" class="h-auto px-2 text-xs" @click="handleDeleteTask">
|
||||
<Button variant="ghost" size="sm" class="h-auto px-2 text-xs" @click="handleDeleteTask">
|
||||
{{ t('Delete Record') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
<AppProgress :value="task.progress" :processing="task.status === 'deleting'" :height="2" />
|
||||
<div class="flex items-center justify-between text-muted-foreground">
|
||||
@@ -21,7 +21,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppProgress } from '@/components/app'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppProgress } from '@/components/app'
|
||||
import type { DeleteTask } from '~/lib/delete-task-manager'
|
||||
import { useDeleteTaskManagerStore } from '~/store/delete-tasks'
|
||||
|
||||
@@ -33,4 +35,4 @@ const props = defineProps<{ task: DeleteTask }>()
|
||||
const handleDeleteTask = () => {
|
||||
store.removeTask(props.task.id)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
+52
-43
@@ -2,23 +2,23 @@
|
||||
<AppDrawer v-model="visible" :title="t('Object Details')" size="lg">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<AppButton variant="outline" size="sm" @click="download">
|
||||
<Button variant="outline" size="sm" @click="download">
|
||||
<Icon name="ri:download-line" class="size-4" />
|
||||
{{ t('Download') }}
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" @click="() => (showPreview = true)">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="() => (showPreview = true)">
|
||||
<Icon name="ri:eye-line" class="size-4" />
|
||||
{{ t('Preview') }}
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" @click="() => (showTagView = true)">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="() => (showTagView = true)">
|
||||
<Icon name="ri:price-tag-3-line" class="size-4" />
|
||||
{{ t('Set Tags') }}
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" @click="geVersions">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="viewVersions">
|
||||
<Icon name="ri:file-list-2-line" class="size-4" />
|
||||
{{ t('Versions') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
</Button>
|
||||
<Button
|
||||
v-if="lockStatus"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -26,7 +26,7 @@
|
||||
>
|
||||
<Icon name="ri:lock-line" class="size-4" />
|
||||
{{ t('Retention') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AppCard :title="t('Info')" padded>
|
||||
@@ -63,10 +63,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<AppInput v-model="signedUrl" id="signedUrl" :placeholder="t('Temporary URL')" />
|
||||
<AppButton variant="outline" size="sm" :disabled="!object?.Key" @click="copyTemporaryUrl">
|
||||
<Input v-model="signedUrl" :placeholder="t('Temporary URL')" readonly />
|
||||
<Button variant="outline" size="sm" :disabled="!object?.Key" @click="copyTemporaryUrl">
|
||||
{{ t('Copy') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
@@ -80,9 +80,9 @@
|
||||
</AppTag>
|
||||
</div>
|
||||
<form class="flex flex-wrap gap-3" @submit.prevent="submitTagForm">
|
||||
<AppInput v-model="tagFormValue.Key" :placeholder="t('Tag Key Placeholder')" />
|
||||
<AppInput v-model="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
|
||||
<AppButton type="submit" variant="primary">{{ t('Add') }}</AppButton>
|
||||
<Input v-model="tagFormValue.Key" :placeholder="t('Tag Key Placeholder')" />
|
||||
<Input v-model="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
|
||||
<Button type="submit" variant="default">{{ t('Add') }}</Button>
|
||||
</form>
|
||||
</AppCard>
|
||||
</AppModal>
|
||||
@@ -102,12 +102,12 @@
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label>{{ t('Retention RetainUntilDate') }}</Label>
|
||||
<AppInput v-model="retainUntilDate" type="datetime-local" />
|
||||
<Input v-model="retainUntilDate" type="datetime-local" />
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="secondary" @click="resetRetention">{{ t('Reset') }}</AppButton>
|
||||
<AppButton type="submit" variant="primary">{{ t('Confirm') }}</AppButton>
|
||||
<AppButton variant="outline" @click="() => (showRetentionView = false)">{{ t('Cancel') }}</AppButton>
|
||||
<Button variant="secondary" @click="resetRetention">{{ t('Reset') }}</Button>
|
||||
<Button type="submit" variant="default">{{ t('Confirm') }}</Button>
|
||||
<Button variant="outline" @click="() => (showRetentionView = false)">{{ t('Cancel') }}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</AppCard>
|
||||
@@ -118,17 +118,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppDrawer, AppInput, AppModal, AppRadioGroup, AppSwitch, AppTag } from '@/components/app'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard, AppDrawer, AppModal, AppRadioGroup, AppSwitch, AppTag } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, computed } from 'vue'
|
||||
import ClipboardJS from 'clipboard'
|
||||
import { ref } from 'vue'
|
||||
import { joinRelativeURL } from 'ufo'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
const props = defineProps<{
|
||||
bucketName: string
|
||||
@@ -140,7 +140,7 @@ const tags = ref<Array<{ Key: string; Value: string }>>([])
|
||||
const lockStatus = ref(false)
|
||||
const retention = ref('')
|
||||
const retainUntilDate = ref('')
|
||||
const retentionMode = ref('GOVERNANCE')
|
||||
const retentionMode = ref<'COMPLIANCE' | 'GOVERNANCE'>('GOVERNANCE')
|
||||
const signedUrl = ref('')
|
||||
const showTagView = ref(false)
|
||||
const showRetentionView = ref(false)
|
||||
@@ -148,8 +148,15 @@ const showPreview = ref(false)
|
||||
|
||||
const tagFormValue = ref({ Key: '', Value: '' })
|
||||
|
||||
const { getObjectInfo, setLegalHold, putObjectTags, getObjectTags, getObjectRetention, putObjectRetention, geObjectVersions } =
|
||||
useObject({ bucket: props.bucketName })
|
||||
const {
|
||||
getObjectInfo,
|
||||
setLegalHold,
|
||||
putObjectTags,
|
||||
getObjectTags,
|
||||
getObjectRetention,
|
||||
putObjectRetention,
|
||||
getObjectVersions,
|
||||
} = useObject({ bucket: props.bucketName })
|
||||
|
||||
const openDrawer = async (bucket: string, key: string) => {
|
||||
visible.value = true
|
||||
@@ -168,7 +175,7 @@ const openDrawer = async (bucket: string, key: string) => {
|
||||
const fetchTags = async (key: string) => {
|
||||
try {
|
||||
const response = await getObjectTags(key)
|
||||
tags.value = response.TagSet || []
|
||||
tags.value = response
|
||||
} catch (error) {
|
||||
tags.value = []
|
||||
}
|
||||
@@ -177,8 +184,9 @@ const fetchTags = async (key: string) => {
|
||||
const fetchRetention = async (key: string) => {
|
||||
try {
|
||||
const response = await getObjectRetention(key)
|
||||
retention.value = response.Mode || ''
|
||||
retainUntilDate.value = response.RetainUntilDate || ''
|
||||
retention.value = response.Mode ?? ''
|
||||
retainUntilDate.value = response.RetainUntilDate ?? ''
|
||||
retentionMode.value = response.Mode === 'COMPLIANCE' ? 'COMPLIANCE' : 'GOVERNANCE'
|
||||
} catch (error) {
|
||||
retention.value = ''
|
||||
retainUntilDate.value = ''
|
||||
@@ -219,7 +227,7 @@ const submitRetention = async () => {
|
||||
try {
|
||||
await putObjectRetention(object.value.Key, {
|
||||
Mode: retentionMode.value,
|
||||
RetainUntilDate: retainUntilDate.value,
|
||||
RetainUntilDate: retainUntilDate.value || undefined,
|
||||
})
|
||||
message.success(t('Update Success'))
|
||||
showRetentionView.value = false
|
||||
@@ -232,7 +240,7 @@ const submitRetention = async () => {
|
||||
const resetRetention = async () => {
|
||||
if (!object.value) return
|
||||
try {
|
||||
await putObjectRetention(object.value.Key, { Mode: 'GOVERNANCE', RetainUntilDate: '' })
|
||||
await putObjectRetention(object.value.Key, { Mode: 'GOVERNANCE' })
|
||||
message.success(t('Update Success'))
|
||||
fetchRetention(object.value.Key)
|
||||
} catch (error: any) {
|
||||
@@ -240,9 +248,9 @@ const resetRetention = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const geVersions = async () => {
|
||||
const viewVersions = async () => {
|
||||
if (!object.value) return
|
||||
await geObjectVersions(object.value.Key)
|
||||
await getObjectVersions(object.value.Key)
|
||||
}
|
||||
|
||||
const download = async () => {
|
||||
@@ -251,16 +259,17 @@ const download = async () => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const copyTemporaryUrl = () => {
|
||||
const clipboard = new ClipboardJS('#signedUrl', {
|
||||
text: () => signedUrl.value,
|
||||
})
|
||||
clipboard.on('success', () => message.success(t('Copy Success')))
|
||||
clipboard.on('error', () => message.error(t('Copy Failed')))
|
||||
clipboard.destroy()
|
||||
const copyTemporaryUrl = async () => {
|
||||
if (!signedUrl.value) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(signedUrl.value)
|
||||
message.success(t('Copy Success'))
|
||||
} catch {
|
||||
message.error(t('Copy Failed'))
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openDrawer,
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
+72
-50
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<PageHeader class="sticky top-0 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<PageHeader class="sticky top-0 z-10 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<AppInput
|
||||
<Input
|
||||
v-model="searchTerm"
|
||||
:placeholder="t('Search')"
|
||||
class="max-w-md"
|
||||
@@ -11,15 +11,15 @@
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<object-upload-stats />
|
||||
<object-delete-stats />
|
||||
<AppButton variant="secondary" @click="() => handleNewObject(true)">
|
||||
<Button variant="secondary" @click="() => handleNewObject(true)">
|
||||
<Icon name="ri:add-line" class="size-4" />
|
||||
<span>{{ t('New Folder') }}</span>
|
||||
</AppButton>
|
||||
<AppButton variant="secondary" @click="() => (uploadPickerVisible = true)">
|
||||
</Button>
|
||||
<Button variant="secondary" @click="() => (uploadPickerVisible = true)">
|
||||
<Icon name="ri:file-add-line" class="size-4" />
|
||||
<span>{{ t('Upload File') }}/{{ t('Folder') }}</span>
|
||||
</AppButton>
|
||||
<AppButton
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
:disabled="!checkedKeys.length"
|
||||
v-show="checkedKeys.length"
|
||||
@@ -27,8 +27,8 @@
|
||||
>
|
||||
<Icon name="ri:delete-bin-5-line" class="size-4" />
|
||||
<span>{{ t('Delete Selected') }}</span>
|
||||
</AppButton>
|
||||
<AppButton
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="!checkedKeys.length"
|
||||
v-show="checkedKeys.length"
|
||||
@@ -36,16 +36,16 @@
|
||||
>
|
||||
<Icon name="ri:download-cloud-2-line" class="size-4" />
|
||||
<span>{{ t('Download') }}</span>
|
||||
</AppButton>
|
||||
<AppButton variant="outline" @click="refresh">
|
||||
</Button>
|
||||
<Button variant="outline" @click="handleRefresh">
|
||||
<Icon name="ri:refresh-line" class="size-4" />
|
||||
<span>{{ t('Refresh') }}</span>
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<AppCard padded class="border border-border/60">
|
||||
<AppCard padded class="border">
|
||||
<AppDataTable
|
||||
:table="table"
|
||||
:is-loading="loading"
|
||||
@@ -55,14 +55,14 @@
|
||||
</AppCard>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" :disabled="!continuationToken" @click="goToPreviousPage">
|
||||
<Button variant="outline" :disabled="!continuationToken" @click="goToPreviousPage">
|
||||
<Icon name="ri:arrow-left-s-line" class="mr-2" />
|
||||
<span>{{ t('Previous Page') }}</span>
|
||||
</AppButton>
|
||||
<AppButton variant="outline" :disabled="!nextToken" @click="goToNextPage">
|
||||
</Button>
|
||||
<Button variant="outline" :disabled="!nextToken" @click="goToNextPage">
|
||||
<span>{{ t('Next Page') }}</span>
|
||||
<Icon name="ri:arrow-right-s-line" class="ml-2" />
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<object-upload-picker
|
||||
@@ -84,26 +84,36 @@
|
||||
refresh()
|
||||
}"
|
||||
/>
|
||||
<object-info ref="infoRef" @refresh-parent="handleObjectDeleted" />
|
||||
<object-info
|
||||
ref="infoRef"
|
||||
:bucket-name="bucketName"
|
||||
@refresh-parent="handleObjectDeleted"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { icon } from '@/utils/ui'
|
||||
import { AppButton, AppCard, AppDataTable, AppInput } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard } from '@/components/app'
|
||||
import AppDataTable from '@/components/app/data-table/AppDataTable.vue'
|
||||
import { useDataTable } from '@/components/app/data-table'
|
||||
import PageHeader from '@/components/page/header.vue'
|
||||
import { ListObjectsV2Command, type CommonPrefix, type _Object } from '@aws-sdk/client-s3'
|
||||
import { ListObjectsV2Command } from '@aws-sdk/client-s3'
|
||||
import dayjs from 'dayjs'
|
||||
import { saveAs } from 'file-saver'
|
||||
import JSZip from 'jszip'
|
||||
import { joinRelativeURL } from 'ufo'
|
||||
import { computed, h, ref, watch } from 'vue'
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
import { useDeleteTaskManagerStore } from '~/store/delete-tasks'
|
||||
import { Icon, NuxtLink } from '#components'
|
||||
|
||||
const { $s3Client } = useNuxtApp()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const dialog = useDialog()
|
||||
const message = useMessage()
|
||||
const props = defineProps<{ bucket: string; path: string }>()
|
||||
@@ -133,6 +143,13 @@ const handleSearch = debounce(() => {
|
||||
const uploadTaskStore = useUploadTaskManagerStore()
|
||||
const deleteTaskStore = useDeleteTaskManagerStore()
|
||||
|
||||
type ObjectRow = {
|
||||
Key: string
|
||||
type: 'prefix' | 'object'
|
||||
Size: number
|
||||
LastModified: string
|
||||
}
|
||||
|
||||
const bucketName = computed(() => props.bucket)
|
||||
const prefix = computed(() => decodeURIComponent(props.path))
|
||||
|
||||
@@ -156,7 +173,7 @@ const handleObjectDeleted = () => {
|
||||
refresh()
|
||||
}
|
||||
|
||||
const fetchObjects = async () => {
|
||||
const fetchObjects = async (): Promise<ObjectRow[]> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
@@ -169,14 +186,14 @@ const fetchObjects = async () => {
|
||||
const response = await $s3Client.send(new ListObjectsV2Command(params))
|
||||
nextToken.value = response.NextContinuationToken
|
||||
|
||||
const prefixData = (response.CommonPrefixes ?? []).map(item => ({
|
||||
const prefixData: ObjectRow[] = (response.CommonPrefixes ?? []).map(item => ({
|
||||
Key: item.Prefix ?? '',
|
||||
type: 'prefix' as const,
|
||||
Size: 0,
|
||||
LastModified: '',
|
||||
}))
|
||||
|
||||
const objectData = (response.Contents ?? []).map(item => ({
|
||||
const objectData: ObjectRow[] = (response.Contents ?? []).map(item => ({
|
||||
Key: item.Key ?? '',
|
||||
type: 'object' as const,
|
||||
Size: item.Size ?? 0,
|
||||
@@ -189,20 +206,23 @@ const fetchObjects = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const { data, refresh } = await useAsyncData(
|
||||
() => ['objects', bucketName.value, prefix.value, continuationToken.value, searchTerm.value],
|
||||
const { data, refresh } = await useAsyncData<ObjectRow[]>(
|
||||
'objects',
|
||||
async () => {
|
||||
const objects = await fetchObjects()
|
||||
if (!searchTerm.value) return objects
|
||||
const term = searchTerm.value.toLowerCase()
|
||||
return objects.filter(item => item.Key.toLowerCase().includes(term))
|
||||
},
|
||||
{ default: () => [] }
|
||||
{
|
||||
default: () => [],
|
||||
watch: [bucketName, prefix, continuationToken, searchTerm],
|
||||
}
|
||||
)
|
||||
|
||||
const checkedKeys = ref<string[]>([])
|
||||
|
||||
const columns = computed(() => {
|
||||
const columns = computed<ColumnDef<ObjectRow, any>[]>(() => {
|
||||
return [
|
||||
{
|
||||
id: 'select',
|
||||
@@ -222,6 +242,7 @@ const columns = computed(() => {
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'object',
|
||||
header: () => t('Object'),
|
||||
cell: ({ row }: any) => {
|
||||
const displayKey = prefix.value ? row.original.Key.substring(prefix.value.length) : row.original.Key
|
||||
@@ -247,10 +268,12 @@ const columns = computed(() => {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
header: () => t('Size'),
|
||||
cell: ({ row }: any) => (row.original.type === 'object' ? formatBytes(row.original.Size) : ''),
|
||||
},
|
||||
{
|
||||
id: 'lastModified',
|
||||
header: () => t('Last Modified'),
|
||||
cell: ({ row }: any) => (row.original.LastModified ? dayjs(row.original.LastModified).format('YYYY-MM-DD HH:mm:ss') : ''),
|
||||
},
|
||||
@@ -262,7 +285,7 @@ const columns = computed(() => {
|
||||
h('div', { class: 'flex justify-center gap-2' }, [
|
||||
row.original.type === 'object'
|
||||
? h(
|
||||
AppButton,
|
||||
Button,
|
||||
{
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
@@ -272,7 +295,7 @@ const columns = computed(() => {
|
||||
)
|
||||
: null,
|
||||
h(
|
||||
AppButton,
|
||||
Button,
|
||||
{
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
@@ -285,31 +308,29 @@ const columns = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const { table } = useDataTable({
|
||||
const { table } = useDataTable<ObjectRow>({
|
||||
data,
|
||||
columns,
|
||||
getRowId: (row: any) => row.Key,
|
||||
state: computed(() => ({
|
||||
rowSelection: checkedKeys.value.reduce((acc: Record<string, boolean>, key) => {
|
||||
acc[key] = true
|
||||
return acc
|
||||
}, {}),
|
||||
})),
|
||||
onRowSelectionChange: updater => {
|
||||
const next = typeof updater === 'function' ? updater({}) : updater
|
||||
checkedKeys.value = Object.keys(next).filter(key => next[key])
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => table.getState().rowSelection,
|
||||
selection => {
|
||||
checkedKeys.value = Object.keys(selection).filter(key => selection[key])
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => uploadTaskStore.tasks,
|
||||
() => setTimeout(refresh, 500),
|
||||
() => setTimeout(() => refresh(), 500),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => deleteTaskStore.tasks,
|
||||
() => setTimeout(refresh, 500),
|
||||
() => setTimeout(() => refresh(), 500),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
@@ -328,6 +349,10 @@ const goToPreviousPage = () => {
|
||||
refresh()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
refresh()
|
||||
}
|
||||
|
||||
const downloadFile = async (key: string) => {
|
||||
const url = bucketPath(['download', key])
|
||||
window.open(url, '_blank')
|
||||
@@ -369,12 +394,9 @@ const confirmDelete = (keys: string[]) => {
|
||||
}
|
||||
|
||||
const handleDelete = async (keys: string[]) => {
|
||||
const bucket = bucketName.value
|
||||
const taskStore = useDeleteTaskManagerStore()
|
||||
for (const key of keys) {
|
||||
await taskStore.addTask({ bucket, key })
|
||||
}
|
||||
deleteTaskStore.addKeys(keys, bucketName.value, prefix.value)
|
||||
checkedKeys.value = []
|
||||
table.resetRowSelection()
|
||||
refresh()
|
||||
}
|
||||
|
||||
@@ -389,4 +411,4 @@ const formatBytes = (bytes: number) => {
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
||||
return `${(bytes / 1024 ** i).toFixed(2)} ${units[i]}`
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -5,27 +5,30 @@
|
||||
<AlertDescription>{{ t('Overwrite Warning') }}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<AppInput
|
||||
<Input
|
||||
v-model="objectKey"
|
||||
:placeholder="t('Name Placeholder', { type: displayType })"
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal">{{ t('Close') }}</AppButton>
|
||||
<AppButton variant="primary" :disabled="objectKey.trim().length < 1" @click="handlePutObject">
|
||||
<Button variant="outline" @click="closeModal">{{ t('Close') }}</Button>
|
||||
<Button variant="default" :disabled="objectKey.trim().length < 1" @click="handlePutObject">
|
||||
{{ t('Create') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { joinRelativeURL } from 'ufo'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { AppButton, AppInput, AppModal } from '@/components/app'
|
||||
import { AppModal } from '@/components/app'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -66,4 +69,4 @@ const handlePutObject = async () => {
|
||||
$message.error(error?.message || t('Create Failed'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -2,19 +2,19 @@
|
||||
<AppModal v-model="visibleProxy" :title="t('Preview')" size="xl" :close-on-backdrop="false">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<AppButton variant="outline" size="sm" @click="closeModal">
|
||||
<Button variant="outline" size="sm" @click="closeModal">
|
||||
<Icon name="ri:close-line" class="size-4" />
|
||||
{{ t('Close') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="min-h-[300px] rounded-md border border-border/60 p-4">
|
||||
<div class="min-h-[300px] rounded-md border p-4">
|
||||
<AppSpinner v-if="loading" class="mx-auto size-8" />
|
||||
<template v-else>
|
||||
<div v-if="isImage" class="flex justify-center">
|
||||
<img :src="previewUrl" alt="preview" class="max-h-[60vh]" />
|
||||
</div>
|
||||
<iframe v-else-if="isPdf" :src="previewUrl" class="h-[70vh] w-full" frameborder="0" />
|
||||
<pre v-else-if="isText" class="max-h-[70vh] overflow-auto whitespace-pre-wrap break-words">{{ textContent }}</pre>
|
||||
<pre v-else-if="isText" class="max-h-[70vh] overflow-auto whitespace-pre-wrap wrap-break-word">{{ textContent }}</pre>
|
||||
<video v-else-if="isVideo" controls class="w-full">
|
||||
<source :src="previewUrl" :type="contentType" />
|
||||
{{ t('Your browser does not support the video tag') }}
|
||||
@@ -33,7 +33,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppModal, AppSpinner } from '@/components/app'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppModal, AppSpinner } from '@/components/app'
|
||||
import { Icon } from '#components'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -119,4 +121,4 @@ const reset = () => {
|
||||
}
|
||||
|
||||
const closeModal = () => emit('update:show', false)
|
||||
</script>
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AppButton
|
||||
<Button
|
||||
v-if="total > 0"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -21,7 +21,7 @@
|
||||
<div v-else>
|
||||
{{ t('Task Completed', { completed: completed.length, failed: failed.length }) }}
|
||||
</div>
|
||||
</AppButton>
|
||||
</Button>
|
||||
|
||||
<AppDrawer v-model="showDrawer" :title="t('Task Management')" size="lg">
|
||||
<div class="flex flex-col gap-4">
|
||||
@@ -41,9 +41,9 @@
|
||||
<div v-if="total > 0" class="space-y-3">
|
||||
<div class="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<div>{{ total - (pending.length + processing.length) }}/{{ total }}</div>
|
||||
<AppButton variant="ghost" size="sm" class="h-auto px-2" @click="clearTasks">
|
||||
<Button variant="ghost" size="sm" class="h-auto px-2" @click="clearTasks">
|
||||
{{ t('Clear Records') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
<AppProgress :value="percentage" :processing="percentage < 100" :height="3" />
|
||||
</div>
|
||||
@@ -76,7 +76,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppDrawer, AppProgress, AppSpinner } from '@/components/app'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppDrawer, AppProgress, AppSpinner } from '@/components/app'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
@@ -147,4 +149,4 @@ const toggleDrawer = () => {
|
||||
const clearTasks = () => {
|
||||
props.onClearTasks()
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -21,18 +21,18 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<AppButton variant="outline" size="sm" :disabled="isFolderLoading" @click="selectFile">
|
||||
<Button variant="outline" size="sm" :disabled="isFolderLoading" @click="selectFile">
|
||||
<Icon name="ri:file-add-line" class="size-4" />
|
||||
{{ t('Select File') }}
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" :disabled="isFolderLoading" @click="selectFolder">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" :disabled="isFolderLoading" @click="selectFolder">
|
||||
<Icon name="ri:folder-add-line" class="size-4" />
|
||||
{{ t('Select Folder') }}
|
||||
</AppButton>
|
||||
<AppButton variant="secondary" size="sm" @click="closeModal">
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" @click="closeModal">
|
||||
<Icon name="ri:close-line" class="size-4" />
|
||||
{{ t('Close') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<AppCard padded class="space-y-4 border border-border/60">
|
||||
<AppCard padded class="space-y-4 border">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="space-y-1 text-sm text-muted-foreground">
|
||||
<p v-if="totalFileCount > 0">
|
||||
@@ -61,7 +61,7 @@
|
||||
{{ t('Memory Usage') }}: {{ getMemoryUsageLevel() }}
|
||||
</p>
|
||||
</div>
|
||||
<AppButton
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!hasFiles || isFolderLoading || isAdding"
|
||||
@@ -69,11 +69,11 @@
|
||||
>
|
||||
<Icon name="ri:delete-bin-line" class="size-4" />
|
||||
{{ t('Clear All') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-md border border-dashed border-border/60 transition"
|
||||
class="rounded-md border border-dashed transition"
|
||||
:class="isDragOver ? 'border-primary bg-primary/5' : ''"
|
||||
@dragenter.prevent="handleDragEnter"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@@ -128,10 +128,10 @@
|
||||
{{ formatBytes(item.size) }}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-right">
|
||||
<AppButton variant="ghost" size="sm" class="h-auto px-2" @click="removeItem(index)">
|
||||
<Button variant="ghost" size="sm" class="h-auto px-2" @click="removeItem(index)">
|
||||
<Icon name="ri:delete-bin-line" class="size-4" />
|
||||
{{ t('Delete') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -139,7 +139,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isFolderLoading" class="space-y-2 rounded-md border border-dashed border-border/60 p-3">
|
||||
<div v-if="isFolderLoading" class="space-y-2 rounded-md border border-dashed p-3">
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ t('Reading Folder Files') }}</span>
|
||||
<span>{{ folderLoadingProgress }}%</span>
|
||||
@@ -147,7 +147,7 @@
|
||||
<AppProgress :value="folderLoadingProgress" :processing="true" />
|
||||
</div>
|
||||
|
||||
<div v-if="isAdding" class="space-y-2 rounded-md border border-dashed border-border/60 p-3">
|
||||
<div v-if="isAdding" class="space-y-2 rounded-md border border-dashed p-3">
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ t('Adding to Upload Queue') }}</span>
|
||||
<span>{{ addProgress }}%</span>
|
||||
@@ -157,25 +157,27 @@
|
||||
</AppCard>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" :disabled="!hasFiles || isAdding || isFolderLoading">
|
||||
<Button variant="outline" :disabled="!hasFiles || isAdding || isFolderLoading">
|
||||
{{ t('Configure') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
variant="primary"
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
:disabled="!hasFiles || isAdding || isFolderLoading"
|
||||
:loading="isAdding"
|
||||
@click="handleUpload"
|
||||
>
|
||||
{{ t('Start Upload') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppModal, AppProgress } from '@/components/app'
|
||||
import { AppCard, AppModal, AppProgress } from '@/components/app'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -3,27 +3,27 @@
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="truncate text-sm font-medium text-foreground">{{ task.file.name }}</div>
|
||||
<div class="flex shrink-0 items-center gap-1.5">
|
||||
<AppButton
|
||||
<Button
|
||||
v-if="task.status === 'uploading'"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
size="sm"
|
||||
class="h-auto px-2 text-xs"
|
||||
@click="handlePauseTask"
|
||||
>
|
||||
{{ t('Pause') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
</Button>
|
||||
<Button
|
||||
v-if="task.status === 'paused'"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
size="sm"
|
||||
class="h-auto px-2 text-xs"
|
||||
@click="handleResumeTask"
|
||||
>
|
||||
{{ t('Resume') }}
|
||||
</AppButton>
|
||||
<AppButton variant="ghost" size="xs" class="h-auto px-2 text-xs" @click="handleDeleteTask">
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-auto px-2 text-xs" @click="handleDeleteTask">
|
||||
{{ t('Delete Record') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AppProgress :value="task.progress" :processing="task.status === 'uploading'" :height="2" />
|
||||
@@ -41,7 +41,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppProgress } from '@/components/app'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppProgress } from '@/components/app'
|
||||
import type { UploadTask } from '~/lib/upload-task-manager'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
import { formatBytes } from '~/utils/functions'
|
||||
|
||||
@@ -3,21 +3,24 @@
|
||||
<AppCard padded>
|
||||
<AppDataTable :table="table" :is-loading="loading" :empty-title="t('No Versions')" />
|
||||
<div class="mt-4 flex justify-end">
|
||||
<AppButton variant="outline" @click="closeModal">{{ t('Close') }}</AppButton>
|
||||
<Button variant="outline" @click="closeModal">{{ t('Close') }}</Button>
|
||||
</div>
|
||||
</AppCard>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { AppButton, AppCard, AppDataTable, AppModal } from '@/components/app'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard, AppModal } from '@/components/app'
|
||||
import AppDataTable from '@/components/app/data-table/AppDataTable.vue'
|
||||
import { useDataTable } from '@/components/app/data-table'
|
||||
import { GetObjectCommand } from '@aws-sdk/client-s3'
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
||||
import dayjs from 'dayjs'
|
||||
import { saveAs } from 'file-saver'
|
||||
import JSZip from 'jszip'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
const props = defineProps<{
|
||||
bucketName: string
|
||||
@@ -33,6 +36,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const ActionButton = Button as unknown as any
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
@@ -44,16 +48,21 @@ const visibleProxy = computed({
|
||||
const versions = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const columns = computed(() => [
|
||||
const objectApi = useObject({ bucket: props.bucketName })
|
||||
|
||||
const columns = computed<ColumnDef<any, any>[]>(() => [
|
||||
{
|
||||
id: 'versionId',
|
||||
header: () => t('VersionId'),
|
||||
cell: ({ row }: any) => shortVersionId(row.original.VersionId),
|
||||
},
|
||||
{
|
||||
id: 'lastModified',
|
||||
header: () => t('LastModified'),
|
||||
cell: ({ row }: any) => (row.original.LastModified ? dayjs(row.original.LastModified).format('YYYY-MM-DD HH:mm:ss') : ''),
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
header: () => t('Size'),
|
||||
cell: ({ row }: any) => (typeof row.original.Size === 'number' ? formatBytes(row.original.Size) : ''),
|
||||
},
|
||||
@@ -62,15 +71,15 @@ const columns = computed(() => [
|
||||
header: () => t('Action'),
|
||||
cell: ({ row }: any) => (
|
||||
<div class="flex gap-2">
|
||||
<AppButton variant="outline" size="sm" onClick={() => previewVersion(row.original)}>
|
||||
<ActionButton variant="outline" size="sm" onClick={() => previewVersion(row.original)}>
|
||||
{t('Preview')}
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" onClick={() => downloadVersion(row.original)}>
|
||||
</ActionButton>
|
||||
<ActionButton variant="outline" size="sm" onClick={() => downloadVersion(row.original)}>
|
||||
{t('Download')}
|
||||
</AppButton>
|
||||
<AppButton variant="destructive" size="sm" onClick={() => deleteVersion(row.original)}>
|
||||
</ActionButton>
|
||||
<ActionButton variant="destructive" size="sm" onClick={() => deleteVersion(row.original)}>
|
||||
{t('Delete')}
|
||||
</AppButton>
|
||||
</ActionButton>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -90,9 +99,8 @@ watch(
|
||||
|
||||
const fetchVersions = async () => {
|
||||
loading.value = true
|
||||
const { getObjectVersions } = useObject({ bucket: props.bucketName })
|
||||
try {
|
||||
const response = await getObjectVersions(props.objectKey)
|
||||
const response = await objectApi.getObjectVersions(props.objectKey)
|
||||
versions.value = response.Versions ?? []
|
||||
} catch (error) {
|
||||
message.error(t('Failed to fetch versions'))
|
||||
@@ -124,9 +132,8 @@ const downloadVersion = async (row: any) => {
|
||||
}
|
||||
|
||||
const deleteVersion = async (row: any) => {
|
||||
const { deleteObject } = useObject({ bucket: props.bucketName })
|
||||
try {
|
||||
await deleteObject(props.objectKey, row.VersionId)
|
||||
await objectApi.deleteObject(props.objectKey, row.VersionId)
|
||||
message.success(t('Delete Success'))
|
||||
fetchVersions()
|
||||
emit('refresh-parent')
|
||||
|
||||
+24
-21
@@ -4,38 +4,38 @@
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<AppButton variant="ghost" size="sm" @click="router.back">
|
||||
<Button variant="ghost" size="sm" @click="router.back">
|
||||
<Icon name="ri:arrow-left-line" class="size-4" />
|
||||
</AppButton>
|
||||
</Button>
|
||||
<h2 class="text-lg font-semibold">{{ object?.Key }}</h2>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">{{ t('Object Detail Description', { bucket: bucketName }) }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<AppButton variant="outline" size="sm" @click="download">
|
||||
<Button variant="outline" size="sm" @click="download">
|
||||
<Icon name="ri:download-line" class="size-4" />
|
||||
{{ t('Download') }}
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" @click="copySignedUrl">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="copySignedUrl">
|
||||
<Icon name="ri:file-copy-line" class="size-4" />
|
||||
{{ t('Copy Temporary URL') }}
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" @click="() => (showPreview = true)">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="() => (showPreview = true)">
|
||||
<Icon name="ri:eye-line" class="size-4" />
|
||||
{{ t('Preview') }}
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" @click="() => (showTagView = true)">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="() => (showTagView = true)">
|
||||
<Icon name="ri:price-tag-3-line" class="size-4" />
|
||||
{{ t('Set Tags') }}
|
||||
</AppButton>
|
||||
<AppButton variant="destructive" size="sm" @click="confirmDelete">
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" @click="confirmDelete">
|
||||
<Icon name="ri:delete-bin-5-line" class="size-4" />
|
||||
{{ t('Delete') }}
|
||||
</AppButton>
|
||||
<AppButton variant="outline" size="sm" @click="refresh">
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="refresh">
|
||||
<Icon name="ri:refresh-line" class="size-4" />
|
||||
{{ t('Refresh') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
@@ -71,10 +71,10 @@
|
||||
</AppTag>
|
||||
</div>
|
||||
<form class="flex flex-wrap gap-3" @submit.prevent="submitTagForm">
|
||||
<AppInput v-model="tagFormValue.Key" :placeholder="t('Tag Key Placeholder')" />
|
||||
<AppInput v-model="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
|
||||
<AppButton type="submit" variant="primary">{{ t('Add') }}</AppButton>
|
||||
<AppButton variant="outline" @click="showTagView = false">{{ t('Cancel') }}</AppButton>
|
||||
<Input v-model="tagFormValue.Key" :placeholder="t('Tag Key Placeholder')" />
|
||||
<Input v-model="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
|
||||
<Button type="submit" variant="default">{{ t('Add') }}</Button>
|
||||
<Button variant="outline" @click="showTagView = false">{{ t('Cancel') }}</Button>
|
||||
</form>
|
||||
</AppCard>
|
||||
</AppModal>
|
||||
@@ -84,8 +84,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppInput, AppModal, AppTag } from '@/components/app'
|
||||
import { AppCard, AppModal, AppTag } from '@/components/app'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -114,7 +117,7 @@ const loadObjectInfo = async () => {
|
||||
object.value = await getObjectInfo(props.objectKey)
|
||||
signedUrl.value = object.value?.SignedUrl || ''
|
||||
const response = await getObjectTags(props.objectKey)
|
||||
tags.value = response.TagSet || []
|
||||
tags.value = response
|
||||
} catch (error) {
|
||||
message.error(t('Failed to fetch object info'))
|
||||
}
|
||||
@@ -181,4 +184,4 @@ const download = () => {
|
||||
}
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="p-4">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard, AppModal } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -120,13 +123,13 @@ const submitForm = async () => {
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="policy-name">{{ t('Policy Name') }}</Label>
|
||||
<AppInput id="policy-name" v-model="form.name" autocomplete="off" />
|
||||
<Input id="policy-name" v-model="form.name" autocomplete="off" />
|
||||
<p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="policy-content">{{ t('Policy Original') }}</Label>
|
||||
<div class="max-h-[60vh] overflow-auto rounded-md border border-border/60">
|
||||
<div class="max-h-[60vh] overflow-auto rounded-md border">
|
||||
<json-editor id="policy-content" v-model="form.content" />
|
||||
</div>
|
||||
<p v-if="errors.content" class="text-sm text-destructive">{{ errors.content }}</p>
|
||||
@@ -135,9 +138,9 @@ const submitForm = async () => {
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">{{ t('Cancel') }}</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">{{ t('Submit') }}</AppButton>
|
||||
<Button variant="outline" @click="closeModal()">{{ t('Cancel') }}</Button>
|
||||
<Button variant="default" :loading="submitting" @click="submitForm">{{ t('Submit') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
</template>
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Priority') }}</Label>
|
||||
<AppInput v-model="formData.level" type="number" min="1" />
|
||||
<Input v-model="formData.level" type="number" min="1" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Mode') }}</Label>
|
||||
@@ -18,53 +18,53 @@
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Endpoint') }}</Label>
|
||||
<AppInput v-model="formData.endpoint" :placeholder="t('Please enter endpoint')" />
|
||||
<Input v-model="formData.endpoint" :placeholder="t('Please enter endpoint')" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Bucket') }}</Label>
|
||||
<AppInput v-model="formData.bucket" :placeholder="t('Please enter bucket')" />
|
||||
<Input v-model="formData.bucket" :placeholder="t('Please enter bucket')" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Access Key') }}</Label>
|
||||
<AppInput v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
|
||||
<Input v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Secret Key') }}</Label>
|
||||
<AppInput v-model="formData.secrretkey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
|
||||
<Input v-model="formData.secrretkey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Region') }}</Label>
|
||||
<AppInput v-model="formData.region" :placeholder="t('Please enter region')" />
|
||||
<Input v-model="formData.region" :placeholder="t('Please enter region')" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Storage Class') }}</Label>
|
||||
<AppInput v-model="formData.storageType" :placeholder="t('Please enter storage class')" />
|
||||
<Input v-model="formData.storageType" :placeholder="t('Please enter storage class')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Prefix') }}</Label>
|
||||
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
|
||||
<Input 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">
|
||||
<Button variant="outline" size="sm" @click="addTag">
|
||||
<Icon name="ri:add-line" class="size-4" />
|
||||
{{ t('Add Tag') }}
|
||||
</AppButton>
|
||||
</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 border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
|
||||
class="grid gap-2 rounded-md border p-3 md:grid-cols-2 md:items-center md:gap-4"
|
||||
>
|
||||
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
|
||||
<Input 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
|
||||
<Input v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-destructive"
|
||||
@@ -72,7 +72,7 @@
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
<Icon name="ri:delete-bin-line" class="size-4" />
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,7 +107,7 @@
|
||||
<div class="space-y-3" v-if="formData.modeType === 'async'">
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Health Check Interval (seconds)') }}</Label>
|
||||
<AppInput
|
||||
<Input
|
||||
v-model="formData.timecheck"
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -117,7 +117,7 @@
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Bandwidth Limit') }}</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<AppInput v-model="formData.bandwidth" type="number" min="0" class="w-32" />
|
||||
<Input v-model="formData.bandwidth" type="number" min="0" class="w-32" />
|
||||
<AppSelect v-model="formData.unit" :options="unitOptions" class="w-28" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,16 +127,19 @@
|
||||
|
||||
<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>
|
||||
<Button variant="outline" @click="handleCancel">{{ t('Cancel') }}</Button>
|
||||
<Button variant="default" :loading="submitting" @click="handleSave">{{ t('Save') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppInput, AppModal, AppSelect, AppSwitch } from '@/components/app'
|
||||
import { AppCard, AppModal, AppSelect, AppSwitch } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -285,7 +288,6 @@ const handleSave = async () => {
|
||||
ID: `replication-rule-${Date.now()}`,
|
||||
Status: 'Enabled',
|
||||
Priority: Number.parseInt(formData.level) || 1,
|
||||
Filter: {},
|
||||
SourceSelectionCriteria: {
|
||||
SseKmsEncryptedObjects: { Status: 'Enabled' },
|
||||
},
|
||||
@@ -301,18 +303,28 @@ const handleSave = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
const validTags = formData.tags.filter(tag => tag.key && tag.value)
|
||||
const filter: any = {}
|
||||
|
||||
if (formData.prefix) {
|
||||
newRule.Filter.Prefix = formData.prefix
|
||||
filter.Prefix = formData.prefix
|
||||
}
|
||||
|
||||
const validTags = formData.tags.filter(tag => tag.key && tag.value)
|
||||
if (validTags.length === 1) {
|
||||
newRule.Filter.Tag = { Key: validTags[0].key, Value: validTags[0].value }
|
||||
const [singleTag] = validTags
|
||||
if (singleTag) {
|
||||
filter.Tag = { Key: singleTag.key, Value: singleTag.value }
|
||||
}
|
||||
} else if (validTags.length > 1) {
|
||||
newRule.Filter.And = {
|
||||
Prefix: formData.prefix || '',
|
||||
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 ?? []
|
||||
@@ -345,4 +357,4 @@ defineExpose({
|
||||
visible.value = true
|
||||
},
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
@@ -8,26 +8,29 @@
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Access Key') }}</Label>
|
||||
<AppInput v-model="formModel.accessKey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
|
||||
<Input v-model="formModel.accessKey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Secret Key') }}</Label>
|
||||
<AppInput v-model="formModel.secretKey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
|
||||
<Input v-model="formModel.secretKey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">{{ t('Cancel') }}</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">{{ t('Submit') }}</AppButton>
|
||||
<Button variant="outline" @click="closeModal()">{{ t('Cancel') }}</Button>
|
||||
<Button variant="default" :loading="submitting" @click="submitForm">{{ t('Submit') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard, AppModal } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -101,4 +104,4 @@ const submitForm = async () => {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-5">
|
||||
<AppCard class="cursor-pointer border border-border/60 transition hover:border-primary" @click="resetType">
|
||||
<AppCard class="cursor-pointer border transition hover:border-primary" @click="resetType">
|
||||
<div class="flex items-center gap-3">
|
||||
<img :src="iconUrl" class="h-10 w-10" alt="" />
|
||||
<div>
|
||||
@@ -32,7 +32,7 @@
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Name') }} (A-Z,0-9,_)</Label>
|
||||
<AppInput
|
||||
<Input
|
||||
v-model="formData.name"
|
||||
:placeholder="t('Please enter name')"
|
||||
autocomplete="off"
|
||||
@@ -43,37 +43,37 @@
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Endpoint') }}</Label>
|
||||
<AppInput v-model="formData.endpoint" :placeholder="t('Please enter endpoint')" />
|
||||
<Input v-model="formData.endpoint" :placeholder="t('Please enter endpoint')" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Access Key') }}</Label>
|
||||
<AppInput v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
|
||||
<Input v-model="formData.accesskey" :placeholder="t('Please enter Access Key')" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Secret Key') }}</Label>
|
||||
<AppInput v-model="formData.secretkey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
|
||||
<Input v-model="formData.secretkey" type="password" autocomplete="off" :placeholder="t('Please enter Secret Key')" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Bucket') }}</Label>
|
||||
<AppInput v-model="formData.bucket" :placeholder="t('Please enter bucket')" />
|
||||
<Input v-model="formData.bucket" :placeholder="t('Please enter bucket')" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Prefix') }}</Label>
|
||||
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
|
||||
<Input v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Region') }}</Label>
|
||||
<AppInput v-model="formData.region" :placeholder="t('Please enter region')" />
|
||||
<Input v-model="formData.region" :placeholder="t('Please enter region')" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Storage Class') }}</Label>
|
||||
<AppInput v-model="formData.storageclass" :placeholder="t('Please Enter storage class')" />
|
||||
<Input v-model="formData.storageclass" :placeholder="t('Please Enter storage class')" />
|
||||
</div>
|
||||
</AppCard>
|
||||
</div>
|
||||
@@ -81,17 +81,20 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="handleCancel">{{ t('Cancel') }}</AppButton>
|
||||
<AppButton variant="primary" :disabled="!formData.type" :loading="submitting" @click="handleSave">
|
||||
<Button variant="outline" @click="handleCancel">{{ t('Cancel') }}</Button>
|
||||
<Button variant="default" :disabled="!formData.type" :loading="submitting" @click="handleSave">
|
||||
{{ t('Save') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard, AppModal } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -214,4 +217,4 @@ defineExpose({
|
||||
resetForm()
|
||||
},
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
@@ -4,7 +4,7 @@
|
||||
v-bind="props"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex flex-col h-[100vh] items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg',
|
||||
'relative flex flex-col h-screen items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
@@ -17,11 +17,11 @@
|
||||
'[--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)]',
|
||||
'[--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)]',
|
||||
'[--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)]',
|
||||
'[background-image:var(--white-gradient),var(--aurora)] dark:[background-image:var(--dark-gradient),var(--aurora)] [background-size:300%,_200%] [background-position:50%_50%,50%_50%]',
|
||||
'[background-image:var(--white-gradient),var(--aurora)] dark:[background-image:var(--dark-gradient),var(--aurora)] bg-size-[300%,200%] bg-position-[50%_50%,50%_50%]',
|
||||
'aurora-background-gradient-after',
|
||||
'aurora-gradient-animation',
|
||||
props.radialGradient &&
|
||||
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
|
||||
`mask-[radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
|
||||
)
|
||||
"
|
||||
></div>
|
||||
@@ -51,8 +51,8 @@ const props = withDefaults(defineProps<AuroraBackgroundProps>(), {
|
||||
after:inset-0
|
||||
after:[background-image:var(--white-gradient),var(--aurora)]
|
||||
after:dark:[background-image:var(--dark-gradient),var(--aurora)]
|
||||
after:[background-size:200%,_100%]
|
||||
after:[background-attachment:fixed]
|
||||
after:bg-size-[200%,100%]
|
||||
after:bg-fixed
|
||||
after:mix-blend-difference;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,21 +16,45 @@ function isValidConfig(config: any): config is ConfigItem {
|
||||
}
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
if (["ZodObject", "ZodArray"].includes(props.shape?.type))
|
||||
return { schema: props.shape?.schema }
|
||||
if (props.shape && ["ZodObject", "ZodArray"].includes(props.shape.type)) {
|
||||
return { schema: props.shape.schema }
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const resolvedComponent = computed(() => {
|
||||
if (isValidConfig(props.config)) {
|
||||
if (typeof props.config.component === "string") {
|
||||
return INPUT_COMPONENTS[props.config.component] ?? INPUT_COMPONENTS.string
|
||||
}
|
||||
return props.config.component
|
||||
}
|
||||
|
||||
if (!props.shape) {
|
||||
throw new Error("Shape is required for auto form field")
|
||||
}
|
||||
|
||||
const handlerKey = DEFAULT_ZOD_HANDLERS[props.shape.type]
|
||||
|
||||
if (!handlerKey) {
|
||||
throw new Error(`Unsupported schema type: ${String(props.shape.type)}`)
|
||||
}
|
||||
|
||||
const component = INPUT_COMPONENTS[handlerKey] ?? INPUT_COMPONENTS.string
|
||||
|
||||
if (!component) {
|
||||
throw new Error(`Component not registered for handler: ${handlerKey}`)
|
||||
}
|
||||
|
||||
return component
|
||||
})
|
||||
|
||||
const { isDisabled, isHidden, isRequired, overrideOptions } = useDependencies(props.fieldName)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="isValidConfig(config)
|
||||
? typeof config.component === 'string'
|
||||
? INPUT_COMPONENTS[config.component!]
|
||||
: config.component
|
||||
: INPUT_COMPONENTS[DEFAULT_ZOD_HANDLERS[shape.type]] "
|
||||
:is="resolvedComponent"
|
||||
v-if="!isHidden"
|
||||
:field-name="fieldName"
|
||||
:label="shape.schema?.description"
|
||||
|
||||
@@ -76,7 +76,7 @@ provide(FieldContextKey, fieldContext)
|
||||
:config="config as ConfigItem"
|
||||
/>
|
||||
|
||||
<div class="!my-4 flex justify-end">
|
||||
<div class="my-4! flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
|
||||
@@ -9,10 +9,11 @@ export type ZodObjectOrWrapped
|
||||
* Beautify a camelCase string.
|
||||
* e.g. "myString" -> "My String"
|
||||
*/
|
||||
export function beautifyObjectName(string: string) {
|
||||
export function beautifyObjectName(value?: string) {
|
||||
const input = value ?? ""
|
||||
// Remove bracketed indices
|
||||
// if numbers only return the string
|
||||
let output = string.replace(/\[\d+\]/g, "").replace(/([A-Z])/g, " $1")
|
||||
let output = input.replace(/\[\d+\]/g, "").replace(/([A-Z])/g, " $1")
|
||||
output = output.charAt(0).toUpperCase() + output.slice(1)
|
||||
return output
|
||||
}
|
||||
@@ -27,7 +28,10 @@ export function getIndexIfArray(string: string) {
|
||||
// Match the index
|
||||
const match = string.match(indexRegex)
|
||||
// Extract the index (number)
|
||||
const index = match ? Number.parseInt(match[1]) : undefined
|
||||
if (!match || match[1] === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const index = Number.parseInt(match[1])
|
||||
return index
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
|
||||
<template>
|
||||
<ol
|
||||
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)"
|
||||
:class="cn('flex flex-wrap items-center gap-1.5 wrap-break-word text-sm text-muted-foreground sm:gap-2.5', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ol>
|
||||
|
||||
@@ -17,7 +17,7 @@ const delegatedProps = reactiveOmit(props, "class")
|
||||
v-bind="delegatedProps"
|
||||
:orientation="props.orientation"
|
||||
:class="cn(
|
||||
'bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto',
|
||||
'bg-input relative m-0! self-stretch data-[orientation=vertical]:h-auto',
|
||||
props.class,
|
||||
)"
|
||||
/>
|
||||
|
||||
@@ -20,13 +20,13 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
'h-9 w-9 p-0 font-normal',
|
||||
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
||||
// Selected
|
||||
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
|
||||
'data-selected:bg-primary data-selected:text-primary-foreground data-selected:opacity-100 data-selected:hover:bg-primary data-selected:hover:text-primary-foreground data-selected:focus:bg-primary data-selected:focus:text-primary-foreground',
|
||||
// Disabled
|
||||
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||
'data-disabled:text-muted-foreground data-disabled:opacity-50',
|
||||
// Unavailable
|
||||
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
||||
'data-unavailable:text-destructive-foreground data-unavailable:line-through',
|
||||
// Outside months
|
||||
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
|
||||
'data-outside-view:text-muted-foreground data-outside-view:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="forwardedProps"
|
||||
|
||||
@@ -40,24 +40,51 @@ const props = withDefaults(defineProps<Pick<BaseChartProps<T>, "data" | "colors"
|
||||
})
|
||||
|
||||
type KeyOfT = Extract<keyof T, string>
|
||||
type Data = typeof props.data[number]
|
||||
type Data = T
|
||||
|
||||
const valueFormatter = props.valueFormatter ?? ((tick: number) => `${tick}`)
|
||||
const category = computed(() => props.category as KeyOfT)
|
||||
const index = computed(() => props.index as KeyOfT)
|
||||
const chartData = computed(() => props.data ?? [])
|
||||
|
||||
const isMounted = useMounted()
|
||||
const activeSegmentKey = ref<string>()
|
||||
const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.data.filter(d => d[props.category]).filter(Boolean).length))
|
||||
const legendItems = computed(() => props.data.map((item, i) => ({
|
||||
name: item[props.index],
|
||||
const colors = computed(() => props.colors?.length ? props.colors : defaultColors(chartData.value.filter(d => d[category.value]).length))
|
||||
const legendItems = computed(() => chartData.value.map((item, i) => ({
|
||||
name: item[index.value],
|
||||
color: colors.value[i],
|
||||
inactive: false,
|
||||
})))
|
||||
|
||||
const totalValue = computed(() => props.data.reduce((prev, curr) => {
|
||||
return prev + curr[props.category]
|
||||
}, 0))
|
||||
const segmentEvents = computed(() => ({
|
||||
[Donut.selectors.segment]: {
|
||||
click: (d: Data, _ev: PointerEvent, elementIndex: number, elements: HTMLElement[] = []) => {
|
||||
const segmentKey = d?.data?.[index.value as keyof Data] as unknown as string | undefined
|
||||
if (!elements.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (segmentKey && activeSegmentKey.value === segmentKey) {
|
||||
activeSegmentKey.value = undefined
|
||||
elements.forEach(el => {
|
||||
el.style.opacity = "1"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
activeSegmentKey.value = segmentKey
|
||||
elements.forEach(el => {
|
||||
el.style.opacity = `${props.filterOpacity}`
|
||||
})
|
||||
const target = elements[elementIndex]
|
||||
if (target) {
|
||||
target.style.opacity = "1"
|
||||
}
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const totalValue = computed(() => chartData.value.reduce((prev, curr) => prev + (Number(curr[category.value]) || 0), 0))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -78,21 +105,7 @@ const totalValue = computed(() => props.data.reduce((prev, curr) => {
|
||||
:arc-width="type === 'donut' ? 20 : 0"
|
||||
:show-background="false"
|
||||
:central-label="type === 'donut' ? valueFormatter(totalValue) : ''"
|
||||
:events="{
|
||||
[Donut.selectors.segment]: {
|
||||
click: (d: Data, ev: PointerEvent, i: number, elements: HTMLElement[]) => {
|
||||
if (d?.data?.[index] === activeSegmentKey) {
|
||||
activeSegmentKey = undefined
|
||||
elements.forEach(el => el.style.opacity = '1')
|
||||
}
|
||||
else {
|
||||
activeSegmentKey = d?.data?.[index]
|
||||
elements.forEach(el => el.style.opacity = `${filterOpacity}`)
|
||||
elements[i].style.opacity = '1'
|
||||
}
|
||||
},
|
||||
},
|
||||
}"
|
||||
:events="segmentEvents"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
|
||||
@@ -21,7 +21,7 @@ function keepStyling() {
|
||||
nextTick(() => {
|
||||
const elements = elRef.value?.querySelectorAll(selector)
|
||||
const classes = buttonVariants({ variant: "ghost", size: "sm" }).split(" ")
|
||||
elements?.forEach(el => el.classList.add(...classes, "!inline-flex", "!mr-2"))
|
||||
elements?.forEach(el => el.classList.add(...classes, "inline-flex!", "mr-2!"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,8 +30,12 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
function onLegendItemClick(d: BulletLegendItemInterface, i: number) {
|
||||
const targetItem = props.items[i]
|
||||
if (!targetItem) {
|
||||
return
|
||||
}
|
||||
emits("legendItemClick", d, i)
|
||||
const isBulletActive = !props.items[i].inactive
|
||||
const isBulletActive = !targetItem.inactive
|
||||
const isFilterApplied = props.items.some(i => i.inactive)
|
||||
if (isFilterApplied && isBulletActive) {
|
||||
// reset filter
|
||||
|
||||
@@ -16,7 +16,7 @@ const props = defineProps<{
|
||||
|
||||
// Use weakmap to store reference to each datapoint for Tooltip
|
||||
const wm = new WeakMap()
|
||||
function template(d: any, i: number, elements: (HTMLElement | SVGElement)[]) {
|
||||
function template(d: any, i: number, elements: (HTMLElement | SVGElement)[] = []) {
|
||||
const valueFormatter = props.valueFormatter ?? ((tick: number) => `${tick}`)
|
||||
if (props.index in d) {
|
||||
if (wm.has(d)) {
|
||||
@@ -42,8 +42,12 @@ function template(d: any, i: number, elements: (HTMLElement | SVGElement)[]) {
|
||||
return wm.get(data)
|
||||
}
|
||||
else {
|
||||
const style = getComputedStyle(elements[i])
|
||||
const omittedData = [{ name: data.name, value: valueFormatter(data[props.index]), color: style.fill }]
|
||||
const element = elements[i]
|
||||
if (!element) {
|
||||
return ""
|
||||
}
|
||||
const style = getComputedStyle(element)
|
||||
const omittedData = [{ name: data?.name, value: valueFormatter(data?.[props.index]), color: style.fill }]
|
||||
const componentDiv = document.createElement("div")
|
||||
const TooltipComponent = props.customTooltip ?? ChartTooltip
|
||||
createApp(TooltipComponent, { title: d[props.index], data: omittedData }).mount(componentDiv)
|
||||
|
||||
@@ -16,7 +16,7 @@ const delegatedProps = reactiveOmit(props, "class")
|
||||
<template>
|
||||
<ComboboxGroup
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
|
||||
:class="cn('overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground', props.class)"
|
||||
>
|
||||
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{{ heading }}
|
||||
|
||||
@@ -16,7 +16,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<template>
|
||||
<ComboboxItem
|
||||
v-bind="forwarded"
|
||||
:class="cn('relative flex cursor-default gap-2 select-none justify-between items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
|
||||
:class="cn('relative flex cursor-default gap-2 select-none justify-between items-center rounded-sm px-2 py-1.5 text-sm outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ComboboxItem>
|
||||
|
||||
@@ -13,7 +13,7 @@ const forwarded = useForwardPropsEmits(props, emits)
|
||||
<template>
|
||||
<Dialog v-bind="forwarded">
|
||||
<DialogContent class="overflow-hidden p-0 shadow-lg">
|
||||
<Command class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
<Command class="**:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 **:[[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
<slot />
|
||||
</Command>
|
||||
</DialogContent>
|
||||
|
||||
@@ -33,7 +33,7 @@ onUnmounted(() => {
|
||||
<ListboxGroup
|
||||
v-bind="delegatedProps"
|
||||
:id="id"
|
||||
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
|
||||
:class="cn('overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground', props.class)"
|
||||
:hidden="isRender ? undefined : true"
|
||||
>
|
||||
<ListboxGroupLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
|
||||
@@ -65,7 +65,7 @@ onUnmounted(() => {
|
||||
v-bind="forwarded"
|
||||
:id="id"
|
||||
ref="itemRef"
|
||||
:class="cn('relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
|
||||
:class="cn('relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
|
||||
@select="() => {
|
||||
filterState.search = ''
|
||||
}"
|
||||
|
||||
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<ContextMenuCheckboxItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<ContextMenuItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
inset && 'pl-8',
|
||||
props.class,
|
||||
)"
|
||||
|
||||
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<ContextMenuRadioItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerRootEmits, DrawerRootProps } from "vaul-vue"
|
||||
import { useForwardPropsEmits } from "reka-ui"
|
||||
import { computed } from "vue"
|
||||
import { DrawerRoot } from "vaul-vue"
|
||||
|
||||
const props = withDefaults(defineProps<DrawerRootProps>(), {
|
||||
@@ -10,10 +11,13 @@ const props = withDefaults(defineProps<DrawerRootProps>(), {
|
||||
const emits = defineEmits<DrawerRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
const forwardedProps = computed(() => ({
|
||||
...(forwarded.value as Record<string, unknown>),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerRoot v-bind="forwarded">
|
||||
<DrawerRoot v-bind="forwardedProps">
|
||||
<slot />
|
||||
</DrawerRoot>
|
||||
</template>
|
||||
|
||||
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<DropdownMenuCheckboxItem
|
||||
v-bind="forwarded"
|
||||
:class=" cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
<DropdownMenuItem
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
props.class,
|
||||
)"
|
||||
|
||||
@@ -24,7 +24,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<DropdownMenuRadioItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@ const props = defineProps<{
|
||||
<p
|
||||
data-slot="field-description"
|
||||
:class="cn(
|
||||
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
|
||||
'text-muted-foreground text-sm leading-normal font-normal group-has-data-[orientation=horizontal]/field:text-balance',
|
||||
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
|
||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
||||
props.class,
|
||||
|
||||
@@ -11,7 +11,7 @@ const props = defineProps<{
|
||||
<div
|
||||
data-slot="field-group"
|
||||
:class="cn(
|
||||
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
|
||||
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -13,8 +13,8 @@ const props = defineProps<{
|
||||
data-slot="field-label"
|
||||
:class="cn(
|
||||
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
||||
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&_>[data-slot=field]]:p-4',
|
||||
'has-[[data-state=checked]]:bg-primary/5 has-[[data-state=checked]]:border-primary dark:has-[[data-state=checked]]:bg-primary/10',
|
||||
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
|
||||
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -6,15 +6,15 @@ export const fieldVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
||||
vertical: ["flex-col *:w-full [&>.sr-only]:w-auto"],
|
||||
horizontal: [
|
||||
"flex-row items-center",
|
||||
"[&>[data-slot=field-label]]:flex-auto",
|
||||
"*:data-[slot=field-label]:flex-auto",
|
||||
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
],
|
||||
responsive: [
|
||||
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
||||
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
||||
"flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto",
|
||||
"@md/field-group:*:data-[slot=field-label]:flex-auto",
|
||||
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
defineEmits(['animationStart', 'animationComplete']);
|
||||
|
||||
const currentWord = ref(props.words[0]);
|
||||
const currentWord = ref<string>(props.words[0] ?? "");
|
||||
const isVisible = ref(true);
|
||||
const timeoutId = ref<number | null>(null);
|
||||
|
||||
@@ -56,8 +56,9 @@ function startAnimation() {
|
||||
isVisible.value = false;
|
||||
|
||||
setTimeout(() => {
|
||||
const currentIndex = props.words.indexOf(currentWord.value);
|
||||
const nextWord = props.words[currentIndex + 1] || props.words[0];
|
||||
const words = props.words ?? [];
|
||||
const currentIndex = words.indexOf(currentWord.value);
|
||||
const nextWord = words[currentIndex + 1] ?? words[0] ?? "";
|
||||
currentWord.value = nextWord;
|
||||
isVisible.value = true;
|
||||
}, 600);
|
||||
|
||||
@@ -87,17 +87,17 @@ const defaultGlobeConfig: GlobeConfig = {
|
||||
...props.globeConfig,
|
||||
};
|
||||
|
||||
const githubGlobeRef = ref<HTMLCanvasElement>();
|
||||
const globeData = ref<GlobeData[]>();
|
||||
const githubGlobeRef = ref<HTMLCanvasElement | null>(null);
|
||||
const globeData = ref<GlobeData[]>([]);
|
||||
|
||||
let numberOfRings: number[] = [];
|
||||
|
||||
let renderer: WebGLRenderer;
|
||||
let scene: Scene;
|
||||
let camera: PerspectiveCamera;
|
||||
let controls: OrbitControls;
|
||||
let renderer: WebGLRenderer | null = null;
|
||||
let scene: Scene | null = null;
|
||||
let camera: PerspectiveCamera | null = null;
|
||||
let controls: OrbitControls | null = null;
|
||||
|
||||
let globe: ThreeGlobe;
|
||||
let globe: ThreeGlobe | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
setupScene();
|
||||
@@ -110,14 +110,30 @@ onMounted(() => {
|
||||
window.addEventListener('resize', onWindowResize, false);
|
||||
|
||||
watch(globeData, () => {
|
||||
if (!globe || !globeData.value) return;
|
||||
if (!globe || globeData.value.length === 0) return;
|
||||
|
||||
numberOfRings = genRandomNumbers(0, props.data.length, Math.floor((props.data.length * 4) / 5));
|
||||
const arcLength = props.data?.length ?? 0;
|
||||
if (arcLength === 0) return;
|
||||
|
||||
numberOfRings = genRandomNumbers(0, arcLength, Math.floor((arcLength * 4) / 5));
|
||||
|
||||
globe.ringsData(globeData.value.filter((d, i) => numberOfRings.includes(i)));
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', onWindowResize, false);
|
||||
if (controls) {
|
||||
controls.dispose();
|
||||
controls = null;
|
||||
}
|
||||
renderer?.dispose();
|
||||
renderer = null;
|
||||
scene = null;
|
||||
camera = null;
|
||||
globe = null;
|
||||
});
|
||||
|
||||
function setupScene() {
|
||||
if (!githubGlobeRef.value) {
|
||||
throw new Error('Canvas not initialized');
|
||||
@@ -202,11 +218,13 @@ function initGlobe() {
|
||||
globeMaterial.emissiveIntensity = defaultGlobeConfig.emissiveIntensity || 0.1;
|
||||
globeMaterial.shininess = defaultGlobeConfig.shininess || 0.9;
|
||||
|
||||
scene.add(globe);
|
||||
if (scene) {
|
||||
scene.add(globe);
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
if (!githubGlobeRef.value) {
|
||||
if (!githubGlobeRef.value || !renderer || !camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,22 +237,31 @@ function onWindowResize() {
|
||||
renderer.setSize(width, height);
|
||||
}
|
||||
function startAnimation() {
|
||||
if (!globe || !globeData.value!) return;
|
||||
if (!globe) return;
|
||||
const arcs = props.data ?? [];
|
||||
const strokeValues = [0.32, 0.28, 0.3] as const;
|
||||
const getStroke = () => {
|
||||
if (!strokeValues.length) {
|
||||
return 0.3;
|
||||
}
|
||||
const index = Math.floor(Math.random() * strokeValues.length);
|
||||
return strokeValues[index] ?? strokeValues[0];
|
||||
};
|
||||
globe
|
||||
.arcsData(props.data)
|
||||
.arcsData(arcs)
|
||||
.arcStartLat((d: any) => d.startLat * 1)
|
||||
.arcStartLng((d: any) => d.startLng * 1)
|
||||
.arcEndLat((d: any) => d.endLat * 1)
|
||||
.arcEndLng((d: any) => d.endLng * 1)
|
||||
.arcColor((e: any) => e.color)
|
||||
.arcAltitude((e: any) => e.arcAlt * 1)
|
||||
.arcStroke((e: any) => [0.32, 0.28, 0.3][Math.round(Math.random() * 4)])
|
||||
.arcStroke(() => getStroke())
|
||||
.arcDashLength(defaultGlobeConfig.arcLength!)
|
||||
.arcDashInitialGap((e: any) => e.order * 1)
|
||||
.arcDashGap(15)
|
||||
.arcDashAnimateTime(defaultGlobeConfig.arcTime!)
|
||||
|
||||
.pointsData(props.data)
|
||||
.pointsData(arcs)
|
||||
.pointColor((e: any) => e.color)
|
||||
.pointsMerge(true)
|
||||
.pointAltitude(0.0)
|
||||
@@ -244,10 +271,14 @@ function startAnimation() {
|
||||
.ringColor((e: any) => (t: any) => e.color(t))
|
||||
.ringMaxRadius(defaultGlobeConfig.maxRings!)
|
||||
.ringPropagationSpeed(3)
|
||||
.ringRepeatPeriod((defaultGlobeConfig.arcTime! * defaultGlobeConfig.arcLength!) / defaultGlobeConfig.rings!);
|
||||
.ringRepeatPeriod((defaultGlobeConfig.arcTime! * defaultGlobeConfig.arcLength!) / (defaultGlobeConfig.rings || 1));
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (!globe || !renderer || !scene || !camera) {
|
||||
requestAnimationFrame(animate);
|
||||
return;
|
||||
}
|
||||
globe.rotation.y += 0.01; // Rotate globe
|
||||
|
||||
renderer.render(scene, camera);
|
||||
@@ -256,22 +287,26 @@ function animate() {
|
||||
}
|
||||
|
||||
function buildData() {
|
||||
const arcs = props.data;
|
||||
let points = [];
|
||||
const arcs = props.data ?? [];
|
||||
const pointSize = props.globeConfig?.pointSize ?? defaultGlobeConfig.pointSize ?? 1;
|
||||
const points: GlobeData[] = [];
|
||||
|
||||
for (let i = 0; i < arcs.length; i++) {
|
||||
const arc = arcs[i];
|
||||
const rgb = hexToRgb(arc.color) as { r: number; g: number; b: number };
|
||||
points.push({
|
||||
size: props.globeConfig.pointSize,
|
||||
if (!arc) continue;
|
||||
const rgb = hexToRgb(arc.color ?? '#ffffff');
|
||||
const basePoint = {
|
||||
size: pointSize,
|
||||
order: arc.order,
|
||||
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
|
||||
};
|
||||
points.push({
|
||||
...basePoint,
|
||||
lat: arc.startLat,
|
||||
lng: arc.startLng,
|
||||
});
|
||||
points.push({
|
||||
size: props.globeConfig.pointSize,
|
||||
order: arc.order,
|
||||
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`,
|
||||
...basePoint,
|
||||
lat: arc.endLat,
|
||||
lng: arc.endLng,
|
||||
});
|
||||
@@ -285,7 +320,7 @@ function buildData() {
|
||||
globeData.value = filteredPoints;
|
||||
}
|
||||
|
||||
function hexToRgb(color: string) {
|
||||
function hexToRgb(color: string): { r: number; g: number; b: number } {
|
||||
let hex = color.replace(/^#/, '');
|
||||
|
||||
// If the hex code is 3 characters, expand it to 6 characters
|
||||
|
||||
@@ -34,7 +34,7 @@ export const itemVariants = cva(
|
||||
)
|
||||
|
||||
export const itemMediaVariants = cva(
|
||||
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
|
||||
"flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none group-has-data-[slot=item-description]/item:translate-y-0.5",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -12,7 +12,7 @@ const props = defineProps<{
|
||||
:class="cn(
|
||||
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
|
||||
'[&_svg:not([class*=\'size-\'])]:size-3',
|
||||
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
|
||||
'in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<MenubarCheckboxItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -22,7 +22,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<MenubarItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
inset && 'pl-8',
|
||||
props.class,
|
||||
)"
|
||||
|
||||
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<MenubarRadioItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
<template>
|
||||
<NavigationMenuIndicator
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in', props.class)"
|
||||
:class="cn('top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in', props.class)"
|
||||
>
|
||||
<div class="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuIndicator>
|
||||
|
||||
@@ -22,7 +22,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'origin-top-center relative mt-1.5 h-[--reka-navigation-menu-viewport-height] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[--reka-navigation-menu-viewport-width] left-[var(--reka-navigation-menu-viewport-left)]',
|
||||
'origin-top-center relative mt-1.5 h-(--reka-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-(--reka-navigation-menu-viewport-width) left-(--reka-navigation-menu-viewport-left)',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -10,5 +10,5 @@ export { default as NavigationMenuTrigger } from "./NavigationMenuTrigger.vue"
|
||||
export { default as NavigationMenuViewport } from "./NavigationMenuViewport.vue"
|
||||
|
||||
export const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50",
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ const props = defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('relative [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5 [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5', props.class)">
|
||||
<div :class="cn('relative *:data-[slot=input]:has-data-[slot=increment]:pr-5 *:data-[slot=input]:has-data-[slot=decrement]:pl-5', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
cn(
|
||||
'mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70',
|
||||
// Radiant effect
|
||||
'radiant-animation bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--radiant-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
|
||||
'radiant-animation bg-clip-text bg-no-repeat bg-position-[0_0] bg-size-[var(--radiant-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
|
||||
// Radiant gradient
|
||||
'bg-gradient-to-r from-transparent via-black via-50% to-transparent dark:via-white',
|
||||
'bg-linear-to-r from-transparent via-black via-50% to-transparent dark:via-white',
|
||||
$props.class
|
||||
)
|
||||
"
|
||||
|
||||
@@ -17,18 +17,18 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
<RangeCalendarCellTrigger
|
||||
:class="cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-9 w-9 p-0 font-normal data-[selected]:opacity-100',
|
||||
'h-9 w-9 p-0 font-normal data-selected:opacity-100',
|
||||
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
||||
// Selection Start
|
||||
'data-[selection-start]:bg-primary data-[selection-start]:text-primary-foreground data-[selection-start]:hover:bg-primary data-[selection-start]:hover:text-primary-foreground data-[selection-start]:focus:bg-primary data-[selection-start]:focus:text-primary-foreground',
|
||||
'data-selection-start:bg-primary data-selection-start:text-primary-foreground data-selection-start:hover:bg-primary data-selection-start:hover:text-primary-foreground data-selection-start:focus:bg-primary data-selection-start:focus:text-primary-foreground',
|
||||
// Selection End
|
||||
'data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground data-[selection-end]:focus:bg-primary data-[selection-end]:focus:text-primary-foreground',
|
||||
'data-selection-end:bg-primary data-selection-end:text-primary-foreground data-selection-end:hover:bg-primary data-selection-end:hover:text-primary-foreground data-selection-end:focus:bg-primary data-selection-end:focus:text-primary-foreground',
|
||||
// Outside months
|
||||
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
|
||||
'data-outside-view:text-muted-foreground data-outside-view:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
|
||||
// Disabled
|
||||
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||
'data-disabled:text-muted-foreground data-disabled:opacity-50',
|
||||
// Unavailable
|
||||
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
||||
'data-unavailable:text-destructive-foreground data-unavailable:line-through',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="forwardedProps"
|
||||
|
||||
@@ -15,7 +15,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SplitterResizeHandle v-bind="forwarded" :class="cn('relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-orientation=vertical]]:h-px [&[data-orientation=vertical]]:w-full [&[data-orientation=vertical]]:after:left-0 [&[data-orientation=vertical]]:after:h-1 [&[data-orientation=vertical]]:after:w-full [&[data-orientation=vertical]]:after:-translate-y-1/2 [&[data-orientation=vertical]]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)">
|
||||
<SplitterResizeHandle v-bind="forwarded" :class="cn('relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-1 data-[orientation=vertical]:after:w-full data-[orientation=vertical]:after:-translate-y-1/2 data-[orientation=vertical]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)">
|
||||
<template v-if="props.withHandle">
|
||||
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||
<GripVertical class="h-2.5 w-2.5" />
|
||||
|
||||
@@ -41,7 +41,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
"
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-[--reka-select-trigger-height] w-full min-w-[--reka-select-trigger-width]')">
|
||||
<SelectViewport :class="cn('p-1', position === 'popper' && 'h-(--reka-select-trigger-height) w-full min-w-(--reka-select-trigger-width)')">
|
||||
<slot />
|
||||
</SelectViewport>
|
||||
<SelectScrollDownButton />
|
||||
|
||||
@@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -17,7 +17,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
<SelectTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
<template>
|
||||
<div
|
||||
v-if="collapsible === 'none'"
|
||||
:class="cn('flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground', props.class)"
|
||||
:class="cn('flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground', props.class)"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
@@ -31,7 +31,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
:side="side"
|
||||
class="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
class="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
:style="{
|
||||
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||
}"
|
||||
@@ -52,24 +52,24 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
<!-- This is what handles the sidebar gap on desktop -->
|
||||
<div
|
||||
:class="cn(
|
||||
'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear',
|
||||
'duration-200 relative h-svh w-(--sidebar-width) bg-transparent transition-[width] ease-linear',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
|
||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
)"
|
||||
/>
|
||||
<div
|
||||
:class="cn(
|
||||
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex',
|
||||
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+_2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="$attrs"
|
||||
|
||||
@@ -11,7 +11,7 @@ const props = defineProps<{
|
||||
<main
|
||||
:class="cn(
|
||||
'relative flex min-h-svh flex-1 flex-col bg-background',
|
||||
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
|
||||
'peer-data-[variant=inset]:min-h-[calc(100svh-(--spacing(4)))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
|
||||
@@ -26,7 +26,7 @@ const width = computed(() => {
|
||||
/>
|
||||
|
||||
<Skeleton
|
||||
class="h-4 flex-1 max-w-[--skeleton-width]"
|
||||
class="h-4 flex-1 max-w-(--skeleton-width)"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
:style="{ '--skeleton-width': width }"
|
||||
/>
|
||||
|
||||
@@ -72,7 +72,7 @@ provideSidebarContext({
|
||||
'--sidebar-width': SIDEBAR_WIDTH,
|
||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||
}"
|
||||
:class="cn('group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar', props.class)"
|
||||
:class="cn('group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar', props.class)"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -18,7 +18,7 @@ const { toggleSidebar } = useSidebar()
|
||||
title="Toggle Sidebar"
|
||||
:class="cn(
|
||||
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
|
||||
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
|
||||
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
|
||||
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
||||
|
||||
@@ -36,7 +36,7 @@ export { default as SidebarTrigger } from "./SidebarTrigger.vue"
|
||||
export { useSidebar } from "./utils"
|
||||
|
||||
export const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -47,7 +47,7 @@ export const sidebarMenuButtonVariants = cva(
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -20,7 +20,7 @@ const forwarded = useForwardProps(delegatedProps)
|
||||
:class="cn(
|
||||
'inline-flex items-center justify-center rounded-full text-muted-foreground/50 w-10 h-10',
|
||||
// Disabled
|
||||
'group-data-[disabled]:text-muted-foreground group-data-[disabled]:opacity-50',
|
||||
'group-data-disabled:text-muted-foreground group-data-disabled:opacity-50',
|
||||
// Active
|
||||
'group-data-[state=active]:bg-primary group-data-[state=active]:text-primary-foreground',
|
||||
// Completed
|
||||
|
||||
@@ -16,7 +16,7 @@ const forwarded = useForwardProps(delegatedProps)
|
||||
<StepperItem
|
||||
v-slot="slotProps"
|
||||
v-bind="forwarded"
|
||||
:class="cn('flex items-center gap-2 group data-[disabled]:pointer-events-none', props.class)"
|
||||
:class="cn('flex items-center gap-2 group data-disabled:pointer-events-none', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</StepperItem>
|
||||
|
||||
@@ -19,7 +19,7 @@ const forwarded = useForwardProps(delegatedProps)
|
||||
:class="cn(
|
||||
'bg-muted',
|
||||
// Disabled
|
||||
'group-data-[disabled]:bg-muted group-data-[disabled]:opacity-50',
|
||||
'group-data-disabled:bg-muted group-data-disabled:opacity-50',
|
||||
// Completed
|
||||
'group-data-[state=completed]:bg-accent-foreground',
|
||||
props.class,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="relative aspect-[2/1] w-full rounded-lg bg-white font-sans dark:bg-black">
|
||||
<div class="relative aspect-2/1 w-full rounded-lg bg-white font-sans dark:bg-black">
|
||||
<NuxtImg
|
||||
:src="`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`"
|
||||
class="pointer-events-none size-full select-none [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_80%,transparent)]"
|
||||
class="pointer-events-none size-full select-none mask-[linear-gradient(to_bottom,transparent,white_10%,white_80%,transparent)]"
|
||||
alt="world map"
|
||||
height="495"
|
||||
width="1056"
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full items-center justify-between gap-2 rounded-none border-t border-sidebar-border px-4 py-3 text-left transition-[padding] duration-200 group-data-[collapsible=icon]:h-12 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0"
|
||||
>
|
||||
<Button variant="ghost"
|
||||
class="w-full items-center justify-between gap-2 rounded-none px-4 py-3 text-left transition-[padding] duration-200 group-data-[collapsible=icon]:h-12 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-full border bg-muted">
|
||||
<img src="~/assets/img/rustfs.png" alt="RustFS" class="h-8 w-8 rounded-full object-cover" />
|
||||
@@ -24,34 +22,34 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '#components';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Icon } from '#components'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { defineProps, toRef, withDefaults } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { defineProps, toRef, withDefaults } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n();
|
||||
const { logout } = useAuth();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n()
|
||||
const { logout } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isCollapsed?: boolean;
|
||||
isCollapsed?: boolean
|
||||
}>(),
|
||||
{
|
||||
isCollapsed: false,
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const isCollapsed = toRef(props, 'isCollapsed');
|
||||
const isCollapsed = toRef(props, 'isCollapsed')
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
router.push('/auth/login');
|
||||
};
|
||||
await logout()
|
||||
router.push('/auth/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<AppModal v-model="visible" :title="group.name || t('Members')" size="lg" :close-on-backdrop="false">
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 px-3 py-2">
|
||||
<div class="flex items-center justify-between rounded-md border px-3 py-2">
|
||||
<span class="text-sm text-muted-foreground">{{ t('Status') }}</span>
|
||||
<AppSwitch v-model="statusBoolean" />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
|
||||
import { AppCard, AppModal } from '@/components/app'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
|
||||
@@ -106,7 +108,7 @@ const submitForm = async () => {
|
||||
<AppCard padded class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<Label class="text-sm font-medium">{{ t('Name') }}</Label>
|
||||
<AppInput v-model="formModel.group" autocomplete="off" />
|
||||
<Input v-model="formModel.group" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
@@ -161,13 +163,13 @@ const submitForm = async () => {
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">
|
||||
<Button variant="outline" @click="closeModal()">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">
|
||||
</Button>
|
||||
<Button variant="default" :loading="submitting" @click="submitForm">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
</template>
|
||||
@@ -2,12 +2,12 @@
|
||||
<AppCard padded class="space-y-4">
|
||||
<div v-if="!editStatus" class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="w-full sm:max-w-xs">
|
||||
<AppInput v-model="searchTerm" :placeholder="t('Search Policy')" />
|
||||
<Input v-model="searchTerm" :placeholder="t('Search Policy')" />
|
||||
</div>
|
||||
<AppButton variant="secondary" class="inline-flex items-center gap-2" @click="startEditing">
|
||||
<Button variant="secondary" class="inline-flex items-center gap-2" @click="startEditing">
|
||||
<Icon class="size-4" name="ri:add-line" />
|
||||
{{ t('Edit Policy') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
@@ -56,12 +56,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:self-start">
|
||||
<AppButton variant="ghost" @click="cancelEditing">
|
||||
<Button variant="ghost" @click="cancelEditing">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="secondary" @click="changePolicies">
|
||||
</Button>
|
||||
<Button variant="secondary" @click="changePolicies">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,8 +88,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppInput } from '@/components/app'
|
||||
import { AppCard } from '@/components/app'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
|
||||
@@ -202,4 +204,4 @@ const changePolicies = async () => {
|
||||
message.error(t('Edit Failed'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -3,11 +3,11 @@
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="w-full sm:max-w-xs">
|
||||
<AppInput v-model="searchTerm" :placeholder="t('Search Policy')" />
|
||||
<Input v-model="searchTerm" :placeholder="t('Search Policy')" />
|
||||
</div>
|
||||
<AppButton variant="secondary" :disabled="!checkedKeys.length || submitting" @click="changePolicies">
|
||||
<Button variant="secondary" :disabled="!checkedKeys.length || submitting" @click="changePolicies">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table class="overflow-hidden rounded-lg border">
|
||||
@@ -26,7 +26,10 @@
|
||||
<TableBody v-if="filteredPolicies.length">
|
||||
<TableRow v-for="policy in filteredPolicies" :key="policy.name">
|
||||
<TableCell>
|
||||
<AppCheckbox :checked="isSelected(policy.name)" @update:checked="value => toggleSelection(policy.name, value)" />
|
||||
<AppCheckbox
|
||||
:checked="isSelected(policy.name)"
|
||||
@update:checked="(value: boolean) => toggleSelection(policy.name, value)"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class="font-medium">{{ policy.name }}</TableCell>
|
||||
</TableRow>
|
||||
@@ -44,7 +47,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppCheckbox, AppInput, AppModal } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard, AppCheckbox, AppModal } from '@/components/app'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -149,4 +155,4 @@ const changePolicies = async () => {
|
||||
defineExpose({
|
||||
openDialog,
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
@@ -36,7 +36,7 @@
|
||||
<Checkbox
|
||||
:checked="checkedKeys.includes(rowKey(row))"
|
||||
aria-label="Select group"
|
||||
@update:checked="value => toggleRow(rowKey(row), value)"
|
||||
@update:checked="(value: boolean) => toggleRow(rowKey(row), value)"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class="font-medium">{{ row.name }}</TableCell>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<Checkbox
|
||||
:checked="checkedKeys.includes(rowKey(row))"
|
||||
aria-label="Select user"
|
||||
@update:checked="value => toggleRow(rowKey(row), value)"
|
||||
@update:checked="(value: boolean) => toggleRow(rowKey(row), value)"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class="font-medium">{{ row.accessKey }}</TableCell>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<AppModal v-model="visible" :title="user.accessKey || t('Account')" size="lg" :close-on-backdrop="false">
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 px-3 py-2">
|
||||
<div class="flex items-center justify-between rounded-md border px-3 py-2">
|
||||
<span class="text-sm text-muted-foreground">{{ t('Status') }}</span>
|
||||
<AppSwitch v-model="statusBoolean" />
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="w-full sm:max-w-xs">
|
||||
<AppInput v-model="searchTerm" :placeholder="t('Search Account')" />
|
||||
<Input v-model="searchTerm" :placeholder="t('Search Account')" />
|
||||
</div>
|
||||
<AppButton variant="secondary" class="inline-flex items-center gap-2" @click="addItem">
|
||||
<Button variant="secondary" class="inline-flex items-center gap-2" @click="addItem">
|
||||
<Icon class="size-4" name="ri:add-line" />
|
||||
{{ t('Add Account') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table class="overflow-hidden rounded-lg border">
|
||||
@@ -34,17 +34,17 @@
|
||||
<TableCell class="text-center">{{ account.name || '-' }}</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex justify-center gap-2">
|
||||
<AppButton variant="ghost" size="sm" class="h-8 w-8 p-0" @click="openEditItem(account)">
|
||||
<Button variant="ghost" size="sm" class="h-8 w-8 p-0" @click="openEditItem(account)">
|
||||
<Icon class="size-4" name="ri:edit-2-line" />
|
||||
</AppButton>
|
||||
<AppButton
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0 text-destructive"
|
||||
@click="confirmDelete(account)"
|
||||
>
|
||||
<Icon class="size-4" name="ri:delete-bin-5-line" />
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -65,11 +65,11 @@
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Access Key') }}</Label>
|
||||
<AppInput v-model="formModel.accessKey" autocomplete="off" />
|
||||
<Input v-model="formModel.accessKey" autocomplete="off" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Secret Key') }}</Label>
|
||||
<AppInput v-model="formModel.secretKey" type="password" autocomplete="off" />
|
||||
<Input v-model="formModel.secretKey" type="password" autocomplete="off" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Expiration') }}</Label>
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Name') }}</Label>
|
||||
<AppInput v-model="formModel.name" autocomplete="off" />
|
||||
<Input v-model="formModel.name" autocomplete="off" />
|
||||
</div>
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<Label>{{ t('Description') }}</Label>
|
||||
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 px-3 py-2">
|
||||
<div class="flex items-center justify-between rounded-md border px-3 py-2">
|
||||
<div>
|
||||
<p class="text-sm font-medium">{{ t('Use main account policy') }}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
@@ -103,8 +103,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="cancelAdd">{{ t('Cancel') }}</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">{{ t('Submit') }}</AppButton>
|
||||
<Button variant="outline" @click="cancelAdd">{{ t('Cancel') }}</Button>
|
||||
<Button variant="default" :loading="submitting" @click="submitForm">{{ t('Submit') }}</Button>
|
||||
</div>
|
||||
</AppCard>
|
||||
</template>
|
||||
@@ -118,8 +118,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppDateTimePicker, AppInput, AppSwitch, AppTextarea } from '@/components/app'
|
||||
import { AppCard, AppDateTimePicker, AppSwitch, AppTextarea } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -331,4 +334,4 @@ const deleteItem = async (row: any) => {
|
||||
message.error(t('Delete Failed'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Access Key') }}</Label>
|
||||
<AppInput v-model="formModel.accessKey" disabled />
|
||||
<Input v-model="formModel.accessKey" disabled />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
@@ -17,17 +17,17 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Name') }}</Label>
|
||||
<AppInput v-model="formModel.name" autocomplete="off" />
|
||||
<Input v-model="formModel.name" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t('Description') }}</Label>
|
||||
<AppInput v-model="formModel.description" autocomplete="off" />
|
||||
<Input v-model="formModel.description" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex flex-col gap-3 rounded-md border p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium">{{ t('Use Main Account Policy') }}</p>
|
||||
@@ -55,18 +55,21 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="cancelEdit">
|
||||
<Button variant="outline" @click="cancelEdit">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">
|
||||
</Button>
|
||||
<Button variant="default" :loading="submitting" @click="submitForm">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</Button>
|
||||
</div>
|
||||
</AppCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AppButton, AppCard, AppDateTimePicker, AppInput, AppSwitch } from '@/components/app'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { AppCard, AppDateTimePicker, AppSwitch } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
@@ -194,4 +197,4 @@ const submitForm = async () => {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user