Skip to content
Merged
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
54 changes: 46 additions & 8 deletions src/panels/minecraft-diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (C) Microsoft Corporation. All rights reserved.

import { Disposable, Webview, WebviewPanel, window, Uri, ViewColumn } from 'vscode';
import { Disposable, Webview, WebviewPanel, window, workspace, Uri, ViewColumn } from 'vscode';
import { EventEmitter } from 'stream';
import { getUri } from '../utilities/getUri';
import { getNonce } from '../utilities/getNonce';
Expand Down Expand Up @@ -37,7 +37,7 @@
this._panel.webview.html = this._getWebviewContent(
this._panel.webview,
extensionUri,
statsTracker.manualControl()
statsTracker.manualControl(),
);

// Handle events from the webview panel
Expand All @@ -48,7 +48,7 @@
this._panel.webview.html = this._getWebviewContent(
this._panel.webview,
extensionUri,
statsTracker.manualControl()
statsTracker.manualControl(),
);
break;
case 'pause':
Expand All @@ -74,6 +74,9 @@
case 'debugger-request':
this._debuggerRequestHandler.handleDebuggerRequest(message.request, message.args);
break;
case 'export-data':
void this.handleExportDataMessage(message);
break;
default:
console.error('Unknown message type:', message.type);
break;
Expand Down Expand Up @@ -122,16 +125,45 @@
schema: schema,
};
this._panel.webview.postMessage(message);
}
},
};

this._statsTracker.addStatListener(this._statsCallback);
}

private async handleExportDataMessage(message: any): Promise<void> {
if (typeof message.content !== 'string') {
console.error('Received export-data message without a valid content string.');
return;
}

const suggestedFileName =
typeof message.suggestedFileName === 'string' && message.suggestedFileName.trim() !== ''
? message.suggestedFileName
: 'diagnostics-export.csv';
const workspaceFolderUri = workspace.workspaceFolders?.[0]?.uri;

const outputUri = await window.showSaveDialog({
title: 'Export Diagnostics Data',
saveLabel: 'Export',
defaultUri: workspaceFolderUri ? Uri.joinPath(workspaceFolderUri, suggestedFileName) : undefined,
filters: {
'CSV Files': ['csv'],

Check warning on line 151 in src/panels/minecraft-diagnostics.ts

View workflow job for this annotation

GitHub Actions / Build and Test

Object Literal Property name `CSV Files` must match one of the following formats: camelCase, snake_case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like there's a warning here

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.

This is the VS Code API fighting with our linter, these need to be human readable: https://code.visualstudio.com/api/references/vscode-api#SaveDialogOptions

I'm thinking I might make a follow up story to do a pass on our linter warnings and see if we can adjust some configurations to make everything happy, as this has come up numerous times now.

},
});

if (!outputUri) {
return;
}

await workspace.fs.writeFile(outputUri, Buffer.from(message.content, 'utf8'));
window.showInformationMessage(`Exported diagnostics data to ${outputUri.fsPath}.`);
}

public static render(extensionUri: Uri, statsTracker: StatsProvider, eventEmitter: EventEmitter): void {
const statsTrackerId = statsTracker.uniqueId;
const existingPanel = MinecraftDiagnosticsPanel.activeDiagnosticsPanels.find(
panel => panel._statsTracker.uniqueId === statsTrackerId
panel => panel._statsTracker.uniqueId === statsTrackerId,
);
if (existingPanel) {
existingPanel._panel.reveal(ViewColumn.One);
Expand All @@ -147,10 +179,16 @@
Uri.joinPath(extensionUri, 'out'),
Uri.joinPath(extensionUri, 'webview-ui/build'),
],
}
},
);
MinecraftDiagnosticsPanel.activeDiagnosticsPanels.push(
new MinecraftDiagnosticsPanel(panel, extensionUri, statsTracker, eventEmitter, new DebuggerRequestHandler(panel.webview)),
new MinecraftDiagnosticsPanel(
panel,
extensionUri,
statsTracker,
eventEmitter,
new DebuggerRequestHandler(panel.webview),
),
);
}
}
Expand All @@ -163,7 +201,7 @@

// Remove the current panel from the active panel list
MinecraftDiagnosticsPanel.activeDiagnosticsPanels = MinecraftDiagnosticsPanel.activeDiagnosticsPanels.filter(
panel => panel !== this
panel => panel !== this,
);

// Dispose of the current webview panel
Expand Down
12 changes: 12 additions & 0 deletions webview-ui/src/diagnostics_panel/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,18 @@ main {
background: color-mix(in srgb, var(--vscode-list-activeSelectionBackground) 33%, var(--vscode-editor-background));
}

.minecraft-entity-system-general-controls {
padding-top: 16px;
}

.minecraft-entity-system-export-actions {
display: flex;
align-items: flex-end;
gap: 8px;
flex-wrap: wrap;
padding-top: 16px;
}

