diff --git a/webapp/packages/plugin-data-spreadsheet-new/package.json b/webapp/packages/plugin-data-spreadsheet-new/package.json index bca6cf863ac..2b1e55e2cd8 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/package.json +++ b/webapp/packages/plugin-data-spreadsheet-new/package.json @@ -25,6 +25,7 @@ "dependencies": { "@cloudbeaver/core-blocks": "workspace:*", "@cloudbeaver/core-browser": "workspace:*", + "@cloudbeaver/core-connections": "workspace:*", "@cloudbeaver/core-data-context": "workspace:*", "@cloudbeaver/core-di": "workspace:*", "@cloudbeaver/core-dialogs": "workspace:*", diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableStatusIndicator.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableStatusIndicator.tsx index e4a5357cb86..9422a9db0bf 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableStatusIndicator.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableStatusIndicator.tsx @@ -9,8 +9,9 @@ import { SqlRowIdentifierState, type SqlResultColumn } from '@cloudbeaver/core-s import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; -import { IconOrImage, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { isResultSetDataSource } from '@cloudbeaver/plugin-data-viewer'; +import { IconOrImage, s, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; +import { DatabaseDataFeature, isResultSetDataSource } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContext } from '../DataGridContext.js'; import { TableDataContext } from '../TableDataContext.js'; @@ -20,16 +21,27 @@ import styles from './TableStatusIndicator.module.css'; export const TableStatusIndicator = observer(function TableStatusIndicator() { const dataGridContext = useContext(DataGridContext); const tableDataContext = useContext(TableDataContext); - const readOnlyConnection = dataGridContext.model.isReadonly(dataGridContext.resultIndex); const translate = useTranslate(); + + const source = dataGridContext.model.source; + const resultSetSource = isResultSetDataSource(source) ? source : null; + + /* We do NOT use `model.isReadonly()` here — + that method aggregates several unrelated reasons + and loses information about WHY editing isn't allowed. */ + const contextInfo = resultSetSource?.executionContext?.context; + const connectionKey = contextInfo ? createConnectionParam(contextInfo.projectId, contextInfo.connectionId) : null; + const connectionInfoLoader = useResource(TableStatusIndicator, ConnectionInfoResource, connectionKey); + const readOnlyConnection = connectionInfoLoader.data?.readOnly ?? false; + + const readOnlyPresentation = source.hasFeature(DatabaseDataFeature.Grouping); + const style = useS(styles); if (!tableDataContext || !dataGridContext) { return null; } - const source = dataGridContext.model.source; - const resultSetSource = isResultSetDataSource(source) ? source : null; const rowIdentifierInfo = resultSetSource?.getRowIdentifierInfo(dataGridContext.resultIndex); const hasRowIdentifier = resultSetSource?.hasElementIdentifier(dataGridContext.resultIndex); @@ -43,7 +55,10 @@ export const TableStatusIndicator = observer(function TableStatusIndicator() { const isPrimaryKey = rowIdentifierInfo?.state === SqlRowIdentifierState.PrimaryKey; const tooltipParts: string[] = []; - if (readOnlyConnection) { + // Presentation-level read-only takes precedence over connection-level read-only. + if (readOnlyPresentation) { + tooltipParts.push(translate('data_grid_table_readonly_presentation_tooltip')); + } else if (readOnlyConnection) { tooltipParts.push(translate('data_grid_table_readonly_connection_tooltip')); } @@ -59,6 +74,15 @@ export const TableStatusIndicator = observer(function TableStatusIndicator() { tooltipParts.push(translate('data_grid_table_virtual_key_tooltip')); } + const showLockIcon = readOnlyConnection || readOnlyPresentation; + + // Hide the entire indicator when there's nothing meaningful to display (Session Manager) + const hasInfo = showLockIcon || !!readOnlyStatus || hasRowIdentifier || isVirtualKey || isPrimaryKey; + + if (!hasInfo) { + return null; + } + const tooltip = tooltipParts.join('\n'); return ( @@ -66,7 +90,7 @@ export const TableStatusIndicator = observer(function TableStatusIndicator() { title={tooltip} className="tw:absolute tw:top-1/2 tw:left-1 tw:-translate-y-1/2 tw:z-1 tw:pointer-events-auto tw:flex tw:items-center tw:gap-1 tw:cursor-help" > - {readOnlyConnection && } + {showLockIcon && }
- extends DatabaseDataSource -{ +export abstract class ResultSetDataSource extends DatabaseDataSource< + TOptions, + IDatabaseResultSet +> { executionContext: IConnectionExecutionContext | null; totalCountRequestTask: ITask | null; private keepExecutionContextOnDispose: boolean; diff --git a/webapp/yarn.lock b/webapp/yarn.lock index b3d07b4a4f1..eb6a7b7f129 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2957,6 +2957,7 @@ __metadata: "@cloudbeaver/core-blocks": "workspace:*" "@cloudbeaver/core-browser": "workspace:*" "@cloudbeaver/core-cli": "workspace:*" + "@cloudbeaver/core-connections": "workspace:*" "@cloudbeaver/core-data-context": "workspace:*" "@cloudbeaver/core-di": "workspace:*" "@cloudbeaver/core-dialogs": "workspace:*"