[Core] [Refactor] [UI] [Source] Add delete

This commit is contained in:
qianmoQ 2024-04-06 23:08:56 +08:00
parent ea8a5e8b34
commit 6a92f24e51
11 changed files with 221 additions and 457 deletions

View File

@ -78,6 +78,7 @@ export default {
test: 'Test', test: 'Test',
field: 'Field', field: 'Field',
upload: 'Upload', upload: 'Upload',
deleteData: 'Delete Data',
tip: { tip: {
pageNotNetwork: 'Oops! Unable to connect to the network, please check if the network is normal!' pageNotNetwork: 'Oops! Unable to connect to the network, please check if the network is normal!'
} }

View File

@ -16,8 +16,13 @@ export default {
ssl: 'SSL', ssl: 'SSL',
file: 'File', file: 'File',
create: 'Create Source', create: 'Create Source',
delete: 'Delete Source [ $NAME ]'
}, },
tip: { tip: {
selectSource: 'Please select a source' selectSource: 'Please select a source',
deleteSuccess: 'Delete source [ $NAME ] success',
deleteAlert1: 'You are deleting a data source. This action permanently deletes all data and configurations associated with that data source. Please be sure to confirm your actions before proceeding.',
deleteAlert2: 'Warning: Doing this will not be undone. All data and configurations associated with that data source will be permanently deleted.',
deleteAlert3: 'To confirm, type [ $NAME ] in the box below'
} }
} }

View File

@ -78,6 +78,7 @@ export default {
test: '测试', test: '测试',
field: '属性', field: '属性',
upload: '上传', upload: '上传',
deleteData: '删除数据',
tip: { tip: {
pageNotNetwork: '哎呀!无法连接到网络,请检查网络是否正常!' pageNotNetwork: '哎呀!无法连接到网络,请检查网络是否正常!'
} }

View File

@ -15,9 +15,14 @@ export default {
database: '数据库', database: '数据库',
ssl: 'SSL', ssl: 'SSL',
file: '文件', file: '文件',
create: '创建数据源' create: '创建数据源',
delete: '删除数据源 [ $NAME ]'
}, },
tip: { tip: {
selectSource: '请选择数据源' selectSource: '请选择数据源',
deleteSuccess: '删除数据源 [ $NAME ] 成功',
deleteAlert1: '您正在删除数据源。此操作将永久删除所有与该数据源相关的数据和配置。请务必在继续操作之前确认您的操作。',
deleteAlert2: '警告:执行此操作将不可逆。所有与该数据源相关的数据和配置都会被永久删除。',
deleteAlert3: '要确认,请在下面的框中键入 [ $NAME ]'
} }
} }

View File

@ -0,0 +1,108 @@
<template>
<Dialog :is-visible="visible" :title="title as string" :width="'40%'">
<div class="pl-3 pr-3 space-y-2">
<Alert type="error">
<template #description>{{ $t('source.tip.deleteAlert1') }}</template>
</Alert>
<Alert type="error">
<template #description>{{ $t('source.tip.deleteAlert2') }}</template>
</Alert>
<Alert type="info">
<template #description>{{ $t('source.tip.deleteAlert3').replace('$NAME', info?.name as string) }}</template>
</Alert>
<Input v-model="inputValue"/>
</div>
<template #footer>
<div class="space-x-5">
<Button variant="outline" size="sm" @click="handlerCancel">
{{ $t('common.cancel') }}
</Button>
<Button size="sm" variant="destructive" :loading="loading" :disabled="loading || inputValue !== info?.name" @click="handlerSubmit()">
{{ title }}
</Button>
</div>
</template>
</Dialog>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Dialog from '@/views/ui/dialog'
import { SourceModel } from '@/model/source'
import SourceService from '@/services/source'
import { ToastUtils } from '@/utils/toast'
import Button from '@/views/ui/button'
import Alert from '@/views/ui/alert'
import { Input } from '@/components/ui/input'
export default defineComponent({
name: 'SourceDelete',
components: {
Input,
Alert,
Button,
Dialog
},
computed: {
visible: {
get(): boolean
{
return this.isVisible
},
set(value: boolean)
{
this.$emit('close', value)
}
}
},
props: {
isVisible: {
type: Boolean
},
info: {
type: Object as () => SourceModel | null
}
},
data()
{
return {
title: null as string | null,
loading: false,
inputValue: ''
}
},
created()
{
this.handlerInitialize()
},
methods: {
handlerInitialize()
{
if (this.info) {
this.title = `${ this.$t('source.common.delete').replace('$NAME', this.info.name as string) }`
}
},
handlerSubmit()
{
if (this.info) {
this.loading = true
SourceService.deleteById(this.info.id as number)
.then((response) => {
if (response.status) {
ToastUtils.success(this.$t('source.tip.deleteSuccess').replace('$NAME', this.info?.name as string))
this.handlerCancel()
}
else {
ToastUtils.error(response.message)
}
})
.finally(() => this.loading = false)
}
},
handlerCancel()
{
this.visible = false
}
}
})
</script>

