blob: 233c5a02ca1d558c7a9bc9c69d374113c15fa19f [file] [log] [blame]
<!--
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>
<!--(toggle for debugging)-->
<el-card v-if="selectedJobId" style="background-color: ghostwhite;" shadow="never">
<!--<el-card>-->
<h1>Details for Job {{ jobFrom ? jobFrom : 'NULL' }}</h1>
<p>
<b>Status: </b>
<el-tag :type="_fromJobStatusToType(selectedJobStatus)">{{ selectedJobStatus }}</el-tag><br>
</p>
<el-collapse accordion @change="handleCollapse">
<!--Event Timeline-->
<!--
<el-collapse-item title=" Event Timeline" name="1">
<el-card header="Timeline" class="detail-card">
<metric-timeline
ref="metricTimeline"
:selectedJobId="selectedJobId"
:groups="groupDataSet"/>
</el-card>
<el-row :gutter="10">
<el-col height="100%" :span="12" :xs="24">
<el-card class="detail-card" header="Select stage">
<stage-select
:selectedJobId="selectedJobId"
:metricLookupMap="metricLookupMap"/>
</el-card>
</el-col>
<el-col :span="12" :xs="24">
<el-card class="detail-card" header="Detail">
<detail-table
:tableData="tableData"/>
</el-card>
</el-col>
</el-row>
</el-collapse-item>
-->
<!--DAG Visualization-->
<el-collapse-item title=" DAG Visualization" name="2">
<no-ssr>
<affix relative-element-selector="#affix-target" style="z-index: 1">
<el-popover v-model="showdetail" trigger="manual" width="400">
<el-button style="float: right;" size="mini" icon="el-icon-close" circle
@click="showdetail = false"></el-button>
<detail-table :tableData="tableData"/>
</el-popover>
</affix>
</no-ssr>
<el-card header="DAG">
<dag :selectedJobId="selectedJobId" :tabIndex="tabIndex" id="affix-target"/>
</el-card>
</el-collapse-item>
<!--Tasks information-->
<el-collapse-item title=" Task Statistics" name="3">
<task-statistics
:selectedJobId="selectedJobId"
:taskStatistics="selectedTaskStatistics"/>
</el-collapse-item>
</el-collapse>
<!--Stages List-->
<h2 ref="stages">Stages
<el-badge type="info" :value="stageList.length"></el-badge></h2>
<div>
<!--<div v-if="pendingStagesData.length !== 0">-->
<el-table class="pending-stages-table" :data="stageList" stripe>
<el-table-column label="Stage id">
<template slot-scope="scope">
{{ scope.row }}
</template>
</el-table-column>
<el-table-column label="State">
<template slot-scope="scope">
<el-tag :type="_fromStageStatusToType(_getStageState(selectedTaskStatistics.stageState[scope.row]))">{{ _getStageState(selectedTaskStatistics.stageState[scope.row]) }}</el-tag><br>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</template>
<script>
import Vue from 'vue';
import { DataSet } from 'vue2vis';
import MetricTimeline from './MetricTimeline'
import DetailTable from './DetailTable';
import StageSelect from './StageSelect';
import DAG from './DAG';
import TaskStatistics from '../../TaskStatistics';
import { STATE, JOB_STATUS } from '../../../assets/constants';
// list of metric, order of elements matters.
export const METRIC_LIST = [
'StageMetric',
'TaskMetric',
];
const LISTENING_EVENT_LIST = [
'job-id-select',
'job-id-deselect',
'build-table-data',
'metric-select',
'metric-deselect',
];
export default {
components: {
'metric-timeline': MetricTimeline,
'stage-select': StageSelect,
'detail-table': DetailTable,
'dag': DAG,
'task-statistics': TaskStatistics,
},
props: ['selectedJobStatus', 'selectedJobMetricDataSet', 'selectedTaskStatistics'],
data() {
return {
STATE: STATE,
// timeline dataset
groupDataSet: new DataSet([]),
// selected metric id
selectedMetricId: '',
// selected job id
selectedJobId: '',
// endpoint or file name of job
jobFrom: '',
metricLookupMap: {}, // metricId -> data
// element-ui specific
collapseActiveNames: ['timeline', 'dag'],
tableData: [],
tabIndex: '0',
showdetail: false,
}
},
// COMPUTED
computed: {
// All stages
stageList() {
return Object.keys(this.metricLookupMap).filter(id => /^Stage[0-9]+$/.test(id.trim()));
},
},
// METHODS
methods: {
// event timeline, dag event handler
handleCollapse(activatedElement) {
if (activatedElement === "1") {
this.$eventBus.$emit('set-timeline-items', this.selectedJobMetricDataSet);
this.$eventBus.$emit('redraw-timeline');
} else if (activatedElement === "2") {
this.$eventBus.$emit('rerender-dag');
}
},
// jump to the table
jump(event, val) {
switch (val) {
case STATE.READY:
this.$refs.pendingStages.scrollIntoView();
break;
case STATE.EXECUTING:
this.$refs.activeStages.scrollIntoView();
break;
case STATE.COMPLETE:
this.$refs.completedStages.scrollIntoView();
break;
case STATE.INCOMPLETE:
this.$refs.skippedStages.scrollIntoView();
break;
}
},
/**
* Set up event handlers for this component.
*/
setUpEventHandlers() {
// event handler for detecting change of job id
this.$eventBus.$on('job-id-select', data => {
this.$eventBus.$emit('clear-stage-select');
this.selectedJobId = data.jobId;
this.jobFrom = data.jobFrom;
this.metricLookupMap = data.metricLookupMap;
this.selectedMetricId = '';
});
this.$eventBus.$on('job-id-deselect', () => {
this.$eventBus.$emit('set-timeline-items', new DataSet([]));
this.$eventBus.$emit('clear-stage-select');
this.selectedJobId = '';
this.jobFrom = '';
this.metricLookupMap = {};
this.selectedMetricId = '';
});
this.$eventBus.$on('build-table-data', ({ metricId, jobId }) => {
if (this.selectedJobId === jobId &&
this.selectedMetricId === metricId) {
this.buildTableData(metricId);
this.showdetail = true;
}
});
// event handler for individual metric selection
this.$eventBus.$on('metric-select', metricId => {
this.selectedMetricId = metricId;
this.buildTableData(metricId);
this.showdetail = true;
this.$eventBus.$emit('metric-select-done');
});
// event handler for individual metric deselection
this.$eventBus.$on('metric-deselect', async () => {
this.tableData = [];
this.selectedMetricId = '';
await this.$nextTick();
this.showdetail = false;
this.$eventBus.$emit('metric-deselect-done');
});
},
/**
* Build table data which will be used in TaskStatistics component.
* @param metricId id of metric. Used to lookup metricLookupMap.
*/
buildTableData(metricId) {
this.tableData = [];
const metric = this._removeUnusedProperties(this.metricLookupMap[metricId]);
Object.keys(metric).forEach(key => {
if (typeof metric[key] === 'object') {
if (key === 'executionProperties') {
let executionPropertyArray = [];
Object.keys(metric[key]).forEach(ep => {
executionPropertyArray.push({
key: ep,
value: metric[key][ep],
});
});
this.tableData.push({
key: key,
value: '',
extra: executionPropertyArray,
});
}
} else {
let value = metric[key] === -1 ? 'N/A' : metric[key];
if (value !== 'N/A' && key.toLowerCase().endsWith('bytes')) {
value = this._bytesToHumanReadable(value);
}
this.tableData.push({
key: key,
value: value,
});
}
});
},
_fromJobStatusToType(status) {
switch (status) {
case JOB_STATUS.RUNNING:
return 'primary';
case JOB_STATUS.COMPLETE:
return 'success';
case JOB_STATUS.FAILED:
return 'danger';
default:
return 'info';
}
},
_getStageState(status) {
if (this.selectedJobStatus === JOB_STATUS.COMPLETE) {
return STATE.COMPLETE
}
if (status == null) {
return STATE.INCOMPLETE
}
return status
},
_fromStageStatusToType(status) {
switch (status) {
case STATE.INCOMPLETE:
return 'primary';
case STATE.COMPLETE:
return 'success';
case JOB_STATUS.FAILED:
return 'danger';
default:
return 'info';
}
},
_bytesToHumanReadable(bytes) {
var i = bytes === 0 ? 0 :
Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) * 1
+ ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i];
},
_removeUnusedProperties(metric) {
let newMetric = Object.assign({}, metric);
delete newMetric.group;
delete newMetric.content;
return newMetric;
},
},
// HOOKS
beforeMount() {
// predefine group sets
METRIC_LIST.forEach(metricType => {
this.groupDataSet.add({
id: metricType,
content: metricType
});
});
this.setUpEventHandlers();
},
beforeDestroy() {
LISTENING_EVENT_LIST.forEach(e => {
this.$eventBus.$off(e);
});
},
}
</script>