forked from github/dataease
Merge pull request #10449 from dataease/pr@dev-v2@perf_link_ticket
perf(仪表板): 公共链接ticket参数
This commit is contained in:
commit
87c2005a5d
@ -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: '请输入密码打开链接',
|
||||
|
@ -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'))
|
||||
|
@ -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
|
||||
|
6
core/core-frontend/src/views/share/link/TicketError.vue
Normal file
6
core/core-frontend/src/views/share/link/TicketError.vue
Normal 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>
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user