View File

@ -29,23 +29,39 @@
<template #action="{ row }"> <template #action="{ row }">
<div class="space-x-2"> <div class="space-x-2">
<Tooltip :content="$t('source.common.modify').replace('$NAME', row.name)"> <Tooltip :content="$t('source.common.modify').replace('$NAME', row.name)">
<Button :disabled="loginUserId !== row.user.id" size="icon" class="rounded-full w-8 h-8" @click="handlerInfo(true, row)"> <Button :disabled="loginUserId !== row.user.id" size="icon" class="rounded-full w-6 h-6" @click="handlerInfo(true, row)">
<Pencil :size="15"/> <Pencil :size="14"/>
</Button> </Button>
</Tooltip> </Tooltip>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button size="icon" class="rounded-full w-6 h-6" variant="outline">
<Cog class="w-full justify-center" :size="14"/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuItem :disabled="loginUserId !== row.user.id" class="cursor-pointer" @click="handlerDelete(true, row)">
<Trash class="mr-2 h-4 w-4"/>
<span>{{ $t('common.deleteData') }}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</template> </template>
</TableCommon> </TableCommon>
</Card> </Card>
<SourceInfo v-if="dataInfoVisible" :is-visible="dataInfoVisible" :info="dataInfo" @close="handlerInfo(false, null)"/> <SourceInfo v-if="dataInfoVisible" :is-visible="dataInfoVisible" :info="dataInfo" @close="handlerInfo(false, null)"/>
<SourceDelete v-if="dataDeleteVisible" :is-visible="dataDeleteVisible" :info="dataInfo" @close="handlerDelete(false, null)"/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import Card from '@/views/ui/card' import Card from '@/views/ui/card'
import { Button } from '@/components/ui/button' import Button from '@/views/ui/button'
import { CirclePlay, CircleX, Pencil, Plus } from 'lucide-vue-next' import { CirclePlay, CircleX, Cog, Pencil, Plus, Trash } from 'lucide-vue-next'
import TableCommon from '@/views/components/table/TableCommon.vue' import TableCommon from '@/views/components/table/TableCommon.vue'
import { FilterModel } from '@/model/filter' import { FilterModel } from '@/model/filter'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@ -59,20 +75,31 @@ import Tag from '@/views/ui/tag'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import Common from '@/utils/common' import Common from '@/utils/common'
import SourceInfo from '@/views/pages/admin/source/SourceInfo.vue' import SourceInfo from '@/views/pages/admin/source/SourceInfo.vue'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import SourceDelete from '@/views/pages/admin/source/SourceDelete.vue'
export default defineComponent({ export default defineComponent({
name: 'SourceHome', name: 'SourceHome',
components: { components: {
SourceDelete,
DropdownMenuItem, DropdownMenuGroup, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuContent, DropdownMenuTrigger, DropdownMenu,
SourceInfo, SourceInfo,
Tag, Tag,
Tooltip, Tooltip,
Switch, Switch,
Avatar, Avatar,
TableCommon, TableCommon,
Pencil, CircleX, CirclePlay, Pencil, CircleX, CirclePlay, Cog, Trash, Plus,
Button, Button,
Card, Card
Plus
}, },
setup() setup()
{ {
@ -93,7 +120,8 @@ export default defineComponent({
data: [], data: [],
pagination: {} as PaginationModel, pagination: {} as PaginationModel,
dataInfoVisible: false, dataInfoVisible: false,
dataInfo: null as SourceModel | null dataInfo: null as SourceModel | null,
dataDeleteVisible: false
} }
}, },
created() created()
@ -126,6 +154,14 @@ export default defineComponent({
if (!opened) { if (!opened) {
this.handlerInitialize() this.handlerInitialize()
} }
},
handlerDelete(opened: boolean, value: null | SourceModel)
{
this.dataDeleteVisible = opened
this.dataInfo = value
if (!opened) {
this.handlerInitialize()
}
} }
} }
}) })

