linkis-web/src/components/consoleComponent/console.vue (678 lines of code) (raw):
<!--
~ 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.
-->
<template>
<div
ref="bottomPanel"
class="log-panel">
<div class="workbench-tabs">
<div class="workbench-tab-wrapper">
<div class="workbench-tab">
<div
:class="{active: scriptViewState.showPanel == 'progress'}"
class="workbench-tab-item"
@click="showPanelTab('progress')">
<span>{{ $t('message.common.tabs.progress') }}</span>
</div>
<div
v-if="script.result"
:class="{active: scriptViewState.showPanel == 'result'}"
class="workbench-tab-item"
@click="showPanelTab('result')">
<span>{{ $t('message.common.tabs.result') }}</span>
</div>
<div
v-if="isLogShow"
:class="{active: scriptViewState.showPanel == 'log'}"
class="workbench-tab-item"
@click="showPanelTab('log')">
<span>{{ $t('message.common.tabs.log') }}</span>
</div>
</div>
<!-- <div
class="workbench-tab-button">
<Icon
type="ios-close"
size="20"
@click="closeConsole"></Icon>
</div> -->
</div>
<div
class="workbench-container">
<we-progress
v-if="scriptViewState.showPanel == 'progress'"
:current="script.progress.current"
:waiting-size="script.progress.waitingSize"
:steps="script.steps"
:status="script.status"
:application="script.application"
:progress-info="script.progress.progressInfo"
:cost-time="script.progress.costTime"
@open-panel="openPanel"
:script-view-state="scriptViewState"/>
<result
v-if="scriptViewState.showPanel == 'result'"
ref="result"
:result="script.result"
:script="script"
:dispatch="dispatch"
:getResultUrl="getResultUrl"
:comData="comData"
@on-analysis="openAnalysisTab"
@on-set-change="changeResultSet"
:script-view-state="scriptViewState"/>
<log
v-if="scriptViewState.showPanel == 'log'"
:logs="script.log"
:log-line="script.logLine"
:script-view-state="scriptViewState"/>
</div>
</div>
</div>
</template>
<script>
import api from '@/common/service/api';
import util from '@/common/util';
import storage from '@/common/helper/storage';
import {debounce, isUndefined, isEmpty, last} from 'lodash';
import {Script} from './modal.js';
import result from './result.vue';
import log from './log.vue';
import weProgress from './progress.vue';
import Execute from '@/common/service/execute';
import mixin from '@/common/service/mixin';
export default {
components: {
result,
log,
weProgress,
},
mixins: [mixin],
props: {
stop: {
type: Boolean,
},
comData: {
type: Object
},
heigth: {
type: [String, Number]
},
dispatch: {
type: Function,
required: true
},
getResultUrl: {
type: String,
defalut: `filesystem`
}
},
data() {
return {
execute: null,
scriptViewState: {
showPanel: 'progress',
cacheLogScroll: 0,
bottomContentHeight: '250'
},
isBottomPanelFull: false,
isLogShow: false,
localLog: {
log: { all: '', error: '', warning: '', info: '' },
logLine: 1,
},
script: {
result: null,
steps: [],
progress: {},
log: { all: '', error: '', warning: '', info: '' },
logLine: 1,
resultList: null,
},
}
},
watch: {
stop(val, oldval) {
if (oldval && this.execute) {
// Keep the workflow page first, it should be used(先保留工作流页,应该会用到)
this.killExecute();
}
},
'scriptResult.headRows': function() {
if(this.visualShow === 'visual'){
this.openAnalysisTab('table')
}
},
executRun(val) {
this.$emit('executRun', val)
}
},
computed: {
executRun() {
return this.execute ? this.execute.run : false
}
},
mounted() {
this.scriptViewState.bottomContentHeight = this.heigth - 75;
},
methods: {
killExecute(flag = false) {
this.execute.trigger('stop');
if (flag) {
this.execute.trigger('kill');
}
},
createScript() {
this.script = new Script({
id: this.comData.id,
title: this.comData.name,
scriptViewState: this.scriptViewState,
});
},
createExecute(needQuery) {
const data = {
getResultUrl: this.getResultUrl,
method: '/api/rest_j/v1/entrance/execute',
websocketTag: this.comData.id,
data: {
executeApplicationName: null,
executionCode: null,
runType: this.comData.runType,
params: null,
postType: 'http',
source: {
scriptPath: null,
},
fromLine: this.script.logLine,
},
}
this.execute = new Execute(data);
if (needQuery) {
this.queryState();
}
},
queryState() {
if (this.comData.execID) {
const option = {
taskID: this.comData.taskID,
execID: this.comData.execID,
isRestore: true,
id: this.comData.id,
nodeId: this.comData.key || undefined,
}
this.execute.halfExecute(option);
this.monitoringData();
}
},
monitoringData() {
if (this.execute.taskID !== this.comData.taskID) {
return;
}
this.execute.on('log', (logs) => {
this.localLog = this.convertLogs(logs);
this.script.log = this.localLog.log;
if (this.scriptViewState.showPanel === 'log') {
this.localLogShow();
}
});
this.execute.on('result', (ret) => {
this.showPanelTab('result');
const storeResult = {
'headRows': ret.metadata,
'bodyRows': ret.fileContent,
'total': ret.totalLine,
'type': ret.type,
'cache': {
offsetX: 0,
offsetY: 0,
},
'path': this.execute.currentResultPath,
'current': 1,
'size': 20,
};
if (this.execute.resultList[0]) {
this.$set(this.execute.resultList[0], 'result', storeResult);
}
this.$set(this.script, 'resultSet', 0);
this.script.result = storeResult;
this.script.resultList = this.execute.resultList;
this.script.resultSet = 0;
// Set to 1 if progress has been obtained(获取过progress的情况下设置为1)
if (this.script.progress.current) {
this.script.progress.current = 1;
}
});
this.execute.on('progress', ({ progress, progressInfo, waitingSize }) => {
// Here progressInfo may just be an empty array, or the first data of the data is an empty object(这里progressInfo可能只是个空数组,或者数据第一个数据是一个空对象)
if (progressInfo.length && !isEmpty(progressInfo[0])) {
progressInfo.forEach((newProgress) => {
let newId = newProgress.id;
let index = this.script.progress.progressInfo.findIndex((progress) => {
return progress.id === newId;
});
if (index !== -1) {
this.script.progress.progressInfo.splice(index, 1, newProgress);
} else {
this.script.progress.progressInfo.push(newProgress);
}
});
} else {
progressInfo = [];
}
if (progress == 1) {
let runningScripts = storage.get(this._running_scripts_key, 'local') || {};
delete runningScripts[this.script.nodeId];
storage.set(this._running_scripts_key, runningScripts, 'local');
}
this.script.progress.current = progress;
if (waitingSize !== null && waitingSize >= 0) {
this.script.progress.waitingSize = waitingSize;
}
});
this.execute.on('steps', (status) => {
if (this.executeLastStatus === status) {
return;
}
this.executeLastStatus = status;
if (status === 'Inited') {
this.script.steps = ['Submitted', 'Inited'];
} else {
const lastStep = last(this.script.steps);
if (this.script.steps.indexOf(status) === -1) {
this.script.steps.push(status);
// When there may be a WaitForRetry state, the background will re-push the Scheduled or running state(针对可能有WaitForRetry状态后,后台会重新推送Scheduled或running状态的时候)
} else if (lastStep !== status) {
this.script.steps.push(status);
}
}
if (status !== 'Inited' && this.script.steps.length === 1) {
this.script.steps.unshift('ellipsis');
}
});
this.execute.on('status', (status) => {
this.script.status = status;
});
this.execute.on('querySuccess', ({ type, task }) => {
const costTime = util.convertTimestamp(task.costTime);
this.script.progress.costTime = costTime;
const name = this.script.title;
this.$Notice.close(name);
this.$Notice.success({
title: this.$root.$t('message.common.notice.querySuccess.title'),
desc: '',
render: (h) => {
return h('span', {
style: {
'word-break': 'break-all',
'line-height': '20px',
},
}, `${this.script.title} ${type}${this.$root.$t('message.common.notice.querySuccess.render')}:${costTime}!`);
},
name,
duration: 3,
});
});
this.execute.on('notice', ({ type, msg, autoJoin }) => {
const name = this.script.title;
const label = autoJoin ? `${this.$root.$t('message.common.script')}${this.script.title} ${msg}` : msg;
this.$Notice.close(name);
this.$Notice[type]({
title: this.$root.$t('message.common.notice.notice.title'),
name,
desc: '',
duration: 5,
render: (h) => {
return h('span', {
style: {
'word-break': 'break-all',
'line-height': '20px',
},
}, label);
},
});
});
this.execute.on('costTime', (time) => {
this.script.progress.costTime = util.convertTimestamp(time);
});
},
resetQuery() {
this.showPanelTab('progress');
this.isLogShow = false;
this.localLog = {
log: { all: '', error: '', warning: '', info: '' },
logLine: 1,
};
this.script.progress = {
current: null,
progressInfo: [],
waitingSize: null,
costTime: null,
};
this.script.log = { all: '', error: '', warning: '', info: '' };
this.script.logLine = 1;
this.script.steps = ['Submitted'];
this.script.diagnosis = null;
this.script.result = {
headRows: [],
bodyRows: [],
type: 'EMPTY',
total: 0,
path: '',
cache: {},
};
this.script.resultList = null;
},
showPanelTab(type) {
this.scriptViewState.showPanel = type;
this.script.showPanel = type;
if (type === 'log') {
this.localLogShow();
}
},
localLogShow() {
if (!this.debounceLocalLogShow) {
this.debounceLocalLogShow = debounce(() => {
if (this.localLog) {
this.script.log = Object.freeze({ ...this.localLog.log });
this.script.logLine = this.localLog.logLine;
}
}, 1500);
}
this.debounceLocalLogShow();
},
openAnalysisTab(type) {
this.visualShow = type;
// flage ? this.visualShow = 'table' : this.visualShow = 'visual'
// this.flage = !this.flage;
if (type === 'visual') {
this.biLoading = true;
let rows = this.scriptResult.headRows;
let model = {}
let dates = ["DATE", "DATETIME", "TIMESTAMP", "TIME", "YEAR"]
let numbers = [
"TINYINT",
"SMALLINT",
"MEDIUMINT",
"INT",
"INTEGER",
"BIGINT",
"FLOAT",
"DOUBLE",
"DOUBLE PRECISION",
"REAL",
"DECIMAL",
"BIT",
"SERIAL",
"BOOL",
"BOOLEAN",
"DEC",
"FIXED",
"NUMERIC"];
rows.forEach(item=>{
let sqlType = item.dataType.toUpperCase();
let visualType = 'string'
if (numbers.indexOf(sqlType) > -1) {
visualType = 'number'
} else if(dates.indexOf(sqlType) > -1) {
visualType = 'date'
}
model[item.columnName] = {
sqlType,
visualType,
modelType: visualType ==="number" ? "value": "category"
}
})
this.visualParams = {
// viewId: id,
// projectId,
json: {
name: `${this.script.fileName.replace(/\./g,'')}${this.script.resultSet}`,
model,
source: {
"engineType": "spark", //引擎类型
"dataSourceType": "resultset", //数据源类型,结果集、脚本、库表
"dataSourceContent": {
"resultLocation": this.scriptResult.path
},
"creator": "IDE"
}
}
}
} else if (type === 'dataWrangler') {
this.dataWranglerParams = {
simpleMode: true,
showBottomBar: false,
importConfig: {
"dataSourceConfig": {
"dataSourceType": "linkis",
"dataSourceOptions": {"taskID": this.work.taskID || this.work.data.history[0].taskID}
},
"config": {
"myConfig": {
"resultSetPath": [this.scriptResult.path]
},
"importConfig": {
"mergeTables": true,
"limitRows": 5000,
"pivotTable": false,
"tableHeaderRows": 1,
}
}
}
}
}
},
changeResultSet(data, cb) {
const resultSet = isUndefined(data.currentSet) ? this.script.resultSet : data.currentSet;
const findResult = this.script.resultList[resultSet];
const resultPath = findResult && findResult.path;
const hasResult = Object.prototype.hasOwnProperty.call(this.script.resultList[resultSet], 'result');
if (!hasResult) {
const pageSize = 5000;
const url = `/${this.getResultUrl}/openFile`;
let params = {
path: resultPath,
pageSize,
}
// If it is api execution, you need to bring taskId(如果是api执行需要带上taskId)
if (this.getResultUrl !== 'filesystem') {
params.taskId = this.comData.taskID
}
api.fetch(url, {
path: resultPath,
pageSize,
}, 'get')
.then((ret) => {
const result = {
'headRows': ret.metadata,
'bodyRows': ret.fileContent,
// If totalLine is null, it will be displayed as 0(如果totalLine是null,就显示为0)
'total': ret.totalLine ? ret.totalLine : 0,
// If the content is null, it will display no data(如果内容为null,就显示暂无数据)
'type': ret.fileContent ? ret.type : 0,
'cache': {
offsetX: 0,
offsetY: 0,
},
'path': resultPath,
'current': 1,
'size': 20,
};
this.$set(this.script.resultList[resultSet], 'result', result);
this.$set(this.script, 'resultSet', resultSet);
this.script.result = result;
this.script.resultSet = resultSet;
this.script.showPanel = 'result';
cb();
}).catch(() => {
cb();
});
} else {
this.script.result = this.script.resultList[resultSet].result;
this.$set(this.script, 'resultSet', resultSet);
this.script.resultSet = resultSet;
this.script.showPanel = 'result';
cb();
}
},
closeConsole() {
this.$emit('close-console');
},
openPanel(type) {
if (type === 'log') {
this.isLogShow = true;
this.showPanelTab(type);
}
},
checkFromCache() {
// Every time you right-click the console, a new instance will be created, so stop the last executed instance first(每次右键控制台,都会创建一个新实例,所以把上一个执行的实例先停掉)
if (this.execute) {
this.killExecute();
}
this.resetQuery();
this.createScript();
this.$nextTick(() => {
this.createExecute(true);
})
},
getLogs() {
api.fetch(`/entrance/${this.comData.execID}/log`, {
fromLine: this.fromLine,
size: -1,
}, 'get')
.then((rst) => {
this.localLog = this.convertLogs(rst.log);
this.script.log = this.localLog.log;
this.script.logLine = this.fromLine = rst.fromLine;
if (this.scriptViewState.showPanel === 'log') {
this.localLogShow();
}
});
},
convertLogs(logs) {
const tmpLogs = this.localLog || {
log: { all: '', error: '', warning: '', info: '' },
logLine: 1,
};
let hasLogInfo = Array.isArray(logs) ? logs.some((it) => it.length > 0) : logs;
if (!hasLogInfo) {
return tmpLogs;
}
const convertLogs = util.convertLog(logs);
Object.keys(convertLogs).forEach((key) => {
const convertLog = convertLogs[key];
if (convertLog) {
tmpLogs.log[key] += convertLog + '\n';
}
if (key === 'all') {
tmpLogs.logLine += convertLog.split('\n').length;
}
});
return tmpLogs;
}
}
}
</script>
<style lang="scss" scoped>
@import '@/common/style/variables.scss';
.log-panel {
margin-top: 1px;
border-top: $border-width-base $border-style-base $border-color-base;
background-color: $background-color-base;
.workbench-tabs {
position: $relative;
height: 100%;
overflow: hidden;
box-sizing: border-box;
z-index: 3;
.workbench-tab-wrapper {
display: flex;
border-top: $border-width-base $border-style-base #dcdcdc;
border-bottom: $border-width-base $border-style-base #dcdcdc;
.workbench-tab {
flex: 1;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
height: 32px;
background-color: $body-background;
width: calc(100% - 45px);
overflow: hidden;
&.work-list-tab {
overflow-x: auto;
overflow-y: hidden;
&::-webkit-scrollbar {
width: 0;
height: 0;
background-color: transparent;
}
.list-group>span {
white-space: nowrap;
display: block;
height: 0;
}
}
.workbench-tab-item {
text-align: center;
border-top: none;
display: inline-block;
height: 32px;
line-height: 32px;
background-color: $background-color-base;
color: $title-color;
cursor: pointer;
min-width: 100px;
max-width: 200px;
overflow: hidden;
margin-right: 2px;
border: 1px solid #eee;
&.active {
margin-top: 1px;
background-color: $body-background;
color: $primary-color;
border-radius: 4px 4px 0 0;
border: 1px solid $border-color-base;
border-bottom: 2px solid $primary-color;
}
}
}
.workbench-tab-control {
flex: 0 0 45px;
text-align: right;
background-color: $body-background;
border-left: $border-width-base $border-style-base $border-color-split;
.ivu-icon {
font-size: $font-size-base;
margin-top: 8px;
margin-right: 2px;
cursor: pointer;
&:hover {
color: $primary-color;
}
&.disable {
color: $btn-disable-color;
}
}
}
.workbench-tab-button {
flex: 0 0 30px;
text-align: center;
background-color: $body-background;
.ivu-icon {
font-size: $font-size-base;
margin-top: 8px;
cursor: pointer;
}
}
}
.workbench-container {
height: calc(100% - 36px);
&.node {
height: 100%;
}
@keyframes ivu-progress-active {
0% {
opacity: .3;
width: 0;
}
100% {
opacity: 0;
width: 100%;
}
}
}
}
}
</style>