[Core] Add dataset list

This commit is contained in:
qianmoQ 2024-03-18 15:41:42 +08:00
parent 2c50070dfc
commit 35101f3730
83 changed files with 1125 additions and 17 deletions

View File

@ -14,15 +14,21 @@
"@vee-validate/zod": "^4.12.6",
"@visactor/vchart": "^1.10.0",
"@visactor/vtable": "^0.21.2",
"@vue-flow/background": "^1.3.0",
"@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.4",
"@vue-flow/node-resizer": "^1.3.6",
"@vueuse/core": "^10.9.0",
"axios": "^1.6.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"echarts": "^5.5.0",
"lodash": "^4.17.21",
"lucide-vue-next": "^0.356.0",
"radix-vue": "^1.5.2",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.1",
"vaul-vue": "^0.1.0",
"vee-validate": "^4.12.6",
"vue": "^3.4.21",

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { HoverCardRoot, type HoverCardRootProps, useForwardProps } from 'radix-vue'
const props = defineProps<HoverCardRootProps>()
const forwardedProps = useForwardProps(props)
</script>
<template>
<HoverCardRoot v-bind="forwardedProps">
<slot />
</HoverCardRoot>
</template>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
HoverCardContent,
type HoverCardContentProps,
HoverCardPortal,
useForwardProps,
} from 'radix-vue'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<HoverCardContentProps & { class?: HTMLAttributes['class'] }>(),
{
sideOffset: 4,
},
)
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<HoverCardPortal>
<HoverCardContent
v-bind="forwardedProps"
:class="
cn(
'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class,
)
"
>
<slot />
</HoverCardContent>
</HoverCardPortal>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { HoverCardTrigger, type HoverCardTriggerProps } from 'radix-vue'
const props = defineProps<HoverCardTriggerProps>()
</script>
<template>
<HoverCardTrigger v-bind="props">
<slot />
</HoverCardTrigger>
</template>

View File

@ -0,0 +1,3 @@
export { default as HoverCard } from './HoverCard.vue'
export { default as HoverCardTrigger } from './HoverCardTrigger.vue'
export { default as HoverCardContent } from './HoverCardContent.vue'

View File

@ -44,4 +44,6 @@ export default {
invalidParam: 'If the parameter is invalid, check whether the parameter is correct',
role: 'Role',
cancel: 'Cancel',
scheduler: 'Scheduler',
executor: 'Executor',
}

View File

