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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ The authentication process will try each method in sequence until successful aut

Note: For local development, Azure CLI or VS Code authentication is recommended.

## Application Insights Integration

The extension now provides improved support for Application Insights queries:

- **Trusted Endpoint Configuration**: The Application Insights API endpoint (`api.applicationinsights.io`) is automatically registered as a trusted endpoint, resolving previous "valid authentication not provided" errors
- **Custom API Client**: Uses a specialized client for Application Insights that properly handles API key authentication and response formats
- **Improved Error Messages**: Clearer error messages when App Insights credentials are missing or expired, with guidance on how to resolve the issue

### Connecting to Application Insights

1. Use the command `Configure Kusto Connection`
2. Select Application Insights as the connection type
3. Enter your Application Insights App ID and App Key
4. The extension will securely store your credentials

## Getting Started

- Open a `*.kql|*.csl` file and start typing to get code completion
Expand Down
147 changes: 117 additions & 30 deletions src/client/datatable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,44 +73,108 @@ function createAgGridData(resultTable: KustoResultTable | any) {
columnDataType.clear();

// Handle both KustoResultTable object and plain JSON from serialization
const isPlainJson = !resultTable.columns && resultTable.data;
// After JSON serialization, the rows() generator function is lost, so we need to check
// if rows is actually a function. If not, use the data array directly.
const hasRowsMethod = typeof resultTable.rows === 'function';

// Check for data in multiple possible locations (data, _rows, or raw array format)
const dataArray = resultTable.data || resultTable._rows || [];
const hasDataArray = Array.isArray(dataArray) && dataArray.length > 0;
const isPlainJson = !hasRowsMethod && hasDataArray;

console.log('[datatable] createAgGridData - hasRowsMethod:', hasRowsMethod);
console.log('[datatable] createAgGridData - hasDataArray:', hasDataArray, 'length:', dataArray.length);
console.log('[datatable] createAgGridData - isPlainJson:', isPlainJson);
console.log('[datatable] createAgGridData - resultTable keys:', Object.keys(resultTable || {}));

if (isPlainJson) {
// Plain JSON structure from serialized response
const rows = resultTable.data as any[];
if (rows.length === 0) {
return gridData;
}
const columns = resultTable.columns || [];
console.log('[datatable] Plain JSON path - columns count:', columns.length);
console.log('[datatable] Plain JSON path - first data item:', JSON.stringify(dataArray[0]).substring(0, 200));

// Infer columns from first row
const firstRow = rows[0];
const columnNames = Object.keys(firstRow);
// Check if rows are arrays (need conversion) or objects (ready to use)
const firstRow = dataArray[0];
const rowsAreArrays = Array.isArray(firstRow);
console.log('[datatable] Plain JSON path - rows are arrays:', rowsAreArrays);

for (const colName of columnNames) {
const columnDef: ColDef = {
headerName: colName,
field: colName,
sortable: true,
filter: true
};
// Build column definitions
if (columns.length > 0) {
// Use explicit columns from the response (preferred - preserves order and handles null values)
for (const col of columns) {
const colName = col.name || col.ColumnName || '';
const colType = col.type || col.ColumnType || 'string';
const columnDef: ColDef = {
headerName: colName,
field: colName,
sortable: true,
filter: true
};

// Check if value is an object (dynamic type)
const sampleValue = firstRow[colName];
if (sampleValue !== null && typeof sampleValue === 'object') {
columnDef.valueGetter = (param) => {
const cellData = param.data[colName];
return typeof cellData === 'string' ? cellData : JSON.stringify(cellData);
// Handle dynamic type columns
if (colType === 'dynamic') {
// Use IIFE to capture colName correctly in closure
const capturedColName = colName;
columnDef.valueGetter = (param) => {
const cellData = param.data?.[capturedColName];
return typeof cellData === 'string' ? cellData : JSON.stringify(cellData);
};
}
columnDataType.set(colName, colType);
gridData.columnDefs.push(columnDef);
}
} else if (!rowsAreArrays && firstRow) {
// Fallback: Infer columns from first row (may miss columns with null values)
const columnNames = Object.keys(firstRow);
console.log('[datatable] Inferring columns from first row:', columnNames.slice(0, 5));

for (const colName of columnNames) {
const columnDef: ColDef = {
headerName: colName,
field: colName,
sortable: true,
filter: true
};
columnDataType.set(colName, 'dynamic');
} else {
columnDataType.set(colName, typeof sampleValue);

// Check if value is an object (dynamic type)
const sampleValue = firstRow[colName];
if (sampleValue !== null && typeof sampleValue === 'object') {
const capturedColName = colName;
columnDef.valueGetter = (param) => {
const cellData = param.data?.[capturedColName];
return typeof cellData === 'string' ? cellData : JSON.stringify(cellData);
};
columnDataType.set(colName, 'dynamic');
} else {
columnDataType.set(colName, typeof sampleValue);
}

gridData.columnDefs.push(columnDef);
}
}

gridData.columnDefs.push(columnDef);
// Convert row data if needed
if (rowsAreArrays && columns.length > 0) {
// Convert array rows to objects using column names
console.log('[datatable] Converting array rows to objects');
gridData.rowData = dataArray.map((row: any[]) => {
const rowObj: { [key: string]: any } = {};
columns.forEach((col: any, idx: number) => {
const colName = col.name || col.ColumnName || `Column${idx}`;
rowObj[colName] = row[idx] !== undefined ? row[idx] : null;
});
return rowObj;
});
} else if (!rowsAreArrays) {
// Rows are already objects - use directly
console.log('[datatable] Using object rows directly');
gridData.rowData = dataArray;
} else {
console.warn('[datatable] Array rows but no columns - cannot convert');
}

// Rows are already in the correct format
gridData.rowData = rows;
console.log('[datatable] Final rowData count:', gridData.rowData.length);
console.log('[datatable] Final rowData[0]:', JSON.stringify(gridData.rowData[0] || {}).substring(0, 200));
} else {
// Proper KustoResultTable object with columns and rows() method
const columns = resultTable.columns;
Expand Down Expand Up @@ -145,12 +209,35 @@ function createAgGridData(resultTable: KustoResultTable | any) {
return gridData;
}
function renderDataTable(results: KustoResponseDataSet, ele: HTMLElement) {
console.log('renderDataTable', results);
console.log('[datatable] renderDataTable called with:', results);
console.log('[datatable] results keys:', Object.keys(results || {}));
console.log('[datatable] primaryResults length:', results?.primaryResults?.length);

if (!hasDataTable(results)) {
console.error('No data table');
console.error('[datatable] No data table found in results');
return;
}
const data = createAgGridData(results.primaryResults[0]);
const primaryResult = results.primaryResults[0];
console.log('[datatable] primaryResult keys:', Object.keys(primaryResult || {}));
console.log('[datatable] primaryResult.columns length:', primaryResult?.columns?.length);
console.log('[datatable] primaryResult.data exists:', !!(primaryResult as any).data);
console.log('[datatable] primaryResult.data length:', (primaryResult as any).data?.length);
console.log('[datatable] primaryResult._rows exists:', !!(primaryResult as any)._rows);
console.log('[datatable] primaryResult._rows length:', (primaryResult as any)._rows?.length);
console.log('[datatable] primaryResult.rows is function:', typeof (primaryResult as any).rows === 'function');

// Log first row from data or _rows
const firstDataRow = (primaryResult as any).data?.[0];
const firstRawRow = (primaryResult as any)._rows?.[0];
console.log('[datatable] First data row sample:', JSON.stringify(firstDataRow)?.substring(0, 300));
console.log('[datatable] First _rows sample:', JSON.stringify(firstRawRow)?.substring(0, 300));

const data = createAgGridData(primaryResult);
console.log('[datatable] AG Grid columnDefs count:', data.columnDefs.length);
console.log('[datatable] AG Grid rowData count:', data.rowData.length);
console.log('[datatable] AG Grid first columnDef:', JSON.stringify(data.columnDefs[0]));
console.log('[datatable] AG Grid first rowData:', JSON.stringify(data.rowData[0])?.substring(0, 300));

ReactDOM.render(React.createElement(DataTable, data, null), ele);
}

Expand Down
32 changes: 23 additions & 9 deletions src/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,33 @@ export function getTabularData(results: KustoResponseDataSet): TabularData | und
if (!hasDataTable(results)) {
return;
}
const primaryTable = results.primaryResults[0];
const primaryTable = results.primaryResults[0] as any;
const fields: Field[] = primaryTable.columns as any;
const dataPoints: Datapoint[] = [];
for (const row of primaryTable.rows()) {
const rowData: Datapoint = {};
primaryTable.columns.forEach((col) => {
if (col.name) {
const value = row.raw ? row.raw[col.ordinal] : row.toJSON()[col.name];
rowData[col.name] = value;

// Handle both KustoResultTable and plain JSON from notebook serialization
if (typeof primaryTable.rows === 'function') {
// KustoResultTable - use rows() generator
for (const row of primaryTable.rows()) {
const rowData: Datapoint = {};
primaryTable.columns.forEach((col: any) => {
if (col.name) {
const value = row.raw ? row.raw[col.ordinal] : row.toJSON()[col.name];
rowData[col.name] = value;
}
});
dataPoints.push(rowData);
}
} else if (primaryTable.data) {
// Plain JSON from serialization - use data array directly
return {
data: primaryTable.data,
schema: {
fields: fields
}
});
dataPoints.push(rowData);
};
}

return {
data: dataPoints,
schema: {
Expand Down
8 changes: 6 additions & 2 deletions src/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { registerInteractiveExperience } from './interactive/interactive';
import { registerExportCommand } from './content/export';
import { StatusBarProvider } from './kernel/statusbar';
import { AzureAuthenticatedConnection } from './kusto/connections/azAuth';
import { Client as KustoClient } from 'azure-kusto-data';
import { Client as KustoClient, kustoTrustedEndpoints, MatchRule } from 'azure-kusto-data';
import { KustoClient as AppInsightsKustoClient } from './kusto/webClient';
import { registerConnection } from './kusto/connections/baseConnection';
import { AppInsightsConnection } from './kusto/connections/appInsights';
import { CellCodeLensProvider } from './interactive/cells';
Expand All @@ -25,6 +26,9 @@ import { registerKqlNotebookConnectionHandler } from './content/kqlConnection';
import { regsisterQuickFixAction } from './content/quickFix';

export async function activate(context: ExtensionContext) {
// Add Application Insights API endpoint as a trusted endpoint for the Kusto SDK
kustoTrustedEndpoints.addTrustedHosts([new MatchRule('api.applicationinsights.io', true)], false);

registerDisposableRegistry(context);
initializeGlobalCache(context.globalState, context.workspaceState);
initializeConstants(context.extension.packageJSON.enableProposedApi); // In browser context dont use proposed API, try to always use stable stuff...
Expand All @@ -38,7 +42,7 @@ export async function activate(context: ExtensionContext) {
'cluster' in info ? undefined : AppInsightsConnection.connectionInfofrom(info)
);
AzureAuthenticatedConnection.registerKustoClient(KustoClient);
AppInsightsConnection.registerKustoClient(KustoClient);
AppInsightsConnection.registerKustoClient(AppInsightsKustoClient);
KernelProvider.register();
StatusBarProvider.register();
ContentProvider.register();
Expand Down
5 changes: 4 additions & 1 deletion src/extension/kusto/connections/appInsights/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ export class AppInsightsConnection extends BaseConnection<AppInsightsConnectionI
}
const secret = await getConnectionSecret(this.info.id);
if (!secret) {
throw new Error('Failed to load secrets from saved information');
throw new Error(
`Failed to load secrets for App Insights connection "${this.info.displayName}". ` +
`The stored credentials may have been cleared. Please delete this connection and re-add it with your AppId and AppKey.`
);
}
this.secretInfo = JSON.parse(secret) as AppInsightsConnectionSecrets;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand Down
Loading