Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions src/components/QueryResultTable/QueryResultTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import React from 'react';

import DataTable from '@gravity-ui/react-data-table';
import type {Column, Settings} from '@gravity-ui/react-data-table';
import {ClipboardButton} from '@gravity-ui/uikit';

import {buildTsvBlobParts} from '../../containers/Tenant/Query/utils/getPreparedResult';
import type {ColumnType, KeyValueRow} from '../../types/api/query';
import {cn} from '../../utils/cn';
import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants';
Expand All @@ -28,32 +30,50 @@ export const b = cn('ydb-query-result-table');

const WIDTH_PREDICTION_ROWS_COUNT = 100;

//used buildTsvBlobParts to convert row to tsv format for copying, so that copied value is the same as when user exports data to tsv
const rowToTsv = (row: KeyValueRow) => buildTsvBlobParts([row]).slice(2).join('');

const copyColumn: Column<KeyValueRow> = {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid a fixed synthetic column name for the copy action

This action column uses a hard-coded name, copy_action, but query results can legally contain a column with the same alias. In that case QueryResultTable passes duplicate column names to react-data-table, which uses column names as accessors and state keys, so a query like SELECT 1 AS "copy_action" can make the real result column ambiguous and lead to the wrong data being extracted or rendered for one of the columns.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@astandrik The fixed copy column name is now generated dynamically inside useMemo by checking against actual column names in the result. Starts with 'copy', and if that already exists as a query result column it wraps to '_copy_', then '__copy__' and so on until a unique name is found. This guarantees no collision with any real query column name regardless of what the user queries.

name: 'copy',
header: '',
width: 40,
render: ({row}) => (
<ClipboardButton
text={rowToTsv(row)}
view="flat-secondary"
size="s"
title={i18n('action.copy-row')}
/>
),
};
const prepareTypedColumns = (columns: ColumnType[], data: KeyValueRow[] | undefined) => {
if (!columns.length) {
return [];
}

const dataSlice = data?.slice(0, WIDTH_PREDICTION_ROWS_COUNT);

return columns.map(({name, type}) => {
const dataColumns = columns.map(({name, type}) => {
const columnType = getColumnType(type);

const column: Column<KeyValueRow> = {
name,
width: getColumnWidth({data: dataSlice, name}),
align: columnType === 'number' ? DataTable.RIGHT : DataTable.LEFT,
render: ({row}) => {
const data = row[name];
const rowData = row[name];
const normalizedData =
columnType === 'binary-string' && typeof data === 'string'
? JSON.stringify(data).slice(1, -1)
: String(data);
columnType === 'binary-string' && typeof rowData === 'string'
? JSON.stringify(rowData).slice(1, -1)
: String(rowData);
return <Cell value={normalizedData} />;
},
};

return column;
});

return dataColumns;
};

const prepareGenericColumns = (data: KeyValueRow[] | undefined) => {
Expand All @@ -63,7 +83,7 @@ const prepareGenericColumns = (data: KeyValueRow[] | undefined) => {

const dataSlice = data?.slice(0, WIDTH_PREDICTION_ROWS_COUNT);

return Object.keys(data[0]).map((name) => {
const dataColumns = Object.keys(data[0]).map((name) => {
const column: Column<KeyValueRow> = {
name,
width: getColumnWidth({data: dataSlice, name}),
Expand All @@ -73,6 +93,7 @@ const prepareGenericColumns = (data: KeyValueRow[] | undefined) => {

return column;
});
return dataColumns;
};

const getRowIndex = (_: unknown, index: number) => index;
Expand All @@ -91,7 +112,20 @@ export const QueryResultTable = (props: QueryResultTableProps) => {
const {columns, data, settings: propsSettings} = props;

const preparedColumns = React.useMemo(() => {
return columns ? prepareTypedColumns(columns, data) : prepareGenericColumns(data);
const dataColumns = columns
? prepareTypedColumns(columns, data)
: prepareGenericColumns(data);

if (!dataColumns.length) {
return dataColumns;
}
const existingNames = new Set(dataColumns.map((col) => col.name));
let copyColumnName = 'copy';
while (existingNames.has(copyColumnName)) {
copyColumnName = `_${copyColumnName}_`;
}

return [...dataColumns, {...copyColumn, name: copyColumnName}];
}, [columns, data]);

const settings = React.useMemo(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/QueryResultTable/i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"empty": "Table is empty"
"empty": "Table is empty",
"action.copy-row": "Copy row"
}
3 changes: 2 additions & 1 deletion src/components/QueryResultTable/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"empty": "Таблица пустая"
"empty": "Таблица пустая",
"action.copy-row": "Скопировать строку"
}
Loading