From af6ddb200ed121ab896ebe23729493e443cad231 Mon Sep 17 00:00:00 2001 From: 735140144 <735140144@qq.com> Date: Thu, 29 Aug 2024 13:35:33 +0800 Subject: [PATCH 1/5] [Feature][Server][UI]Improve Notification with quick link without check Token --- .../JobHistoryExecutionController.java | 135 ++++++++++ .../validator/JobResultValidator.java | 3 +- datavines-ui/package.json | 1 + datavines-ui/src/router/index.tsx | 4 + datavines-ui/src/utils/base64.ts | 11 + .../src/view/JobHistory/Instance/index.tsx | 253 ++++++++++++++++++ .../useInstanceErrorDataModal/index.tsx | 109 ++++++++ .../Instance/useInstanceResult/index.old.tsx | 142 ++++++++++ .../Instance/useInstanceResult/index.tsx | 118 ++++++++ .../JobHistory/Instance/useLogger/index.tsx | 123 +++++++++ 10 files changed, 898 insertions(+), 1 deletion(-) create mode 100644 datavines-server/src/main/java/io/datavines/server/api/controller/JobHistoryExecutionController.java create mode 100644 datavines-ui/src/utils/base64.ts create mode 100644 datavines-ui/src/view/JobHistory/Instance/index.tsx create mode 100644 datavines-ui/src/view/JobHistory/Instance/useInstanceErrorDataModal/index.tsx create mode 100644 datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.old.tsx create mode 100644 datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.tsx create mode 100644 datavines-ui/src/view/JobHistory/Instance/useLogger/index.tsx diff --git a/datavines-server/src/main/java/io/datavines/server/api/controller/JobHistoryExecutionController.java b/datavines-server/src/main/java/io/datavines/server/api/controller/JobHistoryExecutionController.java new file mode 100644 index 000000000..60bdfb865 --- /dev/null +++ b/datavines-server/src/main/java/io/datavines/server/api/controller/JobHistoryExecutionController.java @@ -0,0 +1,135 @@ +/* + * 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. + */ +package io.datavines.server.api.controller; + +import io.datavines.core.constant.DataVinesConstants; +import io.datavines.core.entity.ResultMap; +import io.datavines.core.enums.Status; +import io.datavines.core.exception.DataVinesServerException; +import io.datavines.server.api.annotation.AuthIgnore; +import io.datavines.server.api.dto.bo.job.JobExecutionPageParam; +import io.datavines.server.api.dto.vo.JobExecutionResultVO; +import io.datavines.server.dqc.coordinator.log.LogService; +import io.datavines.server.repository.entity.JobExecution; +import io.datavines.server.repository.service.JobExecutionErrorDataService; +import io.datavines.server.repository.service.JobExecutionResultService; +import io.datavines.server.repository.service.JobExecutionService; +import io.datavines.server.utils.FileUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; + +import static io.datavines.common.utils.OSUtils.judgeConcurrentHost; + + +@Slf4j +@Api(value = "job", tags = "job", produces = MediaType.APPLICATION_JSON_VALUE) +@RestController +@RequestMapping(value = DataVinesConstants.BASE_API_PATH + "/history/job/execution", produces = MediaType.APPLICATION_JSON_VALUE) +public class JobHistoryExecutionController { + + @Autowired + private JobExecutionService jobExecutionService; + + @Autowired + private JobExecutionResultService jobExecutionResultService; + + @Autowired + private JobExecutionErrorDataService jobExecutionErrorDataService; + + @Resource + private LogService logService; + + @AuthIgnore + @ApiOperation(value = "get job execution page", response = JobExecutionResultVO.class, responseContainer = "page") + @PostMapping(value = "/page") + public Object page(@Valid @RequestBody JobExecutionPageParam jobExecutionPageParam) { + if(jobExecutionPageParam.getJobId()==null){ + return ResponseEntity.ok(new ResultMap().fail()); + } + return ResponseEntity.ok(new ResultMap().success().payload(jobExecutionService.getJobExecutionPage(jobExecutionPageParam))); + } + + @AuthIgnore + @ApiOperation(value = "get job execution result", response = JobExecutionResultVO.class) + @GetMapping(value = "/list/result/{executionId}") + public Object getJobExecutionResultInfoList(@PathVariable("executionId") Long executionId) { + return ResponseEntity.ok(new ResultMap().success().payload(jobExecutionResultService.getResultVOListByJobExecutionId(executionId))); + } + + @AuthIgnore + @ApiOperation(value = "get job execution error data page", response = Object.class, responseContainer = "page") + @GetMapping(value = "/errorDataPage") + public Object readErrorDataPage(@RequestParam("taskId") Long taskId, + @RequestParam("pageNumber") Integer pageNumber, + @RequestParam("pageSize") Integer pageSize){ + return ResponseEntity.ok(new ResultMap().success().payload(jobExecutionErrorDataService.readErrorDataPage(taskId, pageNumber, pageSize))); + } + + @AuthIgnore + @ApiOperation(value = "queryLogWithOffsetLine", notes = "query task log with offsetLine") + @GetMapping(value = "/queryLogWithOffsetLine") + public Object queryLogWithOffsetLine(@RequestParam("taskId") Long taskId, + @RequestParam("offsetLine") int offsetLine, + HttpServletRequest request, HttpServletResponse response) throws IOException { + String taskHost = jobExecutionService.getJobExecutionHost(taskId); + Boolean isConcurrentHost = judgeConcurrentHost(taskHost); + if (isConcurrentHost) { + return ResponseEntity.ok(new ResultMap().success().payload(logService.queryLog(taskId, offsetLine))); + } + + response.sendRedirect(request.getScheme() + "://" + taskHost + + "/api/v1/history/job/execution/queryLogWithOffsetLine?offsetLine=" + offsetLine + "&taskId=" + taskId + "&Authorization=" + request.getHeader("Authorization")); + return null; + } + + @AuthIgnore + @ApiOperation(value = "download", notes = "download log file") + @GetMapping(value = "/download") + public void download(@RequestParam("taskId") Long taskId, HttpServletRequest request, HttpServletResponse response) throws IOException { + JobExecution jobExecution = jobExecutionService.getById(taskId); + if(null == jobExecution){ + throw new DataVinesServerException(Status.TASK_NOT_EXIST_ERROR, taskId); + } + String taskHost = jobExecution.getExecuteHost(); + if(StringUtils.isEmpty(taskHost)){ + throw new DataVinesServerException(Status.TASK_EXECUTE_HOST_NOT_EXIST_ERROR, taskId); + } + Boolean isConcurrentHost = judgeConcurrentHost(taskHost); + if (isConcurrentHost) { + if(StringUtils.isEmpty(jobExecution.getLogPath())){ + throw new DataVinesServerException(Status.TASK_LOG_PATH_NOT_EXIST_ERROR, taskId); + } + FileUtils.downloadToResp(jobExecution.getLogPath(), response); + return; + } + response.sendRedirect(request.getScheme() + "://" + taskHost + "/api/v1/history/job/execution/download?taskId=" + taskId+"&Authorization="+request.getHeader("Authorization")); + } + + +} diff --git a/datavines-server/src/main/java/io/datavines/server/dqc/coordinator/validator/JobResultValidator.java b/datavines-server/src/main/java/io/datavines/server/dqc/coordinator/validator/JobResultValidator.java index 3d5c9c606..a20bb2c14 100644 --- a/datavines-server/src/main/java/io/datavines/server/dqc/coordinator/validator/JobResultValidator.java +++ b/datavines-server/src/main/java/io/datavines/server/dqc/coordinator/validator/JobResultValidator.java @@ -43,6 +43,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; import java.util.*; import static io.datavines.common.ConfigConstants.FIX_VALUE; @@ -142,7 +143,7 @@ private void sendErrorEmail(Long jobExecutionId) { dataSourceName = dataSource.getName(); dataSourceType = dataSource.getType(); if (!CommonPropertyUtils.DATAVINES_FQDN_DEFAULT.equals(CommonPropertyUtils.getString(CommonPropertyUtils.DATAVINES_FQDN))) { - fqdn = CommonPropertyUtils.getString(CommonPropertyUtils.DATAVINES_FQDN) + String.format("/#/main/detail/%s/jobs/instance?jobId=%s", dataSourceId, jobId); + fqdn = CommonPropertyUtils.getString(CommonPropertyUtils.DATAVINES_FQDN) + String.format("/#/history?%s", Base64.getEncoder().encodeToString(String.format("jobId=%s&ds=%s",jobId,dataSourceName).getBytes(StandardCharsets.UTF_8))); } } diff --git a/datavines-ui/package.json b/datavines-ui/package.json index d416041a3..5edf1b0d5 100644 --- a/datavines-ui/package.json +++ b/datavines-ui/package.json @@ -23,6 +23,7 @@ "ahooks": "^3.7.4", "antd": "^5.0.5", "axios": "^0.21.1", + "base-64": "^1.0.0", "dayjs": "^1.11.7", "echarts": "^5.4.0", "moment": "^2.29.4", diff --git a/datavines-ui/src/router/index.tsx b/datavines-ui/src/router/index.tsx index 6e6579afe..50eeb5046 100644 --- a/datavines-ui/src/router/index.tsx +++ b/datavines-ui/src/router/index.tsx @@ -16,6 +16,10 @@ export const routerNoLogin: TRouterItem[] = [ path: '/forgetPwd', component: lazy(() => import(/* webpackChunkName: 'view-forgetPwd' */ '@/view/ForgetPassword')), }, + { + path: '/history', + component: lazy(()=>import('@/view/JobHistory/Instance')) + } ]; export { diff --git a/datavines-ui/src/utils/base64.ts b/datavines-ui/src/utils/base64.ts new file mode 100644 index 000000000..b63d26ef0 --- /dev/null +++ b/datavines-ui/src/utils/base64.ts @@ -0,0 +1,11 @@ +// @ts-ignore +import { decode } from "base-64"; + +export function base64Decode(encodedString: string): string | null { + try { + return decode(encodedString); + } catch (error) { + console.error('Base64 decoding error:', error); + return null; + } +} \ No newline at end of file diff --git a/datavines-ui/src/view/JobHistory/Instance/index.tsx b/datavines-ui/src/view/JobHistory/Instance/index.tsx new file mode 100644 index 000000000..c87a461dc --- /dev/null +++ b/datavines-ui/src/view/JobHistory/Instance/index.tsx @@ -0,0 +1,253 @@ +import {ColumnsType} from "antd/lib/table"; +import {TJobsInstanceTableData, TJobsInstanceTableItem} from "@/type/JobsInstance"; +import {defaultRender} from "utils/helper"; +import React, {Suspense, useState} from "react"; +import {useIntl} from "react-intl"; +import {SearchForm, Title} from "@/component"; +import {Form, Layout, Menu, Spin, Table} from "antd"; +import {$http} from "@/http"; +import {MenuFoldOutlined, MenuUnfoldOutlined} from "@ant-design/icons"; +import AsideMenu,{ MenuItem } from "component/Menu/MenuAside"; +import {IF, useWatch} from '@/common'; +import {useInstanceResult} from "./useInstanceResult"; +import {useInstanceErrorDataModal} from "./useInstanceErrorDataModal"; +import {useLogger} from "./useLogger"; +import querystring from "querystring"; +import { base64Decode } from '@/utils/base64'; +import ContentLayout from '@/component/ContentLayout'; +import Jobs from "view/Main/HomeDetail/Jobs"; + +const JobHistoryInstance = () => { + const intl = useIntl(); + const form = Form.useForm()[0]; + + const [loading, setLoading] = useState(false); + const { Render: RenderErrorDataModal, show: showErrorDataModal } = useInstanceErrorDataModal({}); + const { Render: RenderResultModal, show: showResultModal } = useInstanceResult({}); + const { Render: RenderLoggerModal, show: showLoggerModal } = useLogger({}); + const [decodedText, setDecodedText] = useState(''); + const [tableData, setTableData] = useState({list: [], total: 0}); + const [pageParams, setPageParams] = useState({ + pageNumber: 1, + pageSize: 10, + }); + + const getData = async (values: any = null) => { + try { + setLoading(true); + let jobId = querystring.parse(base64Decode(window.location.href.split('?')[1] as string) || '').jobId; + const res = (await $http.post('/history/job/execution/page', { + jobId: jobId, + ...pageParams, + ...(values || form.getFieldsValue()), + })) || []; + console.log(res) + setTableData({ + list: res?.records || [], + total: res.total || 0, + }); + } catch (error) { + console.log(error) + } finally { + setLoading(false); + } + }; + + useWatch([pageParams], () => { + getData(); + }, {immediate: true}); + + const onSearch = (_values: any) => { + setPageParams({...pageParams, pageNumber: 1}); + getData(); + }; + const onChange = ({current, pageSize}: any) => { + setPageParams({ + pageNumber: current, + pageSize, + }); + getData(); + }; + + const {Header, Content, Sider} = Layout; + const menus = [{ + path: '/history', + exact: false, + key: '/history', + label: 'history', + menuHide: true + }] as MenuItem[]; + + const [collapsed, setCollapsed] = useState(true); + const onCollapse = (bool: boolean) => { + setCollapsed(bool); + }; + + const columns: ColumnsType = [ + { + title: intl.formatMessage({id: 'jobs_task_name'}), + dataIndex: 'name', + key: 'name', + width: 300, + render: (text: string) => defaultRender(text, 300), + }, + { + title: intl.formatMessage({id: 'jobs_task_schema_name'}), + dataIndex: 'schemaName', + key: 'schemaName', + width: 100, + render: (text: string) => defaultRender(text, 300), + }, + { + title: intl.formatMessage({id: 'jobs_task_table_name'}), + dataIndex: 'tableName', + key: 'tableName', + width: 200, + render: (text: string) => defaultRender(text, 300), + }, + { + title: intl.formatMessage({id: 'jobs_task_column_name'}), + dataIndex: 'columnName', + key: 'columnName', + width: 200, + render: (text: string) => defaultRender(text, 300), + }, + { + title: intl.formatMessage({id: 'jobs_task_metric_type'}), + dataIndex: 'metricType', + key: 'metricType', + width: 200, + render: (text: string) => defaultRender(text, 300), + }, + { + title: intl.formatMessage({id: 'jobs_task_type'}), + dataIndex: 'jobType', + key: 'jobType', + width: 140, + render: (text: string) =>
{text}
, + }, + { + title: intl.formatMessage({id: 'jobs_task_status'}), + dataIndex: 'status', + key: 'status', + width: 140, + render: (text: string) =>
{text}
, + }, + { + title: intl.formatMessage({id: 'jobs_task_check_status'}), + dataIndex: 'checkState', + key: 'checkState', + width: 140, + render: (text: string) =>
{text}
, + }, + { + title: intl.formatMessage({id: 'jobs_task_start_time'}), + dataIndex: 'startTime', + key: 'startTime', + width: 180, + render: (text: string) =>
{text || '--'}
, + }, + { + title: intl.formatMessage({id: 'jobs_task_end_time'}), + dataIndex: 'endTime', + key: 'endTime', + width: 180, + render: (text: string) =>
{text || '--'}
, + }, + { + title: intl.formatMessage({ id: 'common_action' }), + fixed: 'right', + key: 'action', + dataIndex: 'action', + width: 300, + render: (text: string, record: TJobsInstanceTableItem) => ( + <> + { onLog(record); }}>{intl.formatMessage({ id: 'jobs_task_log_btn' })} + { onResult(record); }}>{intl.formatMessage({ id: 'jobs_task_result' })} + { onErrorData(record); }}>{intl.formatMessage({ id: 'jobs_task_error_data' })} + + ), + }, + ]; + const onLog = (record: TJobsInstanceTableItem) => { + showLoggerModal(record); + }; + const onResult = (record: TJobsInstanceTableItem) => { + showResultModal(record); + }; + const onErrorData = (record: TJobsInstanceTableItem) => { + showErrorDataModal(record); + }; + return ( + + + + {collapsed ? : } + + )} + collapsed={collapsed} + onCollapse={onCollapse} + > + + + + + + +
+ + {intl.formatMessage({id: 'job_log_view_log'})} + + +
+ {/* */} + <div> + <div className="dv-flex-between"> + <SearchForm form={form} onSearch={onSearch} + placeholder={intl.formatMessage({id: 'common_search'})}/> + </div> + </div> + <Table<TJobsInstanceTableItem> + size="middle" + loading={loading} + rowKey="id" + columns={columns} + dataSource={tableData.list || []} + onChange={onChange} + // scroll={{ x: 1500 }} + pagination={{ + size: 'small', + total: tableData.total, + showSizeChanger: true, + current: pageParams.pageNumber, + pageSize: pageParams.pageSize, + }} + /> + <RenderLoggerModal/> + <RenderErrorDataModal/> + <RenderResultModal/> + </div> + </div> + </ContentLayout> + </Layout> + </Layout> + </Layout> + ); +} + +export default JobHistoryInstance; \ No newline at end of file diff --git a/datavines-ui/src/view/JobHistory/Instance/useInstanceErrorDataModal/index.tsx b/datavines-ui/src/view/JobHistory/Instance/useInstanceErrorDataModal/index.tsx new file mode 100644 index 000000000..b0ad5ed03 --- /dev/null +++ b/datavines-ui/src/view/JobHistory/Instance/useInstanceErrorDataModal/index.tsx @@ -0,0 +1,109 @@ +/* eslint-disable react/no-danger */ +import React, { useRef, useState } from 'react'; +import { ModalProps, Table } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import { + useModal, useImmutable, usePersistFn, useWatch, +} from 'src/common'; +import { useIntl } from 'react-intl'; +import { $http } from '@/http'; +import { defaultRender } from '@/utils/helper'; + +type InnerProps = { + [key: string]: any +} +type tableItem = { + [key: string]: any; +} +const Inner = (props: InnerProps) => { + const [loading, setLoading] = useState(false); + const [tableData, setTableData] = useState<{ list: tableItem[], total: number}>({ list: [], total: 0 }); + const [pageParams, setPageParams] = useState({ + pageNumber: 1, + pageSize: 10, + }); + const [columns, setColumns] = useState<ColumnsType<tableItem>>([]); + const getData = async () => { + try { + setLoading(true); + const res = (await $http.get('/history/job/execution/errorDataPage', { + taskId: props.record.id, + ...pageParams, + })) || []; + setColumns((res.columns || []).map((item: any) => ({ + title: item.name, + dataIndex: item.name, + key: 'name', + width: 180, + render: (text: any) => defaultRender(text, 180), + }))); + setTableData({ + list: res?.resultList || [], + total: res.totalCount || 0, + }); + } catch (error) { + } finally { + setLoading(false); + } + }; + useWatch([pageParams], async () => { + getData(); + }, { immediate: true }); + const onChange = ({ current, pageSize }: any) => { + setPageParams({ + pageNumber: current, + pageSize, + }); + }; + return ( + <div> + <Table<tableItem> + size="middle" + loading={loading} + rowKey="id" + columns={columns} + dataSource={tableData.list || []} + onChange={onChange} + scroll={{ + x: (columns.length) * 120, + }} + pagination={{ + size: 'small', + total: tableData.total, + showSizeChanger: true, + current: pageParams.pageNumber, + pageSize: pageParams.pageSize, + }} + /> + </div> + ); +}; + +export const useInstanceErrorDataModal = (options: ModalProps) => { + const intl = useIntl(); + const recordRef = useRef<any>(); + const onOk = usePersistFn(() => { + hide(); + }); + const { + Render, hide, show, ...rest + } = useModal<any>({ + title: `${intl.formatMessage({ id: 'jobs_task_error_data_view' })}`, + className: 'dv-modal-fullscreen', + footer: null, + width: '90%', + ...(options || {}), + afterClose() { + recordRef.current = null; + }, + onOk, + }); + return { + Render: useImmutable(() => (<Render><Inner record={recordRef.current} /></Render>)), + show(record: any) { + recordRef.current = record; + show(record); + }, + ...rest, + }; +}; diff --git a/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.old.tsx b/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.old.tsx new file mode 100644 index 000000000..56f1a2cbe --- /dev/null +++ b/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.old.tsx @@ -0,0 +1,142 @@ +/* eslint-disable react/no-danger */ +import React, { useRef, useState } from 'react'; +import { ModalProps, Row, Col } from 'antd'; +import { + useModal, useImmutable, useMount, +} from 'src/common'; +import { useIntl } from 'react-intl'; +import { $http } from '@/http'; +import { useSelector } from '@/store'; + +type ResultProps = { + checkResult?: string, + checkSubject?: string, + expectedType?: string, + metricName?: string, + resultFormulaFormat?: string, + metricParameter?: Record<string, any> +} +const Inner = (props: any) => { + const { locale } = useSelector((r) => r.commonReducer); + const [result, setResult] = useState<ResultProps>({}); + const intl = useIntl(); + const getIntl = (id: any) => intl.formatMessage({ id }); + const getData = async () => { + try { + const res = (await $http.get<ResultProps>(`history/job/execution/result/${props.record.id}`)) || {}; + setResult(res); + } catch (error) { + console.log(error); + } finally { + } + }; + + const getItem = (key: string, value: any) => ( + <Row style={{ marginBottom: key === 'jobs_task_check_formula' ? 30 : 10 }}> + <Col span={locale === 'zh_CN' ? 4 : 7} style={{ textAlign: 'right' }}> + {getIntl(key)} + : + </Col> + <Col span={locale === 'zh_CN' ? 20 : 17}>{value}</Col> + </Row> + ); + const getParams = () => { + const metricParameter = result.metricParameter || {}; + if (Object.keys(metricParameter).length <= 0) { + return null; + } + return ( + <> + <Row style={{ + marginBottom: 10, + }} + > + <Col span={locale === 'zh_CN' ? 4 : 7} style={{ textAlign: 'right' }}> + {getIntl('jobs_task_check_params')} + : + </Col> + <Col + span={locale === 'zh_CN' ? 20 : 17} + > + <div style={{ height: 22 }}>{' '}</div> + { + Object.keys(metricParameter).map((item) => ( + <div> + <span style={{ marginRight: 5 }}>-</span> + {item} + <span style={{ marginRight: 2 }}>:</span> + {metricParameter[item]} + </div> + )) + } + </Col> + </Row> + </> + ); + }; + useMount(() => { + getData(); + }); + return ( + <div style={{ + fontSize: 14, + minHeight: 260, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + }} + > + { + Object.keys(result).length > 0 ? ( + <> + {getItem('jobs_task_check_subject', result.checkSubject)} + {getItem('jobs_task_check_rule', result.metricName)} + {getParams()} + {getItem('jobs_task_check_result', result.checkResult)} + {getItem('jobs_task_check_expectVal_type', result.expectedType)} + {getItem('jobs_task_check_formula', result.resultFormulaFormat)} + </> + ) : ( + <span style={{ + textAlign: 'center', + }} + > + 暂无数据 + </span> + ) + } + + {/* <Row style={{ marginBottom: 10, fontWeight: 500 }}> + <Col span={locale === 'zh_CN' ? 4 : 7} style={{ textAlign: 'right' }}> + {getIntl('jobs_task_check_explain')} + : + </Col> + <Col span={locale === 'zh_CN' ? 20 : 17}>{getIntl('jobs_task_check_explain_text')}</Col> + </Row> */} + </div> + ); +}; + +export const useInstanceResult = (options: ModalProps) => { + const intl = useIntl(); + const recordRef = useRef<any>(); + const { + Render, show, ...rest + } = useModal<any>({ + title: `${intl.formatMessage({ id: 'jobs_task_check_result' })}`, + footer: null, + width: '600px', + ...(options || {}), + afterClose() { + recordRef.current = null; + }, + }); + return { + Render: useImmutable(() => (<Render><Inner record={recordRef.current} /></Render>)), + show(record: any) { + recordRef.current = record; + show(record); + }, + ...rest, + }; +}; diff --git a/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.tsx b/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.tsx new file mode 100644 index 000000000..da38b80a6 --- /dev/null +++ b/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.tsx @@ -0,0 +1,118 @@ +/* eslint-disable react/no-danger */ +import React, { useRef, useState } from 'react'; +import { + ModalProps, Table, +} from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import { + useModal, useImmutable, useMount, +} from 'src/common'; +import { useIntl } from 'react-intl'; +import { $http } from '@/http'; +import { defaultRender } from '@/utils/helper'; + +type ResultItem = { + checkSubject: string; + metricName: string; + metricParameter: string; + checkResult: string; + expectedType: string; + resultFormulaFormat: string; +} + +type TResultItemData = { + list: ResultItem[]; + total: number; +} + +const Inner = (props: any) => { + const [loading, setLoading] = useState(false); + const [tableData, setTableData] = useState<TResultItemData>({ list: [], total: 0 }); + const intl = useIntl(); + const getData = async () => { + try { + setLoading(true); + const res = (await $http.get<any>(`history/job/execution/list/result/${props.record.id}`)) || {}; + setTableData({ list: res || [], total: (res || []).length }); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + }; + const columns: ColumnsType<ResultItem> = [ + { + title: intl.formatMessage({ id: 'jobs_task_check_subject' }), + dataIndex: 'checkSubject', + key: 'checkSubject', + width: 260, + render: (text: any) => defaultRender(text, 200), + }, + { + title: intl.formatMessage({ id: 'jobs_task_check_rule' }), + dataIndex: 'metricName', + key: 'metricName', + width: 180, + render: (text: any) => defaultRender(text, 200), + }, + { + title: intl.formatMessage({ id: 'jobs_task_check_result' }), + dataIndex: 'checkResult', + key: 'checkResult', + width: 120, + render: (text: any) => defaultRender(text, 200), + }, + { + title: intl.formatMessage({ id: 'jobs_task_check_expectVal_type' }), + dataIndex: 'expectedType', + key: 'expectedType', + width: 180, + render: (text: any) => defaultRender(text, 200), + }, + { + title: intl.formatMessage({ id: 'jobs_task_check_formula' }), + dataIndex: 'resultFormulaFormat', + key: 'resultFormulaFormat', + width: 200, + render: (text: any) => defaultRender(text, 200), + }, + ]; + + useMount(getData); + return ( + <div> + <Table<ResultItem> + loading={loading} + size="middle" + bordered + columns={columns} + dataSource={tableData.list || []} + pagination={false} + /> + </div> + ); +}; + +export const useInstanceResult = (options: ModalProps) => { + const intl = useIntl(); + const recordRef = useRef<any>(); + const { + Render, show, ...rest + } = useModal<any>({ + title: `${intl.formatMessage({ id: 'jobs_task_check_result' })}`, + footer: null, + width: '1000px', + ...(options || {}), + afterClose() { + recordRef.current = null; + }, + }); + return { + Render: useImmutable(() => (<Render><Inner record={recordRef.current} /></Render>)), + show(record: any) { + recordRef.current = record; + show(record); + }, + ...rest, + }; +}; diff --git a/datavines-ui/src/view/JobHistory/Instance/useLogger/index.tsx b/datavines-ui/src/view/JobHistory/Instance/useLogger/index.tsx new file mode 100644 index 000000000..6b1e304cf --- /dev/null +++ b/datavines-ui/src/view/JobHistory/Instance/useLogger/index.tsx @@ -0,0 +1,123 @@ +/* eslint-disable react/no-danger */ +import React, { useRef, useState, useImperativeHandle } from 'react'; +import { ModalProps, Spin } from 'antd'; +import { FullscreenExitOutlined, DownloadOutlined, SyncOutlined } from '@ant-design/icons'; +import { + useModal, useContextModal, useImmutable, usePersistFn, useMount, IF, +} from 'src/common'; +import { useIntl } from 'react-intl'; +import { $http } from '@/http'; +import { download } from '@/utils'; + +type InnerProps = { + innerRef: any +} + +const dealMsg = (msg: string) => { + if (msg) { + return msg.replace(/\r\n/g, '<br>'); + } + return ''; +}; +const Inner = ({ innerRef }: InnerProps) => { + const [loading, setLoading] = useState(false); + const { data } = useContextModal(); + const [wholeLog, setWholeLog] = useState<{offsetLine: number, msg: string}[]>([]); + const getData = async (offsetLine: number) => { + try { + setLoading(true); + const res = (await $http.get('history/job/execution/queryLogWithOffsetLine', { + taskId: data.id, + offsetLine, + })) || []; + res.msg = dealMsg(res.msg); + if (offsetLine === 0) { + setWholeLog([res]); + } else { + setWholeLog([...wholeLog, res]); + } + } catch (error) { + } finally { + setLoading(false); + } + }; + useMount(async () => { + getData(0); + }); + useImperativeHandle(innerRef, () => ({ + onRefresh() { + getData(wholeLog[wholeLog.length - 1]?.offsetLine || 0); + }, + })); + return ( + <Spin spinning={loading}> + <div style={{ minHeight: 300 }}> + { + wholeLog.map((item) => ( + <div dangerouslySetInnerHTML={{ __html: item.msg }} /> + )) + } + <div /> + </div> + </Spin> + ); +}; + +export const useLogger = (options: ModalProps) => { + const intl = useIntl(); + const innerRef = useRef<any>(); + const recordRef = useRef<any>(); + const onOk = usePersistFn(() => { + hide(); + }); + const onDownload = usePersistFn(async () => { + try { + const blob = await $http.get('history/job/execution/download', { taskId: recordRef.current?.id }, { + responseType: 'blob', + }); + download(blob); + } catch (error) { + } + }); + const { + Render, hide, show, ...rest + } = useModal<any>({ + title: ( + <div className="dv-flex-between"> + <span>{intl.formatMessage({ id: 'job_log_view_log' })}</span> + <div style={{ marginRight: 30 }}> + <a + style={{ marginRight: 10 }} + onClick={() => { + innerRef.current.onRefresh(); + }} + > + <SyncOutlined style={{ marginRight: 5 }} /> + {intl.formatMessage({ id: 'job_log_refresh' })} + </a> + <a style={{ marginRight: 10 }} onClick={onDownload}> + <DownloadOutlined style={{ marginRight: 5 }} /> + {intl.formatMessage({ id: 'job_log_download' })} + </a> + + </div> + </div> + ), + className: 'dv-modal-fullscreen', + footer: null, + width: '90%', + ...(options || {}), + afterClose() { + recordRef.current = null; + }, + onOk, + }); + return { + Render: useImmutable(() => (<Render><Inner innerRef={innerRef} /></Render>)), + show(record: any) { + recordRef.current = record; + show(record); + }, + ...rest, + }; +}; From d2d86e4eb48c48fc0477d9e9d212d9294d735487 Mon Sep 17 00:00:00 2001 From: 735140144 <735140144@qq.com> Date: Thu, 29 Aug 2024 17:39:00 +0800 Subject: [PATCH 2/5] [Feature][Server]Improve sla notification click url in email and wecombot --- .../datavines/notification/plugin/email/EMailSender.java | 8 +++++++- .../notification/plugin/email/EmailConstants.java | 2 ++ .../notification/plugin/wecombot/WecomBotSender.java | 8 +++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-email/src/main/java/io/datavines/notification/plugin/email/EMailSender.java b/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-email/src/main/java/io/datavines/notification/plugin/email/EMailSender.java index 4c43c55b1..0b79273f7 100644 --- a/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-email/src/main/java/io/datavines/notification/plugin/email/EMailSender.java +++ b/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-email/src/main/java/io/datavines/notification/plugin/email/EMailSender.java @@ -177,8 +177,14 @@ private String getTextTypeMessage(String content) { ArrayNode list = JSONUtils.parseArray(content); StringBuilder contents = new StringBuilder(100); for (JsonNode jsonNode : list) { + String nodeMessage = jsonNode.toString().replace("\"", ""); contents.append(EmailConstants.TR); - contents.append(EmailConstants.TD).append(jsonNode.toString().replace("\"", "")).append(EmailConstants.TD_END); + if (nodeMessage.startsWith("Task Execution Record")||nodeMessage.startsWith("任务执行记录")){ + String formatMessage = String.format("%s : <a href=\"%s\">%s</a>", nodeMessage.substring(0,nodeMessage.indexOf(" : ")),nodeMessage.substring(nodeMessage.indexOf(":")+2),nodeMessage.substring(nodeMessage.indexOf(":")+2)); + contents.append(EmailConstants.TD).append(formatMessage).append(EmailConstants.TD_END); + }else { + contents.append(EmailConstants.TD).append(jsonNode.toString().replace("\"", "")).append(EmailConstants.TD_END); + } contents.append(EmailConstants.TR_END); } return EmailConstants.HTML_HEADER_PREFIX + contents.toString() + EmailConstants.TABLE_HTML_TAIL + EmailConstants.BODY_HTML_TAIL; diff --git a/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-email/src/main/java/io/datavines/notification/plugin/email/EmailConstants.java b/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-email/src/main/java/io/datavines/notification/plugin/email/EmailConstants.java index c122e35fa..a9e232cb7 100644 --- a/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-email/src/main/java/io/datavines/notification/plugin/email/EmailConstants.java +++ b/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-email/src/main/java/io/datavines/notification/plugin/email/EmailConstants.java @@ -73,4 +73,6 @@ private EmailConstants() { public static final String EXCEL_SUFFIX_XLSX = ".xlsx"; public static final String SINGLE_SLASH = "/"; + + public static final String URL="<a href=\"mailto:"; } diff --git a/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-wecombot/src/main/java/io/datavines/notification/plugin/wecombot/WecomBotSender.java b/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-wecombot/src/main/java/io/datavines/notification/plugin/wecombot/WecomBotSender.java index 234185d72..391090d09 100644 --- a/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-wecombot/src/main/java/io/datavines/notification/plugin/wecombot/WecomBotSender.java +++ b/datavines-notification/datavines-notification-plugins/datavines-notification-plugin-wecombot/src/main/java/io/datavines/notification/plugin/wecombot/WecomBotSender.java @@ -91,7 +91,13 @@ private String getMarkdownMessage(String subject, String content) { if (StringUtils.isNotEmpty(content)) { ArrayNode list = JSONUtils.parseArray(content); for (JsonNode jsonNode : list) { - contents.append(WecomBotConstants.QUOTE_START).append(jsonNode.toString().replace("\"", "")).append(WecomBotConstants.END); + String nodeMessage = jsonNode.toString().replace("\"", ""); + if (nodeMessage.startsWith("Task Execution Record")||nodeMessage.startsWith("任务执行记录")){ + String formatMessage = String.format("%s : [%s](%s)", nodeMessage.substring(0,nodeMessage.indexOf(" : ")),nodeMessage.substring(nodeMessage.indexOf(":")+2),nodeMessage.substring(nodeMessage.indexOf(":")+2)); + contents.append(WecomBotConstants.QUOTE_START).append(formatMessage).append(WecomBotConstants.END); + }else { + contents.append(WecomBotConstants.QUOTE_START).append(nodeMessage).append(WecomBotConstants.END); + } } } return contents.toString(); From 8a691b8675cc227114d5da5ad659a66d4e55d10a Mon Sep 17 00:00:00 2001 From: 735140144 <735140144@qq.com> Date: Tue, 3 Sep 2024 14:47:50 +0800 Subject: [PATCH 3/5] [Feature][Server]Improve sla notification click url in email and wecombot [Feature][Server][UI]Improve Notification with quick link without check Token --- .../JobHistoryExecutionController.java | 36 --- .../validator/JobResultValidator.java | 2 +- datavines-ui/src/router/index.tsx | 2 +- .../src/view/JobHistory/Instance/index.tsx | 253 ------------------ .../useInstanceErrorDataModal/index.tsx | 109 -------- .../Instance/useInstanceResult/index.old.tsx | 142 ---------- .../Instance/useInstanceResult/index.tsx | 118 -------- .../JobHistory/Instance/useLogger/index.tsx | 123 --------- 8 files changed, 2 insertions(+), 783 deletions(-) delete mode 100644 datavines-ui/src/view/JobHistory/Instance/index.tsx delete mode 100644 datavines-ui/src/view/JobHistory/Instance/useInstanceErrorDataModal/index.tsx delete mode 100644 datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.old.tsx delete mode 100644 datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.tsx delete mode 100644 datavines-ui/src/view/JobHistory/Instance/useLogger/index.tsx diff --git a/datavines-server/src/main/java/io/datavines/server/api/controller/JobHistoryExecutionController.java b/datavines-server/src/main/java/io/datavines/server/api/controller/JobHistoryExecutionController.java index 60bdfb865..64263edfe 100644 --- a/datavines-server/src/main/java/io/datavines/server/api/controller/JobHistoryExecutionController.java +++ b/datavines-server/src/main/java/io/datavines/server/api/controller/JobHistoryExecutionController.java @@ -21,12 +21,8 @@ import io.datavines.core.enums.Status; import io.datavines.core.exception.DataVinesServerException; import io.datavines.server.api.annotation.AuthIgnore; -import io.datavines.server.api.dto.bo.job.JobExecutionPageParam; -import io.datavines.server.api.dto.vo.JobExecutionResultVO; import io.datavines.server.dqc.coordinator.log.LogService; import io.datavines.server.repository.entity.JobExecution; -import io.datavines.server.repository.service.JobExecutionErrorDataService; -import io.datavines.server.repository.service.JobExecutionResultService; import io.datavines.server.repository.service.JobExecutionService; import io.datavines.server.utils.FileUtils; import io.swagger.annotations.Api; @@ -41,7 +37,6 @@ import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.validation.Valid; import java.io.IOException; import static io.datavines.common.utils.OSUtils.judgeConcurrentHost; @@ -56,40 +51,9 @@ public class JobHistoryExecutionController { @Autowired private JobExecutionService jobExecutionService; - @Autowired - private JobExecutionResultService jobExecutionResultService; - - @Autowired - private JobExecutionErrorDataService jobExecutionErrorDataService; - @Resource private LogService logService; - @AuthIgnore - @ApiOperation(value = "get job execution page", response = JobExecutionResultVO.class, responseContainer = "page") - @PostMapping(value = "/page") - public Object page(@Valid @RequestBody JobExecutionPageParam jobExecutionPageParam) { - if(jobExecutionPageParam.getJobId()==null){ - return ResponseEntity.ok(new ResultMap().fail()); - } - return ResponseEntity.ok(new ResultMap().success().payload(jobExecutionService.getJobExecutionPage(jobExecutionPageParam))); - } - - @AuthIgnore - @ApiOperation(value = "get job execution result", response = JobExecutionResultVO.class) - @GetMapping(value = "/list/result/{executionId}") - public Object getJobExecutionResultInfoList(@PathVariable("executionId") Long executionId) { - return ResponseEntity.ok(new ResultMap().success().payload(jobExecutionResultService.getResultVOListByJobExecutionId(executionId))); - } - - @AuthIgnore - @ApiOperation(value = "get job execution error data page", response = Object.class, responseContainer = "page") - @GetMapping(value = "/errorDataPage") - public Object readErrorDataPage(@RequestParam("taskId") Long taskId, - @RequestParam("pageNumber") Integer pageNumber, - @RequestParam("pageSize") Integer pageSize){ - return ResponseEntity.ok(new ResultMap().success().payload(jobExecutionErrorDataService.readErrorDataPage(taskId, pageNumber, pageSize))); - } @AuthIgnore @ApiOperation(value = "queryLogWithOffsetLine", notes = "query task log with offsetLine") diff --git a/datavines-server/src/main/java/io/datavines/server/dqc/coordinator/validator/JobResultValidator.java b/datavines-server/src/main/java/io/datavines/server/dqc/coordinator/validator/JobResultValidator.java index a20bb2c14..5dc50ba39 100644 --- a/datavines-server/src/main/java/io/datavines/server/dqc/coordinator/validator/JobResultValidator.java +++ b/datavines-server/src/main/java/io/datavines/server/dqc/coordinator/validator/JobResultValidator.java @@ -143,7 +143,7 @@ private void sendErrorEmail(Long jobExecutionId) { dataSourceName = dataSource.getName(); dataSourceType = dataSource.getType(); if (!CommonPropertyUtils.DATAVINES_FQDN_DEFAULT.equals(CommonPropertyUtils.getString(CommonPropertyUtils.DATAVINES_FQDN))) { - fqdn = CommonPropertyUtils.getString(CommonPropertyUtils.DATAVINES_FQDN) + String.format("/#/history?%s", Base64.getEncoder().encodeToString(String.format("jobId=%s&ds=%s",jobId,dataSourceName).getBytes(StandardCharsets.UTF_8))); + fqdn = CommonPropertyUtils.getString(CommonPropertyUtils.DATAVINES_FQDN) + String.format("/#/history?%s", Base64.getEncoder().encodeToString(String.format("jobId=%s&executionId=%s",jobId,jobExecutionId).getBytes(StandardCharsets.UTF_8))); } } diff --git a/datavines-ui/src/router/index.tsx b/datavines-ui/src/router/index.tsx index 50eeb5046..43d3eba2d 100644 --- a/datavines-ui/src/router/index.tsx +++ b/datavines-ui/src/router/index.tsx @@ -18,7 +18,7 @@ export const routerNoLogin: TRouterItem[] = [ }, { path: '/history', - component: lazy(()=>import('@/view/JobHistory/Instance')) + component: lazy(()=>import('@/view/JobHistory')) } ]; diff --git a/datavines-ui/src/view/JobHistory/Instance/index.tsx b/datavines-ui/src/view/JobHistory/Instance/index.tsx deleted file mode 100644 index c87a461dc..000000000 --- a/datavines-ui/src/view/JobHistory/Instance/index.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import {ColumnsType} from "antd/lib/table"; -import {TJobsInstanceTableData, TJobsInstanceTableItem} from "@/type/JobsInstance"; -import {defaultRender} from "utils/helper"; -import React, {Suspense, useState} from "react"; -import {useIntl} from "react-intl"; -import {SearchForm, Title} from "@/component"; -import {Form, Layout, Menu, Spin, Table} from "antd"; -import {$http} from "@/http"; -import {MenuFoldOutlined, MenuUnfoldOutlined} from "@ant-design/icons"; -import AsideMenu,{ MenuItem } from "component/Menu/MenuAside"; -import {IF, useWatch} from '@/common'; -import {useInstanceResult} from "./useInstanceResult"; -import {useInstanceErrorDataModal} from "./useInstanceErrorDataModal"; -import {useLogger} from "./useLogger"; -import querystring from "querystring"; -import { base64Decode } from '@/utils/base64'; -import ContentLayout from '@/component/ContentLayout'; -import Jobs from "view/Main/HomeDetail/Jobs"; - -const JobHistoryInstance = () => { - const intl = useIntl(); - const form = Form.useForm()[0]; - - const [loading, setLoading] = useState(false); - const { Render: RenderErrorDataModal, show: showErrorDataModal } = useInstanceErrorDataModal({}); - const { Render: RenderResultModal, show: showResultModal } = useInstanceResult({}); - const { Render: RenderLoggerModal, show: showLoggerModal } = useLogger({}); - const [decodedText, setDecodedText] = useState(''); - const [tableData, setTableData] = useState<TJobsInstanceTableData>({list: [], total: 0}); - const [pageParams, setPageParams] = useState({ - pageNumber: 1, - pageSize: 10, - }); - - const getData = async (values: any = null) => { - try { - setLoading(true); - let jobId = querystring.parse(base64Decode(window.location.href.split('?')[1] as string) || '').jobId; - const res = (await $http.post('/history/job/execution/page', { - jobId: jobId, - ...pageParams, - ...(values || form.getFieldsValue()), - })) || []; - console.log(res) - setTableData({ - list: res?.records || [], - total: res.total || 0, - }); - } catch (error) { - console.log(error) - } finally { - setLoading(false); - } - }; - - useWatch([pageParams], () => { - getData(); - }, {immediate: true}); - - const onSearch = (_values: any) => { - setPageParams({...pageParams, pageNumber: 1}); - getData(); - }; - const onChange = ({current, pageSize}: any) => { - setPageParams({ - pageNumber: current, - pageSize, - }); - getData(); - }; - - const {Header, Content, Sider} = Layout; - const menus = [{ - path: '/history', - exact: false, - key: '/history', - label: 'history', - menuHide: true - }] as MenuItem[]; - - const [collapsed, setCollapsed] = useState(true); - const onCollapse = (bool: boolean) => { - setCollapsed(bool); - }; - - const columns: ColumnsType<TJobsInstanceTableItem> = [ - { - title: intl.formatMessage({id: 'jobs_task_name'}), - dataIndex: 'name', - key: 'name', - width: 300, - render: (text: string) => defaultRender(text, 300), - }, - { - title: intl.formatMessage({id: 'jobs_task_schema_name'}), - dataIndex: 'schemaName', - key: 'schemaName', - width: 100, - render: (text: string) => defaultRender(text, 300), - }, - { - title: intl.formatMessage({id: 'jobs_task_table_name'}), - dataIndex: 'tableName', - key: 'tableName', - width: 200, - render: (text: string) => defaultRender(text, 300), - }, - { - title: intl.formatMessage({id: 'jobs_task_column_name'}), - dataIndex: 'columnName', - key: 'columnName', - width: 200, - render: (text: string) => defaultRender(text, 300), - }, - { - title: intl.formatMessage({id: 'jobs_task_metric_type'}), - dataIndex: 'metricType', - key: 'metricType', - width: 200, - render: (text: string) => defaultRender(text, 300), - }, - { - title: intl.formatMessage({id: 'jobs_task_type'}), - dataIndex: 'jobType', - key: 'jobType', - width: 140, - render: (text: string) => <div>{text}</div>, - }, - { - title: intl.formatMessage({id: 'jobs_task_status'}), - dataIndex: 'status', - key: 'status', - width: 140, - render: (text: string) => <div>{text}</div>, - }, - { - title: intl.formatMessage({id: 'jobs_task_check_status'}), - dataIndex: 'checkState', - key: 'checkState', - width: 140, - render: (text: string) => <div>{text}</div>, - }, - { - title: intl.formatMessage({id: 'jobs_task_start_time'}), - dataIndex: 'startTime', - key: 'startTime', - width: 180, - render: (text: string) => <div>{text || '--'}</div>, - }, - { - title: intl.formatMessage({id: 'jobs_task_end_time'}), - dataIndex: 'endTime', - key: 'endTime', - width: 180, - render: (text: string) => <div>{text || '--'}</div>, - }, - { - title: intl.formatMessage({ id: 'common_action' }), - fixed: 'right', - key: 'action', - dataIndex: 'action', - width: 300, - render: (text: string, record: TJobsInstanceTableItem) => ( - <> - <a style={{ marginRight: 5 }} onClick={() => { onLog(record); }}>{intl.formatMessage({ id: 'jobs_task_log_btn' })}</a> - <a style={{ marginRight: 5 }} onClick={() => { onResult(record); }}>{intl.formatMessage({ id: 'jobs_task_result' })}</a> - <a style={{ marginRight: 5 }} onClick={() => { onErrorData(record); }}>{intl.formatMessage({ id: 'jobs_task_error_data' })}</a> - </> - ), - }, - ]; - const onLog = (record: TJobsInstanceTableItem) => { - showLoggerModal(record); - }; - const onResult = (record: TJobsInstanceTableItem) => { - showResultModal(record); - }; - const onErrorData = (record: TJobsInstanceTableItem) => { - showErrorDataModal(record); - }; - return ( - <Layout> - <Layout> - <Sider - style={{ - height: '100vh', overflow: 'auto', backgroundColor: '#fff', - }} - trigger={( - <div style={{position: 'absolute', right: 15, fontSize: 16}}> - {collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>} - </div> - )} - collapsed={collapsed} - onCollapse={onCollapse} - > - <AsideMenu - menus={menus} - /> - </Sider> - - <Layout> - <ContentLayout> - - <div style={{ - height: '100%', - }} - > - <Title> - {intl.formatMessage({id: 'job_log_view_log'})} - - -
- {/* */} - <div> - <div className="dv-flex-between"> - <SearchForm form={form} onSearch={onSearch} - placeholder={intl.formatMessage({id: 'common_search'})}/> - </div> - </div> - <Table<TJobsInstanceTableItem> - size="middle" - loading={loading} - rowKey="id" - columns={columns} - dataSource={tableData.list || []} - onChange={onChange} - // scroll={{ x: 1500 }} - pagination={{ - size: 'small', - total: tableData.total, - showSizeChanger: true, - current: pageParams.pageNumber, - pageSize: pageParams.pageSize, - }} - /> - <RenderLoggerModal/> - <RenderErrorDataModal/> - <RenderResultModal/> - </div> - </div> - </ContentLayout> - </Layout> - </Layout> - </Layout> - ); -} - -export default JobHistoryInstance; \ No newline at end of file diff --git a/datavines-ui/src/view/JobHistory/Instance/useInstanceErrorDataModal/index.tsx b/datavines-ui/src/view/JobHistory/Instance/useInstanceErrorDataModal/index.tsx deleted file mode 100644 index b0ad5ed03..000000000 --- a/datavines-ui/src/view/JobHistory/Instance/useInstanceErrorDataModal/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable react/no-danger */ -import React, { useRef, useState } from 'react'; -import { ModalProps, Table } from 'antd'; -import { ColumnsType } from 'antd/lib/table'; -import { - useModal, useImmutable, usePersistFn, useWatch, -} from 'src/common'; -import { useIntl } from 'react-intl'; -import { $http } from '@/http'; -import { defaultRender } from '@/utils/helper'; - -type InnerProps = { - [key: string]: any -} -type tableItem = { - [key: string]: any; -} -const Inner = (props: InnerProps) => { - const [loading, setLoading] = useState(false); - const [tableData, setTableData] = useState<{ list: tableItem[], total: number}>({ list: [], total: 0 }); - const [pageParams, setPageParams] = useState({ - pageNumber: 1, - pageSize: 10, - }); - const [columns, setColumns] = useState<ColumnsType<tableItem>>([]); - const getData = async () => { - try { - setLoading(true); - const res = (await $http.get('/history/job/execution/errorDataPage', { - taskId: props.record.id, - ...pageParams, - })) || []; - setColumns((res.columns || []).map((item: any) => ({ - title: item.name, - dataIndex: item.name, - key: 'name', - width: 180, - render: (text: any) => defaultRender(text, 180), - }))); - setTableData({ - list: res?.resultList || [], - total: res.totalCount || 0, - }); - } catch (error) { - } finally { - setLoading(false); - } - }; - useWatch([pageParams], async () => { - getData(); - }, { immediate: true }); - const onChange = ({ current, pageSize }: any) => { - setPageParams({ - pageNumber: current, - pageSize, - }); - }; - return ( - <div> - <Table<tableItem> - size="middle" - loading={loading} - rowKey="id" - columns={columns} - dataSource={tableData.list || []} - onChange={onChange} - scroll={{ - x: (columns.length) * 120, - }} - pagination={{ - size: 'small', - total: tableData.total, - showSizeChanger: true, - current: pageParams.pageNumber, - pageSize: pageParams.pageSize, - }} - /> - </div> - ); -}; - -export const useInstanceErrorDataModal = (options: ModalProps) => { - const intl = useIntl(); - const recordRef = useRef<any>(); - const onOk = usePersistFn(() => { - hide(); - }); - const { - Render, hide, show, ...rest - } = useModal<any>({ - title: `${intl.formatMessage({ id: 'jobs_task_error_data_view' })}`, - className: 'dv-modal-fullscreen', - footer: null, - width: '90%', - ...(options || {}), - afterClose() { - recordRef.current = null; - }, - onOk, - }); - return { - Render: useImmutable(() => (<Render><Inner record={recordRef.current} /></Render>)), - show(record: any) { - recordRef.current = record; - show(record); - }, - ...rest, - }; -}; diff --git a/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.old.tsx b/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.old.tsx deleted file mode 100644 index 56f1a2cbe..000000000 --- a/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.old.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* eslint-disable react/no-danger */ -import React, { useRef, useState } from 'react'; -import { ModalProps, Row, Col } from 'antd'; -import { - useModal, useImmutable, useMount, -} from 'src/common'; -import { useIntl } from 'react-intl'; -import { $http } from '@/http'; -import { useSelector } from '@/store'; - -type ResultProps = { - checkResult?: string, - checkSubject?: string, - expectedType?: string, - metricName?: string, - resultFormulaFormat?: string, - metricParameter?: Record<string, any> -} -const Inner = (props: any) => { - const { locale } = useSelector((r) => r.commonReducer); - const [result, setResult] = useState<ResultProps>({}); - const intl = useIntl(); - const getIntl = (id: any) => intl.formatMessage({ id }); - const getData = async () => { - try { - const res = (await $http.get<ResultProps>(`history/job/execution/result/${props.record.id}`)) || {}; - setResult(res); - } catch (error) { - console.log(error); - } finally { - } - }; - - const getItem = (key: string, value: any) => ( - <Row style={{ marginBottom: key === 'jobs_task_check_formula' ? 30 : 10 }}> - <Col span={locale === 'zh_CN' ? 4 : 7} style={{ textAlign: 'right' }}> - {getIntl(key)} - : - </Col> - <Col span={locale === 'zh_CN' ? 20 : 17}>{value}</Col> - </Row> - ); - const getParams = () => { - const metricParameter = result.metricParameter || {}; - if (Object.keys(metricParameter).length <= 0) { - return null; - } - return ( - <> - <Row style={{ - marginBottom: 10, - }} - > - <Col span={locale === 'zh_CN' ? 4 : 7} style={{ textAlign: 'right' }}> - {getIntl('jobs_task_check_params')} - : - </Col> - <Col - span={locale === 'zh_CN' ? 20 : 17} - > - <div style={{ height: 22 }}>{' '}</div> - { - Object.keys(metricParameter).map((item) => ( - <div> - <span style={{ marginRight: 5 }}>-</span> - {item} - <span style={{ marginRight: 2 }}>:</span> - {metricParameter[item]} - </div> - )) - } - </Col> - </Row> - </> - ); - }; - useMount(() => { - getData(); - }); - return ( - <div style={{ - fontSize: 14, - minHeight: 260, - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - }} - > - { - Object.keys(result).length > 0 ? ( - <> - {getItem('jobs_task_check_subject', result.checkSubject)} - {getItem('jobs_task_check_rule', result.metricName)} - {getParams()} - {getItem('jobs_task_check_result', result.checkResult)} - {getItem('jobs_task_check_expectVal_type', result.expectedType)} - {getItem('jobs_task_check_formula', result.resultFormulaFormat)} - </> - ) : ( - <span style={{ - textAlign: 'center', - }} - > - 暂无数据 - </span> - ) - } - - {/* <Row style={{ marginBottom: 10, fontWeight: 500 }}> - <Col span={locale === 'zh_CN' ? 4 : 7} style={{ textAlign: 'right' }}> - {getIntl('jobs_task_check_explain')} - : - </Col> - <Col span={locale === 'zh_CN' ? 20 : 17}>{getIntl('jobs_task_check_explain_text')}</Col> - </Row> */} - </div> - ); -}; - -export const useInstanceResult = (options: ModalProps) => { - const intl = useIntl(); - const recordRef = useRef<any>(); - const { - Render, show, ...rest - } = useModal<any>({ - title: `${intl.formatMessage({ id: 'jobs_task_check_result' })}`, - footer: null, - width: '600px', - ...(options || {}), - afterClose() { - recordRef.current = null; - }, - }); - return { - Render: useImmutable(() => (<Render><Inner record={recordRef.current} /></Render>)), - show(record: any) { - recordRef.current = record; - show(record); - }, - ...rest, - }; -}; diff --git a/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.tsx b/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.tsx deleted file mode 100644 index da38b80a6..000000000 --- a/datavines-ui/src/view/JobHistory/Instance/useInstanceResult/index.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* eslint-disable react/no-danger */ -import React, { useRef, useState } from 'react'; -import { - ModalProps, Table, -} from 'antd'; -import { ColumnsType } from 'antd/lib/table'; -import { - useModal, useImmutable, useMount, -} from 'src/common'; -import { useIntl } from 'react-intl'; -import { $http } from '@/http'; -import { defaultRender } from '@/utils/helper'; - -type ResultItem = { - checkSubject: string; - metricName: string; - metricParameter: string; - checkResult: string; - expectedType: string; - resultFormulaFormat: string; -} - -type TResultItemData = { - list: ResultItem[]; - total: number; -} - -const Inner = (props: any) => { - const [loading, setLoading] = useState(false); - const [tableData, setTableData] = useState<TResultItemData>({ list: [], total: 0 }); - const intl = useIntl(); - const getData = async () => { - try { - setLoading(true); - const res = (await $http.get<any>(`history/job/execution/list/result/${props.record.id}`)) || {}; - setTableData({ list: res || [], total: (res || []).length }); - } catch (error) { - console.log(error); - } finally { - setLoading(false); - } - }; - const columns: ColumnsType<ResultItem> = [ - { - title: intl.formatMessage({ id: 'jobs_task_check_subject' }), - dataIndex: 'checkSubject', - key: 'checkSubject', - width: 260, - render: (text: any) => defaultRender(text, 200), - }, - { - title: intl.formatMessage({ id: 'jobs_task_check_rule' }), - dataIndex: 'metricName', - key: 'metricName', - width: 180, - render: (text: any) => defaultRender(text, 200), - }, - { - title: intl.formatMessage({ id: 'jobs_task_check_result' }), - dataIndex: 'checkResult', - key: 'checkResult', - width: 120, - render: (text: any) => defaultRender(text, 200), - }, - { - title: intl.formatMessage({ id: 'jobs_task_check_expectVal_type' }), - dataIndex: 'expectedType', - key: 'expectedType', - width: 180, - render: (text: any) => defaultRender(text, 200), - }, - { - title: intl.formatMessage({ id: 'jobs_task_check_formula' }), - dataIndex: 'resultFormulaFormat', - key: 'resultFormulaFormat', - width: 200, - render: (text: any) => defaultRender(text, 200), - }, - ]; - - useMount(getData); - return ( - <div> - <Table<ResultItem> - loading={loading} - size="middle" - bordered - columns={columns} - dataSource={tableData.list || []} - pagination={false} - /> - </div> - ); -}; - -export const useInstanceResult = (options: ModalProps) => { - const intl = useIntl(); - const recordRef = useRef<any>(); - const { - Render, show, ...rest - } = useModal<any>({ - title: `${intl.formatMessage({ id: 'jobs_task_check_result' })}`, - footer: null, - width: '1000px', - ...(options || {}), - afterClose() { - recordRef.current = null; - }, - }); - return { - Render: useImmutable(() => (<Render><Inner record={recordRef.current} /></Render>)), - show(record: any) { - recordRef.current = record; - show(record); - }, - ...rest, - }; -}; diff --git a/datavines-ui/src/view/JobHistory/Instance/useLogger/index.tsx b/datavines-ui/src/view/JobHistory/Instance/useLogger/index.tsx deleted file mode 100644 index 6b1e304cf..000000000 --- a/datavines-ui/src/view/JobHistory/Instance/useLogger/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* eslint-disable react/no-danger */ -import React, { useRef, useState, useImperativeHandle } from 'react'; -import { ModalProps, Spin } from 'antd'; -import { FullscreenExitOutlined, DownloadOutlined, SyncOutlined } from '@ant-design/icons'; -import { - useModal, useContextModal, useImmutable, usePersistFn, useMount, IF, -} from 'src/common'; -import { useIntl } from 'react-intl'; -import { $http } from '@/http'; -import { download } from '@/utils'; - -type InnerProps = { - innerRef: any -} - -const dealMsg = (msg: string) => { - if (msg) { - return msg.replace(/\r\n/g, '<br>'); - } - return ''; -}; -const Inner = ({ innerRef }: InnerProps) => { - const [loading, setLoading] = useState(false); - const { data } = useContextModal(); - const [wholeLog, setWholeLog] = useState<{offsetLine: number, msg: string}[]>([]); - const getData = async (offsetLine: number) => { - try { - setLoading(true); - const res = (await $http.get('history/job/execution/queryLogWithOffsetLine', { - taskId: data.id, - offsetLine, - })) || []; - res.msg = dealMsg(res.msg); - if (offsetLine === 0) { - setWholeLog([res]); - } else { - setWholeLog([...wholeLog, res]); - } - } catch (error) { - } finally { - setLoading(false); - } - }; - useMount(async () => { - getData(0); - }); - useImperativeHandle(innerRef, () => ({ - onRefresh() { - getData(wholeLog[wholeLog.length - 1]?.offsetLine || 0); - }, - })); - return ( - <Spin spinning={loading}> - <div style={{ minHeight: 300 }}> - { - wholeLog.map((item) => ( - <div dangerouslySetInnerHTML={{ __html: item.msg }} /> - )) - } - <div /> - </div> - </Spin> - ); -}; - -export const useLogger = (options: ModalProps) => { - const intl = useIntl(); - const innerRef = useRef<any>(); - const recordRef = useRef<any>(); - const onOk = usePersistFn(() => { - hide(); - }); - const onDownload = usePersistFn(async () => { - try { - const blob = await $http.get('history/job/execution/download', { taskId: recordRef.current?.id }, { - responseType: 'blob', - }); - download(blob); - } catch (error) { - } - }); - const { - Render, hide, show, ...rest - } = useModal<any>({ - title: ( - <div className="dv-flex-between"> - <span>{intl.formatMessage({ id: 'job_log_view_log' })}</span> - <div style={{ marginRight: 30 }}> - <a - style={{ marginRight: 10 }} - onClick={() => { - innerRef.current.onRefresh(); - }} - > - <SyncOutlined style={{ marginRight: 5 }} /> - {intl.formatMessage({ id: 'job_log_refresh' })} - </a> - <a style={{ marginRight: 10 }} onClick={onDownload}> - <DownloadOutlined style={{ marginRight: 5 }} /> - {intl.formatMessage({ id: 'job_log_download' })} - </a> - - </div> - </div> - ), - className: 'dv-modal-fullscreen', - footer: null, - width: '90%', - ...(options || {}), - afterClose() { - recordRef.current = null; - }, - onOk, - }); - return { - Render: useImmutable(() => (<Render><Inner innerRef={innerRef} /></Render>)), - show(record: any) { - recordRef.current = record; - show(record); - }, - ...rest, - }; -}; From cb5aecb946773121b23030f0bd08c01cebb97ec6 Mon Sep 17 00:00:00 2001 From: 735140144 <735140144@qq.com> Date: Tue, 3 Sep 2024 14:48:42 +0800 Subject: [PATCH 4/5] [Feature][Server]Improve sla notification click url in email and wecombot [Feature][Server][UI]Improve Notification with quick link without check Token --- datavines-ui/src/view/JobHistory/index.tsx | 106 +++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 datavines-ui/src/view/JobHistory/index.tsx diff --git a/datavines-ui/src/view/JobHistory/index.tsx b/datavines-ui/src/view/JobHistory/index.tsx new file mode 100644 index 000000000..636e68e0a --- /dev/null +++ b/datavines-ui/src/view/JobHistory/index.tsx @@ -0,0 +1,106 @@ +/* eslint-disable react/no-danger */ +import React, {useRef, useState, useImperativeHandle} from 'react'; +import {ModalProps, Spin} from 'antd'; +import {FullscreenExitOutlined, DownloadOutlined, SyncOutlined} from '@ant-design/icons'; +import { + useModal, useContextModal, useImmutable, usePersistFn, useMount, IF, useWatch, +} from '@/common'; +import {useIntl} from 'react-intl'; +import {$http} from '@/http'; +import {download} from '@/utils'; +import querystring from "querystring"; +import {base64Decode} from "utils/base64"; + +type InnerProps = { + innerRef: any +} + +const JobHistory = () => { + const intl = useIntl(); + const innerRef = useRef<any>(); + const recordRef = useRef<any>(); + const dealMsg = (msg: string) => { + if (msg) { + return msg.replace(/\r\n/g, '<br>'); + } + return ''; + }; + const [loading, setLoading] = useState(false); + const [wholeLog, setWholeLog] = useState<{ offsetLine: number, msg: string }[]>([]); + let executionId = querystring.parse(base64Decode(window.location.href.split('?')[1] as string) || '').executionId; + const getData = async (offsetLine: number) => { + try { + setLoading(true); + const res = (await $http.get('history/job/execution/queryLogWithOffsetLine', { + taskId: executionId, + offsetLine, + })) || []; + res.msg = dealMsg(res.msg); + if (offsetLine === 0) { + setWholeLog([res]); + } else { + setWholeLog([...wholeLog, res]); + } + } catch (error) { + } finally { + setLoading(false); + } + }; + + const onDownload = usePersistFn(async () => { + try { + const blob = await $http.get('history/job/execution/download', { taskId: executionId }, { + responseType: 'blob', + }); + download(blob); + } catch (error) { + } + }); + + useMount(async () => { + getData(0); + }); + useImperativeHandle(innerRef, () => ({ + onRefresh() { + getData(wholeLog[wholeLog.length - 1]?.offsetLine || 0); + }, + })); + return ( + <div className={"ant-modal-content"}> + <div style={{position: 'fixed', padding: "10px 0", top: 0, left: 0, width: '100%', zIndex: 1000, display: 'flex', justifyContent: 'space-between', alignItems: 'center',fontSize:16,fontWeight:600,lineHeight:1.5}}> + <span style={{marginLeft: 20}}>{intl.formatMessage({id: 'job_log_view_log'})}</span> + <div style={{marginRight: 30,color:'#1677ff'}}> + <a + style={{marginRight: 10}} + onClick={() => { + innerRef.current.onRefresh(); + }} + > + <SyncOutlined style={{marginRight: 5}}/> + {intl.formatMessage({id: 'job_log_refresh'})} + </a> + <a style={{marginRight: 10}} onClick={onDownload}> + <DownloadOutlined style={{marginRight: 5}}/> + {intl.formatMessage({id: 'job_log_download'})} + </a> + + </div> + </div> + <div style={{minHeight: 300, padding: "60px 20px",lineHeight:1.5,fontSize:14}}> + { + wholeLog.map((item) => ( + <div dangerouslySetInnerHTML={{__html: item.msg}}/> + )) + } + <div/> + </div> + </div> + ) + +} +export default JobHistory; + + + + + From d56f9fbf394ca537c8d65333eac106fc0746a535 Mon Sep 17 00:00:00 2001 From: 735140144 <735140144@qq.com> Date: Tue, 3 Sep 2024 14:50:12 +0800 Subject: [PATCH 5/5] [Feature][Server]Improve sla notification click url in email and wecombot [Feature][Server][UI]Improve Notification with quick link without check Token --- datavines-ui/src/view/JobHistory/index.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/datavines-ui/src/view/JobHistory/index.tsx b/datavines-ui/src/view/JobHistory/index.tsx index 636e68e0a..b828d2a6c 100644 --- a/datavines-ui/src/view/JobHistory/index.tsx +++ b/datavines-ui/src/view/JobHistory/index.tsx @@ -1,24 +1,16 @@ /* eslint-disable react/no-danger */ import React, {useRef, useState, useImperativeHandle} from 'react'; -import {ModalProps, Spin} from 'antd'; -import {FullscreenExitOutlined, DownloadOutlined, SyncOutlined} from '@ant-design/icons'; -import { - useModal, useContextModal, useImmutable, usePersistFn, useMount, IF, useWatch, -} from '@/common'; +import { DownloadOutlined, SyncOutlined} from '@ant-design/icons'; +import {usePersistFn, useMount,} from '@/common'; import {useIntl} from 'react-intl'; import {$http} from '@/http'; import {download} from '@/utils'; import querystring from "querystring"; import {base64Decode} from "utils/base64"; -type InnerProps = { - innerRef: any -} - const JobHistory = () => { const intl = useIntl(); const innerRef = useRef<any>(); - const recordRef = useRef<any>(); const dealMsg = (msg: string) => { if (msg) { return msg.replace(/\r\n/g, '<br>');