View File

@ -0,0 +1,50 @@
<template>
<Alert :class="typeClass">
<AlertTitle v-if="title">{{ title }}</AlertTitle>
<AlertDescription>
<span v-if="description">{{ description }}</span>
<slot v-else name="description"/>
</AlertDescription>
</Alert>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import '@/views/ui/alert/style.css'
enum Type
{
success = 'alert-success',
error = 'alert-error',
info = 'alert-info',
warning = 'alert-warning'
}
export default defineComponent({
name: 'DcAlert',
components: {
Alert, AlertDescription, AlertTitle
},
props: {
title: {
type: String
},
description: {
type: String
},
type: {
type: String,
default: 'info'
}
},
computed: {
typeClass()
{
return {
[(Type as any)[this.type]]: true
}
}
}
})
</script>

View File

@ -0,0 +1,3 @@
import Alert from '@/views/ui/alert/alert.vue'
export default Alert

View File

@ -0,0 +1 @@
.alert-error{border:1px solid #ffb08f;background-color:#ffefe6}.alert-success{border:1px solid #a6e1a6;background-color:#e6ffe6}.alert-warning{border:1px solid #ffe599;background-color:#ffffe6}.alert-info{border:1px solid #bde5f8;background-color:#e6f2ff}

View File

@ -1,354 +0,0 @@
<template>
<div>
<Modal v-model="visible" :title="title" :footer="null" width="70%" :closable="false" :mask-closable="false">
<Row :gutter="16">
<Col :span="4"/>
<Col :span="5">
<Card :bordered="false" dis-hover>
<div style="text-align:center">
<Avatar :size="40"
:src="formState?.type ? '/static/images/plugin/' + formState?.type.split('_')[0].replace(' Community', '') + '.png' : ''" icon="ios-person"/>
<p>{{ !formState['type'] ? '_' : formState['type'] }}</p>
</div>
</Card>
</Col>
<Col :span="6">
<Card :bordered="false" dis-hover>
<div style="text-align:center">
<Progress :percent="testInfo.percent"
:status="(testInfo.connected && testInfo.successful) ? 'success' : 'wrong'">
</Progress>
{{ formState.version }}
</div>
</Card>
</Col>
<Col :span="5">
<Card :bordered="false" dis-hover>
<div style="text-align:center">
<Avatar :size="40" :style="{'background-color': !testInfo.percent ? '#CCC' : testInfo.connected ? '#52c41a' : '#ff4d4f'}" icon="ios-cube"/>
<p>{{ $t('common.source') }}</p>
</div>
</Card>
</Col>
<Col :span="3"/>
</Row>
<Form :model="formState" :label-width="80">
<Tabs v-model="activeKey" :animated="false" @update:modelValue="handlerFilterConfigure($event)">
<TabPane :label="$t('common.' + type)" v-for="type in pluginTabs" :name="type" v-bind:key="type" :disabled="!formState.type" icon="md-apps">
<div v-if="type === 'source'">
<RadioGroup v-if="plugins" v-model="formState.type" type="button" @on-change="handlerChangePlugin($event)">
<div v-for="key in Object.keys(plugins)" v-bind:key="key">
<Divider orientation="left">{{ key }} ({{ plugins[key].length }})</Divider>
<Space wrap :size="[8, 16]">
<Tooltip v-for="plugin in plugins[key]" :content="plugin.description" transfer v-bind:key="plugin.name">
<Radio :label="plugin.name + '_' + plugin.type">
<Avatar :src="'/static/images/plugin/' + plugin.name.split('_')[0].replace(' Community', '') + '.png'" size="small"/>
<span style="margin-left: 2px;">{{ plugin.name }}</span>
</Radio>
</Tooltip>
</Space>
</div>
</RadioGroup>
</div>
<div v-else style="margin-top: 10px;">
<Row>
<Col :span="5"/>
<Col :span="14">
<FormItem v-for="configure in pluginTabConfigure" :required="configure.required" v-bind:key="configure.field" :prop="configure.field">
<template #label>
<span v-if="configure.field !== 'configures'">{{ $t('common.' + configure.field) }}</span>
</template>
<Input v-if="configure.type === 'String'" type="text" :disabled="configure.disabled" v-model="configure.value"/>
<InputNumber v-else-if="configure.type === 'Number'" :disabled="configure.disabled" :max="configure.max" :min="configure.min" v-model="configure.value"/>
<Switch v-else-if="configure.type === 'Boolean'" :disabled="configure.disabled" v-model="configure.value"/>
<Upload v-else-if="configure.type === 'File'" multiple
:headers="{
'Authorization': auth.type + ' ' + auth.token,
'PluginType': formState.type.split(' ')[0]
}"
:format="['xml']"
:on-success="handlerUploadSuccess"
:on-remove="handlerUploadRemove"
action="/api/v1/source/uploadFile">
<Button icon="ios-cloud-upload-outline">{{ $t('common.upload') }}</Button>
</Upload>
<div v-else>
<div style="margin-top: 10px;">
<FormItem style="margin-bottom: 5px;">
<Button size="small" type="primary" shape="circle" icon="md-add" @click="handlerPlusConfigure(configure.value)"/>
</FormItem>
<FormItem v-for="(element, index) in configure.value" :key="index" style="margin-bottom: 5px;">
<Row :gutter="12">
<Col :span="10">
<FormItem>
<Input v-model="element.field">
<template #prepend>
<span>{{ $t('common.field') }}</span>
</template>
</Input>
</FormItem>
</Col>
<Col :span="10">
<FormItem>
<Input v-model="element.value">
<template #prepend>
<span>{{ $t('common.value') }}</span>
</template>
</Input>
</FormItem>
</Col>
<Col :span="2">
<Button size="small" type="error" shape="circle" icon="md-remove" @click="handlerMinusConfigure(element, configure.value)"/>
</Col>
</Row>
</FormItem>
</div>
</div>
</FormItem>
</Col>
<Col :span="5"/>
</Row>
</div>
</TabPane>
</Tabs>
</Form>
<template #footer>
<Button key="cancel" type="error" size="small" :disabled="loading.test || loading.save" @click="handlerCancel()">
{{ $t('common.cancel') }}
</Button>
<Button type="primary" size="small" :loading="loading.test" :disabled="loading.save" @click="handlerTest()">
{{ $t('common.test') }}
</Button>
<Button type="primary" size="small" :loading="loading.save" :disabled="!testInfo.connected || loading.test" @click="handlerSave()">
{{ $t('common.save') }}
</Button>
</template>
</Modal>
</div>
</template>
<script lang="ts">
import {SourceModel} from "@/model/SourceModel";
import {SourceService} from "@/services/SourceService";
import {emptySource} from "@/views/admin/source/SourceGenerate";
import {defineComponent, reactive, ref} from "vue";
import {Configure} from "@/model/Configure";
import {clone, join} from 'lodash'
import SourceV2Service from "@/services/SourceV2Service";
import Common from "@/common/Common";
import {ResponseModel} from "@/model/ResponseModel";
interface TestInfo
{
connected: boolean,
percent: number,
successful: boolean
}
export default defineComponent({
name: "SourceDetail",
props: {
isVisible: {
type: Boolean,
default: () => false
},
id: {
type: Number,
default: () => 0
}
},
setup()
{
const layout = {
labelCol: {span: 6},
wrapperCol: {span: 12},
};
return {
activeKey: ref('source'),
layout
};
},
components: {},
data()
{
return {
title: '',
isUpdate: false,
formState: {} as SourceModel,
plugins: null,
testInfo: {} as TestInfo,
pluginTabs: ['source'],
pluginConfigure: null,
pluginTabConfigure: null,
applyPlugin: null,
loading: {
test: false,
save: false
},
auth: null
}
},
created()
{
this.auth = JSON.parse(localStorage.getItem(Common.token) || '{}');
if (this.id <= 0) {
this.title = 'Create New Source';
this.formState = reactive(emptySource);
}
else {
this.title = 'Modify Source';
this.isUpdate = true;
}
this.handlerInitialize();
},
methods: {
handlerInitialize()
{
this.formState.type = null
if (this.id > 0) {
SourceV2Service.getById(this.id)
.then(response => {
if (response.status) {
this.formState = reactive(response.data);
this.formState.type = this.formState.type + '_' + this.formState.protocol;
this.applyPlugin = response.data['schema'];
this.pluginConfigure = response.data['schema']['configures'];
// Clear
this.pluginTabs = ['source'];
this.pluginTabs = [...this.pluginTabs, ...Array.from(new Set(this.pluginConfigure.map(v => v.group)))];
}
});
}
new SourceService().getPlugins()
.then(response => {
if (response.status) {
this.plugins = response.data;
}
});
},
handlerCancel()
{
this.visible = false;
},
handlerSave()
{
this.loading.save = true;
const temp = clone(this.formState.type).split('_');
let type = temp[1]
let name = temp[0]
if (temp.length === 3) {
type = temp[2]
name = join([temp[0], temp[1]], ' ')
}
const configure = {
id: this.id,
type: type,
name: name,
configure: this.applyPlugin,
version: this.formState.version
};
SourceV2Service.saveAndUpdate(configure, this.isUpdate)
.then((response) => {
if (response.status) {
this.$Message.success("Create successful");
this.visible = false;
}
})
.finally(() => {
this.loading.save = false;
});
},
handlerTest()
{
this.loading.test = true;
const temp = clone(this.formState.type).split('_');
let type = temp[1]
let name = temp[0]
if (temp.length === 3) {
type = temp[2]
name = join([temp[0], temp[1]], ' ')
}
const configure = {
type: type,
name: name,
configure: this.applyPlugin
};
new SourceService()
.testConnection(configure)
.then((response) => {
this.testInfo.percent = 100;
if (response.status) {
this.$Message.success("Test successful");
this.testInfo.connected = true;
this.testInfo.successful = true;
this.formState.version = response.data?.columns[0]?.version;
}
else {
this.$Message.error(response.message);
this.testInfo.connected = false;
this.testInfo.successful = false;
}
})
.finally(() => {
this.loading.test = false;
});
},
handlerPlusConfigure(array: Array<Configure>)
{
if (array === null) {
array = new Array<Configure>();
}
const configure: Configure = {field: '', value: ''};
array.push(configure);
},
handlerMinusConfigure(configure: Configure, array: Array<Configure>)
{
const index = array.indexOf(configure);
if (index !== -1) {
array.splice(index, 1);
}
},
handlerChangePlugin(value)
{
const pluginAndType = value.split('_');
const applyPlugins: [] = this.plugins[pluginAndType[1]];
const applyPlugin = applyPlugins.filter(plugin => plugin['name'] === pluginAndType[0])[0];
this.applyPlugin = applyPlugin['configure'];
this.pluginConfigure = applyPlugin['configure']['configures'];
// Clear
this.pluginTabs = ['source'];
this.pluginTabs = [...this.pluginTabs, ...Array.from(new Set(this.pluginConfigure.map(v => v.group)))];
},
handlerFilterConfigure(group: string)
{
if (group === 'source') {
return;
}
this.pluginTabConfigure = this.pluginConfigure.filter(field => field.group === group);
},
handlerUploadSuccess(response: ResponseModel)
{
if (response.status) {
const configure = this.applyPlugin.configures.filter(configure => configure.field === 'file')
configure[0].value.push(response.data)
}
},
handlerUploadRemove(file)
{
const configure = this.applyPlugin.configures.filter(configure => configure.field === 'file')
configure[0].value = configure[0].value.filter(value => !value.endsWith(file.name))
}
},
computed: {
visible: {
get(): boolean
{
return this.isVisible
},
set(value: boolean)
{
this.$emit('close', value)
}
}
}
});
</script>

