Merge pull request #10449 from dataease/pr@dev-v2@perf_link_ticket

perf(仪表板): 公共链接ticket参数
This commit is contained in:
fit2cloud-chenyw 2024-06-22 12:23:52 +08:00 committed by GitHub
commit 87c2005a5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 188 additions and 66 deletions

View File

@ -2269,7 +2269,10 @@ export default {
you_can_type_here: '可以在这里输入其他内容'
},
link_ticket: {
require: 'Ticket 必填'
require: '必选',
back: '返回公共链接设置页面',
refresh: '刷新',
time_tips: '单位: 分钟范围: [0-1440],0代表无期限自首次使用ticket访问开始'
},
pblink: {
key_pwd: '请输入密码打开链接',

View File

@ -11,7 +11,7 @@ import { ElMessage } from 'element-plus-secondary'
import { useEmbedded } from '@/store/modules/embedded'
import { useI18n } from '@/hooks/web/useI18n'
import { XpackComponent } from '@/components/plugin'
import { propTypes } from '@/utils/propTypes'
const dvMainStore = dvMainStoreWithOut()
const { t } = useI18n()
const embeddedStore = useEmbedded()
@ -32,7 +32,8 @@ const props = defineProps({
isSelector: {
type: Boolean,
default: false
}
},
ticketArgs: propTypes.string.def(null)
})
const loadCanvasDataAsync = async (dvId, dvType) => {
@ -57,6 +58,14 @@ const loadCanvasDataAsync = async (dvId, dvType) => {
}
}
let argsObject = null
try {
argsObject = JSON.parse(props.ticketArgs)
} catch (error) {
console.error(error)
}
const hasTicketArgs = argsObject && Object.keys(argsObject)
//
let attachParam
await getOuterParamsInfo(dvId).then(rsp => {
@ -65,9 +74,14 @@ const loadCanvasDataAsync = async (dvId, dvType) => {
// iframe iframe
const attachParamsEncode = router.currentRoute.value.query.attachParams
if (attachParamsEncode) {
if (attachParamsEncode || hasTicketArgs) {
try {
attachParam = JSON.parse(Base64.decode(decodeURIComponent(attachParamsEncode)))
if (attachParam) {
attachParam = JSON.parse(Base64.decode(decodeURIComponent(attachParamsEncode)))
}
if (hasTicketArgs) {
attachParam = Object.assign({}, attachParam, argsObject)
}
} catch (e) {
console.error(e)
ElMessage.error(t('visualization.outer_param_decode_error'))

View File

@ -2,6 +2,12 @@ import request from '@/config/axios'
import { useCache } from '@/hooks/web/useCache'
import { isInIframe } from '@/utils/utils'
const { wsCache } = useCache()
export interface TicketValidVO {
ticketValid: boolean
ticketExp: boolean
args: string
}
export interface ProxyInfo {
resourceId: string
uid: string
@ -9,17 +15,36 @@ export interface ProxyInfo {
pwdValid?: boolean
type: string
inIframeError: boolean
ticketValidVO: TicketValidVO
}
class ShareProxy {
uuid: string
constructor() {
this.uuid = ''
}
getTicket() {
const curLocation = window.location.href
const pmIndex = curLocation.lastIndexOf('?')
if (pmIndex == -1) {
return null
}
const searchText = curLocation.substring(pmIndex + 1)
const regex = /([^&=]+)=([^&]*)/g
let m
while ((m = regex.exec(searchText)) !== null) {
const key = decodeURIComponent(m[1])
if (key === 'ticket') {
return decodeURIComponent(m[2])
}
}
return null
}
setUuid() {
const curLocation = window.location.href
const pmIndex = curLocation.lastIndexOf('?')
const uuidObj = curLocation.substring(
curLocation.lastIndexOf('de-link/') + 8,
curLocation.lastIndexOf('?') > 0 ? curLocation.lastIndexOf('?') : curLocation.length
pmIndex > 0 ? pmIndex : curLocation.length
)
this.uuid = uuidObj
}
@ -31,7 +56,8 @@ class ShareProxy {
const uuid = this.uuid
const url = '/share/proxyInfo'
const inIframe = isInIframe()
const param = { uuid, ciphertext: null, inIframe }
const ticket = this.getTicket()
const param = { uuid, ciphertext: null, inIframe, ticket }
const ciphertext = wsCache.get(`link-${uuid}`)
if (ciphertext) {
param['ciphertext'] = ciphertext

View File

@ -0,0 +1,6 @@
<script lang="ts" setup>
import EmptyBackground from '@/components/empty-background/src/EmptyBackground.vue'
</script>
<template>
<EmptyBackground img-type="noneWhite" description="Ticket参数错误" />
</template>

View File

@ -4,28 +4,40 @@
<LinkError v-else-if="!loading && !linkExist" />
<Exp v-else-if="!loading && linkExp" />
<PwdTips v-else-if="!loading && !pwdValid" />
<TicketError
v-else-if="!loading && (!state.ticketValidVO.ticketValid || state.ticketValidVO.ticketExp)"
/>
<PreviewCanvas
v-else
:class="{ 'hidden-link': loading }"
ref="pcanvas"
public-link-status="true"
public-link-status
:ticket-args="state.ticketValidVO.args"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, nextTick, ref } from 'vue'
import { onMounted, nextTick, ref, reactive } from 'vue'
import PreviewCanvas from '@/views/data-visualization/PreviewCanvas.vue'
import { ProxyInfo, shareProxy } from './ShareProxy'
import Exp from './exp.vue'
import LinkError from './error.vue'
import PwdTips from './pwd.vue'
import IframeError from './IframeError.vue'
import TicketError from './TicketError.vue'
const pcanvas = ref(null)
const iframeError = ref(true)
const linkExist = ref(false)
const loading = ref(true)
const linkExp = ref(false)
const pwdValid = ref(false)
const state = reactive({
ticketValidVO: {
ticketValid: false,
ticketExp: false,
args: ''
}
})
onMounted(async () => {
const proxyInfo = (await shareProxy.loadProxy()) as ProxyInfo
if (proxyInfo?.inIframeError) {
@ -41,6 +53,7 @@ onMounted(async () => {
linkExist.value = true
linkExp.value = !!proxyInfo.exp
pwdValid.value = !!proxyInfo.pwdValid
state.ticketValidVO = proxyInfo.ticketValidVO
nextTick(() => {
const method = pcanvas?.value?.loadCanvasDataAsync
if (method) {

View File

@ -346,12 +346,14 @@ const expChangeHandler = exp => {
}
const beforeClose = async done => {
if (!shareEnable.value) {
showTicket.value = false
done()
return
}
const pwdValid = validatePwdFormat()
const uuidValid = await validateUuid()
if (pwdValid && uuidValid) {
showTicket.value = false
done()
}
}

View File

@ -4,7 +4,7 @@
<div class="ticket-model-start">
<el-tooltip class="item" effect="dark" :content="$t('link_ticket.back')" placement="top">
<span class="back-tips">
<el-icon class="custom-el-icon back-icon" @click="close">
<el-icon class="custom-el-icon back-icon" @click.stop="close">
<Icon class="toolbar-icon" name="icon_left_outlined" />
</el-icon>
</span>
@ -20,7 +20,7 @@
</div>
</div>
<div class="ticket-add">
<el-button @click="addRow" text>
<el-button @click.stop="addRow" text>
<template #icon>
<icon name="icon_add_outlined"></icon>
</template>
@ -29,29 +29,16 @@
</div>
<div class="ticket-table">
<el-table :data="state.tableData" style="width: 100%" size="small">
<el-table-column prop="ticket" label="ticket" width="130">
<el-table-column prop="ticket" label="Ticket" width="130">
<template v-slot="scope">
<div class="ticket-row">
<span>{{ scope.row.ticket }}</span>
<span :title="scope.row.ticket">{{ scope.row.ticket }}</span>
<el-tooltip class="item" effect="dark" :content="$t('commons.copy')" placement="top">
<el-button
text
v-clipboard:copy="`${props.linkUrl}?ticket=${scope.row.ticket}`"
v-clipboard:success="onCopy"
v-clipboard:error="onError"
>
<el-button text @click.stop="copyTicket(scope.row.ticket)">
<template #icon>
<Icon name="de-copy"></Icon>
</template>
</el-button>
<!-- <span
v-clipboard:copy="`${props.linkUrl}?ticket=${scope.row.ticket}`"
v-clipboard:success="onCopy"
v-clipboard:error="onError"
class="copy-i"
>
<icon name="de-copy"></icon>
</span> -->
</el-tooltip>
<el-tooltip
class="item"
@ -59,14 +46,11 @@
:content="`${$t('link_ticket.refresh')} ticket`"
placement="top"
>
<el-button text @click="refreshTicket(scope.row)">
<el-button text @click.stop="refreshTicket(scope.row)">
<template #icon>
<Icon name="icon_refresh_outlined"></Icon>
</template>
</el-button>
<!-- <span class="refresh-i" @click="refreshTicket(scope.row)">
<icon name="icon_refresh_outlined" />
</span> -->
</el-tooltip>
</div>
</template>
@ -74,17 +58,17 @@
<el-table-column prop="exp" :label="$t('visualization.over_time')" width="100">
<template v-slot:header>
<span>{{ $t('visualization.over_time') }}</span>
<el-tooltip
class="item"
effect="dark"
:content="$t('link_ticket.time_tips')"
placement="top"
>
<span class="check-tips">
<svg-icon icon-class="de-icon-info" />
</span>
</el-tooltip>
<div class="ticket-exp-head">
<span>{{ $t('visualization.over_time') }}</span>
<el-tooltip
class="item"
effect="dark"
:content="$t('link_ticket.time_tips')"
placement="top"
>
<Icon name="dv-info"></Icon>
</el-tooltip>
</div>
</template>
<template v-slot="scope">
<el-input
@ -129,7 +113,7 @@
:content="$t('commons.delete')"
placement="top"
>
<el-button text @click="deleteTicket(scope.row, scope.$idnex)">
<el-button text @click.stop="deleteTicket(scope.row, scope.$index)">
<template #icon>
<Icon name="icon_delete-trash_outlined"></Icon>
</template>
@ -141,12 +125,12 @@
:content="scope.row.isEdit ? $t('commons.save') : $t('commons.edit')"
placement="top"
>
<el-button v-if="!scope.row.isEdit" text @click="editRow(scope.row)">
<el-button v-if="!scope.row.isEdit" text @click.stop="editRow(scope.row)">
<template #icon>
<Icon name="icon_edit_outlined"></Icon>
</template>
</el-button>
<el-button v-else text @click="saveRow(scope.row, scope.$index)">
<el-button v-else text @click.stop="saveRow(scope.row, scope.$index)">
<template #icon>
<Icon name="edit-done"></Icon>
</template>
@ -169,8 +153,9 @@ import { ref, reactive, onMounted, toRefs } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { useI18n } from '@/hooks/web/useI18n'
import request from '@/config/axios'
import { ElMessage } from 'element-plus-secondary'
import { ElMessage, ElMessageBox } from 'element-plus-secondary'
import useClipboard from 'vue-clipboard3'
const { toClipboard } = useClipboard()
const { t } = useI18n()
const props = defineProps({
linkUrl: propTypes.string.def(null),
@ -214,8 +199,27 @@ const requireTicketChange = val => {
emits('requireChange', val)
})
}
const createLimit = (count?: number) => {
const realCount = count ? count : state.tableData.length || 0
if (realCount > 4) {
ElMessageBox.confirm('提示', {
confirmButtonType: 'primary',
type: 'warning',
confirmButtonText: t('common.roger_that'),
cancelButtonText: t('dataset.cancel'),
autofocus: false,
showClose: false,
showCancelButton: false,
tip: '最多支持创建5个Ticket'
})
return false
}
return true
}
const addRow = () => {
if (!createLimit()) {
return
}
const row = {
ticket: '',
exp: 30,
@ -230,6 +234,15 @@ const addRow = () => {
})
}
const copyTicket = async ticket => {
const url = `${props.linkUrl}?ticket=${ticket}`
try {
await toClipboard(url)
ElMessage.success(t('common.copy_success'))
} catch (e) {
ElMessage.warning(t('common.copy_unsupported'), e)
}
}
const refreshTicket = row => {
const url = '/ticket/saveTicket'
const param = JSON.parse(JSON.stringify(row))
@ -311,12 +324,7 @@ const saveRow = (row, index) => {
const editRow = row => {
row.isEdit = true
}
const onCopy = () => {
ElMessage.success('复制成功')
}
const onError = e => {
console.log(e)
}
const finish = () => {
close()
}
@ -325,7 +333,7 @@ const loadTicketData = () => {
const resourceId = props.resourceId
const url = `/ticket/query/${resourceId}`
request.get({ url }).then(res => {
state.tableData = res.data
state.tableData = res.data || []
})
}
onMounted(() => {
@ -335,7 +343,7 @@ onMounted(() => {
<style lang="less" scoped>
.ticket {
height: 261px;
min-height: 280px;
.ticket-model {
display: flex;
height: 22px;
@ -343,6 +351,7 @@ onMounted(() => {
padding: 0;
.ticket-model-start {
display: flex;
align-items: center;
color: #1f2329;
font-family: PingFang SC;
font-weight: 500;
@ -385,6 +394,8 @@ onMounted(() => {
}
}
.ticket-table {
border-top: 1px solid #d5d7d8;
min-height: 156px;
padding: 0 0;
height: 50px;
overflow-y: overlay;
@ -399,19 +410,35 @@ onMounted(() => {
margin-bottom: 12px;
margin-right: -80px;
}
:deep(.check-tips) {
margin-left: 4px;
:deep(.ticket-exp-head) {
display: flex;
line-height: 22px;
height: 22px;
align-items: center;
svg {
margin-left: 4px;
width: 16px;
height: 16px;
cursor: pointer;
}
}
:deep(.ticket-row) {
display: flex;
align-items: center;
height: 22px;
span {
width: 66px;
margin-right: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
button {
height: 16px;
line-height: 16px;
width: 16px;
}
height: 22px;
.ed-button + .ed-button {
margin-left: 8px;
}
@ -451,7 +478,7 @@ onMounted(() => {
}
}
.ticket-btn {
margin-top: 16px;
margin: 16px 0;
float: right;
}
}

View File

@ -5,7 +5,7 @@
width="480"
placement="bottom-start"
:show-arrow="false"
popper-class="share-popover"
:popper-class="`share-popover ${showTicket ? 'share-ticket-popover' : ''}`"
@show="share"
>
<template #reference>
@ -21,15 +21,12 @@
{{ t('visualization.share') }}
</el-button>
</template>
<div class="share-container">
<div class="share-container" :class="{ 'hidden-link-container': showTicket }">
<div class="share-title share-padding">公共链接分享</div>
<div class="open-share flex-align-center share-padding">
<el-switch size="small" v-model="shareEnable" @change="enableSwitcher" />
{{ shareTips }}
</div>
<!-- <div v-if="shareEnable" class="text share-padding">
<el-input v-model="linkAddr" disabled />
</div> -->
<div v-if="shareEnable" class="custom-link-line share-padding">
<el-input
ref="linkUuidRef"
@ -117,11 +114,22 @@
<el-divider v-if="shareEnable" class="share-divider" />
<div v-if="shareEnable" class="share-foot share-padding">
<el-button secondary @click="openTicket">Ticket 设置</el-button>
<el-button :disabled="!shareEnable || expError" type="primary" @click="copyInfo">
{{ t('visualization.copy_link') }}
</el-button>
</div>
</div>
<div v-if="shareEnable && showTicket" class="share-ticket-container">
<share-ticket
:link-url="linkAddr"
:uuid="state.detailInfo.uuid"
:resource-id="props.resourceId"
:ticket-require="state.detailInfo.ticketRequire"
@require-change="updateRequireTicket"
@close="closeTicket"
/>
</div>
</el-popover>
</template>
@ -133,6 +141,8 @@ import { propTypes } from '@/utils/propTypes'
import { ShareInfo, SHARE_BASE, shortcuts } from './option'
import { ElMessage, ElLoading } from 'element-plus-secondary'
import useClipboard from 'vue-clipboard3'
import ShareTicket from './ShareTicket.vue'
const { toClipboard } = useClipboard()
const { t } = useI18n()
const props = defineProps({
@ -150,6 +160,7 @@ const linkAddr = ref('')
const expError = ref(false)
const linkCustom = ref(false)
const linkUuidRef = ref(null)
const showTicket = ref(false)
const state = reactive({
detailInfo: {
id: '',
@ -186,6 +197,7 @@ const clickOutPopover = e => {
}
const openPopover = () => {
if (!popoverVisible.value) {
showTicket.value = false
popoverVisible.value = true
}
}
@ -468,6 +480,16 @@ const finishEditUuid = async () => {
linkCustom.value = !uuidValid
}
const openTicket = () => {
showTicket.value = true
}
const closeTicket = () => {
showTicket.value = false
}
const updateRequireTicket = val => {
state.detailInfo.ticketRequire = val
}
const execute = () => {
share()
}
@ -477,12 +499,21 @@ defineExpose({
</script>
<style lang="less">
.share-popover {
.share-popover:not(.share-ticket-popover) {
padding: 16px 0px !important;
}
.share-ticket-popover {
padding: 0 !important;
}
</style>
<style lang="less" scoped>
.hidden-link-container {
display: none;
}
.share-ticket-container {
padding: 16px;
}
.share-container {
.share-title {
font-weight: 500;