From e5e095ad40cc6ba651153abe97df654ef3350136 Mon Sep 17 00:00:00 2001 From: Vadim Ogievetsky Date: Fri, 23 Jan 2026 14:45:06 -0800 Subject: [PATCH 1/3] Init server props table --- web-console/src/dialogs/index.ts | 1 + .../service-properties-table.scss | 38 ++++++ .../service-properties-table.tsx | 103 +++++++++++++++ .../service-table-action-dialog.tsx | 60 +++++++++ .../src/views/services-view/services-view.tsx | 120 +++++++++++++++--- 5 files changed, 301 insertions(+), 21 deletions(-) create mode 100644 web-console/src/dialogs/service-table-action-dialog/service-properties-table/service-properties-table.scss create mode 100644 web-console/src/dialogs/service-table-action-dialog/service-properties-table/service-properties-table.tsx create mode 100644 web-console/src/dialogs/service-table-action-dialog/service-table-action-dialog.tsx diff --git a/web-console/src/dialogs/index.ts b/web-console/src/dialogs/index.ts index 6a3c336831be..ba1a5d12dcb2 100644 --- a/web-console/src/dialogs/index.ts +++ b/web-console/src/dialogs/index.ts @@ -31,6 +31,7 @@ export * from './lookup-edit-dialog/lookup-edit-dialog'; export * from './numeric-input-dialog/numeric-input-dialog'; export * from './overlord-dynamic-config-dialog/overlord-dynamic-config-dialog'; export * from './retention-dialog/retention-dialog'; +export * from './service-table-action-dialog/service-table-action-dialog'; export * from './snitch-dialog/snitch-dialog'; export * from './spec-dialog/spec-dialog'; export * from './string-input-dialog/string-input-dialog'; diff --git a/web-console/src/dialogs/service-table-action-dialog/service-properties-table/service-properties-table.scss b/web-console/src/dialogs/service-table-action-dialog/service-properties-table/service-properties-table.scss new file mode 100644 index 000000000000..c7f3e48e178c --- /dev/null +++ b/web-console/src/dialogs/service-table-action-dialog/service-properties-table/service-properties-table.scss @@ -0,0 +1,38 @@ +/* + * 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. + */ + +.service-properties-table { + position: relative; + height: 100%; + + .loader { + position: relative; + } + + .ReactTable { + height: 100%; + + .code { + word-break: break-all; + white-space: pre-wrap; + max-height: 100px; + overflow: auto; + font-family: monospace; + } + } +} diff --git a/web-console/src/dialogs/service-table-action-dialog/service-properties-table/service-properties-table.tsx b/web-console/src/dialogs/service-table-action-dialog/service-properties-table/service-properties-table.tsx new file mode 100644 index 000000000000..4c89664f3608 --- /dev/null +++ b/web-console/src/dialogs/service-table-action-dialog/service-properties-table/service-properties-table.tsx @@ -0,0 +1,103 @@ +/* + * 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 { L } from 'druid-query-toolkit'; +import React from 'react'; +import ReactTable from 'react-table'; + +import { Loader } from '../../../components'; +import { useQueryManager } from '../../../hooks'; +import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../../react-table'; +import { queryDruidSql } from '../../../utils'; + +import './service-properties-table.scss'; + +export interface ServicePropertiesTableRow { + property: string; + value: string; + service_name: string; + node_roles: string | null; +} + +export interface ServicePropertiesTableProps { + server: string; +} + +export const ServicePropertiesTable = React.memo(function ServicePropertiesTable( + props: ServicePropertiesTableProps, +) { + const [propertiesState] = useQueryManager({ + initQuery: props.server, + processQuery: async (server, signal) => { + return await queryDruidSql( + { + query: `SELECT "property", "value", "service_name", "node_roles" FROM sys.server_properties WHERE "server" = ${L( + server, + )} ORDER BY "property" ASC`, + }, + signal, + ); + }, + }); + + function renderTable() { + const properties = propertiesState.data || []; + const firstRow = properties[0]; + return ( + <> + {firstRow && ( +

+ Server {props.server} is running as{' '} + {firstRow.service_name}. +

+ )} + STANDARD_TABLE_PAGE_SIZE} + filterable + columns={[ + { + Header: 'Property', + accessor: 'property', + width: 300, + className: 'padded', + }, + { + Header: 'Value', + accessor: 'value', + width: 600, + className: 'padded code', + }, + ]} + noDataText={propertiesState.getErrorMessage() || 'No properties found'} + /> + + ); + } + + return ( +
+ {propertiesState.loading ? : renderTable()} +
+ ); +}); diff --git a/web-console/src/dialogs/service-table-action-dialog/service-table-action-dialog.tsx b/web-console/src/dialogs/service-table-action-dialog/service-table-action-dialog.tsx new file mode 100644 index 000000000000..d63380df379b --- /dev/null +++ b/web-console/src/dialogs/service-table-action-dialog/service-table-action-dialog.tsx @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState } from 'react'; + +import type { BasicAction } from '../../utils/basic-action'; +import type { SideButtonMetaData } from '../table-action-dialog/table-action-dialog'; +import { TableActionDialog } from '../table-action-dialog/table-action-dialog'; + +import { ServicePropertiesTable } from './service-properties-table/service-properties-table'; + +type ServiceTableActionDialogTab = 'properties'; + +interface ServiceTableActionDialogProps { + service: string; + actions: BasicAction[]; + onClose: () => void; +} + +export const ServiceTableActionDialog = React.memo(function ServiceTableActionDialog( + props: ServiceTableActionDialogProps, +) { + const { service, actions, onClose } = props; + const [activeTab, setActiveTab] = useState('properties'); + + const sideButtonMetadata: SideButtonMetaData[] = [ + { + icon: 'properties', + text: 'Properties', + active: activeTab === 'properties', + onClick: () => setActiveTab('properties'), + }, + ]; + + return ( + + {activeTab === 'properties' && } + + ); +}); diff --git a/web-console/src/views/services-view/services-view.tsx b/web-console/src/views/services-view/services-view.tsx index 6bb6b485e428..adb920ac6773 100644 --- a/web-console/src/views/services-view/services-view.tsx +++ b/web-console/src/views/services-view/services-view.tsx @@ -31,12 +31,13 @@ import { ActionCell, MoreButton, RefreshButton, + TableClickableCell, TableColumnSelector, type TableColumnSelectorColumn, TableFilterableCell, ViewControlBar, } from '../../components'; -import { AsyncActionDialog } from '../../dialogs'; +import { AsyncActionDialog, ServiceTableActionDialog } from '../../dialogs'; import type { QueryWithContext } from '../../druid-models'; import { getConsoleViewIcon } from '../../druid-models'; import type { Capabilities, CapabilitiesMode } from '../../helpers'; @@ -138,6 +139,9 @@ export interface ServicesViewState { middleManagerDisableWorkerHost?: string; middleManagerEnableWorkerHost?: string; + serviceTableActionDialogServer?: string; + serviceTableActionDialogActions: BasicAction[]; + visibleColumns: LocalStorageBackedVisibility; } @@ -283,6 +287,8 @@ ORDER BY this.state = { servicesState: QueryState.INIT, + serviceTableActionDialogActions: [], + visibleColumns: new LocalStorageBackedVisibility( LocalStorageKeys.SERVICE_TABLE_COLUMN_SELECTION, ), @@ -477,7 +483,34 @@ ORDER BY show: visibleColumns.shown('Service'), accessor: 'service', width: 300, - Cell: this.renderFilterableCell('service'), + Cell: ({ value, original, aggregated }) => { + if (aggregated) return ''; + + const { service_type } = original; + const workerInfo = workerInfoLookup[value]; + + // Make clickable if SQL is available to show properties + if (!capabilities.hasSql()) return value; + + return ( + + this.setState({ + serviceTableActionDialogServer: value, + serviceTableActionDialogActions: this.getServiceActions( + value, + service_type, + workerInfo, + ), + }) + } + hoverIcon={IconNames.PROPERTIES} + > + {value} + + ); + }, Aggregated: () => '', }, { @@ -794,22 +827,24 @@ ORDER BY }, { Header: ACTION_COLUMN_LABEL, - show: capabilities.hasOverlordAccess(), + show: capabilities.hasSql(), id: ACTION_COLUMN_ID, width: ACTION_COLUMN_WIDTH, accessor: 'service', filterable: false, sortable: false, - Cell: ({ value, aggregated }) => { + Cell: ({ value, original, aggregated }) => { if (aggregated) return ''; + const { service_type } = original; const workerInfo = workerInfoLookup[value]; - if (!workerInfo) return null; - const { worker } = workerInfo; - const disabled = worker.version === ''; - const workerActions = this.getWorkerActions(worker.host, disabled); - return ; + // Get all applicable actions (worker actions + properties action) + const serviceActions = this.getServiceActions(value, service_type, workerInfo, true); + if (serviceActions.length === 0) return null; + + const menuTitle = workerInfo ? workerInfo.worker.host : value; + return ; }, Aggregated: () => '', }, @@ -817,24 +852,53 @@ ORDER BY }, ); - private getWorkerActions(workerHost: string, disabled: boolean): BasicAction[] { - if (disabled) { - return [ - { + private getServiceActions( + server: string, + serviceType: string, + workerInfo?: WorkerInfo, + fromTable?: boolean, + ): BasicAction[] { + const actions: BasicAction[] = []; + + // Add worker-specific actions (enable/disable) if this is a worker + if (workerInfo) { + const { worker } = workerInfo; + const disabled = worker.version === ''; + + if (disabled) { + actions.push({ icon: IconNames.TICK, title: 'Enable', - onAction: () => this.setState({ middleManagerEnableWorkerHost: workerHost }), - }, - ]; - } else { - return [ - { + onAction: () => this.setState({ middleManagerEnableWorkerHost: worker.host }), + }); + } else { + actions.push({ icon: IconNames.DISABLE, title: 'Disable', - onAction: () => this.setState({ middleManagerDisableWorkerHost: workerHost }), + onAction: () => this.setState({ middleManagerDisableWorkerHost: worker.host }), + }); + } + } + + // Add properties action when called from table + if (fromTable) { + actions.push({ + icon: IconNames.PROPERTIES, + title: 'View properties', + onAction: () => { + this.setState({ + serviceTableActionDialogServer: server, + serviceTableActionDialogActions: this.getServiceActions( + server, + serviceType, + workerInfo, + ), + }); }, - ]; + }); } + + return actions; } renderDisableWorkerAction() { @@ -969,7 +1033,21 @@ ORDER BY {this.renderServicesTable()} {this.renderDisableWorkerAction()} {this.renderEnableWorkerAction()} + {this.renderServiceTableActionDialog()} ); } + + renderServiceTableActionDialog() { + const { serviceTableActionDialogServer, serviceTableActionDialogActions } = this.state; + if (!serviceTableActionDialogServer) return; + + return ( + this.setState({ serviceTableActionDialogServer: undefined })} + /> + ); + } } From f01304d3132b55886b25f51745f1f36eb8210598 Mon Sep 17 00:00:00 2001 From: Vadim Ogievetsky Date: Tue, 27 Jan 2026 15:02:26 -0800 Subject: [PATCH 2/3] Add trim starts --- web-console/src/components/clearable-input/clearable-input.tsx | 2 +- .../src/dialogs/lookup-edit-dialog/lookup-edit-dialog.tsx | 2 +- web-console/src/react-table/react-table-inputs.tsx | 2 +- .../components/column-picker-menu/column-picker-menu.tsx | 2 +- .../views/explore-view/components/control-pane/control-pane.tsx | 2 +- .../contains-filter-control/contains-filter-control.tsx | 2 +- .../filter-menu/regexp-filter-control/regexp-filter-control.tsx | 2 +- .../expression-editor-dialog/expression-editor-dialog.tsx | 2 +- .../workbench-view/tab-rename-dialog/tab-rename-dialog.tsx | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web-console/src/components/clearable-input/clearable-input.tsx b/web-console/src/components/clearable-input/clearable-input.tsx index de75d7dbdca7..926c34cf8401 100644 --- a/web-console/src/components/clearable-input/clearable-input.tsx +++ b/web-console/src/components/clearable-input/clearable-input.tsx @@ -35,7 +35,7 @@ export const ClearableInput = React.memo(function ClearableInput(props: Clearabl onValueChange(e.target.value)} + onChange={(e: any) => onValueChange(e.target.value.trimStart())} rightElement={ value ? (