Skip to content

Commit 5e40831

Browse files
authored
Schema tool window (#871)
## Release notes: product changes The Query page now comes with a Schema tool window: a listing of all your schema types and their capabilities. ## Motivation While writing queries, you want a reference guide side-by-side with your query editor that shows you what queries you can write. Now, we've got a reference panel that shows your current TypeQL schema and the capabilities of all its types. And one day, it could come with a TypeQL language guide. ## Implementation `SchemaState` now maintains a network-shaped `Schema`, an object consisting of `entities`, `relations`, and `attributes`, which may have `supertype`, `subtypes`, `playedRoles`, `relatedRoles`, and `ownedAttributes`. The query tool state maps this `Schema` into a tree structure. The query page renders this tree structure.
1 parent ebe461d commit 5e40831

15 files changed

+654
-687
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Directive, HostBinding, HostListener } from "@angular/core";
2+
3+
@Directive({
4+
selector: "[detectScroll]"
5+
})
6+
export class DetectScrollDirective {
7+
@HostBinding("class.scrolled") scrolled = false;
8+
9+
@HostListener("scroll", ["$event.target"]) onScroll(target: any) {
10+
this.scrolled = target.scrollTop > 0;
11+
}
12+
}

src/framework/typedb-driver/response.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export interface ConceptDocumentsQueryResponse extends QueryResponseBase {
6969

7070
export type QueryResponse = OkQueryResponse | ConceptRowsQueryResponse | ConceptDocumentsQueryResponse;
7171

72+
export type ApiOkResponse<OK_RES = {}> = { ok: OK_RES };
73+
7274
export type ApiError = { code: string; message: string };
7375

7476
export interface ApiErrorResponse {
@@ -80,9 +82,9 @@ export function isApiError(err: any): err is ApiError {
8082
return typeof err.code === "string" && typeof err.message === "string";
8183
}
8284

83-
export type ApiResponse<OK_RES = {} | null> = { ok: OK_RES } | ApiErrorResponse;
85+
export type ApiResponse<OK_RES = {} | null> = ApiOkResponse<OK_RES> | ApiErrorResponse;
8486

85-
export function isOkResponse<OK_RES>(res: ApiResponse<OK_RES>): res is { ok: OK_RES } {
87+
export function isOkResponse<OK_RES>(res: ApiResponse<OK_RES>): res is ApiOkResponse<OK_RES> {
8688
return "ok" in res;
8789
}
8890

src/module/query/query-tool.component.html

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,35 @@
11
<ts-page-scaffold pageAvailability="ready">
22
<article #articleRef class="query-page" style="display: flex; flex-direction: row;">
3-
<div resizable [percent]="30" class="history-pane card">
4-
<div class="card-header">
5-
<i class="fa-light fa-notebook"></i>
6-
<h4>History</h4>
3+
<div resizable [percent]="20" class="schema-pane card">
4+
<div class="card-header-wrapper">
5+
<div class="card-header">
6+
<i class="fa-light fa-vector-square"></i>
7+
<h4>Schema</h4>
8+
</div>
79
</div>
8-
<div class="history-container">
9-
<ol>
10-
@for (entry of state.history.entries; track entry) {
11-
<li class="history-entry">
12-
<aside class="text-muted">
13-
<time>{{ entry.startedAtTimestamp | date:'shortTime' }}</time>
14-
<span class="bullet"></span>
15-
@if (isTransactionOperation(entry)) {
16-
<p class="transaction-operation-type">{{ transactionOperationString(entry) }}</p>
17-
} @else {
18-
<p class="transaction-operation-type">ran query</p>
19-
}
20-
<span class="flex-spacer"></span>
21-
<div class="action-status">
22-
@if (entry.status === "pending") {
23-
<tp-spinner [size]="14"/>
24-
} @else {
25-
<span>{{ actionDurationString(entry) }}</span>
26-
@if (entry.status === "success") {
27-
<i class="fa-light fa-check"></i>
28-
} @else {
29-
<i class="fa-light fa-xmark" richTooltip [richTooltipContent]="historyEntryErrorTooltip(entry)" (click)="copyHistoryEntryErrorTooltip(entry)"></i>
30-
}
31-
}
32-
</div>
33-
</aside>
34-
@if (isQueryRun(entry)) {
35-
<mat-form-field>
36-
<textarea [value]="queryHistoryPreview(entry.query)" readonly matInput
37-
cdkTextareaAutosize #autosize="cdkTextareaAutosize" cdkAutosizeMinRows="1" cdkAutosizeMaxRows="3">
38-
</textarea>
39-
</mat-form-field>
40-
}
41-
<!-- @if (!$last && isTransactionOperation(entry) && entry.transactionOperation.operationType === "close") {-->
42-
<!-- <mat-divider/>-->
43-
<!-- }-->
44-
</li>
45-
}
46-
</ol>
10+
<div class="schema-container" detectScroll>
11+
<div class="gutter"></div>
12+
<mat-tree #tree [dataSource]="state.schemaWindow.dataSource" [childrenAccessor]="state.schemaWindow.childrenAccessor">
13+
<!-- This is the tree node template for leaf nodes -->
14+
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
15+
<!-- use a disabled button to provide padding for tree leaf -->
16+
<button matIconButton disabled></button>
17+
<ts-schema-tree-node [data]="node"/>
18+
</mat-tree-node>
19+
<!-- This is the tree node template for expandable nodes -->
20+
<mat-tree-node *matTreeNodeDef="let node;when: state.schemaWindow.hasChild" matTreeNodePadding matTreeNodeToggle [cdkTreeNodeTypeaheadLabel]="node.name">
21+
<button matIconButton matTreeNodeToggle aria-label="Toggle node">
22+
<mat-icon class="mat-icon-rtl-mirror">
23+
{{tree.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
24+
</mat-icon>
25+
</button>
26+
<ts-schema-tree-node [data]="node"/>
27+
</mat-tree-node>
28+
</mat-tree>
29+
<div class="gutter"></div>
4730
</div>
4831
</div>
49-
<div class="main-panes" resizable [percent]="70" style="display: flex; flex-direction: column;">
32+
<div class="main-panes" resizable [percent]="60" style="display: flex; flex-direction: column;">
5033
<div resizable [percent]="40" class="query-pane card">
5134
<div class="card-header">
5235
<i class="fa-light fa-code"></i>
@@ -154,5 +137,48 @@ <h4>Output</h4>
154137
}
155138
</div>
156139
</div>
140+
<div resizable [percent]="20" class="history-pane card">
141+
<div class="card-header">
142+
<i class="fa-light fa-notebook"></i>
143+
<h4>History</h4>
144+
</div>
145+
<div class="history-container">
146+
<ol>
147+
@for (entry of state.history.entries; track entry) {
148+
<li class="history-entry">
149+
<aside class="text-muted">
150+
<time>{{ entry.startedAtTimestamp | date:'shortTime' }}</time>
151+
<span class="bullet"></span>
152+
@if (isTransactionOperation(entry)) {
153+
<p class="transaction-operation-type">{{ transactionOperationString(entry) }}</p>
154+
} @else {
155+
<p class="transaction-operation-type">ran query</p>
156+
}
157+
<span class="flex-spacer"></span>
158+
<div class="action-status">
159+
@if (entry.status === "pending") {
160+
<tp-spinner [size]="14"/>
161+
} @else {
162+
<span>{{ actionDurationString(entry) }}</span>
163+
@if (entry.status === "success") {
164+
<i class="fa-light fa-check"></i>
165+
} @else {
166+
<i class="fa-light fa-xmark" richTooltip [richTooltipContent]="historyEntryErrorTooltip(entry)" (click)="copyHistoryEntryErrorTooltip(entry)"></i>
167+
}
168+
}
169+
</div>
170+
</aside>
171+
@if (isQueryRun(entry)) {
172+
<mat-form-field>
173+
<textarea class="history-query-text" [value]="queryHistoryPreview(entry.query)" readonly matInput
174+
cdkTextareaAutosize #autosize="cdkTextareaAutosize" cdkAutosizeMinRows="1" cdkAutosizeMaxRows="3">
175+
</textarea>
176+
</mat-form-field>
177+
}
178+
</li>
179+
}
180+
</ol>
181+
</div>
182+
</div>
157183
</article>
158184
</ts-page-scaffold>

src/module/query/query-tool.component.scss

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,8 @@
1010
@use "typography";
1111
@use "shapes";
1212

13-
:host {
14-
height: 100%;
15-
}
16-
17-
.query-page-rough {
18-
height: 100%;
19-
margin-top: 16px;
20-
margin-bottom: 16px;
21-
display: flex;
22-
flex-direction: column;
23-
gap: 16px;
24-
}
25-
2613
.query-page {
27-
height: 100%;
28-
margin-top: 16px;
29-
margin-bottom: 16px;
14+
height: calc(100% - 86px);
3015
gap: 4px !important;
3116

3217
::ng-deep app-drag-handle span {
@@ -66,6 +51,18 @@
6651
@include shapes.light-source-gradient(primary.$purple, primary.$deep-purple);
6752
transition: background 0.1s linear;
6853
padding: 16px;
54+
55+
&:has(.card-header-wrapper) {
56+
padding: 0;
57+
}
58+
}
59+
60+
.card-header-wrapper {
61+
padding: 16px;
62+
63+
.card-header {
64+
margin-bottom: 0;
65+
}
6966
}
7067

7168
.card-header {
@@ -76,6 +73,67 @@
7673
height: 32px;
7774
}
7875

76+
.schema-pane {
77+
display: flex;
78+
flex-direction: column;
79+
}
80+
81+
.schema-container {
82+
display: flex;
83+
overflow: auto;
84+
scrollbar-gutter: stable;
85+
overscroll-behavior: contain;
86+
87+
.gutter {
88+
flex: 0 0 16px;
89+
}
90+
91+
&.scrolled {
92+
border-top: 1px solid primary.$black-purple;
93+
}
94+
95+
mat-tree {
96+
padding: 0;
97+
box-sizing: border-box;
98+
height: 100%;
99+
margin: 0;
100+
101+
.mat-mdc-icon-button {
102+
margin: 0;
103+
padding: 0;
104+
width: 1em;
105+
height: 1em;
106+
}
107+
108+
mat-tree-node[aria-level="3"] {
109+
margin-left: 32px;
110+
padding-left: 8px !important; /* overrides inline style from Angular Material */
111+
padding-right: 8px;
112+
align-items: stretch;
113+
border-bottom: 1px solid primary.$light-purple;
114+
border-left: 1px solid primary.$light-purple;
115+
border-right: 1px solid primary.$light-purple;
116+
font-weight: typography.$thin;
117+
118+
button {
119+
display: none;
120+
}
121+
122+
&:nth-child(odd) {
123+
background: primary.$black-purple;
124+
}
125+
126+
&:nth-child(even) {
127+
background: primary.$purple;
128+
}
129+
}
130+
131+
mat-tree-node[aria-level="2"] + mat-tree-node[aria-level="3"] {
132+
border-top: 1px solid primary.$light-purple;
133+
}
134+
}
135+
}
136+
79137
mat-form-field {
80138
width: 100%;
81139
}

src/module/query/query-tool.component.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,36 @@ import { MatButtonModule } from "@angular/material/button";
1212
import { MatButtonToggleModule } from "@angular/material/button-toggle";
1313
import { MatDividerModule } from "@angular/material/divider";
1414
import { MatFormFieldModule } from "@angular/material/form-field";
15+
import { MatIconModule } from "@angular/material/icon";
1516
import { MatInputModule } from "@angular/material/input";
1617
import { MatSortModule } from "@angular/material/sort";
1718
import { MatTableModule } from "@angular/material/table";
19+
import { MatTreeModule } from "@angular/material/tree";
1820
import { MatTooltipModule } from "@angular/material/tooltip";
1921
import { RouterLink } from "@angular/router";
2022
import { ResizableDirective } from "@hhangular/resizable";
2123
import { distinctUntilChanged, filter, first, map, startWith } from "rxjs";
2224
import { otherExampleLinter, TypeQL } from "../../framework/codemirror-lang-typeql";
2325
import { DriverAction, TransactionOperationAction, isQueryRun, isTransactionOperation } from "../../concept/action";
2426
import { basicDark } from "../../framework/code-editor/theme";
27+
import { DetectScrollDirective } from "../../framework/scroll-container/detect-scroll.directive";
2528
import { SpinnerComponent } from "../../framework/spinner/spinner.component";
2629
import { RichTooltipDirective } from "../../framework/tooltip/rich-tooltip.directive";
2730
import { AppData } from "../../service/app-data.service";
2831
import { DriverState } from "../../service/driver-state.service";
2932
import { QueryToolState } from "../../service/query-tool-state.service";
3033
import { SnackbarService } from "../../service/snackbar.service";
3134
import { PageScaffoldComponent } from "../scaffold/page/page-scaffold.component";
35+
import { SchemaTreeNodeComponent } from "./schema-tree-node/schema-tree-node.component";
3236

3337
@Component({
3438
selector: "ts-query-tool",
3539
templateUrl: "query-tool.component.html",
3640
styleUrls: ["query-tool.component.scss"],
3741
imports: [
38-
RouterLink, AsyncPipe, PageScaffoldComponent, MatDividerModule, MatFormFieldModule,
42+
RouterLink, AsyncPipe, PageScaffoldComponent, MatDividerModule, MatFormFieldModule, MatTreeModule, MatIconModule,
3943
MatInputModule, FormsModule, ReactiveFormsModule, MatButtonToggleModule, CodeEditor, ResizableDirective,
40-
DatePipe, SpinnerComponent, MatTableModule, MatSortModule, MatTooltipModule, MatButtonModule, RichTooltipDirective,
44+
DatePipe, SpinnerComponent, MatTableModule, MatSortModule, MatTooltipModule, MatButtonModule, RichTooltipDirective, SchemaTreeNodeComponent, DetectScrollDirective,
4145
]
4246
})
4347
export class QueryToolComponent implements OnInit, AfterViewInit, OnDestroy {
@@ -49,8 +53,10 @@ export class QueryToolComponent implements OnInit, AfterViewInit, OnDestroy {
4953
readonly codeEditorTheme = basicDark;
5054
codeEditorHidden = true;
5155

52-
constructor(protected state: QueryToolState, public driver: DriverState, private appData: AppData, private snackbar: SnackbarService) {
53-
}
56+
constructor(
57+
protected state: QueryToolState, public driver: DriverState,
58+
private appData: AppData, private snackbar: SnackbarService
59+
) {}
5460

5561
ngOnInit() {
5662
this.appData.viewState.setLastUsedTool("query");
@@ -67,6 +73,7 @@ export class QueryToolComponent implements OnInit, AfterViewInit, OnDestroy {
6773
ngAfterViewInit() {
6874
const articleWidth = this.articleRef.nativeElement.clientWidth;
6975
this.resizables.first.percent = (articleWidth * 0.15 + 100) / articleWidth * 100;
76+
this.resizables.last.percent = (articleWidth * 0.15 + 100) / articleWidth * 100;
7077
this.graphViewRef.changes.pipe(
7178
map(x => x as QueryList<ElementRef<HTMLElement>>),
7279
startWith(this.graphViewRef),
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@switch (data.nodeKind) {
2+
@case ("root") {
3+
{{ data.label }}
4+
}
5+
@case ("concept") {
6+
{{ data.concept.label }}@if (data.concept.kind === "attributeType") {<em class="text-muted">, value {{ data.concept.valueType }}</em>}
7+
}
8+
@case ("link") {
9+
@switch (data.linkKind) {
10+
@case ("sub") {
11+
<p>sub {{ data.supertype.label }}</p>
12+
}
13+
@case ("owns") {
14+
<p>owns {{ data.ownedAttribute.label }}, value {{ data.ownedAttribute.valueType }}</p>
15+
}
16+
@case ("relates") {
17+
<p>relates {{ data.role.label }}</p>
18+
}
19+
@case ("plays") {
20+
<p>plays {{ data.role.label }}</p>
21+
}
22+
}
23+
}
24+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/*!/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
import { Component, Input } from "@angular/core";
8+
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
9+
import { MatButtonModule } from "@angular/material/button";
10+
import { MatButtonToggleModule } from "@angular/material/button-toggle";
11+
import { MatDividerModule } from "@angular/material/divider";
12+
import { MatFormFieldModule } from "@angular/material/form-field";
13+
import { MatIconModule } from "@angular/material/icon";
14+
import { MatInputModule } from "@angular/material/input";
15+
import { MatSortModule } from "@angular/material/sort";
16+
import { MatTableModule } from "@angular/material/table";
17+
import { MatTreeModule } from "@angular/material/tree";
18+
import { MatTooltipModule } from "@angular/material/tooltip";
19+
import { SchemaTreeNode } from "../../../service/query-tool-state.service";
20+
21+
@Component({
22+
selector: "ts-schema-tree-node",
23+
templateUrl: "schema-tree-node.component.html",
24+
styleUrls: ["schema-tree-node.component.scss"],
25+
imports: [
26+
MatDividerModule, MatFormFieldModule, MatTreeModule, MatIconModule,
27+
MatInputModule, FormsModule, ReactiveFormsModule, MatButtonToggleModule,
28+
MatTableModule, MatSortModule, MatTooltipModule, MatButtonModule,
29+
]
30+
})
31+
export class SchemaTreeNodeComponent {
32+
@Input({ required: true }) data!: SchemaTreeNode;
33+
}

0 commit comments

Comments
 (0)