Rafactor UI for dataset (#708)

This commit is contained in:
qianmoQ 2024-03-21 13:07:34 +08:00 committed by GitHub
commit 0663a001b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 976 additions and 40 deletions

View File

@ -26,6 +26,7 @@
"echarts": "^5.5.0",
"lodash": "^4.17.21",
"lucide-vue-next": "^0.356.0",
"md-editor-v3": "^4.12.1",
"radix-vue": "^1.5.2",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",

View File

@ -1,7 +1,7 @@
import {createI18n} from 'vue-i18n'
import messages from '@/i18n/langs'
const language = (navigator.language || 'en').toLocaleLowerCase();
const language = (navigator.language || 'en').toLocaleLowerCase()
const i18n = createI18n({
fallbackLocale: 'zh_cn',
@ -15,6 +15,6 @@ const i18n = createI18n({
missingWarn: false,
locale: localStorage.getItem('lang') || language.split('-')[0] || 'en',
messages
});
})
export default i18n

View File

@ -53,4 +53,18 @@ export default {
value: 'Value',
alias: 'Alias',
sort: 'Sort',
count: 'Count',
content: 'Content',
feedback: 'Feedback',
selectLanguage: 'Select Language',
region: {
asia: {
common: 'Asia',
chineseSimple: 'Simple Chinese'
},
northAmerica: {
common: 'North America',
english: 'English'
}
}
}

View File

@ -45,7 +45,6 @@ export default {
syncModeTiming: 'Timing synchronization',
syncModeOutSync: 'Out Sync',
rebuild: 'Rebuild',
rebuildProgress: 'Rebuilding will only progress unfinished',
complete: 'Complete',
failed: 'Failed',
stateOfStart: 'Start',
@ -54,8 +53,6 @@ export default {
stateOfCreateTable: 'Create Table State',
modifyNotSupportDataPreview: 'Data preview is not supported to modify',
syncData: 'Sync Data',
syncDataTip: 'The data synchronization schedule will run in the background, see the logs for the specific synchronization results',
adhocDndTip: 'Drag the indicator dimension on the left to the corresponding position to query and render the data',
visualType: 'Visual Type',
visualTypeTable: 'Table',
visualTypeLine: 'Line',
@ -98,17 +95,24 @@ export default {
columnExpressionLessThan: 'Less Than',
columnExpressionLessThanOrEquals: 'Less Than Or Equals',
columnExpressionIsNotContains: 'Is Not Contains',
validatorSamplingTip: 'The order by key must contain a sampling key',
customFunction: 'Custom Function',
clearDataTip: 'Clear data will not be able to rollback, clear operation will run in the background, please be patient',
lifeCycleTip: 'Data set life cycle will be calculated according to the specified list expression',
lifeCycleMonth: 'Month',
lifeCycleWeek: 'Week',
lifeCycleDay: 'Day',
lifeCycleHour: 'Hour',
notSpecifiedTitle: 'Not Specified',
history: 'Sync History',
clearData: 'Clear Data',
error: 'View Error',
info: 'View Info',
},
tip: {
selectExpressionTip: 'Please select the expression',
selectExpression: 'Please select the expression',
syncData: 'The data synchronization schedule will run in the background, see the logs for the specific synchronization results',
clearData: 'Clear data will not be able to rollback, clear operation will run in the background, please be patient',
lifeCycle: 'Data set life cycle will be calculated according to the specified list expression',
validatorSampling: 'The order by key must contain a sampling key',
adhocDnd: 'Drag the indicator dimension on the left to the corresponding position to query and render the data',
rebuildProgress: 'Rebuilding will only progress unfinished',
}
}

View File

@ -4,6 +4,7 @@ import role from '@/i18n/langs/zhCn/role'
import schedule from '@/i18n/langs/en/schedule'
import dashboard from '@/i18n/langs/en/dashboard'
import dataset from '@/i18n/langs/en/dataset'
import state from '@/i18n/langs/en/state'
export default {
common: common,
@ -11,5 +12,6 @@ export default {
role: role,
schedule: schedule,
dashboard: dashboard,
dataset: dataset
dataset: dataset,
state: state
}

View File

@ -0,0 +1,11 @@
export default {
common: {
create: 'Created',
running: 'Running',
success: 'Success',
failure: 'Failure',
stop: 'Stopped',
timeout: 'Timeout',
queue: 'Queue'
}
}

View File

@ -53,4 +53,18 @@ export default {
value: '值',
alias: '别名',
sort: '排序',
count: '总数',
content: '内容',
feedback: '反馈',
selectLanguage: '选择语言',
region: {
asia: {
common: '亚洲',
chineseSimple: '简体中文'
},
northAmerica: {
common: '北美洲',
english: '英语'
}
}
}

View File

@ -44,8 +44,7 @@ export default {
syncModeManual: '手动',
syncModeTiming: '定时同步',
syncModeOutSync: '不同步',
rebuild: '重建',
rebuildProgress: '重建只会进行未完成进度',
rebuild: '重新构建',
complete: '完成',
failed: '失败',
stateOfStarted: '已启动',
@ -54,8 +53,6 @@ export default {
stateOfCreateTable: '创建表状态',
modifyNotSupportDataPreview: '修改暂不支持数据预览',
syncData: '同步数据',
syncDataTip: '数据同步计划将在后台运行,具体同步结果请参考日志',
adhocDndTip: '拖拽左侧指标|维度到相应位置即可查询并渲染数据',
visualType: '可视化类型',
visualTypeTable: '表格',
visualTypeLine: '折线图',
@ -98,17 +95,24 @@ export default {
columnExpressionGreaterThanOrEquals: '大于等于',
columnExpressionLessThan: '小于',
columnExpressionLessThanOrEquals: '小于等于',
validatorSamplingTip: '排序键中必须包含抽样键',
customFunction: '自定义函数',
clearDataTip: '清除数据后无法进行回滚,清除操作将在后台运行,请耐心等待',
lifeCycleTip: '数据集生命周期会根据指定列表达式进行计算',
lifeCycleMonth: '月',
lifeCycleWeek: '周',
lifeCycleDay: '天',
lifeCycleHour: '小时',
notSpecifiedTitle: '未指定'
notSpecifiedTitle: '未指定',
history: '同步历史',
clearData: '清除数据',
error: '查看错误',
info: '查看详情',
},
tip: {
selectExpressionTip: '请选择表达式'
selectExpression: '请选择表达式',
syncData: '数据同步计划将在后台运行,具体同步结果请参考日志',
clearData: '清除数据后无法进行回滚,清除操作将在后台运行,请耐心等待',
lifeCycle: '数据集生命周期会根据指定列表达式进行计算',
validatorSampling: '排序键中必须包含抽样键',
adhocDnd: '拖拽左侧指标|维度到相应位置即可查询并渲染数据',
rebuildProgress: '重建只会进行未完成进度',
}
}

View File

@ -4,6 +4,7 @@ import role from '@/i18n/langs/zhCn/role'
import schedule from '@/i18n/langs/zhCn/schedule'
import dashboard from '@/i18n/langs/zhCn/dashboard'
import dataset from '@/i18n/langs/zhCn/dataset'
import state from '@/i18n/langs/zhCn/state'
export default {
common: common,
@ -11,5 +12,6 @@ export default {
role: role,
schedule: schedule,
dashboard: dashboard,
dataset: dataset
dataset: dataset,
state: state
}

View File

@ -0,0 +1,11 @@
export default {
common: {
create: '已创建',
running: '运行中',
success: '运行成功',
failure: '运行失败',
stop: '已停止',
timeout: '运行超时',
queue: '排队中'
}
}

View File

@ -0,0 +1,19 @@
export interface DatasetModel {
id: number
name: string
description: string
query: string
syncMode: string
expression: string
state: Array<string>
message: string
tableName: string
code: string
scheduler: string
executor: string
totalRows: number
totalSize: string
lifeCycle: string
lifeCycleColumn: string
lifeCycleType: string
}

View File

@ -118,6 +118,15 @@ const createAdminRouter = (router: any) => {
},
component: () => import('@/views/pages/admin/dataset/DatasetHome.vue')
},
{
path: 'dataset/info/:code?',
layout: LayoutContainer,
meta: {
title: 'common.dataset',
isRoot: false
},
component: () => import('@/views/pages/admin/dataset/DatasetInfo.vue')
},
{
path: 'dataset/adhoc/:code',
layout: LayoutContainer,
@ -126,6 +135,15 @@ const createAdminRouter = (router: any) => {
isRoot: false
},
component: () => import('@/views/pages/admin/dataset/DatasetAdhoc.vue')
},
{
path: 'dataset/adhoc/:code/:id',
layout: LayoutContainer,
meta: {
title: 'common.dataset',
isRoot: false
},
component: () => import('@/views/pages/admin/dataset/DatasetAdhoc.vue')
}
]
}

View File

@ -1,6 +1,7 @@
import { ResponseModel } from '@/model/response'
import { BaseService } from '@/services/base'
import { HttpUtils } from '@/utils/http'
import { FilterModel } from '@/model/filter'
const DEFAULT_PATH = '/api/v1/dataset'
@ -34,6 +35,51 @@ export class DatasetService
{
return new HttpUtils().get(`${DEFAULT_PATH}/columns/${code}`)
}
/**
* Rebuilds the specified item identified by the given ID.
*
* @param {number} id - the ID of the item to be rebuilt
* @return {Promise<ResponseModel>} a Promise that resolves with the response from the rebuild request
*/
rebuild(id: number): Promise<ResponseModel>
{
return new HttpUtils().put(`${DEFAULT_PATH}/rebuild/${id}`)
}
/**
* Retrieves the history for a given code using the provided pagination model.
*
* @param {string} code - The code for which to retrieve the history.
* @param {FilterModel} configure - The file model to use for the request.
* @return {Promise<ResponseModel>} A promise that resolves with the response model containing the history data.
*/
getHistory(code: string, configure: FilterModel): Promise<ResponseModel>
{
return new HttpUtils().post(`${DEFAULT_PATH}/history/${code}`, configure)
}
/**
* Sync data with the server using the provided id.
*
* @param {number} id - The id of the data to sync
* @return {Promise<ResponseModel>} A promise that resolves with the response from the server
*/
syncData(id: number): Promise<ResponseModel>
{
return new HttpUtils().put(`${DEFAULT_PATH}/syncData/${id}`)
}
/**
* Clears the data associated with the given code.
*
* @param {string} code - the code for which data needs to be cleared
* @return {Promise<ResponseModel>} a Promise that resolves with the response from the server
*/
clearData(code: string): Promise<ResponseModel>
{
return new HttpUtils().put(`${DEFAULT_PATH}/clearData/${code}`)
}
}
export default new DatasetService()

View File

@ -11,9 +11,64 @@ const getCurrentUserId = (): number => {
return JSON.parse(localStorage.getItem(token) || '{}').id
}
/**
* Retrieves the text based on the given origin value.
*
* @param {any} i18n - the internationalization object
* @param {string} origin - the origin value to determine the text to retrieve
* @return {string} the text based on the origin value
*/
const getText = (i18n: any, origin: string): string => {
switch (origin) {
case 'CREATED':
return i18n.t('state.common.create')
case 'RUNNING':
return i18n.t('state.common.running')
case 'SUCCESS':
return i18n.t('state.common.success')
case 'FAILURE':
return i18n.t('state.common.failure')
case 'STOPPED':
return i18n.t('state.common.stop')
case 'TIMEOUT':
return i18n.t('state.common.timeout')
case 'QUEUE':
return i18n.t('state.common.queue')
default:
return origin
}
}
/**
* Returns the color based on the origin.
*
* @param {string} origin - The origin value.
* @return {string} The color based on the origin.
*/
const getColor = (origin: string): string => {
switch (origin) {
case 'CREATED':
return 'hsl(220.9 39.3% 11%)'
case 'RUNNING':
return 'hsl(221.2 83.2% 53.3%)'
case 'SUCCESS':
return 'hsl(142.1 76.2% 36.3%)'
case 'FAILURE':
return 'hsl(346.8 77.2% 49.8%)'
case 'STOPPED':
return '#17233d'
case 'TIMEOUT':
return 'hsl(47.9 95.8% 53.1%)'
default:
return 'hsl(24.6 95% 53.1%)'
}
}
export default {
token: token,
menu: menu,
getCurrentUserId: getCurrentUserId,
userEditorConfigure: userEditorConfigure
userEditorConfigure: userEditorConfigure,
getText: getText,
getColor: getColor
}

View File

@ -0,0 +1,68 @@
<template>
<div>
<AlertDialog :default-open="visible">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle class="border-b -mt-4 pb-2">{{ $t('common.content') }}</AlertDialogTitle>
</AlertDialogHeader>
<MdPreview v-if="content" :modelValue="content" style="padding: 0"/>
<AlertDialogFooter class="-mb-4 border-t pt-2">
<Button variant="outline" @click="handlerCancel">{{ $t('common.cancel') }}</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { AlertDialog, AlertDialogContent, AlertDialogFooter, AlertDialogHeader } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { MdPreview } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
export default defineComponent({
name: 'MarkdownPreview',
props: {
isVisible: {
type: Boolean,
default: () => false
},
content: {
type: String,
default: () => ''
}
},
computed: {
visible: {
get(): boolean
{
return this.isVisible
},
set(value: boolean)
{
this.$emit('close', value)
}
}
},
components: {
AlertDialogFooter, Button, AlertDialogHeader, AlertDialogContent, AlertDialog,
MdPreview
},
methods: {
handlerCancel()
{
this.visible = false
}
}
})
</script>
<style scoped>
:deep(.md-editor-preview-wrapper) {
padding: 0;
}
:deep(.default-theme pre) {
margin: 0;
}
</style>

View File

@ -2,7 +2,7 @@
<div>
<Alert v-if="configuration?.headers.length === 0 && !configuration?.message" class="mt-20">
<AlertDescription>
{{ $t('dataset.common.adhocDndTip') }}
{{ $t('dataset.tip.adhocDnd') }}
</AlertDescription>
</Alert>
<Alert v-else-if="configuration?.message" variant="destructive" class="mt-20">

View File

@ -53,6 +53,7 @@ export default defineComponent({
x2Field: this.configuration.chartConfigure?.x2Axis,
yField: this.configuration.chartConfigure?.yAxis
}
console.log(options)
if (!reset) {
instance = new VChart(options, {dom: this.$refs.content as HTMLElement})
instance.renderAsync()

View File

@ -45,12 +45,16 @@ export default defineComponent({
setTimeout(() => {
try {
if (this.configuration) {
let outerRadius = 0.8
if (this.configuration.chartConfigure?.outerRadius) {
outerRadius = this.configuration.chartConfigure?.outerRadius[0]
}
const options = {
type: 'pie',
data: [{values: this.configuration.columns}],
categoryField: this.configuration.chartConfigure?.xAxis,
valueField: this.configuration.chartConfigure?.yAxis,
outerRadius: this.configuration.chartConfigure?.outerRadius[0]
outerRadius: outerRadius
}
if (!reset) {
instance = new VChart(options, {dom: this.$refs.content as HTMLElement})

View File

@ -1,7 +1,7 @@
<template>
<div>
<div class="hidden flex-col md:flex">
<LayoutHeader></LayoutHeader>
<LayoutHeader @changeLanguage="setLangCondition($event)"></LayoutHeader>
<LayoutBreadcrumb></LayoutBreadcrumb>
<div class="flex-1 space-y-4 pl-8 pr-8">
<RouterView></RouterView>
@ -11,14 +11,70 @@
</template>
<script lang="ts">
import {defineComponent} from 'vue'
import LayoutHeader from '@/views/layouts/common/components/LayoutHeader.vue';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import LayoutBreadcrumb from '@/views/layouts/common/components/LayoutBreadcrumb.vue';
import { defineComponent } from 'vue'
import LayoutHeader from '@/views/layouts/common/components/LayoutHeader.vue'
import { Button } from '@/components/ui/button'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import LayoutBreadcrumb from '@/views/layouts/common/components/LayoutBreadcrumb.vue'
import { TokenUtils } from '@/utils/token'
import { ObjectUtils } from '@/utils/object'
import { HttpUtils } from '@/utils/http'
import UserService from '@/services/user'
import CommonUtils from '@/utils/common'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'LayoutContainer',
components: {LayoutBreadcrumb, AvatarFallback, AvatarImage, Avatar, Button, LayoutHeader}
components: {LayoutBreadcrumb, AvatarFallback, AvatarImage, Avatar, Button, LayoutHeader},
beforeUnmount()
{
clearInterval(this.timer)
},
setup()
{
const {locale} = useI18n()
const setLangCondition = (language: string) => {
const prefix = 'language_'
if (language.startsWith(prefix)) {
locale.value = language.substring(prefix.length)
}
else {
locale.value = language
}
}
return {
setLangCondition
}
},
data()
{
return {
timer: null as any
}
},
created()
{
this.handlerInitialize()
},
methods: {
handlerInitialize()
{
const user = TokenUtils.getAuthUser()
if (ObjectUtils.isNotEmpty(user)) {
this.timer = setInterval(() => {
const runTime = new Date().toLocaleTimeString()
console.log(`[DataCap] refresh on time ${runTime}`)
const client = new HttpUtils().getAxios()
client.all([UserService.getMenus(), UserService.getInfo()])
.then(client.spread((fetchMenu, fetchInfo) => {
if (fetchMenu.status && fetchInfo.status) {
localStorage.setItem(CommonUtils.menu, JSON.stringify(fetchMenu.data))
localStorage.setItem(CommonUtils.userEditorConfigure, JSON.stringify(fetchInfo.data.editorConfigure))
}
}))
}, 1000 * 60)
}
}
}
});
</script>

View File

@ -37,6 +37,21 @@
</div>
<!-- Controller -->
<div class="flex items-center">
<div class="mr-3 mt-1.5">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<a target="_blank" href="https://github.com/devlive-community/datacap/issues/new/choose">
<CircleHelp :size="20"/>
</a>
</TooltipTrigger>
<TooltipContent>{{ $t('common.feedback') }}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div class="ml-3 mr-5 mt-0.5">
<LanguageSwitcher @changeLanguage="handlerChangeLanguage($event)"/>
</div>
<div v-if="isLoggedIn" class="flex gap-x-2">
<RouterLink to="/auth/signin">
<Button size="sm" variant="outline">{{ $t('user.common.signin') }}</Button>
@ -114,7 +129,9 @@ import {
navigationMenuTriggerStyle
} from '@/components/ui/navigation-menu'
import NavigationMenuListItem from '@/views/layouts/common/components/components/NavigationMenuListItem.vue'
import { LogOut, User } from 'lucide-vue-next'
import { CircleHelp, LogOut, User } from 'lucide-vue-next'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import LanguageSwitcher from '@/views/layouts/common/components/components/LanguageSwitcher.vue'
interface NavigationItem
{
@ -157,14 +174,23 @@ export default defineComponent({
logout
}
},
methods: {navigationMenuTriggerStyle, cn},
components: {
LanguageSwitcher,
TooltipContent, Tooltip, TooltipTrigger, TooltipProvider,
NavigationMenuLink, NavigationMenuContent, NavigationMenuTrigger, NavigationMenuItem, NavigationMenuList, NavigationMenu,
DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuItem, DropdownMenuShortcut, DropdownMenuContent, DropdownMenuTrigger, DropdownMenu,
AvatarFallback, AvatarImage, Avatar,
NavigationMenuListItem,
Button,
LogOut, User
CircleHelp, LogOut, User
},
methods: {
navigationMenuTriggerStyle,
cn,
handlerChangeLanguage(language: string)
{
this.$emit('changeLanguage', language)
}
}
});
</script>

View File

@ -0,0 +1,39 @@
<template>
<Select v-model="language" @update:modelValue="handlerChangeLang">
<SelectTrigger class="w-[150px]">
<SelectValue :placeholder="$t('common.selectLanguage')"/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{{ $t('common.region.asia.common') }}</SelectLabel>
<SelectItem class="pl-6" value="language_zh">{{ $t('common.region.asia.chineseSimple') }}</SelectItem>
<SelectLabel>{{ $t('common.region.northAmerica.common') }}</SelectLabel>
<SelectItem class="pl-6" value="language_en">{{ $t('common.region.northAmerica.english') }}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select'
export default defineComponent({
name: 'LanguageSwitcher',
components: {
Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue
},
data()
{
return {
language: 'language_zh'
}
},
methods: {
handlerChangeLang()
{
this.$emit('changeLanguage', this.language)
}
}
})
</script>

View File

@ -0,0 +1,113 @@
<template>
<div>
<AlertDialog :default-open="visible">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle class="border-b -mt-4 pb-2">
{{ `[ ${info?.name} ] ${$t('dataset.common.clearData')}` }}
</AlertDialogTitle>
</AlertDialogHeader>
<Alert variant="destructive">
<AlertTitle>{{ $t('dataset.tip.clearData') }}</AlertTitle>
</Alert>
<div class="flex">
<Card class="left text-center w-1/2">
<CardHeader class="border-b p-4">
<CardTitle>{{ $t('dataset.common.totalRows') }}</CardTitle>
</CardHeader>
<CardContent class="mt-3">
<p>{{ info?.totalRows }}</p>
</CardContent>
</Card>
<Card class="ml-3 right text-center w-1/2">
<CardHeader class="border-b p-4">
<CardTitle>{{ $t('dataset.common.totalSize') }}</CardTitle>
</CardHeader>
<CardContent class="mt-3">
<p>{{ info?.totalSize }}</p>
</CardContent>
</Card>
</div>
<AlertDialogFooter class="-mb-4 border-t pt-2">
<Button variant="outline" @click="handlerCancel">{{ $t('common.cancel') }}</Button>
<Button :disabled="loading" @click="handlerSubmit">
<Loader2 v-if="loading" class="w-full justify-center animate-spin"/>
{{ $t('dataset.common.clearData') }}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import DatasetService from '@/services/dataset'
import { DatasetModel } from '@/model/dataset'
import { ToastUtils } from '@/utils/toast'
import { AlertDialog, AlertDialogContent, AlertDialogFooter, AlertDialogHeader } from '@/components/ui/alert-dialog'
import { Alert, AlertTitle } from '@/components/ui/alert'
import { Loader2 } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export default defineComponent({
name: 'DatasetClear',
components: {
CardContent, CardTitle, CardHeader, Card,
Button,
Loader2,
AlertDialogFooter, AlertDialogHeader, AlertTitle, Alert, AlertDialog, AlertDialogContent
},
props: {
isVisible: {
type: Boolean,
default: () => false
},
info: {
type: Object as () => DatasetModel | null
}
},
computed: {
visible: {
get(): boolean
{
return this.isVisible
},
set(value: boolean)
{
this.$emit('close', value)
}
}
},
data()
{
return {
loading: false
}
},
methods: {
handlerSubmit()
{
if (this.info) {
this.loading = true
DatasetService.clearData(this.info.code)
.then((response: { status: boolean; }) => {
if (response.status) {
ToastUtils.success(`${this.$t('dataset.common.clearData')} [ ${this.info?.name} ] ${this.$t('common.successfully')}`)
this.handlerCancel()
}
else {
ToastUtils.error(`${this.$t('dataset.common.clearData')} [ ${this.info?.name} ] ${this.$t('common.fail')}`)
}
})
.finally(() => this.loading = false)
}
},
handlerCancel()
{
this.visible = false
}
}
});
</script>

View File

@ -0,0 +1,134 @@
<template>
<Dialog :open="isVisible" persistent @update:open="handlerCancel">
<DialogContent class="min-w-[60%]">
<DialogHeader class="border-b">
<DialogTitle class="pb-3.5">
{{ `[ ${info?.name} ] ${$t('dataset.common.history')}` }}
</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<CardContent class="grid gap-4">
<TableCommon :loading="loading" :columns="headers" :data="data" :pagination="pagination" @changePage="handlerChangePage">
<template #state="{ row }">
<Badge :style="{backgroundColor: Common.getColor(row?.state)}">
<HoverCard v-if="row?.state === 'FAILURE'">
<HoverCardTrigger as-child>
<Button variant="link">
{{ getStateText(row?.state) }}
</Button>
</HoverCardTrigger>
<HoverCardContent class="w-full">
{{ row?.message }}
</HoverCardContent>
</HoverCard>
<span v-else>{{ getStateText(row?.state) }}</span>
</Badge>
</template>
</TableCommon>
</CardContent>
</DialogContent>
</Dialog>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { CardContent } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog'
import TableCommon from '@/views/components/table/TableCommon.vue'
import { FilterModel } from '@/model/filter'
import { useI18n } from 'vue-i18n'
import { createHistoryHeaders } from './DatasetUtils'
import { PaginationModel, PaginationRequest } from '@/model/pagination'
import DatasetService from '@/services/dataset'
import { DatasetModel } from '@/model/dataset'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Badge } from '@/components/ui/badge'
import Common from '@/utils/common'
export default defineComponent({
name: 'DatasetHistory',
components: {
Badge,
TableCommon,
DialogHeader, Dialog, CardContent, DialogContent,
HoverCard, HoverCardContent, HoverCardTrigger
},
props: {
isVisible: {
type: Boolean,
default: () => false
},
info: {
type: Object as () => DatasetModel | null
}
},
computed: {
Common()
{
return Common
},
visible: {
get(): boolean
{
return this.isVisible
},
set(value: boolean)
{
this.$emit('close', value)
}
}
},
setup()
{
const i18n = useI18n()
const filter: FilterModel = new FilterModel()
const headers = createHistoryHeaders(i18n)
return {
i18n,
filter,
headers
}
},
data()
{
return {
loading: false,
data: [],
pagination: {} as PaginationModel
}
},
created()
{
this.handlerInitialize()
},
methods: {
handlerInitialize()
{
this.loading = true
DatasetService.getHistory(this.info?.code as string, this.filter)
.then((response) => {
if (response.status) {
this.data = response.data.content
this.pagination = PaginationRequest.of(response.data)
}
})
.finally(() => this.loading = false)
},
handlerChangePage(value: PaginationModel)
{
this.filter.page = value.currentPage
this.filter.size = value.pageSize
this.handlerInitialize()
},
handlerCancel()
{
this.visible = false
},
getStateText(origin: string): string
{
return Common.getText(this.i18n, origin)
}
}
})
</script>

View File

@ -41,12 +41,41 @@
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuItem>
<RouterLink :to="`/admin/dataset/info/${row?.code}`" target="_blank" class="flex items-center">
<Info class="mr-2 h-4 w-4"/>
<span>{{ $t('dataset.common.info') }}</span>
</RouterLink>
</DropdownMenuItem>
<DropdownMenuItem :disabled="!isSuccess(row?.state)">
<RouterLink :to="`/admin/dataset/adhoc/${row?.code}`" target="_blank" class="flex items-center">
<BarChart2 class="mr-2 h-4 w-4"/>
<span>{{ $t('dataset.common.adhoc') }}</span>
</RouterLink>
</DropdownMenuItem>
<DropdownMenuSeparator/>
<DropdownMenuItem :disabled="!isSuccess(row?.state)" style="cursor: pointer;" @click="handlerSyncData(row, true)">
<RefreshCcw class="mr-2 h-4 w-4"/>
<span>{{ $t('dataset.common.syncData') }}</span>
</DropdownMenuItem>
<DropdownMenuItem style="cursor: pointer;" @click="handlerHistory(row, true)">
<History class="mr-2 h-4 w-4"/>
<span>{{ $t('dataset.common.history') }}</span>
</DropdownMenuItem>
<DropdownMenuSeparator/>
<DropdownMenuItem :disabled="isSuccess(row?.state)" style="cursor: pointer;" @click="handlerError(row, true)">
<TriangleAlert class="mr-2 h-4 w-4"/>
<span>{{ $t('dataset.common.error') }}</span>
</DropdownMenuItem>
<DropdownMenuItem :disabled="isSuccess(row?.state)" style="cursor: pointer;" @click="handlerRebuild(row, true)">
<CirclePlay v-if="row?.state === 'SUCCESS'" class="mr-2 h-4 w-4"/>
<CircleStop v-else class="mr-2 h-4 w-4"/>
{{ $t('dataset.common.rebuild') }}
</DropdownMenuItem>
<DropdownMenuItem :disabled="!(row?.totalRows > 0)" style="cursor: pointer;" @click="handlerClearData(row, true)">
<SquareX class="mr-2 h-4 w-4"/>
{{ $t('dataset.common.clearData') }}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
@ -54,6 +83,11 @@
</TableCommon>
</CardContent>
</Card>
<DatasetRebuild v-if="rebuildVisible" :is-visible="rebuildVisible" :data="contextData" @close="handlerRebuild(null, false)"/>
<DatasetHistory v-if="historyVisible" :is-visible="historyVisible" :info="contextData" @close="handlerHistory(null, false)"/>
<DatasetSync v-if="syncDataVisible" :is-visible="syncDataVisible" :info="contextData" @close="handlerSyncData(null, false)"/>
<DatasetClear v-if="clearDataVisible" :is-visible="clearDataVisible" :info="contextData" @close="handlerClearData(null, false)"/>
<MarkdownPreview v-if="errorVisible && contextData" :is-visible="errorVisible" :content="'```java\n' + contextData.message + '\n```'" @close="handlerError(null, false)"/>
</div>
</template>
@ -71,7 +105,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import DatasetState from '@/views/pages/admin/dataset/components/DatasetState.vue'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { BarChart2, Cog } from 'lucide-vue-next'
import { BarChart2, CirclePlay, CircleStop, Cog, History, Info, RefreshCcw, SquareX, TriangleAlert } from 'lucide-vue-next'
import {
DropdownMenu,
DropdownMenuContent,
@ -81,10 +115,21 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { DatasetModel } from '@/model/dataset'
import DatasetRebuild from '@/views/pages/admin/dataset/DatasetRebuild.vue'
import DatasetHistory from '@/views/pages/admin/dataset/DatasetHistory.vue'
import DatasetSync from '@/views/pages/admin/dataset/DatasetSync.vue'
import DatasetClear from '@/views/pages/admin/dataset/DatasetClear.vue'
import MarkdownPreview from '@/views/components/markdown/MarkdownView.vue'
export default defineComponent({
name: 'DatasetHome',
components: {
MarkdownPreview,
DatasetClear,
DatasetSync,
DatasetHistory,
DatasetRebuild,
DropdownMenuItem, DropdownMenuGroup, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuContent, DropdownMenuTrigger, DropdownMenu,
HoverCardContent, HoverCardTrigger, HoverCard,
DatasetState,
@ -93,7 +138,7 @@ export default defineComponent({
TooltipTrigger, TooltipProvider, TooltipContent, Tooltip,
TableCommon,
CardContent, CardHeader, CardTitle, Card,
Cog, BarChart2
Cog, BarChart2, CirclePlay, CircleStop, History, RefreshCcw, SquareX, TriangleAlert, Info
},
setup()
{
@ -110,7 +155,13 @@ export default defineComponent({
return {
loading: false,
data: [],
pagination: {} as PaginationModel
pagination: {} as PaginationModel,
contextData: null as DatasetModel | null,
rebuildVisible: false,
historyVisible: false,
syncDataVisible: false,
clearDataVisible: false,
errorVisible: false
}
},
created()
@ -136,6 +187,43 @@ export default defineComponent({
this.filter.size = value.pageSize
this.handlerInitialize()
},
handlerRebuild(record: DatasetModel | null, opened: boolean)
{
if (record && this.isSuccess(record.state)) {
return
}
this.rebuildVisible = opened
this.contextData = record
},
handlerHistory(record: DatasetModel | null, opened: boolean)
{
this.contextData = record
this.historyVisible = opened
},
handlerSyncData(record: DatasetModel | null, opened: boolean)
{
if (record && !this.isSuccess(record.state)) {
return
}
this.contextData = record
this.syncDataVisible = opened
},
handlerClearData(record: DatasetModel | null, opened: boolean)
{
if (record && !(record.totalRows > 0)) {
return
}
this.contextData = record
this.clearDataVisible = opened
if (!opened) {
this.handlerInitialize()
}
},
handlerError(record: DatasetModel | null, opened: boolean)
{
this.errorVisible = opened
this.contextData = record
},
getState(state: Array<any> | null): string | null
{
if (state && state.length > 0) {

View File

@ -0,0 +1,11 @@
<template>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'DatasetInfo'
});
</script>

View File

@ -0,0 +1,87 @@
<template>
<div>
<AlertDialog :default-open="visible">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle class="border-b -mt-4 pb-2">
{{ $t('dataset.common.rebuild') + ' [ ' + data?.name + ' ]' }}
</AlertDialogTitle>
</AlertDialogHeader>
<Alert>
<AlertTitle>{{ $t('dataset.common.rebuildProgress') }}</AlertTitle>
</Alert>
<AlertDialogFooter class="-mb-4 border-t pt-2">
<Button :disabled="loading" @click="handlerRebuild">
<Loader2 v-if="loading" class="w-full justify-center animate-spin"/>
{{ $t('dataset.common.rebuild') }}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import DatasetService from '@/services/dataset'
import { ToastUtils } from '@/utils/toast'
import { DatasetModel } from '@/model/dataset'
import { AlertDialog, AlertDialogContent, AlertDialogFooter, AlertDialogHeader } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { Alert, AlertTitle } from '@/components/ui/alert';
import { Loader2 } from 'lucide-vue-next';
export default defineComponent({
name: 'DatasetRebuild',
components: {Loader2, AlertTitle, Alert, AlertDialogContent, AlertDialogFooter, Button, AlertDialogHeader, AlertDialog},
props: {
isVisible: {
type: Boolean,
default: () => false
},
data: {
type: Object as () => DatasetModel | null
}
},
data()
{
return {
loading: false
}
},
methods: {
handlerRebuild()
{
if (this.data) {
this.loading = true
DatasetService.rebuild(this.data.id)
.then((response) => {
if (response.status) {
ToastUtils.success(`${this.$t('dataset.common.rebuild')} [ ${this.data?.name} ] ${this.$t('common.successfully')}`)
this.handlerCancel()
}
})
.finally(() => {
this.loading = false
})
}
},
handlerCancel()
{
this.visible = false
}
},
computed: {
visible: {
get(): boolean
{
return this.isVisible
},
set(value: boolean)
{
this.$emit('close', value)
}
}
}
});
</script>

View File

@ -0,0 +1,96 @@
<template>
<div>
<AlertDialog :default-open="visible">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle class="border-b -mt-4 pb-2">
{{ `[ ${info?.name} ] ${$t('dataset.common.syncData')}` }}
</AlertDialogTitle>
</AlertDialogHeader>
<Alert>
<AlertTitle>
{{ $t('dataset.tip.syncData') }}
</AlertTitle>
</Alert>
<AlertDialogFooter class="-mb-4 border-t pt-2">
<Button variant="outline" @click="handlerCancel">{{ $t('common.cancel') }}</Button>
<Button :disabled="loading" @click="handlerSubmit">
<Loader2 v-if="loading" class="w-full justify-center animate-spin"/>
{{ $t('dataset.common.syncData') }}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import DatasetService from '@/services/dataset'
import { DatasetModel } from '@/model/dataset'
import { ToastUtils } from '@/utils/toast'
import { AlertDialog, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { Loader2 } from 'lucide-vue-next'
import { Alert, AlertTitle } from '@/components/ui/alert'
export default defineComponent({
name: 'DatasetSync',
components: {
AlertTitle, Alert,
Loader2,
Button,
AlertDialog, AlertDialogContent, AlertDialogTitle, AlertDialogFooter, AlertDialogHeader
},
props: {
isVisible: {
type: Boolean,
default: () => false
},
info: {
type: Object as () => DatasetModel | null
}
},
computed: {
visible: {
get(): boolean
{
return this.isVisible
},
set(value: boolean)
{
this.$emit('close', value)
}
}
},
data()
{
return {
loading: false
}
},
methods: {
handlerSubmit()
{
if (this.info) {
this.loading = true
DatasetService.syncData(this.info.id)
.then(response => {
if (response.status) {
ToastUtils.success(`${this.$t('dataset.common.syncData')} [ ${this.info?.name} ] ${this.$t('common.successfully')}`)
this.handlerCancel()
}
else {
ToastUtils.error(`${this.$t('dataset.common.syncData')} [ ${this.info?.name} ] ${this.$t('common.fail')}`)
}
})
.finally(() => this.loading = false)
}
},
handlerCancel()
{
this.visible = false
}
}
})
</script>

View File

@ -22,6 +22,18 @@ const createHeaders = (i18n: any) => {
]
}
export {
createHeaders
const createHistoryHeaders = (i18n: any) => {
return [
{key: 'id', hidden: true, header: i18n.t('common.id'), width: 80, class: 'text-center'},
{key: 'elapsed', hidden: true, header: i18n.t('common.elapsed'), width: 80, class: 'text-center'},
{key: 'count', hidden: true, header: i18n.t('common.count'), width: 80, class: 'text-center'},
{key: 'createTime', hidden: true, header: i18n.t('common.createTime'), class: 'text-center'},
{key: 'updateTime', hidden: true, header: i18n.t('common.updateTime'), class: 'text-center'},
{key: 'state', hidden: true, header: i18n.t('common.state'), slot: 'state', width: 100, class: 'text-center'}
]
}
export {
createHeaders,
createHistoryHeaders
}

View File

@ -16,7 +16,7 @@
<FormControl>
<Select v-model="formState.expression">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('dataset.tip.selectExpressionTip')"/>
<SelectValue :placeholder="$t('dataset.tip.selectExpression')"/>
</SelectTrigger>
<SelectContent>
<SelectItem :value="Expression.IS_NULL">{{ $t('dataset.common.columnExpressionIsNull') }}</SelectItem>
@ -59,7 +59,7 @@
<FormControl>
<Select v-model="formState.expression">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('dataset.tip.selectExpressionTip')"/>
<SelectValue :placeholder="$t('dataset.tip.selectExpression')"/>
</SelectTrigger>
<SelectContent>
<SelectItem v-if="formState.type === ColumnType.NUMBER" :value="Expression.SUM"> {{ $t('dataset.common.columnExpressionSum') }}</SelectItem>