>([])
+ const location = useLocation()
+ useEffect(() => {
+ if (isEqual('/', location.pathname)) {
+ setSelectedKeys([location.pathname])
+ } else {
+ setSelectedKeys([headerNav.filter(nav => !isEqual(nav?.key, '/')).find(nav => contain(location.pathname, nav?.key))?.key as string ?? '/'])
+ }
+ setCurrentMenu(headerNav.find(nav => isEqual(nav?.key, location.pathname)))
+ }, [location])
+
+ return (
+
+
+
+
Hudi 服务总台
+
Hudi 全链路服务监控和控制台
+
+
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/service-web-client/src/pages/Cloud.tsx b/service-web-client/src/pages/Cloud.tsx
new file mode 100644
index 0000000..1d6ed27
--- /dev/null
+++ b/service-web-client/src/pages/Cloud.tsx
@@ -0,0 +1,96 @@
+import React from 'react'
+import {
+ amisRender,
+ commonInfo,
+ crudCommonOptions,
+ serviceLogByAppName,
+ serviceLogByAppNameAndHost,
+ time,
+} from '../util/amis.ts'
+
+const cloudCrud = (title: string, path: string) => {
+ return {
+ type: 'crud',
+ title: title,
+ api: `${commonInfo.baseUrl}${path}`,
+ ...crudCommonOptions(),
+ interval: 2000,
+ headerToolbar: ['reload'],
+ loadDataOnce: true,
+ perPage: 100,
+ columns: [
+ {
+ label: '名称',
+ type: 'tpl',
+ tpl: `\${name} \${IF(size === undefined, '', '' + size + '')}`,
+ },
+ {
+ name: 'status',
+ label: '状态',
+ align: 'center',
+ width: 60,
+ },
+ {
+ name: 'serviceUpTime',
+ label: '启动时间',
+ ...time('serviceUpTime'),
+ align: 'center',
+ width: 160,
+ },
+ {name: 'url', label: '地址'},
+ {
+ type: 'operation',
+ label: '操作',
+ width: 100,
+ fixed: 'right',
+ className: 'nowrap',
+ buttons: [
+ {
+ label: '日志',
+ type: 'action',
+ level: 'link',
+ tooltip: '打开Grafana日志',
+ onEvent: {
+ click: {
+ actions: [
+ {
+ actionType: 'custom',
+ // @ts-ignore
+ script: (context, doAction, event) => {
+ let data = context.props.data
+ let url = ''
+ if (data['metadata']) {
+ url = serviceLogByAppNameAndHost(data.serviceId, data.metadata.hostname)
+ } else if (data['name']) {
+ url = serviceLogByAppName(data.name)
+ }
+ window.open(url, '_blank')
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ ],
+ }
+}
+
+const Cloud: React.FC = () => {
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ body: [
+ cloudCrud('服务列表', '/cloud/list'),
+ cloudCrud('服务列表 (IP)', '/cloud/list_ip'),
+ ],
+ },
+ )}
+
+ )
+}
+
+export default Cloud
\ No newline at end of file
diff --git a/service-web-client/src/pages/Conversation.tsx b/service-web-client/src/pages/Conversation.tsx
new file mode 100644
index 0000000..39ea4c3
--- /dev/null
+++ b/service-web-client/src/pages/Conversation.tsx
@@ -0,0 +1,18 @@
+import {amisRender} from '../util/amis.ts'
+
+function Conversation() {
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ body: [
+ "逗你的,什么都没做,哎嘿!"
+ ],
+ },
+ )}
+
+ )
+}
+
+export default Conversation
\ No newline at end of file
diff --git a/service-web-client/src/pages/Home.tsx b/service-web-client/src/pages/Home.tsx
new file mode 100644
index 0000000..b8d2411
--- /dev/null
+++ b/service-web-client/src/pages/Home.tsx
@@ -0,0 +1,83 @@
+import {
+ CheckSquareOutlined,
+ CloudOutlined,
+ ClusterOutlined,
+ CompressOutlined,
+ InfoCircleOutlined,
+ SunOutlined,
+ SyncOutlined,
+ TableOutlined,
+ ToolOutlined,
+} from '@ant-design/icons'
+import {Layout, Menu, theme} from 'antd'
+import type {ItemType} from 'antd/es/menu/interface'
+import {isNil} from 'licia'
+import React from 'react'
+import {NavLink, Outlet} from 'react-router'
+import {commonInfo} from '../util/amis.ts'
+
+const {Sider, Content} = Layout
+
+const generateNavItem: any = (key: string, label: string, icon?: any) => {
+ let nav: any = {
+ key: key,
+ label: {label},
+ }
+ if (!isNil(icon)) {
+ nav['icon'] = icon
+ }
+ return nav
+}
+const siderNav: ItemType[] = [
+ generateNavItem('/', '概览', ),
+ generateNavItem('/home/table', '表任务', ),
+ generateNavItem('/home/queue', '压缩队列', ),
+ generateNavItem('/home/version', '跨天', ),
+ generateNavItem(`/home/yarn/${commonInfo.clusters.sync_names()}/root/Sync`, '同步集群', ),
+ generateNavItem(
+ `/home/yarn/${commonInfo.clusters.compaction_names()}/default/Compaction`,
+ '压缩集群',
+ ,
+ ),
+ generateNavItem('/home/cloud', '服务', ),
+ {
+ label: '集群',
+ icon: ,
+ children: [
+ generateNavItem('/home/yarn_cluster', '总览', ),
+ ...Object.keys(commonInfo.clusters.compaction).map(name => generateNavItem(
+ // @ts-ignore
+ `/home/yarn/${name}/${commonInfo.clusters.compaction[name]}`,
+ `${name} 集群`,
+ ,
+ )),
+ ],
+ },
+ generateNavItem('/home/tool', '工具', ),
+ generateNavItem('/home/task', '任务', ),
+]
+
+const Home: React.FC = () => {
+ const {
+ token: {
+ colorBgContainer,
+ },
+ } = theme.useToken()
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Home
\ No newline at end of file
diff --git a/service-web-client/src/pages/Overview.tsx b/service-web-client/src/pages/Overview.tsx
new file mode 100644
index 0000000..cf7ce5b
--- /dev/null
+++ b/service-web-client/src/pages/Overview.tsx
@@ -0,0 +1,456 @@
+import React from 'react'
+import {amisRender, commonInfo, crudCommonOptions, readOnlyDialogOptions} from '../util/amis.ts'
+
+const color = (number: number) => {
+ let color = 'text-success'
+ if (number > 30) {
+ color = 'text-primary'
+ }
+ if (number > 90) {
+ color = 'text-danger'
+ }
+ return color
+}
+
+const versionDetailDialog = (variable: string, target: string) => {
+ return {
+ disabledOn: `${variable} === 0`,
+ type: 'action',
+ label: '详情',
+ level: 'link',
+ size: 'sm',
+ actionType: 'dialog',
+ dialog: {
+ title: '详情',
+ actions: [],
+ size: 'md',
+ closeOnEsc: false,
+ closeOnOutside: false,
+ body: [
+ {
+ type: 'service',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/overview/version_detail`,
+ data: {
+ target: `${target}`,
+ version: '${version}',
+ },
+ },
+ body: [
+ {
+ type: 'table',
+ source: '${items}',
+ affixHeader: false,
+ columns: [
+ {
+ label: 'Flink job id',
+ fixed: 'left',
+ type: 'wrapper',
+ size: 'none',
+ body: [
+ {
+ type: 'tpl',
+ tpl: '${id}',
+ },
+ {
+ type: 'action',
+ level: 'link',
+ label: '',
+ icon: 'fa fa-copy',
+ size: 'xs',
+ actionType: 'copy',
+ content: '${id}',
+ tooltip: '复制 ID',
+ },
+ ],
+ },
+ {
+ label: '别名',
+ type: 'wrapper',
+ fixed: 'left',
+ size: 'none',
+ className: 'nowrap',
+ body: [
+ {
+ type: 'tpl',
+ tpl: '${alias}',
+ },
+ {
+ type: 'action',
+ level: 'link',
+ label: '',
+ icon: 'fa fa-copy',
+ size: 'xs',
+ actionType: 'copy',
+ content: '${alias}',
+ tooltip: '复制别名',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ }
+}
+
+const tableDetailDialog = (variable: string, targetList: any) => {
+ return {
+ disabledOn: `${variable} === 0`,
+ type: 'action',
+ label: '详情',
+ level: 'link',
+ size: 'sm',
+ actionType: 'dialog',
+ dialog: {
+ title: '详情',
+ size: 'md',
+ ...readOnlyDialogOptions(),
+ body: [
+ {
+ type: 'table',
+ source: `\${${targetList}}`,
+ affixHeader: false,
+ columns: [
+ {
+ label: 'Flink job id',
+ fixed: 'left',
+ type: 'wrapper',
+ size: 'none',
+ body: [
+ {
+ type: 'tpl',
+ tpl: '${id}',
+ },
+ {
+ type: 'action',
+ level: 'link',
+ label: '',
+ icon: 'fa fa-copy',
+ size: 'xs',
+ actionType: 'copy',
+ content: '${id}',
+ tooltip: '复制 ID',
+ },
+ ],
+ },
+ {
+ label: '别名',
+ type: 'wrapper',
+ fixed: 'left',
+ size: 'none',
+ className: 'nowrap',
+ body: [
+ {
+ type: 'tpl',
+ tpl: '${alias}',
+ },
+ {
+ type: 'action',
+ level: 'link',
+ label: '',
+ icon: 'fa fa-copy',
+ size: 'xs',
+ actionType: 'copy',
+ content: '${alias}',
+ tooltip: '复制别名',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ }
+}
+
+const overviewYarnJob = (cluster: string, search: string, queue: string | undefined, yarnQueue: string) => {
+ return {
+ className: 'font-mono',
+ type: 'service',
+ api: `${commonInfo.baseUrl}/overview/yarn-job?cluster=${cluster}&search=${search}`,
+ interval: 10000,
+ silentPolling: true,
+ body: [
+ {
+ type: 'tpl',
+ className: 'mr-1 font-bold',
+ tpl: `\${PADSTART('${cluster}', 3)}`,
+ },
+ queue === undefined ? {} : {
+ type: 'service',
+ className: 'inline ml-2',
+ api: `${commonInfo.baseUrl}/overview/queue?queue=compaction-queue-${cluster}`,
+ interval: 10000,
+ silentPolling: true,
+ body: [
+ {
+ type: 'tpl',
+ tpl: '${PADSTART(size, 2)}',
+ },
+ ],
+ },
+ ' ',
+ {
+ type: 'service',
+ className: 'inline',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/overview/yarn-cluster`,
+ data: {
+ cluster: cluster,
+ queue: yarnQueue,
+ },
+ // @ts-ignore
+ adaptor: function (payload, response) {
+ let rootUsed = (payload['data']['root']['usedCapacity'] * 100 / payload['data']['root']['capacity'])
+ let targetUsed = (payload['data']['target']['absoluteUsedCapacity'] * 100 / payload['data']['target']['absoluteMaxCapacity'])
+ return {
+ ...payload,
+ data: {
+ ...payload.data,
+ rootUsed: rootUsed,
+ rootUsedColor: color(rootUsed),
+ targetUsed: targetUsed,
+ targetUsedColor: color(targetUsed),
+ },
+ }
+ },
+ },
+ interval: 10000,
+ silentPolling: true,
+ body: [
+ '(',
+ {
+ type: 'tpl',
+ tpl: '${PADSTART(ROUND(rootUsed), 3)}%',
+ },
+ ',',
+ {
+ type: 'tpl',
+ tpl: '${PADSTART(ROUND(targetUsed), 3)}%',
+ },
+ ')',
+ ],
+ },
+ '(',
+ {
+ type: 'tpl',
+ tpl: '${PADSTART(scheduling, 2)}',
+ },
+ ',',
+ {
+ type: 'tpl',
+ tpl: '${PADSTART(running, 3)}',
+ },
+ ')',
+ ],
+ }
+}
+
+const Overview: React.FC = () => {
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ body: [
+ {
+ type: 'service',
+ // language=JavaScript
+ dataProvider: 'const timer = setInterval(() => {\n setData({date: new Date().toLocaleString()})\n}, 1000)\nreturn () => {\n clearInterval(timer)\n}',
+ body: [
+ '当前时间:',
+ {
+ type: 'tpl',
+ className: 'font-bold',
+ tpl: '${date}',
+ },
+ ],
+ },
+ {type: 'divider'},
+ '
表数量 (重点表数量, 普通表数量)',
+ {
+ type: 'service',
+ api: `${commonInfo.baseUrl}/overview`,
+ interval: 60000,
+ silentPolling: true,
+ body: [
+ {
+ type: 'tpl',
+ tpl: '逻辑表:
${PADSTART(table_count, 4)} (${PADSTART(table_focus_count, 4)}, ${PADSTART(table_count - table_focus_count, 4)})',
+ },
+ '
',
+ {
+ type: 'tpl',
+ tpl: '湖底表:
${PADSTART(hudi_count, 4)} (${PADSTART(hudi_focus_count, 4)}, ${PADSTART(hudi_count - hudi_focus_count, 4)})',
+ },
+ '
',
+ {
+ type: 'tpl',
+ tpl: '嗨福表:
${PADSTART(hive_count, 4)} (${PADSTART(hive_focus_count, 4)}, ${PADSTART(hive_count - hive_focus_count, 4)})',
+ },
+ ],
+ },
+ {type: 'divider'},
+ {
+ type: 'service',
+ api: `${commonInfo.baseUrl}/overview/sync_running_status`,
+ interval: 10000,
+ silentPolling: true,
+ body: [
+ {
+ type: 'tpl',
+ tpl: '任务数
${totalJob}',
+ },
+ {
+ className: 'mx-2',
+ type: 'tpl',
+ tpl: '运行中
${PADSTART(runningJob, 3)}',
+ },
+ {
+ type: 'tpl',
+ tpl: '已停止
${PADSTART(unRunningJob, 3)}',
+ },
+ tableDetailDialog('unRunningJob', 'unRunningJobList'),
+ '
',
+ {
+ type: 'tpl',
+ tpl: '总表数
${totalTable}',
+ },
+ {
+ className: 'mx-2',
+ type: 'tpl',
+ tpl: '运行中
${PADSTART(runningTable, 3)}',
+ },
+ {
+ type: 'tpl',
+ tpl: '已停止
${PADSTART(unRunningTable, 3)}',
+ },
+ tableDetailDialog('unRunningTable', 'unRunningTableList'),
+ ],
+ },
+ {type: 'divider'},
+ '
集群 (集群总资源使用,队列资源使用)(调度中任务数,运行中任务数)',
+ overviewYarnJob(commonInfo.clusters.sync_names(), 'Sync', undefined, 'default'),
+ {type: 'divider'},
+ {
+ className: 'my-2',
+ type: 'service',
+ api: `${commonInfo.baseUrl}/overview/queue?queue=compaction-queue-pre`,
+ interval: 10000,
+ silentPolling: true,
+ body: [
+ {
+ type: 'tpl',
+ tpl: '预调度队列:
${size}',
+ },
+ ],
+ },
+ '
集群 压缩队列任务数(集群总资源使用,队列资源使用)(调度中任务数,运行中任务数)',
+ // @ts-ignore
+ ...Object.keys(commonInfo.clusters.compaction).map(name => overviewYarnJob(name, 'Compaction', `compaction-queue-${name}`, commonInfo.clusters.compaction[name])),
+ {type: 'divider'},
+ {
+ type: 'service',
+ api: `${commonInfo.baseUrl}/overview/version`,
+ interval: 10000,
+ silentPolling: true,
+ body: [
+ '版本:',
+ {
+ type: 'tpl',
+ className: 'font-bold',
+ tpl: '${version}',
+ },
+ '
',
+ '
未接收, 未跨天',
+ '
',
+ '重点表:',
+ {
+ type: 'tpl',
+ tpl: '
${PADSTART(unReceive.focus, 3)}',
+ },
+ versionDetailDialog('unReceive.focus', 'unReceive_focus'),
+ ',',
+ {
+ type: 'tpl',
+ tpl: '
${PADSTART(unSchedule.focus, 3)}',
+ },
+ versionDetailDialog('unSchedule.focus', 'unScheduled_focus'),
+ '
',
+ '普通表:',
+ {
+ type: 'tpl',
+ tpl: '
${PADSTART(unReceive.normal, 3)}',
+ },
+ versionDetailDialog('unReceive.normal', 'unReceive_normal'),
+ ',',
+ {
+ type: 'tpl',
+ tpl: '
${PADSTART(unSchedule.normal, 3)}',
+ },
+ versionDetailDialog('unSchedule.normal', 'unScheduled_normal'),
+ ],
+ },
+ {type: 'divider'},
+ {
+ type: 'service',
+ api: `${commonInfo.baseUrl}/overview/schedule_jobs`,
+ interval: 60000,
+ silentPolling: true,
+ body: [
+ '调度策略',
+ {
+ type: 'each',
+ name: 'items',
+ items: {
+ type: 'tpl',
+ tpl: '
${trigger} (${job})
',
+ },
+ },
+ ],
+ },
+ {type: 'divider'},
+ {
+ type: 'crud',
+ title: '监控指标运行进度',
+ api: `${commonInfo.baseUrl}/overview/monitor_progress`,
+ ...crudCommonOptions(),
+ interval: 2000,
+ loadDataOnce: true,
+ columns: [
+ {
+ name: 'name',
+ label: '名称',
+ width: 120,
+ },
+ {
+ name: 'running',
+ label: '状态',
+ type: 'mapping',
+ width: 50,
+ map: {
+ 'true': '运行中',
+ 'false': '未运行',
+ },
+ },
+ {
+ label: '进度',
+ type: 'progress',
+ value: '${ROUND(progress * 100)}',
+ map: 'bg-primary',
+ },
+ ],
+ },
+ ],
+ },
+ )}
+
+ )
+}
+
+export default Overview
\ No newline at end of file
diff --git a/service-web-client/src/pages/Queue.tsx b/service-web-client/src/pages/Queue.tsx
new file mode 100644
index 0000000..f6a48ba
--- /dev/null
+++ b/service-web-client/src/pages/Queue.tsx
@@ -0,0 +1,96 @@
+import {
+ amisRender,
+ commonInfo,
+ copyField,
+ crudCommonOptions,
+ paginationCommonOptions,
+ time,
+ yarnQueueCrud,
+} from '../util/amis.ts'
+
+const queueCrud = (name: string) => {
+ return {
+ type: 'crud',
+ title: name,
+ api: `\${base}/queue/all?name=${name}`,
+ ...crudCommonOptions(),
+ interval: 10000,
+ loadDataOnce: true,
+ perPage: 5,
+ headerToolbar: [
+ 'reload',
+ 'filter-toggler',
+ {
+ type: 'tpl',
+ tpl: '共 ${total|default:0} 个任务',
+ },
+ paginationCommonOptions(false),
+ ],
+ footerToolbar: [],
+ columns: [
+ {
+ name: 'data.flinkJobId',
+ label: '任务 ID',
+ width: 190,
+ ...copyField('data.flinkJobId'),
+ },
+ {
+ name: 'data.alias',
+ label: '别名',
+ className: 'nowrap',
+ ...copyField('data.alias'),
+ },
+ {
+ name: 'data.batch',
+ label: '批次',
+ width: 100,
+ type: 'tpl',
+ tpl: '${data.batch}',
+ },
+ {
+ name: 'priority',
+ label: '优先级',
+ width: 60,
+ align: 'center',
+ type: 'tpl',
+ tpl: '${priority}',
+ },
+ {
+ name: 'data.comment',
+ label: '备注',
+ className: 'nowrap',
+ },
+ {
+ name: 'createTime',
+ label: '任务提交时间',
+ ...time('createTime'),
+ width: 160,
+ fixed: 'right',
+ },
+ ],
+ }
+}
+
+const Queue = () => {
+ let items = []
+ for (let name of Object.keys(commonInfo.clusters.compaction)) {
+ // @ts-ignore
+ items.push(yarnQueueCrud(name, commonInfo.clusters.compaction[name]))
+ items.push(queueCrud(`compaction-queue-${name}`))
+ }
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ body: [
+ queueCrud('compaction-queue-pre'),
+ ...items,
+ ],
+ },
+ )}
+
+ )
+}
+
+export default Queue
\ No newline at end of file
diff --git a/service-web-client/src/pages/Table.tsx b/service-web-client/src/pages/Table.tsx
new file mode 100644
index 0000000..fa78933
--- /dev/null
+++ b/service-web-client/src/pages/Table.tsx
@@ -0,0 +1,256 @@
+import {
+ aliasTextInput,
+ amisRender,
+ commonInfo,
+ compactionStatusMapping,
+ crudCommonOptions,
+ filterableField,
+ flinkJobDialog,
+ flinkJobIdTextInput,
+ hudiTableTypeMapping,
+ mappingField,
+ paginationCommonOptions,
+ runModeMapping,
+ tableMetaDialog,
+ tableRunningStateMapping,
+ timeAndFrom,
+} from '../util/amis.ts'
+
+function Table() {
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ size: 'none',
+ body: [
+ {
+ id: 'table-service',
+ type: 'service',
+ data: {},
+ body: [
+ {
+ type: 'crud',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/table/list`,
+ data: {
+ page: '${page|default:undefined}',
+ count: '${perPage|default:undefined}',
+ order: '${orderBy|default:undefined}',
+ direction: '${orderDir|default:undefined}',
+ search_flink_job_id: '${flinkJobId|default:undefined}',
+ search_alias: '${alias|default:undefined}',
+ filter_hudi_table_type: '${tableMeta\\.hudi\\.targetTableType|default:undefined}',
+ filter_run_mode: '${flinkJob\\.runMode|default:undefined}',
+ filter_compaction_status: '${syncState\\.compactionStatus|default:undefined}',
+ },
+ },
+ ...crudCommonOptions(),
+ // interval: 10000,
+ filter: {
+ title: '表筛选',
+ body: [
+ {
+ type: 'group',
+ body: [
+ {
+ ...flinkJobIdTextInput('58d0da94-1b3c-4234-948d-482ae3425e70'),
+ size: 'lg',
+ },
+ {
+ ...aliasTextInput('58d0da94-1b3c-4234-948d-482ae3425e70'),
+ size: 'lg',
+ },
+ ],
+ },
+ ],
+ actions: [
+ {
+ type: 'submit',
+ level: 'primary',
+ label: '查询',
+ },
+ {
+ type: 'reset',
+ label: '重置',
+ },
+ ],
+ },
+ filterTogglable: true,
+ filterDefaultVisible: true,
+ perPage: 20,
+ headerToolbar: [
+ 'reload',
+ 'filter-toggler',
+ paginationCommonOptions(),
+ ],
+ footerToolbar: [
+ paginationCommonOptions(),
+ ],
+ columns: [
+ {
+ label: 'Flink job id',
+ width: 195,
+ fixed: 'left',
+ type: 'wrapper',
+ size: 'none',
+ body: [
+ {
+ type: 'action',
+ level: 'link',
+ label: '${flinkJobId}',
+ size: 'xs',
+ actionType: 'dialog',
+ tooltip: '查看详情',
+ dialog: flinkJobDialog(),
+ },
+ {
+ type: 'action',
+ level: 'link',
+ label: '',
+ icon: 'fa fa-copy',
+ size: 'xs',
+ actionType: 'copy',
+ content: '${flinkJobId}',
+ tooltip: '复制 ID',
+ },
+ ],
+ },
+ {
+ label: '别名',
+ type: 'wrapper',
+ fixed: 'left',
+ size: 'none',
+ className: 'nowrap',
+ body: [
+ {
+ type: 'action',
+ level: 'link',
+ label: '${tableMeta.alias}',
+ size: 'xs',
+ actionType: 'dialog',
+ tooltip: '查看详情',
+ dialog: tableMetaDialog(),
+ },
+ {
+ type: 'action',
+ level: 'link',
+ label: '',
+ icon: 'fa fa-copy',
+ size: 'xs',
+ actionType: 'copy',
+ content: '${tableMeta.alias}',
+ tooltip: '复制别名',
+ },
+ ],
+ },
+ {
+ name: 'tableMeta.hudi.targetTableType',
+ label: '表类型',
+ width: 60,
+ align: 'center',
+ ...mappingField('tableMeta.hudi.targetTableType', hudiTableTypeMapping),
+ filterable: filterableField(hudiTableTypeMapping, true),
+ },
+ {
+ name: 'flinkJob.runMode',
+ label: '任务类型',
+ width: 60,
+ align: 'center',
+ ...mappingField('flinkJob.runMode', runModeMapping),
+ filterable: filterableField(runModeMapping, true),
+ },
+ {
+ name: 'syncRunning',
+ label: '同步运行状态',
+ align: 'center',
+ ...mappingField('syncRunning', tableRunningStateMapping),
+ className: 'bg-green-50',
+ width: 75,
+ },
+ {
+ name: 'source_start_time',
+ label: '同步启动时间',
+ ...timeAndFrom('syncState.sourceStartTime', 'syncState.sourceStartTimeFromNow', '未启动'),
+ sortable: true,
+ className: 'bg-green-50',
+ },
+ {
+ name: 'source_receive_time',
+ label: '同步接收时间',
+ ...timeAndFrom('syncState.sourceReceiveTime', 'syncState.sourceReceiveTimeFromNow', '无数据'),
+ sortable: true,
+ className: 'bg-green-50',
+ },
+ {
+ name: 'source_checkpoint_time',
+ label: '同步心跳时间',
+ ...timeAndFrom('syncState.sourceCheckpointTime', 'syncState.sourceCheckpointTimeFromNow', '未启动'),
+ sortable: true,
+ className: 'bg-green-50',
+ },
+ {
+ name: 'source_publish_time',
+ label: '源端发布时间',
+ ...timeAndFrom('syncState.sourcePublishTime', 'syncState.sourcePublishTimeFromNow', '无增量'),
+ sortable: true,
+ className: 'bg-green-50',
+ },
+ {
+ name: 'source_operation_time',
+ label: '源端业务时间',
+ ...timeAndFrom('syncState.sourceOperationTime', 'syncState.sourceOperationTimeFromNow', '无增量'),
+ sortable: true,
+ className: 'bg-green-50',
+ },
+ {
+ name: 'compactionRunning',
+ label: '压缩运行状态',
+ align: 'center',
+ ...mappingField('compactionRunning', tableRunningStateMapping),
+ className: 'bg-cyan-50',
+ width: 75,
+ },
+ {
+ name: 'syncState.compactionStatus',
+ label: '压缩状态',
+ width: 60,
+ align: 'center',
+ ...mappingField('syncState.compactionStatus', compactionStatusMapping),
+ filterable: filterableField(compactionStatusMapping, true),
+ className: 'bg-cyan-50',
+ },
+ {
+ name: 'compaction_start_time',
+ label: '压缩启动时间',
+ ...timeAndFrom('syncState.compactionStartTime', 'syncState.compactionStartTimeFromNow'),
+ sortable: true,
+ className: 'bg-cyan-50',
+ },
+ {
+ name: 'compaction_latest_operation_time',
+ label: '压缩业务时间',
+ ...timeAndFrom('syncState.compactionLatestOperationTime', 'syncState.compactionLatestOperationTimeFromNow', '无'),
+ sortable: true,
+ className: 'bg-cyan-50',
+ },
+ {
+ name: 'compaction_finish_time',
+ label: '压缩完成时间',
+ ...timeAndFrom('syncState.compactionFinishTime', 'syncState.compactionFinishTimeFromNow'),
+ sortable: true,
+ className: 'bg-cyan-50',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ )}
+
+ )
+}
+
+export default Table
\ No newline at end of file
diff --git a/service-web-client/src/pages/Task.tsx b/service-web-client/src/pages/Task.tsx
new file mode 100644
index 0000000..1a9a973
--- /dev/null
+++ b/service-web-client/src/pages/Task.tsx
@@ -0,0 +1,195 @@
+import React from 'react'
+import {amisRender, commonInfo, paginationCommonOptions, serviceLogByAppName, yarnCrudColumns} from '../util/amis.ts'
+
+const Task: React.FC = () => {
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ body: [
+ {
+ type: 'form',
+ title: '检索文件',
+ actions: [
+ {
+ type: 'submit',
+ label: '提交任务',
+ actionType: 'ajax',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/task/scan`,
+ data: {
+ key: '${key|default:undefined}',
+ hdfs: '${hdfs|default:undefined}',
+ pulsar: '${pulsar|default:undefined}',
+ topic: '${topic|default:undefined}',
+ mode: '${scan_mode|default:undefined}',
+ fields: '${fields|default:undefined}',
+ },
+ },
+ },
+ ],
+ body: [
+ {
+ name: 'scan_mode',
+ type: 'checkboxes',
+ label: '检索范围',
+ checkAll: true,
+ required: true,
+ value: 'log',
+ options: [
+ {label: '消息队列', value: 'queue'},
+ {label: '日志文件', value: 'log'},
+ {label: '数据文件', value: 'base'},
+ ],
+ },
+ {
+ type: 'input-text',
+ name: 'key',
+ label: '检索字段',
+ required: true,
+ clearable: true,
+ description: '检索带有该字符的记录',
+ },
+ {
+ type: 'input-text',
+ name: 'hdfs',
+ label: 'HDFS路经',
+ requiredOn: '${CONTAINS(scan_mode, \'log\') || CONTAINS(scan_mode, \'base\')}',
+ visibleOn: '${CONTAINS(scan_mode, \'log\') || CONTAINS(scan_mode, \'base\')}',
+ clearable: true,
+ description: '输入表HDFS路径',
+ autoComplete: `${commonInfo.baseUrl}/table/all_hdfs?key=$term`,
+ },
+ {
+ type: 'input-text',
+ name: 'fields',
+ label: '指定字段',
+ visibleOn: '${CONTAINS(scan_mode, \'base\')}',
+ clearable: true,
+ description: '逗号分隔,可以大幅提高parquet文件检索速度,但无法获取指定字段外的字段内容',
+ },
+ {
+ type: 'group',
+ body: [
+ {
+ type: 'input-text',
+ name: 'topic',
+ label: 'Pulsar主题',
+ requiredOn: '${CONTAINS(scan_mode, \'queue\')}',
+ visibleOn: '${CONTAINS(scan_mode, \'queue\')}',
+ clearable: true,
+ description: '输入Pulsar主题',
+ autoComplete: `${commonInfo.baseUrl}/table/all_pulsar_topic?key=$term`,
+ columnRatio: 4,
+ },
+ {
+ type: 'input-text',
+ name: 'pulsar',
+ label: 'Pulsar地址',
+ requiredOn: '${CONTAINS(scan_mode, \'queue\')}',
+ visibleOn: '${CONTAINS(scan_mode, \'queue\')}',
+ clearable: true,
+ description: '输入Pulsar地址',
+ autoComplete: `${commonInfo.baseUrl}/table/all_pulsar?key=$term`,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'form',
+ title: '综合查询',
+ actions: [
+ {
+ type: 'action',
+ label: '总数&最后操作时间',
+ actionType: 'ajax',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/task/table_summary`,
+ data: {
+ hdfs: '${hdfs|default:undefined}',
+ },
+ },
+ },
+ {
+ type: 'action',
+ label: '最后10条记录',
+ actionType: 'ajax',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/task/table_sampling`,
+ data: {
+ hdfs: '${hdfs|default:undefined}',
+ },
+ },
+ },
+ ],
+ body: [
+ {
+ type: 'input-text',
+ name: 'hdfs',
+ label: 'HDFS路经',
+ required: true,
+ clearable: true,
+ description: '输入表HDFS路径',
+ autoComplete: `${commonInfo.baseUrl}/table/all_hdfs?key=$term`,
+ },
+ ],
+ },
+ {
+ type: 'crud',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/yarn/job_list`,
+ data: {
+ clusters: commonInfo.clusters.sync_names(),
+ page: '${page|default:undefined}',
+ count: '${perPage|default:undefined}',
+ order: '${orderBy|default:undefined}',
+ direction: '${orderDir|default:undefined}',
+ filter_state: '${state|default:undefined}',
+ filter_final_status: '${finalStatus|default:undefined}',
+ search_id: '${id|default:undefined}',
+ search_name: 'Service_Task',
+ precise: false,
+ },
+ },
+ affixHeader: false,
+ interval: 10000,
+ syncLocation: false,
+ silentPolling: true,
+ resizable: false,
+ perPage: 10,
+ headerToolbar: [
+ 'reload',
+ {
+ label: '任务管理器日志',
+ type: 'action',
+ tooltip: '打开Grafana日志',
+ onEvent: {
+ click: {
+ actions: [
+ {
+ actionType: 'custom',
+ script: () => window.open(serviceLogByAppName('service-executor-manager'), '_blank'),
+ },
+ ],
+ },
+ },
+ },
+ paginationCommonOptions(),
+ ],
+ footerToolbar: [],
+ columns: yarnCrudColumns(),
+ },
+ ],
+ },
+ )}
+
+ )
+}
+
+export default Task
\ No newline at end of file
diff --git a/service-web-client/src/pages/Tool.tsx b/service-web-client/src/pages/Tool.tsx
new file mode 100644
index 0000000..60be7fe
--- /dev/null
+++ b/service-web-client/src/pages/Tool.tsx
@@ -0,0 +1,316 @@
+import React from 'react'
+import {
+ aliasTextInput,
+ amisRender,
+ commonInfo,
+ crudCommonOptions,
+ flinkJobIdTextInput,
+ formReloadFlinkJobIdTextInputAndAliasTextInput,
+ hdfsDialog,
+ paginationCommonOptions,
+ readOnlyDialogOptions,
+ timelineColumns,
+} from '../util/amis.ts'
+
+const Tool: React.FC = () => {
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ body: [
+ {
+ type: 'panel',
+ title: '乱七八糟小工具',
+ body: [
+ {
+ type: 'action',
+ label: 'SQL日志',
+ actionType: 'dialog',
+ dialog: {
+ title: '日志',
+ ...readOnlyDialogOptions(),
+ size: 'lg',
+ body: {
+ type: 'crud',
+ api: `${commonInfo.baseUrl}/log/query_sql_log`,
+ ...crudCommonOptions(),
+ loadDataOnce: true,
+ perPage: 50,
+ headerToolbar: [
+ 'reload',
+ paginationCommonOptions(undefined, 10),
+ ],
+ footerToolbar: [
+ paginationCommonOptions(undefined, 10),
+ ],
+ columns: [
+ {
+ name: 'sql',
+ label: 'SQL',
+ },
+ {
+ name: 'createTime',
+ label: '执行时间',
+ },
+ ],
+ },
+ },
+ },
+ {
+ type: 'action',
+ label: 'ZK节点',
+ className: 'ml-2',
+ actionType: 'dialog',
+ dialog: {
+ title: '日志',
+ ...readOnlyDialogOptions(),
+ size: 'lg',
+ body: {},
+ },
+ },
+ ],
+ },
+ {
+ type: 'form',
+ title: 'HDFS文件管理器',
+ actions: [
+ {
+ label: '直接下载',
+ type: 'action',
+ onEvent: {
+ click: {
+ actions: [
+ {
+ actionType: 'custom',
+ // @ts-ignore
+ script: (context, action, event) => {
+ let downloadUrl = `${event.data.base}/hudi/hdfs_download?root=${encodeURI(event.data.hdfs)}`
+ window.open(downloadUrl, '_blank')
+ },
+ },
+ ],
+ },
+ },
+ },
+ {
+ type: 'submit',
+ label: '查看',
+ actionType: 'dialog',
+ dialog: hdfsDialog('hdfs'),
+ },
+ ],
+ body: [
+ {
+ type: 'input-text',
+ name: 'hdfs',
+ label: 'HDFS根路经',
+ required: true,
+ clearable: true,
+ description: '输入表HDFS路径',
+ autoComplete: `${commonInfo.baseUrl}/table/all_hdfs?key=$term`,
+ },
+ ],
+ },
+ {
+ type: 'form',
+ title: '查询时间线',
+ actions: [
+ {
+ type: 'submit',
+ label: '查询时间线',
+ actionType: 'dialog',
+ dialog: {
+ title: 'Hudi 表时间线',
+ actions: [],
+ size: 'lg',
+ body: {
+ type: 'crud',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/hudi/timeline/list_hdfs`,
+ data: {
+ page: '${page|default:undefined}',
+ count: '${perPage|default:undefined}',
+ order: '${orderBy|default:undefined}',
+ direction: '${orderDir|default:undefined}',
+ hdfs: '${hdfs|default:undefined}',
+ filter_type: '${type|default:active}',
+ filter_action: '${action|default:undefined}',
+ filter_state: '${state|default:undefined}',
+ },
+ },
+ ...crudCommonOptions(),
+ perPage: 50,
+ headerToolbar: [
+ 'reload',
+ paginationCommonOptions(undefined, 10),
+ ],
+ footerToolbar: [
+ paginationCommonOptions(undefined, 10),
+ ],
+ columns: timelineColumns(),
+ },
+ },
+ },
+ {
+ type: 'submit',
+ label: '查询表结构',
+ actionType: 'dialog',
+ dialog: {
+ title: 'Hudi 表结构',
+ actions: [],
+ size: 'lg',
+ body: {
+ type: 'service',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/hudi/schema`,
+ data: {
+ hdfs: '${hdfs|default:undefined}',
+ },
+ },
+ body: {
+ type: 'page',
+ body: {
+ type: 'json',
+ source: '${detail}',
+ levelExpand: 3,
+ },
+ },
+ },
+ },
+ },
+ ],
+ body: [
+ {
+ type: 'input-text',
+ name: 'hdfs',
+ label: 'HDFS路经',
+ required: true,
+ clearable: true,
+ description: '输入表HDFS路径',
+ autoComplete: `${commonInfo.baseUrl}/table/all_hdfs?key=$term`,
+ },
+ ],
+ },
+ {
+ type: 'form',
+ title: '提交压缩任务',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/schedule/table`,
+ data: {
+ flink_job_id: '${flinkJobId|default:undefined}',
+ alias: '${alias|default:undefined}',
+ recommend: '${recommend === \'undefined\' ? undefined : recommend|default:undefined}',
+ force: '${force === \'undefined\' ? undefined : force|default:undefined}',
+ },
+ },
+ ...formReloadFlinkJobIdTextInputAndAliasTextInput('0fe6a96c-6b6e-4346-b18e-c631c2389f48'),
+ body: [
+ {
+ type: 'group',
+ body: [
+ flinkJobIdTextInput('0fe6a96c-6b6e-4346-b18e-c631c2389f48', true),
+ aliasTextInput('0fe6a96c-6b6e-4346-b18e-c631c2389f48', true),
+ ],
+ },
+ {
+ type: 'group',
+ body: [
+ {
+ name: 'recommend',
+ type: 'radios',
+ label: '优先指定集群',
+ selectFirst: true,
+ options: [
+ {label: '无', value: 'undefined'},
+ ...Object.keys(commonInfo.clusters.compaction)
+ .map(name => {
+ return {label: name, value: name}
+ }),
+ ],
+ },
+ {
+ name: 'force',
+ type: 'radios',
+ label: '强制指定集群',
+ selectFirst: true,
+ options: [
+ {label: '无', value: 'undefined'},
+ ...Object.keys(commonInfo.clusters.compaction)
+ .map(name => {
+ return {label: name, value: name}
+ }),
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'form',
+ title: '批量提交压缩任务',
+ api: {
+ method: 'post',
+ url: `${commonInfo.baseUrl}/schedule/table_batch`,
+ dataType: 'form',
+ },
+ body: [
+ {
+ name: 'lines',
+ type: 'textarea',
+ label: '表信息 (flink_job_id alias\\n)',
+ clearable: true,
+ minRows: 5,
+ maxRows: 5,
+ className: 'no-resize',
+ required: true,
+ },
+ ],
+ },
+ {
+ type: 'form',
+ title: '停止所有压缩任务',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/schedule/stop_all`,
+ data: {
+ flink_job_id: '${flinkJobId|default:undefined}',
+ alias: '${alias|default:undefined}',
+ disable_meta: '${disableMeta|default:undefined}',
+ },
+ },
+ ...formReloadFlinkJobIdTextInputAndAliasTextInput('163e043e-8cee-41fd-b5a4-0442ac682aec'),
+ body: [
+ {
+ type: 'group',
+ body: [
+ {
+ ...flinkJobIdTextInput('163e043e-8cee-41fd-b5a4-0442ac682aec', true),
+ columnRatio: 5,
+ },
+ {
+ ...aliasTextInput('163e043e-8cee-41fd-b5a4-0442ac682aec', true),
+ columnRatio: 5,
+ },
+ {
+ name: 'disableMeta',
+ type: 'checkbox',
+ label: '是否禁用表',
+ option: '表status设为n',
+ columnRatio: 2,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ )}
+
+ )
+}
+
+export default Tool
\ No newline at end of file
diff --git a/service-web-client/src/pages/Version.tsx b/service-web-client/src/pages/Version.tsx
new file mode 100644
index 0000000..afc3a45
--- /dev/null
+++ b/service-web-client/src/pages/Version.tsx
@@ -0,0 +1,185 @@
+import {
+ aliasTextInput,
+ amisRender,
+ commonInfo,
+ crudCommonOptions,
+ filterableField,
+ flinkJobDialog,
+ flinkJobIdTextInput,
+ mappingField,
+ paginationCommonOptions,
+ tableMetaDialog,
+ versionUpdateStateMapping,
+} from '../util/amis.ts'
+
+function Version() {
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ body: [
+ {
+ type: 'crud',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/version_update/list`,
+ data: {
+ page: '${page|default:undefined}',
+ count: '${perPage|default:undefined}',
+ order: '${orderBy|default:undefined}',
+ direction: '${orderDir|default:undefined}',
+ search_flink_job_id: '${flinkJobId|default:undefined}',
+ search_alias: '${alias|default:undefined}',
+ search_version: '${version|default:undefined}',
+ filter_schedules: '${updated|default:undefined}',
+ },
+ },
+ data: {
+ now: '${DATETOSTR(DATEMODIFY(NOW(), -1, \'days\'), \'YYYYMMDD\')}',
+ },
+ ...crudCommonOptions(),
+ interval: 10000,
+ filter: {
+ mode: 'inline',
+ title: '表筛选',
+ body: [
+ {
+ type: 'group',
+ body: [
+ {
+ ...flinkJobIdTextInput('c5cac9d3-844a-4d86-b2c5-0c10f2283667'),
+ size: 'md',
+ },
+ {
+ ...aliasTextInput('c5cac9d3-844a-4d86-b2c5-0c10f2283667'),
+ size: 'md',
+ },
+ {
+ type: 'input-date',
+ name: 'version',
+ label: '版本',
+ clearable: true,
+ placeholder: '通过版本搜索',
+ size: 'md',
+ format: 'YYYYMMDD',
+ inputFormat: 'YYYYMMDD',
+ value: '${now}',
+ },
+ ],
+ },
+ ],
+ actions: [
+ {
+ type: 'submit',
+ level: 'primary',
+ label: '查询',
+ },
+ {
+ type: 'reset',
+ label: '重置',
+ },
+ ],
+ },
+ filterTogglable: true,
+ filterDefaultVisible: true,
+ perPage: 20,
+ headerToolbar: [
+ 'reload',
+ 'filter-toggler',
+ {
+ type: 'tpl',
+ tpl: '共 ${total|default:0} 个表,其中 ${scheduled|default:0} 个表已跨天,${unScheduled|default:0} 个表未跨天',
+ },
+ paginationCommonOptions(),
+ ],
+ footerToolbar: [
+ paginationCommonOptions(),
+ ],
+ columns: [
+ {
+ label: 'Flink job id',
+ width: 195,
+ fixed: 'left',
+ type: 'wrapper',
+ size: 'none',
+ body: [
+ {
+ type: 'action',
+ level: 'link',
+ label: '${flinkJobId}',
+ size: 'xs',
+ actionType: 'dialog',
+ tooltip: '查看详情',
+ dialog: flinkJobDialog(),
+ },
+ {
+ type: 'action',
+ level: 'link',
+ label: '',
+ icon: 'fa fa-copy',
+ size: 'xs',
+ actionType: 'copy',
+ content: '${flinkJobId}',
+ tooltip: '复制 ID',
+ },
+ ],
+ },
+ {
+ label: '别名',
+ type: 'wrapper',
+ fixed: 'left',
+ size: 'none',
+ className: 'nowrap',
+ body: [
+ {
+ type: 'action',
+ level: 'link',
+ label: '${tableMeta.alias}',
+ size: 'xs',
+ actionType: 'dialog',
+ tooltip: '查看详情',
+ dialog: tableMetaDialog(),
+ },
+ {
+ type: 'action',
+ level: 'link',
+ label: '',
+ icon: 'fa fa-copy',
+ size: 'xs',
+ actionType: 'copy',
+ content: '${tableMeta.alias}',
+ tooltip: '复制别名',
+ },
+ ],
+ },
+ {
+ name: 'priority',
+ label: '表优先级',
+ align: 'center',
+ width: 75,
+ sortable: true,
+ }, {
+ name: 'version',
+ label: '版本',
+ align: 'center',
+ width: 75,
+ },
+ {
+ name: 'updated',
+ label: '状态',
+ align: 'center',
+ ...mappingField('updated', versionUpdateStateMapping),
+ filterable: filterableField(versionUpdateStateMapping, true),
+ width: 70,
+ },
+ ],
+ },
+ ],
+ },
+ )}
+
+ )
+}
+
+export default Version
\ No newline at end of file
diff --git a/service-web-client/src/pages/Yarn.tsx b/service-web-client/src/pages/Yarn.tsx
new file mode 100644
index 0000000..6ce9c64
--- /dev/null
+++ b/service-web-client/src/pages/Yarn.tsx
@@ -0,0 +1,122 @@
+import React from 'react'
+import {useParams} from 'react-router'
+import {
+ amisRender,
+ commonInfo,
+ crudCommonOptions,
+ paginationCommonOptions,
+ yarnCrudColumns,
+ yarnQueueCrud,
+} from '../util/amis.ts'
+
+const Yarn: React.FC = () => {
+ const {clusters, queue, search} = useParams()
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ body: [
+ {
+ id: `${clusters}-yarn-service`,
+ name: `${clusters}-yarn-service`,
+ type: 'service',
+ body: [
+ {
+ type: 'tpl',
+ tpl: '集群资源',
+ },
+ yarnQueueCrud(clusters, queue),
+ {
+ type: 'tpl',
+ tpl: '集群任务',
+ // className: 'mb-2 block',
+ },
+ {
+ type: 'crud',
+ api: {
+ method: 'get',
+ url: `${commonInfo.baseUrl}/yarn/job_list`,
+ data: {
+ clusters: `${clusters}`,
+ page: '${page|default:undefined}',
+ count: '${perPage|default:undefined}',
+ order: '${orderBy|default:undefined}',
+ direction: '${orderDir|default:undefined}',
+ filter_state: '${state|default:undefined}',
+ filter_final_status: '${finalStatus|default:undefined}',
+ search_id: '${id|default:undefined}',
+ search_name: '${name|default:undefined}',
+ completion: 'true',
+ },
+ },
+ defaultParams: {
+ name: search,
+ },
+ ...crudCommonOptions(),
+ interval: 10000,
+ filter: {
+ mode: 'inline',
+ title: '任务筛选',
+ body: [
+ {
+ type: 'group',
+ body: [
+ {
+ type: 'input-text',
+ name: 'id',
+ label: 'ID',
+ clearable: true,
+ placeholder: '通过 ID 搜索',
+ size: 'md',
+ },
+ {
+ type: 'input-text',
+ name: 'name',
+ label: '名称',
+ clearable: true,
+ placeholder: '通过名称搜索',
+ size: 'md',
+ },
+ ],
+ },
+ ],
+ actions: [
+ {
+ type: 'submit',
+ level: 'primary',
+ label: '查询',
+ },
+ {
+ type: 'reset',
+ label: '重置',
+ },
+ ],
+ },
+ filterTogglable: true,
+ filterDefaultVisible: false,
+ perPage: 20,
+ headerToolbar: [
+ 'reload',
+ 'filter-toggler',
+ {
+ type: 'tpl',
+ tpl: '共 ${total|default:0} 个任务,其中 ${running|default:0} 个任务运行中,${unRunning|default:0} 个任务处于非运行状态',
+ },
+ paginationCommonOptions(),
+ ],
+ footerToolbar: [
+ paginationCommonOptions(),
+ ],
+ columns: yarnCrudColumns(),
+ },
+ ],
+ },
+ ],
+ },
+ )}
+
+ )
+}
+
+export default Yarn
\ No newline at end of file
diff --git a/service-web-client/src/pages/YarnCluster.tsx b/service-web-client/src/pages/YarnCluster.tsx
new file mode 100644
index 0000000..85e215f
--- /dev/null
+++ b/service-web-client/src/pages/YarnCluster.tsx
@@ -0,0 +1,19 @@
+import React from 'react'
+import {amisRender, commonInfo, yarnQueueCrud} from '../util/amis.ts'
+
+const YarnCluster: React.FC = () => {
+ return (
+
+ {amisRender(
+ {
+ type: 'wrapper',
+ body: [
+ ...Object.keys(commonInfo.clusters.compaction).map(name => yarnQueueCrud(name)),
+ ],
+ },
+ )}
+
+ )
+}
+
+export default YarnCluster
\ No newline at end of file
diff --git a/service-web-client/src/util/amis.ts b/service-web-client/src/util/amis.ts
new file mode 100644
index 0000000..0584098
--- /dev/null
+++ b/service-web-client/src/util/amis.ts
@@ -0,0 +1,2482 @@
+import {attachmentAdpator, makeTranslator, render, type Schema} from 'amis'
+
+import 'amis/lib/themes/antd.css'
+import 'amis/lib/helper.css'
+import 'amis/sdk/iconfont.css'
+import '@fortawesome/fontawesome-free/css/all.min.css'
+import axios from 'axios'
+
+export const commonInfo = {
+ baseUrl: 'http://132.126.207.130:35690/hudi_services/service_web',
+ // baseUrl: '/hudi_services/service_web',
+ clusters: {
+ // hudi同步运行集群和yarn队列名称
+ sync: {
+ 'b12': 'default',
+ },
+ sync_names() {
+ return Object.keys(this.sync).join(',')
+ },
+ // hudi压缩运行集群和yarn队列名称
+ compaction: {
+ 'b12': 'default',
+ 'b1': 'datalake',
+ 'a4': 'ten_iap.datalake',
+ },
+ compaction_names() {
+ return Object.keys(this.compaction).join(',')
+ },
+ },
+ loki: {
+ // grafana链接,用于直接打开grafana日志查看
+ grafanaUrl: 'http://132.126.207.125:35700',
+ // grafana对应hudi使用的loki配置的datasource id
+ hudi: {
+ datasource: 'f648174e-7593-45cf-8fe8-8f8d5cf0fdde',
+ },
+ // grafana对应服务使用的loki配置的datasource id
+ service: {
+ datasource: 'b6fee51c-facd-4261-a0eb-8c69a975fba3',
+ },
+ },
+}
+
+const __ = makeTranslator('zh')
+
+const responseAdaptor = () => (response: any) => {
+ let payload = response.data || {} // blob 下可能会返回内容为空?
+ if (payload.hasOwnProperty('errno')) {
+ payload.status = payload.errno
+ payload.msg = payload.errmsg
+ } else if (payload.hasOwnProperty('no')) {
+ payload.status = payload.no
+ payload.msg = payload.error
+ }
+ return {
+ ...response,
+ data: payload,
+ }
+}
+
+export const amisRender = (schema: Schema) => {
+ return render(
+ schema,
+ {
+ theme: 'antd',
+ },
+ {
+ fetcher: async (api: any) => {
+ let {url, method, data, responseType, config, headers} = api
+ config = config || {}
+ config.url = url
+ config.withCredentials = true
+ responseType && (config.responseType = responseType)
+
+ if (config.cancelExecutor) {
+ config.cancelToken = new (axios as any).CancelToken(
+ config.cancelExecutor,
+ )
+ }
+
+ config.headers = headers || {}
+ config.method = method
+ config.data = data
+
+ if (method === 'get' && data) {
+ config.params = data
+ } else if (data && data instanceof FormData) {
+ // config.headers['Content-Type'] = 'multipart/form-data';
+ } else if (
+ data &&
+ typeof data !== 'string' &&
+ !(data instanceof Blob) &&
+ !(data instanceof ArrayBuffer)
+ ) {
+ data = JSON.stringify(data)
+ config.headers['Content-Type'] = 'application/json'
+ }
+
+ // 支持返回各种报错信息
+ config.validateStatus = function () {
+ return true
+ }
+
+ let response = await axios(config)
+ response = await attachmentAdpator(response, __, api)
+ response = responseAdaptor()(response)
+
+ if (response.status >= 400) {
+ if (response.data) {
+ // 主要用于 raw: 模式下,后端自己校验登录,
+ if (
+ response.status === 401 &&
+ response.data.location &&
+ response.data.location.startsWith('http')
+ ) {
+ location.href = response.data.location.replace(
+ '{{redirect}}',
+ encodeURIComponent(location.href),
+ )
+ return new Promise(() => {
+ })
+ } else if (response.data.msg) {
+ throw new Error(response.data.msg)
+ } else {
+ throw new Error(
+ 'System.requestError' + JSON.stringify(response.data, null, 2),
+ )
+ }
+ } else {
+ throw new Error(
+ `${'System.requestErrorStatus'} ${response.status}`,
+ )
+ }
+ }
+
+ return response
+ },
+ isCancel: (value: any) => (axios as any).isCancel(value),
+ },
+ )
+}
+
+function generateLokiPanel(queries: Array) {
+ return {
+ LWF: {
+ queries: [
+ ...queries.map(item => {
+ let name = item['name']
+ let datasource = item['datasource']
+ let queryMap = item['queryMap']
+ let query = Object.keys(queryMap)
+ .sort()
+ .map(key => `${key}="${queryMap[key]}"`)
+ let match = '\\d{4}-(?P