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;