From 178f15493d957f6b073386d9aac4fe1165d6f900 Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Sat, 13 Jan 2024 03:59:43 -0800 Subject: [PATCH 01/12] Split up the explorer views by product --- vscode/images/Conform_symbol-whitesquare.svg | 48 ++++++++++++++++ vscode/images/Deviant_symbol-whitesquare.svg | 51 +++++++++++++++++ vscode/images/Modality_symbol-whitesquare.svg | 57 +++++++++++++++++++ vscode/package.json | 38 +++++++++++-- 4 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 vscode/images/Conform_symbol-whitesquare.svg create mode 100644 vscode/images/Deviant_symbol-whitesquare.svg create mode 100644 vscode/images/Modality_symbol-whitesquare.svg diff --git a/vscode/images/Conform_symbol-whitesquare.svg b/vscode/images/Conform_symbol-whitesquare.svg new file mode 100644 index 0000000..823e203 --- /dev/null +++ b/vscode/images/Conform_symbol-whitesquare.svg @@ -0,0 +1,48 @@ + + + + + + + diff --git a/vscode/images/Deviant_symbol-whitesquare.svg b/vscode/images/Deviant_symbol-whitesquare.svg new file mode 100644 index 0000000..8cdf2d1 --- /dev/null +++ b/vscode/images/Deviant_symbol-whitesquare.svg @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/vscode/images/Modality_symbol-whitesquare.svg b/vscode/images/Modality_symbol-whitesquare.svg new file mode 100644 index 0000000..28d36e4 --- /dev/null +++ b/vscode/images/Modality_symbol-whitesquare.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + diff --git a/vscode/package.json b/vscode/package.json index 621cceb..289cb8a 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -65,14 +65,24 @@ "viewsContainers": { "activitybar": [ { - "id": "auxon-explorer", - "title": "Auxon", - "icon": "images/Auxon_symbol-whitesquare.svg" + "id": "modality-explorer", + "title": "Modality", + "icon": "images/Modality_symbol-whitesquare.svg" + }, + { + "id": "conform-explorer", + "title": "Conform", + "icon": "images/Conform_symbol-whitesquare.svg" + }, + { + "id": "deviant-explorer", + "title": "Deviant", + "icon": "images/Deviant_symbol-whitesquare.svg" } ] }, "views": { - "auxon-explorer": [ + "modality-explorer": [ { "id": "auxon.workspaces", "name": "Workspaces" @@ -88,11 +98,19 @@ { "id": "auxon.events", "name": "Events" - }, + } + ], + "conform-explorer": [ { "id": "auxon.specs", "name": "Specs" } + ], + "deviant-explorer": [ + { + "id": "auxon.mutators", + "name": "Mutators" + } ] }, "menus": { @@ -369,6 +387,11 @@ "command": "auxon.specs.hideResults", "when": "view == auxon.specs && auxon.specs.results == 'SHOW'", "group": "7_modification@2" + }, + { + "command": "auxon.mutators.refresh", + "when": "view == auxon.mutators", + "group": "navigation@1" } ] }, @@ -607,6 +630,11 @@ "command": "auxon.modality.log", "title": "View Log", "icon": "$(open-preview)" + }, + { + "command": "auxon.mutators.refresh", + "title": "Refresh Mutator List", + "icon": "$(refresh)" } ], "configuration": { From d5a2e49939fa8b056ba12ae9a5ecbda598bb95ce Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Sat, 13 Jan 2024 04:00:17 -0800 Subject: [PATCH 02/12] Update timelines treeview icon --- vscode/src/timelines.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vscode/src/timelines.ts b/vscode/src/timelines.ts index 22b24ec..6cda000 100644 --- a/vscode/src/timelines.ts +++ b/vscode/src/timelines.ts @@ -368,6 +368,7 @@ abstract class TimelineTreeItemData { export class TimelineGroupByNameTreeItemData extends TimelineTreeItemData { constructor(public name: string, private childItems: TimelineTreeItemData[]) { super(); + super.iconPath = new vscode.ThemeIcon("git-pull-request-draft"); } contextValue = "timelineGroup"; From f2b4b55210f3ee462aa76a40d0a0f0f23611d4de Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Mon, 15 Jan 2024 06:09:44 -0800 Subject: [PATCH 03/12] Add mutators and mutations deviant tree views --- vscode/generated/src/modality-api.ts | 121 ++++++++ vscode/package.json | 98 ++++++ vscode/src/main.ts | 12 + vscode/src/modality-api.json | 241 +++++++++++++++ vscode/src/modalityApi.ts | 61 ++++ vscode/src/mutations.ts | 385 +++++++++++++++++++++++ vscode/src/mutators.ts | 444 +++++++++++++++++++++++++++ 7 files changed, 1362 insertions(+) create mode 100644 vscode/src/mutations.ts create mode 100644 vscode/src/mutators.ts diff --git a/vscode/generated/src/modality-api.ts b/vscode/generated/src/modality-api.ts index eec799b..4aadab5 100644 --- a/vscode/generated/src/modality-api.ts +++ b/vscode/generated/src/modality-api.ts @@ -17,6 +17,20 @@ export interface paths { */ get: operations["get_events_summary_for_timeline"]; }; + "/v2/mutations": { + /** + * List mutations + * @description List mutations + */ + get: operations["list_mutations"]; + }; + "/v2/mutators": { + /** + * List mutators + * @description List mutators + */ + get: operations["list_mutators"]; + }; "/v2/specs": { /** * List all specs @@ -319,6 +333,43 @@ export interface components { MaybeAttrVal: OneOf<["None", { Some: components["schemas"]["AttrVal"]; }]>; + Mutation: { + experiment_name?: string | null; + mutation_id: components["schemas"]["MutationId"]; + mutator_attributes: { + [key: string]: components["schemas"]["AttrVal"] | undefined; + }; + mutator_id: components["schemas"]["MutatorId"]; + params: { + [key: string]: components["schemas"]["AttrVal"] | undefined; + }; + }; + /** Format: uuid */ + MutationId: string; + /** @description Mutations operation errors */ + MutationsError: OneOf<["InvalidMutatorId", { + /** @description Internal Server Error */ + Internal: string; + }]>; + Mutator: { + mutator_attributes: { + [key: string]: components["schemas"]["AttrVal"] | undefined; + }; + mutator_id: components["schemas"]["MutatorId"]; + mutator_state: components["schemas"]["MutatorState"]; + }; + /** Format: uuid */ + MutatorId: string; + /** @enum {string} */ + MutatorState: "Available" | "Retired" | "TimedOut" | "Disconnected"; + /** @description Mutator operation errors */ + MutatorsError: OneOf<[{ + /** @description Invalid mutator filter expression */ + InvalidMutatorFilter: string; + }, { + /** @description Internal Server Error */ + Internal: string; + }]>; Nanoseconds: number; SegmentCoverage: { coverage_aggregates: components["schemas"]["CoverageAggregates"]; @@ -497,6 +548,76 @@ export interface operations { }; }; }; + /** + * List mutations + * @description List mutations + */ + list_mutations: { + parameters: { + query: { + /** @description Mutator ID */ + mutator_id?: string | null; + /** @description Experiment name */ + experiment?: string | null; + }; + }; + responses: { + /** @description List mutations successfully */ + 200: { + content: { + "application/json": (components["schemas"]["Mutation"])[]; + }; + }; + /** @description Invalid mutator_id */ + 400: { + content: { + "application/json": components["schemas"]["MutationsError"]; + }; + }; + /** @description Operation not authorized */ + 403: never; + /** @description Internal Server Error */ + 500: { + content: { + "application/json": components["schemas"]["MutationsError"]; + }; + }; + }; + }; + /** + * List mutators + * @description List mutators + */ + list_mutators: { + parameters: { + query: { + /** @description Mutator filter expression */ + mutator_filter?: string | null; + }; + }; + responses: { + /** @description List mutators successfully */ + 200: { + content: { + "application/json": (components["schemas"]["Mutator"])[]; + }; + }; + /** @description Invalid mutator_filter */ + 400: { + content: { + "application/json": components["schemas"]["MutatorsError"]; + }; + }; + /** @description Operation not authorized */ + 403: never; + /** @description Internal Server Error */ + 500: { + content: { + "application/json": components["schemas"]["MutatorsError"]; + }; + }; + }; + }; /** * List all specs * @description List all specs diff --git a/vscode/package.json b/vscode/package.json index 289cb8a..0c5cb4c 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -110,6 +110,10 @@ { "id": "auxon.mutators", "name": "Mutators" + }, + { + "id": "auxon.mutations", + "name": "Mutations" } ] }, @@ -392,6 +396,51 @@ "command": "auxon.mutators.refresh", "when": "view == auxon.mutators", "group": "navigation@1" + }, + { + "command": "auxon.mutators.showUnavailable", + "when": "view == auxon.mutators && auxon.mutators.unavailable == 'HIDE'", + "group": "7_modification@1" + }, + { + "command": "auxon.mutators.hideUnavailable", + "when": "view == auxon.mutators && auxon.mutators.unavailable == 'SHOW'", + "group": "7_modification@1" + }, + { + "command": "auxon.mutators.groupMutatorsByName", + "when": "view == auxon.mutators && auxon.mutators.groupBy != 'MUTATOR_NAME'", + "group": "7_modification@3" + }, + { + "command": "auxon.mutators.disableMutatorGrouping", + "when": "view == auxon.mutators && auxon.mutators.groupBy == 'MUTATOR_NAME'", + "group": "7_modification@3" + }, + { + "command": "auxon.mutations.refresh", + "when": "view == auxon.mutations", + "group": "navigation@1" + }, + { + "command": "auxon.mutations.groupMutationsByName", + "when": "view == auxon.mutations && auxon.mutations.groupBy != 'MUTATOR_NAME'", + "group": "7_modification@3" + }, + { + "command": "auxon.mutations.disableMutationGrouping", + "when": "view == auxon.mutations && auxon.mutations.groupBy == 'MUTATOR_NAME'", + "group": "7_modification@3" + }, + { + "command": "auxon.mutations.filterBySelectedMutator", + "when": "view == auxon.mutations && auxon.mutations.filterBy != 'MUTATOR_ID'", + "group": "7_modification@3" + }, + { + "command": "auxon.mutations.disableMutationFiltering", + "when": "view == auxon.mutations && auxon.mutations.filterBy == 'MUTATOR_ID'", + "group": "7_modification@3" } ] }, @@ -635,6 +684,55 @@ "command": "auxon.mutators.refresh", "title": "Refresh Mutator List", "icon": "$(refresh)" + }, + { + "command": "auxon.mutators.setSelectedMutator", + "title": "Sets the selected mutator in the mutators tree view" + }, + { + "command": "auxon.mutators.showUnavailable", + "title": "Show Unavailable" + }, + { + "command": "auxon.mutators.hideUnavailable", + "title": "✓ Show Unavailable" + }, + { + "command": "auxon.mutators.groupMutatorsByName", + "title": "Group By Mutator Name" + }, + { + "command": "auxon.mutators.disableMutatorGrouping", + "title": "✓ Group By Mutator Name" + }, + { + "command": "auxon.mutations.refresh", + "title": "Refresh Mutation List", + "icon": "$(refresh)" + }, + { + "command": "auxon.mutations.setSelectedMutator", + "title": "Set the selected mutator in the mutations tree view" + }, + { + "command": "auxon.mutations.setSelectedMutation", + "title": "Sets the selected mutation in the mutations tree view" + }, + { + "command": "auxon.mutations.groupMutationsByName", + "title": "Group By Mutator Name" + }, + { + "command": "auxon.mutations.disableMutationGrouping", + "title": "✓ Group By Mutator Name" + }, + { + "command": "auxon.mutations.filterBySelectedMutator", + "title": "Filter By Selected Mutator" + }, + { + "command": "auxon.mutations.disableMutationFiltering", + "title": "✓ Filter By Selected Mutator" } ], "configuration": { diff --git a/vscode/src/main.ts b/vscode/src/main.ts index 08a419f..d50ce48 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -17,6 +17,8 @@ import * as specFileCommands from "./specFileCommands"; import * as transitionGraph from "./transitionGraph"; import * as config from "./config"; import * as speqtrLinkProvider from "./speqtrLinkProvider"; +import * as mutators from "./mutators"; +import * as mutations from "./mutations"; export let log: vscode.OutputChannel; let lspClient: LanguageClient; @@ -59,6 +61,8 @@ export async function activate(context: vscode.ExtensionContext) { const timelinesTreeDataProvider = new timelines.TimelinesTreeDataProvider(apiClient); const eventsTreeDataProvider = new events.EventsTreeDataProvider(apiClient); const specsTreeDataProvider = new specs.SpecsTreeDataProvider(apiClient, specCoverageProvider); + const mutatorsTreeDataProvider = new mutators.MutatorsTreeDataProvider(apiClient); + const mutationsTreeDataProvider = new mutations.MutationsTreeDataProvider(apiClient); workspacesTreeDataProvider.onDidChangeActiveWorkspace((ws_ver) => { log.appendLine(`Active workspace change! ${ws_ver}`); @@ -70,6 +74,9 @@ export async function activate(context: vscode.ExtensionContext) { eventsTreeDataProvider.activeWorkspaceVersionId = ws_ver; eventsTreeDataProvider.refresh(); + + mutatorsTreeDataProvider.refresh(); + mutationsTreeDataProvider.refresh(); }); segmentsTreeDataProvider.onDidChangeUsedSegments((ev) => { @@ -81,6 +88,9 @@ export async function activate(context: vscode.ExtensionContext) { eventsTreeDataProvider.activeSegments = ev.activeSegmentIds; eventsTreeDataProvider.refresh(); + + mutatorsTreeDataProvider.refresh(); + mutationsTreeDataProvider.refresh(); }); workspacesTreeDataProvider.register(context); @@ -88,6 +98,8 @@ export async function activate(context: vscode.ExtensionContext) { timelinesTreeDataProvider.register(context); eventsTreeDataProvider.register(context); specsTreeDataProvider.register(context); + mutatorsTreeDataProvider.register(context); + mutationsTreeDataProvider.register(context); } export function deactivate(): Thenable | undefined { diff --git a/vscode/src/modality-api.json b/vscode/src/modality-api.json index 09f2614..cbdb63c 100644 --- a/vscode/src/modality-api.json +++ b/vscode/src/modality-api.json @@ -73,6 +73,145 @@ ] } }, + "/v2/mutations": { + "get": { + "tags": ["mutations"], + "summary": "List mutations", + "description": "List mutations", + "operationId": "list_mutations", + "parameters": [ + { + "name": "mutator_id", + "in": "query", + "description": "Mutator ID", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "explode": true + }, + { + "name": "experiment", + "in": "query", + "description": "Experiment name", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "explode": true + } + ], + "responses": { + "200": { + "description": "List mutations successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mutation" + } + } + } + } + }, + "400": { + "description": "Invalid mutator_id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationsError" + } + } + } + }, + "403": { + "description": "Operation not authorized" + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationsError" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": ["access"] + } + ] + } + }, + "/v2/mutators": { + "get": { + "tags": ["mutators"], + "summary": "List mutators", + "description": "List mutators", + "operationId": "list_mutators", + "parameters": [ + { + "name": "mutator_filter", + "in": "query", + "description": "Mutator filter expression", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "explode": true + } + ], + "responses": { + "200": { + "description": "List mutators successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mutator" + } + } + } + } + }, + "400": { + "description": "Invalid mutator_filter", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutatorsError" + } + } + } + }, + "403": { + "description": "Operation not authorized" + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutatorsError" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": ["access"] + } + ] + } + }, "/v2/specs": { "get": { "tags": ["specs"], @@ -2219,6 +2358,108 @@ ], "description": "A serialization helper type, for when you actually want Option. (We're\nnot allowed to implement ToSchema on that...)" }, + "Mutation": { + "type": "object", + "required": ["mutation_id", "mutator_id", "params", "mutator_attributes"], + "properties": { + "experiment_name": { + "type": "string", + "nullable": true + }, + "mutation_id": { + "$ref": "#/components/schemas/MutationId" + }, + "mutator_attributes": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AttrVal" + } + }, + "mutator_id": { + "$ref": "#/components/schemas/MutatorId" + }, + "params": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AttrVal" + } + } + } + }, + "MutationId": { + "type": "string", + "format": "uuid" + }, + "MutationsError": { + "oneOf": [ + { + "type": "string", + "enum": ["InvalidMutatorId"] + }, + { + "type": "object", + "required": ["Internal"], + "properties": { + "Internal": { + "type": "string", + "description": "Internal Server Error" + } + } + } + ], + "description": "Mutations operation errors" + }, + "Mutator": { + "type": "object", + "required": ["mutator_id", "mutator_attributes", "mutator_state"], + "properties": { + "mutator_attributes": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AttrVal" + } + }, + "mutator_id": { + "$ref": "#/components/schemas/MutatorId" + }, + "mutator_state": { + "$ref": "#/components/schemas/MutatorState" + } + } + }, + "MutatorId": { + "type": "string", + "format": "uuid" + }, + "MutatorState": { + "type": "string", + "enum": ["Available", "Retired", "TimedOut", "Disconnected"] + }, + "MutatorsError": { + "oneOf": [ + { + "type": "object", + "required": ["InvalidMutatorFilter"], + "properties": { + "InvalidMutatorFilter": { + "type": "string", + "description": "Invalid mutator filter expression" + } + } + }, + { + "type": "object", + "required": ["Internal"], + "properties": { + "Internal": { + "type": "string", + "description": "Internal Server Error" + } + } + } + ], + "description": "Mutator operation errors" + }, "Nanoseconds": { "type": "integer" }, diff --git a/vscode/src/modalityApi.ts b/vscode/src/modalityApi.ts index 5574497..cc2eae0 100644 --- a/vscode/src/modalityApi.ts +++ b/vscode/src/modalityApi.ts @@ -45,6 +45,11 @@ export type WorkspaceSegmentId = gen.components["schemas"]["WorkspaceSegmentId"] export type WorkspaceSegmentMetadata = gen.components["schemas"]["WorkspaceSegmentMetadata"]; export type WorkspaceSegmentName = gen.components["schemas"]["WorkspaceSegmentName"]; export type WorkspaceVersionId = gen.components["schemas"]["WorkspaceVersionId"]; +export type MutatorId = gen.components["schemas"]["MutatorId"]; +export type MutatorState = gen.components["schemas"]["MutatorState"]; +export type Mutator = gen.components["schemas"]["Mutator"]; +export type MutationId = gen.components["schemas"]["MutatorId"]; +export type Mutation = gen.components["schemas"]["Mutation"]; type InternalClient = ReturnType>; @@ -112,6 +117,18 @@ export class Client { spec(specName: string): SpecClient { return new SpecClient(this.client, specName); } + + mutators(): MutatorsClient { + return new MutatorsClient(this.client); + } + + mutator(mutatorId: MutatorId): MutatorClient { + return new MutatorClient(this.client, mutatorId); + } + + mutations(): MutationsClient { + return new MutationsClient(this.client); + } } export class WorkspacesClient { @@ -422,6 +439,50 @@ export class SpecVersionClient { } } +export class MutatorsClient { + constructor(private readonly client: InternalClient) {} + + async list(): Promise { + const res = await this.client.get("/v2/mutators", { + params: { + // @ts-ignore + query: [], + }, + }); + return unwrapData(res); + } +} + +export class MutatorClient { + constructor(private readonly client: InternalClient, private readonly mutatorId: MutatorId) {} + + async mutations(): Promise { + const q = []; + q.push(["mutator_id", this.mutatorId]); + const res = await this.client.get("/v2/mutations", { + params: { + // @ts-ignore + query: q, + }, + }); + return unwrapData(res); + } +} + +export class MutationsClient { + constructor(private readonly client: InternalClient) {} + + async list(): Promise { + const res = await this.client.get("/v2/mutations", { + params: { + // @ts-ignore + query: [], + }, + }); + return unwrapData(res); + } +} + /** * Convert a repsonse to just the data; if it's an error, throw the error. */ diff --git a/vscode/src/mutations.ts b/vscode/src/mutations.ts new file mode 100644 index 0000000..9279711 --- /dev/null +++ b/vscode/src/mutations.ts @@ -0,0 +1,385 @@ +import * as vscode from "vscode"; +import * as api from "./modalityApi"; + +class MutationsTreeMemento { + constructor(private readonly memento: vscode.Memento) {} + + // TODO filters/selections + // - all-of-history vs selected-segment + getGroupByMutatorName(): boolean { + return this.memento.get("mutationsTree_groupByMutatorName", false); + } + + async setGroupByMutatorName(val: boolean): Promise { + return this.memento.update("mutationsTree_groupByMutatorName", val); + } + + getFilterBySelectedMutator(): boolean { + return this.memento.get("mutationsTree_filterBySelectedMutator", false); + } + + async setFilterBySelectedMutator(val: boolean): Promise { + return this.memento.update("mutationsTree_filterBySelectedMutator", val); + } +} + +export class MutationsTreeDataProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event; + + workspaceState?: MutationsTreeMemento; + data: MutationsTreeItemData[]; + view: vscode.TreeView; + + selectedMutatorId?: api.MutatorId; + + constructor(private readonly apiClient: api.Client) {} + + register(context: vscode.ExtensionContext) { + this.workspaceState = new MutationsTreeMemento(context.workspaceState); + this.view = vscode.window.createTreeView("auxon.mutations", { + treeDataProvider: this, + canSelectMany: false, + }); + + context.subscriptions.push( + this.view, + vscode.commands.registerCommand("auxon.mutations.refresh", () => this.refresh()), + vscode.commands.registerCommand("auxon.mutations.setSelectedMutator", (mutatorId) => { + this.setSelectedMutator(mutatorId); + }), + vscode.commands.registerCommand("auxon.mutations.setSelectedMutation", (mutationId) => { + this.setSelectedMutation(mutationId); + }), + vscode.commands.registerCommand("auxon.mutations.disableMutationGrouping", () => { + this.disableMutationGrouping(); + }), + vscode.commands.registerCommand("auxon.mutations.groupMutationsByName", () => { + this.groupMutationsByName(); + }), + vscode.commands.registerCommand("auxon.mutations.disableMutationFiltering", () => { + this.disableMutationFiltering(); + }), + vscode.commands.registerCommand("auxon.mutations.filterBySelectedMutator", () => { + this.filterBySelectedMutator(); + }) + ); + + this.refresh(); + } + + refresh(): void { + vscode.commands.executeCommand( + "setContext", + "auxon.mutations.groupBy", + this.workspaceState.getGroupByMutatorName() ? "MUTATOR_NAME" : "NONE" + ); + vscode.commands.executeCommand( + "setContext", + "auxon.mutations.filterBy", + this.workspaceState.getFilterBySelectedMutator() ? "MUTATOR_ID" : "NONE" + ); + + this._onDidChangeTreeData.fire(undefined); + } + + getTreeItem(element: MutationsTreeItemData): vscode.TreeItem { + return element.treeItem(this.workspaceState); + } + + async getChildren(element?: MutationsTreeItemData): Promise { + if (this.workspaceState.getFilterBySelectedMutator() && this.selectedMutatorId == null) { + // Need a selected mutator to populate with + return []; + } else if (!element) { + let mutations = []; + if (this.workspaceState.getFilterBySelectedMutator()) { + mutations = await this.apiClient.mutator(this.selectedMutatorId).mutations(); + } else { + mutations = await this.apiClient.mutations().list(); + } + + let items = []; + if (this.workspaceState.getGroupByMutatorName()) { + const root = new MutationsGroupByNameTreeItemData("", []); + for (const m of mutations) { + root.insertNode(new Mutation(m)); + } + root.updateDescriptions(); + items = await root.children(this.apiClient, this.workspaceState); + } else { + items = mutations.map((m) => new NamedMutationTreeItemData(new Mutation(m))); + } + // TODO - sort by created-at when it's added + const { compare } = Intl.Collator("en-US"); + this.data = items.sort((a, b) => compare(a.mutatorName, b.mutatorName)); + return this.data; + } else { + return await element.children(this.apiClient, this.workspaceState); + } + } + + getParent(element: MutationsTreeItemData): vscode.ProviderResult { + if (this.workspaceState.getGroupByMutatorName()) { + for (const group of this.data) { + if (!(group instanceof MutationsGroupByNameTreeItemData)) { + throw new Error("Internal error: mutations tree node not of expected type"); + } + if (group.childItems.includes(element)) { + return group; + } + } + } + return undefined; + } + + // Set the selected mutator when grouping by mutator name or only showing a single mutator + setSelectedMutator(mutatorId: api.MutatorId) { + if (this.workspaceState.getFilterBySelectedMutator()) { + if (this.selectedMutatorId != mutatorId) { + this.selectedMutatorId = mutatorId; + this.refresh(); + } + } else if (this.workspaceState.getGroupByMutatorName() && this.selectedMutatorId != mutatorId) { + this.selectedMutatorId = mutatorId; + + for (const group of this.data) { + if (!(group instanceof MutationsGroupByNameTreeItemData)) { + throw new Error("Internal error: mutations tree node not of expected type"); + } + const item = group.childItems.find((i) => i.mutatorId == mutatorId); + if (item) { + // Just reveal the parent group + this.view.reveal(group, { focus: true, select: true, expand: 1 }); + return; + } + } + } + } + + setSelectedMutation(_mutationId: api.MutationId) { + // TODO - add this when experiments are added + } + + disableMutationGrouping() { + this.workspaceState.setGroupByMutatorName(false); + this.selectedMutatorId = null; + this.refresh(); + } + + groupMutationsByName() { + this.workspaceState.setGroupByMutatorName(true); + this.selectedMutatorId = null; + this.refresh(); + } + + disableMutationFiltering() { + this.workspaceState.setFilterBySelectedMutator(false); + this.selectedMutatorId = null; + this.refresh(); + } + + filterBySelectedMutator() { + this.workspaceState.setFilterBySelectedMutator(true); + this.selectedMutatorId = null; + this.refresh(); + } +} + +// This is the base of all the tree item data classes +abstract class MutationsTreeItemData { + abstract contextValue: string; + + id?: string = undefined; + mutatorId?: api.MutatorId = undefined; + mutationId?: api.MutationId = undefined; + description?: string = undefined; + tooltip?: vscode.MarkdownString = undefined; + iconPath?: vscode.ThemeIcon = undefined; + + constructor(public name: string) {} + + treeItem(workspaceData: MutationsTreeMemento): vscode.TreeItem { + let state = vscode.TreeItemCollapsibleState.Collapsed; + if (!this.canHaveChildren(workspaceData)) { + state = vscode.TreeItemCollapsibleState.None; + } + + const item = new vscode.TreeItem(this.name, state); + item.contextValue = this.contextValue; + item.description = this.description; + item.tooltip = this.tooltip; + item.iconPath = this.iconPath; + + // Mutator selection populates mutations view + if (this.contextValue == "namedMutation" && !workspaceData.getFilterBySelectedMutator()) { + const command = { + title: "Sets the selected mutator in the mutators tree view", + command: "auxon.mutators.setSelectedMutator", + arguments: [this.mutatorId], + }; + item.command = command; + } + + return item; + } + + canHaveChildren(_workspaceData: MutationsTreeMemento): boolean { + return false; + } + + async children(_apiClient: api.Client, _workspaceState: MutationsTreeMemento): Promise { + return []; + } +} + +export class MutationsGroupByNameTreeItemData extends MutationsTreeItemData { + contextValue = "mutationsGroup"; + constructor(public name: string, public childItems: MutationsTreeItemData[]) { + super(name); + super.iconPath = new vscode.ThemeIcon("replace-all"); + const tooltip = `- **Mutator Name**: ${name}`; + this.tooltip = new vscode.MarkdownString(tooltip); + } + + override canHaveChildren(_workspaceData: MutationsTreeMemento): boolean { + return true; + } + + override async children( + _apiClient: api.Client, + _workspaceState: MutationsTreeMemento + ): Promise { + return this.childItems; + } + + insertNode(mutation: Mutation) { + let nextNodeIndex = this.childItems.findIndex((item) => item.name == mutation.mutatorName); + if (nextNodeIndex == -1) { + this.childItems.push(new MutationsGroupByNameTreeItemData(mutation.mutatorName, [])); + nextNodeIndex = this.childItems.length - 1; + } + + const nextNode = this.childItems[nextNodeIndex]; + if (!(nextNode instanceof MutationsGroupByNameTreeItemData)) { + throw new Error("Internal error: mutations tree node not of expected type"); + } + nextNode.childItems.push(new NamedMutationTreeItemData(mutation)); + } + + updateDescriptions() { + for (const group of this.childItems) { + if (group instanceof MutationsGroupByNameTreeItemData && group.childItems.length > 0) { + const mutationCount = group.childItems.length; + const mutatorIds = new Set(); + for (const item of group.childItems) { + mutatorIds.add(item.mutatorId); + } + group.description = `${mutationCount} mutation`; + if (mutationCount > 1) { + group.description += "s"; + } + group.description += `, ${mutatorIds.size} mutator`; + if (mutatorIds.size > 1) { + group.description += "s"; + } + } + } + } +} + +export class NamedMutationTreeItemData extends MutationsTreeItemData { + contextValue = "namedMutation"; + constructor(public mutation: Mutation) { + super(mutation.mutatorName); + this.id = `${mutation.mutationId}`; + this.mutatorId = mutation.mutatorId; + this.mutationId = mutation.mutationId; + this.description = mutation.mutationId; + let tooltip = `- **Mutator Name**: ${mutation.mutatorName}`; + tooltip += `\n- **Mutator Id**: ${mutation.mutatorId}`; + tooltip += `\n- **Mutation Id**: ${mutation.mutationId}`; + this.tooltip = new vscode.MarkdownString(tooltip); + super.iconPath = new vscode.ThemeIcon("zap"); + } + + override canHaveChildren(_workspaceData: MutationsTreeMemento): boolean { + // TODO - always true once created-at/etc is added + return this.mutation.experimentName != null || this.mutation.params.size != 0; + } + + override async children( + _apiClient: api.Client, + _workspaceState: MutationsTreeMemento + ): Promise { + const children = []; + if (this.mutation.experimentName) { + children.push(new MutationDetailLeafTreeItemData(`Experiment: ${this.mutation.experimentName}`)); + } + if (this.mutation.params.size != 0) { + children.push(new MutationParametersTreeItemData(this.mutation)); + } + return children; + } +} + +export class MutationParametersTreeItemData extends MutationsTreeItemData { + contextValue = "mutationParameters"; + constructor(public mutation: Mutation) { + super("Parameters"); + } + + override canHaveChildren(_workspaceData: MutationsTreeMemento): boolean { + return true; + } + + override async children( + _apiClient: api.Client, + _workspaceState: MutationsTreeMemento + ): Promise { + const children = []; + for (const [paramName, paramValue] of this.mutation.params) { + children.push(new MutationDetailLeafTreeItemData(`${paramName}: ${paramValue}`)); + } + return children; + } +} + +export class MutationDetailLeafTreeItemData extends MutationsTreeItemData { + contextValue = "mutationDetail"; + constructor(public name: string) { + super(name); + } +} + +// TODO +// - add created-at datetime, sort the list +// - add checklist coordinate fields +class Mutation { + mutatorId: api.MutatorId; + mutatorName = ""; + mutatorDescription?: string = undefined; + mutationId: api.MutationId; + experimentName?: string = undefined; + params: Map; + + constructor(private mutation: api.Mutation) { + this.mutatorId = mutation.mutator_id; + this.mutationId = mutation.mutation_id; + if (mutation.experiment_name) { + this.experimentName = mutation.experiment_name; + } + if (Object.prototype.hasOwnProperty.call(mutation.mutator_attributes, "mutator.name")) { + this.mutatorName = mutation.mutator_attributes["mutator.name"] as string; + } + if (Object.prototype.hasOwnProperty.call(mutation.mutator_attributes, "mutator.description")) { + this.mutatorDescription = mutation.mutator_attributes["mutator.description"] as string; + } + this.params = new Map(); + for (const [paramName, paramValue] of Object.entries(mutation.params)) { + this.params.set(paramName, paramValue); + } + } +} diff --git a/vscode/src/mutators.ts b/vscode/src/mutators.ts new file mode 100644 index 0000000..bc1f0ab --- /dev/null +++ b/vscode/src/mutators.ts @@ -0,0 +1,444 @@ +import * as vscode from "vscode"; +import * as api from "./modalityApi"; + +class MutatorsTreeMemento { + constructor(private readonly memento: vscode.Memento) {} + + getShowUnavailable(): boolean { + return this.memento.get("mutatorsTree_showUnavailable", false); + } + + async setShowUnavailable(val: boolean): Promise { + return this.memento.update("mutatorsTree_showUnavailable", val); + } + + getGroupByMutatorName(): boolean { + return this.memento.get("mutatorsTree_groupByMutatorName", false); + } + + async setGroupByMutatorName(val: boolean): Promise { + return this.memento.update("mutatorsTree_groupByMutatorName", val); + } +} + +export class MutatorsTreeDataProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event; + + workspaceState?: MutatorsTreeMemento; + data: MutatorsTreeItemData[]; + view: vscode.TreeView; + + constructor(private readonly apiClient: api.Client) {} + + register(context: vscode.ExtensionContext) { + this.workspaceState = new MutatorsTreeMemento(context.workspaceState); + this.view = vscode.window.createTreeView("auxon.mutators", { + treeDataProvider: this, + canSelectMany: false, + }); + + context.subscriptions.push( + this.view, + vscode.commands.registerCommand("auxon.mutators.refresh", () => this.refresh()), + vscode.commands.registerCommand("auxon.mutators.showUnavailable", () => this.showUnavailable(true)), + vscode.commands.registerCommand("auxon.mutators.hideUnavailable", () => this.showUnavailable(false)), + vscode.commands.registerCommand("auxon.mutators.setSelectedMutator", (mutatorId) => { + this.setSelectedMutator(mutatorId); + }), + vscode.commands.registerCommand("auxon.mutators.disableMutatorGrouping", () => { + this.disableMutatorGrouping(); + }), + vscode.commands.registerCommand("auxon.mutators.groupMutatorsByName", () => { + this.groupMutatorsByName(); + }) + ); + + this.refresh(); + } + + refresh(): void { + vscode.commands.executeCommand( + "setContext", + "auxon.mutators.unavailable", + this.workspaceState.getShowUnavailable() ? "SHOW" : "HIDE" + ); + vscode.commands.executeCommand( + "setContext", + "auxon.mutators.groupBy", + this.workspaceState.getGroupByMutatorName() ? "MUTATOR_NAME" : "NONE" + ); + + this._onDidChangeTreeData.fire(undefined); + } + + showUnavailable(show: boolean) { + this.workspaceState.setShowUnavailable(show); + this.refresh(); + } + + getTreeItem(element: MutatorsTreeItemData): vscode.TreeItem { + return element.treeItem(this.workspaceState); + } + + async getChildren(element?: MutatorsTreeItemData): Promise { + if (!element) { + let mutators = await this.apiClient.mutators().list(); + if (!this.workspaceState.getShowUnavailable()) { + mutators = mutators.filter((m) => m.mutator_state === "Available"); + } + let items = []; + if (this.workspaceState.getGroupByMutatorName()) { + const root = new MutatorsGroupByNameTreeItemData("", []); + for (const m of mutators) { + root.insertNode(new Mutator(m)); + } + root.updateDescriptions(); + items = await root.children(this.apiClient, this.workspaceState); + } else { + items = mutators.map((m) => new NamedMutatorTreeItemData(new Mutator(m))); + } + const { compare } = Intl.Collator("en-US"); + this.data = items.sort((a, b) => compare(a.name, b.name)); + return this.data; + } else { + return await element.children(this.apiClient, this.workspaceState); + } + } + + getParent(element: MutatorsTreeItemData): vscode.ProviderResult { + if (this.workspaceState.getGroupByMutatorName()) { + for (const group of this.data) { + if (!(group instanceof MutatorsGroupByNameTreeItemData)) { + throw new Error("Internal error: mutators tree node not of expected type"); + } + if (group.childItems.includes(element)) { + return group; + } + } + } + return undefined; + } + + setSelectedMutator(mutatorId: api.MutatorId) { + if (this.workspaceState.getGroupByMutatorName()) { + for (const group of this.data) { + if (!(group instanceof MutatorsGroupByNameTreeItemData)) { + throw new Error("Internal error: mutators tree node not of expected type"); + } + const item = group.childItems.find((i) => i.mutatorId == mutatorId); + if (item) { + // Treeview doesn't appear to handle selecting nested items well. + // Instead we need to reveal the parent first then the item + this.view.reveal(group, { focus: true, select: true, expand: 1 }).then(() => { + this.view.reveal(item, { focus: true, select: true, expand: 10 }); + }); + return; + } + } + } else { + const item = this.data.find((i) => i.mutatorId == mutatorId); + if (item) { + this.view.reveal(item, { focus: true, select: true, expand: 10 }); + } + } + } + + disableMutatorGrouping() { + this.workspaceState.setGroupByMutatorName(false); + this.refresh(); + } + + groupMutatorsByName() { + this.workspaceState.setGroupByMutatorName(true); + this.refresh(); + } +} + +// This is the base of all the tree item data classes +abstract class MutatorsTreeItemData { + abstract contextValue: string; + + id?: string = undefined; + mutatorId?: api.MutatorId = undefined; + description?: string = undefined; + tooltip?: vscode.MarkdownString = undefined; + iconPath?: vscode.ThemeIcon = undefined; + + constructor(public name: string) {} + + treeItem(workspaceData: MutatorsTreeMemento): vscode.TreeItem { + let state = vscode.TreeItemCollapsibleState.Collapsed; + if (!this.canHaveChildren(workspaceData)) { + state = vscode.TreeItemCollapsibleState.None; + } + + const item = new vscode.TreeItem(this.name, state); + item.contextValue = this.contextValue; + item.description = this.description; + item.tooltip = this.tooltip; + item.iconPath = this.iconPath; + + // Mutator selection populates mutations view + if (this.contextValue == "mutator") { + const command = { + title: "Set the selected mutator in the mutations tree view", + command: "auxon.mutations.setSelectedMutator", + arguments: [this.mutatorId], + }; + item.command = command; + } + + return item; + } + + canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + return false; + } + + async children(_apiClient: api.Client, _workspaceState: MutatorsTreeMemento): Promise { + return []; + } +} + +export class MutatorsGroupByNameTreeItemData extends MutatorsTreeItemData { + contextValue = "mutatorsGroup"; + constructor(public name: string, public childItems: MutatorsTreeItemData[]) { + super(name); + super.iconPath = new vscode.ThemeIcon("github-action"); + const tooltip = `- **Mutator Name**: ${name}`; + this.tooltip = new vscode.MarkdownString(tooltip); + } + + override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + return true; + } + + override async children( + _apiClient: api.Client, + _workspaceState: MutatorsTreeMemento + ): Promise { + return this.childItems; + } + + insertNode(mutator: Mutator) { + let nextNodeIndex = this.childItems.findIndex((item) => item.name == mutator.name); + if (nextNodeIndex == -1) { + this.childItems.push(new MutatorsGroupByNameTreeItemData(mutator.name, [])); + nextNodeIndex = this.childItems.length - 1; + } + + const nextNode = this.childItems[nextNodeIndex]; + if (!(nextNode instanceof MutatorsGroupByNameTreeItemData)) { + throw new Error("Internal error: mutators tree node not of expected type"); + } + nextNode.childItems.push(new NamedMutatorTreeItemData(mutator)); + } + + updateDescriptions() { + for (const group of this.childItems) { + if (group instanceof MutatorsGroupByNameTreeItemData && group.childItems.length > 0) { + const mutatorCount = group.childItems.length; + group.description = `${mutatorCount} mutator`; + if (mutatorCount > 1) { + group.description += "s"; + } + } + } + } +} + +export class NamedMutatorTreeItemData extends MutatorsTreeItemData { + contextValue = "mutator"; + constructor(public mutator: Mutator) { + super(mutator.name); + this.id = `${mutator.id}`; + this.mutatorId = mutator.id; + this.description = mutator.id; + let tooltip = `- **Mutator Name**: ${mutator.name}`; + tooltip += `\n- **Mutator Id**: ${mutator.id}`; + this.tooltip = new vscode.MarkdownString(tooltip); + this.iconPath = new vscode.ThemeIcon("outline-view-icon"); + } + + override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + return true; + } + + override async children( + _apiClient: api.Client, + _workspaceState: MutatorsTreeMemento + ): Promise { + const children = []; + if (this.mutator.description) { + children.push(new MutatorDetailLeafTreeItemData(`${this.mutator.description}`)); + } + if (this.mutator.layer) { + children.push(new MutatorDetailLeafTreeItemData(`Layer: ${this.mutator.layer}`)); + } + if (this.mutator.operation) { + children.push(new MutatorDetailLeafTreeItemData(`Operation: ${this.mutator.operation}`)); + } + if (this.mutator.group) { + children.push(new MutatorDetailLeafTreeItemData(`Group: ${this.mutator.group}`)); + } + children.push(new MutatorDetailLeafTreeItemData(`State: ${this.mutator.state}`)); + if (this.mutator.orgMetadataAttrs.size != 0) { + children.push(new MutatorOrgMetadataTreeItemData(this.mutator.orgMetadataAttrs)); + } + if (this.mutator.params.length != 0) { + children.push(new MutatorParametersTreeItemData(this.mutator.params)); + } + return children; + } +} + +export class MutatorOrgMetadataTreeItemData extends MutatorsTreeItemData { + contextValue = "mutatorOrgMetadata"; + constructor(public orgMetadataAttrs: Map) { + super("Organization Metadata"); + super.iconPath = new vscode.ThemeIcon("organization"); + } + + override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + return true; + } + + override async children( + _apiClient: api.Client, + _workspaceState: MutatorsTreeMemento + ): Promise { + const children = []; + for (const [k, v] of this.orgMetadataAttrs) { + children.push(new MutatorDetailLeafTreeItemData(`${k}: ${v}`)); + } + return children; + } +} + +export class MutatorParametersTreeItemData extends MutatorsTreeItemData { + contextValue = "mutatorParameters"; + constructor(public params: MutatorParameter[]) { + super("Parameters"); + super.iconPath = new vscode.ThemeIcon("output"); + } + + override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + return true; + } + + override async children( + _apiClient: api.Client, + _workspaceState: MutatorsTreeMemento + ): Promise { + const children = []; + for (const p of this.params) { + children.push(new MutatorParameterTreeItemData(p)); + } + return children; + } +} + +export class MutatorParameterTreeItemData extends MutatorsTreeItemData { + contextValue = "mutatorParameter"; + constructor(public param: MutatorParameter) { + super(param.name); + } + + override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + return true; + } + + override async children( + _apiClient: api.Client, + _workspaceState: MutatorsTreeMemento + ): Promise { + const children = []; + if (this.param.description) { + children.push(new MutatorDetailLeafTreeItemData(`${this.param.description}`)); + } + for (const [k, v] of this.param.attrs) { + children.push(new MutatorDetailLeafTreeItemData(`${k}: ${v}`)); + } + return children; + } +} + +export class MutatorDetailLeafTreeItemData extends MutatorsTreeItemData { + contextValue = "mutatorDetail"; + constructor(public name: string) { + super(name); + } +} + +export class Mutator { + id: api.MutatorId; + state: api.MutatorState; + name = ""; + description?: string = undefined; + layer?: string = undefined; + operation?: string = undefined; + group?: string = undefined; + orgMetadataAttrs: Map; + params: MutatorParameter[]; + + constructor(private mutator: api.Mutator) { + this.id = mutator.mutator_id; + this.state = mutator.mutator_state; + this.orgMetadataAttrs = new Map(); + this.params = []; + const paramAttrsByPrefix: Map> = new Map(); + + for (const key in mutator.mutator_attributes) { + if (key == "mutator.name") { + this.name = mutator.mutator_attributes[key] as string; + } else if (key == "mutator.description") { + this.description = mutator.mutator_attributes[key] as string; + } else if (key == "mutator.layer") { + this.layer = mutator.mutator_attributes[key] as string; + } else if (key == "mutator.operation") { + this.operation = mutator.mutator_attributes[key] as string; + } else if (key == "mutator.group") { + this.group = mutator.mutator_attributes[key] as string; + } else if (key.startsWith("mutator.params")) { + const pk = key.replace("mutator.params.", ""); + const pnamePrefix = pk.split(".", 1)[0]; + if (!paramAttrsByPrefix.has(pnamePrefix)) { + paramAttrsByPrefix.set(pnamePrefix, new Map()); + } + const paramAttrs = paramAttrsByPrefix.get(pnamePrefix); + paramAttrs.set(pk.replace(`${pnamePrefix}.`, ""), mutator.mutator_attributes[key]); + } else { + // Remaining are organization_custom_metadata attributes + this.orgMetadataAttrs.set(key.replace("mutator.", ""), mutator.mutator_attributes[key]); + } + } + + for (const [_prefix, attrs] of paramAttrsByPrefix) { + if (attrs.size != 0) { + this.params.push(new MutatorParameter(attrs)); + } + } + } +} + +class MutatorParameter { + name = ""; + description?: string = undefined; + attrs: Map; + + constructor(private paramAttrs: Map) { + this.attrs = new Map(); + for (const [k, v] of paramAttrs) { + if (k == "name") { + this.name = v as string; + } else if (k == "description") { + this.description = v as string; + } else { + this.attrs.set(k, v); + } + } + } +} From 2fa5c98d7ecca86f37867adda990dc5f72701270 Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Tue, 16 Jan 2024 08:22:42 -0800 Subject: [PATCH 04/12] Add mutation control plane controls UI --- vscode/package-lock.json | 4 +- vscode/package.json | 41 ++++++++ vscode/src/deviantCommands.ts | 174 ++++++++++++++++++++++++++++++++++ vscode/src/main.ts | 2 + vscode/src/mutations.ts | 14 ++- vscode/src/mutators.ts | 18 +++- 6 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 vscode/src/deviantCommands.ts diff --git a/vscode/package-lock.json b/vscode/package-lock.json index ec5b154..e463be1 100644 --- a/vscode/package-lock.json +++ b/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "speqtr", - "version": "0.3.0", + "version": "0.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "speqtr", - "version": "0.3.0", + "version": "0.5.1", "dependencies": { "@types/lodash": "^4.14.200", "@viz-js/viz": "^3.0.1", diff --git a/vscode/package.json b/vscode/package.json index 0c5cb4c..37b8839 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -309,6 +309,16 @@ "command": "auxon.specs.deleteManyResults", "when": "view == auxon.specs && viewItem == specResult && listMultiSelection && false", "group": "2_context@2" + }, + { + "command": "auxon.mutators.createMutation", + "when": "view == auxon.mutators && viewItem == mutator", + "group": "inline" + }, + { + "command": "auxon.mutations.clearMutation", + "when": "view == auxon.mutations && viewItem == mutation", + "group": "inline" } ], "view/title": [ @@ -392,6 +402,11 @@ "when": "view == auxon.specs && auxon.specs.results == 'SHOW'", "group": "7_modification@2" }, + { + "command": "auxon.deviant.clearAllMutations", + "when": "view == auxon.mutations", + "group": "7_modification@1" + }, { "command": "auxon.mutators.refresh", "when": "view == auxon.mutators", @@ -680,6 +695,22 @@ "title": "View Log", "icon": "$(open-preview)" }, + { + "command": "auxon.deviant.clearMutation", + "title": "Clear mutations using the deviant CLI" + }, + { + "command": "auxon.deviant.clearAllMutations", + "title": "Clear All Mutations" + }, + { + "command": "auxon.deviant.createMutation", + "title": "Create a mutation using the deviant CLI" + }, + { + "command": "auxon.deviant.runCreateMutationWizard", + "title": "Create a Mutation" + }, { "command": "auxon.mutators.refresh", "title": "Refresh Mutator List", @@ -705,6 +736,11 @@ "command": "auxon.mutators.disableMutatorGrouping", "title": "✓ Group By Mutator Name" }, + { + "command": "auxon.mutators.createMutation", + "title": "Create a Mutation", + "icon": "$(zap)" + }, { "command": "auxon.mutations.refresh", "title": "Refresh Mutation List", @@ -733,6 +769,11 @@ { "command": "auxon.mutations.disableMutationFiltering", "title": "✓ Filter By Selected Mutator" + }, + { + "command": "auxon.mutations.clearMutation", + "title": "Clear Mutation", + "icon": "$(notebook-state-error)" } ], "configuration": { diff --git a/vscode/src/deviantCommands.ts b/vscode/src/deviantCommands.ts new file mode 100644 index 0000000..8f3eccc --- /dev/null +++ b/vscode/src/deviantCommands.ts @@ -0,0 +1,174 @@ +import * as vscode from "vscode"; +import * as config from "./config"; +import * as util from "util"; +import * as child_process from "child_process"; +import { Mutator, MutatorParameter } from "./mutators"; + +const execFile = util.promisify(child_process.execFile); + +export function register(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand("auxon.deviant.clearMutation", runDeviantMutationClearCommand), + vscode.commands.registerCommand("auxon.deviant.clearAllMutations", clearAllMutations), + vscode.commands.registerCommand("auxon.deviant.createMutation", runDeviantMutationCreateCommand), + vscode.commands.registerCommand("auxon.deviant.runCreateMutationWizard", runCreateMutationWizard) + ); +} + +export type MutationClearCommandArgs = { + mutationId?: string; +}; + +async function clearAllMutations() { + await runDeviantMutationClearCommand({}); +} + +async function runDeviantMutationClearCommand(args: MutationClearCommandArgs) { + const deviantPath = config.toolPath("deviant"); + + const commandArgs = ["mutation", "clear"]; + + if (args.mutationId) { + commandArgs.push("--mutation-id", args.mutationId); + } + + try { + const res = await execFile(deviantPath, commandArgs, { encoding: "utf8" }); + await vscode.window.showInformationMessage(res.stdout); + } catch (e) { + vscode.window.showErrorMessage(e.stderr.trim()); + } +} + +export type MutationCreateCommandArgs = { + mutatorId?: string; + params?: string[]; + experimentName?: string; +}; + +async function runDeviantMutationCreateCommand(args: MutationCreateCommandArgs) { + const deviantPath = config.toolPath("deviant"); + + const commandArgs = ["mutation", "create"]; + + if (args.mutatorId) { + commandArgs.push("--mutator-id", args.mutatorId); + } + + if (args.experimentName) { + commandArgs.push("--experiment", args.experimentName); + } + + if (args.params) { + for (const paramKeyValuePair of args.params) { + commandArgs.push("--params", paramKeyValuePair); + } + } + + try { + const res = await execFile(deviantPath, commandArgs, { encoding: "utf8" }); + await vscode.window.showInformationMessage(res.stdout); + } catch (e) { + vscode.window.showErrorMessage(e.stderr.trim()); + } +} + +// TODO - add linked experiment option +async function runCreateMutationWizard(mutator: Mutator) { + const title = `Create a mutation for mutator '${mutator.name}'`; + + if (mutator.params.length == 0) { + const options = { + title, + placeHolder: "This mutator doesn't have any parameters", + ignoreFocusOut: true, + validateInput: validateParameterLess, + }; + const result = await vscode.window.showInputBox(options); + if (result !== undefined) { + vscode.commands.executeCommand("auxon.deviant.createMutation", { + mutatorId: mutator.id, + }); + } + } else { + let step = 1; + const maxSteps = mutator.params.length; + const params = []; + + for (const param of mutator.params) { + const options = { + title: `${title} (${step}/${maxSteps})`, + placeHolder: `Enter the parameter value for '${param.name}' or leave blank for Deviant-suggested values`, + ignoreFocusOut: true, + validateInput: (input) => validateParameter(input, param), + }; + let paramValue = await vscode.window.showInputBox(options); + if (paramValue === undefined) { + // User canceled + return; + } else if (paramValue !== "") { + // Normalize so the CLI parses as a float + if (param.valueType === "Float" && !paramValue.includes(".")) { + paramValue = `${paramValue}.0`; + } + + params.push(`${param.name}=${paramValue}`); + } + + step += 1; + } + + vscode.commands.executeCommand("auxon.deviant.createMutation", { + mutatorId: mutator.id, + params, + }); + } +} + +function validateParameterLess(input: string): string | null { + if (input.length === 0) { + return null; + } else { + return "This mutator is parameter-less. Leave the field empty to continue."; + } +} + +function validateParameter(input: string, param: MutatorParameter): string | null { + if (input === "") { + // Empty string is allowed, means we'll use a deviant-suggested value + return null; + } + + if (param.valueType === "Float") { + if (isFloat(input)) { + return null; + } else { + return "Value must be a float type"; + } + } else if (param.valueType === "Integer") { + if (isInt(input)) { + return null; + } else { + return "Value must be an integer type"; + } + } else { + return null; + } +} + +function isFloat(val) { + const floatRegex = /^-?\d+(?:[.]\d*?)?$/; + if (!floatRegex.test(val)) return false; + + val = parseFloat(val); + if (isNaN(val)) return false; + return true; +} + +function isInt(val) { + const intRegex = /^-?\d+$/; + if (!intRegex.test(val)) return false; + + const intVal = parseInt(val, 10); + return parseFloat(val) == intVal && !isNaN(intVal); +} diff --git a/vscode/src/main.ts b/vscode/src/main.ts index d50ce48..3fd1c8e 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -19,6 +19,7 @@ import * as config from "./config"; import * as speqtrLinkProvider from "./speqtrLinkProvider"; import * as mutators from "./mutators"; import * as mutations from "./mutations"; +import * as deviantCommands from "./deviantCommands"; export let log: vscode.OutputChannel; let lspClient: LanguageClient; @@ -52,6 +53,7 @@ export async function activate(context: vscode.ExtensionContext) { transitionGraph.register(context, apiClient); specFileCommands.register(context); speqtrLinkProvider.register(context); + deviantCommands.register(context); const specCoverageProvider = new specCoverage.SpecCoverageProvider(apiClient); await specCoverageProvider.initialize(context); diff --git a/vscode/src/mutations.ts b/vscode/src/mutations.ts index 9279711..45270db 100644 --- a/vscode/src/mutations.ts +++ b/vscode/src/mutations.ts @@ -64,6 +64,9 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider { this.filterBySelectedMutator(); + }), + vscode.commands.registerCommand("auxon.mutations.clearMutation", (itemData) => { + this.clearMutation(itemData); }) ); @@ -186,6 +189,13 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider { this.groupMutatorsByName(); + }), + vscode.commands.registerCommand("auxon.mutators.createMutation", (itemData) => { + this.createMutation(itemData); }) ); @@ -155,6 +158,15 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider; constructor(private paramAttrs: Map) { @@ -436,6 +450,8 @@ class MutatorParameter { this.name = v as string; } else if (k == "description") { this.description = v as string; + } else if (k == "value_type") { + this.valueType = v as string; } else { this.attrs.set(k, v); } From c96338dfe69714b2f811b1c4f29ed4aadf8177d8 Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Tue, 16 Jan 2024 08:48:02 -0800 Subject: [PATCH 05/12] Cleanup mutation-create info message --- vscode/src/deviantCommands.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vscode/src/deviantCommands.ts b/vscode/src/deviantCommands.ts index 8f3eccc..08ee464 100644 --- a/vscode/src/deviantCommands.ts +++ b/vscode/src/deviantCommands.ts @@ -49,7 +49,7 @@ export type MutationCreateCommandArgs = { async function runDeviantMutationCreateCommand(args: MutationCreateCommandArgs) { const deviantPath = config.toolPath("deviant"); - const commandArgs = ["mutation", "create"]; + const commandArgs = ["mutation", "create", "--format", "json"]; if (args.mutatorId) { commandArgs.push("--mutator-id", args.mutatorId); @@ -67,7 +67,8 @@ async function runDeviantMutationCreateCommand(args: MutationCreateCommandArgs) try { const res = await execFile(deviantPath, commandArgs, { encoding: "utf8" }); - await vscode.window.showInformationMessage(res.stdout); + const output = JSON.parse(res.stdout) as string; + await vscode.window.showInformationMessage(`Created mutation '${output["mutation_id"]}'`); } catch (e) { vscode.window.showErrorMessage(e.stderr.trim()); } From a0cdd5837a4547d497b36d140804a054a120684c Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Tue, 16 Jan 2024 09:04:33 -0800 Subject: [PATCH 06/12] Remove unused parameters in the mutations/mutators tree view interface --- vscode/src/mutations.ts | 31 +++++++++----------------- vscode/src/mutators.ts | 49 ++++++++++++++--------------------------- 2 files changed, 28 insertions(+), 52 deletions(-) diff --git a/vscode/src/mutations.ts b/vscode/src/mutations.ts index 45270db..1e9b09f 100644 --- a/vscode/src/mutations.ts +++ b/vscode/src/mutations.ts @@ -111,7 +111,7 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider new NamedMutationTreeItemData(new Mutation(m))); } @@ -120,7 +120,7 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider compare(a.mutatorName, b.mutatorName)); return this.data; } else { - return await element.children(this.apiClient, this.workspaceState); + return element.children(); } } @@ -213,7 +213,7 @@ abstract class MutationsTreeItemData { treeItem(workspaceData: MutationsTreeMemento): vscode.TreeItem { let state = vscode.TreeItemCollapsibleState.Collapsed; - if (!this.canHaveChildren(workspaceData)) { + if (!this.canHaveChildren()) { state = vscode.TreeItemCollapsibleState.None; } @@ -236,11 +236,11 @@ abstract class MutationsTreeItemData { return item; } - canHaveChildren(_workspaceData: MutationsTreeMemento): boolean { + canHaveChildren(): boolean { return false; } - async children(_apiClient: api.Client, _workspaceState: MutationsTreeMemento): Promise { + children(): MutationsTreeItemData[] { return []; } } @@ -254,14 +254,11 @@ export class MutationsGroupByNameTreeItemData extends MutationsTreeItemData { this.tooltip = new vscode.MarkdownString(tooltip); } - override canHaveChildren(_workspaceData: MutationsTreeMemento): boolean { + override canHaveChildren(): boolean { return true; } - override async children( - _apiClient: api.Client, - _workspaceState: MutationsTreeMemento - ): Promise { + override children(): MutationsTreeItemData[] { return this.childItems; } @@ -315,15 +312,12 @@ export class NamedMutationTreeItemData extends MutationsTreeItemData { super.iconPath = new vscode.ThemeIcon("zap"); } - override canHaveChildren(_workspaceData: MutationsTreeMemento): boolean { + override canHaveChildren(): boolean { // TODO - always true once created-at/etc is added return this.mutation.experimentName != null || this.mutation.params.size != 0; } - override async children( - _apiClient: api.Client, - _workspaceState: MutationsTreeMemento - ): Promise { + override children(): MutationsTreeItemData[] { const children = []; if (this.mutation.experimentName) { children.push(new MutationDetailLeafTreeItemData(`Experiment: ${this.mutation.experimentName}`)); @@ -341,14 +335,11 @@ export class MutationParametersTreeItemData extends MutationsTreeItemData { super("Parameters"); } - override canHaveChildren(_workspaceData: MutationsTreeMemento): boolean { + override canHaveChildren(): boolean { return true; } - override async children( - _apiClient: api.Client, - _workspaceState: MutationsTreeMemento - ): Promise { + override children(): MutationsTreeItemData[] { const children = []; for (const [paramName, paramValue] of this.mutation.params) { children.push(new MutationDetailLeafTreeItemData(`${paramName}: ${paramValue}`)); diff --git a/vscode/src/mutators.ts b/vscode/src/mutators.ts index a2ccf30..476aadb 100644 --- a/vscode/src/mutators.ts +++ b/vscode/src/mutators.ts @@ -83,7 +83,7 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider { @@ -99,7 +99,7 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider new NamedMutatorTreeItemData(new Mutator(m))); } @@ -107,7 +107,7 @@ export class MutatorsTreeDataProvider implements vscode.TreeDataProvider compare(a.name, b.name)); return this.data; } else { - return await element.children(this.apiClient, this.workspaceState); + return element.children(); } } @@ -181,9 +181,9 @@ abstract class MutatorsTreeItemData { constructor(public name: string) {} - treeItem(workspaceData: MutatorsTreeMemento): vscode.TreeItem { + treeItem(): vscode.TreeItem { let state = vscode.TreeItemCollapsibleState.Collapsed; - if (!this.canHaveChildren(workspaceData)) { + if (!this.canHaveChildren()) { state = vscode.TreeItemCollapsibleState.None; } @@ -206,11 +206,11 @@ abstract class MutatorsTreeItemData { return item; } - canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + canHaveChildren(): boolean { return false; } - async children(_apiClient: api.Client, _workspaceState: MutatorsTreeMemento): Promise { + children(): MutatorsTreeItemData[] { return []; } } @@ -224,14 +224,11 @@ export class MutatorsGroupByNameTreeItemData extends MutatorsTreeItemData { this.tooltip = new vscode.MarkdownString(tooltip); } - override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + override canHaveChildren(): boolean { return true; } - override async children( - _apiClient: api.Client, - _workspaceState: MutatorsTreeMemento - ): Promise { + override children(): MutatorsTreeItemData[] { return this.childItems; } @@ -275,14 +272,11 @@ export class NamedMutatorTreeItemData extends MutatorsTreeItemData { this.iconPath = new vscode.ThemeIcon("outline-view-icon"); } - override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + override canHaveChildren(): boolean { return true; } - override async children( - _apiClient: api.Client, - _workspaceState: MutatorsTreeMemento - ): Promise { + override children(): MutatorsTreeItemData[] { const children = []; if (this.mutator.description) { children.push(new MutatorDetailLeafTreeItemData(`${this.mutator.description}`)); @@ -314,14 +308,11 @@ export class MutatorOrgMetadataTreeItemData extends MutatorsTreeItemData { super.iconPath = new vscode.ThemeIcon("organization"); } - override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + override canHaveChildren(): boolean { return true; } - override async children( - _apiClient: api.Client, - _workspaceState: MutatorsTreeMemento - ): Promise { + override children(): MutatorsTreeItemData[] { const children = []; for (const [k, v] of this.orgMetadataAttrs) { children.push(new MutatorDetailLeafTreeItemData(`${k}: ${v}`)); @@ -337,14 +328,11 @@ export class MutatorParametersTreeItemData extends MutatorsTreeItemData { super.iconPath = new vscode.ThemeIcon("output"); } - override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + override canHaveChildren(): boolean { return true; } - override async children( - _apiClient: api.Client, - _workspaceState: MutatorsTreeMemento - ): Promise { + override children(): MutatorsTreeItemData[] { const children = []; for (const p of this.params) { children.push(new MutatorParameterTreeItemData(p)); @@ -359,14 +347,11 @@ export class MutatorParameterTreeItemData extends MutatorsTreeItemData { super(param.name); } - override canHaveChildren(_workspaceData: MutatorsTreeMemento): boolean { + override canHaveChildren(): boolean { return true; } - override async children( - _apiClient: api.Client, - _workspaceState: MutatorsTreeMemento - ): Promise { + override children(): MutatorsTreeItemData[] { const children = []; if (this.param.description) { children.push(new MutatorDetailLeafTreeItemData(`${this.param.description}`)); From 55f1d0f09f6b488eb3f308f31c513e7bfe04d440 Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Tue, 16 Jan 2024 12:30:03 -0800 Subject: [PATCH 07/12] Update settings and view container names --- vscode/package.json | 8 ++++---- vscode/src/main.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vscode/package.json b/vscode/package.json index 37b8839..f3e2dcf 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -66,17 +66,17 @@ "activitybar": [ { "id": "modality-explorer", - "title": "Modality", + "title": "Auxon Modality", "icon": "images/Modality_symbol-whitesquare.svg" }, { "id": "conform-explorer", - "title": "Conform", + "title": "Auxon Conform", "icon": "images/Conform_symbol-whitesquare.svg" }, { "id": "deviant-explorer", - "title": "Deviant", + "title": "Auxon Deviant", "icon": "images/Deviant_symbol-whitesquare.svg" } ] @@ -778,7 +778,7 @@ ], "configuration": { "type": "object", - "title": "Auxon SpeQTr", + "title": "Auxon", "properties": { "auxon.tooldir": { "type": [ diff --git a/vscode/src/main.ts b/vscode/src/main.ts index 3fd1c8e..2747512 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -25,7 +25,7 @@ export let log: vscode.OutputChannel; let lspClient: LanguageClient; export async function activate(context: vscode.ExtensionContext) { - log = vscode.window.createOutputChannel("Auxon SpeQTr"); + log = vscode.window.createOutputChannel("Auxon"); // If this is a fresh install, prompt for new first user creation await user.handleNewUserCreation(); From 4148fd5ff30761faa4de95b463979f0d2abd1c90 Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Wed, 17 Jan 2024 04:59:23 -0800 Subject: [PATCH 08/12] Show workspaces/segments views from all auxon explorer view tabs --- vscode/package.json | 46 +++++++++++++++++++++++++------------- vscode/src/segments.ts | 48 +++++++++++++++++++++++++++++++++++----- vscode/src/workspaces.ts | 8 ++++++- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/vscode/package.json b/vscode/package.json index f3e2dcf..c13a0ce 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -84,11 +84,11 @@ "views": { "modality-explorer": [ { - "id": "auxon.workspaces", + "id": "auxon.modality_workspaces", "name": "Workspaces" }, { - "id": "auxon.segments", + "id": "auxon.modality_segments", "name": "Segments" }, { @@ -101,12 +101,28 @@ } ], "conform-explorer": [ + { + "id": "auxon.conform_workspaces", + "name": "Workspaces" + }, + { + "id": "auxon.conform_segments", + "name": "Segments" + }, { "id": "auxon.specs", "name": "Specs" } ], "deviant-explorer": [ + { + "id": "auxon.deviant_workspaces", + "name": "Workspaces" + }, + { + "id": "auxon.deviant_segments", + "name": "Segments" + }, { "id": "auxon.mutators", "name": "Mutators" @@ -148,29 +164,29 @@ "view/item/context": [ { "command": "auxon.workspaces.setActive", - "when": "view == auxon.workspaces && viewItem == workspace", + "when": "view =~ /auxon\\.[a-zA-Z]*_workspaces/ && viewItem == workspace", "group": "navigation@1" }, { "command": "auxon.modality.log", - "when": "view == auxon.segments || view == auxon.timelines || (view == auxon.events && viewItem == event)", + "when": "view =~ /auxon\\.[a-zA-Z]*_segments/ || view == auxon.timelines || (view == auxon.events && viewItem == event)", "group": "inline" }, { "command": "auxon.segments.setActive", - "when": "(view == auxon.segments && viewItem == segment) && !listMultiSelection" + "when": "(view =~ /auxon\\.[a-zA-Z]*_segments/ && viewItem == segment) && !listMultiSelection" }, { "command": "auxon.segments.setActiveFromSelection", - "when": "(view == auxon.segments && viewItem == segment) && listMultiSelection" + "when": "(view =~ /auxon\\.[a-zA-Z]*_segments/ && viewItem == segment) && listMultiSelection" }, { "command": "auxon.segments.transitionGraph", - "when": "view == auxon.segments" + "when": "view =~ /auxon\\.[a-zA-Z]*_segments/" }, { "command": "auxon.segments.specCoverage", - "when": "view == auxon.segments" + "when": "view =~ /auxon\\.[a-zA-Z]*_segments/" }, { "command": "auxon.timelines.inspect", @@ -324,27 +340,27 @@ "view/title": [ { "command": "auxon.workspaces.refresh", - "when": "view == auxon.workspaces", + "when": "view =~ /auxon\\.[a-zA-Z]*_workspaces/", "group": "navigation@1" }, { "command": "auxon.segments.refresh", - "when": "view == auxon.segments", + "when": "view =~ /auxon\\.[a-zA-Z]*_segments/", "group": "navigation@1" }, { "command": "auxon.segments.setLatestActive", - "when": "view == auxon.segments", + "when": "view =~ /auxon\\.[a-zA-Z]*_segments/", "group": "7_modification@1" }, { "command": "auxon.segments.setAllActive", - "when": "view == auxon.segments", + "when": "view =~ /auxon\\.[a-zA-Z]*_segments/", "group": "7_modification@2" }, { "command": "auxon.segments.setWholeWorkspaceActive", - "when": "view == auxon.segments", + "when": "view =~ /auxon\\.[a-zA-Z]*_segments/", "group": "7_modification@3" }, { @@ -697,7 +713,7 @@ }, { "command": "auxon.deviant.clearMutation", - "title": "Clear mutations using the deviant CLI" + "title": "Clear Mutation" }, { "command": "auxon.deviant.clearAllMutations", @@ -705,7 +721,7 @@ }, { "command": "auxon.deviant.createMutation", - "title": "Create a mutation using the deviant CLI" + "title": "Create Mutation" }, { "command": "auxon.deviant.runCreateMutationWizard", diff --git a/vscode/src/segments.ts b/vscode/src/segments.ts index 0ee8d30..8842089 100644 --- a/vscode/src/segments.ts +++ b/vscode/src/segments.ts @@ -16,7 +16,10 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider; + modalityView: vscode.TreeView; + conformView: vscode.TreeView; + deviantView: vscode.TreeView; + activeView: vscode.TreeView; private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); @@ -29,13 +32,46 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider { + if (ev.visible) { + this.activeView = this.modalityView; + } + }); + + this.conformView = vscode.window.createTreeView("auxon.conform_segments", { + treeDataProvider: this, + canSelectMany: true, + }); + const conformViewListener = this.conformView.onDidChangeVisibility(async (ev) => { + if (ev.visible) { + this.activeView = this.conformView; + } + }); + + this.deviantView = vscode.window.createTreeView("auxon.deviant_segments", { + treeDataProvider: this, + canSelectMany: true, + }); + const deviantViewListener = this.deviantView.onDidChangeVisibility(async (ev) => { + if (ev.visible) { + this.activeView = this.deviantView; + } + }); + + // Default to the modality view + this.activeView = this.modalityView; context.subscriptions.push( - this.view, + this.modalityView, + modalityViewListener, + this.conformView, + conformViewListener, + this.deviantView, + deviantViewListener, vscode.commands.registerCommand("auxon.segments.refresh", () => this.refresh()), vscode.commands.registerCommand("auxon.segments.setActive", (itemData) => this.setActiveCommand(itemData)), vscode.commands.registerCommand("auxon.segments.setActiveFromSelection", () => @@ -122,11 +158,11 @@ export class SegmentsTreeDataProvider implements vscode.TreeDataProvider this.refresh()), From 01202298c8b510de6e4b07f04e812e11095fdd14 Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Wed, 17 Jan 2024 06:28:51 -0800 Subject: [PATCH 09/12] Refresh mutations view after creating mutation --- vscode/src/deviantCommands.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vscode/src/deviantCommands.ts b/vscode/src/deviantCommands.ts index 08ee464..b51d61a 100644 --- a/vscode/src/deviantCommands.ts +++ b/vscode/src/deviantCommands.ts @@ -34,10 +34,12 @@ async function runDeviantMutationClearCommand(args: MutationClearCommandArgs) { try { const res = await execFile(deviantPath, commandArgs, { encoding: "utf8" }); - await vscode.window.showInformationMessage(res.stdout); + const _dont_wait = vscode.window.showInformationMessage(res.stdout); } catch (e) { vscode.window.showErrorMessage(e.stderr.trim()); } + + vscode.commands.executeCommand("auxon.mutations.refresh"); } export type MutationCreateCommandArgs = { @@ -68,10 +70,12 @@ async function runDeviantMutationCreateCommand(args: MutationCreateCommandArgs) try { const res = await execFile(deviantPath, commandArgs, { encoding: "utf8" }); const output = JSON.parse(res.stdout) as string; - await vscode.window.showInformationMessage(`Created mutation '${output["mutation_id"]}'`); + const _dont_wait = vscode.window.showInformationMessage(`Created mutation '${output["mutation_id"]}'`); } catch (e) { vscode.window.showErrorMessage(e.stderr.trim()); } + + vscode.commands.executeCommand("auxon.mutations.refresh"); } // TODO - add linked experiment option From b0763d2d77232bbb98816c95d4dbfa5c3b95b50a Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Wed, 17 Jan 2024 06:30:19 -0800 Subject: [PATCH 10/12] Enable mutator group-by-name by default --- vscode/src/mutators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode/src/mutators.ts b/vscode/src/mutators.ts index 476aadb..0fb1b3f 100644 --- a/vscode/src/mutators.ts +++ b/vscode/src/mutators.ts @@ -13,7 +13,7 @@ class MutatorsTreeMemento { } getGroupByMutatorName(): boolean { - return this.memento.get("mutatorsTree_groupByMutatorName", false); + return this.memento.get("mutatorsTree_groupByMutatorName", true); } async setGroupByMutatorName(val: boolean): Promise { From 017d5c3dfdb90791d0a1f2b7154a327cf679ec9a Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Thu, 18 Jan 2024 10:54:53 -0800 Subject: [PATCH 11/12] Only show mutations observed for the active segment --- vscode/generated/src/modality-api.ts | 115 ++++++++++- vscode/src/main.ts | 2 +- vscode/src/modality-api.json | 293 ++++++++++++++++++++++++++- vscode/src/modalityApi.ts | 18 ++ vscode/src/mutations.ts | 67 ++++-- 5 files changed, 469 insertions(+), 26 deletions(-) diff --git a/vscode/generated/src/modality-api.ts b/vscode/generated/src/modality-api.ts index 4aadab5..7425f34 100644 --- a/vscode/generated/src/modality-api.ts +++ b/vscode/generated/src/modality-api.ts @@ -19,11 +19,18 @@ export interface paths { }; "/v2/mutations": { /** - * List mutations - * @description List mutations + * List all mutations + * @description List all mutations */ get: operations["list_mutations"]; }; + "/v2/mutations/{workspace_version_id}/segments/{rule_name}/{segment_name}": { + /** + * List all mutations for the given segment + * @description List all mutations for the given segment + */ + get: operations["list_segment_mutations"]; + }; "/v2/mutators": { /** * List mutators @@ -334,7 +341,9 @@ export interface components { Some: components["schemas"]["AttrVal"]; }]>; Mutation: { - experiment_name?: string | null; + /** Format: int64 */ + created_at_utc_seconds: number; + linked_experiment?: string | null; mutation_id: components["schemas"]["MutationId"]; mutator_attributes: { [key: string]: components["schemas"]["AttrVal"] | undefined; @@ -343,11 +352,36 @@ export interface components { params: { [key: string]: components["schemas"]["AttrVal"] | undefined; }; + region_details_summary?: components["schemas"]["MutationRegionDetailsSummary"] | null; }; /** Format: uuid */ MutationId: string; + MutationRegionDetails: { + /** + * @description Maps to the "modality.mutation.clear_communicated" event name, + * and the `modality.mutation.success` attribute on such an event + */ + clear_communicated_and_success?: ((components["schemas"]["EventCoordinate"] & (boolean | null))[]) | null; + /** + * @description Maps to the "modality.mutation.command_communicated" event name, + * and the `modality.mutation.success` attribute on such an event + */ + command_communicated_and_success?: ((components["schemas"]["EventCoordinate"] & (boolean | null))[]) | null; + /** + * @description Maps to the "modality.mutation.injected" event name, + * and the `modality.mutation.success` attribute on such an event + */ + inject_attempted_and_success?: ((components["schemas"]["EventCoordinate"] & (boolean | null))[]) | null; + }; + MutationRegionDetailsSummary: { + overall: components["schemas"]["MutationRegionDetails"]; + regions: ((components["schemas"]["RegionKind"] & components["schemas"]["MutationRegionDetails"])[])[]; + }; /** @description Mutations operation errors */ MutationsError: OneOf<["InvalidMutatorId", { + /** @description Workspace not found */ + WorkspaceNotFound: string; + }, "SegmentNotFound", { /** @description Internal Server Error */ Internal: string; }]>; @@ -371,10 +405,20 @@ export interface components { Internal: string; }]>; Nanoseconds: number; + RegionKind: OneOf<[{ + WholeWorkspace: components["schemas"]["WholeWorkspaceRegionKind"]; + }, { + Segment: components["schemas"]["SegmentRegionKind"]; + }]>; SegmentCoverage: { coverage_aggregates: components["schemas"]["CoverageAggregates"]; spec_coverages: (components["schemas"]["SpecCoverage"])[]; }; + SegmentRegionKind: { + id: components["schemas"]["WorkspaceSegmentId"]; + timeline_filter?: components["schemas"]["UnstructuredTimelineFilter"] | null; + workspace_name: components["schemas"]["WorkspaceName"]; + }; SegmentationRuleName: string; SpecContent: { metadata: components["schemas"]["SpecVersionMetadata"]; @@ -476,10 +520,21 @@ export interface components { /** @description Internal Server Error */ Internal: string; }]>; + /** + * @description Stringy representation of an unparsed, unstructured DSL for expressing how to filter timelines, + * likely through attribute evaluation. + */ + UnstructuredTimelineFilter: string; + WholeWorkspaceRegionKind: { + timeline_filter?: components["schemas"]["UnstructuredTimelineFilter"] | null; + workspace_name: components["schemas"]["WorkspaceName"]; + workspace_version_id: components["schemas"]["WorkspaceVersionId"]; + }; Workspace: { name: string; version_id: components["schemas"]["WorkspaceVersionId"]; }; + WorkspaceName: string; /** @description A specific segment of a workspace. */ WorkspaceSegmentId: { rule_name: components["schemas"]["SegmentationRuleName"]; @@ -549,8 +604,8 @@ export interface operations { }; }; /** - * List mutations - * @description List mutations + * List all mutations + * @description List all mutations */ list_mutations: { parameters: { @@ -584,6 +639,56 @@ export interface operations { }; }; }; + /** + * List all mutations for the given segment + * @description List all mutations for the given segment + */ + list_segment_mutations: { + parameters: { + query: { + /** @description Mutator ID */ + mutator_id?: string | null; + /** @description Experiment name */ + experiment?: string | null; + }; + path: { + /** @description Workspace Version Id */ + workspace_version_id: components["schemas"]["WorkspaceVersionId"]; + /** @description Segmentation Rule Name */ + rule_name: components["schemas"]["SegmentationRuleName"]; + /** @description Segment Name */ + segment_name: components["schemas"]["WorkspaceSegmentName"]; + }; + }; + responses: { + /** @description List mutations successfully */ + 200: { + content: { + "application/json": (components["schemas"]["Mutation"])[]; + }; + }; + /** @description Invalid workspace_version_id */ + 400: { + content: { + "application/json": components["schemas"]["MutationsError"]; + }; + }; + /** @description Operation not authorized */ + 403: never; + /** @description Workspace or segment not found */ + 404: { + content: { + "application/json": components["schemas"]["MutationsError"]; + }; + }; + /** @description Internal Server Error */ + 500: { + content: { + "application/json": components["schemas"]["MutationsError"]; + }; + }; + }; + }; /** * List mutators * @description List mutators diff --git a/vscode/src/main.ts b/vscode/src/main.ts index 2747512..7b00d8f 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -92,7 +92,7 @@ export async function activate(context: vscode.ExtensionContext) { eventsTreeDataProvider.refresh(); mutatorsTreeDataProvider.refresh(); - mutationsTreeDataProvider.refresh(); + mutationsTreeDataProvider.setActiveSegmentIds(ev.activeSegmentIds); }); workspacesTreeDataProvider.register(context); diff --git a/vscode/src/modality-api.json b/vscode/src/modality-api.json index cbdb63c..274f40d 100644 --- a/vscode/src/modality-api.json +++ b/vscode/src/modality-api.json @@ -76,8 +76,8 @@ "/v2/mutations": { "get": { "tags": ["mutations"], - "summary": "List mutations", - "description": "List mutations", + "summary": "List all mutations", + "description": "List all mutations", "operationId": "list_mutations", "parameters": [ { @@ -148,6 +148,118 @@ ] } }, + "/v2/mutations/{workspace_version_id}/segments/{rule_name}/{segment_name}": { + "get": { + "tags": ["mutations"], + "summary": "List all mutations for the given segment", + "description": "List all mutations for the given segment", + "operationId": "list_segment_mutations", + "parameters": [ + { + "name": "workspace_version_id", + "in": "path", + "description": "Workspace Version Id", + "required": true, + "schema": { + "$ref": "#/components/schemas/WorkspaceVersionId" + } + }, + { + "name": "rule_name", + "in": "path", + "description": "Segmentation Rule Name", + "required": true, + "schema": { + "$ref": "#/components/schemas/SegmentationRuleName" + } + }, + { + "name": "segment_name", + "in": "path", + "description": "Segment Name", + "required": true, + "schema": { + "$ref": "#/components/schemas/WorkspaceSegmentName" + } + }, + { + "name": "mutator_id", + "in": "query", + "description": "Mutator ID", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "explode": true + }, + { + "name": "experiment", + "in": "query", + "description": "Experiment name", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "explode": true + } + ], + "responses": { + "200": { + "description": "List mutations successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Mutation" + } + } + } + } + }, + "400": { + "description": "Invalid workspace_version_id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationsError" + } + } + } + }, + "403": { + "description": "Operation not authorized" + }, + "404": { + "description": "Workspace or segment not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationsError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationsError" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": ["access"] + } + ] + } + }, "/v2/mutators": { "get": { "tags": ["mutators"], @@ -2360,9 +2472,13 @@ }, "Mutation": { "type": "object", - "required": ["mutation_id", "mutator_id", "params", "mutator_attributes"], + "required": ["mutation_id", "mutator_id", "params", "mutator_attributes", "created_at_utc_seconds"], "properties": { - "experiment_name": { + "created_at_utc_seconds": { + "type": "integer", + "format": "int64" + }, + "linked_experiment": { "type": "string", "nullable": true }, @@ -2383,6 +2499,14 @@ "additionalProperties": { "$ref": "#/components/schemas/AttrVal" } + }, + "region_details_summary": { + "allOf": [ + { + "$ref": "#/components/schemas/MutationRegionDetailsSummary" + } + ], + "nullable": true } } }, @@ -2390,12 +2514,104 @@ "type": "string", "format": "uuid" }, + "MutationRegionDetails": { + "type": "object", + "properties": { + "clear_communicated_and_success": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/EventCoordinate" + }, + { + "type": "boolean", + "nullable": true + } + ] + }, + "description": "Maps to the \"modality.mutation.clear_communicated\" event name,\nand the `modality.mutation.success` attribute on such an event", + "nullable": true + }, + "command_communicated_and_success": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/EventCoordinate" + }, + { + "type": "boolean", + "nullable": true + } + ] + }, + "description": "Maps to the \"modality.mutation.command_communicated\" event name,\nand the `modality.mutation.success` attribute on such an event", + "nullable": true + }, + "inject_attempted_and_success": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/EventCoordinate" + }, + { + "type": "boolean", + "nullable": true + } + ] + }, + "description": "Maps to the \"modality.mutation.injected\" event name,\nand the `modality.mutation.success` attribute on such an event", + "nullable": true + } + } + }, + "MutationRegionDetailsSummary": { + "type": "object", + "required": ["overall", "regions"], + "properties": { + "overall": { + "$ref": "#/components/schemas/MutationRegionDetails" + }, + "regions": { + "type": "array", + "items": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/RegionKind" + }, + { + "$ref": "#/components/schemas/MutationRegionDetails" + } + ] + } + } + } + } + }, "MutationsError": { "oneOf": [ { "type": "string", "enum": ["InvalidMutatorId"] }, + { + "type": "object", + "required": ["WorkspaceNotFound"], + "properties": { + "WorkspaceNotFound": { + "type": "string", + "description": "Workspace not found" + } + } + }, + { + "type": "string", + "enum": ["SegmentNotFound"] + }, { "type": "object", "required": ["Internal"], @@ -2463,6 +2679,28 @@ "Nanoseconds": { "type": "integer" }, + "RegionKind": { + "oneOf": [ + { + "type": "object", + "required": ["WholeWorkspace"], + "properties": { + "WholeWorkspace": { + "$ref": "#/components/schemas/WholeWorkspaceRegionKind" + } + } + }, + { + "type": "object", + "required": ["Segment"], + "properties": { + "Segment": { + "$ref": "#/components/schemas/SegmentRegionKind" + } + } + } + ] + }, "SegmentCoverage": { "type": "object", "required": ["spec_coverages", "coverage_aggregates"], @@ -2478,6 +2716,26 @@ } } }, + "SegmentRegionKind": { + "type": "object", + "required": ["workspace_name", "id"], + "properties": { + "id": { + "$ref": "#/components/schemas/WorkspaceSegmentId" + }, + "timeline_filter": { + "allOf": [ + { + "$ref": "#/components/schemas/UnstructuredTimelineFilter" + } + ], + "nullable": true + }, + "workspace_name": { + "$ref": "#/components/schemas/WorkspaceName" + } + } + }, "SegmentationRuleName": { "type": "string" }, @@ -2804,6 +3062,30 @@ ], "description": "Timelines operation errors" }, + "UnstructuredTimelineFilter": { + "type": "string", + "description": "Stringy representation of an unparsed, unstructured DSL for expressing how to filter timelines,\nlikely through attribute evaluation." + }, + "WholeWorkspaceRegionKind": { + "type": "object", + "required": ["workspace_name", "workspace_version_id"], + "properties": { + "timeline_filter": { + "allOf": [ + { + "$ref": "#/components/schemas/UnstructuredTimelineFilter" + } + ], + "nullable": true + }, + "workspace_name": { + "$ref": "#/components/schemas/WorkspaceName" + }, + "workspace_version_id": { + "$ref": "#/components/schemas/WorkspaceVersionId" + } + } + }, "Workspace": { "type": "object", "required": ["name", "version_id"], @@ -2816,6 +3098,9 @@ } } }, + "WorkspaceName": { + "type": "string" + }, "WorkspaceSegmentId": { "type": "object", "description": "A specific segment of a workspace.", diff --git a/vscode/src/modalityApi.ts b/vscode/src/modalityApi.ts index cc2eae0..1c98df2 100644 --- a/vscode/src/modalityApi.ts +++ b/vscode/src/modalityApi.ts @@ -312,6 +312,24 @@ export class SegmentClient { return unwrapData(res); } + + async mutations(mutatorId?: MutatorId): Promise { + const q = []; + if (typeof mutatorId !== "undefined") { + q.push(["mutator_id", mutatorId]); + } + const res = await this.client.get( + "/v2/mutations/{workspace_version_id}/segments/{rule_name}/{segment_name}", + { + params: { + path: this.segmentId, + // @ts-ignore + query: q, + }, + } + ); + return unwrapData(res); + } } export class TimelinesClient { diff --git a/vscode/src/mutations.ts b/vscode/src/mutations.ts index 1e9b09f..6a6a911 100644 --- a/vscode/src/mutations.ts +++ b/vscode/src/mutations.ts @@ -4,8 +4,6 @@ import * as api from "./modalityApi"; class MutationsTreeMemento { constructor(private readonly memento: vscode.Memento) {} - // TODO filters/selections - // - all-of-history vs selected-segment getGroupByMutatorName(): boolean { return this.memento.get("mutationsTree_groupByMutatorName", false); } @@ -32,8 +30,8 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider; - - selectedMutatorId?: api.MutatorId; + selectedMutatorId?: api.MutatorId = undefined; + activeSegmentId?: api.WorkspaceSegmentId = undefined; constructor(private readonly apiClient: api.Client) {} @@ -93,31 +91,42 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider { + if (!this.activeSegmentId) { + return []; + } + if (this.workspaceState.getFilterBySelectedMutator() && this.selectedMutatorId == null) { // Need a selected mutator to populate with return []; } else if (!element) { let mutations = []; + if (this.workspaceState.getFilterBySelectedMutator()) { - mutations = await this.apiClient.mutator(this.selectedMutatorId).mutations(); + mutations = await this.apiClient.segment(this.activeSegmentId).mutations(this.selectedMutatorId) } else { - mutations = await this.apiClient.mutations().list(); + mutations = await this.apiClient.segment(this.activeSegmentId).mutations() } - let items = []; + this.data = []; if (this.workspaceState.getGroupByMutatorName()) { const root = new MutationsGroupByNameTreeItemData("", []); for (const m of mutations) { root.insertNode(new Mutation(m)); } root.updateDescriptions(); - items = root.children(); + root.sortMutationsByCreatedAt(); + const { compare } = Intl.Collator("en-US"); + this.data = root.children().sort((a, b) => compare(a.name, b.name)); } else { - items = mutations.map((m) => new NamedMutationTreeItemData(new Mutation(m))); + this.data = mutations.map((m) => new NamedMutationTreeItemData(new Mutation(m))); + this.data.sort((a, b) => { + if (!(a instanceof NamedMutationTreeItemData) || !(b instanceof NamedMutationTreeItemData)) { + throw new Error("Internal error: mutations tree node not of expected type"); + } + // Most recent first + return b.mutation.createdAt.getTime() - a.mutation.createdAt.getTime(); + }); } - // TODO - sort by created-at when it's added - const { compare } = Intl.Collator("en-US"); - this.data = items.sort((a, b) => compare(a.mutatorName, b.mutatorName)); return this.data; } else { return element.children(); @@ -138,6 +147,15 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider 0) { + group.childItems.sort((a, b) => { + if (!(a instanceof NamedMutationTreeItemData) || !(b instanceof NamedMutationTreeItemData)) { + throw new Error("Internal error: mutations tree node not of expected type"); + } + // Most recent first + return b.mutation.createdAt.getTime() - a.mutation.createdAt.getTime(); + }); + } + } + } } export class NamedMutationTreeItemData extends MutationsTreeItemData { @@ -313,12 +345,12 @@ export class NamedMutationTreeItemData extends MutationsTreeItemData { } override canHaveChildren(): boolean { - // TODO - always true once created-at/etc is added - return this.mutation.experimentName != null || this.mutation.params.size != 0; + return true; } override children(): MutationsTreeItemData[] { const children = []; + children.push(new MutationDetailLeafTreeItemData(`Created At: ${this.mutation.createdAt}`)); if (this.mutation.experimentName) { children.push(new MutationDetailLeafTreeItemData(`Experiment: ${this.mutation.experimentName}`)); } @@ -363,14 +395,17 @@ class Mutation { mutatorName = ""; mutatorDescription?: string = undefined; mutationId: api.MutationId; + createdAt: Date; experimentName?: string = undefined; params: Map; constructor(private mutation: api.Mutation) { this.mutatorId = mutation.mutator_id; this.mutationId = mutation.mutation_id; - if (mutation.experiment_name) { - this.experimentName = mutation.experiment_name; + this.createdAt = new Date(0); + this.createdAt.setUTCSeconds(mutation.created_at_utc_seconds); + if (mutation.linked_experiment) { + this.experimentName = mutation.linked_experiment; } if (Object.prototype.hasOwnProperty.call(mutation.mutator_attributes, "mutator.name")) { this.mutatorName = mutation.mutator_attributes["mutator.name"] as string; From 11641426775f577e912c7968f83774e9dc5437b2 Mon Sep 17 00:00:00 2001 From: Jon Lamb Date: Thu, 18 Jan 2024 12:04:49 -0800 Subject: [PATCH 12/12] Add mutation region detail coordinate information and filtering --- vscode/generated/src/modality-api.ts | 2 +- vscode/package.json | 28 +++++ vscode/src/modality-api.json | 2 +- vscode/src/modalityApi.ts | 19 ++-- vscode/src/mutations.ts | 155 ++++++++++++++++++++++++--- 5 files changed, 180 insertions(+), 26 deletions(-) diff --git a/vscode/generated/src/modality-api.ts b/vscode/generated/src/modality-api.ts index 7425f34..44b688c 100644 --- a/vscode/generated/src/modality-api.ts +++ b/vscode/generated/src/modality-api.ts @@ -276,7 +276,7 @@ export interface components { percentage_specs_passing: number; }; EventCoordinate: { - opaque_event_id?: (number)[]; + id?: (number)[]; timeline_id?: components["schemas"]["TimelineId"]; }; EventSummary: { diff --git a/vscode/package.json b/vscode/package.json index c13a0ce..b4971aa 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -335,6 +335,11 @@ "command": "auxon.mutations.clearMutation", "when": "view == auxon.mutations && viewItem == mutation", "group": "inline" + }, + { + "command": "auxon.mutations.viewLogFromMutation", + "when": "view == auxon.mutations && viewItem == mutationCoordinate", + "group": "inline" } ], "view/title": [ @@ -472,6 +477,16 @@ "command": "auxon.mutations.disableMutationFiltering", "when": "view == auxon.mutations && auxon.mutations.filterBy == 'MUTATOR_ID'", "group": "7_modification@3" + }, + { + "command": "auxon.mutations.showCleared", + "when": "view == auxon.mutations && auxon.mutations.cleared == 'HIDE'", + "group": "7_modification@1" + }, + { + "command": "auxon.mutations.hideCleared", + "when": "view == auxon.mutations && auxon.mutations.cleared == 'SHOW'", + "group": "7_modification@1" } ] }, @@ -786,10 +801,23 @@ "command": "auxon.mutations.disableMutationFiltering", "title": "✓ Filter By Selected Mutator" }, + { + "command": "auxon.mutations.showCleared", + "title": "Show Cleared Mutations" + }, + { + "command": "auxon.mutations.hideCleared", + "title": "✓ Show Cleared Mutations" + }, { "command": "auxon.mutations.clearMutation", "title": "Clear Mutation", "icon": "$(notebook-state-error)" + }, + { + "command": "auxon.mutations.viewLogFromMutation", + "title": "View Log from this Coordinate", + "icon": "$(open-preview)" } ], "configuration": { diff --git a/vscode/src/modality-api.json b/vscode/src/modality-api.json index 274f40d..c31bad1 100644 --- a/vscode/src/modality-api.json +++ b/vscode/src/modality-api.json @@ -2283,7 +2283,7 @@ "EventCoordinate": { "type": "object", "properties": { - "opaque_event_id": { + "id": { "type": "array", "items": { "type": "integer", diff --git a/vscode/src/modalityApi.ts b/vscode/src/modalityApi.ts index 1c98df2..c8ab27a 100644 --- a/vscode/src/modalityApi.ts +++ b/vscode/src/modalityApi.ts @@ -24,6 +24,7 @@ export type CaseCoverage = gen.components["schemas"]["CaseCoverage"]; export type CoverageAggregates = gen.components["schemas"]["CoverageAggregates"]; export type LogicalTime = gen.components["schemas"]["LogicalTime"]; export type Nanoseconds = gen.components["schemas"]["Nanoseconds"]; +export type EventCoordinate = gen.components["schemas"]["EventCoordinate"]; export type SegmentationRuleName = gen.components["schemas"]["SegmentationRuleName"]; export type SpecContent = gen.components["schemas"]["SpecContent"]; export type SpecStructure = gen.components["schemas"]["SpecStructure"]; @@ -50,6 +51,7 @@ export type MutatorState = gen.components["schemas"]["MutatorState"]; export type Mutator = gen.components["schemas"]["Mutator"]; export type MutationId = gen.components["schemas"]["MutatorId"]; export type Mutation = gen.components["schemas"]["Mutation"]; +export type MutationRegionDetails = gen.components["schemas"]["MutationRegionDetails"]; type InternalClient = ReturnType>; @@ -318,16 +320,13 @@ export class SegmentClient { if (typeof mutatorId !== "undefined") { q.push(["mutator_id", mutatorId]); } - const res = await this.client.get( - "/v2/mutations/{workspace_version_id}/segments/{rule_name}/{segment_name}", - { - params: { - path: this.segmentId, - // @ts-ignore - query: q, - }, - } - ); + const res = await this.client.get("/v2/mutations/{workspace_version_id}/segments/{rule_name}/{segment_name}", { + params: { + path: this.segmentId, + // @ts-ignore + query: q, + }, + }); return unwrapData(res); } } diff --git a/vscode/src/mutations.ts b/vscode/src/mutations.ts index 6a6a911..d580bf0 100644 --- a/vscode/src/mutations.ts +++ b/vscode/src/mutations.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; import * as api from "./modalityApi"; +import * as modalityLog from "./modalityLog"; class MutationsTreeMemento { constructor(private readonly memento: vscode.Memento) {} @@ -19,6 +20,14 @@ class MutationsTreeMemento { async setFilterBySelectedMutator(val: boolean): Promise { return this.memento.update("mutationsTree_filterBySelectedMutator", val); } + + getShowClearedMutations(): boolean { + return this.memento.get("mutationsTree_showClearedMutations", false); + } + + async setShowClearedMutations(val: boolean): Promise { + return this.memento.update("mutationsTree_showClearedMutations", val); + } } export class MutationsTreeDataProvider implements vscode.TreeDataProvider { @@ -63,9 +72,14 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider { this.filterBySelectedMutator(); }), + vscode.commands.registerCommand("auxon.mutations.showCleared", () => this.showClearedMutations(true)), + vscode.commands.registerCommand("auxon.mutations.hideCleared", () => this.showClearedMutations(false)), vscode.commands.registerCommand("auxon.mutations.clearMutation", (itemData) => { this.clearMutation(itemData); - }) + }), + vscode.commands.registerCommand("auxon.mutations.viewLogFromMutation", (itemData) => + this.viewLogFromMutation(itemData) + ) ); this.refresh(); @@ -82,6 +96,11 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider new Mutation(m)); + if (!this.workspaceState.getShowClearedMutations()) { + mutations = mutations.filter((m) => !m.wasCleared()); } this.data = []; if (this.workspaceState.getGroupByMutatorName()) { const root = new MutationsGroupByNameTreeItemData("", []); for (const m of mutations) { - root.insertNode(new Mutation(m)); + root.insertNode(m); } root.updateDescriptions(); root.sortMutationsByCreatedAt(); const { compare } = Intl.Collator("en-US"); - this.data = root.children().sort((a, b) => compare(a.name, b.name)); + this.data = await root.children(this.apiClient); + this.data.sort((a, b) => compare(a.name, b.name)); } else { - this.data = mutations.map((m) => new NamedMutationTreeItemData(new Mutation(m))); + this.data = mutations.map((m) => new NamedMutationTreeItemData(m)); this.data.sort((a, b) => { if (!(a instanceof NamedMutationTreeItemData) || !(b instanceof NamedMutationTreeItemData)) { throw new Error("Internal error: mutations tree node not of expected type"); @@ -129,7 +154,7 @@ export class MutationsTreeDataProvider implements vscode.TreeDataProvider { return []; } } @@ -276,7 +326,7 @@ export class MutationsGroupByNameTreeItemData extends MutationsTreeItemData { return true; } - override children(): MutationsTreeItemData[] { + override async children(_apiClient: api.Client): Promise { return this.childItems; } @@ -348,12 +398,46 @@ export class NamedMutationTreeItemData extends MutationsTreeItemData { return true; } - override children(): MutationsTreeItemData[] { + override async children(apiClient: api.Client): Promise { const children = []; children.push(new MutationDetailLeafTreeItemData(`Created At: ${this.mutation.createdAt}`)); if (this.mutation.experimentName) { children.push(new MutationDetailLeafTreeItemData(`Experiment: ${this.mutation.experimentName}`)); } + + if (this.mutation.regionDetailsSummary && this.mutation.regionDetailsSummary.command_communicated_and_success) { + const coord = this.mutation.regionDetailsSummary.command_communicated_and_success[0]; + const maybeSuccess = this.mutation.regionDetailsSummary.command_communicated_and_success[1]; + const timeline = await apiClient.timeline(coord.timeline_id).get(); + let timelineStr = `${coord.timeline_id}`; + if (Object.prototype.hasOwnProperty.call(timeline.attributes, "timeline.name")) { + timelineStr = timeline.attributes["timeline.name"] as string; + } + children.push( + new MutationCoordinateTreeItemData(`Communicated Timeline: ${timelineStr}`, coord, maybeSuccess) + ); + } + if (this.mutation.regionDetailsSummary && this.mutation.regionDetailsSummary.inject_attempted_and_success) { + const coord = this.mutation.regionDetailsSummary.inject_attempted_and_success[0]; + const maybeSuccess = this.mutation.regionDetailsSummary.inject_attempted_and_success[1]; + const timeline = await apiClient.timeline(coord.timeline_id).get(); + let timelineStr = `${coord.timeline_id}`; + if (Object.prototype.hasOwnProperty.call(timeline.attributes, "timeline.name")) { + timelineStr = timeline.attributes["timeline.name"] as string; + } + children.push(new MutationCoordinateTreeItemData(`Injected Timeline: ${timelineStr}`, coord, maybeSuccess)); + } + if (this.mutation.regionDetailsSummary && this.mutation.regionDetailsSummary.clear_communicated_and_success) { + const coord = this.mutation.regionDetailsSummary.clear_communicated_and_success[0]; + const maybeSuccess = this.mutation.regionDetailsSummary.clear_communicated_and_success[1]; + const timeline = await apiClient.timeline(coord.timeline_id).get(); + let timelineStr = `${coord.timeline_id}`; + if (Object.prototype.hasOwnProperty.call(timeline.attributes, "timeline.name")) { + timelineStr = timeline.attributes["timeline.name"] as string; + } + children.push(new MutationCoordinateTreeItemData(`Cleared Timeline: ${timelineStr}`, coord, maybeSuccess)); + } + if (this.mutation.params.size != 0) { children.push(new MutationParametersTreeItemData(this.mutation)); } @@ -365,13 +449,14 @@ export class MutationParametersTreeItemData extends MutationsTreeItemData { contextValue = "mutationParameters"; constructor(public mutation: Mutation) { super("Parameters"); + super.iconPath = new vscode.ThemeIcon("output"); } override canHaveChildren(): boolean { return true; } - override children(): MutationsTreeItemData[] { + override async children(_apiClient: api.Client): Promise { const children = []; for (const [paramName, paramValue] of this.mutation.params) { children.push(new MutationDetailLeafTreeItemData(`${paramName}: ${paramValue}`)); @@ -380,6 +465,38 @@ export class MutationParametersTreeItemData extends MutationsTreeItemData { } } +export class MutationCoordinateTreeItemData extends MutationsTreeItemData { + contextValue = "mutationCoordinate"; + constructor(public name: string, public coordinate: api.EventCoordinate, public maybeSuccess?: boolean) { + super(name); + this.iconPath = new vscode.ThemeIcon("git-commit"); + } + + override canHaveChildren(): boolean { + return true; + } + + override async children(_apiClient: api.Client): Promise { + const children = []; + + let msg = "NA"; + let icon = new vscode.ThemeIcon("question", new vscode.ThemeColor("testing.iconQueued")); + if (this.maybeSuccess === true) { + msg = "Yes"; + icon = new vscode.ThemeIcon("verified-filled", new vscode.ThemeColor("testing.iconPassed")); + } else if (this.maybeSuccess === false) { + msg = "No"; + icon = new vscode.ThemeIcon("testing-failed-icon", new vscode.ThemeColor("testing.iconFailed")); + } + + const item = new MutationDetailLeafTreeItemData(`Was Successful: ${msg}`); + item.iconPath = icon; + children.push(item); + + return children; + } +} + export class MutationDetailLeafTreeItemData extends MutationsTreeItemData { contextValue = "mutationDetail"; constructor(public name: string) { @@ -387,9 +504,6 @@ export class MutationDetailLeafTreeItemData extends MutationsTreeItemData { } } -// TODO -// - add created-at datetime, sort the list -// - add checklist coordinate fields class Mutation { mutatorId: api.MutatorId; mutatorName = ""; @@ -398,6 +512,9 @@ class Mutation { createdAt: Date; experimentName?: string = undefined; params: Map; + // N.B. currently just surfacing the overall summary, could consider + // the per-region summary if we expand beyond single segments + regionDetailsSummary?: api.MutationRegionDetails; constructor(private mutation: api.Mutation) { this.mutatorId = mutation.mutator_id; @@ -417,5 +534,15 @@ class Mutation { for (const [paramName, paramValue] of Object.entries(mutation.params)) { this.params.set(paramName, paramValue); } + if (mutation.region_details_summary) { + this.regionDetailsSummary = mutation.region_details_summary.overall; + } + } + + wasCleared(): boolean { + if (this.regionDetailsSummary && this.regionDetailsSummary.clear_communicated_and_success) { + return true; + } + return false; } }