diff --git a/dolphinscheduler-ui-next/src/router/modules/projects.ts b/dolphinscheduler-ui-next/src/router/modules/projects.ts index 683517ca9c..afc401dbcd 100644 --- a/dolphinscheduler-ui-next/src/router/modules/projects.ts +++ b/dolphinscheduler-ui-next/src/router/modules/projects.ts @@ -121,6 +121,16 @@ export default { auth: [] } }, + { + path: '/projects/:projectCode/workflow/instances/:id/gantt', + name: 'workflow-instance-gantt', + component: components['projects-workflow-instance-gantt'], + meta: { + title: '工作流实例甘特图', + showSide: true, + auth: [] + } + }, { path: '/projects/:projectCode/task/definitions', name: 'task-definition', diff --git a/dolphinscheduler-ui-next/src/service/modules/process-instances/index.ts b/dolphinscheduler-ui-next/src/service/modules/process-instances/index.ts index 67b6323aad..42578abc71 100644 --- a/dolphinscheduler-ui-next/src/service/modules/process-instances/index.ts +++ b/dolphinscheduler-ui-next/src/service/modules/process-instances/index.ts @@ -118,7 +118,7 @@ export function queryTaskListByProcessId(id: number, code: number): any { }) } -export function vieGanttTree(id: IdReq, code: CodeReq): any { +export function viewGanttTree(id: number, code: number): any { return axios({ url: `/projects/${code}/process-instances/${id}/view-gantt`, method: 'get' diff --git a/dolphinscheduler-ui-next/src/utils/common.ts b/dolphinscheduler-ui-next/src/utils/common.ts index e5675b9f4e..f328dbb4f3 100644 --- a/dolphinscheduler-ui-next/src/utils/common.ts +++ b/dolphinscheduler-ui-next/src/utils/common.ts @@ -261,7 +261,7 @@ export const tasksState = (t: any): ITaskState => ({ SUCCESS: { id: 7, desc: `${t('project.workflow.success')}`, - color: '#33cc00', + color: '#95DF96', icon: CheckCircleOutlined, isSpin: false, classNames: 'success' diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/table-action.tsx b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/table-action.tsx index a4800418f2..faa9e6029b 100644 --- a/dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/table-action.tsx +++ b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/components/table-action.tsx @@ -62,6 +62,14 @@ export default defineComponent({ }) } + const handleGantt = () => { + router.push({ + name: 'workflow-instance-gantt', + params: { id: props.row!.id }, + query: { code: props.row!.processDefinitionCode } + }) + } + const handleReRun = () => { ctx.emit('reRun') } @@ -89,6 +97,7 @@ export default defineComponent({ handleStop, handleSuspend, handleDeleteInstance, + handleGantt, ...toRefs(props) } }, @@ -280,8 +289,8 @@ export default defineComponent({ size='tiny' type='info' circle - /* TODO: Goto gantt*/ disabled={this.row?.disabled} + onClick={this.handleGantt} > diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/components/gantt-chart.tsx b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/components/gantt-chart.tsx new file mode 100644 index 0000000000..f4785b6ce3 --- /dev/null +++ b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/components/gantt-chart.tsx @@ -0,0 +1,214 @@ +/* + * 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 { defineComponent, ref, PropType } from 'vue' +import * as echarts from 'echarts' +import type { Ref } from 'vue' +import { useI18n } from 'vue-i18n' +import initChart from '@/components/chart' +import { tasksState } from '@/utils/common' +import { format } from 'date-fns' +import { parseTime } from '@/utils/common' +import { ISeriesData } from '../type' + +const props = { + height: { + type: [String, Number] as PropType, + default: window.innerHeight - 174 + }, + width: { + type: [String, Number] as PropType, + default: '100%' + }, + seriesData: { + type: Array as PropType>, + default: () => [] + }, + taskList: { + type: Array as PropType>, + default: [] + } +} + +const GanttChart = defineComponent({ + name: 'GanttChart', + props, + setup(props) { + const graphChartRef: Ref = ref(null) + const { t } = useI18n() + + const state = tasksState(t) + + const data: ISeriesData = {} + Object.keys(state).forEach((key) => (data[key] = [])) + const series = Object.keys(state).map((key) => ({ + id: key, + type: 'custom', + name: state[key].desc, + renderItem: renderItem, + itemStyle: { + opacity: 0.8, + color: state[key].color, + color0: state[key].color + }, + encode: { + x: [1, 2], + y: 0 + }, + data: data[key] + })) + + // format series data + let minTime = Number(new Date()) + props.seriesData.forEach(function (task, index) { + minTime = minTime < task.startDate[0] ? minTime : task.startDate[0] + data[task.status].push({ + name: task.name, + value: [ + index, + task.startDate[0], + task.endDate[0], + task.endDate[0] - task.startDate[0] + ], + itemStyle: { + color: state[task.status].color + } + }) + }) + + // customer render + function renderItem(params: any, api: any) { + const taskIndex = api.value(0) + const start = api.coord([api.value(1), taskIndex]) + const end = api.coord([api.value(2), taskIndex]) + const height = api.size([0, 1])[1] * 0.6 + + const rectShape = echarts.graphic.clipRectByRect( + { + x: start[0], + y: start[1] - height / 2, + width: end[0] - start[0], + height: height + }, + { + x: params.coordSys.x, + y: params.coordSys.y, + width: params.coordSys.width, + height: params.coordSys.height + } + ) + return ( + rectShape && { + type: 'rect', + transition: ['shape'], + shape: rectShape, + style: api.style() + } + ) + } + + const option = { + title: { + text: t('project.workflow.task_state'), + textStyle: { + fontWeight: 'normal', + fontSize: 14 + }, + left: 50 + }, + tooltip: { + formatter: function (params: any) { + const taskName = params.data.name + const data = props.seriesData.filter( + (item) => item.taskName === taskName + ) + let str = `taskName : ${taskName}
` + str += `status : ${state[data[0].status].desc} (${ + data[0].status + })
` + str += `startTime : ${data[0].isoStart}
` + str += `endTime : ${data[0].isoEnd}
` + str += `duration : ${data[0].duration}
` + return str + } + }, + legend: { + left: 150, + padding: [5, 5, 5, 5] + }, + dataZoom: [ + { + type: 'slider', + filterMode: 'weakFilter', + showDataShadow: false, + top: + props.taskList.length * 100 > 200 + ? props.taskList.length * 100 + : 200, + labelFormatter: '' + }, + { + type: 'inside', + filterMode: 'weakFilter' + } + ], + grid: { + height: props.taskList.length * 50, + top: 80 + }, + xAxis: { + min: minTime, + scale: true, + position: 'top', + axisTick: { show: true }, + splitLine: { show: false }, + axisLabel: { + formatter: function (val: number) { + return format(parseTime(val), 'HH:mm:ss') + } + } + }, + yAxis: { + axisTick: { show: false }, + splitLine: { show: false }, + axisLine: { show: false }, + max: props.taskList.length, + data: props.taskList + }, + series: series + } + + initChart(graphChartRef, option) + + return { graphChartRef } + }, + render() { + const { height, width } = this + + return ( +
+ ) + } +}) + +export default GanttChart diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/index.tsx b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/index.tsx new file mode 100644 index 0000000000..9f68e37c10 --- /dev/null +++ b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/index.tsx @@ -0,0 +1,73 @@ +/* + * 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 { defineComponent, onMounted, toRefs, watch } from 'vue' +import { useI18n } from 'vue-i18n' +import { useRoute } from 'vue-router' +import Card from '@/components/card' +import GanttChart from './components/gantt-chart' +import { useGantt } from './use-gantt' + +const workflowRelation = defineComponent({ + name: 'workflow-relation', + setup() { + const { t, locale } = useI18n() + const route = useRoute() + + const { variables, getGantt } = useGantt() + + const id = Number(route.params.id) + const code = Number(route.params.projectCode) + + const handleResetDate = () => { + variables.seriesData = [] + variables.taskList = [] + getGantt(id, code) + } + + onMounted(() => { + getGantt(id, code) + }) + + watch( + () => [locale.value], + () => { + handleResetDate() + } + ) + + return { t, ...toRefs(variables) } + }, + render() { + const { t } = this + return ( + + {{ + default: () => + this.seriesData.length > 0 && ( + + ) + }} + + ) + } +}) + +export default workflowRelation diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/type.ts b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/type.ts new file mode 100644 index 0000000000..24068b5ca9 --- /dev/null +++ b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/type.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ + +interface ITask { + taskName: string + startDate: Array + endDate: Array + isoStart: string + isoEnd: string + status: string + duration: string +} + +interface IGanttRes { + height: number + taskNames: Array + taskStatus: Object + tasks: Array +} + +interface ISeriesData { + [taskState: string]: Array +} + +export { ITask, IGanttRes, ISeriesData } diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/use-gantt.ts b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/use-gantt.ts new file mode 100644 index 0000000000..1560d456f1 --- /dev/null +++ b/dolphinscheduler-ui-next/src/views/projects/workflow/instance/gantt/use-gantt.ts @@ -0,0 +1,53 @@ +/* + * 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 { reactive } from 'vue' +import { useAsyncState } from '@vueuse/core' +import { viewGanttTree } from '@/service/modules/process-instances' +import { IGanttRes } from './type' + +export function useGantt() { + const variables = reactive({ + seriesData: [], + taskList: [] as Array + }) + + const formatGantt = (obj: IGanttRes) => { + variables.seriesData = [] + variables.taskList = [] + + variables.seriesData = obj.tasks.map((item) => { + variables.taskList.push(item.taskName) + return { + name: item.taskName, + ...item + } + }) as any + } + + const getGantt = (id: number, code: number) => { + const { state } = useAsyncState( + viewGanttTree(id, code).then((res: IGanttRes) => { + formatGantt(res) + }), + {} + ) + return state + } + + return { variables, getGantt } +}