This is an automated email from the ASF dual-hosted git repository.

dockerzhang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/inlong.git


The following commit(s) were added to refs/heads/master by this push:
     new 22d9a33a00 [INLONG-11510][Dashboard] Add page to dirty data query 
(#11512)
22d9a33a00 is described below

commit 22d9a33a00ea25e576dd986103e7be9009de18b7
Author: kamianlaida <[email protected]>
AuthorDate: Wed Nov 20 22:04:59 2024 +0800

    [INLONG-11510][Dashboard] Add page to dirty data query (#11512)
---
 inlong-dashboard/src/ui/locales/cn.json            |  33 +-
 inlong-dashboard/src/ui/locales/en.json            |  34 +-
 .../src/ui/pages/GroupDetail/DataStorage/index.tsx |  24 +-
 .../ui/pages/SynchronizeDetail/SyncSink/index.tsx  |  24 +-
 .../src/ui/pages/common/DirtyModal/conf.tsx        |  47 ++
 .../src/ui/pages/common/DirtyModal/index.tsx       | 593 +++++++++++++++++++++
 6 files changed, 751 insertions(+), 4 deletions(-)

diff --git a/inlong-dashboard/src/ui/locales/cn.json 
b/inlong-dashboard/src/ui/locales/cn.json
index 535063ee00..4f631bb151 100644
--- a/inlong-dashboard/src/ui/locales/cn.json
+++ b/inlong-dashboard/src/ui/locales/cn.json
@@ -1,5 +1,6 @@
 {
   "basic.Edit": "编辑",
+  "basic.Search": "搜索",
   "basic.Detail": "详情",
   "basic.Operating": "操作",
   "basic.OperatingSuccess": "操作成功",
@@ -1020,5 +1021,35 @@
   "pages.GroupDataTemplate.VisibleRange.InCharges":"责任人",
   "pages.GroupDataTemplate.VisibleRange.Tenant":"租户",
   "miscellaneous.total": "... 共",
-  "miscellaneous.tenants": "个租户"
+  "miscellaneous.tenants": "个租户",
+  "meta.Sinks.DirtyData.DirtyDataPartition": "脏数据分区",
+  "meta.Sinks.DirtyData.DataFlowId": "数据目标Id",
+  "meta.Sinks.DirtyData.GroupId": "数据组Id",
+  "meta.Sinks.DirtyData.StreamId": "数据流Id",
+  "meta.Sinks.DirtyData.ReportTime": "上报时间",
+  "meta.Sinks.DirtyData.DataTime": "数据时间",
+  "meta.Sinks.DirtyData.ServerType": "服务类型",
+  "meta.Sinks.DirtyData.DirtyType": "脏数据类型",
+  "meta.Sinks.DirtyData.DirtyMessage": "脏数据信息",
+  "meta.Sinks.DirtyData.ExtInfo": "额外信息",
+  "meta.Sinks.DirtyData.DirtyData": "脏数据",
+  "meta.Sinks.DirtyData.DirtyDetailWarning": "脏数据任务正在运行,请稍后再试",
+  "meta.Sinks.DirtyData.DirtyTrendWarning": "脏数据趋势任务正在运行,请稍后再试",
+  "meta.Sinks.DirtyData.DataCount": "脏数据条数",
+  "meta.Sinks.DirtyData.Search.DirtyType": "脏数据类型",
+  "meta.Sinks.DirtyData.Search.ServerType": "服务类型",
+  "meta.Sinks.DirtyData.StartTimeError": "开始时间不能大于当前时间",
+  "meta.Sinks.DirtyData.endTimeNotGreaterThanStartTime": "结束时间不能大于当前时间",
+  "meta.Sinks.DirtyData.TimeIntervalError": "时间间隔不能超过七天",
+  "meta.Sinks.DirtyTrend.DataTimeUnit":"时间单位",
+  "meta.Sinks.DirtyTrend.Day":"天",
+  "meta.Sinks.DirtyTrend.Hour":"小时",
+  "meta.Sinks.DirtyData.Detail":"详情",
+  "meta.Sinks.DirtyData.Trend":"趋势",
+  "meta.Sinks.DirtyData":"脏数据查询",
+  "meta.Sinks.DirtyData.DirtyType.DeserializeError":"反序列化错误",
+  "meta.Sinks.DirtyData.DirtyType.FieldMappingError":"字段映射错误",
+  "meta.Sinks.DirtyData.DirtyType.LoadError":"加载错误",
+  "meta.Sinks.DirtyData.Search.KeyWordHelp":"请输入关键字",
+  "meta.Sinks.DirtyData.Search.KeyWord":"关键字"
 }
diff --git a/inlong-dashboard/src/ui/locales/en.json 
b/inlong-dashboard/src/ui/locales/en.json
index b74f54e58e..b7e60d316b 100644
--- a/inlong-dashboard/src/ui/locales/en.json
+++ b/inlong-dashboard/src/ui/locales/en.json
@@ -1,5 +1,6 @@
 {
   "basic.Edit": "Edit",
+  "basic.Search": "Search",
   "basic.Detail": "Detail",
   "basic.Operating": "Operation",
   "basic.OperatingSuccess": "Operating success",
@@ -374,6 +375,7 @@
   "meta.Sinks.Cls.Tag": "Tag",
   "meta.Sinks.Cls.Tokenizer": "Tokenizer rule",
   "meta.Sinks.Cls.IsMetaField": "Is meta field",
+
   "meta.Group.InlongGroupId": "Inlong group id",
   "meta.Group.InlongGroupIdRules": "Only English letters, numbers, dots(.), 
minus(-), and underscores(_)",
   "meta.Group.InlongGroupName": "Inlong group name",
@@ -1020,5 +1022,35 @@
   "pages.GroupDataTemplate.VisibleRange.InCharges":"Owner",
   "pages.GroupDataTemplate.VisibleRange.Tenant":"Tenant",
   "miscellaneous.total": "... total ",
-  "miscellaneous.tenants": " tenants"
+  "miscellaneous.tenants": " tenants",
+  "meta.Sinks.DirtyData.DirtyDataPartition": "Dirty Data Partition",
+  "meta.Sinks.DirtyData.DataFlowId": "Data Flow Id",
+  "meta.Sinks.DirtyData.GroupId": "Group Id",
+  "meta.Sinks.DirtyData.StreamId": "Stream Id",
+  "meta.Sinks.DirtyData.ReportTime": "Report Time",
+  "meta.Sinks.DirtyData.DataTime": "Data Time",
+  "meta.Sinks.DirtyData.ServerType": "Server Type",
+  "meta.Sinks.DirtyData.DirtyType": "Dirty Type",
+  "meta.Sinks.DirtyData.DirtyMessage": "Dirty Message",
+  "meta.Sinks.DirtyData.ExtInfo": "Ext Info",
+  "meta.Sinks.DirtyData.DirtyData": "Dirty Data",
+  "meta.Sinks.DirtyData.DirtyDetailWarning": "The dirty data task is running, 
please try again later",
+  "meta.Sinks.DirtyData.DirtyTrendWarning": "The dirty data trending task is 
running, try again later",
+  "meta.Sinks.DirtyData.DataCount": "Data Count",
+  "meta.Sinks.DirtyData.Search.DirtyType": "Dirty Type",
+  "meta.Sinks.DirtyData.Search.ServerType": "Server Type",
+  "meta.Sinks.DirtyData.StartTimeError": "The start time cannot be greater 
than the current time",
+  "meta.Sinks.DirtyData.endTimeNotGreaterThanStartTime": "The end time cannot 
be greater than the current time",
+  "meta.Sinks.DirtyData.TimeIntervalError":  "The time interval cannot be more 
than seven days",
+  "meta.Sinks.DirtyTrend.DataTimeUnit":"DataTime Unit",
+  "meta.Sinks.DirtyTrend.Day":"Day",
+  "meta.Sinks.DirtyTrend.Hour":"Hour",
+  "meta.Sinks.DirtyData.Detail":"Detail",
+  "meta.Sinks.DirtyData.Trend":"Trend",
+  "meta.Sinks.DirtyData":"Dirty Data Query",
+  "meta.Sinks.DirtyData.DirtyType.DeserializeError":"Deserialize Error",
+  "meta.Sinks.DirtyData.DirtyType.FieldMappingError":"Field Mapping Error",
+  "meta.Sinks.DirtyData.DirtyType.LoadError":"Load Error",
+  "meta.Sinks.DirtyData.Search.KeyWordHelp":"Please enter a keyword",
+  "meta.Sinks.DirtyData.Search.KeyWord":"Key word"
 }
diff --git a/inlong-dashboard/src/ui/pages/GroupDetail/DataStorage/index.tsx 
b/inlong-dashboard/src/ui/pages/GroupDetail/DataStorage/index.tsx
index b91cac850d..5f1086d91e 100644
--- a/inlong-dashboard/src/ui/pages/GroupDetail/DataStorage/index.tsx
+++ b/inlong-dashboard/src/ui/pages/GroupDetail/DataStorage/index.tsx
@@ -25,6 +25,7 @@ import {
   TableOutlined,
   EditOutlined,
   DeleteOutlined,
+  AreaChartOutlined,
 } from '@ant-design/icons';
 import HighTable from '@/ui/components/HighTable';
 import { defaultSize } from '@/configs/pagination';
@@ -36,6 +37,7 @@ import request from '@/core/utils/request';
 import { pickObjectArray } from '@/core/utils';
 import { CommonInterface } from '../common';
 import { sinks } from '@/plugins/sinks';
+import DirtyModal from '@/ui/pages/common/DirtyModal';
 
 interface Props extends CommonInterface {
   inlongStreamId?: string;
@@ -58,7 +60,12 @@ const Comp = ({ inlongGroupId, inlongStreamId, readonly }: 
Props, ref) => {
   const [createModal, setCreateModal] = useState<Record<string, unknown>>({
     open: false,
   });
-
+  const [dirtyModal, setDirtyModal] = useState<Record<string, unknown>>({
+    open: false,
+  });
+  const onOpenDirtyModal = useCallback(({ id }) => {
+    setDirtyModal({ open: true, id });
+  }, []);
   const {
     data,
     loading,
@@ -173,6 +180,9 @@ const Comp = ({ inlongGroupId, inlongStreamId, readonly }: 
Props, ref) => {
               <Button type="link" onClick={() => onDelete(record)}>
                 {i18n.t('basic.Delete')}
               </Button>
+              <Button type="link" onClick={() => onOpenDirtyModal(record)}>
+                {i18n.t('meta.Sinks.DirtyData')}
+              </Button>
             </>
           ),
       } as any,
@@ -238,6 +248,9 @@ const Comp = ({ inlongGroupId, inlongStreamId, readonly }: 
Props, ref) => {
                   <Button key="del" type="link" onClick={() => onDelete(item)}>
                     <DeleteOutlined />
                   </Button>,
+                  <Button type="link" onClick={() => onOpenDirtyModal(item)}>
+                    <AreaChartOutlined />
+                  </Button>,
                 ]}
               >
                 <span>
@@ -277,6 +290,15 @@ const Comp = ({ inlongGroupId, inlongStreamId, readonly }: 
Props, ref) => {
         }}
         onCancel={() => setCreateModal({ open: false })}
       />
+      <DirtyModal
+        {...dirtyModal}
+        open={dirtyModal.open as boolean}
+        onOk={async () => {
+          await getList();
+          setDirtyModal({ open: false });
+        }}
+        onCancel={() => setDirtyModal({ open: false })}
+      />
     </>
   );
 };