.minecraft-grouped-statistic-table-root {
width: 100%;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,18 @@ export type MinecraftGroupedStatisticTableSelectionSnapshot = {
resolvedSelectedRows: GroupedStatisticTableRow[];
};

export type MinecraftGroupedStatisticTableExportRow = {
rowKey: string;
groupKey: string;
row: GroupedStatisticTableRow;
trendValues: number[];
};

export type MinecraftGroupedStatisticTableHandle = {
getSelectionSnapshot: () => MinecraftGroupedStatisticTableSelectionSnapshot;
getRowsForExport: () => MinecraftGroupedStatisticTableExportRow[];
clearSelection: () => void;
clearTrendData: () => void;
};

type MinecraftGroupedStatisticTableProps = {
Expand Down Expand Up @@ -120,6 +129,8 @@ const SPARKLINE_HISTORY_RETENTION_TICKS = 8;
const TICKS_PER_SECOND = 20;
const TICKS_PER_SPARKLINE_UPDATE = 12; // TODO I don't know if this is accurate or not, but it gives us the right values for now

export const SPARKLINE_SAMPLE_INTERVAL_SECONDS = TICKS_PER_SPARKLINE_UPDATE / TICKS_PER_SECOND;

function sparklineTickRangeToSeconds(updatePoints: number): string {
const totalTicks = updatePoints * TICKS_PER_SPARKLINE_UPDATE;
const seconds = totalTicks / TICKS_PER_SECOND;
Expand Down Expand Up @@ -733,17 +744,49 @@ const MinecraftGroupedStatisticTable = forwardRef<
};
}, [deselectedRowsInSelectedGroups, groupedRowsByKey, rowGroupLookup, rowLookup, selectedGroups, selectedRows]);

const createExportRow = useCallback(
(rowKey: string): MinecraftGroupedStatisticTableExportRow | undefined => {
const row = rowLookup.get(rowKey);
const groupKey = rowGroupLookup.get(rowKey);

if (!row || !groupKey) {
return undefined;
}

return {
rowKey,
groupKey,
row,
trendValues: [...(rowHistoryRef.current.get(row.category) ?? [])],
};
},
[rowGroupLookup, rowLookup],
);

const rowsForExport = useMemo((): MinecraftGroupedStatisticTableExportRow[] => {
return Array.from(rowLookup.keys())
.sort((left, right) => left.localeCompare(right))
.map(rowKey => createExportRow(rowKey))
.filter((row): row is MinecraftGroupedStatisticTableExportRow => row !== undefined);
}, [createExportRow, rowLookup]);

useImperativeHandle(
ref,
() => ({
getSelectionSnapshot: () => selectionSnapshot,
getRowsForExport: () => rowsForExport,
clearSelection: () => {
setSelectedGroups(new Set());
setSelectedRows(new Set());
setDeselectedRowsInSelectedGroups(new Set());
},
clearTrendData: () => {
rowHistoryRef.current.clear();
rowHistoryMissingTickCountRef.current.clear();
sparklineBoundsRef.current.clear();
},
}),
[selectionSnapshot],
[selectionSnapshot, rowsForExport],
);

const prevSelectionRowKeysRef = useRef<string[]>([]);
Expand Down Expand Up @@ -1037,6 +1080,24 @@ const MinecraftGroupedStatisticTable = forwardRef<
sparklineBoundsRef.current.clear();
}, [sparklineColumnIndex]);

useEffect(() => {
if (sparklineTickRange <= 0) {
rowHistoryRef.current.clear();
rowHistoryMissingTickCountRef.current.clear();
sparklineBoundsRef.current.clear();
return;
}

rowHistoryRef.current.forEach((history, category) => {
if (history.length <= sparklineTickRange) {
return;
}

const trimmedHistory = history.slice(history.length - sparklineTickRange);
rowHistoryRef.current.set(category, trimmedHistory);
});
}, [sparklineTickRange]);

useEffect(() => {
if (!selectionEnabled || !defaultSelectAllGroups || hasInitializedDefaultSelection) {
return;
Expand Down
50 changes: 50 additions & 0 deletions webview-ui/src/diagnostics_panel/exporters/TableCsvExporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (C) Microsoft Corporation. All rights reserved.

export type CsvCellValue = string | number;

export type CsvExportRow = Record<string, CsvCellValue>;

export type DiagnosticsExportFormat = 'csv';

export interface CsvExporter {
readonly format: 'csv';
readonly fileExtension: string;
readonly mimeType: string;
exportRows<THeader extends string>(headers: readonly THeader[], rows: Array<Record<THeader, CsvCellValue>>): string;
}

function escapeCsvValue(value: string | number): string {
const serialized = String(value);

if (!/[",\n\r]/.test(serialized)) {
return serialized;
}

return `"${serialized.replace(/"/g, '""')}"`;
}

function toCsvRow(values: readonly (string | number)[]): string {
return values.map(escapeCsvValue).join(',');
}

export class TableCsvExporter implements CsvExporter {
public readonly format = 'csv' as const;

public readonly fileExtension = 'csv';

public readonly mimeType = 'text/csv';

public exportRows<THeader extends string>(
headers: readonly THeader[],
rows: Array<Record<THeader, CsvCellValue>>,
): string {
const csvLines: string[] = [toCsvRow(headers)];

rows.forEach(row => {
const values = headers.map(header => row[header] ?? '');
csvLines.push(toCsvRow(values));
});

return csvLines.join('\n');
}
}
Loading