Skip to content

Commit 078aa19

Browse files
williamsyang-workPatrickTasse
authored andcommitted
Add JSON config editor for trace customization
This commit implements a JSON configuration editor to enable users to customize trace outputs with a schema-based editor. The implementation includes: - JSON editor UI with schema validation - File operations for saving and loading configurations - Integration with Trace Explorer views - Error reporting and validation feedback - Command toolbar for managing configurations The feature provides a complete workflow for customizing outputs with validation against schema definitions from the trace server. Signed-off-by: Will Yang <william.yang@ericsson.com>
1 parent 09cc0bb commit 078aa19

File tree

12 files changed

+946
-36
lines changed

12 files changed

+946
-36
lines changed

vscode-trace-common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"src"
1010
],
1111
"dependencies": {
12-
"traceviewer-base": "^0.7.2",
12+
"traceviewer-base": "^0.8.0",
1313
"tsp-typescript-client": "^0.6.0",
1414
"vscode-messenger": "^0.5.0"
1515
},

vscode-trace-common/src/messages/vscode-messages.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import type { MessageParticipant, NotificationType } from 'vscode-messenger-common';
2+
import type { MessageParticipant, NotificationType, RequestType } from 'vscode-messenger-common';
3+
import { CustomizationConfigObject, CustomizationSubmission } from '../types/customization';
34
export const VSCODE_MESSAGES = {
45
ADD_OUTPUT: 'add-output',
56
ALERT: 'alert',
@@ -49,7 +50,8 @@ export const VSCODE_MESSAGES = {
4950
OUTPUT_DATA_CHANGED: 'outputDataChanged',
5051
CONTRIBUTE_CONTEXT_MENU: 'contributeContextMenu',
5152
CONTEXT_MENU_ITEM_CLICKED: 'contextMenuItemClicked',
52-
SOURCE_LOOKUP: 'sourceLookup'
53+
SOURCE_LOOKUP: 'sourceLookup',
54+
USER_CUSTOMIZATION_JSON_INPUT: 'userCustomizationJsonInput'
5355
};
5456

5557
export interface StatusNotifier {
@@ -111,3 +113,8 @@ export const restoreView: NotificationType<any> = { method: VSCODE_MESSAGES.REST
111113
export const sourceCodeLookup: NotificationType<{ path: string; line: number }> = {
112114
method: VSCODE_MESSAGES.SOURCE_LOOKUP
113115
};
116+
117+
export const userCustomizedOutput: RequestType<
118+
{ configs: CustomizationConfigObject[] },
119+
{ userConfig: CustomizationSubmission }
120+
> = { method: VSCODE_MESSAGES.USER_CUSTOMIZATION_JSON_INPUT };
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { QuickPickItem } from 'vscode';
3+
4+
/**
5+
* Top level object as it comes in from the server
6+
*/
7+
export interface CustomizationConfigObject {
8+
id: string;
9+
name: string;
10+
description: string;
11+
schema: Schema;
12+
}
13+
14+
/**
15+
* Top level schema with id and name
16+
*/
17+
export interface Schema {
18+
$schema: string;
19+
$id: string;
20+
type?: string;
21+
title?: string;
22+
description: string;
23+
default?: any;
24+
items?: { type: string };
25+
oneOf?: { required: string[] }[];
26+
properties?: { [key: string]: SchemaProperty };
27+
required?: string[];
28+
additionalProperties?: boolean;
29+
}
30+
31+
/**
32+
* Type for schema objects (and nested schema objects)
33+
*/
34+
export interface SchemaProperty {
35+
type?: string;
36+
title?: string;
37+
description: string;
38+
default?: any;
39+
items?: { type: string };
40+
oneOf?: { required: string[] }[];
41+
properties?: { [key: string]: SchemaProperty };
42+
required?: string[];
43+
additionalProperties?: boolean;
44+
const?: any;
45+
errorMessage?: any;
46+
}
47+
48+
/**
49+
* Types of values that can be used as defaults in schema
50+
*/
51+
export type DefaultValue = string | number | boolean | null | Record<string, any> | any[];
52+
53+
/**
54+
* Result of validating a JSON file against a schema
55+
*/
56+
export interface ValidationResult {
57+
isValid: boolean;
58+
content?: CustomizationSubmission;
59+
errors?: string[];
60+
}
61+
62+
export interface CustomizationSubmission {
63+
name: string;
64+
sourceTypeId: string;
65+
description: string;
66+
parameters: {
67+
[key: string]: any;
68+
};
69+
}
70+
71+
/**
72+
* Configuration for schema selection picker
73+
*/
74+
export interface SchemaPickerItem extends QuickPickItem {
75+
schemaId: string;
76+
}

vscode-trace-extension/package.json

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@
3434
],
3535
"contributes": {
3636
"commands": [
37+
{
38+
"command": "traceViewer.customization.submitConfig",
39+
"title": "Submit",
40+
"icon": "$(pass)"
41+
},
42+
{
43+
"command": "traceViewer.customization.loadConfig",
44+
"title": "Load",
45+
"icon": "$(file-submodule)"
46+
},
47+
{
48+
"command": "traceViewer.customization.saveConfig",
49+
"title": "Save",
50+
"icon": "$(save)"
51+
},
3752
{
3853
"command": "outputs.reset",
3954
"title": "Reset",
@@ -239,6 +254,21 @@
239254
"when": "activeWebviewPanelId == 'react'",
240255
"command": "traceViewer.shortcuts",
241256
"group": "navigation@9"
257+
},
258+
{
259+
"when": "resourcePath in traceViewer.customization.configPath",
260+
"command": "traceViewer.customization.submitConfig",
261+
"group": "navigation"
262+
},
263+
{
264+
"when": "resourcePath in traceViewer.customization.configPath",
265+
"command": "traceViewer.customization.saveConfig",
266+
"group": "navigation"
267+
},
268+
{
269+
"when": "resourcePath in traceViewer.customization.configPath",
270+
"command": "traceViewer.customization.loadConfig",
271+
"group": "navigation"
242272
}
243273
],
244274
"commandPalette": [
@@ -296,12 +326,15 @@
296326
"@fortawesome/react-fontawesome": "^0.1.4",
297327
"@vscode/codicons": "^0.0.33",
298328
"@vscode/vsce": "2.25.0",
299-
"vscode-messenger": "^0.5.0",
329+
"ag-grid-react": "^28.2.0",
330+
"ajv": "^8.17.1",
300331
"chart.js": "^2.8.0",
332+
"jsonc-parser": "^3.3.1",
301333
"lodash": "^4.17.15",
302334
"terser": "4.8.1",
303-
"traceviewer-base": "^0.7.2",
304-
"traceviewer-react-components": "^0.7.2",
335+
"traceviewer-base": "^0.8.0",
336+
"traceviewer-react-components": "^0.8.0",
337+
"vscode-messenger": "^0.5.0",
305338
"vscode-trace-common": "0.4.0"
306339
},
307340
"devDependencies": {

vscode-trace-extension/src/extension.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TraceExplorerItemPropertiesProvider } from './trace-explorer/properties
55
import { TraceExplorerTimeRangeDataProvider } from './trace-explorer/time-range/trace-explorer-time-range-data-webview-provider';
66
import { TraceExplorerAvailableViewsProvider } from './trace-explorer/available-views/trace-explorer-available-views-webview-provider';
77
import { TraceExplorerOpenedTracesViewProvider } from './trace-explorer/opened-traces/trace-explorer-opened-traces-webview-provider';
8+
import { JsonConfigEditor } from './json-editor/json-editor';
89
import {
910
openDialog,
1011
fileHandler,
@@ -44,6 +45,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Extern
4445
vscode.commands.executeCommand('setContext', 'traceViewer.serverUp', false);
4546

4647
const resourceTypeHandler: TraceExplorerResourceTypeHandler = TraceExplorerResourceTypeHandler.getInstance();
48+
const jsonConfigEditor: JsonConfigEditor = new JsonConfigEditor(messenger);
4749

4850
await updateTspClientUrl();
4951
const serverStatusBarItemPriority = 1;
@@ -102,6 +104,24 @@ export async function activate(context: vscode.ExtensionContext): Promise<Extern
102104
})
103105
);
104106

107+
context.subscriptions.push(
108+
vscode.commands.registerCommand('traceViewer.customization.submitConfig', async () => {
109+
await jsonConfigEditor.closeIfValid();
110+
})
111+
);
112+
113+
context.subscriptions.push(
114+
vscode.commands.registerCommand('traceViewer.customization.saveConfig', async () => {
115+
await jsonConfigEditor.saveDocumentIfValid();
116+
})
117+
);
118+
119+
context.subscriptions.push(
120+
vscode.commands.registerCommand('traceViewer.customization.loadConfig', async () => {
121+
await jsonConfigEditor.loadExistingConfig();
122+
})
123+
);
124+
105125
// Listening to configuration change for the trace server URL
106126
context.subscriptions.push(
107127
vscode.workspace.onDidChangeConfiguration(async e => {
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import * as vscode from 'vscode';
2+
import * as fs from 'fs';
3+
import { DefaultValue } from 'vscode-trace-common/lib/types/customization';
4+
5+
/**
6+
* Service for handling file operations related to JSON configurations
7+
*/
8+
export class FileService {
9+
private fileWatchers: Map<string, vscode.Disposable>;
10+
private autoSaveListeners: Map<string, vscode.Disposable>;
11+
12+
constructor() {
13+
this.fileWatchers = new Map();
14+
this.autoSaveListeners = new Map();
15+
}
16+
17+
/**
18+
* Creates a configuration file from the provided config object.
19+
* If there is a file open, it modifies the content instead.
20+
*
21+
* This is the physical file that is displayed in the editor
22+
* @param fileUri The file path to write over or create the temp config file
23+
* @param json The json object to add to the file
24+
* @param meta Optional metadata for the file header
25+
*/
26+
public async loadJSONConfigFile(fileUri: vscode.Uri, json: DefaultValue): Promise<void> {
27+
const fileContent = [
28+
'/**',
29+
'* A toolbar is located in the top-right',
30+
'* • Submit the current config',
31+
'* • Save this config for future use',
32+
'* • Load an existing config file',
33+
'*',
34+
'* You can also submit by simply closing the file',
35+
'*/',
36+
JSON.stringify(json, undefined, 2)
37+
].join('\n');
38+
39+
// First, find the editor that's showing this document
40+
const openConfigEditor = vscode.window.visibleTextEditors.find(
41+
editor => editor.document.uri.toString() === fileUri.toString()
42+
);
43+
44+
if (openConfigEditor) {
45+
// If the editor is open, update its content using edit
46+
await openConfigEditor.edit(editBuilder => {
47+
// Replace the entire content
48+
const fullRange = new vscode.Range(
49+
0,
50+
0,
51+
openConfigEditor.document.lineCount - 1,
52+
openConfigEditor.document.lineAt(openConfigEditor.document.lineCount - 1).text.length
53+
);
54+
editBuilder.replace(fullRange, fileContent);
55+
});
56+
} else {
57+
// If no editor is open for this file, just write to the file
58+
await vscode.workspace.fs.writeFile(fileUri, Buffer.from(fileContent, 'utf-8'));
59+
}
60+
61+
const document = await vscode.workspace.openTextDocument(fileUri);
62+
await document.save();
63+
64+
// Setup watcher for this file
65+
this.watchConfigFile(fileUri);
66+
}
67+
68+
/**
69+
* Set up a watcher for the config file to enable settings-like behavior
70+
*/
71+
private watchConfigFile(fileUri: vscode.Uri): void {
72+
const key = fileUri.toString();
73+
74+
if (this.fileWatchers.has(key)) {
75+
this.fileWatchers.get(key)?.dispose();
76+
}
77+
78+
const watcher = vscode.workspace.createFileSystemWatcher(
79+
new vscode.RelativePattern(
80+
vscode.workspace.getWorkspaceFolder(fileUri)?.uri || fileUri,
81+
vscode.workspace.asRelativePath(fileUri)
82+
)
83+
);
84+
85+
this.fileWatchers.set(key, watcher);
86+
this.setupAutoSaveForFile(fileUri);
87+
}
88+
89+
/**
90+
* Sets up auto-save functionality for a specific file
91+
* @param fileUri The URI of the file to auto-save
92+
* @param delayMs Optional delay in milliseconds before saving (default: 500ms)
93+
* @private Internal method used by file watchers
94+
*/
95+
private async setupAutoSaveForFile(fileUri: vscode.Uri, delayMs: number = 250): Promise<void> {
96+
const key = fileUri.toString();
97+
98+
if (this.autoSaveListeners.has(key)) {
99+
this.autoSaveListeners.get(key)?.dispose();
100+
this.autoSaveListeners.delete(key);
101+
}
102+
103+
let saveTimeout: ReturnType<typeof setTimeout>;
104+
105+
try {
106+
const document = await vscode.workspace.openTextDocument(fileUri);
107+
108+
const changeListener = vscode.workspace.onDidChangeTextDocument(event => {
109+
if (event.document.uri.toString() === document.uri.toString()) {
110+
if (saveTimeout) {
111+
clearTimeout(saveTimeout);
112+
}
113+
114+
saveTimeout = setTimeout(async () => {
115+
try {
116+
await document.save();
117+
} catch (error) {
118+
console.error('Failed to auto-save document:', error);
119+
}
120+
}, delayMs);
121+
}
122+
});
123+
124+
// Store the listener
125+
this.autoSaveListeners.set(key, changeListener);
126+
} catch (error) {
127+
console.error(`Failed to set up auto-save for ${fileUri.toString()}:`, error);
128+
}
129+
}
130+
131+
/**
132+
* Cleans up the temporary file and any watchers
133+
* @param filePath Path to the temporary file
134+
*/
135+
public cleanupTempFile(filePath: string): void {
136+
const fileUri = vscode.Uri.file(filePath);
137+
const key = fileUri.toString();
138+
139+
if (this.fileWatchers.has(key)) {
140+
this.fileWatchers.get(key)?.dispose();
141+
this.fileWatchers.delete(key);
142+
}
143+
144+
if (this.autoSaveListeners.has(key)) {
145+
this.autoSaveListeners.get(key)?.dispose();
146+
this.autoSaveListeners.delete(key);
147+
}
148+
149+
if (fs.existsSync(filePath)) {
150+
try {
151+
fs.unlinkSync(filePath);
152+
} catch (error) {
153+
console.error('Failed to delete temporary file:', error);
154+
}
155+
}
156+
}
157+
158+
/**
159+
* Dispose all resources
160+
*/
161+
public dispose(): void {
162+
for (const watcher of this.fileWatchers.values()) {
163+
watcher.dispose();
164+
}
165+
this.fileWatchers.clear();
166+
167+
for (const listener of this.autoSaveListeners.values()) {
168+
listener.dispose();
169+
}
170+
this.autoSaveListeners.clear();
171+
}
172+
}

0 commit comments

Comments
 (0)