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 */} +
+ {t('project.node.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