diff --git a/src/Client/README.md b/src/Client/README.md index 12bfc33..4ab7e45 100644 --- a/src/Client/README.md +++ b/src/Client/README.md @@ -3,61 +3,38 @@ Edit, run and chart Kusto queries (KQL). Explore databases and query results. -## Features - -- KQL Documents - - IntelliSense (auto completions) - - Syntax and Semantic coloring - - Semantic Copy/Paste - - Copy query text as colorized html - - Hover Tips - - Describes item under the mouse cursor - - Shows related diagnostics - - Syntax highlighting - - Highlights references to the same item - - Highlights balanced parentheses, brackets and braces - - Goto Definition - - Goto the declaration location of items declared in your query or as part of the database - - Find All References - - Find all references to an item in your query - - Diagnostics (errors/warnings) - - All kusto diagnostics appear in VS Code Problems tab - - Formatting - - Pretty print a single query using Format button above first line of query - - Pretty print the entire document using VS Code's Format Document (ALT-SHIFT-F). - - Adjust format settings in VS Code `Settings/Extensions/Kusto` - - Query execution - - Place the caret inside the query and press F5 - - Code Actions - - Quick fixes are indicated by ellipses beneath the related item, and are accissible within the hover tip. - - Refactorings are indicated by a lightbulb menu that appears after clicking on related text item. - - CoPilot also makes suggestions and offers fixes in the same UI. -- Connection panel - - Add and remove server connections - - Add and remove server group folders - - Drag and drop server into and out of groups folders - - Assign a connection to a KQL document by selecting a database - - Explore database contents and view entity definitions -- Results Panel - - View and explorer the tabular results of queries - - Tab through multiple result tables - - Copy a table as html and markdown - - Copy a table as a Kusto datatable expression -- Chart Panel - - View chart as result of query with render operator - - Copy charts as SVG, PNG and bitmap -- Chat Panel - - Ask CoPilot questions using @kusto in the chat interface - -## Usage - -1. Open a `.kql` or `.csl` file -2. Select or add a connection in the connections view of the explorer panel -3. Select a default database for the the document under the connection item -4. Write your query -5. Press F5 to execute +## Get Started +- Open or create a `.kql` file in VS Code +- Adjust placement of connection panel (it will appear at bottom of list) +- Add a server connection and select a default database for the document +- Write a Kusto query or consult copilot for help writing one +- Press F5 to execute the query and view results in the results panel +- Use the chart button in the results panel to create a chart for your result data +- Edit the chart options to customize the chart type, axes, legend and more +- Add another query to the kql document, rinse and repeat +- Save your results and charts as a .kqr file to share with others or re-open later + +## Query Editor (.kql documents) +- Syntax and semantic coloring of query text +- Autocompletion (Intellisense) +- Hover tips +- Pretty Printing (formatting) +- Goto definition and find all references for functions, tables, columns, and more +- Code actions and quick fixes for common issues and refactorings +- Copy colorized query text to the clipboard for pasting into other documents +- Have mutliple independent queries in the same document, separated by a blank line. + +## Results Panel (bottom panel) +- Copy contents of cells or entire table to clipboard +- Drag and drop a table into your document as a KQL datatable expression +- Chart your data (it will open in a results viewer tab) + +## Results Viewer (.kqr documents) +- Shows both result data and chart in the same view +- Copy the chart as an image to clipboard for either light-mode or dark-mode pasting. +- Copy the data to clipboard just like results panel. +- Edit the chart options to customize the chart type, axes, legend and more +- Save to a .kqr file to share with others or re-open later ## Requirements - -- VS Code 1.9.0 or higher - +- VS Code 1.9.0 or higher \ No newline at end of file diff --git a/src/Client/features/documentPanels.ts b/src/Client/features/documentPanels.ts index d40eb1b..ceec831 100644 --- a/src/Client/features/documentPanels.ts +++ b/src/Client/features/documentPanels.ts @@ -304,24 +304,20 @@ async function copyQueryTransparent(client: LanguageClient, codeLensRange?: serv return; } - const selection = { - start: { line: queryRange.start.line, character: queryRange.start.character }, - end: { line: queryRange.end.line, character: queryRange.end.character } - }; - - // Request light-mode HTML from the server (darkMode = false) - const result = await server.getQueryAsHtml(client, uri, selection, false); - if (!result?.html) { - return; - } - - // Get plain text of the query for the text format + // Get plain text of the query const range = new vscode.Range( queryRange.start.line, queryRange.start.character, queryRange.end.line, queryRange.end.character ); const plainText = editor.document.getText(range); + // Request light-mode HTML from the server (darkMode = false) + const connection = await getDocumentConnection(uri); + const result = await server.getQueryAsHtml(client, plainText, connection?.cluster, connection?.database, false); + if (!result?.html) { + return; + } + // Place both HTML and plain text on the clipboard const items: import('./clipboard').ClipboardItem[] = [ { format: 'HTML Format', data: wrapHtmlForClipboard(result.html) }, diff --git a/src/Client/features/resultsViewer.ts b/src/Client/features/resultsViewer.ts index a8a2eba..a13221b 100644 --- a/src/Client/features/resultsViewer.ts +++ b/src/Client/features/resultsViewer.ts @@ -398,6 +398,7 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { const state = editorStates.get(webviewPanel); const hasChart = !!state?.resultData?.chartOptions; vscode.commands.executeCommand('setContext', 'kusto.resultEditorHasChart', hasChart); + vscode.commands.executeCommand('setContext', 'kusto.resultEditorChartActive', state?.activeView === 'chart'); } }; webviewPanel.onDidChangeViewState(() => updateChartContext()); @@ -417,6 +418,7 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { if (state) { state.activeView = message.viewId; } + vscode.commands.executeCommand('setContext', 'kusto.resultEditorChartActive', message.viewId === 'chart'); return; } if (message.command === 'copyText' && typeof message.text === 'string') { @@ -429,19 +431,22 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { state.chartOptionsOverride = message.chartOptions as server.ChartOptions; if (chartOptionsTimer) { clearTimeout(chartOptionsTimer); } chartOptionsTimer = setTimeout(async () => { - await this.updateChartOnly(state, webviewPanel); - // Persist updated chart options to the backing file - const updatedData: server.ResultData = { ...state.resultData, chartOptions: state.chartOptionsOverride! }; - const content = JSON.stringify(updatedData, null, 2); - const fullRange = new vscode.Range( - document.positionAt(0), - document.positionAt(document.getText().length) - ); - const edit = new vscode.WorkspaceEdit(); - edit.replace(document.uri, fullRange, content); - ignoringSelfEdit = true; - await vscode.workspace.applyEdit(edit); - ignoringSelfEdit = false; + try { + await this.updateChartOnly(state, webviewPanel); + // Persist updated chart options to the backing file + const updatedData: server.ResultData = { ...state.resultData, chartOptions: state.chartOptionsOverride! }; + const content = JSON.stringify(updatedData, null, 2); + const fullRange = new vscode.Range( + document.positionAt(0), + document.positionAt(document.getText().length) + ); + const edit = new vscode.WorkspaceEdit(); + edit.replace(document.uri, fullRange, content); + ignoringSelfEdit = true; + await vscode.workspace.applyEdit(edit); + } finally { + ignoringSelfEdit = false; + } }, 600); return; } @@ -488,6 +493,7 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { if (chartOptionsTimer) { clearTimeout(chartOptionsTimer); } editorStates.delete(webviewPanel); vscode.commands.executeCommand('setContext', 'kusto.resultEditorHasChart', false); + vscode.commands.executeCommand('setContext', 'kusto.resultEditorChartActive', false); changeSubscription.dispose(); themeSubscription.dispose(); }); @@ -511,7 +517,7 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { const darkMode = isDarkMode(); - // Fetch both table HTML and chart HTML in parallel + // Fetch table HTML and chart HTML in parallel const [dataResult, chartResult] = await Promise.all([ Promise.resolve(resultDataToHtml(resultData)), resultData.chartOptions @@ -525,6 +531,8 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { // Update context key for editor title actions if (webviewPanel.active) { vscode.commands.executeCommand('setContext', 'kusto.resultEditorHasChart', hasChart); + const activeView = editorStates.get(webviewPanel)?.activeView ?? (hasChart ? 'chart' : 'table-0'); + vscode.commands.executeCommand('setContext', 'kusto.resultEditorChartActive', activeView === 'chart'); } if (!hasTable && !hasChart) { @@ -539,12 +547,14 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { editorStates.set(webviewPanel, { resultData, tableNames, - activeView: existingState?.activeView ?? firstActiveView + activeView: existingState?.activeView ?? firstActiveView, + chartOptionsOverride: existingState?.chartOptionsOverride }); const chartOptions = existingState?.chartOptionsOverride ?? resultData.chartOptions; const columnNames = resultData.tables[0]?.columns?.map(c => c.name) ?? []; - const html = this.buildDualViewHtml(dataResult, chartResult?.html, hasChart, chartOptions, columnNames); + const html = this.buildDualViewHtml(dataResult, chartResult?.html, hasChart, chartOptions, columnNames, + resultData.query, resultData.cluster, resultData.database); webviewPanel.webview.html = injectChartMessageHandler(html); } @@ -570,13 +580,16 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { chartHtml: string | undefined, hasChart: boolean, chartOptions?: server.ChartOptions, - columnNames?: string[] + columnNames?: string[], + queryText?: string, + cluster?: string, + database?: string ): string { const tables = dataResult?.tables ?? []; // Build individual table divs const tableContents = tables.map((t, i) => - `
${t.html}
` + `
${t.html}
` ).join(''); // Extract the chart body content from the full HTML @@ -603,6 +616,11 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { ).join(''); } + const hasQuery = !!queryText; + const queryButton = hasQuery + ? `` + : ''; + const firstActiveView = hasChart ? 'chart' : 'table-0'; // Build chart options edit panel @@ -777,6 +795,7 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { opacity: 1; } /* Table styles */ + .view-content table:first-child { margin-top: 4px; } table { border-collapse: collapse; width: 100%; } th, td { padding: 4px 8px; @@ -791,17 +810,46 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { z-index: 1; font-weight: 600; } + /* Query tab styles */ + .query-info { + padding: 8px 12px; + display: flex; + gap: 16px; + border-bottom: 1px solid var(--vscode-panel-border, #444); + background: var(--vscode-editorWidget-background, var(--vscode-editor-background)); + } + .query-meta { + font-size: 12px; + color: var(--vscode-descriptionForeground, var(--vscode-foreground)); + } + .query-content { + margin: 0; + padding: 4px 12px; + overflow: auto; + font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace); + font-size: var(--vscode-editor-font-size, 13px); + white-space: pre-wrap; + color: var(--vscode-descriptionForeground, grey); + }
${chartButton} ${tableButtons} + ${queryButton}
- ${hasChart ? `
${chartContent}
` : ''} + ${hasChart ? `
${chartContent}
` : ''} ${tableContents} + ${hasQuery ? `
+
+ ${cluster ? `Cluster: ${this.escapeHtml(cluster)}` : ''} + ${database ? `Database: ${this.escapeHtml(database)}` : ''} +
+
${this.escapeHtml(queryText!)}
+
` : ''}
${editPanelHtml}
@@ -825,7 +873,7 @@ export class ResultEditorProvider implements vscode.CustomTextEditorProvider { // Hide/restore edit panel based on view and user preference var editPanel = document.getElementById('edit-panel'); if (editPanel) { - if (viewId.startsWith('table-')) { + if (viewId.startsWith('table-') || viewId === 'query') { editPanel.classList.remove('visible'); } else if (editPanelUserVisible) { editPanel.classList.add('visible'); @@ -1201,7 +1249,8 @@ export async function displayChart( const hasChart = !!chartResult?.html; const columnNames = resultData.tables[0]?.columns?.map(c => c.name) ?? []; - const html = htmlBuilder.buildDualViewHtml(dataResult, chartResult?.html, hasChart, chartOptions, columnNames); + const html = htmlBuilder.buildDualViewHtml(dataResult, chartResult?.html, hasChart, chartOptions, columnNames, + resultData.query, resultData.cluster, resultData.database); showSingletonPanel(injectChartMessageHandler(html), resultData, (dataResult?.tables ?? []).map(t => t.name)); } @@ -1230,7 +1279,7 @@ function showSingletonPanel(html: string, resultData: server.ResultData, tableNa if (!singletonPanel) { singletonPanel = vscode.window.createWebviewPanel( 'kusto', - 'Chart', + 'Results', { viewColumn, preserveFocus: true }, { enableScripts: true, retainContextWhenHidden: true } ); @@ -1240,8 +1289,10 @@ function showSingletonPanel(html: string, resultData: server.ResultData, tableNa singletonPanel.onDidChangeViewState(() => { if (singletonPanel?.active) { - const hasChart = !!editorStates.get(singletonPanel)?.resultData?.chartOptions; + const state = editorStates.get(singletonPanel); + const hasChart = !!state?.resultData?.chartOptions; vscode.commands.executeCommand('setContext', 'kusto.resultEditorHasChart', hasChart); + vscode.commands.executeCommand('setContext', 'kusto.resultEditorChartActive', state?.activeView === 'chart'); } }); @@ -1249,6 +1300,7 @@ function showSingletonPanel(html: string, resultData: server.ResultData, tableNa if (message.command === 'viewChanged' && typeof message.viewId === 'string') { const state = editorStates.get(singletonPanel!); if (state) { state.activeView = message.viewId; } + vscode.commands.executeCommand('setContext', 'kusto.resultEditorChartActive', message.viewId === 'chart'); return; } if (message.command === 'copyText' && typeof message.text === 'string') { @@ -1279,6 +1331,7 @@ function showSingletonPanel(html: string, resultData: server.ResultData, tableNa singletonResultData = undefined; singletonChartOptionsOverride = undefined; vscode.commands.executeCommand('setContext', 'kusto.resultEditorHasChart', false); + vscode.commands.executeCommand('setContext', 'kusto.resultEditorChartActive', false); vscode.commands.executeCommand('kusto.chartPanelStateChanged'); }); } @@ -1291,6 +1344,8 @@ function showSingletonPanel(html: string, resultData: server.ResultData, tableNa activeView: hasChart ? 'chart' : 'table-0' }); + vscode.commands.executeCommand('setContext', 'kusto.resultEditorChartActive', hasChart); + singletonPanel.webview.html = html; singletonPanel.reveal(viewColumn, true); } diff --git a/src/Client/features/server.ts b/src/Client/features/server.ts index 9fe1492..0f2bddd 100644 --- a/src/Client/features/server.ts +++ b/src/Client/features/server.ts @@ -230,26 +230,24 @@ export interface ChartAsHtmlResult { } /** - * Gets the HTML representation of a query from the document. + * Gets the HTML representation of query text with syntax colorization. * @param client The language client for LSP communication - * @param uri The document URI - * @param selection The selection range of the query + * @param query The query text to colorize + * @param cluster Optional cluster name for semantic coloring + * @param database Optional database name for semantic coloring * @param darkMode Whether to render in dark mode - * @returns The query as HTML, or null if not available + * @returns The colorized query as HTML, or null if not available */ export function getQueryAsHtml( client: LanguageClient, - uri: string, - selection: SelectionRange, + query: string, + cluster?: string, + database?: string, darkMode?: boolean ): Promise { return client.sendRequest( 'kusto/getQueryAsHtml', - { - textDocument: { uri }, - selection, - darkMode - } + { query, cluster, database, darkMode } ); } @@ -386,6 +384,9 @@ export interface DataAsExpression { /** Serializable representation of a query execution result. */ export interface ResultData { + query?: string; + cluster?: string; + database?: string; tables: ResultTable[]; chartOptions?: ChartOptions; } diff --git a/src/Client/package-lock.json b/src/Client/package-lock.json index 2230f11..2258daf 100644 --- a/src/Client/package-lock.json +++ b/src/Client/package-lock.json @@ -1,12 +1,12 @@ { "name": "kusto-explorer-vscode", - "version": "0.1.1", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kusto-explorer-vscode", - "version": "0.1.1", + "version": "0.2.0", "license": "MIT", "dependencies": { "vscode-languageclient": "^9.0.0" diff --git a/src/Client/package.json b/src/Client/package.json index 649c14e..1ded4a1 100644 --- a/src/Client/package.json +++ b/src/Client/package.json @@ -342,22 +342,22 @@ }, { "command": "kusto.copyChartLight", - "when": "activeWebviewPanelId == kusto || activeWebviewPanelId == kusto.resultEditor", + "when": "(activeWebviewPanelId == kusto || activeWebviewPanelId == kusto.resultEditor) && kusto.resultEditorChartActive", "group": "navigation@1" }, { "command": "kusto.copyChartDark", - "when": "activeWebviewPanelId == kusto || activeWebviewPanelId == kusto.resultEditor", + "when": "(activeWebviewPanelId == kusto || activeWebviewPanelId == kusto.resultEditor) && kusto.resultEditorChartActive", "group": "navigation@2" }, { "command": "kusto.moveChartToMain", - "when": "activeWebviewPanelId == kusto || activeWebviewPanelId == kusto.resultEditor", + "when": "(activeWebviewPanelId == kusto || activeWebviewPanelId == kusto.resultEditor) && kusto.resultEditorChartActive", "group": "navigation@3" }, { "command": "kusto.toggleChartEditor", - "when": "(activeWebviewPanelId == kusto || activeWebviewPanelId == kusto.resultEditor) && kusto.resultEditorHasChart", + "when": "(activeWebviewPanelId == kusto || activeWebviewPanelId == kusto.resultEditor) && kusto.resultEditorHasChart && kusto.resultEditorChartActive", "group": "navigation@3" }, { @@ -391,17 +391,17 @@ "webview/context": [ { "command": "kusto.copyCell", - "when": "webviewId == 'kusto.resultsView' || (webviewId == 'kusto.resultEditor' && !chartVisible) || (webviewId == 'kusto' && !chartVisible)", + "when": "webviewId == 'kusto.resultsView' || (webviewId == 'kusto.resultEditor' && !chartVisible && !queryVisible) || (webviewId == 'kusto' && !chartVisible)", "group": "9_cutcopypaste@1" }, { "command": "kusto.copyData", - "when": "webviewId == 'kusto.resultsView' || (webviewId == 'kusto.resultEditor' && !chartVisible) || (webviewId == 'kusto' && !chartVisible)", + "when": "webviewId == 'kusto.resultsView' || (webviewId == 'kusto.resultEditor' && !chartVisible && !queryVisible) || (webviewId == 'kusto' && !chartVisible)", "group": "9_cutcopypaste@2" }, { "command": "kusto.copyTableAsExpression", - "when": "webviewId == 'kusto.resultsView' || (webviewId == 'kusto.resultEditor' && !chartVisible) || (webviewId == 'kusto' && !chartVisible)", + "when": "webviewId == 'kusto.resultsView' || (webviewId == 'kusto.resultEditor' && !chartVisible && !queryVisible) || (webviewId == 'kusto' && !chartVisible)", "group": "9_cutcopypaste@3" }, { diff --git a/src/Server/Charting/PlotlyChartManager.cs b/src/Server/Charting/PlotlyChartManager.cs index 4d101f2..7478bf1 100644 --- a/src/Server/Charting/PlotlyChartManager.cs +++ b/src/Server/Charting/PlotlyChartManager.cs @@ -893,8 +893,8 @@ private static bool TryGetDouble(object? value, out double result) private DataColumn[] Get2dYColumns(DataTable data, ChartOptions options, DataColumn keyColumn) { return options.YColumns != null - ? data.Columns.OfType().Where(c => options.YColumns.Contains(c.ColumnName)).ToArray() - : data.Columns.OfType().Where(c => c != keyColumn).ToArray(); + ? options.YColumns.Select(name => (DataColumn)data.Columns[name]!).Where(c => c != null).ToArray() // the exact columns requested in order + : data.Columns.OfType().Where(c => c != keyColumn).ToArray(); // the first column in table order that is not the key column } private (object[]? x, double[]? y) Get2DChartData(DataColumn xColumn, DataColumn yColumn) diff --git a/src/Server/Server.cs b/src/Server/Server.cs index e41c132..60f5b23 100644 --- a/src/Server/Server.cs +++ b/src/Server/Server.cs @@ -2076,7 +2076,7 @@ public class RunQueryDiagnostic { return new RunQueryResult { - Data = runResult.ExecuteResult != null ? ResultData.FromExecuteResult(runResult.ExecuteResult) : null, + Data = runResult.ExecuteResult != null ? ResultData.FromExecuteResult(runResult.ExecuteResult, @params.Query, runResult.Cluster, runResult.Database) : null, Connection = runResult.Connection, Cluster = runResult.Cluster, Database = runResult.Database @@ -2138,37 +2138,64 @@ public class RunQueryResult #region Html [JsonRpcMethod("kusto/getQueryAsHtml", UseSingleObjectParameterDeserialization = true)] - public Task OnGetQueryAsHtmlAsync(GetQueryAsHtmlParams @params, CancellationToken cancellationToken) + public async Task OnGetQueryAsHtmlAsync(GetQueryAsHtmlParams @params, CancellationToken cancellationToken) { try { - if (_documentManager.TryGetDocument(@params.TextDocument.Uri, out var document)) + // Get globals with cluster/database context for semantic coloring + var globals = _symbolManager.Globals; + if (@params.Cluster != null) { - var range = GetTextRange(document.Text, @params.Selection); - var queryFragment = document.ToHmtlFragment(range.Start, range.Length, @params.DarkMode == true); - return Task.FromResult( - new GetQueryAsHtmlResult + await _symbolManager.EnsureClustersAsync([@params.Cluster], contextCluster: null, cancellationToken).ConfigureAwait(false); + globals = _symbolManager.Globals; + if (globals.GetCluster(@params.Cluster) is { } clusterSymbol) + { + globals = globals.WithCluster(clusterSymbol); + if (@params.Database != null) { - Html = $"{queryFragment}" - }); + await _symbolManager.EnsureDatabaseAsync(@params.Cluster, @params.Database, contextCluster: null, cancellationToken).ConfigureAwait(false); + globals = _symbolManager.Globals; + clusterSymbol = globals.GetCluster(@params.Cluster); + if (clusterSymbol != null) + { + globals = globals.WithCluster(clusterSymbol); + if (clusterSymbol.GetDatabase(@params.Database) is { } databaseSymbol) + { + globals = globals.WithDatabase(databaseSymbol); + } + } + } + } } + + var tempUri = new Uri("temp://queryAsHtml"); + var document = new SectionedDocument(tempUri, @params.Query, globals); + var queryFragment = document.ToHmtlFragment(0, document.Text.Length, @params.DarkMode == true); + + return new GetQueryAsHtmlResult + { + Html = $"{queryFragment}" + }; } catch (Exception ex) { _ = this.SendWindowLogMessageAsync(ex.Message); } - return Task.FromResult(null); + return null; } [DataContract] public class GetQueryAsHtmlParams { - [DataMember(Name = "textDocument")] - public required LSP.TextDocumentIdentifier TextDocument { get; init; } + [DataMember(Name = "query")] + public required string Query { get; init; } + + [DataMember(Name = "cluster")] + public string? Cluster { get; init; } - [DataMember(Name = "selection")] - public required LSP.Range Selection { get; init; } + [DataMember(Name = "database")] + public string? Database { get; init; } [DataMember(Name = "darkMode")] public bool? DarkMode { get; init; } diff --git a/src/Server/Utilities/ResultData.cs b/src/Server/Utilities/ResultData.cs index 327943e..79d10f5 100644 --- a/src/Server/Utilities/ResultData.cs +++ b/src/Server/Utilities/ResultData.cs @@ -13,9 +13,33 @@ namespace Kusto.Vscode; [DataContract] public class ResultData { + /// + /// The query that produced the result + /// + [DataMember(Name = "query")] + public string? Query { get; init; } + + /// + /// The cluster that ran the query + /// + [DataMember(Name = "cluster")] + public string? Cluster { get; init; } + + /// + /// The default database context for the query + /// + [DataMember(Name = "database")] + public string? Database { get; init; } + + /// + /// The resulting data tables + /// [DataMember(Name = "tables")] public required ImmutableList Tables { get; init; } + /// + /// The chart options for rendering a chart from the result data + /// [DataMember(Name = "chartOptions")] public ChartOptions? ChartOptions { get; init; } @@ -23,9 +47,12 @@ public class ResultData /// Converts an to a . /// /// The query execution result to convert. + /// The query text that produced the result. + /// The cluster where the query was executed. + /// The database where the query was executed. /// Optional table name to filter to a specific table. /// The serializable result, or null if there are no tables. - public static ResultData FromExecuteResult(ExecuteResult executeResult, string? tableName = null) + public static ResultData FromExecuteResult(ExecuteResult executeResult, string? query = null, string? cluster = null, string? database = null, string? tableName = null) { ImmutableList resultTables = ImmutableList.Empty; @@ -44,6 +71,9 @@ public static ResultData FromExecuteResult(ExecuteResult executeResult, string? return new ResultData { + Query = query, + Cluster = cluster, + Database = database, Tables = resultTables, ChartOptions = executeResult.ChartOptions };