diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx index 1898f39e70d..9f1eeeee5dc 100644 --- a/web-console/src/console-application.tsx +++ b/web-console/src/console-application.tsx @@ -25,7 +25,7 @@ import { HashRouter, Route, Switch } from 'react-router-dom'; import { ExternalLink, HeaderActiveTab, HeaderBar, Loader } from './components'; import { AppToaster } from './singletons/toaster'; -import { QueryManager } from './utils'; +import { localStorageGet, LocalStorageKeys, QueryManager } from './utils'; import { DRUID_DOCS_API, DRUID_DOCS_SQL } from './variables'; import { DatasourcesView, @@ -56,20 +56,27 @@ export class ConsoleApplication extends React.PureComponent< ConsoleApplicationProps, ConsoleApplicationState > { - static MESSAGE_KEY = 'druid-console-message'; - static MESSAGE_DISMISSED = 'dismissed'; + static STATUS_TIMEOUT = 2000; + private capabilitiesQueryManager: QueryManager; static async discoverCapabilities(): Promise { + const capabilitiesOverride = localStorageGet(LocalStorageKeys.CAPABILITIES_OVERRIDE); + if (capabilitiesOverride) return capabilitiesOverride as Capabilities; + try { - await axios.post('/druid/v2/sql', { query: 'SELECT 1337' }); + await axios.post( + '/druid/v2/sql', + { query: 'SELECT 1337', context: { timeout: ConsoleApplication.STATUS_TIMEOUT } }, + { timeout: ConsoleApplication.STATUS_TIMEOUT }, + ); } catch (e) { const { response } = e; if (response.status !== 405 || response.statusText !== 'Method Not Allowed') { return 'working-with-sql'; // other failure } try { - await axios.get('/status'); + await axios.get('/status', { timeout: ConsoleApplication.STATUS_TIMEOUT }); } catch (e) { return 'broken'; // total failure } diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index 33e1437a21b..63e6a4c6fe7 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -321,3 +321,7 @@ export function downloadFile(text: string, type: string, filename: string): void }); FileSaver.saveAs(blob, filename); } + +export function escapeSqlIdentifier(identifier: string): string { + return `"${identifier.replace(/"/g, '""')}"`; +} diff --git a/web-console/src/utils/local-storage-keys.tsx b/web-console/src/utils/local-storage-keys.tsx index 620dc0e7de5..0dec7d50aa2 100644 --- a/web-console/src/utils/local-storage-keys.tsx +++ b/web-console/src/utils/local-storage-keys.tsx @@ -17,6 +17,7 @@ */ export const LocalStorageKeys = { + CAPABILITIES_OVERRIDE: 'capabilities-override' as 'capabilities-override', INGESTION_SPEC: 'ingestion-spec' as 'ingestion-spec', DATASOURCE_TABLE_COLUMN_SELECTION: 'datasource-table-column-selection' as 'datasource-table-column-selection', SEGMENT_TABLE_COLUMN_SELECTION: 'segment-table-column-selection' as 'segment-table-column-selection', diff --git a/web-console/src/views/datasource-view/datasource-view.tsx b/web-console/src/views/datasource-view/datasource-view.tsx index d9dfae14c9a..aca475b4b5d 100644 --- a/web-console/src/views/datasource-view/datasource-view.tsx +++ b/web-console/src/views/datasource-view/datasource-view.tsx @@ -37,6 +37,7 @@ import { AppToaster } from '../../singletons/toaster'; import { addFilter, countBy, + escapeSqlIdentifier, formatBytes, formatNumber, getDruidErrorMessage, @@ -558,7 +559,7 @@ GROUP BY 1`; { icon: IconNames.APPLICATION, title: 'Query with SQL', - onAction: () => goToQuery(`SELECT * FROM "${datasource}"`), + onAction: () => goToQuery(`SELECT * FROM ${escapeSqlIdentifier(datasource)}`), }, { icon: IconNames.AUTOMATIC_UPDATES, diff --git a/web-console/src/views/query-view/column-tree/column-tree.tsx b/web-console/src/views/query-view/column-tree/column-tree.tsx index 4e0d24f6c74..e83371808fe 100644 --- a/web-console/src/views/query-view/column-tree/column-tree.tsx +++ b/web-console/src/views/query-view/column-tree/column-tree.tsx @@ -21,7 +21,7 @@ import { IconNames } from '@blueprintjs/icons'; import React, { ChangeEvent } from 'react'; import { Loader } from '../../../components'; -import { groupBy } from '../../../utils'; +import { escapeSqlIdentifier, groupBy } from '../../../utils'; import { ColumnMetadata } from '../../../utils/column-metadata'; import './column-tree.scss'; @@ -159,13 +159,13 @@ export class ColumnTree extends React.PureComponent String(child.label)); + columns = nodeData.childNodes.map(child => escapeSqlIdentifier(String(child.label))); } else { columns = ['*']; } if (tableSchema === 'druid') { onQueryStringChange(`SELECT ${columns.join(', ')} -FROM "${nodeData.label}" +FROM ${escapeSqlIdentifier(String(nodeData.label))} WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`); } else { onQueryStringChange(`SELECT ${columns.join(', ')} @@ -176,23 +176,31 @@ FROM ${tableSchema}.${nodeData.label}`); case 2: // Column const schemaNode = selectedNode; const columnSchema = schemaNode.label; - const columnTable = schemaNode.childNodes ? schemaNode.childNodes[nodePath[0]].label : '?'; + const columnTable = schemaNode.childNodes + ? String(schemaNode.childNodes[nodePath[0]].label) + : '?'; if (columnSchema === 'druid') { if (nodeData.icon === IconNames.TIME) { - onQueryStringChange(`SELECT TIME_FLOOR("${nodeData.label}", 'PT1H') AS "Time", COUNT(*) AS "Count" -FROM "${columnTable}" + onQueryStringChange(`SELECT + TIME_FLOOR(${escapeSqlIdentifier(String(nodeData.label))}, 'PT1H') AS "Time", + COUNT(*) AS "Count" +FROM ${escapeSqlIdentifier(columnTable)} WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY GROUP BY 1 ORDER BY "Time" ASC`); } else { - onQueryStringChange(`SELECT "${nodeData.label}", COUNT(*) AS "Count" -FROM "${columnTable}" + onQueryStringChange(`SELECT + "${nodeData.label}", + COUNT(*) AS "Count" +FROM ${escapeSqlIdentifier(columnTable)} WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY GROUP BY 1 ORDER BY "Count" DESC`); } } else { - onQueryStringChange(`SELECT "${nodeData.label}", COUNT(*) AS "Count" + onQueryStringChange(`SELECT + ${escapeSqlIdentifier(String(nodeData.label))}, + COUNT(*) AS "Count" FROM ${columnSchema}.${columnTable} GROUP BY 1 ORDER BY "Count" DESC`);