Browse Source

[Feature][UI Next] Add dag view log

3.0.0/version-upgrade
Devosend 2 years ago committed by GitHub
parent
commit
8136b8e064
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      dolphinscheduler-ui-next/src/locales/modules/en_US.ts
  2. 12
      dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
  3. 13
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-context-menu.tsx
  4. 17
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx
  5. 14
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-menu.ts
  6. 376
      dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/log-modal.tsx
  7. 97
      dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/log.module.scss

12
dolphinscheduler-ui-next/src/locales/modules/en_US.ts

@ -481,7 +481,17 @@ const project = {
type: 'Type',
retry_count: 'Retry Count',
submit_time: 'Submit Time',
refresh_status_succeeded: 'Refresh status succeeded'
refresh_status_succeeded: 'Refresh status succeeded',
view_log: 'View log',
update_log_success: 'Update log success',
no_more_log: 'No more logs',
no_log: 'No log',
loading_log: 'Loading Log...',
close: 'Close',
download_log: 'Download Log',
refresh_log: 'Refresh Log',
enter_full_screen: 'Enter full screen',
cancel_full_screen: 'Cancel full screen'
},
task: {
task_name: 'Task Name',

12
dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts

@ -479,7 +479,17 @@ const project = {
type: '类型',
retry_count: '重试次数',
submit_time: '提交时间',
refresh_status_succeeded: '刷新状态成功'
refresh_status_succeeded: '刷新状态成功',
view_log: '查看日志',
update_log_success: '更新日志成功',
no_more_log: '暂无更多日志',
no_log: '暂无日志',
loading_log: '正在努力请求日志中...',
close: '关闭',
download_log: '下载日志',
refresh_log: '刷新日志',
enter_full_screen: '进入全屏',
cancel_full_screen: '取消全屏'
},
task: {
task_name: '任务名称',

13
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-context-menu.tsx

@ -56,7 +56,7 @@ const props = {
export default defineComponent({
name: 'dag-context-menu',
props,
emits: ['hide', 'start', 'edit', 'copyTask', 'removeTasks'],
emits: ['hide', 'start', 'edit', 'viewLog', 'copyTask', 'removeTasks'],
setup(props, ctx) {
const graph = inject('graph', ref())
const route = useRoute()
@ -80,6 +80,10 @@ export default defineComponent({
ctx.emit('edit', Number(props.cell?.id))
}
const handleViewLog = () => {
ctx.emit('viewLog')
}
const handleCopy = () => {
const genNums = 1
const type = props.cell?.data.taskType
@ -112,7 +116,8 @@ export default defineComponent({
startRunning,
handleEdit,
handleCopy,
handleDelete
handleDelete,
handleViewLog
}
},
render() {
@ -156,7 +161,9 @@ export default defineComponent({
>
{t('project.node.delete')}
</div>
{/* TODO: view log */}
<div class={`${styles['menu-item']}`} onClick={this.handleViewLog}>
{t('project.node.view_log')}
</div>
</div>
)
)

17
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx

@ -44,9 +44,10 @@ import { useThemeStore } from '@/store/theme/theme'
import VersionModal from '../../definition/components/version-modal'
import { WorkflowDefinition } from './types'
import DagSaveModal from './dag-save-modal'
import ContextMenuItem from './dag-context-menu'
import TaskModal from '@/views/projects/task/components/node/detail-modal'
import StartModal from '@/views/projects/workflow/definition/components/start-modal'
import ContextMenuItem from './dag-context-menu'
import LogModal from '@/views/projects/workflow/instance/components/log-modal'
import './x6-style.scss'
const props = {
@ -113,8 +114,11 @@ export default defineComponent({
pageY,
menuVisible,
startModalShow,
logModalShow,
menuHide,
menuStart
menuStart,
viewLog,
hideLog
} = useNodeMenu({
graph
})
@ -244,6 +248,7 @@ export default defineComponent({
onEdit={editTask}
onCopyTask={copyTask}
onRemoveTasks={removeTasks}
onViewLog={viewLog}
/>
{!!props.definition && (
<StartModal
@ -251,6 +256,14 @@ export default defineComponent({
v-model:show={startModalShow.value}
/>
)}
{!!props.instance && logModalShow.value && (
<LogModal
v-model:show={logModalShow.value}
taskInstanceId={props.instance.id}
taskInstanceType={props.instance.taskType}
onHideLog={hideLog}
/>
)}
</div>
)
}

14
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-menu.ts

@ -29,6 +29,7 @@ interface Options {
export function useNodeMenu(options: Options) {
const { graph } = options
const startModalShow = ref(false)
const logModalShow = ref(false)
const menuVisible = ref(false)
const pageX = ref()
const pageY = ref()
@ -45,6 +46,14 @@ export function useNodeMenu(options: Options) {
startModalShow.value = true
}
const viewLog = () => {
logModalShow.value = true
}
const hideLog = () => {
logModalShow.value = false
}
onMounted(() => {
if (graph.value) {
// contextmenu
@ -67,9 +76,12 @@ export function useNodeMenu(options: Options) {
pageX,
pageY,
startModalShow,
logModalShow,
menuVisible,
menuCell,
menuHide,
menuStart
menuStart,
viewLog,
hideLog
}
}

376
dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/log-modal.tsx

@ -0,0 +1,376 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import _ from 'lodash'
import {
defineComponent,
PropType,
Transition,
toRefs,
ref,
onMounted,
computed,
reactive,
renderSlot
} from 'vue'
import { useI18n } from 'vue-i18n'
import { NButton, NIcon, NTooltip } from 'naive-ui'
import { queryLog } from '@/service/modules/log'
import {
DownloadOutlined,
SyncOutlined,
FullscreenOutlined,
FullscreenExitOutlined
} from '@vicons/antd'
import { downloadFile } from '@/service/service'
import styles from './log.module.scss'
const props = {
taskInstanceId: {
type: Number as PropType<number>,
default: -1
},
taskInstanceType: {
type: String as PropType<string>,
default: ''
}
}
export default defineComponent({
name: 'workflow-instance-log',
props,
emits: ['hideLog'],
setup(props, ctx) {
const { t } = useI18n()
const loadingRef = ref(false)
const loadingIndex = ref(0)
const isDataRef = ref(true)
const logBox = ref()
const logContent = ref()
const logContentBox = ref()
const textareaLog = ref()
const isScreen = ref(false)
const textareaHeight = computed(() =>
logContentBox.value ? logContentBox.value.clientHeight : 0
)
const boxRef = reactive({
width: '',
height: '',
marginLeft: '',
marginRight: '',
marginTop: ''
})
const refreshLog = () => {
loadingRef.value = true
queryLog({
taskInstanceId: props.taskInstanceId,
skipLineNum: loadingIndex.value * 1000,
limit: loadingIndex.value === 0 ? 1000 : (loadingIndex.value + 1) * 1000
})
.then((res: any) => {
setTimeout(() => {
loadingRef.value = false
if (res) {
window.$message.success(t('project.workflow.update_log_success'))
} else {
window.$message.warning(t('project.workflow.no_more_log'))
}
}, 1500)
textareaLog.value.innerHTML = res || t('project.workflow.no_log')
})
.catch((error: any) => {
window.$message.error(error.message || '')
loadingRef.value = false
})
}
const showLog = () => {
queryLog({
taskInstanceId: props.taskInstanceId,
skipLineNum: loadingIndex.value * 1000,
limit: loadingIndex.value === 0 ? 1000 : (loadingIndex.value + 1) * 1000
})
.then((res: any) => {
if (!res) {
isDataRef.value = false
setTimeout(() => {
window.$message.warning(t('project.workflow.no_more_log'))
}, 1000)
textareaLog.value.innerHTML = t('project.workflow.no_log')
} else {
isDataRef.value = true
textareaLog.value.innerHTML = res || t('project.workflow.no_log')
setTimeout(() => {
textareaLog.value.scrollTop = 2
}, 800)
}
})
.catch((error: any) => {
window.$message.error(error.message || '')
})
}
const initLog = () => {
window.$message.info(t('project.workflow.loading_log'))
showLog()
}
const downloadLog = () => {
downloadFile('log/download-log', {
taskInstanceId: props.taskInstanceId
})
}
const screenOpen = () => {
isScreen.value = true
const winW = window.innerWidth - 40
const winH = window.innerHeight - 40
boxRef.width = `${winW}px`
boxRef.height = `${winH}px`
boxRef.marginLeft = `-${winW / 2}px`
boxRef.marginRight = `-${winH / 2}px`
boxRef.marginTop = `-${winH / 2}px`
logContent.value.animate({ scrollTop: 0 }, 0)
}
const screenClose = () => {
isScreen.value = false
boxRef.width = ''
boxRef.height = ''
boxRef.marginLeft = ''
boxRef.marginRight = ''
boxRef.marginTop = ''
logContent.value.animate({ scrollTop: 0 }, 0)
}
const toggleScreen = () => {
if (isScreen.value) {
screenClose()
} else {
screenOpen()
}
}
const close = () => {
ctx.emit('hideLog')
}
/**
* up
*/
const onUp = _.debounce(
function () {
loadingIndex.value = loadingIndex.value - 1
showLog()
},
1000,
{
leading: false,
trailing: true
}
)
/**
* down
*/
const onDown = _.debounce(
function () {
loadingIndex.value = loadingIndex.value + 1
showLog()
},
1000,
{
leading: false,
trailing: true
}
)
const onTextareaScroll = () => {
textareaLog.value.onscroll = () => {
// Listen for scrollbar events
if (
textareaLog.value.scrollTop + textareaLog.value.clientHeight ===
textareaLog.value.clientHeight
) {
if (loadingIndex.value > 0) {
window.$message.info(t('project.workflow.loading_log'))
onUp()
}
}
// Listen for scrollbar events
if (
textareaLog.value.scrollHeight ===
textareaLog.value.clientHeight + textareaLog.value.scrollTop
) {
// No data is not requested
if (isDataRef.value) {
window.$message.info(t('project.workflow.loading_log'))
onDown()
}
}
}
}
onMounted(() => {
initLog()
onTextareaScroll()
})
return {
t,
logBox,
logContentBox,
loadingRef,
textareaLog,
logContent,
textareaHeight,
isScreen,
boxRef,
showLog,
downloadLog,
refreshLog,
toggleScreen,
close,
...toRefs(props)
}
},
render() {
return (
<div>
<span class={styles['log-model']}>
{this.taskInstanceId && this.taskInstanceType !== 'SUB_PROCESS' && (
<span>
{renderSlot(this.$slots, 'history')}
<slot name='history'></slot>
<span onClick={this.showLog}>
{renderSlot(this.$slots, 'log')}
</span>
</span>
)}
<Transition name='fade'>
{
<div class={styles['log-pop']}>
<div class={styles['log-box']} style={{ ...this.boxRef }}>
<div class={styles['title']}>
<div class={styles['left-item']}>
{this.t('project.workflow.view_log')}
</div>
<div class={styles['right-item']}>
<NTooltip>
{{
trigger: () => (
<NButton
strong
secondary
circle
type='info'
class={styles.button}
onClick={this.downloadLog}
>
<NIcon>
<DownloadOutlined />
</NIcon>
</NButton>
),
default: () => this.t('project.workflow.download_log')
}}
</NTooltip>
<NTooltip>
{{
trigger: () => (
<NButton
strong
secondary
circle
type='info'
class={styles.button}
onClick={() =>
!this.loadingRef && this.refreshLog()
}
>
<NIcon>
<SyncOutlined />
</NIcon>
</NButton>
),
default: () => this.t('project.workflow.refresh_log')
}}
</NTooltip>
<NTooltip>
{{
trigger: () => (
<NButton
strong
secondary
circle
type='info'
class={styles.button}
onClick={this.toggleScreen}
>
<NIcon>
{this.isScreen ? (
<FullscreenExitOutlined />
) : (
<FullscreenOutlined />
)}
</NIcon>
</NButton>
),
default: () =>
this.isScreen
? this.t('project.workflow.cancel_full_screen')
: this.t('project.workflow.enter_full_screen')
}}
</NTooltip>
</div>
</div>
<div class={styles['content']} ref='logContent'>
<div class={styles['content-log-box']} ref='logContentBox'>
<textarea
class={styles['textarea-ft']}
style={`width: 100%; height: ${this.textareaHeight}px`}
spellcheck='false'
ref='textareaLog'
readonly
></textarea>
</div>
</div>
<div class={styles['operation']}>
<NButton
type='primary'
size='small'
round
onClick={this.close}
>
{this.t('project.workflow.close')}
</NButton>
</div>
</div>
</div>
}
</Transition>
</span>
</div>
)
}
})

97
dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/log.module.scss

@ -0,0 +1,97 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.log-pop {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,.4);
z-index: 10;
.log-box {
width: 660px;
height: 520px;
background: #fff;
border-radius: 3px;
position: absolute;
left:50%;
top: 50%;
margin-left: -340px;
margin-top: -250px;
.title {
height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #dcdedc;
.left-item {
font-size: 16px;
color: #333;
display: inline-block;
padding-left: 20px;
}
.right-item {
padding-right: 10px;
.button {
margin-right: 10px;
}
}
}
.content {
height: calc(100% - 100px);
background: #002A35;
padding:6px 2px;
.content-log-box {
width: 100%;
height: 100%;
word-break:break-all;
textarea {
background: none;
color: #9CABAF;
border: 0;
font-family: 'Microsoft Yahei,Arial,Hiragino Sans GB,tahoma,SimSun,sans-serif';
font-weight: bold;
resize:none;
line-height: 1.6;
padding: 0px;
}
}
}
.operation {
text-align: right;
height: 50px;
line-height: 44px;
border-top: 1px solid #dcdedc;
padding-right: 20px;
background: #fff;
position: relative;
}
}
}
@-webkit-keyframes rotateloading{from{-webkit-transform: rotate(0deg)}
to{-webkit-transform: rotate(360deg)}
}
@-moz-keyframes rotateloading{from{-moz-transform: rotate(0deg)}
to{-moz-transform: rotate(359deg)}
}
@-o-keyframes rotateloading{from{-o-transform: rotate(0deg)}
to{-o-transform: rotate(359deg)}
}
@keyframes rotateloading{from{transform: rotate(0deg)}
to{transform: rotate(359deg)}
}
Loading…
Cancel
Save