@ -1,7 +1,8 @@
export default {
common: {
list: 'Dashboard List',
delete: 'Delete Dashboard'
delete: 'Delete Dashboard',
modify: 'Modify Dashboard'
},
tip: {
deleteTip1: 'You are deleting a dashboard. This action permanently deletes the dashboard. Please be sure to confirm your actions before proceeding. ',

View File

@ -1,6 +1,19 @@
export default {
common: {
list: 'Dataset List',
notSpecifiedTitle: 'Not Specified Title',
adhocDndTip: 'Drag the indicator dimension on the left to the corresponding position to query and render the data'
adhocDndTip: 'Drag the indicator dimension on the left to the corresponding position to query and render the data',
syncMode: 'Sync Mode',
syncModeManual: 'Manual',
syncModeTiming: 'Timing synchronization',
syncModeOutSync: 'Out Sync',
totalRows: 'Total Rows',
totalSize: 'Total Size',
complete: 'Complete',
failed: 'Failed',
stateOfStart: 'Start',
stateOfMetadata: 'Metadata State',
stateOfMetadataStarted: 'Metadata Started',
stateOfCreateTable: 'Create Table State'
}
}

View File

@ -43,5 +43,7 @@ export default {
noData: '没有数据可以展示',
invalidParam: '参数无效,请检查参数是否填写正确',
role: '权限',
cancel: '取消'
cancel: '取消',
scheduler: '调度器',
executor: '执行器',
}

View File

@ -1,7 +1,8 @@
export default {
common: {
list: '仪表盘列表',
delete: '删除仪表盘'
delete: '删除仪表盘',
modify: '修改仪表盘'
},
tip: {
deleteTip1: '您正在删除仪表板。此操作将永久删除仪表板。在继续操作之前,请务必确认您的操作。',

View File

@ -1,6 +1,19 @@
export default {
common: {
list: '数据集列表',
notSpecifiedTitle: '未指定标题',
adhocDndTip: '拖拽左侧指标|维度到相应位置即可查询并渲染数据'
adhocDndTip: '拖拽左侧指标|维度到相应位置即可查询并渲染数据',
syncMode: '同步模式',
syncModeManual: '手动',
syncModeTiming: '定时同步',
syncModeOutSync: '不同步',
totalRows: '总行数',
totalSize: '总大小',
complete: '完成',
failed: '失败',
stateOfStarted: '已启动',
stateOfMetadata: '元数据状态',
stateOfMetadataStarted: '元数据已启动',
stateOfCreateTable: '创建表状态',
}
}

View File

@ -0,0 +1,9 @@
export interface ExecuteModel
{
name: string
content: string
env?: object
format?: string
limit?: number
mode?: string
}

View File

@ -99,6 +99,24 @@ const createAdminRouter = (router: any) => {
isRoot: false
},
component: () => import('@/views/pages/admin/dashboard/DashboardHome.vue')
},
{
name: 'info',
path: 'dashboard/info/:id',
meta: {
title: 'common.dashboard',
isRoot: false
},
component: () => import('@/views/pages/admin/dashboard/DashboardInfo.vue')
},
{
name: 'dataset',
path: 'dataset',
meta: {
title: 'common.dataset',
isRoot: false
},
component: () => import('@/views/pages/admin/dataset/DatasetHome.vue')
}
]
}

View File

@ -0,0 +1,29 @@
import { ExecuteModel } from '@/model/execute';
import { BaseService } from '@/services/base'
import { ResponseModel } from '@/model/response'
import { HttpUtils } from '@/utils/http'
const DEFAULT_PATH = '/api/v1/execute'
export class ExecuteService
extends BaseService
{
constructor()
{
super(DEFAULT_PATH)
}
/**
* Executes the given configuration and returns a Promise that resolves to a ResponseModel.
*
* @param {ExecuteModel} configure - the configuration to be executed
* @param {any} cancelToken - a token to cancel the request
* @return {Promise<ResponseModel>} a Promise that resolves to a ResponseModel
*/
execute(configure: ExecuteModel, cancelToken: any): Promise<ResponseModel>
{
return new HttpUtils().post(`${DEFAULT_PATH}`, JSON.stringify(configure), cancelToken)
}
}
export default new ExecuteService()

View File

@ -0,0 +1,14 @@
import { BaseService } from '@/services/base'
const DEFAULT_PATH = '/api/v1/report'
export class ReportService
extends BaseService
{
constructor()
{
super(DEFAULT_PATH)
}
}
export default new ReportService()

View File

@ -0,0 +1,17 @@
/**
* Returns an array of unique values extracted from the specified key in each object within the provided array.
*
* @param {string} key - The key to extract values from each object in the array.
* @param {any[]} columns - The array of objects to extract values from.
* @return {any[]} An array containing unique values extracted from the specified key in each object.
*/
export function getValueByKey(key: string, columns: []): any[]
{
const container: any[] = []
columns.forEach(column => {
if (container.indexOf(column[key]) === -1) {
container.push(column[key])
}
});
return container
}

View File

@ -0,0 +1,9 @@
import { EchartsType } from '@/views/components/echarts/EchartsType'
export class EchartsConfigure
{
headers: [] | undefined
types: [] | undefined
columns: [] | undefined
type: EchartsType = EchartsType.LINE
}

View File

@ -0,0 +1,250 @@
<template>
<div>
<Modal v-model="visible"
placement="right"
:title="$t('common.visualization')"
:mask-closable="false"
:width="'90%'"
:transfer="false">
<Form :inline="true">
<FormItem :label="$t('common.name')"
:label-width="80">
<Input v-model="formState.name"/>
</FormItem>
<FormItem :label="$t('common.realtime')"
:label-width="80">
<Switch v-model="formState.realtime"/>
</FormItem>
</Form>
<Layout v-if="configure">
<Layout>
<Content>
<EchartsPreview :key="referKey"
:height="'500px'"
:configure="chartOptions">
</EchartsPreview>
</Content>
<Sider style="background-color: #FFFFFF;"
hide-trigger>
<Form label-position="left"
:label-width="50">
<Card padding="0"
dis-hover
:bordered="false">
<template #title>
<RadioGroup v-model="chartType"
type="button"
@on-change="handlerChangeValue('Type')">
<Space>
<Radio label="line">
<FontAwesomeIcon icon="chart-line">
</FontAwesomeIcon>
</Radio>
<Radio label="bar">
<FontAwesomeIcon icon="chart-bar">
</FontAwesomeIcon>
</Radio>
</Space>
</RadioGroup>
</template>
<Collapse v-if="chartType"
v-model="collapseValue"
accordion>
<Panel name="xAxis">
{{ $t('common.xAxis') }}
<template #content>
<FormItem :label="$t('common.column')">
<Select v-model="defaultConfigure.xAxis" @on-change="handlerChangeValue('xAxis')">
<Option v-for="value of configure.headers" :value="value" v-bind:key="value">{{ value }}</Option>
</Select>
</FormItem>
<FormItem :label="$t('common.type')">
<RadioGroup v-model="chartOptions.xAxis.type" type="button" size="small" @on-change="handlerChangeValue">
<Radio label="value">{{ $t('common.column') }}</Radio>
<Radio label="category">{{ $t('common.tag') }}</Radio>
</RadioGroup>
</FormItem>
</template>
</Panel>
<Panel v-if="chartOptions.xAxis && !chartOptions.yAxis.disabled" disabled name="yAxis">
{{ $t('common.yAxis') }}
<template #content>
<FormItem label="Value">
<Select v-model="defaultConfigure.yAxis">
<Option v-for="value of configure.headers" :value="value" v-bind:key="value">{{ value }}</Option>
</Select>
</FormItem>
<FormItem label="Type">
<RadioGroup v-model="chartOptions.yAxis.type" @on-change="handlerChangeValue">
<Radio label="value"></Radio>
<Radio label="category"></Radio>
</RadioGroup>
</FormItem>
</template>
</Panel>
<Panel v-if="chartOptions.xAxis" name="Series">
{{ $t('common.data') }}
<template #content>
<FormItem :label="$t('common.column')">
<Select v-model="defaultConfigure.series" @on-change="handlerChangeValue('Series')">
<Option v-for="value of configure.headers" :value="value" v-bind:key="value">{{ value }}</Option>
</Select>
</FormItem>
</template>
</Panel>
</Collapse>
</Card>
</Form>
</Sider>
</Layout>
</Layout>
<Result v-else
type="warning">
<template #desc>
</template>
<template #actions>
</template>
</Result>
<template #footer>
<Button type="primary"
:loading="loading"
@click="handlerPublish">
{{ $t('common.publish') }}
</Button>
</template>
</Modal>
</div>
</template>
<script lang="ts">
import {defineComponent} from 'vue';
import {ChartConfigure} from "@/components/editor/echarts/configure/ChartConfigure";
import {getValueByKey} from "./DataUtils";
import {getTimestamp} from "@/common/DateCommon";
import {SeriesConfigure} from "@/components/editor/echarts/configure/SeriesConfigure";
import {isEmpty} from "lodash";
import {EchartsConfigure} from "@/components/editor/echarts/EchartsConfigure";
import EchartsPreview from "@/components/editor/echarts/EchartsPreview.vue";
import {AxisConfigure} from "@/components/editor/echarts/configure/AxisConfigure";
import {FontAwesomeIcon} from "@fortawesome/vue-fontawesome";
import ReportService from "@/services/admin/ReportService";
export default defineComponent({
name: 'EchartsEditor',
components: {FontAwesomeIcon, EchartsPreview},
props: {
isVisible: {
type: Boolean,
default: () => false
},
configure: {
type: EchartsConfigure,
default: () => null
},
sourceId: {
type: Number
},
query: {
type: String
}
},
data()
{
return {
collapseValue: 'xAxis',
referKey: 0,
defaultConfigure: {
xAxis: '',
yAxis: '',
series: ''
},
chartOptions: null as ChartConfigure,
chartType: null,
formState: {name: null, realtime: null, type: 'QUERY', configure: null, source: {id: null}, query: null},
loading: false
}
},
created()
{
this.handlerInitialize();
},
methods: {
handlerInitialize()
{
this.chartOptions = new ChartConfigure();
},
handlerChangeValue(type: string)
{
this.referKey = getTimestamp();
switch (type) {
case 'xAxis':
this.chartOptions.xAxis = new AxisConfigure();
this.chartOptions.xAxis.data = getValueByKey(this.defaultConfigure.xAxis, this.configure.columns);
this.chartOptions.xAxis.meta.column = this.defaultConfigure.xAxis;
this.chartOptions.yAxis = new AxisConfigure();
this.chartOptions.yAxis.type = 'value';
this.chartOptions.yAxis.data = getValueByKey(this.defaultConfigure.yAxis, this.configure.columns);
this.chartOptions.yAxis.disabled = true;
this.chartOptions.yAxis.meta.column = this.defaultConfigure.yAxis;
break;
case 'Series': {
const series: SeriesConfigure = new SeriesConfigure();
series.data = getValueByKey(this.defaultConfigure.series, this.configure.columns);
series.type = this.chartType;
series.meta.column = this.defaultConfigure.series;
this.chartOptions.series = [];
this.chartOptions.series.push(series);
break;
}
case 'Type': {
if (this.chartOptions.series) {
this.chartOptions.series[0].type = this.chartType;
}
}
}
this.handlerSetDefaultValue();
},
handlerSetDefaultValue()
{
if (isEmpty(this.defaultConfigure.xAxis)) {
this.chartOptions.xAxis.data = null;
}
if (isEmpty(this.defaultConfigure.yAxis)) {
this.chartOptions.yAxis.data = null;
}
},
handlerPublish()
{
this.loading = true;
this.formState.configure = JSON.stringify(this.chartOptions);
this.formState.source.id = this.sourceId;
this.formState.query = this.query;
ReportService.saveOrUpdate(this.formState)
.then(response => {
if (response.status) {
this.$Message.success(this.$t('report.publishSuccess').replace('REPLACE_NAME', this.formState.name));
this.visible = false;
}
})
.finally(() => this.loading = false);
}
},
computed: {
visible: {
get(): boolean
{
return this.isVisible;
},
set(value: boolean)
{
this.$emit('close', value);
}
}
}
});
</script>
<style scoped>
.ivu-drawer-body {
padding: 0px;
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<div>
<CircularLoading v-if="loading" :show="loading"/>
<div :style="{width: width, height: height, padding: '0'}" :id="key"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import * as echarts from 'echarts'
import { v4 as uuidv4 } from 'uuid'
import CircularLoading from '@/views/components/loading/CircularLoading.vue'
import { ChartConfigure } from './configure/ChartConfigure'
import ReportService from '@/services/report'
import { getValueByKey } from './DataUtils'
import { SeriesConfigure } from './configure/SeriesConfigure'
import { ExecuteModel } from '@/model/execute'
import ExecuteService from '@/services/execute'
export default defineComponent({
name: 'EchartsPreview',
components: {CircularLoading},
props: {
width: {
type: String,
default: () => '100%'
},
height: {
type: String,
default: () => '300px'
},
configure: {
type: Object as () => ChartConfigure
},
id: {
type: Number
}
},
watch: {
width: 'handlerInitialize',
height: 'handlerInitialize'
},
created()
{
this.handlerInitialize()
},
data()
{
return {
loading: false,
key: null as string | null
}
},
methods: {
handlerInitialize()
{
this.key = uuidv4()
setTimeout(() => {
try {
if (this.key) {
const echartsContainer = document.getElementById(this.key)
const echartsChart = echarts.init(echartsContainer)
echartsChart.resize()
if (this.id) {
this.loading = true
ReportService.getById(this.id)
.then(response => {
if (response.status && response.data.realtime) {
if (response.data.source) {
const queryConfigure: ExecuteModel = {
name: response.data.source.id,
content: response.data.query,
format: 'JSON',
mode: 'REPORT'
}
ExecuteService.execute(queryConfigure, null)
.then(response => {
const configure: any = this.configure as ChartConfigure;
configure.xAxis.data = getValueByKey(configure.xAxis.meta.column, response.data.columns)
configure.yAxis.data = getValueByKey(configure.yAxis.meta.column, response.data.columns)
configure.series.forEach((item: SeriesConfigure) => {
const series: SeriesConfigure = item as SeriesConfigure
series.data = getValueByKey(series.meta.column, response.data.columns)
})
echartsChart.setOption(configure)
})
.finally(() => this.loading = false)
}
}
else {
echartsChart.setOption(this.configure)
}
})
.finally(() => this.loading = false)
}
else {
echartsChart.setOption(this.configure)
}
}
}
catch (e) {
console.error(e)
}
}, 0)
}
}
});
</script>

View File

@ -0,0 +1,5 @@
export enum EchartsType
{
LINE = ('line'),
BAR = ('bar')
}

View File

@ -0,0 +1,7 @@
export class AxisConfigure
{
type = 'category'
data: any[] = []
disabled = false
meta = {column: null}
}

View File

@ -0,0 +1,15 @@
import { SeriesConfigure } from '@/views/components/echarts/configure/SeriesConfigure'
import { AxisConfigure } from '@/views/components/echarts/configure/AxisConfigure'
import { TooltipConfigure } from '@/views/components/echarts/configure/TooltipConfigure'
export class ChartConfigure
{
xAxis: AxisConfigure | undefined
yAxis: AxisConfigure | undefined
series: Array<SeriesConfigure> | undefined
tooltip: TooltipConfigure = new TooltipConfigure()
}
export default function toOptions(options: ChartConfigure): any {
return JSON.stringify(options)
}

View File

@ -0,0 +1,11 @@
import { EchartsType } from '@/views/components/echarts/EchartsType'
export class SeriesConfigure
{
data: any[] = []
type: EchartsType = EchartsType.LINE
smooth = true
meta = {
column: ''
}
}

View File

@ -0,0 +1,3 @@
export class TooltipConfigure {
trigger = 'axis'
}

View File

@ -0,0 +1,12 @@
class CommonUtils
{
getXAxis(type = 'value', items: any[])
{
return {
type: type,
data: items
}
}
}
export default new CommonUtils()

View File

@ -1,10 +1,10 @@
<template>
<div>
<AlertDialog :open="visible" :default-open="visible">
<AlertDialogContent>
<AlertDialogContent v-if="data">
<AlertDialogHeader>
<AlertDialogTitle>
{{ $t('dashboard.common.delete') + ' [ ' + data?.name + ' ]' }}
{{ $t('dashboard.common.delete') + ' [ ' + data.name + ' ]' }}
</AlertDialogTitle>
<AlertDialogDescription>
<Alert variant="destructive" class="mt-3">
@ -14,7 +14,7 @@
{{ $t('dashboard.tip.deleteTip2') }}
</Alert>
<Alert class="mt-3">
{{ $t('dashboard.tip.deleteTip3').replace('$NAME', data?.name) }}
{{ $t('dashboard.tip.deleteTip3').replace('$NAME', data.name) }}
<Input v-model="inputValue" class="mt-3"/>
</Alert>
</AlertDialogDescription>
@ -23,7 +23,7 @@
<Button variant="outline" @click="handlerCancel">
{{ $t('common.cancel') }}
</Button>
<Button :disabled="inputValue !== data?.name || loading" @click="handlerDelete">
<Button :disabled="inputValue !== data.name || loading" @click="handlerDelete">
<Loader2 v-if="loading" class="w-full justify-center animate-spin"/>
{{ $t('dashboard.common.delete') }}
</Button>
@ -58,7 +58,8 @@ export default defineComponent({
default: () => false
},
data: {
type: Object as () => DashboardModel
type: Object as () => DashboardModel | null,
default: null
}
},
computed: {
@ -77,7 +78,7 @@ export default defineComponent({
{
return {
loading: false,
inputValue: null
inputValue: ''
}
},
methods: {

View File

@ -19,6 +19,12 @@
<Cog :size="20"/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<RouterLink :to="`/admin/dashboard/info/${item.id}`" target="_blank" class="flex items-center">
<Pencil :size="15" class="mr-1"/>
{{ $t('dashboard.common.modify') }}
</RouterLink>
</DropdownMenuItem>
<DropdownMenuItem @click="handlerDelete(true, item)">
<Trash :size="15" class="mr-1"/>
{{ $t('dashboard.common.delete') }}
@ -43,20 +49,20 @@
<script lang="ts">
import { defineComponent } from 'vue'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Cog, Loader2, Trash } from 'lucide-vue-next';
import { Cog, Loader2, Pencil, Trash } from 'lucide-vue-next'
import DashboardService from '@/services/dashboard'
import { FilterModel } from '@/model/filter'
import { PaginationModel, PaginationRequest } from '@/model/pagination'
import { DashboardModel } from '@/model/dashboard'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import DashboardDelete from '@/views/pages/admin/dashboard/DashboardDelete.vue';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import DashboardDelete from '@/views/pages/admin/dashboard/DashboardDelete.vue'
export default defineComponent({
name: 'DashboardHome',
components: {
DashboardDelete,
DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuContent, DropdownMenuTrigger, DropdownMenu,
Loader2, Cog, Trash,
Loader2, Cog, Trash, Pencil,
CardContent, CardHeader, CardTitle, Card
},
setup()
@ -94,7 +100,7 @@ export default defineComponent({
})
.finally(() => this.loading = false)
},
handlerDelete(opened: boolean, data: any)
handlerDelete(opened: boolean, data: DashboardModel | null)
{
this.deleteVisible = opened
this.dataInfo = data

View File

@ -1,12 +1,59 @@
<template>
<div class="w-full">
<CircularLoading v-if="loading" :show="loading" class="mt-20"></CircularLoading>
<div v-else>
<DashboardEditor :elements="nodes" :source-configure="sourceConfigure"/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import CircularLoading from '@/views/components/loading/CircularLoading.vue'
import DashboardService from '@/services/dashboard'
import { ToastUtils } from '@/utils/toast'
import { useRouter } from 'vue-router'
import { DashboardModel } from '@/model/dashboard'
import DashboardEditor from '@/views/pages/admin/dashboard/components/DashboardEditor.vue'
export default defineComponent({
name: 'DashboardInfo'
name: 'DashboardInfo',
components: {DashboardEditor, CircularLoading},
data()
{
return {
loading: false,
saving: false,
configure: null as DashboardModel | null,
nodes: [],
sourceConfigure: null as DashboardModel | null
}
},
created()
{
this.handlerInitialize()
},
methods: {
handlerInitialize()
{
this.loading = true
const router = useRouter()
const params = router.currentRoute.value.params
DashboardService.getById(params['id'] as unknown as number)
.then(response => {
if (response.status) {
const configure = JSON.parse(response.data.configure)
configure.nodes?.forEach((node: any) => {
this.nodes.push({id: node.id, type: node.type, label: node.label, position: node.position, data: node.data})
})
this.sourceConfigure = response.data
}
else {
ToastUtils.error(response.message)
}
})
.finally(() => this.loading = false)
}
}
})
</script>

View File

@ -0,0 +1,64 @@
<template>
<div>
<CircularLoading v-if="loading" :show="loading"/>
<Card v-else class="mt-3" v-for="node in data">
<CardHeader>
<CardTitle>{{ node.name }}</CardTitle>
</CardHeader>
<CardContent>
<div :draggable="true" @dragstart="onDragStart($event, node)">
<EchartsPreview :key="node.id" :height="'200px'" :id="node.id" :configure="JSON.parse(node.configure)"/>
</div>
</CardContent>
</Card>
</div>
</template>
<script lang="ts">
import '../style.css'
import { defineComponent } from 'vue'
import ReportService from '@/services/report'
import CircularLoading from '@/views/components/loading/CircularLoading.vue'
import { FilterModel } from '@/model/filter'
import EchartsPreview from '@/views/components/echarts/EchartsPreview.vue'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export default defineComponent({
name: 'DashboardChart',
components: {
CardContent, CardHeader, CardTitle, Card,
EchartsPreview,
CircularLoading
},
setup()
{
const onDragStart = (event: { dataTransfer: { setData: (arg0: string, arg1: any) => void; effectAllowed: string; }; }, node: any) => {
if (event.dataTransfer) {
event.dataTransfer.setData('application/vueflow', JSON.stringify(node))
event.dataTransfer.effectAllowed = 'move'
}
}
return {
onDragStart
}
},
data()
{
return {
loading: false,
data: []
}
},
created()
{
this.loading = true
const filter: FilterModel = new FilterModel()
ReportService.getAll(filter)
.then((response) => {
if (response.status) {
this.data = response.data.content
}
})
.finally(() => this.loading = false)
}
});
</script>

View File

@ -0,0 +1,135 @@
<template>
<div class="w-full h-full" :style="{width: width + 'px', height: height + 'px'}">
<VueFlow :default-viewport="{ zoom: 1.5 }" :min-zoom="0.2" :max-zoom="4" @dragover="onDragOver">
<template #node-resizable="{ data }">
<DashboardNode :configure="JSON.parse(data.configure)" :id="data.id"/>
</template>
<Controls/>
<Background/>
<Panel position="top-right">
<Space>
<Tooltip :content="$t('pipeline.resetTransform')">
<Button type="primary"
shape="circle"
size="small"
@click="resetTransform">
<FontAwesomeIcon icon="rotate"/>
</Button>
</Tooltip>
<Tooltip :content="$t('common.save')">
<Button type="primary"
shape="circle"
size="small"
@click="saveConfigure(configure, true)">
<FontAwesomeIcon icon="save"/>
</Button>
</Tooltip>
</Space>
</Panel>
</VueFlow>
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick, ref, watch } from 'vue'
import { Panel, useVueFlow, VueFlow } from '@vue-flow/core'
import { Controls } from '@vue-flow/controls'
import { v4 as uuidv4 } from 'uuid'
import { Background } from '@vue-flow/background'
import DashboardNode from '@/views/pages/admin/dashboard/components/DashboardNode.vue'
import DashboardChart from '@/views/pages/admin/dashboard/components/DashboardChart.vue'
export default defineComponent({
name: 'DashboardEditor',
components: {
DashboardNode, DashboardChart,
Background, VueFlow, Controls, Panel
},
props: {
elements: {
type: Array,
default: () => ref([])
},
sourceConfigure: {
type: Object
}
},
setup(props, {emit})
{
const configureVisible = ref(false);
const configure = ref({
id: props.sourceConfigure ? props.sourceConfigure.id : null,
name: props.sourceConfigure ? props.sourceConfigure.name : null,
configure: null,
version: 'V1',
reports: []
});
const {findNode, onConnect, addEdges, addNodes, project, vueFlowRef, setTransform, toObject} = useVueFlow({nodes: []})
props.elements.forEach((item) => {
const newNode = {id: item.id, position: item.position, label: item.label, type: 'resizable', data: item.data}
addNodes([newNode])
})
onConnect((params: any) => addEdges(params))
const onDrop = (event: { dataTransfer: { getData: (arg0: string) => any; }; clientX: number; clientY: number; }) => {
const data = JSON.parse(event.dataTransfer?.getData('application/vueflow'));
const {left, top} = vueFlowRef.value.getBoundingClientRect();
const position = project({x: event.clientX - left, y: event.clientY - top});
const newNode = {id: `${uuidv4()}`, position, label: `${data.name}`, type: 'resizable', data: data}
addNodes([newNode])
nextTick(() => {
const node = findNode(newNode.id)
const stop = watch(
() => node.dimensions,
(dimensions) => {
if (dimensions.width > 0 && dimensions.height > 0) {
node.position = {x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2}
stop()
}
},
{deep: true, flush: 'post'}
)
})
}
const onDragOver = (event: { preventDefault: () => void; dataTransfer: { dropEffect: string; }; }) => {
event.preventDefault()
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move'
}
}
const resetTransform = () => {
return setTransform({x: 0, y: 0, zoom: 1});
}
const saveConfigure = (configure: any, opened: boolean) => {
configureVisible.value = opened;
if (!opened) {
const obj = toObject()
configure.configure = JSON.stringify(obj);
obj.nodes.forEach((item: { data: { id: any } }) => {
configure.reports.push({id: item.data.id})
})
emit('onCommit', configure);
}
}
const height = window.innerHeight - 120
const width = window.innerWidth - 35
return {
onDrop,
onDragOver,
resetTransform,
saveConfigure,
configure,
configureVisible,
height,
width
}
}
});
</script>

View File

@ -0,0 +1,24 @@
<template>
<div>
<NodeResizer min-width="100" min-height="30"/>
<EchartsPreview :width="'250px'" :height="'200px'" :id="id" :configure="configure"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { NodeResizer } from '@vue-flow/node-resizer'
import EchartsPreview from '@/views/components/echarts/EchartsPreview.vue'
export default defineComponent({
name: 'DashboardNode',
components: {NodeResizer, EchartsPreview},
props: {
configure: {
type: Object
},
id: {
type: Number
}
}
});
</script>

View File

@ -50,10 +50,12 @@ import { defineComponent } from 'vue'
import { GridItem, GridLayout } from 'vue3-grid-layout-next'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import VisualView from '@/views/components/visual/VisualView.vue'
import EchartsPreview from '@/views/components/echarts/EchartsPreview.vue'
export default defineComponent({
name: 'DashboardView',
components: {
EchartsPreview,
VisualView,
CardContent, CardHeader, CardTitle, Card,
GridItem, GridLayout

View File

@ -0,0 +1,19 @@
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';
@import '@vue-flow/controls/dist/style.css';
@import '@vue-flow/node-resizer/dist/style.css';
.dndflow {
color: #fff;
font-weight: 700;
font-size: 12px;
flex-direction: column;
display: flex;
height: 100%;
}
.dndflow .nodes > * {
margin-bottom: 10px;
cursor: grab;
font-weight: 500;
}

View File

@ -0,0 +1,118 @@
<template>
<div class="w-full">
<Card>
<CardHeader class="border-b p-4">
<CardTitle>{{ $t('dataset.common.list') }}</CardTitle>
</CardHeader>
<CardContent>
<TableCommon :loading="loading" :columns="headers" :data="data" :pagination="pagination" @changePage="handlerChangePage">
<template #source="{row}">
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Avatar size="sm">
<AvatarImage :src="'/static/images/plugin/' + row?.source.type + '.png'" :alt="row?.source.type"/>
<AvatarFallback>{{ row?.source.type }}</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent align="center">{{ row?.source.type }}</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
<template #syncMode="{ row }">
<Badge v-if="row?.syncMode === 'MANUAL'">{{ $t('dataset.common.syncModeManual') }}</Badge>
<Badge v-else-if="row?.syncMode === 'TIMING'">{{ $t('dataset.common.syncModeTiming') }}</Badge>
<Badge v-else-if="row?.syncMode === 'OUT_SYNC'">{{ $t('dataset.common.syncModeOutSync') }}</Badge>
</template>
<template #state="{ row }">
<HoverCard>
<HoverCardTrigger>{{ getState(row?.state) }}</HoverCardTrigger>
<HoverCardContent>
<DatasetState class="mt-[25px]" :states="row?.state"/>
</HoverCardContent>
</HoverCard>
</template>
</TableCommon>
</CardContent>
</Card>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import TableCommon from '@/views/components/table/TableCommon.vue'
import { FilterModel } from '@/model/filter'
import { useI18n } from 'vue-i18n'
import { PaginationModel, PaginationRequest } from '@/model/pagination'
import { createHeaders } from './DatasetUtils'
import DatasetService from '@/services/dataset'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
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';
export default defineComponent({
name: 'DatasetHome',
components: {
HoverCardContent, HoverCardTrigger, HoverCard,
DatasetState,
Badge,
AvatarFallback, AvatarImage, Avatar,
TooltipTrigger, TooltipProvider, TooltipContent, Tooltip,
TableCommon,
CardContent, CardHeader, CardTitle, Card
},
setup()
{
const filter: FilterModel = new FilterModel()
const headers = createHeaders(useI18n())
return {
filter,
headers
}
},
data()
{
return {
loading: false,
data: [],
pagination: {} as PaginationModel
}
},
created()
{
this.handlerInitialize()
},
methods: {
handlerInitialize()
{
this.loading = true
DatasetService.getAll(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()
},
getState(state: Array<any> | null): string | null
{
if (state && state.length > 0) {
return state[state.length - 1]
}
return null
}
}
})
</script>

View File

@ -0,0 +1,27 @@
/**
* Generates headers for a table based on i18n translations.
*
* @param {any} i18n - the internationalization object for translations
* @return {Array} an array of header objects for the table
*/
const createHeaders = (i18n: any) => {
return [
{key: 'id', hidden: true, header: i18n.t('common.id'), width: 80},
{key: 'name', hidden: true, header: i18n.t('common.name'), width: 100},
{key: 'description', hidden: true, header: i18n.t('common.description'), width: 200},
{key: 'source', hidden: true, header: i18n.t('common.source'), slot: 'source', width: 100},
{key: 'syncMode', hidden: true, header: i18n.t('dataset.common.syncMode'), slot: 'syncMode', width: 80},
{key: 'scheduler', hidden: true, header: i18n.t('common.scheduler'), width: 80},
{key: 'executor', hidden: true, header: i18n.t('common.executor'), width: 80},
{key: 'state', hidden: true, header: i18n.t('common.state'), slot: 'state', width: 80, class: 'text-center'},
{key: 'totalRows', hidden: true, header: i18n.t('dataset.common.totalRows')},
{key: 'totalSize', hidden: true, header: i18n.t('dataset.common.totalSize')},
{key: 'createTime', hidden: true, header: i18n.t('common.createTime')},
{key: 'updateTime', hidden: true, header: i18n.t('common.updateTime')},
{key: 'action', hidden: true, header: i18n.t('common.action'), slot: 'action', width: 80, class: 'text-right'}
]
}
export {
createHeaders
}

View File

@ -0,0 +1,53 @@
<template>
<div v-for="state in states" :key="state">
<Alert class="flex items-center" v-if="state === 'START'">
<AlertTitle class="flex-grow">
{{ $t('dataset.common.stateOfStarted') }}
</AlertTitle>
<AlertDescription class="ml-4">
{{ $t('dataset.complete') }}
</AlertDescription>
</Alert>
<Alert class="flex items-center mt-2" v-else-if="state.startsWith('METADATA_')">
<AlertTitle class="flex-grow">
{{ $t('dataset.common.stateOfMetadata') }}
</AlertTitle>
<AlertDescription class="ml-4">
<span v-if="state.endsWith('SUCCESS')" class="content">
{{ $t('dataset.common.complete') }}
</span>
<span v-else-if="state.endsWith('FAILED')" class="content">
{{ $t('dataset.common.failed') }}
</span>
</AlertDescription>
</Alert>
<Alert class="flex items-center mt-2" v-else-if="state.startsWith('TABLE_')">
<AlertTitle class="flex-grow">
{{ $t('dataset.common.stateOfCreateTable') }}
</AlertTitle>
<AlertDescription class="ml-4">
<span v-if="state.endsWith('SUCCESS')" class="content">
{{ $t('dataset.common.complete') }}
</span>
<span v-else-if="state.endsWith('FAILED')" class="content">
{{ $t('dataset.common.failed') }}
</span>
</AlertDescription>
</Alert>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Alert, AlertTitle } from '@/components/ui/alert'
export default defineComponent({
name: 'DatasetState',
components: {AlertTitle, Alert},
props: {
states: {
type: Array
}
}
});
</script>