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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new eb2044f3d fix(web): keep list pages readable at normal window widths 
(#2485)
eb2044f3d is described below

commit eb2044f3da74d6a3c9dee61b4cb4f274c0328d7d
Author: lizhimins <[email protected]>
AuthorDate: Fri Aug 21 16:05:10 2026 +0800

    fix(web): keep list pages readable at normal window widths (#2485)
    
    Hardcoded scroll.x values drift away from the column definitions, so tables
    showed a horizontal scrollbar even on a wide window. Add tableScrollX() and
    compute the threshold from the declared column widths on every list page.
    
    Also fold in three list-page fixes found while going through them: retry the
    cluster page bootstrap up to three times before showing the failure banner
    (instances are not served yet right after a deploy), label open-source
    instances as 开源版 in the region column instead of a bare dash, and pass
    the topic to the Aliyun trace API, which rejects a message-id-only query.
---
 .../provider/alibaba/AliyunInstanceProvider.java   |  1 +
 .../pages/cluster/__tests__/ClusterPage.test.tsx   | 13 ++--
 web/src/pages/cluster/certs.tsx                    |  3 +-
 web/src/pages/cluster/clients.tsx                  |  3 +-
 web/src/pages/cluster/index.tsx                    | 17 ++++-
 web/src/pages/instance/__tests__/DLQPage.test.tsx  |  6 +-
 .../pages/instance/__tests__/InstancePage.test.tsx |  2 +-
 web/src/pages/instance/acl.tsx                     |  3 +
 web/src/pages/instance/dlq.tsx                     |  2 +
 web/src/pages/instance/index.tsx                   |  8 +-
 web/src/pages/instance/message.tsx                 |  2 +
 web/src/pages/instance/topic.tsx                   |  2 +
 web/src/pages/ops/alerts.tsx                       |  2 +
 web/src/pages/ops/audit.tsx                        |  3 +-
 web/src/pages/studio/AlertManagement.tsx           |  3 +-
 web/src/utils/table.test.ts                        | 48 ++++++++++++
 web/src/utils/table.ts                             | 85 ++++++++++++++++++++++
 17 files changed, 184 insertions(+), 19 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
index f42381beb..3cf687d62 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
@@ -506,6 +506,7 @@ public class AliyunInstanceProvider implements 
InstanceProvider {
         Context ctx = resolve(instanceId);
         GetTraceRequest request = GetTraceRequest.builder()
                 .instanceId(ctx.cloudInstanceId())
+                .topicName(topic)
                 .messageId(msgId)
                 .build();
         GetTraceResponse response = clientFactory.call(ctx.credentialId(), 
ctx.regionId(),
diff --git a/web/src/pages/cluster/__tests__/ClusterPage.test.tsx 
b/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
index 3ade31836..2b1aef10e 100644
--- a/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClusterPage.test.tsx
@@ -272,7 +272,7 @@ describe('Cluster page', () => {
     vi.restoreAllMocks();
   });
 
-  it('surfaces instance bootstrap failures and retries without querying a 
default cluster', async () => {
+  it('auto-retries instance bootstrap failures without querying a default 
cluster', async () => {
     instanceServiceMocks.listInstances
       .mockRejectedValueOnce(new Error('managed instances unavailable'))
       .mockResolvedValueOnce([
@@ -289,16 +289,15 @@ describe('Cluster page', () => {
           gmtModified: '',
         },
       ]);
-    const user = userEvent.setup();
     renderWithProviders(<ClusterPage />);
 
-    const alert = await screen.findByRole('alert');
     expect(clusterServiceMocks.listClusters).not.toHaveBeenCalled();
 
-    await user.click(within(alert).getByRole('button', { name: /重\s*试/ }));
-    expect(await screen.findByText('rocketmq-prod-0')).toBeInTheDocument();
-    
expect(clusterServiceMocks.listClusters).toHaveBeenCalledWith('instance-1');
-  });
+    await waitFor(
+      () => 
expect(clusterServiceMocks.listClusters).toHaveBeenCalledWith('instance-1'),
+      { timeout: 6000 },
+    );
+  }, 10000);
 
   it('opens proxy detail dialog from the proxy table', async () => {
     const user = userEvent.setup();
diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
index d64e6cd85..1f6397f23 100644
--- a/web/src/pages/cluster/certs.tsx
+++ b/web/src/pages/cluster/certs.tsx
@@ -38,6 +38,7 @@ import InfoBanner from '../../components/InfoBanner';
 import type { K8sCertInfo } from '../../api/cluster';
 import { listK8sCerts, createK8sCert, deleteK8sCert } from 
'../../services/clusterService';
 import { formatDateTime } from '../../utils/format';
+import { tableScrollX } from '../../utils/table';
 
 const { Text } = Typography;
 
@@ -286,7 +287,7 @@ const K8sCertsPage = () => {
           loading={loading}
           pagination={{ pageSize: 20 }}
           size="small"
-          scroll={{ x: 1400 }}
+          scroll={{ x: tableScrollX(certColumns) }}
         />
       </Card>
 
diff --git a/web/src/pages/cluster/clients.tsx 
b/web/src/pages/cluster/clients.tsx
index 4a8b9a86f..349b5265f 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -44,6 +44,7 @@ import { listRegistryClusters } from 
'../../services/clusterService';
 import type { ClusterInfo } from '../../api/cluster';
 import { formatDateTime } from '../../utils/format';
 import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { tableScrollX } from '../../utils/table';
 
 const { Text } = Typography;
 const DEFAULT_LOAD_ERROR = '客户端连接加载失败,请稍后重试';
@@ -572,7 +573,7 @@ const ClientsPage = () => {
           }
           loading={loading}
           onChange={(_, filters) => setColumnFilters(filters)}
-          scroll={{ x: 1320 }}
+          scroll={{ x: tableScrollX(columns) }}
           pagination={{
             pageSize: 20,
             showSizeChanger: true,
diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx
index 1541cf277..e090fd700 100644
--- a/web/src/pages/cluster/index.tsx
+++ b/web/src/pages/cluster/index.tsx
@@ -74,6 +74,7 @@ import {
 import { listInstances } from '../../services/instanceService';
 import { supportsApacheRuntime } from '../../api/instance';
 import { isMockMode } from '../../services/dataMode';
+import { tableScrollX } from '../../utils/table';
 
 const { Text } = Typography;
 
@@ -312,6 +313,7 @@ const ClusterPage = () => {
   );
   const tRef = useRef(t);
   const selectedInstanceIdRef = useRef<string | undefined>(undefined);
+  const instanceLoadRetryRef = useRef(0);
 
   useEffect(() => {
     let cancelled = false;
@@ -325,6 +327,7 @@ const ClusterPage = () => {
           ? requestedInstanceId
           : apacheInstances[0]?.name;
         selectedInstanceIdRef.current = initialInstanceId;
+        instanceLoadRetryRef.current = 0;
         setInstanceLoadError(null);
         if (initialInstanceId) void requestRefreshRef.current('manual');
       })
@@ -333,10 +336,15 @@ const ClusterPage = () => {
         selectedInstanceIdRef.current = undefined;
         setClusters([]);
         setSelectedProxy(null);
-        setInstanceLoadError(tRef.current('common.fetchDataFailed'));
         setLoading(false);
-        setAutoRefresh(false);
-        autoRefreshRef.current = false;
+        if (instanceLoadRetryRef.current < 3) {
+          instanceLoadRetryRef.current += 1;
+          window.setTimeout(() => setInstanceLoadKey((key) => key + 1), 3000);
+        } else {
+          setInstanceLoadError(tRef.current('common.fetchDataFailed'));
+          setAutoRefresh(false);
+          autoRefreshRef.current = false;
+        }
       });
     return () => {
       cancelled = true;
@@ -687,6 +695,7 @@ const ClusterPage = () => {
             rowKey="addr"
             pagination={{ pageSize: 20 }}
             size="small"
+            scroll={{ x: tableScrollX(brokerColumns) }}
           />
         </Card>
 
@@ -912,6 +921,7 @@ const ClusterPage = () => {
             rowKey="id"
             pagination={{ pageSize: 20 }}
             size="small"
+            scroll={{ x: tableScrollX(registryColumns) }}
           />
         </Card>
       </div>
@@ -1066,6 +1076,7 @@ const ClusterPage = () => {
             rowKey={(r) => `${r.clusterName}-${r.addr}`}
             pagination={{ pageSize: 20 }}
             size="small"
+            scroll={{ x: tableScrollX(proxyColumns) }}
           />
         </Card>
       </div>
diff --git a/web/src/pages/instance/__tests__/DLQPage.test.tsx 
b/web/src/pages/instance/__tests__/DLQPage.test.tsx
index 50ef82460..c311e8ee4 100644
--- a/web/src/pages/instance/__tests__/DLQPage.test.tsx
+++ b/web/src/pages/instance/__tests__/DLQPage.test.tsx
@@ -218,7 +218,11 @@ describe('DLQ page', () => {
     renderWithProviders(<DLQPage />);
 
     await screen.findByText('cg-order');
-    await user.click(screen.getByText('最近入队时间'));
+    // A horizontally scrollable table also renders antd's hidden measure row, 
which repeats the
+    // column titles, so the sort trigger has to be looked up inside the 
visible header.
+    const header = document.querySelector('thead');
+    if (!header) throw new Error('DLQ table header not found');
+    await user.click(within(header as HTMLElement).getByText('最近入队时间'));
 
     expect(screen.getByText('-')).toBeInTheDocument();
   });
diff --git a/web/src/pages/instance/__tests__/InstancePage.test.tsx 
b/web/src/pages/instance/__tests__/InstancePage.test.tsx
index 8c7f9d6b8..3b31c4795 100644
--- a/web/src/pages/instance/__tests__/InstancePage.test.tsx
+++ b/web/src/pages/instance/__tests__/InstancePage.test.tsx
@@ -771,7 +771,7 @@ describe('InstancePage', () => {
     const cloudRow = (await screen.findByText('rmq-cloud-1')).closest('tr')!;
     expect(within(cloudRow).getByText('cn-hangzhou')).toBeInTheDocument();
     const apacheRow = screen.getByText('open-source-1').closest('tr')!;
-    expect(within(apacheRow).getAllByText('-').length).toBeGreaterThan(0);
+    expect(within(apacheRow).getAllByText('开源版').length).toBeGreaterThan(0);
     
expect(within(apacheRow).queryByText('cn-hangzhou')).not.toBeInTheDocument();
   });
 
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index b3d1984e0..680519be5 100644
--- a/web/src/pages/instance/acl.tsx
+++ b/web/src/pages/instance/acl.tsx
@@ -65,6 +65,7 @@ import {
 } from '../../services/aclService';
 import type { AclRule, AclUser, AclClusterConfig, PlainAccessConfig } from 
'../../api/acl';
 import { useInstanceFilter } from '../../hooks/useInstanceFilter';
+import { tableScrollX } from '../../utils/table';
 
 type AclRuleFormValues = Pick<
   AclRule,
@@ -1054,6 +1055,7 @@ const AclPageContent = ({
                       },
                     }}
                     size="small"
+                    scroll={{ x: tableScrollX(ruleColumns) }}
                   />
                 </div>
               ),
@@ -1106,6 +1108,7 @@ const AclPageContent = ({
                       },
                     }}
                     size="small"
+                    scroll={{ x: tableScrollX(userColumns) }}
                   />
                 </div>
               ),
diff --git a/web/src/pages/instance/dlq.tsx b/web/src/pages/instance/dlq.tsx
index 7bb1efbf2..0a4fcfde6 100644
--- a/web/src/pages/instance/dlq.tsx
+++ b/web/src/pages/instance/dlq.tsx
@@ -40,6 +40,7 @@ import type { DLQGroup } from '../../api/message';
 import { exportDLQMessages, listDLQGroups, resendDLQ } from 
'../../services/messageService';
 import { useInstanceFilter } from '../../hooks/useInstanceFilter';
 import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
+import { tableScrollX } from '../../utils/table';
 
 const { Text } = Typography;
 const { RangePicker } = DatePicker;
@@ -477,6 +478,7 @@ const DLQPage = () => {
             },
           }}
           size="small"
+          scroll={{ x: tableScrollX(columns, { selection: true }) }}
         />
       </Card>
 
diff --git a/web/src/pages/instance/index.tsx b/web/src/pages/instance/index.tsx
index 592c9975d..020b5831d 100644
--- a/web/src/pages/instance/index.tsx
+++ b/web/src/pages/instance/index.tsx
@@ -48,6 +48,7 @@ import {
 } from '../../api/aliyunCatalog';
 import { listTencentInstances, listTencentRegions } from 
'../../api/tencentCatalog';
 import { formatDateTime } from '../../utils/format';
+import { tableScrollX } from '../../utils/table';
 import {
   createInstance,
   deleteInstance,
@@ -458,9 +459,9 @@ const InstancePage = () => {
       sorter: (a, b) => (a.regionId ?? '').localeCompare(b.regionId ?? ''),
       render: (regionId: string | undefined, record: Instance) => (
         <Text type="secondary" style={{ fontSize: 14 }}>
-          {!record.vendor || record.vendor === 'APACHE' || !regionId
-            ? '-'
-            : record.regionName || regionId}
+          {!record.vendor || record.vendor === 'APACHE'
+            ? '开源版'
+            : record.regionName || regionId || '-'}
         </Text>
       ),
     },
@@ -697,6 +698,7 @@ const InstancePage = () => {
           pagination={false}
           size="small"
           tableLayout="fixed"
+          scroll={{ x: tableScrollX(columns, { selection: true }) }}
         />
       </Card>
 
diff --git a/web/src/pages/instance/message.tsx 
b/web/src/pages/instance/message.tsx
index 8a3d21b21..b36d99e28 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -60,6 +60,7 @@ import { getMessageTrace, queryMessages } from 
'../../services/messageService';
 import { listTopics } from '../../services/topicService';
 import { useInstanceFilter } from '../../hooks/useInstanceFilter';
 import { downloadBlob } from '../../utils/download';
+import { tableScrollX } from '../../utils/table';
 
 const { Paragraph, Text } = Typography;
 const { RangePicker } = DatePicker;
@@ -946,6 +947,7 @@ const MessagePageContent = ({
             showTotal: (total) => `共 ${total} 条消息`,
           }}
           size="small"
+          scroll={{ x: tableScrollX(columns) }}
         />
       </Card>
 
diff --git a/web/src/pages/instance/topic.tsx b/web/src/pages/instance/topic.tsx
index 51ea45fc7..c2bc59e59 100644
--- a/web/src/pages/instance/topic.tsx
+++ b/web/src/pages/instance/topic.tsx
@@ -79,6 +79,7 @@ import {
 } from '../../utils/resourceCsvImport';
 import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
 import { parseMessageProperties } from '../../utils/messageProperties';
+import { tableScrollX } from '../../utils/table';
 
 const { Text } = Typography;
 
@@ -1204,6 +1205,7 @@ const TopicPage = () => {
               },
             }}
             size="small"
+            scroll={{ x: tableScrollX(columns, { selection: true }) }}
             onRow={(record) => ({
               onClick: () => void openDetail(record),
               style: { cursor: 'pointer' },
diff --git a/web/src/pages/ops/alerts.tsx b/web/src/pages/ops/alerts.tsx
index d29fb091d..a68058c3f 100644
--- a/web/src/pages/ops/alerts.tsx
+++ b/web/src/pages/ops/alerts.tsx
@@ -48,6 +48,7 @@ import {
   updateAlertRule,
 } from '../../services/opsService';
 import { attachThresholdUnit } from './alertRulePayload';
+import { tableScrollX } from '../../utils/table';
 
 const { TextArea } = Input;
 
@@ -440,6 +441,7 @@ const AlertsPage = () => {
           loading={loading}
           rowSelection={rowSelection}
           pagination={false}
+          scroll={{ x: tableScrollX(columns, { selection: true }) }}
         />
       </Card>
 
diff --git a/web/src/pages/ops/audit.tsx b/web/src/pages/ops/audit.tsx
index 59eb03c12..0884db083 100644
--- a/web/src/pages/ops/audit.tsx
+++ b/web/src/pages/ops/audit.tsx
@@ -46,6 +46,7 @@ import {
   listAuditRecords,
 } from '../../services/opsService';
 import { downloadBlob } from '../../utils/download';
+import { tableScrollX } from '../../utils/table';
 
 const emptyFilterOptions: AuditFilterOptions = {
   operationTypes: [],
@@ -372,7 +373,7 @@ const AuditPage: React.FC = () => {
           dataSource={records}
           rowKey="id"
           loading={loading}
-          scroll={{ x: 1470 }}
+          scroll={{ x: tableScrollX(columns) }}
           pagination={{
             current: page,
             pageSize,
diff --git a/web/src/pages/studio/AlertManagement.tsx 
b/web/src/pages/studio/AlertManagement.tsx
index 0af48aa36..3f99d48fd 100644
--- a/web/src/pages/studio/AlertManagement.tsx
+++ b/web/src/pages/studio/AlertManagement.tsx
@@ -56,6 +56,7 @@ import {
 } from '../../api/alertManagement';
 import type { AlertRule as PersistedAlertRule, AlertRuleRequest } from 
'../../api/alertManagement';
 import { downloadBlob } from '../../utils/download';
+import { tableScrollX } from '../../utils/table';
 
 const { TextArea } = Input;
 
@@ -759,7 +760,7 @@ const AlertManagementPage: React.FC = () => {
             showSizeChanger: true,
             showTotal: (total) => `${t('common.total')} ${total}`,
           }}
-          scroll={{ x: 1200 }}
+          scroll={{ x: tableScrollX(columns, { selection: true }) }}
         />
       </Card>
 
diff --git a/web/src/utils/table.test.ts b/web/src/utils/table.test.ts
new file mode 100644
index 000000000..d0b3df110
--- /dev/null
+++ b/web/src/utils/table.test.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { describe, expect, it } from 'vitest';
+
+import { tableScrollX } from './table';
+
+describe('tableScrollX', () => {
+  it('sums the declared column widths', () => {
+    expect(tableScrollX([{ width: 220 }, { width: 200 }, { width: 100 
}])).toBe(520);
+  });
+
+  it('reserves room for the selection and expand columns', () => {
+    expect(tableScrollX([{ width: 100 }], { selection: true })).toBe(140);
+    expect(tableScrollX([{ width: 100 }], { expandable: true })).toBe(148);
+    expect(tableScrollX([{ width: 100 }], { selection: true, expandable: true, 
extra: 12 })).toBe(
+      200,
+    );
+  });
+
+  it('falls back to a default share for columns without a numeric width', () 
=> {
+    expect(tableScrollX([{ width: 100 }, {}, { width: '30%' }])).toBe(340);
+    expect(tableScrollX([{ width: '80px' }])).toBe(80);
+  });
+
+  it('adds up grouped children and skips hidden columns', () => {
+    expect(tableScrollX([{ children: [{ width: 100 }, { width: 60 }] }, { 
width: 40 }])).toBe(200);
+    expect(tableScrollX([{ width: 100 }, { width: 999, hidden: true 
}])).toBe(100);
+  });
+
+  it('tolerates an undefined column list', () => {
+    expect(tableScrollX(undefined)).toBe(0);
+  });
+});
diff --git a/web/src/utils/table.ts b/web/src/utils/table.ts
new file mode 100644
index 000000000..bf8e0a06e
--- /dev/null
+++ b/web/src/utils/table.ts
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Minimum table width helpers.
+ *
+ * Without `scroll.x` antd keeps `table-layout: auto` and squeezes every 
column into the
+ * viewport, so on a narrow window (or after zooming in) the declared column 
widths are
+ * ignored and cells wrap. Passing a numeric `scroll.x` switches the table to
+ * `table-layout: fixed` with `min-width: 100%`, which keeps the columns 
readable and shows
+ * a horizontal scrollbar when the window is narrower, while still stretching 
to fill a
+ * wide window.
+ *
+ * The value is derived from the column definitions instead of being 
hard-coded so it stays
+ * correct when columns are added, removed or resized.
+ */
+
+/** Width antd reserves for the leading checkbox column (not part of 
`columns`). */
+const SELECTION_COLUMN_WIDTH = 40;
+
+/** Width antd reserves for the leading expand-icon column (not part of 
`columns`). */
+const EXPAND_COLUMN_WIDTH = 48;
+
+/** Fallback share for columns that declare no width, so they never collapse 
to nothing. */
+const UNSIZED_COLUMN_WIDTH = 120;
+
+interface ColumnLike {
+  width?: number | string;
+  children?: readonly ColumnLike[];
+  hidden?: boolean;
+}
+
+export interface TableScrollXOptions {
+  /** The table renders a `rowSelection` checkbox column. */
+  selection?: boolean;
+  /** The table renders an expandable row icon column. */
+  expandable?: boolean;
+  /** Extra pixels to reserve on top of the declared columns. */
+  extra?: number;
+}
+
+function columnWidth(column: ColumnLike): number {
+  if (column.hidden) {
+    return 0;
+  }
+  if (column.children?.length) {
+    return column.children.reduce((total, child) => total + 
columnWidth(child), 0);
+  }
+  if (typeof column.width === 'number') {
+    return column.width;
+  }
+  if (typeof column.width === 'string') {
+    const parsed = Number.parseFloat(column.width);
+    return Number.isFinite(parsed) && column.width.endsWith('px') ? parsed : 
UNSIZED_COLUMN_WIDTH;
+  }
+  return UNSIZED_COLUMN_WIDTH;
+}
+
+/** Sums the declared column widths into the `scroll.x` a table needs to stay 
readable. */
+export function tableScrollX(
+  columns: readonly ColumnLike[] | undefined,
+  options: TableScrollXOptions = {},
+): number {
+  const declared = (columns ?? []).reduce((total, column) => total + 
columnWidth(column), 0);
+  return (
+    declared +
+    (options.selection ? SELECTION_COLUMN_WIDTH : 0) +
+    (options.expandable ? EXPAND_COLUMN_WIDTH : 0) +
+    (options.extra ?? 0)
+  );
+}

Reply via email to