diff --git a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
index 14cc11a222..5a83bc016e 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
+++ b/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',
diff --git a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
index 1a8d29946f..3b283f84eb 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
+++ b/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: '任务名称',
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-context-menu.tsx b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-context-menu.tsx
index f2964aa395..944a64034b 100644
--- a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-context-menu.tsx
+++ b/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')}
- {/* TODO: view log */}
+
)
)
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx
index 6f469baf7d..ca69b8de21 100644
--- a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx
+++ b/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 && (
)}
+ {!!props.instance && logModalShow.value && (
+
+ )}
)
}
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-menu.ts b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-menu.ts
index df66c3e396..61f011bfb3 100644
--- a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-menu.ts
+++ b/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
}
}
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/log-modal.tsx b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/log-modal.tsx
new file mode 100644
index 0000000000..e28cfb1e98
--- /dev/null
+++ b/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,
+ default: -1
+ },
+ taskInstanceType: {
+ type: String as PropType,
+ 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 (
+
+
+ {this.taskInstanceId && this.taskInstanceType !== 'SUB_PROCESS' && (
+
+ {renderSlot(this.$slots, 'history')}
+
+
+ {renderSlot(this.$slots, 'log')}
+
+
+ )}
+
+ {
+
+
+
+
+ {this.t('project.workflow.view_log')}
+
+
+
+ {{
+ trigger: () => (
+
+
+
+
+
+ ),
+ default: () => this.t('project.workflow.download_log')
+ }}
+
+
+ {{
+ trigger: () => (
+
+ !this.loadingRef && this.refreshLog()
+ }
+ >
+
+
+
+
+ ),
+ default: () => this.t('project.workflow.refresh_log')
+ }}
+
+
+ {{
+ trigger: () => (
+
+
+ {this.isScreen ? (
+
+ ) : (
+
+ )}
+
+
+ ),
+ default: () =>
+ this.isScreen
+ ? this.t('project.workflow.cancel_full_screen')
+ : this.t('project.workflow.enter_full_screen')
+ }}
+
+
+
+
+
+
+ {this.t('project.workflow.close')}
+
+
+
+
+ }
+
+
+
+ )
+ }
+})
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/log.module.scss b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/log.module.scss
new file mode 100644
index 0000000000..a6afd05f18
--- /dev/null
+++ b/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)}
+}
\ No newline at end of file