View File

@ -1,92 +0,0 @@
import {SourceModel} from "@/model/SourceModel";
const emptySource: SourceModel = {
name: "",
description: "",
protocol: "",
host: "",
port: 0,
username: "",
password: "",
catalog: "",
database: "",
type: "",
configures: {}
};
const createHeaders = (i18n: any) => {
return [
{
title: i18n.t('common.no'),
key: 'id',
},
{
title: i18n.t('common.name'),
key: 'name',
slot: 'name',
ellipsis: true
},
{
title: i18n.t('common.type'),
key: 'type',
slot: 'type'
},
{
title: i18n.t('common.protocol'),
key: 'protocol',
ellipsis: true
},
{
title: i18n.t('common.host'),
key: 'host',
slot: 'host',
ellipsis: true
},
{
title: i18n.t('common.port'),
key: 'port',
ellipsis: true
},
{
title: i18n.t('common.public'),
slot: 'public',
key: 'public'
},
{
title: i18n.t('common.version'),
slot: 'version',
align: 'center'
},
{
title: i18n.t('common.available'),
key: 'available',
slot: 'available',
ellipsis: true,
align: 'center'
},
{
title: i18n.t('common.createTime'),
key: 'createTime',
ellipsis: true,
tooltip: true
},
{
title: i18n.t('common.updateTime'),
key: 'updateTime',
ellipsis: true,
tooltip: true
},
{
title: i18n.t('common.action'),
slot: 'action',
key: 'action',
width: 150,
align: 'center'
}
];
};
export {
createHeaders,
emptySource
}