From 7d600c5fce74cfcc156091fbd6a737991936a3d3 Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Thu, 12 Jun 2025 20:42:45 +0100 Subject: [PATCH 01/11] Introduce schema tree view on Query page --- src/framework/typedb-driver/response.ts | 6 +- src/module/query/query-tool.component.html | 96 ++++++++++--- src/module/query/query-tool.component.ts | 11 +- src/service/driver-state.service.ts | 4 +- src/service/query-tool-state.service.ts | 91 +++++++++++- src/service/schema-state.service.ts | 153 ++++++++++++++++++++- styles/material.scss | 11 ++ 7 files changed, 343 insertions(+), 29 deletions(-) diff --git a/src/framework/typedb-driver/response.ts b/src/framework/typedb-driver/response.ts index 812cdcb9..a287e78f 100644 --- a/src/framework/typedb-driver/response.ts +++ b/src/framework/typedb-driver/response.ts @@ -69,6 +69,8 @@ export interface ConceptDocumentsQueryResponse extends QueryResponseBase { export type QueryResponse = OkQueryResponse | ConceptRowsQueryResponse | ConceptDocumentsQueryResponse; +export type ApiOkResponse = { ok: OK_RES }; + export type ApiError = { code: string; message: string }; export interface ApiErrorResponse { @@ -80,9 +82,9 @@ export function isApiError(err: any): err is ApiError { return typeof err.code === "string" && typeof err.message === "string"; } -export type ApiResponse = { ok: OK_RES } | ApiErrorResponse; +export type ApiResponse = ApiOkResponse | ApiErrorResponse; -export function isOkResponse(res: ApiResponse): res is { ok: OK_RES } { +export function isOkResponse(res: ApiResponse): res is ApiOkResponse { return "ok" in res; } diff --git a/src/module/query/query-tool.component.html b/src/module/query/query-tool.component.html index 213f4817..ea42d7f5 100644 --- a/src/module/query/query-tool.component.html +++ b/src/module/query/query-tool.component.html @@ -1,11 +1,29 @@
-
+
- -

History

+ +

Schema

-
+
+ + + + + + {{node.name}} + + + + + {{node.name}} + +
    @for (entry of state.history.entries; track entry) {
  1. @@ -13,7 +31,7 @@

    History

    @if (isTransactionOperation(entry)) { -

    {{ transactionOperationString(entry) }}

    +

    {{ transactionOperationString(entry) }}

    } @else {

    ran query

    } @@ -22,31 +40,31 @@

    History

    @if (entry.status === "pending") { } @else { - {{ actionDurationString(entry) }} - @if (entry.status === "success") { - - } @else { - - } + {{ actionDurationString(entry) }} + @if (entry.status === "success") { + + } @else { + + } }
@if (isQueryRun(entry)) { - } - - - + + + }
-
+
@@ -154,5 +172,51 @@

Output

}
+
+
+ +

History

+
+
+
    + @for (entry of state.history.entries; track entry) { +
  1. + + @if (isQueryRun(entry)) { + + + + } + + + +
  2. + } +
+
+
diff --git a/src/module/query/query-tool.component.ts b/src/module/query/query-tool.component.ts index 32cb0f99..62e7285a 100644 --- a/src/module/query/query-tool.component.ts +++ b/src/module/query/query-tool.component.ts @@ -12,9 +12,11 @@ import { MatButtonModule } from "@angular/material/button"; import { MatButtonToggleModule } from "@angular/material/button-toggle"; import { MatDividerModule } from "@angular/material/divider"; import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatIconModule } from "@angular/material/icon"; import { MatInputModule } from "@angular/material/input"; import { MatSortModule } from "@angular/material/sort"; import { MatTableModule } from "@angular/material/table"; +import { MatTreeModule } from "@angular/material/tree"; import { MatTooltipModule } from "@angular/material/tooltip"; import { RouterLink } from "@angular/router"; import { ResizableDirective } from "@hhangular/resizable"; @@ -36,7 +38,7 @@ import { PageScaffoldComponent } from "../scaffold/page/page-scaffold.component" styleUrls: ["query-tool.component.scss"], standalone: true, imports: [ - RouterLink, AsyncPipe, PageScaffoldComponent, MatDividerModule, MatFormFieldModule, + RouterLink, AsyncPipe, PageScaffoldComponent, MatDividerModule, MatFormFieldModule, MatTreeModule, MatIconModule, MatInputModule, FormsModule, ReactiveFormsModule, MatButtonToggleModule, CodeEditor, ResizableDirective, DatePipe, SpinnerComponent, MatTableModule, MatSortModule, MatTooltipModule, MatButtonModule, RichTooltipDirective, ], @@ -50,8 +52,10 @@ export class QueryToolComponent implements OnInit, AfterViewInit, OnDestroy { readonly codeEditorTheme = basicDark; codeEditorHidden = true; - constructor(protected state: QueryToolState, public driver: DriverState, private appData: AppData, private snackbar: SnackbarService) { - } + constructor( + protected state: QueryToolState, public driver: DriverState, + private appData: AppData, private snackbar: SnackbarService + ) {} ngOnInit() { this.appData.viewState.setLastUsedTool("query"); @@ -68,6 +72,7 @@ export class QueryToolComponent implements OnInit, AfterViewInit, OnDestroy { ngAfterViewInit() { const articleWidth = this.articleRef.nativeElement.clientWidth; this.resizables.first.percent = (articleWidth * 0.15 + 100) / articleWidth * 100; + this.resizables.last.percent = (articleWidth * 0.15 + 100) / articleWidth * 100; this.graphViewRef.changes.pipe( map(x => x as QueryList>), startWith(this.graphViewRef), diff --git a/src/service/driver-state.service.ts b/src/service/driver-state.service.ts index a7c75624..fae90dec 100644 --- a/src/service/driver-state.service.ts +++ b/src/service/driver-state.service.ts @@ -13,7 +13,7 @@ import { ConnectionConfig, databasesSortedByName, DEFAULT_DATABASE_NAME } from " import { Transaction } from "../concept/transaction"; import { TypeDBHttpDriver } from "../framework/typedb-driver"; import { Database } from "../framework/typedb-driver/database"; -import { ApiResponse, isApiErrorResponse, isOkResponse, QueryResponse, VersionResponse } from "../framework/typedb-driver/response"; +import { ApiOkResponse, ApiResponse, isApiErrorResponse, isOkResponse, QueryResponse, VersionResponse } from "../framework/typedb-driver/response"; import { requireValue } from "../framework/util/observable"; import { INTERNAL_ERROR } from "../framework/util/strings"; import { AppData } from "./app-data.service"; @@ -309,7 +309,7 @@ export class DriverState { ), lockId); } - runBackgroundReadQueries(queries: string[]): Observable> { + runBackgroundReadQueries(queries: string[]): Observable> { const driver = this.requireDriver(`${this.constructor.name}.${this.query.name} > ${this.requireDriver.name}`); const databaseName = this.requireDatabase(`${this.constructor.name}.${this.openTransaction.name} > ${this.requireDatabase.name}`).name; return fromPromise(driver.openTransaction(databaseName, "read")).pipe( diff --git a/src/service/query-tool-state.service.ts b/src/service/query-tool-state.service.ts index 1c548fe0..288dcac6 100644 --- a/src/service/query-tool-state.service.ts +++ b/src/service/query-tool-state.service.ts @@ -17,7 +17,10 @@ import { Concept, Value } from "../framework/typedb-driver/concept"; import { ApiResponse, ConceptDocument, ConceptRow, isApiErrorResponse, QueryResponse } from "../framework/typedb-driver/response"; import { INTERNAL_ERROR } from "../framework/util/strings"; import { DriverState } from "./driver-state.service"; +import { SchemaState, SchemaTree, SchemaTreeType } from "./schema-state.service"; import { SnackbarService } from "./snackbar.service"; +import { FlatTreeControl } from "@angular/cdk/tree"; +import { MatTreeFlatDataSource, MatTreeFlattener } from "@angular/material/tree"; export type OutputType = "raw" | "log" | "table" | "graph"; @@ -34,11 +37,12 @@ export class QueryToolState { queryControl = new FormControl("", {nonNullable: true}); outputTypeControl = new FormControl("log" as OutputType, { nonNullable: true }); outputTypes: OutputType[] = ["log", "table", "graph", "raw"]; - readonly history = new HistoryWindowState(this.driver); + readonly schemaWindow = new SchemaWindowState(this.schema); readonly logOutput = new LogOutputState(); readonly tableOutput = new TableOutputState(); readonly graphOutput = new GraphOutputState(); readonly rawOutput = new RawOutputState(); + readonly history = new HistoryWindowState(this.driver); answersOutputEnabled = true; readonly runDisabledReason$ = combineLatest( [this.driver.status$, this.driver.database$, this.driver.autoTransactionEnabled$, this.driver.transaction$, this.queryControl.valueChanges.pipe(startWith(this.queryControl.value))] @@ -53,7 +57,7 @@ export class QueryToolState { readonly outputDisabledReason$ = this.driver.status$.pipe(map(x => x === "connected" ? null : NO_SERVER_CONNECTED)); readonly outputDisabled$ = this.outputDisabledReason$.pipe(map(x => x != null)); - constructor(private driver: DriverState, private snackbar: SnackbarService) { + constructor(private driver: DriverState, private schema: SchemaState, private snackbar: SnackbarService) { (window as any)["queryToolState"] = this; this.outputDisabled$.subscribe((disabled) => { if (disabled) this.outputTypeControl.disable(); @@ -131,6 +135,89 @@ export class QueryToolState { } } +interface SchemaTreeNode { + label?: string; + type?: SchemaTreeType; + children: SchemaTreeNode[]; +} + +/** Flat node with expandable and level information */ +interface FlatNode { + expandable: boolean; + name: string; + level: number; +} + +export class SchemaWindowState { + private _transformer = (node: SchemaTreeNode, level: number) => { + return { + expandable: !!node.children.length, + name: node.label || node.type?.label || ``, + level: level, + }; + }; + + treeFlattener = new MatTreeFlattener( + this._transformer, + node => node.level, + node => node.expandable, + node => node.children, + ); + + treeControl = this.createTreeControl(); + entitiesTreeControl = this.createTreeControl(); + relationsTreeControl = this.createTreeControl(); + attributesTreeControl = this.createTreeControl(); + dataSource: MatTreeFlatDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + dataSourcesObj: Record<"Entities" | "Relations" | "Attributes", { title: string, treeControl: FlatTreeControl, dataSource: MatTreeFlatDataSource }> = { + "Entities": { title: "Entities", treeControl: this.entitiesTreeControl, dataSource: new MatTreeFlatDataSource(this.entitiesTreeControl, this.treeFlattener) }, + "Relations": { title: "Relations", treeControl: this.relationsTreeControl, dataSource: new MatTreeFlatDataSource(this.relationsTreeControl, this.treeFlattener) }, + "Attributes": { title: "Attributes", treeControl: this.attributesTreeControl, dataSource: new MatTreeFlatDataSource(this.attributesTreeControl, this.treeFlattener) }, + }; + dataSources = Object.values(this.dataSourcesObj); + + private createTreeControl() { + return new FlatTreeControl( + node => node.level, + node => node.expandable, + ); + } + + constructor(public schemaState: SchemaState) { + schemaState.tree.data$.subscribe(data => { + this.populateDataSources(data); + }); + } + + hasChild = (_: number, node: FlatNode) => node.expandable; + + private populateDataSources(data: SchemaTree | null) { + if (!data) { + this.dataSources.forEach(x => x.dataSource.data = []); + return; + } + this.dataSource.data = [{ + label: "Entities", + children: data.entities.map(x => ({ + type: x, + children: [], + })), + }, { + label: "Relations", + children: data.relations.map(x => ({ + type: x, + children: [], + })), + }, { + label: "Attributes", + children: data.attributes.map(x => ({ + type: x, + children: [], + })), + }]; + } +} + export class HistoryWindowState { readonly entries: DriverAction[] = []; diff --git a/src/service/schema-state.service.ts b/src/service/schema-state.service.ts index 52db2e8a..170f3e93 100644 --- a/src/service/schema-state.service.ts +++ b/src/service/schema-state.service.ts @@ -12,7 +12,8 @@ import { createSigmaRenderer, GraphVisualiser } from "../framework/graph-visuali import { defaultSigmaSettings } from "../framework/graph-visualiser/defaults"; import { newVisualGraph } from "../framework/graph-visualiser/graph"; import { Layouts } from "../framework/graph-visualiser/layouts"; -import { ApiResponse, isApiErrorResponse, QueryResponse } from "../framework/typedb-driver/response"; +import { AttributeType, EntityType, RelationType, RoleType, Type } from "../framework/typedb-driver/concept"; +import { ApiOkResponse, ApiResponse, ConceptRowsQueryResponse, isApiErrorResponse, QueryResponse } from "../framework/typedb-driver/response"; import { DriverState } from "./driver-state.service"; import { SnackbarService } from "./snackbar.service"; @@ -35,7 +36,8 @@ type VisualiserStatus = "ok" | "running" | "noAnswers" | "error"; export class SchemaState { readonly visualiser = new VisualiserState(); - queryResponses$ = new BehaviorSubject[] | null>(null); + readonly tree = new TreeState(); + queryResponses$ = new BehaviorSubject[] | null>(null); isRefreshing = false; readonly refreshDisabledReason$ = combineLatest([this.driver.status$, this.driver.database$]).pipe(map(([status, db]) => { if (status !== "connected") return NO_SERVER_CONNECTED; @@ -51,6 +53,9 @@ export class SchemaState { ).subscribe(() => { this.refresh(); }); + this.queryResponses$.subscribe(data => { + this.tree.push(data); + }); } refresh() { @@ -67,12 +72,15 @@ export class SchemaState { } this.initialiseOutput(); - const responses: ApiResponse[] = []; + const responses: ApiOkResponse[] = []; this.isRefreshing = true; this.driver.runBackgroundReadQueries(schemaQueriesList).pipe( finalize(() => { this.isRefreshing = false; }) ).subscribe({ - next: (res) => { responses.push(res); }, + next: (res) => { + if (res.ok.answerType !== `conceptRows`) throw `Unexpected answerType: '${res.ok.answerType}' (expected 'conceptRows')`; + responses.push(res as ApiOkResponse); + }, error: (err) => { this.handleQueryError(err); }, complete: () => { this.queryResponses$.next(responses); }, }); @@ -196,3 +204,140 @@ export interface SigmaState { camera: Camera; settings: any; } + +interface SchemaTreeEntity extends EntityType { + supertype?: SchemaTreeEntity; + ownedAttributes: SchemaTreeAttribute[]; + playableRoles: SchemaTreeRole[]; +} + +interface SchemaTreeRelation extends RelationType { + supertype?: SchemaTreeRelation; + ownedAttributes: SchemaTreeAttribute[]; + playableRoles: SchemaTreeRole[]; + roleplayers: SchemaTreeRole[]; +} + +interface SchemaTreeAttribute extends AttributeType { + supertype?: SchemaTreeAttribute; +} + +export type SchemaTreeType = SchemaTreeEntity | SchemaTreeRelation | SchemaTreeAttribute; + +interface SchemaTreeRole extends RoleType { + relationLabel: string; +} + +export interface SchemaTree { + entities: SchemaTreeEntity[]; + relations: SchemaTreeRelation[]; + attributes: SchemaTreeAttribute[]; +} + +function entityOf(entityType: EntityType): SchemaTreeEntity { + return { + kind: entityType.kind, + label: entityType.label, + supertype: undefined, + ownedAttributes: [], + playableRoles: [], + }; +} + +function relationOf(relationType: RelationType): SchemaTreeRelation { + return { + kind: relationType.kind, + label: relationType.label, + supertype: undefined, + ownedAttributes: [], + playableRoles: [], + roleplayers: [], + }; +} + +function attributeOf(attributeType: AttributeType): SchemaTreeAttribute { + return { + kind: attributeType.kind, + label: attributeType.label, + supertype: undefined, + valueType: attributeType.valueType, + }; +} + +function typeOf(type: Type): SchemaTreeType { + switch (type.kind) { + case "entityType": return entityOf(type); + case "relationType": return relationOf(type); + case "attributeType": return attributeOf(type); + default: throw `Unexpected type: ${JSON.stringify(type)}`; + } +} + +export class TreeState { + readonly data$ = new BehaviorSubject(null); + + push(data: ApiOkResponse[] | null) { + if (!data) { + this.data$.next(null); + return; + } + + const treeBuilder = new TreeBuilder(data); + const tree = treeBuilder.build(); + this.data$.next(tree); + } +} + +class TreeBuilder { + readonly typeHierarchy: ConceptRowsQueryResponse; + readonly ownedAttributes: ConceptRowsQueryResponse; + readonly roleplayers: ConceptRowsQueryResponse; + readonly playableRoles: ConceptRowsQueryResponse; + readonly entityTypes = {} as Record; + readonly relationTypes = {} as Record; + readonly attributeTypes = {} as Record; + + constructor(data: ApiOkResponse[]) { + const [typeHierarchy, ownedAttributes, roleplayers, playableRoles] = data.map(x => x.ok); + this.typeHierarchy = typeHierarchy; + this.ownedAttributes = ownedAttributes; + this.roleplayers = roleplayers; + this.playableRoles = playableRoles; + } + + build(): SchemaTree { + this.processTypeHierarchy(); + return { + entities: Object.values(this.entityTypes), + relations: Object.values(this.relationTypes), + attributes: Object.values(this.attributeTypes), + }; + } + + processTypeHierarchy() { + for (const answer of this.typeHierarchy.answers) { + const [type, supertype] = [answer.data["t"], answer.data["supertype"]]; + if (!type || !supertype) throw `Unexpected type hierarchy answer: ${JSON.stringify(answer.data)}`; + let treeType: SchemaTreeType; + switch (type.kind) { + case "entityType": + this.entityTypes[type.label] = treeType = entityOf(type); + break; + case "relationType": + this.relationTypes[type.label] = treeType = relationOf(type); + break; + case "attributeType": + this.attributeTypes[type.label] = treeType = attributeOf(type); + break; + case "roleType": + return; + default: + throw `Unexpected type hierarchy answer: ${JSON.stringify(answer.data)}`; + } + if (supertype.kind !== type.kind) throw `Unexpected type hierarchy answer: ${JSON.stringify(answer.data)}`; + if (type.label !== supertype.label) { + treeType.supertype = typeOf(supertype); + } + } + } +} diff --git a/styles/material.scss b/styles/material.scss index ca0237be..ce353545 100644 --- a/styles/material.scss +++ b/styles/material.scss @@ -796,4 +796,15 @@ body { .mdc-tooltip__surface { padding: 6px 10px; } + + /* Tree */ + .mat-tree { + --mat-tree-container-background-color: transparent; + --mat-tree-node-min-height: 32px; + --mat-tree-node-text-size: 13px; + } + + mat-tree-node[aria-level="2"] { + padding-left: 16px !important; /* overrides inline style from Angular Material */ + } } From d75f7c46caff755c9798cf0b157bda56ae458001 Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Tue, 17 Jun 2025 18:56:23 +0100 Subject: [PATCH 02/11] Enrich schema tree view with owns, plays and relates links --- src/module/query/query-tool.component.html | 15 +- src/module/query/query-tool.component.ts | 3 +- .../schema-tree-node.component.html | 24 ++ .../schema-tree-node.component.scss | 263 ++++++++++++ .../schema-tree-node.component.ts | 33 ++ src/service/query-tool-state.service.ts | 148 ++++--- src/service/schema-state.service.ts | 400 ++++++++++++------ styles/material.scss | 15 +- 8 files changed, 689 insertions(+), 212 deletions(-) create mode 100644 src/module/query/schema-tree-node/schema-tree-node.component.html create mode 100644 src/module/query/schema-tree-node/schema-tree-node.component.scss create mode 100644 src/module/query/schema-tree-node/schema-tree-node.component.ts diff --git a/src/module/query/query-tool.component.html b/src/module/query/query-tool.component.html index ea42d7f5..1120b3f3 100644 --- a/src/module/query/query-tool.component.html +++ b/src/module/query/query-tool.component.html @@ -6,22 +6,21 @@

Schema

- + - - {{node.name}} + + - - - {{node.name}} +
    diff --git a/src/module/query/query-tool.component.ts b/src/module/query/query-tool.component.ts index 127a2cc9..22dae024 100644 --- a/src/module/query/query-tool.component.ts +++ b/src/module/query/query-tool.component.ts @@ -31,6 +31,7 @@ import { DriverState } from "../../service/driver-state.service"; import { QueryToolState } from "../../service/query-tool-state.service"; import { SnackbarService } from "../../service/snackbar.service"; import { PageScaffoldComponent } from "../scaffold/page/page-scaffold.component"; +import { SchemaTreeNodeComponent } from "./schema-tree-node/schema-tree-node.component"; @Component({ selector: "ts-query-tool", @@ -39,7 +40,7 @@ import { PageScaffoldComponent } from "../scaffold/page/page-scaffold.component" imports: [ RouterLink, AsyncPipe, PageScaffoldComponent, MatDividerModule, MatFormFieldModule, MatTreeModule, MatIconModule, MatInputModule, FormsModule, ReactiveFormsModule, MatButtonToggleModule, CodeEditor, ResizableDirective, - DatePipe, SpinnerComponent, MatTableModule, MatSortModule, MatTooltipModule, MatButtonModule, RichTooltipDirective, + DatePipe, SpinnerComponent, MatTableModule, MatSortModule, MatTooltipModule, MatButtonModule, RichTooltipDirective, SchemaTreeNodeComponent, ] }) export class QueryToolComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/src/module/query/schema-tree-node/schema-tree-node.component.html b/src/module/query/schema-tree-node/schema-tree-node.component.html new file mode 100644 index 00000000..2893a61d --- /dev/null +++ b/src/module/query/schema-tree-node/schema-tree-node.component.html @@ -0,0 +1,24 @@ +@switch (data.nodeKind) { + @case ("root") { + {{ data.label }} + } + @case ("concept") { + {{ data.concept.label }}@if (data.concept.kind === "attributeType") {, value {{ data.concept.valueType }}} + } + @case ("link") { + @switch (data.linkKind) { + @case ("sub") { + sub {{ data.supertype.label }} + } + @case ("owns") { + owns {{ data.ownedAttribute.label }}, value {{ data.ownedAttribute.valueType }} + } + @case ("relates") { + relates {{ data.role.label }} + } + @case ("plays") { + plays {{ data.role.label }} + } + } + } +} diff --git a/src/module/query/schema-tree-node/schema-tree-node.component.scss b/src/module/query/schema-tree-node/schema-tree-node.component.scss new file mode 100644 index 00000000..2326cf79 --- /dev/null +++ b/src/module/query/schema-tree-node/schema-tree-node.component.scss @@ -0,0 +1,263 @@ +/*!/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +@use "media"; +@use "primary"; +@use "secondary"; +@use "typography"; +@use "shapes"; + +:host { + height: 100%; +} + +.query-page-rough { + height: 100%; + margin-top: 16px; + margin-bottom: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.query-page { + height: 100%; + margin-top: 16px; + margin-bottom: 16px; + gap: 4px !important; + + ::ng-deep app-drag-handle span { + display: none; + } +} + +.tp-bento-container { + gap: 16px; +} + +.tool-windows { + flex: 0 0 360px; +} + +.main-panes { + flex: 1; + gap: 4px !important; + + @media (max-width: media.$max-width-mobile) { + margin-left: -8px; + } +} + +.history-pane, .query-pane, .run-pane { + display: flex; + flex-direction: column; +} + +.run-pane mat-form-field { + height: 100%; +} + +.card { + @include shapes.standard-border; + border-radius: shapes.$border-radius-panel; + @include shapes.light-source-gradient(primary.$purple, primary.$deep-purple); + transition: background 0.1s linear; + padding: 16px; +} + +.card-header { + display: flex; + gap: 16px; + align-items: center; + margin-bottom: 12px; + height: 32px; +} + +mat-form-field { + width: 100%; +} + +button { + i.fa-play { + margin: 0; + } + + &:enabled i.fa-play { + color: secondary.$deep-green; + } +} + +.query-text-box { + resize: none; + font-family: "Monaco" !important; + font-size: 14px !important; + font-weight: 400 !important; + line-height: 22px !important; + letter-spacing: 0.02em !important; +} + +.code-editor-container { + height: 100%; + overflow: auto; + border: 1px solid primary.$light-purple; + background: primary.$black-purple; + + code-editor { + height: 100%; + } +} + +.answers-outer-container { + height: 100%; + width: 100%; + border: 1px solid primary.$light-purple; + background: primary.$black-purple; + position: relative; +} + +.answers-container { + position: absolute; + inset: 0; + overflow: auto; +} + +.answers-text-container { + mat-form-field { + flex: 1; + display: flex; + flex-direction: column; + } + + ::ng-deep .mdc-text-field, + ::ng-deep .mat-mdc-form-field-flex, + ::ng-deep .mat-mdc-form-field-infix { + width: 100%; + height: 100%; + --mdc-outlined-text-field-container-shape: 0; + } + + ::ng-deep .mdc-notched-outline { + display: none; + } + + ::ng-deep .mdc-text-field { + --mdc-outlined-text-field-input-text-color: #{secondary.$pink}; + } + + ::ng-deep .mat-mdc-form-field:not(.form-field-dense) .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { + padding: 4px 2px 4px 6px; + } + + .answers-text-box { + height: 100% !important; + resize: none; + @include typography.code; + } +} + +.status-text-container { + width: 100%; + height: 100%; + display: flex; +} + +.status-text { + @include typography.p1; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80%; + text-align: center; + z-index: 10; /* Ensure it's above the canvas elements */ + white-space: normal !important; + overflow-wrap: break-word; + padding: 10px; + margin: 0; +} + +.answers-placeholder-container { + height: 100%; + overflow: auto; + border: 3px dashed primary.$light-purple; + border-radius: shapes.$border-radius-panel; + display: flex; + justify-content: center; + align-items: center; + background: primary.$black-purple; + --mat-button-filled-container-height: 40px; +} + +#structureView { + width: 100%; + height: 100%; + display: flex; +} + +.history-pane { + @media (max-width: media.$max-width-mobile) { + display: none; + } + + .history-container { + height: 100%; + width: 100%; + overflow: auto; + } + + ol li { + list-style: none; + margin-top: 8px; + + aside { + display: flex; + align-items: center; + + .bullet { + margin: 0 4px; + } + + tp-spinner { + width: unset; + } + + .action-status { + margin-left: 8px; + } + + .action-status i { + margin-left: 6px; + font-size: 12px; + + &.fa-xmark { + color: #{primary.$red}; + cursor: pointer; + } + } + } + + .mat-divider { + margin-top: 8px; + } + } + + mat-form-field { + margin-top: 4px; + + ::ng-deep .mdc-notched-outline { + + } + + textarea { + resize: none; + @include typography.code; + } + } + + .transaction-operation-type { + min-width: 140px; + } +} diff --git a/src/module/query/schema-tree-node/schema-tree-node.component.ts b/src/module/query/schema-tree-node/schema-tree-node.component.ts new file mode 100644 index 00000000..f5e7e3d0 --- /dev/null +++ b/src/module/query/schema-tree-node/schema-tree-node.component.ts @@ -0,0 +1,33 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { Component, Input } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { MatButtonModule } from "@angular/material/button"; +import { MatButtonToggleModule } from "@angular/material/button-toggle"; +import { MatDividerModule } from "@angular/material/divider"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatIconModule } from "@angular/material/icon"; +import { MatInputModule } from "@angular/material/input"; +import { MatSortModule } from "@angular/material/sort"; +import { MatTableModule } from "@angular/material/table"; +import { MatTreeModule } from "@angular/material/tree"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { SchemaTreeNode } from "../../../service/query-tool-state.service"; + +@Component({ + selector: "ts-schema-tree-node", + templateUrl: "schema-tree-node.component.html", + styleUrls: ["schema-tree-node.component.scss"], + imports: [ + MatDividerModule, MatFormFieldModule, MatTreeModule, MatIconModule, + MatInputModule, FormsModule, ReactiveFormsModule, MatButtonToggleModule, + MatTableModule, MatSortModule, MatTooltipModule, MatButtonModule, + ] +}) +export class SchemaTreeNodeComponent { + @Input({ required: true }) data!: SchemaTreeNode; +} diff --git a/src/service/query-tool-state.service.ts b/src/service/query-tool-state.service.ts index 288dcac6..d6198ed7 100644 --- a/src/service/query-tool-state.service.ts +++ b/src/service/query-tool-state.service.ts @@ -17,7 +17,7 @@ import { Concept, Value } from "../framework/typedb-driver/concept"; import { ApiResponse, ConceptDocument, ConceptRow, isApiErrorResponse, QueryResponse } from "../framework/typedb-driver/response"; import { INTERNAL_ERROR } from "../framework/util/strings"; import { DriverState } from "./driver-state.service"; -import { SchemaState, SchemaTree, SchemaTreeType } from "./schema-state.service"; +import { SchemaState, Schema, SchemaAttribute, SchemaRole, SchemaConcept } from "./schema-state.service"; import { SnackbarService } from "./snackbar.service"; import { FlatTreeControl } from "@angular/cdk/tree"; import { MatTreeFlatDataSource, MatTreeFlattener } from "@angular/material/tree"; @@ -135,84 +135,114 @@ export class QueryToolState { } } -interface SchemaTreeNode { - label?: string; - type?: SchemaTreeType; - children: SchemaTreeNode[]; +export type SchemaTreeNodeKind = "root" | "concept" | "link"; + +export interface SchemaTreeNodeBase { + nodeKind: SchemaTreeNodeKind; + children?: SchemaTreeNode[]; +} + +export interface SchemaTreeRootNode extends SchemaTreeNodeBase { + nodeKind: "root"; + label: string; + children: SchemaTreeConceptNode[]; +} + +export interface SchemaTreeConceptNode extends SchemaTreeNodeBase { + nodeKind: "concept"; + concept: SchemaConcept; + children: SchemaTreeLinkNode[]; +} + +export type SchemaTreeLinkKind = "sub" | "owns" | "plays" | "relates"; + +export interface SchemaTreeLinkNodeBase extends SchemaTreeNodeBase { + nodeKind: "link"; + linkKind: SchemaTreeLinkKind; +} + +export interface SchemaTreeSubLinkNode extends SchemaTreeLinkNodeBase { + linkKind: "sub"; + supertype: SchemaConcept; } -/** Flat node with expandable and level information */ -interface FlatNode { - expandable: boolean; - name: string; - level: number; +export interface SchemaTreeOwnsLinkNode extends SchemaTreeLinkNodeBase { + linkKind: "owns"; + ownedAttribute: SchemaAttribute; } +export interface SchemaTreePlaysLinkNode extends SchemaTreeLinkNodeBase { + linkKind: "plays"; + role: SchemaRole; +} + +export interface SchemaTreeRelatesLinkNode extends SchemaTreeLinkNodeBase { + linkKind: "relates"; + role: SchemaRole; +} + +export type SchemaTreeLinkNode = SchemaTreeSubLinkNode | SchemaTreeOwnsLinkNode | SchemaTreePlaysLinkNode | SchemaTreeRelatesLinkNode; + +export type SchemaTreeNode = SchemaTreeRootNode | SchemaTreeConceptNode | SchemaTreeLinkNode; + export class SchemaWindowState { - private _transformer = (node: SchemaTreeNode, level: number) => { - return { - expandable: !!node.children.length, - name: node.label || node.type?.label || ``, - level: level, - }; - }; - - treeFlattener = new MatTreeFlattener( - this._transformer, - node => node.level, - node => node.expandable, - node => node.children, - ); - - treeControl = this.createTreeControl(); - entitiesTreeControl = this.createTreeControl(); - relationsTreeControl = this.createTreeControl(); - attributesTreeControl = this.createTreeControl(); - dataSource: MatTreeFlatDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); - dataSourcesObj: Record<"Entities" | "Relations" | "Attributes", { title: string, treeControl: FlatTreeControl, dataSource: MatTreeFlatDataSource }> = { - "Entities": { title: "Entities", treeControl: this.entitiesTreeControl, dataSource: new MatTreeFlatDataSource(this.entitiesTreeControl, this.treeFlattener) }, - "Relations": { title: "Relations", treeControl: this.relationsTreeControl, dataSource: new MatTreeFlatDataSource(this.relationsTreeControl, this.treeFlattener) }, - "Attributes": { title: "Attributes", treeControl: this.attributesTreeControl, dataSource: new MatTreeFlatDataSource(this.attributesTreeControl, this.treeFlattener) }, - }; - dataSources = Object.values(this.dataSourcesObj); - - private createTreeControl() { - return new FlatTreeControl( - node => node.level, - node => node.expandable, - ); - } + dataSource: SchemaTreeRootNode[] = []; + // dataSourcesObj: Record<"Entities" | "Relations" | "Attributes", { title: string, treeControl: FlatTreeControl, dataSource: MatTreeFlatDataSource }> = { + // "Entities": { title: "Entities", treeControl: this.entitiesTreeControl, dataSource: new MatTreeFlatDataSource(this.entitiesTreeControl, this.treeFlattener) }, + // "Relations": { title: "Relations", treeControl: this.relationsTreeControl, dataSource: new MatTreeFlatDataSource(this.relationsTreeControl, this.treeFlattener) }, + // "Attributes": { title: "Attributes", treeControl: this.attributesTreeControl, dataSource: new MatTreeFlatDataSource(this.attributesTreeControl, this.treeFlattener) }, + // }; + // dataSources = Object.values(this.dataSourcesObj); constructor(public schemaState: SchemaState) { - schemaState.tree.data$.subscribe(data => { - this.populateDataSources(data); + schemaState.value$.subscribe(schema => { + this.populateDataSources(schema); }); } - hasChild = (_: number, node: FlatNode) => node.expandable; + hasChild = (_: number, node: SchemaTreeNode) => !!node.children?.length; + + childrenAccessor = (node: SchemaTreeNode) => node.children ?? []; - private populateDataSources(data: SchemaTree | null) { - if (!data) { - this.dataSources.forEach(x => x.dataSource.data = []); + private populateDataSources(schema: Schema | null) { + if (!schema) { + // this.dataSources.forEach(x => x.dataSource.data = []); return; } - this.dataSource.data = [{ + this.dataSource = [{ + nodeKind: "root", label: "Entities", - children: data.entities.map(x => ({ - type: x, - children: [], + children: schema.entities.map(x => ({ + nodeKind: "concept", + concept: x, + children: ([ + ...(x.supertype ? [{ nodeKind: "link", linkKind: "sub", supertype: x.supertype }] : []), + ...x.ownedAttributes.map(y => ({ nodeKind: "link", linkKind: "owns", ownedAttribute: y })), + ...x.playableRoles.map(y => ({ nodeKind: "link", linkKind: "plays", role: y })), + ] as SchemaTreeLinkNode[]), })), }, { + nodeKind: "root", label: "Relations", - children: data.relations.map(x => ({ - type: x, - children: [], + children: schema.relations.map(x => ({ + nodeKind: "concept", + concept: x, + children: ([ + ...(x.supertype ? [{ nodeKind: "link", linkKind: "sub", supertype: x.supertype }] : []), + ...x.roleplayers.map(y => ({ nodeKind: "link", linkKind: "relates", role: y })), + ...x.ownedAttributes.map(y => ({ nodeKind: "link", linkKind: "owns", ownedAttribute: y })), + ...x.playableRoles.map(y => ({ nodeKind: "link", linkKind: "plays", role: y })), + ] as SchemaTreeLinkNode[]), })), }, { + nodeKind: "root", label: "Attributes", - children: data.attributes.map(x => ({ - type: x, - children: [], + children: schema.attributes.map(x => ({ + nodeKind: "concept", + concept: x, + children: ([ + ...(x.supertype ? [{ nodeKind: "link", linkKind: "sub", supertype: x.supertype }] : []), + ] as SchemaTreeLinkNode[]), })), }]; } diff --git a/src/service/schema-state.service.ts b/src/service/schema-state.service.ts index 170f3e93..1198b1fb 100644 --- a/src/service/schema-state.service.ts +++ b/src/service/schema-state.service.ts @@ -30,14 +30,44 @@ const schemaQueriesList = Object.values(schemaQueries); type VisualiserStatus = "ok" | "running" | "noAnswers" | "error"; +interface SchemaEntity extends EntityType { + supertype?: SchemaEntity; + subtypes: SchemaEntity[]; + ownedAttributes: SchemaAttribute[]; + playableRoles: SchemaRole[]; +} + +interface SchemaRelation extends RelationType { + supertype?: SchemaRelation; + subtypes: SchemaRelation[]; + ownedAttributes: SchemaAttribute[]; + playableRoles: SchemaRole[]; + roleplayers: SchemaRole[]; +} + +export interface SchemaAttribute extends AttributeType { + supertype?: SchemaAttribute; + subtypes: SchemaAttribute[]; +} + +export type SchemaConcept = SchemaEntity | SchemaRelation | SchemaAttribute; + +export type SchemaRole = RoleType; + +export interface Schema { + entities: SchemaEntity[]; + relations: SchemaRelation[]; + attributes: SchemaAttribute[]; +} + @Injectable({ providedIn: "root", }) export class SchemaState { readonly visualiser = new VisualiserState(); - readonly tree = new TreeState(); queryResponses$ = new BehaviorSubject[] | null>(null); + readonly value$ = new BehaviorSubject(null); isRefreshing = false; readonly refreshDisabledReason$ = combineLatest([this.driver.status$, this.driver.database$]).pipe(map(([status, db]) => { if (status !== "connected") return NO_SERVER_CONNECTED; @@ -54,7 +84,7 @@ export class SchemaState { this.refresh(); }); this.queryResponses$.subscribe(data => { - this.tree.push(data); + this.push(data); }); } @@ -87,6 +117,17 @@ export class SchemaState { }); } + push(data: ApiOkResponse[] | null) { + if (!data) { + this.value$.next(null); + return; + } + + const schemaBuilder = new SchemaBuilder(data); + const schema = schemaBuilder.build(); + this.value$.next(schema); + } + private initialiseOutput() { this.visualiser.destroy(); this.visualiser.status = "running"; @@ -121,6 +162,224 @@ export class SchemaState { } } +function entityOf(entityType: EntityType): SchemaEntity { + return { + kind: entityType.kind, + label: entityType.label, + supertype: undefined, + subtypes: [], + ownedAttributes: [], + playableRoles: [], + }; +} + +function relationOf(relationType: RelationType): SchemaRelation { + return { + kind: relationType.kind, + label: relationType.label, + supertype: undefined, + subtypes: [], + ownedAttributes: [], + playableRoles: [], + roleplayers: [], + }; +} + +function attributeOf(attributeType: AttributeType): SchemaAttribute { + return { + kind: attributeType.kind, + label: attributeType.label, + supertype: undefined, + subtypes: [], + valueType: attributeType.valueType, + }; +} + +class SchemaBuilder { + readonly typeHierarchy: ConceptRowsQueryResponse; + readonly ownedAttributes: ConceptRowsQueryResponse; + readonly roleplayers: ConceptRowsQueryResponse; + readonly playableRoles: ConceptRowsQueryResponse; + readonly entityTypes = {} as Record; + readonly relationTypes = {} as Record; + readonly attributeTypes = {} as Record; + + constructor(data: ApiOkResponse[]) { + const [typeHierarchy, ownedAttributes, roleplayers, playableRoles] = data.map(x => x.ok); + this.typeHierarchy = typeHierarchy; + this.ownedAttributes = ownedAttributes; + this.roleplayers = roleplayers; + this.playableRoles = playableRoles; + } + + build(): Schema { + this.populateConcepts(); + this.buildTypeHierarchy(); + this.attachOwnedAttributes(); + this.attachPlayableRoles(); + this.attachRoleplayers(); + return { + entities: Object.values(this.entityTypes), + relations: Object.values(this.relationTypes), + attributes: Object.values(this.attributeTypes), + }; + } + + private populateConcepts() { + for (const answer of this.typeHierarchy.answers) { + const [type, supertype] = [answer.data["t"], answer.data["supertype"]]; + if (!type || !supertype) throw this.unexpectedTypeHierarchyAnswer(answer); + switch (type.kind) { + case "entityType": + this.entityTypes[type.label] = this.entityTypes[type.label] ?? entityOf(type); + break; + case "relationType": + this.relationTypes[type.label] = this.relationTypes[type.label] ?? relationOf(type); + break; + case "attributeType": + this.attributeTypes[type.label] = this.attributeTypes[type.label] ?? attributeOf(type); + break; + case "roleType": + continue; + default: + throw this.unexpectedTypeHierarchyAnswer(answer); + } + } + } + + private buildTypeHierarchy() { + for (const answer of this.typeHierarchy.answers) { + const [type, supertype] = [answer.data["t"], answer.data["supertype"]] as Type[]; + if (type.label === supertype.label) continue; + let node: SchemaConcept; + let supernode: SchemaConcept; + switch (type.kind) { + case "entityType": + node = this.expectEntityType(type.label); + supernode = this.expectEntityType(supertype.label); + break; + case "relationType": + node = this.expectRelationType(type.label); + supernode = this.expectRelationType(supertype.label); + break; + case "attributeType": + node = this.expectAttributeType(type.label); + supernode = this.expectAttributeType(supertype.label); + break; + case "roleType": + continue; + default: + throw this.unexpectedTypeHierarchyAnswer(answer); + } + node.supertype = supernode; + (supernode.subtypes as SchemaConcept[]).push(node); + } + } + + private attachOwnedAttributes() { + for (const answer of this.ownedAttributes.answers) { + const [ownerType, ownedAttr] = [answer.data["t"], answer.data["attr"]]; + if (!ownerType || !ownedAttr || ownedAttr.kind !== "attributeType") throw this.unexpectedOwnedAttributesAnswer(answer); + let ownerNode: SchemaConcept; + const ownedAttrNode: SchemaAttribute = this.expectAttributeType(ownedAttr.label); + switch (ownerType.kind) { + case "entityType": + ownerNode = this.expectEntityType(ownerType.label); + break; + case "relationType": + ownerNode = this.expectRelationType(ownerType.label); + break; + default: + throw this.unexpectedOwnedAttributesAnswer(answer); + } + this.propagateOwnedAttributes(ownerNode, ownedAttrNode); + } + } + + private propagateOwnedAttributes(ownerNode: SchemaEntity | SchemaRelation, ownedAttrNode: SchemaAttribute) { + ownerNode.ownedAttributes.push(ownedAttrNode); + for (const ownerSubnode of ownerNode.subtypes) { + this.propagateOwnedAttributes(ownerSubnode, ownedAttrNode); + } + } + + private attachRoleplayers() { + for (const answer of this.roleplayers.answers) { + const [rel, role] = [answer.data["t"], answer.data["related"]]; + if (!rel || !role || rel.kind !== "relationType" || role.kind !== "roleType") throw this.unexpectedRoleplayersAnswer(answer); + const relNode: SchemaRelation = this.expectRelationType(rel.label); + this.propagateRoleplayers(relNode, role); + } + } + + private propagateRoleplayers(relNode: SchemaRelation, role: RoleType) { + relNode.roleplayers.push(role); + for (const relSubnode of relNode.subtypes) { + this.propagateRoleplayers(relSubnode, role); + } + } + + private attachPlayableRoles() { + for (const answer of this.playableRoles.answers) { + const [obj, role] = [answer.data["t"], answer.data["played"]]; + if (!obj || !role || role.kind !== "roleType") throw this.unexpectedPlayableRolesAnswer(answer); + let objNode: SchemaEntity | SchemaRelation; + switch (obj.kind) { + case "entityType": + objNode = this.expectEntityType(obj.label); + break; + case "relationType": + objNode = this.expectRelationType(obj.label); + break; + default: + throw this.unexpectedPlayableRolesAnswer(answer); + } + this.propagatePlayableRoles(objNode, role); + } + } + + private propagatePlayableRoles(objNode: SchemaEntity | SchemaRelation, role: RoleType) { + objNode.playableRoles.push(role); + for (const objSubnode of objNode.subtypes) { + this.propagatePlayableRoles(objSubnode, role); + } + } + + private expectEntityType(label: string): SchemaEntity { + const type = this.entityTypes[label]; + if (!type) throw `Missing expected entity type in schema with label '${label}'`; + return type; + } + + private expectRelationType(label: string): SchemaRelation { + const type = this.relationTypes[label]; + if (!type) throw `Missing expected relation type in schema with label '${label}'`; + return type; + } + + private expectAttributeType(label: string): SchemaAttribute { + const type = this.attributeTypes[label]; + if (!type) throw `Missing expected attribute type in schema with label '${label}'`; + return type; + } + + private unexpectedTypeHierarchyAnswer(answer: ConceptRowsQueryResponse["answers"][number]) { + return `Unexpected type hierarchy answer: ${JSON.stringify(answer.data)}`; + } + + private unexpectedOwnedAttributesAnswer(answer: ConceptRowsQueryResponse["answers"][number]) { + return `Unexpected owned attributes answer: ${JSON.stringify(answer.data)}`; + } + + private unexpectedPlayableRolesAnswer(answer: ConceptRowsQueryResponse["answers"][number]) { + return `Unexpected playable roles answer: ${JSON.stringify(answer.data)}`; + } + + private unexpectedRoleplayersAnswer(answer: ConceptRowsQueryResponse["answers"][number]) { + return `Unexpected roleplayers answer: ${JSON.stringify(answer.data)}`; + } +} + export class VisualiserState { status: VisualiserStatus = "ok"; @@ -204,140 +463,3 @@ export interface SigmaState { camera: Camera; settings: any; } - -interface SchemaTreeEntity extends EntityType { - supertype?: SchemaTreeEntity; - ownedAttributes: SchemaTreeAttribute[]; - playableRoles: SchemaTreeRole[]; -} - -interface SchemaTreeRelation extends RelationType { - supertype?: SchemaTreeRelation; - ownedAttributes: SchemaTreeAttribute[]; - playableRoles: SchemaTreeRole[]; - roleplayers: SchemaTreeRole[]; -} - -interface SchemaTreeAttribute extends AttributeType { - supertype?: SchemaTreeAttribute; -} - -export type SchemaTreeType = SchemaTreeEntity | SchemaTreeRelation | SchemaTreeAttribute; - -interface SchemaTreeRole extends RoleType { - relationLabel: string; -} - -export interface SchemaTree { - entities: SchemaTreeEntity[]; - relations: SchemaTreeRelation[]; - attributes: SchemaTreeAttribute[]; -} - -function entityOf(entityType: EntityType): SchemaTreeEntity { - return { - kind: entityType.kind, - label: entityType.label, - supertype: undefined, - ownedAttributes: [], - playableRoles: [], - }; -} - -function relationOf(relationType: RelationType): SchemaTreeRelation { - return { - kind: relationType.kind, - label: relationType.label, - supertype: undefined, - ownedAttributes: [], - playableRoles: [], - roleplayers: [], - }; -} - -function attributeOf(attributeType: AttributeType): SchemaTreeAttribute { - return { - kind: attributeType.kind, - label: attributeType.label, - supertype: undefined, - valueType: attributeType.valueType, - }; -} - -function typeOf(type: Type): SchemaTreeType { - switch (type.kind) { - case "entityType": return entityOf(type); - case "relationType": return relationOf(type); - case "attributeType": return attributeOf(type); - default: throw `Unexpected type: ${JSON.stringify(type)}`; - } -} - -export class TreeState { - readonly data$ = new BehaviorSubject(null); - - push(data: ApiOkResponse[] | null) { - if (!data) { - this.data$.next(null); - return; - } - - const treeBuilder = new TreeBuilder(data); - const tree = treeBuilder.build(); - this.data$.next(tree); - } -} - -class TreeBuilder { - readonly typeHierarchy: ConceptRowsQueryResponse; - readonly ownedAttributes: ConceptRowsQueryResponse; - readonly roleplayers: ConceptRowsQueryResponse; - readonly playableRoles: ConceptRowsQueryResponse; - readonly entityTypes = {} as Record; - readonly relationTypes = {} as Record; - readonly attributeTypes = {} as Record; - - constructor(data: ApiOkResponse[]) { - const [typeHierarchy, ownedAttributes, roleplayers, playableRoles] = data.map(x => x.ok); - this.typeHierarchy = typeHierarchy; - this.ownedAttributes = ownedAttributes; - this.roleplayers = roleplayers; - this.playableRoles = playableRoles; - } - - build(): SchemaTree { - this.processTypeHierarchy(); - return { - entities: Object.values(this.entityTypes), - relations: Object.values(this.relationTypes), - attributes: Object.values(this.attributeTypes), - }; - } - - processTypeHierarchy() { - for (const answer of this.typeHierarchy.answers) { - const [type, supertype] = [answer.data["t"], answer.data["supertype"]]; - if (!type || !supertype) throw `Unexpected type hierarchy answer: ${JSON.stringify(answer.data)}`; - let treeType: SchemaTreeType; - switch (type.kind) { - case "entityType": - this.entityTypes[type.label] = treeType = entityOf(type); - break; - case "relationType": - this.relationTypes[type.label] = treeType = relationOf(type); - break; - case "attributeType": - this.attributeTypes[type.label] = treeType = attributeOf(type); - break; - case "roleType": - return; - default: - throw `Unexpected type hierarchy answer: ${JSON.stringify(answer.data)}`; - } - if (supertype.kind !== type.kind) throw `Unexpected type hierarchy answer: ${JSON.stringify(answer.data)}`; - if (type.label !== supertype.label) { - treeType.supertype = typeOf(supertype); - } - } - } -} diff --git a/styles/material.scss b/styles/material.scss index 5572ba19..37c8b03b 100644 --- a/styles/material.scss +++ b/styles/material.scss @@ -823,13 +823,18 @@ body { } /* Tree */ - .mat-tree { - --mat-tree-container-background-color: transparent; - --mat-tree-node-min-height: 32px; - --mat-tree-node-text-size: 13px; - } + @include mat.tree-overrides(( + container-background-color: transparent, + node-min-height: 30px, + node-text-size: 14px, + node-text-weight: typography.$regular, + )); mat-tree-node[aria-level="2"] { padding-left: 16px !important; /* overrides inline style from Angular Material */ } + + mat-tree-node[aria-level="3"] { + padding-left: 32px !important; /* overrides inline style from Angular Material */ + } } From 0a2ea882f91bf1866193d97e4e7ff95b5962c96e Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Wed, 18 Jun 2025 16:23:34 +0100 Subject: [PATCH 03/11] Represent schema as a map of labels to concepts. Standardise terminology --- src/service/query-tool-state.service.ts | 12 ++--- src/service/schema-state.service.ts | 70 ++++++++++++------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/service/query-tool-state.service.ts b/src/service/query-tool-state.service.ts index d6198ed7..c76bdcb4 100644 --- a/src/service/query-tool-state.service.ts +++ b/src/service/query-tool-state.service.ts @@ -212,32 +212,32 @@ export class SchemaWindowState { this.dataSource = [{ nodeKind: "root", label: "Entities", - children: schema.entities.map(x => ({ + children: Object.values(schema.entities).sort((a, b) => a.label.localeCompare(b.label)).map(x => ({ nodeKind: "concept", concept: x, children: ([ ...(x.supertype ? [{ nodeKind: "link", linkKind: "sub", supertype: x.supertype }] : []), ...x.ownedAttributes.map(y => ({ nodeKind: "link", linkKind: "owns", ownedAttribute: y })), - ...x.playableRoles.map(y => ({ nodeKind: "link", linkKind: "plays", role: y })), + ...x.playedRoles.map(y => ({ nodeKind: "link", linkKind: "plays", role: y })), ] as SchemaTreeLinkNode[]), })), }, { nodeKind: "root", label: "Relations", - children: schema.relations.map(x => ({ + children: Object.values(schema.relations).sort((a, b) => a.label.localeCompare(b.label)).map(x => ({ nodeKind: "concept", concept: x, children: ([ ...(x.supertype ? [{ nodeKind: "link", linkKind: "sub", supertype: x.supertype }] : []), - ...x.roleplayers.map(y => ({ nodeKind: "link", linkKind: "relates", role: y })), + ...x.relatedRoles.map(y => ({ nodeKind: "link", linkKind: "relates", role: y })), ...x.ownedAttributes.map(y => ({ nodeKind: "link", linkKind: "owns", ownedAttribute: y })), - ...x.playableRoles.map(y => ({ nodeKind: "link", linkKind: "plays", role: y })), + ...x.playedRoles.map(y => ({ nodeKind: "link", linkKind: "plays", role: y })), ] as SchemaTreeLinkNode[]), })), }, { nodeKind: "root", label: "Attributes", - children: schema.attributes.map(x => ({ + children: Object.values(schema.attributes).sort((a, b) => a.label.localeCompare(b.label)).map(x => ({ nodeKind: "concept", concept: x, children: ([ diff --git a/src/service/schema-state.service.ts b/src/service/schema-state.service.ts index 1198b1fb..6e189dc1 100644 --- a/src/service/schema-state.service.ts +++ b/src/service/schema-state.service.ts @@ -23,8 +23,8 @@ const NO_DATABASE_SELECTED = `No database selected`; const schemaQueries = { typeHierarchy: `match { $t sub! $supertype; } or {$t sub $supertype; $t is $supertype; };`, ownedAttributes: `match { $t owns $attr; not { $t sub! $sown; $sown owns $attr; }; };`, - roleplayers: `match { $t relates $related; not { $t sub! $srel; $srel relates $related; }; };`, - playableRoles: `match { $t plays $played; not { $t sub! $splay; $splay plays $played; }; };`, + relatedRoles: `match { $t relates $related; not { $t sub! $srel; $srel relates $related; }; };`, + playedRoles: `match { $t plays $played; not { $t sub! $splay; $splay plays $played; }; };`, } as const satisfies Record; const schemaQueriesList = Object.values(schemaQueries); @@ -34,15 +34,15 @@ interface SchemaEntity extends EntityType { supertype?: SchemaEntity; subtypes: SchemaEntity[]; ownedAttributes: SchemaAttribute[]; - playableRoles: SchemaRole[]; + playedRoles: SchemaRole[]; } interface SchemaRelation extends RelationType { supertype?: SchemaRelation; subtypes: SchemaRelation[]; ownedAttributes: SchemaAttribute[]; - playableRoles: SchemaRole[]; - roleplayers: SchemaRole[]; + playedRoles: SchemaRole[]; + relatedRoles: SchemaRole[]; } export interface SchemaAttribute extends AttributeType { @@ -55,9 +55,9 @@ export type SchemaConcept = SchemaEntity | SchemaRelation | SchemaAttribute; export type SchemaRole = RoleType; export interface Schema { - entities: SchemaEntity[]; - relations: SchemaRelation[]; - attributes: SchemaAttribute[]; + entities: Record; + relations: Record; + attributes: Record; } @Injectable({ @@ -169,7 +169,7 @@ function entityOf(entityType: EntityType): SchemaEntity { supertype: undefined, subtypes: [], ownedAttributes: [], - playableRoles: [], + playedRoles: [], }; } @@ -180,8 +180,8 @@ function relationOf(relationType: RelationType): SchemaRelation { supertype: undefined, subtypes: [], ownedAttributes: [], - playableRoles: [], - roleplayers: [], + playedRoles: [], + relatedRoles: [], }; } @@ -198,30 +198,30 @@ function attributeOf(attributeType: AttributeType): SchemaAttribute { class SchemaBuilder { readonly typeHierarchy: ConceptRowsQueryResponse; readonly ownedAttributes: ConceptRowsQueryResponse; - readonly roleplayers: ConceptRowsQueryResponse; - readonly playableRoles: ConceptRowsQueryResponse; + readonly relatedRoles: ConceptRowsQueryResponse; + readonly playedRoles: ConceptRowsQueryResponse; readonly entityTypes = {} as Record; readonly relationTypes = {} as Record; readonly attributeTypes = {} as Record; constructor(data: ApiOkResponse[]) { - const [typeHierarchy, ownedAttributes, roleplayers, playableRoles] = data.map(x => x.ok); + const [typeHierarchy, ownedAttributes, relatedRoles, playedRoles] = data.map(x => x.ok); this.typeHierarchy = typeHierarchy; this.ownedAttributes = ownedAttributes; - this.roleplayers = roleplayers; - this.playableRoles = playableRoles; + this.relatedRoles = relatedRoles; + this.playedRoles = playedRoles; } build(): Schema { this.populateConcepts(); this.buildTypeHierarchy(); this.attachOwnedAttributes(); - this.attachPlayableRoles(); - this.attachRoleplayers(); + this.attachPlayedRoles(); + this.attachRelatedRoles(); return { - entities: Object.values(this.entityTypes), - relations: Object.values(this.relationTypes), - attributes: Object.values(this.attributeTypes), + entities: this.entityTypes, + relations: this.relationTypes, + attributes: this.attributeTypes, }; } @@ -303,8 +303,8 @@ class SchemaBuilder { } } - private attachRoleplayers() { - for (const answer of this.roleplayers.answers) { + private attachRelatedRoles() { + for (const answer of this.relatedRoles.answers) { const [rel, role] = [answer.data["t"], answer.data["related"]]; if (!rel || !role || rel.kind !== "relationType" || role.kind !== "roleType") throw this.unexpectedRoleplayersAnswer(answer); const relNode: SchemaRelation = this.expectRelationType(rel.label); @@ -313,16 +313,16 @@ class SchemaBuilder { } private propagateRoleplayers(relNode: SchemaRelation, role: RoleType) { - relNode.roleplayers.push(role); + relNode.relatedRoles.push(role); for (const relSubnode of relNode.subtypes) { this.propagateRoleplayers(relSubnode, role); } } - private attachPlayableRoles() { - for (const answer of this.playableRoles.answers) { + private attachPlayedRoles() { + for (const answer of this.playedRoles.answers) { const [obj, role] = [answer.data["t"], answer.data["played"]]; - if (!obj || !role || role.kind !== "roleType") throw this.unexpectedPlayableRolesAnswer(answer); + if (!obj || !role || role.kind !== "roleType") throw this.unexpectedPlayedRolesAnswer(answer); let objNode: SchemaEntity | SchemaRelation; switch (obj.kind) { case "entityType": @@ -332,16 +332,16 @@ class SchemaBuilder { objNode = this.expectRelationType(obj.label); break; default: - throw this.unexpectedPlayableRolesAnswer(answer); + throw this.unexpectedPlayedRolesAnswer(answer); } - this.propagatePlayableRoles(objNode, role); + this.propagatePlayedRoles(objNode, role); } } - private propagatePlayableRoles(objNode: SchemaEntity | SchemaRelation, role: RoleType) { - objNode.playableRoles.push(role); + private propagatePlayedRoles(objNode: SchemaEntity | SchemaRelation, role: RoleType) { + objNode.playedRoles.push(role); for (const objSubnode of objNode.subtypes) { - this.propagatePlayableRoles(objSubnode, role); + this.propagatePlayedRoles(objSubnode, role); } } @@ -371,12 +371,12 @@ class SchemaBuilder { return `Unexpected owned attributes answer: ${JSON.stringify(answer.data)}`; } - private unexpectedPlayableRolesAnswer(answer: ConceptRowsQueryResponse["answers"][number]) { - return `Unexpected playable roles answer: ${JSON.stringify(answer.data)}`; + private unexpectedPlayedRolesAnswer(answer: ConceptRowsQueryResponse["answers"][number]) { + return `Unexpected played roles answer: ${JSON.stringify(answer.data)}`; } private unexpectedRoleplayersAnswer(answer: ConceptRowsQueryResponse["answers"][number]) { - return `Unexpected roleplayers answer: ${JSON.stringify(answer.data)}`; + return `Unexpected related roles answer: ${JSON.stringify(answer.data)}`; } } From 9cd9551203338fa6f3af61e3a0ac383b2345acb2 Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Wed, 18 Jun 2025 16:25:42 +0100 Subject: [PATCH 04/11] Clear schema tool window on unloading schema --- src/service/query-tool-state.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/query-tool-state.service.ts b/src/service/query-tool-state.service.ts index c76bdcb4..a3dc420d 100644 --- a/src/service/query-tool-state.service.ts +++ b/src/service/query-tool-state.service.ts @@ -206,7 +206,7 @@ export class SchemaWindowState { private populateDataSources(schema: Schema | null) { if (!schema) { - // this.dataSources.forEach(x => x.dataSource.data = []); + this.dataSource.length = 0; return; } this.dataSource = [{ From 1db29e8cef398da2795ff88e9016dc91664f9bc2 Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Wed, 18 Jun 2025 16:27:53 +0100 Subject: [PATCH 05/11] Clean up comments --- src/service/query-tool-state.service.ts | 606 ------------------------ 1 file changed, 606 deletions(-) diff --git a/src/service/query-tool-state.service.ts b/src/service/query-tool-state.service.ts index a3dc420d..42a91c74 100644 --- a/src/service/query-tool-state.service.ts +++ b/src/service/query-tool-state.service.ts @@ -187,12 +187,6 @@ export type SchemaTreeNode = SchemaTreeRootNode | SchemaTreeConceptNode | Schema export class SchemaWindowState { dataSource: SchemaTreeRootNode[] = []; - // dataSourcesObj: Record<"Entities" | "Relations" | "Attributes", { title: string, treeControl: FlatTreeControl, dataSource: MatTreeFlatDataSource }> = { - // "Entities": { title: "Entities", treeControl: this.entitiesTreeControl, dataSource: new MatTreeFlatDataSource(this.entitiesTreeControl, this.treeFlattener) }, - // "Relations": { title: "Relations", treeControl: this.relationsTreeControl, dataSource: new MatTreeFlatDataSource(this.relationsTreeControl, this.treeFlattener) }, - // "Attributes": { title: "Attributes", treeControl: this.attributesTreeControl, dataSource: new MatTreeFlatDataSource(this.attributesTreeControl, this.treeFlattener) }, - // }; - // dataSources = Object.values(this.dataSourcesObj); constructor(public schemaState: SchemaState) { schemaState.value$.subscribe(schema => { @@ -617,603 +611,3 @@ export class RawOutputState { this.control.patchValue(``); } } - -/* -class QueryRunner constructor( - val transactionState: TransactionState, // TODO: restrict in the future, when TypeDB 3.0 answers return complete info - private val notificationSrv: NotificationService, - private val preferenceSrv: PreferenceService, - private val queries: String, - private val onComplete: () -> Unit -) { - - sealed class Response { - - object Done : Response() - - data class Message(val type: Type, val text: String) : Response() { - enum class Type { INFO, SUCCESS, ERROR, TYPEQL } - } - - data class Value(val value: com.vaticle.typedb.driver.api.concept.value.Value?) : Response() - - sealed class Stream : Response() { - - val queue = LinkedBlockingQueue>() - - class ConceptMapGroups : Stream() - class ValueGroups : Stream() - class JSONs : Stream() - class ConceptMaps constructor(val source: Source) : Stream() { - enum class Source { INSERT, UPDATE, GET } - } - } -} - - companion object { - const val RESULT_ = "## Result> " - const val ERROR_ = "## Error> " - const val RUNNING_ = "## Running> " - const val COMPLETED = "## Completed" - const val TERMINATED = "## Terminated" - const val DEFINE_QUERY = "Define query:" - const val DEFINE_QUERY_SUCCESS = "Define query successfully defined new types in the schema." - const val UNDEFINE_QUERY = "Undefine query:" - const val UNDEFINE_QUERY_SUCCESS = "Undefine query successfully undefined types in the schema." - const val DELETE_QUERY = "Delete query:" - const val DELETE_QUERY_SUCCESS = "Delete query successfully deleted things from the database." - const val INSERT_QUERY = "Insert query:" - const val INSERT_QUERY_SUCCESS = "Insert query successfully inserted new things to the database:" - const val INSERT_QUERY_NO_RESULT = "Insert query did not insert any new thing to the database." - const val UPDATE_QUERY = "Update query:" - const val UPDATE_QUERY_SUCCESS = "Update query successfully updated things in the databases:" - const val UPDATE_QUERY_NO_RESULT = "Update query did not update any thing in the databases." - const val GET_QUERY = "Get query:" - const val GET_QUERY_SUCCESS = "Get query successfully matched concepts in the database:" - const val GET_QUERY_NO_RESULT = "Get query did not match any concepts in the database." - const val GET_AGGREGATE_QUERY = "Get Aggregate query:" - const val GET_AGGREGATE_QUERY_SUCCESS = "Get Aggregate query successfully calculated:" - const val GET_GROUP_QUERY = "Get Group query:" - const val GET_GROUP_QUERY_SUCCESS = "Get Group query successfully matched concept groups in the database:" - const val GET_GROUP_QUERY_NO_RESULT = "Get Group query did not match any concept groups in the database." - const val GET_GROUP_AGGREGATE_QUERY = "Get Group Aggregate query:" - const val GET_GROUP_AGGREGATE_QUERY_SUCCESS = - "Get Group Aggregate query successfully aggregated matched concept groups in the database:" - const val GET_GROUP_AGGREGATE_QUERY_NO_RESULT = - "Get Group Aggregate query did not match any concept groups to aggregate in the database." - const val FETCH_QUERY = "Fetch query:" - const val FETCH_QUERY_SUCCESS = "Fetch query successfully retrieved data from the database:" - const val FETCH_QUERY_NO_RESULT = "Fetch query did not retrieve any data from the database." - - private const val COUNT_DOWN_LATCH_PERIOD_MS: Long = 50 - private val LOGGER = KotlinLogging.logger {} - } - - var startTime: Long? = null - var endTime: Long? = null - val responses = LinkedBlockingQueue() - val isConsumed: Boolean get() = consumerLatch.count == 0L - val isRunning = AtomicBoolean(false) -private val consumerLatch = CountDownLatch(1) -private val coroutines = CoroutineScope(Dispatchers.Default) -private val hasStopSignal get() = transactionState.hasStopSignalAtomic -private val transaction get() = transactionState.transaction!! -private val onClose = LinkedBlockingQueue<() -> Unit>() - - fun onClose(function: () -> Unit) = onClose.put(function) - - fun setConsumed() = consumerLatch.countDown() - -private fun collectEmptyLine() = collectMessage(INFO, "") - -private fun collectMessage(type: Response.Message.Type, string: String) { - responses.put(Response.Message(type, string)) - } - - internal fun launch() = coroutines.launchAndHandle(notificationSrv, LOGGER) { - try { - isRunning.set(true) - startTime = System.currentTimeMillis() - runQueries(TypeQL.parseQueries(queries).toList()) - } catch (e: Exception) { - collectEmptyLine() - collectMessage(ERROR, ERROR_ + e.message) - } finally { - endTime = System.currentTimeMillis() - isRunning.set(false) - responses.add(Response.Done) - var isConsumed: Boolean - if (!hasStopSignal.state) { - do { - isConsumed = consumerLatch.count == 0L - if (!isConsumed) delay(COUNT_DOWN_LATCH_PERIOD_MS) - } while (!isConsumed && !hasStopSignal.state) - } - onComplete() - } - } - -private fun runQueries(queries: List) = queries.forEach { query -> - if (hasStopSignal.state) return@forEach - when (query) { - is TypeQLDefine -> runDefineQuery(query) - is TypeQLUndefine -> runUndefineQuery(query) - is TypeQLDelete -> runDeleteQuery(query) - is TypeQLInsert -> runInsertQuery(query) - is TypeQLUpdate -> runUpdateQuery(query) - is TypeQLGet -> runGetQuery(query) - is TypeQLGet.Aggregate -> runGetAggregateQuery(query) - is TypeQLGet.Group -> runGetGroupQuery(query) - is TypeQLGet.Group.Aggregate -> runGetGroupAggregateQuery(query) - is TypeQLFetch -> runFetchQuery(query) - else -> throw IllegalStateException("Unrecognised TypeQL query") - } - } - -private fun runDefineQuery(query: TypeQLDefine) = runUnitQuery( - name = DEFINE_QUERY, - successMsg = DEFINE_QUERY_SUCCESS, - queryStr = query.toString() - ) { transaction.query().define(query).resolve() } - -private fun runUndefineQuery(query: TypeQLUndefine) = runUnitQuery( - name = UNDEFINE_QUERY, - successMsg = UNDEFINE_QUERY_SUCCESS, - queryStr = query.toString() - ) { transaction.query().undefine(query).resolve() } - -private fun runDeleteQuery(query: TypeQLDelete) = runUnitQuery( - name = DELETE_QUERY, - successMsg = DELETE_QUERY_SUCCESS, - queryStr = query.toString() - ) { transaction.query().delete(query).resolve() } - -private fun runInsertQuery(query: TypeQLInsert) = runStreamingQuery( - name = INSERT_QUERY, - successMsg = INSERT_QUERY_SUCCESS, - noResultMsg = INSERT_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.ConceptMaps(INSERT) - ) { transaction.query().insert(query, transactionState.defaultTypeDBOptions().prefetch(true)) } - -private fun runUpdateQuery(query: TypeQLUpdate) = runStreamingQuery( - name = UPDATE_QUERY, - successMsg = UPDATE_QUERY_SUCCESS, - noResultMsg = UPDATE_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.ConceptMaps(UPDATE) - ) { transaction.query().update(query, transactionState.defaultTypeDBOptions().prefetch(true)) } - -private fun runGetQuery(query: TypeQLGet) = runStreamingQuery( - name = GET_QUERY, - successMsg = GET_QUERY_SUCCESS, - noResultMsg = GET_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.ConceptMaps(GET) - ) { - if (query.modifiers().limit().isPresent) { - transaction.query().get(query) - } else { - val queryWithLimit = TypeQLGet.Limited(query, preferenceSrv.getQueryLimit) - transaction.query().get(queryWithLimit) - } - } - -private fun runGetAggregateQuery(query: TypeQLGet.Aggregate) { - printQueryStart(GET_AGGREGATE_QUERY, query.toString()) - val result = transaction.query().get(query).resolve().orElse(null) - collectEmptyLine() - collectMessage(SUCCESS, RESULT_ + GET_AGGREGATE_QUERY_SUCCESS) - responses.put(Response.Value(result)) - } - -private fun runGetGroupQuery(query: TypeQLGet.Group) = runStreamingQuery( - name = GET_GROUP_QUERY, - successMsg = GET_GROUP_QUERY_SUCCESS, - noResultMsg = GET_GROUP_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.ConceptMapGroups() - ) { transaction.query().get(query) } - -private fun runGetGroupAggregateQuery(query: TypeQLGet.Group.Aggregate) = runStreamingQuery( - name = GET_GROUP_AGGREGATE_QUERY, - successMsg = GET_GROUP_AGGREGATE_QUERY_SUCCESS, - noResultMsg = GET_GROUP_AGGREGATE_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.ValueGroups() - ) { transaction.query().get(query) } - -private fun runFetchQuery(query: TypeQLFetch) = runStreamingQuery( - name = FETCH_QUERY, - successMsg = FETCH_QUERY_SUCCESS, - noResultMsg = FETCH_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.JSONs() - ) { - if (query.modifiers().limit().isPresent) { - transaction.query().fetch(query) - } else { - val queryWithLimit = TypeQLFetch.Limited(query, preferenceSrv.getQueryLimit) - transaction.query().fetch(queryWithLimit) - } - } - -private fun runUnitQuery(name: String, successMsg: String, queryStr: String, queryFn: () -> Unit) { - printQueryStart(name, queryStr) - queryFn() - collectEmptyLine() - collectMessage(SUCCESS, RESULT_ + successMsg) - } - -private fun runStreamingQuery( - name: String, - successMsg: String, - noResultMsg: String, - queryStr: String, - stream: Response.Stream, - queryFn: () -> Stream -) { - printQueryStart(name, queryStr) - collectResponseStream(queryFn(), successMsg, noResultMsg, stream) - } - -private fun printQueryStart(name: String, queryStr: String) { - collectEmptyLine() - collectMessage(INFO, RUNNING_ + name) - collectMessage(TYPEQL, queryStr) - } - -private fun collectResponseStream( - results: Stream, - successMsg: String, - noResultMsg: String, - stream: Response.Stream -) { - var started = false - var error = false - try { - collectEmptyLine() - results.peek { - if (started) return@peek - collectMessage(SUCCESS, RESULT_ + successMsg) - responses.put(stream) - started = true - }.forEach { - if (hasStopSignal.state) return@forEach - stream.queue.put(Either.first(it)) - } - } catch (e: Exception) { - collectMessage(ERROR, ERROR_ + e.message) - error = true - } finally { - if (started) stream.queue.put(Either.second(Response.Done)) - if (error || hasStopSignal.state) collectMessage(ERROR, TERMINATED) - else if (started) collectMessage(INFO, COMPLETED) - else collectMessage(SUCCESS, RESULT_ + noResultMsg) - } - } - - fun close() { - hasStopSignal.set(true) - onClose.forEach { it() } - } -} - */ - -/* -class QueryRunner( - val transactionState: TransactionState, // TODO: restrict in the future, when TypeDB 3.0 answers return complete info - private val notificationSrv: NotificationService, - private val preferenceSrv: PreferenceService, - private val queries: String, - private val onComplete: () -> Unit -) { - - sealed class Response { - - object Done : Response() - - data class Message(val type: Type, val text: String) : Response() { - enum class Type { INFO, SUCCESS, ERROR, TYPEQL } - } - - data class Value(val value: com.typedb.driver.api.concept.value.Value) : Response() - - sealed class Stream : Response() { - - val queue = LinkedBlockingQueue>() - - class JSONs : Stream() - class ConceptRows : Stream() - } -} - - companion object { - const val RESULT_ = "## Result> " - const val ERROR_ = "## Error> " - const val RUNNING_ = "## Running> " - const val COMPLETED = "## Completed" - const val TERMINATED = "## Terminated" - const val QUERY = "Query:" - const val QUERY_SUCCESS = "Success." - const val QUERY_NO_RESULT = "Query returned no results." - const val DEFINE_QUERY = "Define query:" - const val DEFINE_QUERY_SUCCESS = "Define query successfully defined new types in the schema." - const val UNDEFINE_QUERY = "Undefine query:" - const val UNDEFINE_QUERY_SUCCESS = "Undefine query successfully undefined types in the schema." - const val DELETE_QUERY = "Delete query:" - const val DELETE_QUERY_SUCCESS = "Delete query successfully deleted things from the database." - const val INSERT_QUERY = "Insert query:" - const val INSERT_QUERY_SUCCESS = "Insert query successfully inserted new things to the database:" - const val INSERT_QUERY_NO_RESULT = "Insert query did not insert any new thing to the database." - const val UPDATE_QUERY = "Update query:" - const val UPDATE_QUERY_SUCCESS = "Update query successfully updated things in the databases:" - const val UPDATE_QUERY_NO_RESULT = "Update query did not update any thing in the databases." - const val GET_QUERY = "Get query:" - const val GET_QUERY_SUCCESS = "Get query successfully matched concepts in the database:" - const val GET_QUERY_NO_RESULT = "Get query did not match any concepts in the database." - const val GET_AGGREGATE_QUERY = "Get Aggregate query:" - const val GET_AGGREGATE_QUERY_SUCCESS = "Get Aggregate query successfully calculated:" - const val GET_GROUP_QUERY = "Get Group query:" - const val GET_GROUP_QUERY_SUCCESS = "Get Group query successfully matched concept groups in the database:" - const val GET_GROUP_QUERY_NO_RESULT = "Get Group query did not match any concept groups in the database." - const val GET_GROUP_AGGREGATE_QUERY = "Get Group Aggregate query:" - const val GET_GROUP_AGGREGATE_QUERY_SUCCESS = - "Get Group Aggregate query successfully aggregated matched concept groups in the database:" - const val GET_GROUP_AGGREGATE_QUERY_NO_RESULT = - "Get Group Aggregate query did not match any concept groups to aggregate in the database." - const val FETCH_QUERY = "Fetch query:" - const val FETCH_QUERY_SUCCESS = "Fetch query successfully retrieved data from the database:" - const val FETCH_QUERY_NO_RESULT = "Fetch query did not retrieve any data from the database." - - private const val COUNT_DOWN_LATCH_PERIOD_MS: Long = 50 - private val LOGGER = KotlinLogging.logger {} - } - - var startTime: Long? = null - var endTime: Long? = null - val responses = LinkedBlockingQueue() - val isConsumed: Boolean get() = consumerLatch.count == 0L - val isRunning = AtomicBoolean(false) -private val consumerLatch = CountDownLatch(1) -private val coroutines = CoroutineScope(Dispatchers.Default) -private val hasStopSignal get() = transactionState.hasStopSignal -private val transaction get() = transactionState.transaction!! -private val onClose = LinkedBlockingQueue<() -> Unit>() - - fun onClose(function: () -> Unit) = onClose.put(function) - - fun setConsumed() = consumerLatch.countDown() - -private fun collectEmptyLine() = collectMessage(INFO, "") - -private fun collectMessage(type: Response.Message.Type, string: String) { - responses.put(Response.Message(type, string)) - } - - internal fun launch() = coroutines.launchAndHandle(notificationSrv, LOGGER) { - try { - isRunning.set(true) - startTime = System.currentTimeMillis() - runQuery(queries) -// runQueries(TypeQL.parseQueries(queries).collect(Collectors.toList())) - } catch (e: Exception) { - collectEmptyLine() - collectMessage(ERROR, ERROR_ + e.message) - } finally { - endTime = System.currentTimeMillis() - isRunning.set(false) - responses.add(Response.Done) - var isConsumed: Boolean - if (!hasStopSignal) { - do { - isConsumed = consumerLatch.count == 0L - if (!isConsumed) delay(COUNT_DOWN_LATCH_PERIOD_MS) - } while (!isConsumed && !hasStopSignal) - } - onComplete() - } - } - -private fun runQuery(query: String) { - if (hasStopSignal) return - - collectEmptyLine() - collectMessage(INFO, RUNNING_) - collectMessage(TYPEQL, query) - - val answer = transaction.query(query).resolve() - - if (answer.isOk) { - collectEmptyLine() - collectMessage(SUCCESS, RESULT_ + QUERY_SUCCESS) - return - } else if (answer.isConceptRows) { - val streamRaw = answer.asConceptRows().stream() - val stream = Response.Stream.ConceptRows() - - var started = false - var error = false - try { - collectEmptyLine() - streamRaw.peek { - if (started) return@peek - collectMessage(SUCCESS, RESULT_ + QUERY_SUCCESS) - responses.put(stream) - started = true - }.forEach { - if (hasStopSignal) return@forEach - stream.queue.put(Either.first(it)) - } - } catch (e: Exception) { - collectMessage(ERROR, ERROR_ + e.message) - error = true - } finally { - if (started) stream.queue.put(Either.second(Response.Done)) - if (error || hasStopSignal) collectMessage(ERROR, TERMINATED) - else if (started) collectMessage(INFO, COMPLETED) - else collectMessage(SUCCESS, RESULT_ + QUERY_NO_RESULT) - } - } else if (answer.isConceptDocuments) { - val streamRaw = answer.asConceptDocuments().stream() - val stream = Response.Stream.JSONs() - - var started = false - var error = false - try { - collectEmptyLine() - streamRaw.peek { - if (started) return@peek - collectMessage(SUCCESS, RESULT_ + QUERY_SUCCESS) - responses.put(stream) - started = true - }.forEach { - if (hasStopSignal) return@forEach - stream.queue.put(Either.first(it)) - } - } catch (e: Exception) { - collectMessage(ERROR, ERROR_ + e.message) - error = true - } finally { - if (started) stream.queue.put(Either.second(Response.Done)) - if (error || hasStopSignal) collectMessage(ERROR, TERMINATED) - else if (started) collectMessage(INFO, COMPLETED) - else collectMessage(SUCCESS, RESULT_ + QUERY_SUCCESS) - } - } else throw IllegalArgumentException() - } - -private fun runDefineQuery(query: TypeQLDefine) = runUnitQuery( - name = DEFINE_QUERY, - successMsg = DEFINE_QUERY_SUCCESS, - queryStr = query.toString() - ) { transaction.query(query.toString()).resolve() } - -private fun runUndefineQuery(query: TypeQLUndefine) = runUnitQuery( - name = UNDEFINE_QUERY, - successMsg = UNDEFINE_QUERY_SUCCESS, - queryStr = query.toString() - ) { transaction.query(query.toString()).resolve() } - -private fun runDeleteQuery(query: TypeQLDelete) = runUnitQuery( - name = DELETE_QUERY, - successMsg = DELETE_QUERY_SUCCESS, - queryStr = query.toString() - ) { transaction.query(query.toString()).resolve() } - -private fun runInsertQuery(query: TypeQLInsert) = runStreamingQuery( - name = INSERT_QUERY, - successMsg = INSERT_QUERY_SUCCESS, - noResultMsg = INSERT_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.ConceptRows() - ) { transaction.query(query.toString()).resolve().asConceptRows().stream() } // TODO: prefetch = true option - -private fun runUpdateQuery(query: TypeQLUpdate) = runStreamingQuery( - name = UPDATE_QUERY, - successMsg = UPDATE_QUERY_SUCCESS, - noResultMsg = UPDATE_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.ConceptRows() - ) { transaction.query(query.toString()).resolve().asConceptRows().stream() } - -private fun runGetQuery(query: TypeQLGet) = runStreamingQuery( - name = GET_QUERY, - successMsg = GET_QUERY_SUCCESS, - noResultMsg = GET_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.ConceptRows() - ) { -// if (query.modifiers().limit().isPresent) { - transaction.query(query.toString()).resolve().asConceptRows().stream() -// } else { -// val queryWithLimit = TypeQLGet.Limited(query, preferenceSrv.getQueryLimit) -// transaction.query().get(queryWithLimit) -// } - } - -private fun runGetAggregateQuery(query: TypeQLGet.Aggregate) { - collectMessage(INFO, "runGetAggregateQuery: unsupported") -// printQueryStart(GET_AGGREGATE_QUERY, query.toString()) -// val result = transaction.query(query.toString()).resolve() -// collectEmptyLine() -// collectMessage(SUCCESS, RESULT_ + GET_AGGREGATE_QUERY_SUCCESS) -// responses.put(Response.Value(result)) - } - -private fun runFetchQuery(query: TypeQLFetch) = runStreamingQuery( - name = FETCH_QUERY, - successMsg = FETCH_QUERY_SUCCESS, - noResultMsg = FETCH_QUERY_NO_RESULT, - queryStr = query.toString(), - stream = Response.Stream.JSONs() - ) { -// if (query.modifiers().limit().isPresent) { - transaction.query(query.toString()).resolve().asConceptDocuments().stream() -// } else { -// val queryWithLimit = TypeQLFetch.Limited(query, preferenceSrv.getQueryLimit) -// transaction.query().fetch(queryWithLimit) -// } - } - -private fun runUnitQuery(name: String, successMsg: String, queryStr: String, queryFn: () -> Unit) { - printQueryStart(name, queryStr) - queryFn() - collectEmptyLine() - collectMessage(SUCCESS, RESULT_ + successMsg) - } - -private fun runStreamingQuery( - name: String, - successMsg: String, - noResultMsg: String, - queryStr: String, - stream: Response.Stream, - queryFn: () -> Stream -) { - printQueryStart(name, queryStr) - collectResponseStream(queryFn(), successMsg, noResultMsg, stream) - } - -private fun printQueryStart(name: String, queryStr: String) { - collectEmptyLine() - collectMessage(INFO, RUNNING_ + name) - collectMessage(TYPEQL, queryStr) - } - -private fun collectResponseStream( - results: Stream, - successMsg: String, - noResultMsg: String, - stream: Response.Stream -) { - var started = false - var error = false - try { - collectEmptyLine() - results.peek { - if (started) return@peek - collectMessage(SUCCESS, RESULT_ + successMsg) - responses.put(stream) - started = true - }.forEach { - if (hasStopSignal) return@forEach - stream.queue.put(Either.first(it)) - } - } catch (e: Exception) { - collectMessage(ERROR, ERROR_ + e.message) - error = true - } finally { - if (started) stream.queue.put(Either.second(Response.Done)) - if (error || hasStopSignal) collectMessage(ERROR, TERMINATED) - else if (started) collectMessage(INFO, COMPLETED) - else collectMessage(SUCCESS, RESULT_ + noResultMsg) - } - } - - fun close() { - transactionState.sendStopSignal() - onClose.forEach { it() } - } -} - */ From 7ff002ab9abc353ef38701a25ab385176ea4ea17 Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Wed, 18 Jun 2025 16:30:06 +0100 Subject: [PATCH 06/11] Typo squatting --- src/service/schema-state.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/service/schema-state.service.ts b/src/service/schema-state.service.ts index 6e189dc1..ad17e5b4 100644 --- a/src/service/schema-state.service.ts +++ b/src/service/schema-state.service.ts @@ -77,7 +77,7 @@ export class SchemaState { readonly refreshEnabled$ = this.refreshDisabledReason$.pipe(map(x => x == null)); constructor(private driver: DriverState, private snackbar: SnackbarService) { - (window as any)["schemaToolState"] = this; + (window as any)["schemaState"] = this; this.driver.database$.pipe( distinctUntilChanged((x, y) => x?.name === y?.name) ).subscribe(() => { @@ -308,14 +308,14 @@ class SchemaBuilder { const [rel, role] = [answer.data["t"], answer.data["related"]]; if (!rel || !role || rel.kind !== "relationType" || role.kind !== "roleType") throw this.unexpectedRoleplayersAnswer(answer); const relNode: SchemaRelation = this.expectRelationType(rel.label); - this.propagateRoleplayers(relNode, role); + this.propagateRelatedRoles(relNode, role); } } - private propagateRoleplayers(relNode: SchemaRelation, role: RoleType) { + private propagateRelatedRoles(relNode: SchemaRelation, role: RoleType) { relNode.relatedRoles.push(role); for (const relSubnode of relNode.subtypes) { - this.propagateRoleplayers(relSubnode, role); + this.propagateRelatedRoles(relSubnode, role); } } From 55b453399d85bf53b719464d28bda0de87ef18be Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Wed, 18 Jun 2025 18:22:45 +0100 Subject: [PATCH 07/11] Schema tool window scrolling behaviour --- src/module/query/query-tool.component.html | 48 +++------------- src/module/query/query-tool.component.scss | 59 ++++++++++++++------ src/module/schema/schema-tool.component.scss | 8 +-- styles/base.scss | 2 +- 4 files changed, 52 insertions(+), 65 deletions(-) diff --git a/src/module/query/query-tool.component.html b/src/module/query/query-tool.component.html index 1120b3f3..e2a2a271 100644 --- a/src/module/query/query-tool.component.html +++ b/src/module/query/query-tool.component.html @@ -1,11 +1,14 @@
    -
    - -

    Schema

    +
    +
    + +

    Schema

    +
    +
    @@ -23,44 +26,7 @@

    Schema

    -
      - @for (entry of state.history.entries; track entry) { -
    1. - - @if (isQueryRun(entry)) { - - - - } - - - -
    2. - } -
    +
    diff --git a/src/module/query/query-tool.component.scss b/src/module/query/query-tool.component.scss index 2326cf79..5ca78762 100644 --- a/src/module/query/query-tool.component.scss +++ b/src/module/query/query-tool.component.scss @@ -10,23 +10,8 @@ @use "typography"; @use "shapes"; -:host { - height: 100%; -} - -.query-page-rough { - height: 100%; - margin-top: 16px; - margin-bottom: 16px; - display: flex; - flex-direction: column; - gap: 16px; -} - .query-page { - height: 100%; - margin-top: 16px; - margin-bottom: 16px; + height: calc(100% - 86px); gap: 4px !important; ::ng-deep app-drag-handle span { @@ -66,6 +51,18 @@ @include shapes.light-source-gradient(primary.$purple, primary.$deep-purple); transition: background 0.1s linear; padding: 16px; + + &:has(.card-header-wrapper) { + padding: 0; + } +} + +.card-header-wrapper { + padding: 16px; + + .card-header { + margin-bottom: 0; + } } .card-header { @@ -76,6 +73,36 @@ height: 32px; } +.schema-pane { + display: flex; + flex-direction: column; +} + +.schema-container { + display: flex; + overflow: auto; + scrollbar-gutter: stable; + overscroll-behavior: contain; + + .gutter { + flex: 0 0 16px; + } + + mat-tree { + padding: 0; + box-sizing: border-box; + height: 100%; + margin: 0; + + .mat-mdc-icon-button { + margin: 0; + padding: 0; + width: 1em; + height: 1em; + } + } +} + mat-form-field { width: 100%; } diff --git a/src/module/schema/schema-tool.component.scss b/src/module/schema/schema-tool.component.scss index 9dcd7f2c..db43a3ef 100644 --- a/src/module/schema/schema-tool.component.scss +++ b/src/module/schema/schema-tool.component.scss @@ -10,14 +10,8 @@ @use "typography"; @use "shapes"; -:host { - height: 100%; -} - .schema-page { - height: 100%; - margin-top: 16px; - margin-bottom: 16px; + height: calc(100% - 86px); gap: 4px !important; ::ng-deep app-drag-handle span { diff --git a/styles/base.scss b/styles/base.scss index fc8dc302..abf16247 100644 --- a/styles/base.scss +++ b/styles/base.scss @@ -509,7 +509,7 @@ button i + span, button span + i { .action-bar { flex: 0 0 49px; display: flex; - margin: 16px 0 0 0; + padding: 19px 0; position: sticky; top: 0; background-color: primary.$black-purple; From 6947f73b392799c80a6d9e493b8a0fa10e31623c Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Wed, 18 Jun 2025 18:42:58 +0100 Subject: [PATCH 08/11] Schema tool window: add top border to scrollable area when scrolled --- .../scroll-container/detect-scroll.directive.ts | 12 ++++++++++++ src/module/query/query-tool.component.html | 2 +- src/module/query/query-tool.component.scss | 4 ++++ src/module/query/query-tool.component.ts | 3 ++- 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/framework/scroll-container/detect-scroll.directive.ts diff --git a/src/framework/scroll-container/detect-scroll.directive.ts b/src/framework/scroll-container/detect-scroll.directive.ts new file mode 100644 index 00000000..35a40b22 --- /dev/null +++ b/src/framework/scroll-container/detect-scroll.directive.ts @@ -0,0 +1,12 @@ +import { Directive, HostBinding, HostListener } from "@angular/core"; + +@Directive({ + selector: "[detectScroll]" +}) +export class DetectScrollDirective { + @HostBinding("class.scrolled") scrolled = false; + + @HostListener("scroll", ["$event.target"]) onScroll(target: any) { + this.scrolled = target.scrollTop > 0; + } +} diff --git a/src/module/query/query-tool.component.html b/src/module/query/query-tool.component.html index e2a2a271..7a367768 100644 --- a/src/module/query/query-tool.component.html +++ b/src/module/query/query-tool.component.html @@ -7,7 +7,7 @@

    Schema

    -
    +
    diff --git a/src/module/query/query-tool.component.scss b/src/module/query/query-tool.component.scss index 5ca78762..dcfb5985 100644 --- a/src/module/query/query-tool.component.scss +++ b/src/module/query/query-tool.component.scss @@ -88,6 +88,10 @@ flex: 0 0 16px; } + &.scrolled { + border-top: 1px solid primary.$black-purple; + } + mat-tree { padding: 0; box-sizing: border-box; diff --git a/src/module/query/query-tool.component.ts b/src/module/query/query-tool.component.ts index 22dae024..307f8eae 100644 --- a/src/module/query/query-tool.component.ts +++ b/src/module/query/query-tool.component.ts @@ -24,6 +24,7 @@ import { distinctUntilChanged, filter, first, map, startWith } from "rxjs"; import { otherExampleLinter, TypeQL } from "../../framework/codemirror-lang-typeql"; import { DriverAction, TransactionOperationAction, isQueryRun, isTransactionOperation } from "../../concept/action"; import { basicDark } from "../../framework/code-editor/theme"; +import { DetectScrollDirective } from "../../framework/scroll-container/detect-scroll.directive"; import { SpinnerComponent } from "../../framework/spinner/spinner.component"; import { RichTooltipDirective } from "../../framework/tooltip/rich-tooltip.directive"; import { AppData } from "../../service/app-data.service"; @@ -40,7 +41,7 @@ import { SchemaTreeNodeComponent } from "./schema-tree-node/schema-tree-node.com imports: [ RouterLink, AsyncPipe, PageScaffoldComponent, MatDividerModule, MatFormFieldModule, MatTreeModule, MatIconModule, MatInputModule, FormsModule, ReactiveFormsModule, MatButtonToggleModule, CodeEditor, ResizableDirective, - DatePipe, SpinnerComponent, MatTableModule, MatSortModule, MatTooltipModule, MatButtonModule, RichTooltipDirective, SchemaTreeNodeComponent, + DatePipe, SpinnerComponent, MatTableModule, MatSortModule, MatTooltipModule, MatButtonModule, RichTooltipDirective, SchemaTreeNodeComponent, DetectScrollDirective, ] }) export class QueryToolComponent implements OnInit, AfterViewInit, OnDestroy { From dc5f5e5d841b81e2da56af36912cd2347d04e3d7 Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Fri, 20 Jun 2025 12:23:48 +0100 Subject: [PATCH 09/11] Make capabilities in schema tree look like a table --- src/module/query/query-tool.component.scss | 26 +++++++++++++++++++ .../schema-tree-node.component.html | 8 +++--- .../schema-tree-node.component.scss | 4 --- styles/material.scss | 4 +-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/module/query/query-tool.component.scss b/src/module/query/query-tool.component.scss index dcfb5985..33d4a099 100644 --- a/src/module/query/query-tool.component.scss +++ b/src/module/query/query-tool.component.scss @@ -104,6 +104,32 @@ width: 1em; height: 1em; } + + mat-tree-node[aria-level="3"] { + margin-left: 32px; + padding-left: 8px !important; /* overrides inline style from Angular Material */ + padding-right: 8px; + align-items: stretch; + border-bottom: 1px solid primary.$light-purple; + border-left: 1px solid primary.$light-purple; + border-right: 1px solid primary.$light-purple; + + button { + display: none; + } + + &:nth-child(odd) { + background: primary.$black-purple; + } + + &:nth-child(even) { + background: primary.$purple; + } + } + + mat-tree-node[aria-level="2"] + mat-tree-node[aria-level="3"] { + border-top: 1px solid primary.$light-purple; + } } } diff --git a/src/module/query/schema-tree-node/schema-tree-node.component.html b/src/module/query/schema-tree-node/schema-tree-node.component.html index 2893a61d..249729e4 100644 --- a/src/module/query/schema-tree-node/schema-tree-node.component.html +++ b/src/module/query/schema-tree-node/schema-tree-node.component.html @@ -8,16 +8,16 @@ @case ("link") { @switch (data.linkKind) { @case ("sub") { - sub {{ data.supertype.label }} +

    sub {{ data.supertype.label }}

    } @case ("owns") { - owns {{ data.ownedAttribute.label }}, value {{ data.ownedAttribute.valueType }} +

    owns {{ data.ownedAttribute.label }}, value {{ data.ownedAttribute.valueType }}

    } @case ("relates") { - relates {{ data.role.label }} +

    relates {{ data.role.label }}

    } @case ("plays") { - plays {{ data.role.label }} +

    plays {{ data.role.label }}

    } } } diff --git a/src/module/query/schema-tree-node/schema-tree-node.component.scss b/src/module/query/schema-tree-node/schema-tree-node.component.scss index 2326cf79..5b0296d8 100644 --- a/src/module/query/schema-tree-node/schema-tree-node.component.scss +++ b/src/module/query/schema-tree-node/schema-tree-node.component.scss @@ -10,10 +10,6 @@ @use "typography"; @use "shapes"; -:host { - height: 100%; -} - .query-page-rough { height: 100%; margin-top: 16px; diff --git a/styles/material.scss b/styles/material.scss index 37c8b03b..f45367e6 100644 --- a/styles/material.scss +++ b/styles/material.scss @@ -834,7 +834,7 @@ body { padding-left: 16px !important; /* overrides inline style from Angular Material */ } - mat-tree-node[aria-level="3"] { - padding-left: 32px !important; /* overrides inline style from Angular Material */ + mat-tree-node[mattreenodetoggle] { + cursor: pointer; } } From 5902b8b83bdc06f8f59c2caf0c315f2b087ebcc8 Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Mon, 23 Jun 2025 11:08:39 +0100 Subject: [PATCH 10/11] Make the table slightly more elegant --- src/module/query/query-tool.component.scss | 1 + .../schema-tree-node.component.scss | 254 ------------------ styles/typography.scss | 4 +- 3 files changed, 3 insertions(+), 256 deletions(-) diff --git a/src/module/query/query-tool.component.scss b/src/module/query/query-tool.component.scss index 33d4a099..95fb6c7d 100644 --- a/src/module/query/query-tool.component.scss +++ b/src/module/query/query-tool.component.scss @@ -113,6 +113,7 @@ border-bottom: 1px solid primary.$light-purple; border-left: 1px solid primary.$light-purple; border-right: 1px solid primary.$light-purple; + font-weight: typography.$thin; button { display: none; diff --git a/src/module/query/schema-tree-node/schema-tree-node.component.scss b/src/module/query/schema-tree-node/schema-tree-node.component.scss index 5b0296d8..f46dbe0d 100644 --- a/src/module/query/schema-tree-node/schema-tree-node.component.scss +++ b/src/module/query/schema-tree-node/schema-tree-node.component.scss @@ -3,257 +3,3 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -@use "media"; -@use "primary"; -@use "secondary"; -@use "typography"; -@use "shapes"; - -.query-page-rough { - height: 100%; - margin-top: 16px; - margin-bottom: 16px; - display: flex; - flex-direction: column; - gap: 16px; -} - -.query-page { - height: 100%; - margin-top: 16px; - margin-bottom: 16px; - gap: 4px !important; - - ::ng-deep app-drag-handle span { - display: none; - } -} - -.tp-bento-container { - gap: 16px; -} - -.tool-windows { - flex: 0 0 360px; -} - -.main-panes { - flex: 1; - gap: 4px !important; - - @media (max-width: media.$max-width-mobile) { - margin-left: -8px; - } -} - -.history-pane, .query-pane, .run-pane { - display: flex; - flex-direction: column; -} - -.run-pane mat-form-field { - height: 100%; -} - -.card { - @include shapes.standard-border; - border-radius: shapes.$border-radius-panel; - @include shapes.light-source-gradient(primary.$purple, primary.$deep-purple); - transition: background 0.1s linear; - padding: 16px; -} - -.card-header { - display: flex; - gap: 16px; - align-items: center; - margin-bottom: 12px; - height: 32px; -} - -mat-form-field { - width: 100%; -} - -button { - i.fa-play { - margin: 0; - } - - &:enabled i.fa-play { - color: secondary.$deep-green; - } -} - -.query-text-box { - resize: none; - font-family: "Monaco" !important; - font-size: 14px !important; - font-weight: 400 !important; - line-height: 22px !important; - letter-spacing: 0.02em !important; -} - -.code-editor-container { - height: 100%; - overflow: auto; - border: 1px solid primary.$light-purple; - background: primary.$black-purple; - - code-editor { - height: 100%; - } -} - -.answers-outer-container { - height: 100%; - width: 100%; - border: 1px solid primary.$light-purple; - background: primary.$black-purple; - position: relative; -} - -.answers-container { - position: absolute; - inset: 0; - overflow: auto; -} - -.answers-text-container { - mat-form-field { - flex: 1; - display: flex; - flex-direction: column; - } - - ::ng-deep .mdc-text-field, - ::ng-deep .mat-mdc-form-field-flex, - ::ng-deep .mat-mdc-form-field-infix { - width: 100%; - height: 100%; - --mdc-outlined-text-field-container-shape: 0; - } - - ::ng-deep .mdc-notched-outline { - display: none; - } - - ::ng-deep .mdc-text-field { - --mdc-outlined-text-field-input-text-color: #{secondary.$pink}; - } - - ::ng-deep .mat-mdc-form-field:not(.form-field-dense) .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { - padding: 4px 2px 4px 6px; - } - - .answers-text-box { - height: 100% !important; - resize: none; - @include typography.code; - } -} - -.status-text-container { - width: 100%; - height: 100%; - display: flex; -} - -.status-text { - @include typography.p1; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 80%; - text-align: center; - z-index: 10; /* Ensure it's above the canvas elements */ - white-space: normal !important; - overflow-wrap: break-word; - padding: 10px; - margin: 0; -} - -.answers-placeholder-container { - height: 100%; - overflow: auto; - border: 3px dashed primary.$light-purple; - border-radius: shapes.$border-radius-panel; - display: flex; - justify-content: center; - align-items: center; - background: primary.$black-purple; - --mat-button-filled-container-height: 40px; -} - -#structureView { - width: 100%; - height: 100%; - display: flex; -} - -.history-pane { - @media (max-width: media.$max-width-mobile) { - display: none; - } - - .history-container { - height: 100%; - width: 100%; - overflow: auto; - } - - ol li { - list-style: none; - margin-top: 8px; - - aside { - display: flex; - align-items: center; - - .bullet { - margin: 0 4px; - } - - tp-spinner { - width: unset; - } - - .action-status { - margin-left: 8px; - } - - .action-status i { - margin-left: 6px; - font-size: 12px; - - &.fa-xmark { - color: #{primary.$red}; - cursor: pointer; - } - } - } - - .mat-divider { - margin-top: 8px; - } - } - - mat-form-field { - margin-top: 4px; - - ::ng-deep .mdc-notched-outline { - - } - - textarea { - resize: none; - @include typography.code; - } - } - - .transaction-operation-type { - min-width: 140px; - } -} diff --git a/styles/typography.scss b/styles/typography.scss index 665ce0ba..19710fff 100644 --- a/styles/typography.scss +++ b/styles/typography.scss @@ -82,7 +82,7 @@ $medium: 500; $regular: 400; $italic: 400; $light: 300; -$extra-light: 200; +$thin: 200; $letter-spacing: 0.02em; @@ -114,7 +114,7 @@ $letter-spacing: 0.02em; } @mixin light-banner { - @include font-style(150px, $extra-light, 150px, $letter-spacing); + @include font-style(150px, $thin, 150px, $letter-spacing); } @mixin subtitle { From ecfa0161176b077d5811f355db8e30fca0d3a96e Mon Sep 17 00:00:00 2001 From: Alex Walker Date: Mon, 23 Jun 2025 11:35:49 +0100 Subject: [PATCH 11/11] Delete some comments --- src/module/query/query-tool.component.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/module/query/query-tool.component.html b/src/module/query/query-tool.component.html index 7a367768..81a487d1 100644 --- a/src/module/query/query-tool.component.html +++ b/src/module/query/query-tool.component.html @@ -175,9 +175,6 @@

    History

    } - - - }