diff --git a/inlong-dashboard/src/ui/pages/SynchronizeDetail/SyncSink/index.tsx 
b/inlong-dashboard/src/ui/pages/SynchronizeDetail/SyncSink/index.tsx
index 9e7c243630..2d5f2a761a 100644
--- a/inlong-dashboard/src/ui/pages/SynchronizeDetail/SyncSink/index.tsx
+++ b/inlong-dashboard/src/ui/pages/SynchronizeDetail/SyncSink/index.tsx
@@ -25,6 +25,7 @@ import {
   TableOutlined,
   EditOutlined,
   DeleteOutlined,
+  AreaChartOutlined,
 } from '@ant-design/icons';
 import HighTable from '@/ui/components/HighTable';
 import { defaultSize } from '@/configs/pagination';
@@ -36,6 +37,7 @@ import request from '@/core/utils/request';
 import { pickObjectArray } from '@/core/utils';
 import { sinks } from '@/plugins/sinks';
 import { CommonInterface } from '../common';
+import DirtyModal from '@/ui/pages/common/DirtyModal';
 
 interface Props extends CommonInterface {
   inlongStreamId: string;
@@ -58,7 +60,12 @@ const Comp = ({ inlongGroupId, inlongStreamId, 
sinkMultipleEnable, readonly }: P
   const [createModal, setCreateModal] = useState<Record<string, unknown>>({
     open: false,
   });
-
+  const [dirtyModal, setDirtyModal] = useState<Record<string, unknown>>({
+    open: false,
+  });
+  const onOpenDirtyModal = useCallback(({ id }) => {
+    setDirtyModal({ open: true, id });
+  }, []);
   const {
     data,
     loading,
@@ -179,6 +186,9 @@ const Comp = ({ inlongGroupId, inlongStreamId, 
sinkMultipleEnable, readonly }: P
               <Button type="link" onClick={() => onDelete(record)}>
                 {i18n.t('basic.Delete')}
               </Button>
+              <Button type="link" onClick={() => onOpenDirtyModal(record)}>
+                {i18n.t('meta.Sinks.DirtyData')}
+              </Button>
             </>
           ),
       } as any,
@@ -249,6 +259,9 @@ const Comp = ({ inlongGroupId, inlongStreamId, 
sinkMultipleEnable, readonly }: P
                   <Button key="del" type="link" onClick={() => onDelete(item)}>
                     <DeleteOutlined />
                   </Button>,
+                  <Button type="link" onClick={() => onOpenDirtyModal(item)}>
+                    <AreaChartOutlined />
+                  </Button>,
                 ]}
               >
                 <span>
@@ -289,6 +302,15 @@ const Comp = ({ inlongGroupId, inlongStreamId, 
sinkMultipleEnable, readonly }: P
         }}
         onCancel={() => setCreateModal({ open: false })}
       />
