Browse Source

[Feature][UI Next] Add dag view log

Devosend 3 years ago
parent
commit
8136b8e064

+ 11 - 1
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',

+ 11 - 1
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: '任务名称',

+ 10 - 3
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>
       )
     )

+ 15 - 2
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>
     )
   }

+ 13 - 1
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 - 0
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 - 0
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)}
+}