Files
hudi-service/service-web/client/src/util/amis.tsx

2520 lines
82 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {AlertComponent, attachmentAdpator, makeTranslator, render, type Schema, ToastComponent} 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'
import {isEqual} from 'licia'
export const commonInfo = {
debug: isEqual(import.meta.env.MODE, 'development'),
baseUrl: 'http://132.126.207.130:35690/hudi_services/service_web',
// baseAiUrl: 'http://132.126.207.130:35690/hudi_services/service_ai_web',
baseAiUrl: 'http://localhost:8080',
authorizationHeaders: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
'Content-Type': 'application/json',
},
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, data: Record<any, any> = {}) => {
const theme = 'antd'
const locale = 'zh-CN'
return (
<>
<ToastComponent
theme={theme}
key="toast"
position={'top-right'}
locale={locale}
/>
<AlertComponent theme={theme} key="alert" locale={locale}/>
{render(
schema,
{
data: data,
theme: theme,
},
{
enableAMISDebug: commonInfo.debug,
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<any>) {
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<time>\\d{2}-\\d{2}\\s*\\d{2}:\\d{2}:\\d{2}).+#@#\\s*(?P<content>[\\w\\W]+)'
let format = '{{.time}} [{{.host}}] [{{.level}}] [{{.app}}] {{.content}}'
// language=TEXT
let expression = `{${query.join(',')}}\n| regexp "${match.replaceAll('\\', '\\\\')}"\n| line_format \`${format}\``
return {
refId: name,
expr: expression,
queryType: 'range',
datasource: {
type: 'loki',
uid: datasource,
},
editorMode: 'code',
}
}),
],
range: {
from: 'now-1h',
to: 'now',
},
},
}
}
function generateQuery(name: string, datasource: string, queryMap: any) {
return {
name: name,
datasource: datasource,
queryMap: queryMap,
}
}
function generateLokiUrl(baseUrl: string, queries: Array<any>) {
return `${baseUrl}/explore?panes=${encodeURIComponent(JSON.stringify(generateLokiPanel(queries)))}&schemaVersion=1&orgId=1`
}
function targetHudiSyncLokiUrlByAlias(flinkJobId: number) {
return generateLokiUrl(
commonInfo.loki.grafanaUrl,
[
generateQuery(
'Hudi 运行日志',
commonInfo.loki.hudi.datasource,
{'flink_job_id': flinkJobId},
),
],
)
}
function targetHudiCompactionLokiUrlByAlias(alias: string) {
return generateLokiUrl(
commonInfo.loki.grafanaUrl,
[
generateQuery(
'Hudi 运行日志',
commonInfo.loki.hudi.datasource,
{'alias': alias},
),
],
)
}
function targetYarnApplicationLokiUrlByAppId(applicationId: string) {
return generateLokiUrl(
commonInfo.loki.grafanaUrl,
[
generateQuery(
'Hudi 运行日志',
commonInfo.loki.hudi.datasource,
{'app_id': applicationId},
),
],
)
}
export function serviceLogByAppName(name: string) {
return generateLokiUrl(
commonInfo.loki.grafanaUrl,
[
generateQuery(
'Service 运行日志',
commonInfo.loki.service.datasource,
{'app': name},
),
],
)
}
export function serviceLogByAppNameAndHost(name: string, host: string) {
return generateLokiUrl(
commonInfo.loki.grafanaUrl,
[
generateQuery(
'Service 运行日志',
commonInfo.loki.service.datasource,
{'app': name, 'host': host},
),
],
)
}
export function horizontalFormOptions() {
return {
mode: 'horizontal',
horizontal: {
leftFixed: 'sm'
},
}
}
export function crudCommonOptions() {
return {
affixHeader: false,
stopAutoRefreshWhenModalIsOpen: true,
resizable: false,
syncLocation: false,
silentPolling: true,
columnsTogglable: false,
}
}
export function readOnlyDialogOptions() {
return {
actions: [],
showCloseButton: false,
closeOnEsc: true,
closeOnOutside: true,
disabled: true,
}
}
export function paginationCommonOptions(perPage = true, maxButtons = 5) {
let option = {
type: 'pagination',
layout: [
'pager',
],
maxButtons: maxButtons,
showPageInput: false,
perPageAvailable: [10, 15, 20, 50, 100, 200],
}
if (perPage) {
option.layout.push('perPage')
}
return option
}
export function paginationTemplate(perPage = 20, maxButtons = 5, extraHeaders: Schema[] = [], extraFooters: Schema[] = []) {
return {
perPage: perPage,
headerToolbar: [
'reload',
paginationCommonOptions(true, maxButtons),
...extraHeaders,
],
footerToolbar: [
'statistics',
paginationCommonOptions(true, maxButtons),
...extraFooters,
],
}
}
export function timeAndFrom(field: string, fromNow: string, emptyText = '未停止', showSource = true) {
let tpl = '${IF(' + field + ' === 0, \'(' + emptyText + ')\', CONCATENATE(\'<span class="font-bold">\',' + fromNow + ',\'</span>\'))}'
if (showSource) {
return {
align: 'center',
width: 80,
type: 'tooltip-wrapper',
content: '${DATETOSTR(DATE(' + field + '))}',
inline: true,
className: 'mr-2',
disabledOn: `\${${field} === 0}`,
body: {
type: 'tpl',
tpl: tpl,
},
}
} else {
return {
align: 'center',
width: 140,
type: 'tpl',
tpl: '${DATETOSTR(DATE(' + field + '))}',
}
}
}
export function applicationLogDialog() {
return {
type: 'action',
level: 'link',
actionType: 'dialog',
dialog: {
title: '应用日志',
size: 'xl',
actions: [],
body: [
{
type: 'service',
api: {
method: 'GET',
url: `${commonInfo.baseUrl}/log/query_application_log`,
data: {
application_id: '${id}',
},
},
body: [
{
disabled: true,
type: 'editor',
name: 'detail',
label: '应用日志',
size: 'xxl',
placeholder: '没有内容',
options: {
wordWrap: 'on',
},
},
],
},
],
},
}
}
export function yarnQueueCrud(clusters?: string, queueNames?: string) {
return {
type: 'crud',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/yarn/queue_list`,
data: {
clusters: '${cluster|default:undefined}',
names: '${queueName|default:undefined}',
},
},
affixHeader: false,
defaultParams: {
...(clusters ? {cluster: clusters} : {}),
...(queueNames ? {queueName: queueNames} : {}),
},
interval: 10000,
syncLocation: false,
silentPolling: true,
headerToolbar: [
'reload',
],
columns: [
{
name: 'queueName',
label: '队列名称',
width: 130,
type: 'tooltip-wrapper',
body: '${TRUNCATE(queueName, 20)}',
content: '${queueName}',
inline: true,
},
{
label: '当前容量',
type: 'progress',
value: '${ROUND((absoluteUsedCapacity * 100 / absoluteMaxCapacity), 0)}',
stripe: true,
animate: true,
showLabel: false,
map: [
{
value: 30,
color: '#28a745',
}, {
value: 90,
color: '#007bff',
},
{
value: 100,
color: '#dc3545',
},
],
},
{
label: '进度',
width: 35,
align: 'center',
type: 'tpl',
tpl: '${ROUND((absoluteUsedCapacity * 100 / absoluteMaxCapacity), 0)}%',
},
{
type: 'operation',
label: '操作',
width: 100,
fixed: 'right',
buttons: [
{
label: '详情',
type: 'button',
level: 'link',
tooltip: '查看队列详情',
visibleOn: '${!root}',
actionType: 'dialog',
dialog: {
closeOnEsc: true,
closeOnOutside: true,
size: 'md',
close: true,
title: '队列详情',
body: {
type: 'property',
items: [
{label: 'CPU', content: '${resourcesUsed.vCores}'},
// 有空看看这个值的单位
{label: '内存', content: '${resourcesUsed.memory}', span: 2},
{label: '容量(%)', content: '${capacity}'},
{label: '最大容量(%)', content: '${maxCapacity}'},
{label: '已用容量(%)', content: '${usedCapacity}'},
{label: '绝对容量(%)', content: '${absoluteCapacity}'},
{label: '绝对最大容量(%)', content: '${absoluteMaxCapacity}'},
{label: '绝对已用容量(%)', content: '${absoluteUsedCapacity}'},
{label: '应用数量', content: '${numApplications}', span: 3},
{label: '最大应用数量', content: '${maxApplications}'},
{label: '活跃应用数量', content: '${numActiveApplications}'},
{label: '等待应用数量', content: '${numPendingApplications}'},
{label: '容器数量', content: '${numContainers}', span: 3},
{label: '已分配容器数量', content: '${numApplications}'},
{label: '预留容器数量数量', content: '${numApplications}'},
{label: '等待容器数量', content: '${numApplications}'},
],
},
actions: [],
},
},
{
visibleOn: '${webUrl}',
label: '管理页面',
type: 'action',
level: 'link',
tooltip: '打开管理页面',
actionType: 'url',
url: '${webUrl}',
blank: true,
},
],
},
],
}
}
export function yarnCrudColumns() {
return [
{
label: '名称',
className: 'nowrap',
type: 'tpl',
tpl: '${IF(syncApplication, \'<span class="rounded-xl label label-primary">S</span>\', IF(compactionApplication, \'<span class="rounded-xl label label-primary">C</span>\', IF(taskApplication, \'<span class="rounded-xl label label-primary">T</span>\', \'\')))}${IF(hudiApplication || taskApplication, \'<span class="mx-2"/>\', \'\')}${IF(syncApplication, flinkJobName, IF(compactionApplication, alias, IF(taskApplication, taskName, name)))}',
},
{
name: 'cluster',
label: '集群',
width: 65,
align: 'center',
type: 'tpl',
tpl: '<span class="label label-info">${cluster}</span>',
},
{
label: '用户',
width: 80,
type: 'tooltip-wrapper',
body: '${TRUNCATE(user, 8)}',
content: '${user}',
align: 'center',
},
{
name: 'state',
label: '运行状态',
width: 70,
align: 'center',
type: 'mapping',
canAccessSuperData: false,
map: {
'NEW': '<span class=\'label bg-pink-300\'>创建中</span>',
'NEW_SAVING': '<span class=\'label bg-purple-300\'>已创建</span>',
'SUBMITTED': '<span class=\'label bg-indigo-300\'>已提交</span>',
'ACCEPTED': '<span class=\'label bg-cyan-300\'>调度中</span>',
'RUNNING': '<span class=\'label label-success\'>运行中</span>',
'FINISHED': '<span class=\'label label-default\'>已结束</span>',
'FAILED': '<span class=\'label label-danger\'>已失败</span>',
'KILLED': '<span class=\'label label-warning\'>被停止</span>',
'*': '<span class=\'label bg-gray-300\'>${state}</span>',
},
filterable: {
multiple: true,
options: [
{label: '创建中', value: 'NEW'},
{label: '已创建', value: 'NEW_SAVING'},
{label: '已提交', value: 'SUBMITTED'},
{label: '调度中', value: 'ACCEPTED'},
{label: '运行中', value: 'RUNNING'},
{label: '已结束', value: 'FINISHED'},
{label: '已失败', value: 'FAILED'},
{label: '被停止', value: 'KILLED'},
],
},
},
{
name: 'finalStatus',
label: '最终状态',
width: 70,
align: 'center',
type: 'mapping',
canAccessSuperData: false,
map: {
'UNDEFINED': '<span class=\'label label-info\'>运行</span>',
'SUCCEEDED': '<span class=\'label label-success\'>成功</span>',
'FAILED': '<span class=\'label label-danger\'>失败</span>',
'KILLED': '<span class=\'label label-warning\'>终止</span>',
'ENDED': '<span class=\'label label-default\'>结束</span>',
'*': '<span class=\'label bg-gray-300\'>${state}</span>',
},
filterable: {
multiple: true,
options: [
{label: '运行', value: 'UNDEFINED'},
{label: '成功', value: 'SUCCEEDED'},
{label: '失败', value: 'FAILED'},
{label: '终止', value: 'KILLED'},
{label: '结束', value: 'ENDED'},
],
},
},
{
name: 'startedTime',
label: '启动时间',
...timeAndFrom('startedTime', 'startTimeFromNow'),
sortable: true,
canAccessSuperData: false,
},
{
name: 'finishedTime',
label: '停止时间',
...timeAndFrom('finishedTime', 'finishTimeFromNow'),
sortable: true,
align: 'center',
canAccessSuperData: false,
},
{
type: 'operation',
width: 160,
label: '操作',
fixed: 'right',
className: 'nowrap',
buttons: [
/*{
disabled: true,
label: "停止",
type: "button",
level: "link",
tooltip: '从队列中移除该任务',
actionType: 'dialog',
dialog: {
closeOnEsc: true,
closeOnOutside: true,
size: 'sm',
close: true,
title: '停止确认',
body: '确认停止任务: <br>${name}<br> ?',
actions: [
{
type: 'button',
label: '取消',
actionType: 'cancel'
},
{
type: 'button',
label: '确认移除',
level: 'danger',
actionType: 'ajax',
api: '',
primary: true,
message: {
success: '操作成功',
failed: '操作失败'
}
}
]
}
},*/
{
visibleOn: 'flinkJobId',
label: 'FID',
type: 'action',
actionType: 'copy',
content: '${flinkJobId}',
level: 'link',
tooltip: '${flinkJobId}',
},
{
visibleOn: 'alias',
label: 'ALIAS',
type: 'action',
actionType: 'copy',
content: '${alias}',
level: 'link',
tooltip: '${alias}',
},
{
disabledOn: '${finalStatus != \'SUCCEEDED\' || state != \'FINISHED\'}',
disabledTip: '无结果',
visibleOn: 'taskId',
label: '结果',
level: 'link',
type: 'action',
actionType: 'dialog',
dialog: {
title: '结果',
...readOnlyDialogOptions(),
size: 'xl',
body: [
{
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/task/results`,
data: {
task_id: '${taskId|default:undefined}',
},
},
body: {
type: 'editor',
disabled: true,
name: 'text',
options: {
wordWrap: 'on',
fontFamily: 'monospace',
},
},
},
],
},
},
{
label: 'ID',
type: 'action',
actionType: 'copy',
content: '${id}',
level: 'link',
tooltip: '${id}',
},
{
disabledOn: '${trackingUrl == null}',
disabledTip: '无应用页面',
label: '页面',
type: 'action',
level: 'link',
tooltip: '打开应用页面: ${trackingUrl}',
actionType: 'url',
url: '${trackingUrl}',
blank: true,
},
{
disabledOn: '${id == null || !(hudiApplication | taskApplication)}',
disabledTip: '找不到id或非hudi服务管理的任务',
label: '日志',
type: 'action',
level: 'link',
tooltip: '打开Grafana日志',
onEvent: {
click: {
actions: [
{
actionType: 'custom',
// @ts-ignore
script: (context, doAction, event) => {
let isSyncApplication = context.props.data?.syncApplication ?? false
let isCompactionApplication = context.props.data?.compactionApplication ?? false
let flinkJobId = context.props.data?.flinkJobId ?? 0
let alias = context.props.data?.alias ?? ''
let appId = context.props.data?.id ?? ''
let url = ''
if (isSyncApplication) {
url = targetHudiSyncLokiUrlByAlias(flinkJobId)
} else if (isCompactionApplication) {
url = targetHudiCompactionLokiUrlByAlias(alias)
} else {
url = targetYarnApplicationLokiUrlByAppId(appId)
}
window.open(url, '_blank')
},
},
],
},
},
},
],
},
]
}
export function simpleYarnDialog(cluster: string, title: string, filterField: string) {
return {
title: title,
actions: [],
data: {
base: '${base}',
name: `\${${filterField}}`,
flinkJob: '${flinkJob}',
tableMeta: '${tableMeta}',
},
size: 'xl',
body: [
{
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/yarn/job_current`,
data: {
clusters: `${cluster}`,
name: '${name}',
},
},
silentPolling: false,
body: [
{
type: 'wrapper',
size: 'none',
visibleOn: '${hasCurrent}',
body: [
{
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/flink/overview`,
data: {
url: '${current.trackingUrl}',
},
},
silentPolling: false,
body: [
{
type: 'property',
title: 'Flink 基本信息',
column: 4,
items: [
{label: 'Flink 版本', content: '${flinkVersion}'},
{label: 'Flink 小版本', content: '${flinkCommit}', span: 3},
{label: '运行中', content: '${jobsRunning}'},
{label: '已结束', content: '${jobsFinished}'},
{label: '已失败', content: '${jobsFailed}'},
{label: '被取消', content: '${jobsCanceled}'},
{
label: 'Slot (可用/总数)',
content: '${slotsAvailable}/${slotsTotal}',
span: 4,
},
],
},
],
},
{
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/flink/jobs`,
data: {
url: '${current.trackingUrl}',
schema: '${tableMeta.schema}',
table: '${tableMeta.table}',
mode: '${flinkJob.runMode}',
},
},
silentPolling: false,
body: [
{
type: 'table',
title: '任务详情',
source: '${items}',
affixHeader: false,
columns: [
{
name: 'name',
label: '名称',
width: 2000,
},
{
label: 'Checkpoint',
width: 60,
align: 'center',
fixed: 'right',
type: 'tpl',
tpl: '${IF(checkpointMetrics, checkpointMetrics.complete + \'/\' + checkpointMetrics.inProgress +\'/\' + checkpointMetrics.failed, \'-\')}',
},
{
name: 'metrics.readRecords',
label: '读记录数',
width: 60,
align: 'center',
fixed: 'right',
},
{
name: 'metrics.writeRecords',
label: '写记录数',
width: 60,
align: 'center',
fixed: 'right',
},
{
label: '操作',
width: 60,
align: 'center',
fixed: 'right',
type: 'wrapper',
size: 'none',
body: [
{
disabled: true,
type: 'button',
label: '详情',
level: 'link',
size: 'xs',
actionType: 'url',
blank: true,
url: '${page}',
},
],
},
],
},
],
},
],
},
{
type: 'tpl',
tpl: '没有正在运行的任务',
visibleOn: '${!hasCurrent}',
},
],
},
{type: 'divider'},
{
type: 'crud',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/yarn/job_list`,
data: {
clusters: `${cluster}`,
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}',
precise: true,
},
},
affixHeader: false,
interval: 10000,
syncLocation: false,
silentPolling: true,
resizable: false,
perPage: 10,
headerToolbar: [
'reload',
paginationCommonOptions(),
],
footerToolbar: [],
columns: yarnCrudColumns(),
},
],
}
}
export function hdfsDialog(hdfsField: string) {
return {
title: {
type: 'tpl',
tpl: `\${${hdfsField}}`,
},
...readOnlyDialogOptions(),
size: 'xl',
body: {
type: 'crud',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/hudi/hdfs_list`,
data: {
root: `\${${hdfsField}|default:undefined}`,
},
},
deferApi: {
method: 'get',
url: `${commonInfo.baseUrl}/hudi/hdfs_list_children`,
data: {
root: '${path|default:undefined}',
},
},
...crudCommonOptions(),
perPage: 10,
headerToolbar: [
'reload',
paginationCommonOptions(undefined, 10),
],
footerToolbar: [
paginationCommonOptions(undefined, 10),
],
columns: [
{
name: 'name',
label: '文件名',
className: 'font-mono',
},
{
name: 'group',
label: '组',
width: 70,
},
{
name: 'owner',
label: '用户',
width: 70,
},
{
label: '大小',
width: 90,
type: 'tpl',
tpl: '${sizeText}',
},
{
label: '修改时间',
width: 140,
type: 'tpl',
tpl: '${DATETOSTR(DATE(modifyTime), \'YYYY-MM-DD HH:mm:ss\')}',
},
{
type: 'operation',
label: '操作',
width: 160,
fixed: 'right',
buttons: [
{
label: '完整路径',
type: 'action',
level: 'link',
size: 'xs',
actionType: 'copy',
content: '${path}',
tooltip: '复制 ${path}',
},
{
disabledOn: 'folder || size > 1048576',
label: '查看',
type: 'action',
level: 'link',
size: 'xs',
actionType: 'dialog',
dialog: {
title: {
type: 'tpl',
tpl: '文件内容:${path}',
},
size: 'md',
...readOnlyDialogOptions(),
body: {
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/hudi/hdfs_read`,
data: {
root: '${path|default:undefined}',
},
},
body: {
type: 'textarea',
name: 'text',
readOnly: true,
},
},
},
},
{
disabledOn: 'folder',
label: '下载',
type: 'action',
level: 'link',
size: 'xs',
onEvent: {
click: {
actions: [
{
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
let downloadUrl = `${event.data.base}/hudi/hdfs_download?root=${encodeURI(event.data.path)}`
window.open(downloadUrl, '_blank')
},
},
],
},
},
},
],
},
],
},
}
}
export function copyField(field: string, tips = '复制', ignoreLength = 0) {
let tpl = ignoreLength === 0 ? `\${${field}}` : `\${TRUNCATE(${field}, ${ignoreLength})}`
return {
type: 'wrapper',
size: 'none',
body: [
{
type: 'tpl',
className: 'mr-1',
tpl: tpl,
},
{
type: 'action',
level: 'link',
label: '',
icon: 'fa fa-copy',
size: 'xs',
actionType: 'copy',
content: `\$${field}`,
tooltip: `${tips}`,
},
],
}
}
export function flinkJobProperty(id: string, name: string, runMode: string, tags: string) {
return {
type: 'property',
title: 'Flink Job 配置',
items: [
{label: 'ID', content: copyField(id, '复制 ID')},
{label: '任务名称', content: copyField(name, '复制任务名')},
{
label: '任务模式',
content: {
...mappingField(`${runMode}`, runModeMapping),
},
},
{
label: '标签',
content: {
type: 'each',
source: `\${SPLIT(${tags}, ",")}`,
items: mappingField('item', tagsMapping),
},
span: 3,
},
],
}
}
export function runMetaProperty(runMode: string) {
return {
type: 'property',
title: `${runMode} 运行时信息`,
items: [
{label: '运行集群', content: `\${${runMode}Runtime.cluster}`},
{label: '运行主机', content: copyField(`${runMode}Runtime.host`)},
{label: '进程', content: copyField(`${runMode}Runtime.jvmPid`)},
{label: '任务 ID', content: copyField(`${runMode}Runtime.applicationId`), span: 2},
{label: 'Jar 版本', content: `\${${runMode}Runtime.executorVersion}`},
{label: '任务名称', content: `\${${runMode}Runtime.flinkJobName}`},
{label: '容器 ID', content: `\${${runMode}Runtime.containerId}`, span: 2},
{label: '容器路径', content: copyField(`${runMode}Runtime.containerPath`, undefined, 120), span: 3},
],
}
}
export function statisticsProperty(title: string, statistic: string) {
return {
type: 'property',
title: title,
column: 3,
items: [
{label: '扫描总时间', content: `\${${statistic}.totalScanTime}`},
{label: '压缩日志文件总数', content: `\${${statistic}.totalLogFilesCompacted}`},
{label: '压缩日志文件大小', content: `\${${statistic}.totalLogFilesSize}`},
{label: '删除记录数', content: `\${${statistic}.totalRecordsDeleted}`},
{label: '更新记录数', content: `\${${statistic}.totalRecordsUpdated}`},
{label: '压缩日志数', content: `\${${statistic}.totalRecordsCompacted}`},
],
}
}
export function flinkJobDialog() {
return {
title: 'Flink job 详情',
actions: [],
closeOnEsc: true,
closeOnOutside: true,
showCloseButton: false,
size: 'md',
body: [
flinkJobProperty('flinkJobId', 'flinkJob.name', 'flinkJob.runMode', 'flinkJob.tags'),
{type: 'divider'},
{
type: 'action',
label: '打开同步详情',
actionType: 'dialog',
dialog: simpleYarnDialog(commonInfo.clusters.sync_names(), '同步详情', 'syncJobName'),
},
{type: 'divider'},
{
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/table/list_metas`,
data: {
flink_job_id: '${flinkJobId}',
},
},
canAccessSuperData: true,
body: [
{
type: 'table',
title: '包含 Hudi 同步表',
source: '${items}',
columns: [
{
label: '别名',
type: 'wrapper',
size: 'none',
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: '复制别名',
},
],
},
{
label: '并行度',
name: 'tableMeta.hudi.writeTasks',
align: 'center',
},
{
label: 'Bucket 数量',
name: 'tableMeta.hudi.bucketIndexNumber',
align: 'center',
},
{
label: '写并行度',
name: 'tableMeta.hudi.writeTasks',
align: 'center',
},
{
label: '压缩并行度',
name: 'tableMeta.hudi.compactionTasks',
align: 'center',
},
],
},
],
},
],
}
}
export function timelineColumns() {
return [
{
name: 'timestamp',
label: '时间点',
width: 150,
sortable: true,
},
{
name: 'action',
label: '类型',
width: 100,
...mappingField('action', hudiTimelineActionMapping),
filterable: filterableField(hudiTimelineActionMapping, false),
},
{
name: 'state',
label: ' 状态',
width: 80,
align: 'center',
...mappingField('state', hudiTimelineStateMapping),
filterable: filterableField(hudiTimelineStateMapping, true),
},
{
name: 'fileTime',
label: ' 文件时间',
width: 150,
align: 'center',
type: 'tpl',
tpl: '${DATETOSTR(DATE(fileTime), \'YYYY-MM-DD HH:mm:ss\')}',
},
{
name: 'fileName',
label: '文件名',
type: 'wrapper',
size: 'none',
className: 'nowrap',
body: [
{
type: 'tpl',
tpl: '${fileName}',
},
{
visibleOn: 'action === \'compaction\'',
type: 'action',
icon: 'fa fa-eye ml-1',
level: 'link',
tooltip: '查看压缩计划',
size: 'xs',
actionType: 'dialog',
dialog: {
title: '压缩计划详情',
actions: [],
size: 'lg',
body: {
type: 'crud',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/hudi/read_compaction_plan`,
data: {
hdfs: '${hdfs|default:undefined}',
flink_job_id: '${flinkJobId|default:undefined}',
alias: '${tableMeta.alias|default:undefined}',
instant: '${timestamp|default:undefined}',
},
// @ts-ignore
adaptor: (payload, response) => {
return {
items: (payload['data']['operations'] ? payload['data']['operations'] : [])
// @ts-ignore
.map(operation => {
if (operation['deltaFilePaths']) {
operation.deltaFilePaths = operation.deltaFilePaths
// @ts-ignore
.map(p => {
return {
path: p,
}
})
}
return operation
}),
}
},
},
...crudCommonOptions(),
loadDataOnce: true,
...paginationTemplate(20, 8),
columns: [
{
name: 'fileId',
label: '文件 ID',
searchable: true,
},
{
name: 'baseInstantTime',
label: '版本时间点',
width: 120,
align: 'center',
},
{
name: 'partitionPath',
label: '分区',
width: 50,
align: 'center',
},
{
label: '读/写(MB)/数',
type: 'tpl',
tpl: '${metrics[\'TOTAL_IO_READ_MB\']} / ${metrics[\'TOTAL_IO_WRITE_MB\']} / ${metrics[\'TOTAL_LOG_FILES\']}',
align: 'center',
width: 90,
},
{
type: 'operation',
label: '操作',
width: 150,
buttons: [
{
label: '数据文件名',
type: 'action',
level: 'link',
size: 'xs',
actionType: 'copy',
content: '${dataFilePath}',
tooltip: '复制 ${dataFilePath}',
},
{
label: '日志文件',
type: 'action',
level: 'link',
size: 'xs',
actionType: 'dialog',
dialog: {
title: '操作日志文件列表',
size: 'md',
...readOnlyDialogOptions(),
body: {
type: 'crud',
source: '${deltaFilePaths}',
mode: 'list',
...crudCommonOptions(),
loadDataOnce: true,
title: null,
listItem: {
title: '${path}',
},
},
},
},
],
},
],
},
},
},
{
visibleOn: 'action === \'rollback\'',
type: 'action',
icon: 'fa fa-eye ml-1',
level: 'link',
tooltip: '查看回滚计划',
size: 'xs',
actionType: 'dialog',
dialog: {
title: '回滚计划详情',
actions: [],
size: 'lg',
body: {
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/hudi/read_rollback_plan`,
data: {
hdfs: '${hdfs|default:undefined}',
flink_job_id: '${flinkJobId|default:undefined}',
alias: '${tableMeta.alias|default:undefined}',
instant: '${timestamp|default:undefined}',
},
},
body: [
{
type: 'property',
title: '回滚目标',
items: [
{
label: 'Action',
content: {
...mappingField('instantToRollback.action', hudiTimelineActionMapping),
},
},
{label: '时间点', content: '${instantToRollback.commitTime}', span: 2},
],
},
{
type: 'crud',
source: '${rollbackRequests}',
...crudCommonOptions(),
columns: [
{
name: 'fileId',
label: '文件 ID',
searchable: true,
},
{
name: 'partitionPath',
label: '分区',
width: 50,
},
{
name: 'latestBaseInstant',
label: '数据文件版本',
},
],
},
],
},
},
},
{
visibleOn: 'action === \'clean\'',
type: 'action',
icon: 'fa fa-eye ml-1',
level: 'link',
tooltip: '查看清理计划',
size: 'xs',
actionType: 'dialog',
dialog: {
title: '清理计划详情',
actions: [],
size: 'xl',
body: {
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/hudi/read_cleaner_plan`,
data: {
hdfs: '${hdfs|default:undefined}',
flink_job_id: '${flinkJobId|default:undefined}',
alias: '${tableMeta.alias|default:undefined}',
instant: '${timestamp|default:undefined}',
},
// @ts-ignore
adaptor: (payload, response) => {
if (payload.data['filePathsToBeDeletedPerPartition']) {
let map = payload.data['filePathsToBeDeletedPerPartition']
let list: Array<Record<string, any>> = []
Object.keys(map)
.forEach(key => {
// @ts-ignore
map[key].forEach(file => list.push({
partitionPath: key,
file: file['filePath'],
}))
})
payload.data['filePathsToBeDeletedPerPartition'] = list
}
return payload
},
},
body: [
{
type: 'property',
title: '最早回滚时间点',
items: [
{label: '策略', content: '${policy}', span: 3},
{
label: '操作',
content: {
...mappingField('earliestInstantToRetain.action', hudiTimelineActionMapping),
},
},
{
label: '状态',
content: {
...mappingField('earliestInstantToRetain.state', hudiTimelineStateMapping),
},
},
{label: '时间点', content: '${earliestInstantToRetain.timestamp}'},
],
},
{
type: 'crud',
source: '${filePathsToBeDeletedPerPartition}',
...crudCommonOptions(),
...paginationTemplate(20, 8),
loadDataOnce: true,
title: '分区删除文件',
columns: [
{
name: 'partitionPath',
label: '分区',
width: 50,
align: 'center',
},
{
name: 'file',
label: '清理文件',
className: 'nowrap',
},
{
type: 'operation',
label: '操作',
width: 100,
buttons: [
{
label: '复制路经',
type: 'action',
level: 'link',
size: 'xs',
actionType: 'copy',
content: '${file}',
tooltip: '复制 ${file}',
},
],
},
],
},
],
},
},
},
],
},
{
name: 'type',
label: '来源',
width: 60,
align: 'center',
...mappingField('type', hudiTimelineTypeMapping),
filterable: filterableField(hudiTimelineTypeMapping, true),
},
]
}
export function tableMetaDialog() {
return {
title: 'Table 详情',
actions: [],
closeOnEsc: true,
closeOnOutside: true,
showCloseButton: false,
size: 'lg',
body: [
{
type: 'wrapper',
size: 'none',
body: [
{
type: 'tpl',
className: 'block font-bold mb-2',
tpl: '常用操作',
},
{
type: 'button-group',
tiled: false,
buttons: [
{
label: '同步情况',
type: 'button',
icon: 'fa fa-arrows-rotate',
actionType: 'dialog',
dialog: simpleYarnDialog(commonInfo.clusters.sync_names(), '同步详情', 'syncJobName'),
},
{
label: '压缩情况',
type: 'action',
icon: 'fa fa-minimize',
actionType: 'dialog',
dialog: simpleYarnDialog(commonInfo.clusters.compaction_names(), '压缩详情', 'compactionJobName'),
},
{
label: '历史压缩',
type: 'action',
icon: 'fa fa-list',
actionType: 'dialog',
dialog: {
title: 'Hudi 表时间线',
actions: [],
size: 'lg',
body: {
type: 'crud',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/table/list_compaction_metrics`,
data: {
page: '${page|default:undefined}',
count: '${perPage|default:undefined}',
order: '${orderBy|default:update_time}',
direction: '${orderDir|default:DESC}',
search_flink_job_id: '${flinkJobId|default:undefined}',
search_alias: '${tableMeta.alias|default:undefined}',
filter_completes: '${complete|default:undefined}',
},
defaultParams: {
filter_type: 'active',
},
},
...crudCommonOptions(),
perPage: 15,
headerToolbar: [
'reload',
paginationCommonOptions(),
],
footerToolbar: [
paginationCommonOptions(),
],
columns: [
{
name: 'compactionPlanInstant',
label: '压缩时间点',
width: 180,
...copyField('compactionPlanInstant'),
},
{
name: 'cluster',
label: '集群',
width: 65,
align: 'center',
type: 'tpl',
tpl: '<span class="label label-info">${cluster}</span>',
},
{
name: 'applicationId',
label: '应用',
...copyField('applicationId'),
},
{
name: 'complete',
label: '状态',
...mappingField('complete', compactionMetricsStateMapping),
filterable: filterableField(compactionMetricsStateMapping, false),
},
{
name: 'startedTime',
label: '启动时间',
...timeAndFrom('startedTime', 'startedTimeFromNow'),
sortable: true,
canAccessSuperData: false,
},
{
name: 'finishedTime',
label: '停止时间',
...timeAndFrom('finishedTime', 'finishedTimeFromNow'),
sortable: true,
align: 'center',
canAccessSuperData: false,
},
{
type: 'operation',
width: 50,
label: '操作',
fixed: 'right',
className: 'nowrap',
buttons: [
{
label: '详情',
type: 'action',
level: 'link',
actionType: 'dialog',
dialog: {
title: '压缩详情',
size: 'xl',
actions: [],
closeOnEsc: true,
closeOnOutside: true,
showCloseButton: false,
body: [
statisticsProperty('压缩预扫描', 'before'),
{type: 'divider'},
statisticsProperty('压缩成果', 'after'),
],
},
},
],
},
],
},
},
},
{
type: 'button',
label: '时间线',
icon: 'fa fa-timeline',
actionType: 'dialog',
dialog: {
title: 'Hudi 表时间线',
actions: [],
size: 'lg',
body: {
type: 'crud',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/hudi/timeline/list`,
data: {
page: '${page|default:undefined}',
count: '${perPage|default:undefined}',
order: '${orderBy|default:undefined}',
direction: '${orderDir|default:undefined}',
flink_job_id: '${flinkJobId|default:undefined}',
alias: '${tableMeta.alias|default:undefined}',
filter_type: '${type|default:active}',
filter_action: '${action|default:undefined}',
filter_state: '${state|default:undefined}',
},
},
...crudCommonOptions(),
perPage: 15,
headerToolbar: [
'reload',
paginationCommonOptions(),
],
footerToolbar: [
paginationCommonOptions(),
],
columns: timelineColumns(),
},
},
},
{
type: 'button',
label: '数据目录',
icon: 'fa fa-folder',
actionType: 'dialog',
dialog: hdfsDialog('tableMeta.hudi.targetHdfsPath'),
},
{
type: 'button',
label: 'Pulsar 队列',
icon: 'fa fa-message',
actionType: 'dialog',
dialog: {
title: '队列详情',
actions: [],
size: 'xl',
body: {
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/pulsar/topic`,
data: {
pulsar_url: '${tableMeta.pulsarAddress|default:undefined}',
topic: '${tableMeta.topic|default:undefined}',
},
},
body: [
{
type: 'property',
title: '基本信息',
items: [
{label: 'Topic', content: copyField('name'), span: 2},
{label: '最末位移', content: copyField('lastMessageId')},
],
},
{type: 'divider'},
{
type: 'property',
title: '指标信息',
column: 4,
items: [
{label: '入队列消息速率', content: '${state.messageRateIn}'},
{
label: '出队列消息速率',
content: '${state.messageRateOut}',
},
{
label: '入队列消息吞吐量',
content: '${state.messageThroughputIn}',
},
{
label: '出队列消息吞吐量',
content: '${state.messageThroughputOut}',
},
{
label: '入队列消息数量',
content: '${state.messageInCounter}',
},
{
label: '出队列消息数量',
content: '${state.messageOutCounter}',
},
{
label: '入队列消息字节数',
content: '${state.byteInCounter}',
},
{
label: '出队列消息字节数',
content: '${state.byteOutCounter}',
},
{label: '存储消息大小', content: '${state.storageSize}'},
{label: '积压消息大小', content: '${state.backlogSize}'},
{
label: '平均消息大小',
content: '${state.averageMessageSize}',
},
],
},
{type: 'divider'},
{
type: 'table',
title: '消费者们',
source: '${subscriptionStateVOS}',
itemAction: {
type: 'action',
actionType: 'dialog',
dialog: {
title: '详情',
closeOnEsc: true,
closeOnOutside: true,
showCloseButton: false,
actions: [],
size: 'md',
body: [
{
type: 'property',
column: 1,
items: [
{
label: '在此订阅上传递的消息的总速率(msg/s)',
content: '${messageRateOut}',
},
{
label: '此订阅提供的总吞吐量(字节/秒)',
content: '${messageThroughputOut}',
},
{
label: '传送给消费者的总字节数(字节)',
content: '${bytesOutCounter}',
},
{
label: '传递给消费者的消息总数(msg)',
content: '${messageOutCounter}',
},
{
label: '此订阅上重新传递的消息的总速率(msg/s)',
content: '${messageRateRedeliver}',
},
{
label: '分块消息调度速率',
content: '${chunkedMessageRate}',
},
{
label: '积压的大小(字节)',
content: '${backlogSize}',
},
{
label: '积压中不包含延迟消息的消息数',
content: '${messageBacklogNoDelayed}',
},
{
label: '验证订阅是否由于达到未确认消息的阈值而被阻止',
content: '${blockedSubscriptionOnUnackedMessages}',
},
{
label: '当前正在跟踪的延迟消息数',
content: '${messageDelayed}',
},
{
label: '订阅的未确认消息数',
content: '${unackedMessages}',
},
{
label: '单个活跃消费者订阅处于活跃状态的消费者名称(故障转移、独占)',
content: '${activeConsumerName}',
},
{
label: '此订阅上过期的消息总速率(msg/s)',
content: '${messageRateExpired}',
},
{
label: '此订阅上过期的消息总数',
content: '${totalMessageExpired}',
},
{
label: '最后一条消息过期时间',
content: {
type: 'tpl',
tpl: '${lastExpireTimestamp|date:YYYY-MM-DD HH\\:mm\\:ss:x}',
},
},
{
label: '上次接收的消费流命令时间',
content: {
type: 'tpl',
tpl: '${lastConsumedFlowTimestamp|date:YYYY-MM-DD HH\\:mm\\:ss:x}',
},
},
{
label: '上次消费消息时间',
content: {
type: 'tpl',
tpl: '${lastConsumedTimestamp|date:YYYY-MM-DD HH\\:mm\\:ss:x}',
},
},
{
label: '上次确认消息时间',
content: {
type: 'tpl',
tpl: '${lastAckedTimestamp|date:YYYY-MM-DD HH\\:mm\\:ss:x}',
},
},
{
label: '此订阅是持久订阅还是临时订阅',
content: '${durable}',
},
{
label: '标记订阅状态在不同区域之间保持同步',
content: '${replicated}',
},
],
},
],
},
},
columns: [
{
name: 'name',
label: '订阅名称',
...copyField('name'),
},
{
name: 'type',
label: '订阅类型',
fixed: 'right',
...mappingField('type', subscriptionTypeMapping),
},
{name: 'messageBacklog', label: '积压', fixed: 'right'},
],
},
{type: 'divider'},
{
type: 'table',
title: '生产者们',
source: '${state.publishers}',
itemAction: {
type: 'action',
actionType: 'dialog',
dialog: {
title: '详情',
closeOnEsc: true,
closeOnOutside: true,
showCloseButton: false,
actions: [],
size: 'md',
body: [
{
type: 'property',
column: 1,
items: [
{
label: '发布消息速率(msg/s)',
content: '${messageRateIn}',
},
{
label: '发布消息吞吐量(字节/秒)',
content: '${messageThroughputIn}',
},
{
label: '消息平均大小(字节)',
content: '${averageMessageSize}',
},
{
label: '接收到的分块消息总数(msg)',
content: '${chunkedMessageRate}',
},
{label: '生产者地址', content: '${address}'},
{label: '客户端版本', content: '${clientVersion}'},
],
},
],
},
},
columns: [
{
name: 'producerId',
label: 'ID',
width: 50,
},
{
name: 'producerName',
label: '名称',
...copyField('producerName'),
},
{
name: 'connectedSince',
label: '连接时间',
type: 'tpl',
tpl: '${connectedSince}',
},
{
name: 'accessMode',
label: '发布类型',
fixed: 'right',
...mappingField('accessMode', publishTypeMapping),
},
],
},
],
},
},
},
{
type: 'button',
label: 'Hudi 表结构',
icon: 'fa fa-table',
actionType: 'dialog',
dialog: {
title: 'Hudi 表结构',
actions: [],
body: {
type: 'service',
api: {
method: 'get',
url: `${commonInfo.baseUrl}/hudi/schema`,
data: {
flink_job_id: '${flinkJobId|default:undefined}',
alias: '${tableMeta.alias|default:undefined}',
},
},
body: {
type: 'page',
body: {
type: 'json',
source: '${detail}',
levelExpand: 3,
},
},
},
},
},
],
},
],
},
{type: 'divider', visibleOn: '${syncRuntime}'},
{
visibleOn: '${syncRuntime}',
...runMetaProperty('sync'),
},
{type: 'divider', visibleOn: '${compactionRuntime}'},
{
visibleOn: '${compactionRuntime}',
...runMetaProperty('compaction'),
},
{type: 'divider'},
flinkJobProperty('flinkJobId', 'flinkJob.name', 'flinkJob.runMode', 'flinkJob.tags'),
{type: 'divider'},
{
type: 'property',
title: 'Table 配置',
items: [
{label: '别名', content: copyField('tableMeta.alias', '复制别名')},
{
label: '分片表',
content: '${IF(tableMeta.type === \'sharding\', \'是\', \'否\')}',
},
{label: '分区字段', content: copyField('tableMeta.partitionField', '复制分区字段')},
{label: '源端', content: copyField('tableMeta.source', '复制源端')},
{label: '源库名', content: copyField('tableMeta.schema', '复制库名')},
{label: '源表名', content: copyField('tableMeta.table', '复制表名')},
{label: '源表类型', content: '${tableMeta.sourceType}'},
{label: '优先级', content: '${tableMeta.priority}', span: 2},
{
label: '标签',
content: {
type: 'each',
source: '${SPLIT(tableMeta.tags, ",")}',
items: mappingField('item', tagsMapping),
},
span: 3,
},
{label: '订阅 Topic', content: copyField('tableMeta.topic', '复制 Topic'), span: 3},
{
label: 'Pulsar 地址',
content: copyField('tableMeta.pulsarAddress', '复制地址', 130),
span: 3,
},
{label: '过滤模式', content: mappingField('tableMeta.filterType', filterModeMapping)},
{label: '过滤字段', content: '${tableMeta.filterField}', span: 2},
{
label: '过滤内容',
content: {
type: 'each',
source: '${SPLIT(tableMeta.filterValues, ",")}',
items: {
type: 'tpl',
tpl: '<span class=\'label bg-info mr-1\'>${item}</span>',
},
},
span: 3,
},
],
},
{type: 'divider'},
{
type: 'property',
title: 'Hudi 配置',
items: [
{label: '表类型', content: mappingField('tableMeta.hudi.targetTableType', hudiTableTypeMapping)},
{label: '库名', content: copyField('tableMeta.hudi.targetDataSource', '复制库名')},
{label: '表名', content: copyField('tableMeta.hudi.targetTable', '复制表名')},
{
label: 'HDFS',
content: copyField('tableMeta.hudi.targetHdfsPath', '复制路径'),
span: 3,
},
{
label: 'Bucket 数量',
content: '${tableMeta.hudi.bucketIndexNumber}',
},
{
label: '保留文件版本',
content: '${tableMeta.hudi.keepFileVersion}',
},
{
label: '保留提交个数',
content: '${tableMeta.hudi.keepCommitVersion}',
},
{label: '写并行度', content: '${tableMeta.hudi.writeTasks}'},
{label: '读并行度', content: '${tableMeta.hudi.sourceTasks}'},
{
label: '压缩并行度',
content: '${tableMeta.hudi.compactionTasks}',
},
{
label: '写任务最大内存',
content: '${tableMeta.hudi. writeTaskMaxMemory}M',
},
{
label: '写批次大小',
content: '${tableMeta.hudi.writeBatchSize}M',
},
{
label: '写限速',
content: '${tableMeta.hudi.writeRateLimit}条/秒',
},
{
label: '压缩策略',
content: '${tableMeta.hudi. compactionStrategy}',
},
{
label: '压缩增量个数',
content: '${tableMeta.hudi.compactionDeltaCommits}',
},
{
label: '压缩增量时间',
content: '${tableMeta.hudi.compactionDeltaSeconds}秒',
},
],
},
{type: 'divider'},
{
type: 'property',
title: 'Yarn 配置',
column: 2,
items: [
{
label: '同步 JM 内存',
content: '${tableMeta.syncYarn.jobManagerMemory}M',
},
{
label: '同步 TM 内存',
content: '${tableMeta.syncYarn.taskManagerMemory}M',
},
{
label: '压缩 JM 内存',
content: '${tableMeta.compactionYarn.jobManagerMemory}M',
},
{
label: '压缩 TM 内存',
content: '${tableMeta.compactionYarn.taskManagerMemory}M',
},
],
},
{type: 'divider'},
{
type: 'property',
title: '其他配置',
column: 4,
items: [
{
label: '指标发布地址',
content: copyField('tableMeta.config.metricPublishUrl'),
span: 4,
},
{
label: '指标打点延迟',
content: '${tableMeta.config.metricPublishDelay}',
},
{
label: '指标打点间隔',
content: '${tableMeta.config.metricPublishPeriod}',
},
{
label: '指标发布超时',
content: '${tableMeta.config.metricPublishTimeout}',
},
{
label: '指标批次数量',
content: '${tableMeta.config.metricPublishBatch}',
},
{
label: 'Prometheus 地址',
content: copyField('tableMeta.config.metricPrometheusUrl'),
span: 4,
},
{
label: '事件提交服务地址',
content: copyField('tableMeta.config.metricApiUrl', '复制 URL', 130),
span: 4,
},
{
label: 'Checkpoint 存储',
content: copyField('tableMeta.config.checkpointRootPath'),
span: 4,
},
{
label: 'Zookeeper 地址',
content: copyField('tableMeta.config.zookeeperUrl'),
span: 4,
},
],
},
{type: 'divider'},
{
type: 'table',
title: '表字段详情',
source: '${tableMeta.fields}',
resizable: false,
columns: [
{name: 'sequence', label: '排序', width: 50, align: 'center'},
{name: 'name', label: '字段名'},
{name: 'type', label: '字段类型', width: 100},
{name: 'length', label: '字段长度', width: 60, align: 'center'},
{name: 'scala', label: '字段精度', width: 60, align: 'center'},
{
label: '是否主键',
align: 'center',
width: 50,
type: 'tpl',
tpl: '${primaryKey|isTrue:\'<i class="fa fa-check-circle text-success text-md p-0"></i>\':\'<i></i>\'|raw}',
},
{
label: '是否分片键',
align: 'center',
width: 70,
type: 'tpl',
tpl: '${partitionKey|isTrue:\'<i class="fa fa-check-circle text-success text-md p-0"></i>\':\'<i></i>\'|raw}',
},
],
},
],
}
}
export function mappingItem(label: string, value: string, color = 'bg-info') {
return {
label: label,
value: value,
color: color,
}
}
export const runModeMapping = [
mappingItem('1对1', 'ONE_IN_ONE', 'bg-pink-300'),
mappingItem('1对多', 'ALL_IN_ONE', 'bg-purple-300'),
mappingItem('按表1对多', 'ALL_IN_ONE_BY_TABLE', 'bg-cyan-300'),
mappingItem('按库1对多', 'ALL_IN_ONE_BY_SCHEMA', 'bg-indigo-300'),
]
export const compactionStatusMapping = [
mappingItem('调度', 'SCHEDULE'),
mappingItem('开始', 'START', 'bg-primary'),
mappingItem('完成', 'FINISH', 'bg-success'),
mappingItem('失败', 'FAIL', 'bg-danger'),
]
export const tagsMapping = [
mappingItem('不压缩', 'NO_COMPACT'),
mappingItem('不调度压缩', 'NO_SCHEDULE_COMPACT'),
mappingItem('备份Pulsar消息', 'PULSAR_BACKUP'),
mappingItem('无预合并', 'NO_PRE_COMBINE'),
mappingItem('不忽略写日志错误', 'NO_IGNORE_FAILED'),
mappingItem('取消算子合并', 'DISABLE_CHAINING'),
mappingItem('跟踪压缩op_ts', 'TRACE_LATEST_OP_TS'),
mappingItem('不使用HSync', 'DISABLE_HSYNC'),
mappingItem('测试包', 'USE_TEST_JAR'),
]
export const hudiTableTypeMapping = [
mappingItem('MOR', 'MERGE_ON_READ'),
mappingItem('COW', 'COPY_ON_WRITE'),
]
export const filterModeMapping = [
mappingItem('无', 'NONE', 'bg-pink-500'),
mappingItem('包含模式', 'INCLUDE', 'bg-purple-500'),
mappingItem('排除模式', 'EXCLUDE', 'bg-cyan-500'),
]
export const subscriptionTypeMapping = [
mappingItem('独占', 'Exclusive', 'bg-pink-500'),
mappingItem('共享', 'Shared', 'bg-purple-500'),
mappingItem('灾备', 'Failover', 'bg-cyan-500'),
mappingItem('Key', 'Key_Shared', 'bg-green-500'),
]
export const publishTypeMapping = [
mappingItem('共享', 'Shared', 'bg-pink-500'),
mappingItem('独占', 'Exclusive', 'bg-purple-500'),
mappingItem('等待独占', 'WaiteForExclusive', 'bg-cyan-500'),
]
export const hudiTimelineActionMapping = [
mappingItem('Commit', 'commit'),
mappingItem('Delta Commit', 'deltacommit'),
mappingItem('Clean', 'clean', 'bg-cyan-500'),
mappingItem('Rollback', 'rollback', 'label-danger'),
mappingItem('Savepoint', 'savepoint'),
mappingItem('Replace Commit', 'replacecommit', 'label-warning'),
mappingItem('Compaction', 'compaction', 'bg-purple-500'),
mappingItem('Restore', 'restore', 'label-warning'),
mappingItem('Indexing', 'indexing'),
mappingItem('Schema Commit', 'schemacommit', 'label-warning'),
]
export const hudiTimelineStateMapping = [
mappingItem('已提交', 'REQUESTED'),
mappingItem('操作中', 'INFLIGHT', 'label-warning'),
mappingItem('已完成', 'COMPLETED', 'label-success'),
mappingItem('错误', 'INVALID', 'label-danger'),
]
export const hudiTimelineTypeMapping = [
mappingItem('活跃', 'active'),
mappingItem('归档', 'archive', 'bg-gray-300'),
]
export const tableRunningStateMapping = [
mappingItem('运行中', 'true', 'label-success'),
mappingItem('未运行', 'false'),
]
export const versionUpdateStateMapping = [
mappingItem('已跨天', 'true', 'label-success'),
mappingItem('未跨天', 'false', 'label-danger'),
]
export const compactionMetricsStateMapping = [
mappingItem('成功', 'true', 'label-success'),
mappingItem('失败', 'false', 'label-danger'),
]
export function mappingField(field: string, mapping: Array<Record<string, string>>) {
let mapData: Record<string, string> = {
'*': `<span class='label bg-gray-300'>\${${field}}</span>`,
}
mapping.forEach(item => {
mapData[item['value']] = `<span class='label ${item['color']}'>${item['label']}</span>`
})
return {
type: 'mapping',
value: `\${${field}}`,
map: mapData,
}
}
export function filterableField(mapping: Array<Record<string, any>>, multiple = false) {
return {
multiple: multiple,
options: [
...mapping,
],
}
}
export function formReloadFlinkJobIdTextInputAndAliasTextInput(id: string) {
return {
onEvent: {
change: {
actions: [
{
actionType: 'reload',
componentId: `flink-job-id-input-select-${id}`,
},
{
actionType: 'reload',
componentId: `alias-input-select-${id}`,
},
],
},
},
}
}
export function flinkJobIdTextInput(id: string, require = false) {
return {
id: `flink-job-id-input-select-${id}`,
type: 'select',
name: 'flinkJobId',
label: 'Flink job id',
placeholder: '通过 ID 搜索',
clearable: true,
required: require,
searchable: true,
source: {
method: 'get',
url: `${commonInfo.baseUrl}/table/all_flink_job_id`,
data: {
alias: '${alias|default:undefined}',
},
},
/*onEvent: {
change: {
actions: [
{
actionType: 'reload',
componentId: `alias-input-select-${id}`,
},
]
}
}*/
}
}
export function aliasTextInput(id: string, require = false) {
return {
id: `alias-input-select-${id}`,
type: 'select',
name: 'alias',
label: '别名',
placeholder: '通过别名搜索',
clearable: true,
required: require,
searchable: true,
source: {
method: 'get',
url: `${commonInfo.baseUrl}/table/all_alias`,
data: {
flink_job_id: '${flinkJobId|default:undefined}',
},
},
/*onEvent: {
change: {
actions: [
{
actionType: 'reload',
componentId: `flink-job-id-input-select-${id}`
},
]
}
}*/
}
}
export function time(field: string) {
return {
type: 'tpl',
tpl: `\${IF(${field}, DATETOSTR(${field}, 'YYYY-MM-DD HH:mm:ss'), undefined)}`,
}
}
export function pictureFromIds(field: string) {
return `\${ARRAYMAP(${field},id => '${commonInfo.baseAiUrl}/upload/download/' + id)}`
}