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

github-merge-queue[bot] pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 6ee0c37447 [Improve][Zeta] Make job detail overview responsive (#12259)
6ee0c37447 is described below

commit 6ee0c374478a7c8be090e3eb33895027c5ac2ffd
Author: zhiweiniu <[email protected]>
AuthorDate: Fri Sep 11 12:58:37 2026 +0000

    [Improve][Zeta] Make job detail overview responsive (#12259)
---
 .../src/components/live-metrics-chart/board.tsx    |  38 +++--
 .../src/components/live-metrics-chart/index.tsx    |  87 +++++++-----
 .../seatunnel-engine-ui/src/views/jobs/detail.scss | 156 ++++++++++++++++++++-
 .../seatunnel-engine-ui/src/views/jobs/detail.tsx  | 143 +++++++++++--------
 4 files changed, 313 insertions(+), 111 deletions(-)

diff --git 
a/seatunnel-engine/seatunnel-engine-ui/src/components/live-metrics-chart/board.tsx
 
b/seatunnel-engine/seatunnel-engine-ui/src/components/live-metrics-chart/board.tsx
index af4d3ed7fd..a22af9c7b4 100644
--- 
a/seatunnel-engine/seatunnel-engine-ui/src/components/live-metrics-chart/board.tsx
+++ 
b/seatunnel-engine/seatunnel-engine-ui/src/components/live-metrics-chart/board.tsx
@@ -36,7 +36,7 @@ export default defineComponent({
       default: 'No metrics'
     },
     height: {
-      type: Number,
+      type: [Number, String] as PropType<number | string>,
       default: 220
     },
     unitTitles: {
@@ -50,29 +50,41 @@ export default defineComponent({
     }
   },
   setup(props) {
+    // A pinned metric stays visible even before realtime samples arrive. 
Removing an empty
+    // group would make the layout jump and would make a successful pin look 
like it was ignored.
     const groups = computed(() => groupSeriesByUnit(props.series || []))
 
     return () => {
       if (!groups.value.length) {
-        return <div class="text-sm text-gray-400 py-2 text-center 
leading-6">{props.emptyText}</div>
+        return (
+          <div class="live-metrics-board-empty text-sm text-gray-400 py-2 
text-center leading-6">
+            {props.emptyText}
+          </div>
+        )
       }
-      const n = groups.value.length
-      const gridClass =
+      const gridClass = props.layout === 'row' ? 'grid gap-2' : 'flex flex-col 
gap-3'
+      const gridStyle =
         props.layout === 'row'
-          ? n >= 3
-            ? 'grid grid-cols-3 gap-2'
-            : n === 2
-              ? 'grid grid-cols-2 gap-2'
-              : 'grid grid-cols-1 gap-2'
-          : 'flex flex-col gap-3'
+          ? {
+              gridTemplateColumns:
+                groups.value.length === 1
+                  ? 'minmax(0, calc((100% - 8px) / 2))'
+                  : 'repeat(auto-fit, minmax(min(320px, 100%), 1fr))'
+            }
+          : undefined
       return (
-        <div class={gridClass}>
+        <div
+          class={`${gridClass} live-metrics-board ${
+            groups.value.length === 1 ? 'live-metrics-board-single' : ''
+          }`}
+          style={gridStyle}
+        >
           {groups.value.map((group) => (
-            <div key={group.unit} class="min-w-0">
+            <div key={group.unit} class="live-metrics-group min-w-0">
               <div class="text-xs text-gray-500 mb-1 leading-4">
                 {props.unitTitles[group.unit] || group.unit}
               </div>
-              <div class="bg-white rounded border border-gray-100 
overflow-hidden">
+              <div class="live-metrics-chart-container bg-white rounded border 
border-gray-100 overflow-hidden">
                 <LiveLineChart
                   series={group.series}
                   windowMs={props.windowMs}
diff --git 
a/seatunnel-engine/seatunnel-engine-ui/src/components/live-metrics-chart/index.tsx
 
b/seatunnel-engine/seatunnel-engine-ui/src/components/live-metrics-chart/index.tsx
index 65a9db069f..7a068d1a8f 100644
--- 
a/seatunnel-engine/seatunnel-engine-ui/src/components/live-metrics-chart/index.tsx
+++ 
b/seatunnel-engine/seatunnel-engine-ui/src/components/live-metrics-chart/index.tsx
@@ -57,7 +57,7 @@ export default defineComponent({
       default: 'No metrics'
     },
     height: {
-      type: Number,
+      type: [Number, String] as PropType<number | string>,
       default: 260
     },
     unit: {
@@ -92,7 +92,11 @@ export default defineComponent({
       const from = now - props.windowMs
       const unit = props.unit || series[0]?.unit
       const single = series.length === 1
-      const compact = props.height < 160
+      const compact =
+        props.height === '100%' || (typeof props.height === 'number' && 
props.height < 160)
+      const isVisiblePoint = (point: { ts: number; value: number }) =>
+        point.ts >= from && Number.isFinite(point.value)
+      const hasVisiblePoints = series.some((item) => (item.points || 
[]).some(isVisiblePoint))
 
       const yAxis =
         unit === 'ratio'
@@ -101,8 +105,6 @@ export default defineComponent({
               min: 0,
               max: 1,
               scale: false,
-              name: compact ? '' : '%',
-              nameTextStyle: { color: '#999', padding: [0, 0, 0, 8], fontSize: 
10 },
               axisLabel: {
                 fontSize: compact ? 9 : 10,
                 margin: compact ? 4 : 8,
@@ -111,26 +113,39 @@ export default defineComponent({
               splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
             }
           : {
+              show: unit !== 'count' || hasVisiblePoints,
               type: 'value' as const,
               min: compact ? 0 : undefined,
+              minInterval: unit === 'count' ? 1 : undefined,
               scale: !compact,
-              name: compact ? '' : unit === 'duration' ? 'ms' : '',
-              nameTextStyle: { color: '#999', fontSize: 10 },
               axisLabel: { fontSize: compact ? 9 : 10, margin: compact ? 4 : 8 
},
               splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
             }
 
       chart.setOption(
         {
-          title: single
-            ? {
-                show: true,
-                text: series[0].name,
-                left: 0,
-                top: 0,
-                textStyle: { color: '#666', fontSize: compact ? 11 : 12, 
fontWeight: 'normal' }
-              }
-            : { show: false },
+          title:
+            unit === 'count' && !hasVisiblePoints
+              ? {
+                  show: true,
+                  text: props.emptyText,
+                  left: 'center',
+                  top: 'middle',
+                  textStyle: {
+                    color: '#9ca3af',
+                    fontSize: compact ? 10 : 12,
+                    fontWeight: 'normal'
+                  }
+                }
+              : single && !compact
+                ? {
+                    show: true,
+                    text: series[0].name,
+                    left: 0,
+                    top: 0,
+                    textStyle: { color: '#666', fontSize: compact ? 11 : 12, 
fontWeight: 'normal' }
+                  }
+                : { show: false },
           color: COLORS,
           tooltip: {
             trigger: 'axis',
@@ -152,24 +167,27 @@ export default defineComponent({
               return [time, ...lines].join('<br/>')
             }
           },
-          legend: single
-            ? { show: false }
-            : {
-                type: 'scroll',
-                top: 0,
-                itemWidth: compact ? 10 : 14,
-                itemHeight: compact ? 8 : 10,
-                textStyle: { fontSize: compact ? 10 : 11 },
-                data: series.map((s) => s.name)
-              },
+          legend:
+            single && !compact
+              ? { show: false }
+              : {
+                  type: 'scroll',
+                  top: 0,
+                  itemWidth: compact ? 10 : 14,
+                  itemHeight: compact ? 8 : 10,
+                  textStyle: { fontSize: compact ? 10 : 11 },
+                  data: series.map((s) => s.name)
+                },
           grid: {
             left: compact ? 28 : 48,
             right: compact ? 4 : 24,
-            top: compact ? (single ? 16 : 20) : single ? 28 : 36,
-            bottom: compact ? 0 : 32,
+            // Reserve one line for the legend so it never overlaps the 
highest Y-axis label.
+            top: compact ? 30 : single ? 28 : 36,
+            bottom: compact ? 10 : 32,
             containLabel: !compact
           },
           xAxis: {
+            show: unit !== 'count' || hasVisiblePoints,
             type: 'time',
             min: from,
             max: now,
@@ -207,7 +225,7 @@ export default defineComponent({
                   }
                 : undefined,
               data: [...(s.points || [])]
-                .filter((p) => p.ts >= from)
+                .filter(isVisiblePoint)
                 .sort((a, b) => a.ts - b.ts)
                 .map((p) => [p.ts, p.value])
             }
@@ -240,12 +258,11 @@ export default defineComponent({
       { deep: true }
     )
 
-    return () => (
-      <div
-        ref={el}
-        class="w-full"
-        style={{ height: `${props.height}px`, minHeight: `${props.height}px` }}
-      />
-    )
+    return () => {
+      const height = typeof props.height === 'number' ? `${props.height}px` : 
props.height
+      return (
+        <div ref={el} class="live-metrics-chart w-full" style={{ height, 
minHeight: height }} />
+      )
+    }
   }
 })
diff --git a/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.scss 
b/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.scss
index 3b0f2506f3..d9f3a25414 100644
--- a/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.scss
+++ b/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.scss
@@ -15,11 +15,163 @@
  * limitations under the License.
  */
 
- .n-data-table .n-data-table-tr.focused-row {
+.job-detail-page {
+  box-sizing: border-box;
+  min-width: 0;
+  min-height: calc(100dvh - 101px);
+  container-type: inline-size;
+  padding: clamp(16px, 1.667vw, 32px) clamp(16px, 3.333vw, 80px) clamp(24px, 
3.333vw, 64px);
+}
+
+.job-detail-title,
+.job-detail-meta,
+.job-detail-meta-item,
+.live-metrics-header {
+  display: flex;
+  align-items: center;
+}
+
+.job-detail-title {
+  flex-wrap: wrap;
+  gap: 8px 12px;
+}
+
+.job-detail-meta {
+  flex-wrap: wrap;
+  gap: 8px 12px;
+}
+
+.job-detail-meta-item {
+  flex: 0 0 auto;
+  gap: 12px;
+  white-space: nowrap;
+}
+
+.job-detail-meta-divider {
+  margin: 0;
+}
+
+.live-metrics-header {
+  flex-wrap: wrap;
+  gap: 4px 12px;
+}
+
+.job-detail-overview {
+  display: grid;
+  height: max(576px, calc(90dvh - 225px));
+  min-height: 0;
+  gap: 8px;
+}
+
+.job-detail-overview.has-pinned-metrics {
+  grid-template-rows: minmax(220px, 4fr) minmax(180px, 3fr) minmax(140px, 3fr);
+}
+
+.job-detail-overview.has-no-pinned-metrics {
+  grid-template-rows: minmax(220px, 5fr) minmax(72px, 1fr) minmax(140px, 4fr);
+}
+
+.job-detail-overview .dag-wrapper {
+  height: 100%;
+  min-height: 0;
+  margin-bottom: 0;
+}
+
+.job-detail-pinned-metrics {
+  position: relative;
+  display: flex;
+  min-height: 0;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.job-detail-pinned-metrics .live-metrics-board,
+.job-detail-pinned-metrics .live-metrics-group {
+  min-height: 0;
+}
+
+.job-detail-pinned-metrics .live-metrics-board,
+.job-detail-pinned-metrics .live-metrics-board-empty {
+  flex: 1;
+}
+
+.job-detail-pinned-metrics .live-metrics-board-empty {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  pointer-events: none;
+}
+
+.job-detail-pinned-metrics .live-metrics-group {
+  display: flex;
+  flex-direction: column;
+}
+
+.job-detail-pinned-metrics .live-metrics-chart-container {
+  min-height: 0;
+  flex: 1;
+}
+
+.job-detail-table {
+  height: 100%;
+  min-height: 0;
+  overflow: auto;
+}
+
+.n-data-table .n-data-table-tr.focused-row {
   &,
   &:hover {
     td.n-data-table-td {
       background-color: #e6f7ff;
     }
   }
- }
\ No newline at end of file
+}
+
+@container (max-width: 720px) {
+  .job-detail-overview {
+    display: block;
+    height: auto;
+  }
+
+  .job-detail-overview .dag-wrapper {
+    height: 220px;
+    margin-bottom: 8px;
+  }
+
+  .job-detail-pinned-metrics {
+    min-height: 180px;
+    margin-bottom: 8px;
+  }
+
+  .job-detail-pinned-metrics .live-metrics-chart {
+    height: 140px !important;
+    min-height: 140px !important;
+  }
+
+  .job-detail-pinned-metrics .live-metrics-board-single {
+    grid-template-columns: minmax(0, 1fr) !important;
+  }
+
+  .job-detail-table {
+    height: auto;
+    overflow: visible;
+  }
+
+  .job-detail-meta {
+    align-items: flex-start;
+  }
+
+  .job-detail-meta-divider {
+    display: none;
+  }
+
+  .live-metrics-header > :last-child {
+    flex-basis: 100%;
+  }
+
+  .job-detail-drawer {
+    width: 92% !important;
+  }
+}
diff --git a/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.tsx 
b/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.tsx
index ac8eaf0b31..1e113c0056 100644
--- a/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.tsx
+++ b/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.tsx
@@ -360,6 +360,8 @@ export default defineComponent({
       )
     })
 
+    const hasPinnedMetrics = computed(() => pinStore.pins.length > 0)
+
     const drawerVertexChartSeries = computed(() => {
       const vertex = job.jobDag?.vertexInfoMap?.find((v) => v.vertexId === 
focusedId.value)
       if (!vertex) return []
@@ -557,83 +559,101 @@ export default defineComponent({
     }
 
     return () => (
-      <div class="w-full bg-white px-12 pt-6 pb-12 border border-gray-100 
rounded-xl">
-        <div class="font-bold text-xl">
-          {job.jobName}
-          <NTag bordered={false} color={getColorFromStatus(job.jobStatus)} 
class="ml-3">
+      <div class="job-detail-page w-full bg-white border border-gray-100 
rounded-xl">
+        <div class="job-detail-title font-bold text-xl">
+          <span>{job.jobName}</span>
+          <NTag bordered={false} color={getColorFromStatus(job.jobStatus)}>
             {job.jobStatus}
           </NTag>
           {realtimeError.value ? (
             <span title={realtimeError.value}>
-              <NTag bordered={false} type="warning" class="ml-3">
+              <NTag bordered={false} type="warning">
                 Realtime metrics unavailable
               </NTag>
             </span>
           ) : null}
         </div>
-        <div class="mt-3 flex items-center gap-3">
-          <span>{t('detail.id')}:</span>
-          <span class="font-bold">{job.jobId}</span>
-          <NDivider vertical />
-          <span>{t('detail.createTime')}:</span>
-          <span class="font-bold">{job.createTime}</span>
-          <NDivider vertical />
-          <span>{t('detail.duration')}:</span>
-          <span class="font-bold">{duration.value}</span>
+        <div class="job-detail-meta mt-3">
+          <div class="job-detail-meta-item">
+            <span>{t('detail.id')}:</span>
+            <span class="font-bold">{job.jobId}</span>
+          </div>
+          <NDivider class="job-detail-meta-divider" vertical />
+          <div class="job-detail-meta-item">
+            <span>{t('detail.createTime')}:</span>
+            <span class="font-bold">{job.createTime}</span>
+          </div>
+          <NDivider class="job-detail-meta-divider" vertical />
+          <div class="job-detail-meta-item">
+            <span>{t('detail.duration')}:</span>
+            <span class="font-bold">{duration.value}</span>
+          </div>
         </div>
         <div class="tab-wrap relative">
           <NTabs v-model:value={select.value} type="line" animated>
             <NTabPane name="Overview" tab={t('detail.tabs.overview')}>
-              <DAG
-                job={job}
-                focusedId={focusedId.value}
-                onNodeClick={onFocus}
-                onEdgeClick={onEdgeFocus}
-                realtimeEdgeStats={realtimeEdgeStats.value}
-                realtimeVertexStats={realtimeVertexStats.value}
-                realtimeTick={realtimeTick.value}
-              />
-              <div class="mt-2 mb-2 border border-gray-100 rounded-lg px-3 
pt-2 pb-2 bg-gray-50">
-                <div class="flex items-baseline justify-between mb-2">
-                  <div class="font-semibold 
text-base">{t('detail.liveMetrics.pinnedTitle')}</div>
-                  <div class="text-xs text-gray-500">
-                    {t('detail.liveMetrics.pinnedHint', { limit: 
LIVE_METRICS_PIN_LIMIT })}
-                    {pinStore.pins.length
-                      ? ` · ${pinStore.pins.length}/${LIVE_METRICS_PIN_LIMIT}`
-                      : ''}
+              <div
+                class={`job-detail-overview ${
+                  hasPinnedMetrics.value ? 'has-pinned-metrics' : 
'has-no-pinned-metrics'
+                }`}
+              >
+                <DAG
+                  job={job}
+                  focusedId={focusedId.value}
+                  onNodeClick={onFocus}
+                  onEdgeClick={onEdgeFocus}
+                  realtimeEdgeStats={realtimeEdgeStats.value}
+                  realtimeVertexStats={realtimeVertexStats.value}
+                  realtimeTick={realtimeTick.value}
+                />
+                <div class="job-detail-pinned-metrics border border-gray-100 
rounded-lg px-3 pt-2 pb-2 bg-gray-50">
+                  <div class="live-metrics-header flex items-baseline 
justify-between mb-2">
+                    <div class="font-semibold 
text-base">{t('detail.liveMetrics.pinnedTitle')}</div>
+                    <div class="text-xs text-gray-500">
+                      {t('detail.liveMetrics.pinnedHint', { limit: 
LIVE_METRICS_PIN_LIMIT })}
+                      {pinStore.pins.length
+                        ? ` · 
${pinStore.pins.length}/${LIVE_METRICS_PIN_LIMIT}`
+                        : ''}
+                    </div>
                   </div>
+                  {pinStore.pins.length ? (
+                    <NSpace class="mb-2" size="small" wrap>
+                      {pinStore.pins.map((p) => (
+                        <NTag key={p.id} closable type="info" onClose={() => 
pinStore.unpin(p.id)}>
+                          {p.name}
+                        </NTag>
+                      ))}
+                    </NSpace>
+                  ) : null}
+                  <LiveMetricsBoard
+                    series={pinnedSeries.value}
+                    windowMs={effectiveRealtimeWindowMs(realtimeWindowMs)}
+                    emptyText={
+                      pinStore.pins.length
+                        ? t('detail.liveMetrics.chartEmpty')
+                        : t('detail.liveMetrics.emptyPinned')
+                    }
+                    height="100%"
+                    layout="row"
+                    unitTitles={{
+                      ratio: t('detail.liveMetrics.unitRatio'),
+                      duration: t('detail.liveMetrics.unitDuration'),
+                      count: t('detail.liveMetrics.unitCount')
+                    }}
+                  />
+                </div>
+                <div class="job-detail-table">
+                  <NDataTable
+                    columns={columns}
+                    data={tableData.value}
+                    pagination={false}
+                    scrollX={1200}
+                    bordered
+                    rowClassName={rowClassName}
+                    rowProps={rowProps}
+                  />
                 </div>
-                {pinStore.pins.length ? (
-                  <NSpace class="mb-2" size="small" wrap>
-                    {pinStore.pins.map((p) => (
-                      <NTag key={p.id} closable type="info" onClose={() => 
pinStore.unpin(p.id)}>
-                        {p.name}
-                      </NTag>
-                    ))}
-                  </NSpace>
-                ) : null}
-                <LiveMetricsBoard
-                  series={pinnedSeries.value}
-                  windowMs={effectiveRealtimeWindowMs(realtimeWindowMs)}
-                  emptyText={t('detail.liveMetrics.emptyPinned')}
-                  height={140}
-                  layout="row"
-                  unitTitles={{
-                    ratio: t('detail.liveMetrics.unitRatio'),
-                    duration: t('detail.liveMetrics.unitDuration'),
-                    count: t('detail.liveMetrics.unitCount')
-                  }}
-                />
               </div>
-              <NDataTable
-                columns={columns}
-                data={tableData.value}
-                pagination={false}
-                scrollX="auto"
-                bordered
-                rowClassName={rowClassName}
-                rowProps={rowProps}
-              />
             </NTabPane>
             <NTabPane name="Exception" tab={t('detail.tabs.exception')}>
               <pre style="white-space: pre-wrap; word-wrap: break-word; 
background-color: #f5f5f5; padding: 12px; border-radius: 4px; overflow: auto; 
max-height: 600px; font-family: monospace; line-height: 1.5;">
@@ -648,6 +668,7 @@ export default defineComponent({
             </NTabPane>
           </NTabs>
           <NDrawer
+            class="job-detail-drawer"
             show={select.value === 'Overview' && drawerShow.value}
             showMask={false}
             width={'40%'}

Reply via email to