+      <DirtyModal
+        {...dirtyModal}
+        open={dirtyModal.open as boolean}
+        onOk={async () => {
+          await getList();
+          setDirtyModal({ open: false });
+        }}
+        onCancel={() => setDirtyModal({ open: false })}
+      />
     </>
   );
 };
diff --git a/inlong-dashboard/src/ui/pages/common/DirtyModal/conf.tsx 
b/inlong-dashboard/src/ui/pages/common/DirtyModal/conf.tsx
new file mode 100644
index 0000000000..ce18eceb8a
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/common/DirtyModal/conf.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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 i18n from '@/i18n';
+
+export const statusList = [
+  {
+    label: i18n.t('meta.Sinks.DirtyData.DirtyType.DeserializeError'),
+    value: 'DeserializeError',
+  },
+  {
+    label: i18n.t('meta.Sinks.DirtyData.DirtyType.FieldMappingError'),
+    value: 'FieldMappingError',
+  },
+  {
+    label: i18n.t('meta.Sinks.DirtyData.DirtyType.LoadError'),
+    value: 'LoadError',
+  },
+];
+
+export const statusMap = statusList.reduce(
+  (acc, cur) => ({
+    ...acc,
+    [cur.value]: cur,
+  }),
+  {},
+);
+
+export const genStatusTag = value => {
+  return statusMap[value];
+};
diff --git a/inlong-dashboard/src/ui/pages/common/DirtyModal/index.tsx 
b/inlong-dashboard/src/ui/pages/common/DirtyModal/index.tsx
new file mode 100644
index 0000000000..d77d5829fc
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/common/DirtyModal/index.tsx
@@ -0,0 +1,593 @@
+/*
+ * 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 React, { useEffect, useState } from 'react';
+import { Button, message, Modal, Tabs, TabsProps } from 'antd';
+import { ModalProps } from 'antd/es/modal';
+import i18n from '@/i18n';
+import HighTable from '@/ui/components/HighTable';
+
+import dayjs from 'dayjs';
+import request from '@/core/utils/request';
+import { useForm } from 'antd/es/form/Form';
+import FormGenerator from '@/ui/components/FormGenerator';
+import Charts from '@/ui/components/Charts';
+import { genStatusTag } from '@/ui/pages/common/DirtyModal/conf';
+
+export interface Props extends ModalProps {
+  id?: number;
+}
+const Comp: React.FC<Props> = ({ ...modalProps }) => {
+  const [form1] = useForm();
+  const [form2] = useForm();
+  const [loading, setLoading] = useState(false);
+  const getColumns = [
+    {
+      title: i18n.t('meta.Sinks.DirtyData.DataFlowId'),
+      dataIndex: 'dataFlowId',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.GroupId'),
+      dataIndex: 'groupId',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.StreamId'),
+      dataIndex: 'streamId',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.ReportTime'),
+      dataIndex: 'reportTime',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.DataTime'),
+      dataIndex: 'dataTime',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.ServerType'),
+      dataIndex: 'serverType',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.DirtyType'),
+      dataIndex: 'dirtyType',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.DirtyMessage'),
+      dataIndex: 'dirtyMessage',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.ExtInfo'),
+      dataIndex: 'extInfo',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.DirtyData'),
+      dataIndex: 'dirtyData',
+      width: 90,
+    },
+    {
+      title: i18n.t('meta.Sinks.DirtyData.DirtyDataPartition'),
+      dataIndex: 'dirtyDataPartition',
+      width: 90,
+    },
+  ];
+
+  const defaultDetailOptions = {
+    keyword: '',
+    dataCount: 10,
+    dirtyType: '',
+    serverType: '',
+    startTime: dayjs().format('YYYYMMDD'),
+    endTime: dayjs().format('YYYYMMDD'),
+  };
+  const defaultTrendOptions = {
+    dataTimeUnit: 'D',
+    dirtyType: '',
+    serverType: '',
+    startTime: dayjs().format('YYYYMMDD'),
+    endTime: dayjs().format('YYYYMMDD'),
+  };
+  const [options, setOptions] = useState(defaultDetailOptions);
+  const [trendOptions, setTrendOption] = useState(defaultTrendOptions);
+  const [data, setData] = useState([]);
+  const [trendData, setTrendData] = useState([]);
+  const [tabValue, setTabValue] = useState('detail');
+  useEffect(() => {
+    if (modalProps.open) {
+      if (tabValue === 'detail') {
+        setOptions(defaultDetailOptions);
+        form1.resetFields();
+        getTaskResult().then(item => {
+          setData(item);
+        });
+      }
+      if (tabValue === 'trend') {
+        setTrendOption(defaultTrendOptions);
+        form2.resetFields();
+        form2.setFieldsValue({
+          dataTimeUnit: 'D',
+        });
+        getTrendData().then(item => {
+          setTrendData(item);
+        });
+      }
+    }
+  }, [modalProps.open, tabValue]);
+  const [messageApi, contextHolder] = message.useMessage();
+  const warning = () => {
+    messageApi.open({
+      type: 'warning',
+      content:
+        tabValue === 'detail'
+          ? i18n.t('meta.Sinks.DirtyData.DirtyDetailWarning')
+          : i18n.t('meta.Sinks.DirtyData.DirtyTrendWarning'),
+    });
+  };
+  const getTaskResult = async () => {
+    setLoading(true);
+    const taskId = await getTaskId();
+    const status = await request({
+      url: '/sink/SqlTaskStatus/' + taskId,
+      method: 'GET',
+    });
+    if (status === 'success') {
+      const data = await request({
+        url: '/sink/getDirtyData/' + taskId,
+        method: 'GET',
+      });
+      setLoading(false);
+      return data;
+    } else {
+      setLoading(false);
+      warning();
+    }
+    return [];
+  };
+  const getTaskId = async () => {
+    const data = await request({
+      url: '/sink/listDirtyData',
+      method: 'POST',
+      data: {
+        ...options,
+        startTime: options.startTime ? 
dayjs(options.startTime).format('YYYYMMDD') : '',
+        endTime: options.endTime ? dayjs(options.endTime).format('YYYYMMDD') : 
'',
+        dataCount: form1.getFieldValue('dataCount') || 10,
+        keyword: form1.getFieldValue('keyword') || '',
+        sinkIdList: [modalProps.id],
+      },
+    });
+    return data.taskId;
+  };
+
+  const getTrendData = async () => {
+    const taskId = await getTrendTaskId();
+    const status = await request({
+      url: '/sink/SqlTaskStatus/' + taskId,
+      method: 'GET',
+    });
+
+    if (status === 'success') {
+      const data = await request({
+        url: '/sink/getDirtyDataTrend/' + taskId,
+        method: 'GET',
+      });
+      return data;
+    } else {
+      warning();
+    }
+    return [];
+  };
+
+  const getTrendTaskId = async () => {
+    const data = await request({
+      url: '/sink/listDirtyDataTrend',
+      method: 'POST',
+      data: {
+        ...trendOptions,
+        sinkIdList: [modalProps.id],
+      },
+    });
+    return data.taskId;
+  };
+  const onSearch = async () => {
+    await form1.validateFields();
+    await getTaskResult().then(item => {
+      setData(item);
+    });
+  };
+  const onTrendSearch = async () => {
+    await form2.validateFields();
+    await getTrendData().then(item => {
+      setTrendData(item);
+    });
+  };
+
+  const getDetailFilterFormContent = defaultValues => [
+    {
+      type: 'input',
+      label: i18n.t('meta.Sinks.DirtyData.Search.KeyWord'),
+      name: 'keyword',
+      props: {
+        placeholder: i18n.t('meta.Sinks.DirtyData.Search.KeyWordHelp'),
+      },
+    },
+    {
+      label: i18n.t('meta.Sinks.DirtyData.DataCount'),
+      type: 'inputnumber',
+      name: 'dataCount',
+    },
+    {
+      label: i18n.t('meta.Sinks.DirtyData.Search.DirtyType'),
+      type: 'select',
+      name: 'dirtyType',
+      props: {
+        allowClear: true,
+        options: [
+          {
+            label: i18n.t('meta.Sinks.DirtyData.DirtyType.DeserializeError'),
+            value: 'DeserializeError',
+          },
+          {
+            label: i18n.t('meta.Sinks.DirtyData.DirtyType.FieldMappingError'),
+            value: 'FieldMappingError',
+          },
+          {
+            label: i18n.t('meta.Sinks.DirtyData.DirtyType.LoadError'),
+            value: 'LoadError',
+          },
+        ],
+      },
+    },
+    {
+      label: i18n.t('meta.Sinks.DirtyData.Search.ServerType'),
+      type: 'select',
+      name: 'serverType',
+      props: {
+        allowClear: true,
+        options: [
+          {
+            label: 'TubeMQ',
+            value: 'TubeMQ',
+          },
+          {
+            label: 'Iceberg',
+            value: 'Iceberg',
+          },
+        ],
+      },
+    },
+    {
+      type: 'datepicker',
+      label: i18n.t('pages.GroupDetail.Audit.StartDate'),
+      name: 'startTime',
+      initialValue: dayjs(options.startTime),
+      props: {
+        allowClear: true,
+        format: 'YYYYMMDD',
+      },
+      rules: [
+        { required: true },
+        ({ getFieldValue }) => ({
+          validator(_, value) {
+            if (Boolean(value)) {
+              if (value.isAfter(dayjs())) {
+                return Promise.reject(new 
Error(i18n.t('meta.Sinks.DirtyData.StartTimeError')));
+              }
+            }
+            return Promise.resolve();
+          },
+        }),
+      ],
+    },
+    {
+      type: 'datepicker',
+      label: i18n.t('pages.GroupDetail.Audit.EndDate'),
+      name: 'endTime',
+      initialValue: dayjs(options.endTime),
+      props: values => {
+        return {
+          allowClear: true,
+          format: 'YYYYMMDD',
+        };
+      },
+      rules: [
+        { required: true },
+        ({ getFieldValue }) => ({
+          validator(_, value) {
+            if (Boolean(value)) {
+              if (value.isAfter(dayjs())) {
+                return Promise.reject(new 
Error(i18n.t('endTimeNotGreaterThanStartTime')));
+              }
+              const timeDiff = value.diff(getFieldValue('startDate'), 'day');
+              if (timeDiff <= 7) {
+                return Promise.resolve();
+              }
+              return Promise.reject(new 
Error(i18n.t('meta.Sinks.DirtyData.TimeIntervalError')));
+            }
+            return Promise.resolve();
+          },
+        }),
+      ],
+    },
+    {
+      type: (
+        <Button type="primary" onClick={onSearch}>
+          {i18n.t('basic.Search')}
+        </Button>
+      ),
+    },
+  ];
+  const getTendFilterFormContent = defaultValues => [
+    {
+      label: i18n.t('meta.Sinks.DirtyData.Search.DirtyType'),
+      type: 'select',
+      name: 'dirtyType',
+      props: {
+        allowClear: true,
+        options: [
+          {
+            label: 'DeserializeError',
+            value: 'DeserializeError',
+          },
+          {
+            label: 'FieldMappingError',
+            value: 'FieldMappingError',
+          },
+          {
+            label: 'LoadError',
+            value: 'LoadError',
+          },
+        ],
+      },
+    },
+    {
+      label: i18n.t('meta.Sinks.DirtyData.Search.ServerType'),
+      type: 'select',
+      name: 'serverType',
+      props: {
+        allowClear: true,
+        options: [
+          {
+            label: 'TubeMQ',
+            value: 'TubeMQ',
+          },
+          {
+            label: 'Iceberg',
+            value: 'Iceberg',
+          },
+        ],
+      },
+    },
+    {
+      label: i18n.t('meta.Sinks.DirtyTrend.DataTimeUnit'),
+      type: 'select',
+      name: 'dataTimeUnit',
+      initialValue: 'D',
+      props: {
+        options: [
+          {
+            label: i18n.t('meta.Sinks.DirtyTrend.Day'),
+            value: 'D',
+          },
+          {
+            label: i18n.t('meta.Sinks.DirtyTrend.Hour'),
+            value: 'H',
+          },
+        ],
+      },
+    },
+
+    {
+      type: 'datepicker',
+      label: i18n.t('pages.GroupDetail.Audit.StartDate'),
+      name: 'startTime',
+      props: values => {
+        return {
+          allowClear: true,
+          showTime: values.dataTimeUnit === 'H',
+          format: values.dataTimeUnit === 'D' ? 'YYYYMMDD' : 'YYYYMMDDHH',
+        };
+      },
+      initialValue: dayjs(trendOptions.startTime),
+      rules: [
+        { required: true },
+        ({ getFieldValue }) => ({
+          validator(_, value) {
+            if (Boolean(value)) {
+              if (value.isAfter(dayjs())) {
+                return Promise.reject(new 
Error(i18n.t('meta.Sinks.DirtyData.StartTimeError')));
+              }
+            }
+            return Promise.resolve();
+          },
+        }),
+      ],
+    },
+    {
+      type: 'datepicker',
+      label: i18n.t('pages.GroupDetail.Audit.EndDate'),
+      name: 'endTime',
+      initialValue: dayjs(trendOptions.endTime),
+      props: values => {
+        return {
+          allowClear: true,
+          showTime: values.dataTimeUnit === 'H',
+          format: values.dataTimeUnit === 'D' ? 'YYYYMMDD' : 'YYYYMMDDHH',
+        };
+      },
+      rules: [
+        { required: true },
+        ({ getFieldValue }) => ({
+          validator(_, value) {
+            if (Boolean(value)) {
+              if (value.isAfter(dayjs())) {
+                return Promise.reject(
+                  new 
Error(i18n.t('meta.Sinks.DirtyData.endTimeNotGreaterThanStartTime')),
+                );
+              }
+              const timeDiff = value.diff(getFieldValue('startTime'), 'day');
+              if (timeDiff <= 7) {
+                return Promise.resolve();
+              }
+              return Promise.reject(new 
Error(i18n.t('meta.Sinks.DirtyData.TimeIntervalError')));
+            }
+            return Promise.resolve();
+          },
+        }),
+      ],
+    },
+    {
+      type: (
+        <Button type="primary" onClick={onTrendSearch}>
+          {i18n.t('basic.Search')}
+        </Button>
+      ),
+    },
+  ];
+
+  const onFilter = allValues => {
+    setOptions(prev => ({
+      ...prev,
+      ...allValues,
+      startTime: allValues.startTime ? +allValues.startTime.$d : '',
+      endTime: allValues.endTime ? +allValues.endTime.$d : '',
+    }));
+  };
+
+  const onTrendFilter = allValues => {
+    setTrendOption(prev => ({
+      ...prev,
+      ...allValues,
+      startTime: allValues.startTime
+        ? allValues.dataTimeUnit === 'H'
+          ? dayjs(allValues.startTime.$d).format('YYYYMMDDHH')
+          : dayjs(allValues.startTime.$d).format('YYYYMMDD')
+        : '',
+      endTime: allValues.endTime
+        ? allValues.dataTimeUnit === 'H'
+          ? dayjs(allValues.endTime.$d).format('YYYYMMDDHH')
+          : dayjs(allValues.endTime.$d).format('YYYYMMDD')
+        : '',
+    }));
+  };
+  const scroll = { x: 2000 };
+  const toChartData = trendData => {
+    return {
+      legend: {
+        data: trendData.map(item => item.reportTime),
+      },
+      tooltip: {
+        trigger: 'axis',
+      },
+      xAxis: {
+        type: 'category',
+        data: trendData.map(item => item.reportTime),
+      },
+      yAxis: {
+        type: 'value',
+      },
+      series: trendData.map(item => ({
+        name: item.reportTime,
+        type: 'line',
+        data: trendData.map(item => item.count),
+      })),
+    };
+  };
+  const items: TabsProps['items'] = [
+    {
+      key: 'detail',
+      label: i18n.t('meta.Sinks.DirtyData.Detail'),
+      children: (
+        <>
+          <FormGenerator
+            form={form1}
+            layout="inline"
+            content={getDetailFilterFormContent(options)}
+            style={{ gap: 10 }}
+            onFilter={onFilter}
+          />
+          <HighTable
+            table={{
+              columns: getColumns,
+              rowKey: 'id',
+              size: 'small',
+              dataSource: data,
+              scroll: scroll,
+              loading,
+            }}
+          />
+        </>
+      ),
+    },
+    {
+      key: 'trend',
+      label: i18n.t('meta.Sinks.DirtyData.Trend'),
+      children: (
+        <>
+          <FormGenerator
+            form={form2}
+            layout="inline"
+            content={getTendFilterFormContent(trendOptions)}
+            style={{ gap: 10 }}
+            onFilter={onTrendFilter}
+          />
+          <Charts height={400} option={toChartData(trendData)} 
forceUpdate={true} />
+        </>
+      ),
+    },
+  ];
+  const onTabChange = (key: string) => {
+    setTabValue(key);
+  };
+  useEffect(() => {
+    onTabChange('detail');
+  }, [modalProps.open]);
+  return (
+    <>
+      {contextHolder}
+      <Modal
+        {...modalProps}
+        title={i18n.t('meta.Sinks.DirtyData')}
+        width={1200}
+        footer={null}
+        afterClose={() => {
+          onTabChange('detail');
+        }}
+      >
+        <div style={{ marginBottom: 40 }}>
+          <Tabs
+            defaultActiveKey="detail"
+            activeKey={tabValue}
+            items={items}
+            onChange={onTabChange}
+          />
+        </div>
+      </Modal>
+    </>
+  );
+};
+
+export default Comp;

Reply via email to