terrier-engine 4.52.6 → 4.52.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data-dive/plots/dive-plot-traces.ts +17 -17
- package/data-dive/queries/column-order-modal.ts +6 -24
- package/data-dive/queries/columns.ts +38 -36
- package/data-dive/queries/queries.ts +49 -72
- package/data-dive/queries/query-editor.ts +29 -26
- package/data-dive/queries/row-order-modal.ts +19 -18
- package/data-dive/queries/tables.ts +34 -34
- package/data-dive/queries/validation.ts +19 -18
- package/package.json +1 -1
- package/terrier/modals.ts +6 -6
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {MarkerStyle, PlotTrace, TraceType, YAxisName} from "tuff-plot/trace"
|
|
1
|
+
import { MarkerStyle, PlotTrace, TraceType, YAxisName } from "tuff-plot/trace"
|
|
2
2
|
import { PartTag } from "tuff-core/parts"
|
|
3
|
-
import {ModalPart} from "../../terrier/modals"
|
|
3
|
+
import { ModalPart } from "../../terrier/modals"
|
|
4
4
|
import Ids from "../../terrier/ids"
|
|
5
|
-
import {DivePlotEditorState} from "./dive-plot-editor"
|
|
6
|
-
import {TerrierFormFields} from "../../terrier/forms"
|
|
7
|
-
import {UnpersistedDdDivePlot} from "../gen/models"
|
|
8
|
-
import {SelectOptions} from "tuff-core/forms"
|
|
9
|
-
import Queries, {Query, QueryResult} from "../queries/queries"
|
|
10
|
-
import {Logger} from "tuff-core/logging"
|
|
5
|
+
import { DivePlotEditorState } from "./dive-plot-editor"
|
|
6
|
+
import { TerrierFormFields } from "../../terrier/forms"
|
|
7
|
+
import { UnpersistedDdDivePlot } from "../gen/models"
|
|
8
|
+
import { SelectOptions } from "tuff-core/forms"
|
|
9
|
+
import Queries, { Query, QueryResult } from "../queries/queries"
|
|
10
|
+
import { Logger } from "tuff-core/logging"
|
|
11
11
|
import Columns from "../queries/columns"
|
|
12
12
|
import Messages from "tuff-core/messages"
|
|
13
|
-
import DivePlotStyles, {DivePlotTraceStyle, TraceStyleFields} from "./dive-plot-styles"
|
|
13
|
+
import DivePlotStyles, { DivePlotTraceStyle, TraceStyleFields } from "./dive-plot-styles"
|
|
14
14
|
import TerrierPart from "../../terrier/parts/terrier-part"
|
|
15
15
|
|
|
16
16
|
const log = new Logger("DivePlotTraces")
|
|
@@ -86,7 +86,7 @@ export class DivePlotTraceEditor extends ModalPart<DivePlotTraceEditorState> {
|
|
|
86
86
|
|
|
87
87
|
this.queries = this.state.dive.query_data?.queries || []
|
|
88
88
|
this.queryOptions = this.queries.map(query => {
|
|
89
|
-
return {value: query.id, title: query.name}
|
|
89
|
+
return { value: query.id, title: query.name }
|
|
90
90
|
}) || []
|
|
91
91
|
|
|
92
92
|
this.trace.query_id ||= this.queries.at(0)?.id || ''
|
|
@@ -100,13 +100,13 @@ export class DivePlotTraceEditor extends ModalPart<DivePlotTraceEditorState> {
|
|
|
100
100
|
this.addAction({
|
|
101
101
|
title: "Save",
|
|
102
102
|
icon: "glyp-checkmark",
|
|
103
|
-
click: {key: this.saveKey}
|
|
103
|
+
click: { key: this.saveKey }
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
this.addAction({
|
|
107
107
|
title: "Delete",
|
|
108
108
|
icon: "glyp-delete",
|
|
109
|
-
click: {key: this.deleteKey},
|
|
109
|
+
click: { key: this.deleteKey },
|
|
110
110
|
classes: ['alert']
|
|
111
111
|
}, 'secondary')
|
|
112
112
|
|
|
@@ -133,9 +133,9 @@ export class DivePlotTraceEditor extends ModalPart<DivePlotTraceEditorState> {
|
|
|
133
133
|
this.axisOptions = []
|
|
134
134
|
if (query) {
|
|
135
135
|
log.info(`Computing axis options for query`, query)
|
|
136
|
-
|
|
136
|
+
for (const { table, column } of Queries.columns(query)) {
|
|
137
137
|
this.axisOptions.push(Columns.computeSelectName(table, column))
|
|
138
|
-
}
|
|
138
|
+
}
|
|
139
139
|
}
|
|
140
140
|
else {
|
|
141
141
|
log.warn(`No query with id ${queryId}`)
|
|
@@ -180,7 +180,7 @@ export class DivePlotTraceEditor extends ModalPart<DivePlotTraceEditorState> {
|
|
|
180
180
|
|
|
181
181
|
async save() {
|
|
182
182
|
const data = await this.fields.serialize()
|
|
183
|
-
this.trace = {...this.trace, ...data}
|
|
183
|
+
this.trace = { ...this.trace, ...data }
|
|
184
184
|
this.trace.style = await this.styleFields.serialize()
|
|
185
185
|
log.info("Saving plot trace", this.trace)
|
|
186
186
|
this.state.onSave(this.trace)
|
|
@@ -224,9 +224,9 @@ export class DivePlotTraceRow extends TerrierPart<DivePlotTraceRowState> {
|
|
|
224
224
|
// style
|
|
225
225
|
content.div('.style', stylePreview => {
|
|
226
226
|
DivePlotStyles.renderPreview(stylePreview, style, this.state.index)
|
|
227
|
-
}).data({tooltip: `${style.colorName} ${style.strokeWidthName} ${style.strokeDasharrayName}`})
|
|
227
|
+
}).data({ tooltip: `${style.colorName} ${style.strokeWidthName} ${style.strokeDasharrayName}` })
|
|
228
228
|
})
|
|
229
|
-
}).emitClick(editKey, {id: trace.id})
|
|
229
|
+
}).emitClick(editKey, { id: trace.id })
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import Arrays from "tuff-core/arrays"
|
|
2
2
|
import { PartTag } from "tuff-core/parts"
|
|
3
|
-
import {ModalPart} from "../../terrier/modals"
|
|
3
|
+
import { ModalPart } from "../../terrier/modals"
|
|
4
4
|
import Columns from "./columns"
|
|
5
|
-
import Queries, {Query} from "./queries"
|
|
5
|
+
import Queries, { Query } from "./queries"
|
|
6
6
|
import Messages from "tuff-core/messages"
|
|
7
7
|
import SortablePlugin from "tuff-sortable/sortable-plugin"
|
|
8
|
-
import {Logger} from "tuff-core/logging"
|
|
8
|
+
import { Logger } from "tuff-core/logging"
|
|
9
9
|
|
|
10
10
|
const log = new Logger("ColumnOrderModal")
|
|
11
11
|
|
|
@@ -26,7 +26,7 @@ export default class ColumnOrderModal extends ModalPart<ColumnOrderState> {
|
|
|
26
26
|
this.addAction({
|
|
27
27
|
title: "Apply",
|
|
28
28
|
icon: "glyp-checkmark",
|
|
29
|
-
click: {key: this.submitKey}
|
|
29
|
+
click: { key: this.submitKey }
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
this.onClick(this.submitKey, _ => {
|
|
@@ -36,25 +36,7 @@ export default class ColumnOrderModal extends ModalPart<ColumnOrderState> {
|
|
|
36
36
|
|
|
37
37
|
// initialize the columns from the query, if present
|
|
38
38
|
const query = this.state.query
|
|
39
|
-
|
|
40
|
-
if (query.columns?.length) {
|
|
41
|
-
this.columns = query.columns
|
|
42
|
-
this.columns.forEach(c => {
|
|
43
|
-
initialColumns.add(c)
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ensure that all columns in the query are represented, regardless of whether they're stored
|
|
48
|
-
Queries.eachColumn(query, (table, col) => {
|
|
49
|
-
const name = Columns.computeSelectName(table, col)
|
|
50
|
-
if (!this.columns.includes(name)) {
|
|
51
|
-
this.columns.push(name)
|
|
52
|
-
initialColumns.delete(name)
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
// remove any of the initial columns that aren't in the query anymore
|
|
57
|
-
Arrays.deleteIf(this.columns, (c) => initialColumns.has(c))
|
|
39
|
+
if (query.columns?.length) this.columns = Array.from(query.columns)
|
|
58
40
|
|
|
59
41
|
// make the list sortable
|
|
60
42
|
this.makePlugin(SortablePlugin, {
|
|
@@ -72,7 +54,7 @@ export default class ColumnOrderModal extends ModalPart<ColumnOrderState> {
|
|
|
72
54
|
container.p().text("Drag and drop the columns to change their order:")
|
|
73
55
|
container.div(".dive-column-sort-zone", zone => {
|
|
74
56
|
for (const col of this.columns) {
|
|
75
|
-
zone.div(".column").data({column: col}).text(col)
|
|
57
|
+
zone.div(".column").data({ column: col }).text(col)
|
|
76
58
|
}
|
|
77
59
|
})
|
|
78
60
|
})
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import {PartTag} from "tuff-core/parts"
|
|
2
|
-
import {ColumnDef, ModelDef, SchemaDef} from "../../terrier/schema"
|
|
3
|
-
import {TableRef, TableView} from "./tables"
|
|
4
|
-
import {Logger} from "tuff-core/logging"
|
|
5
|
-
import Forms, {FormFields, SelectOptions} from "tuff-core/forms"
|
|
1
|
+
import { PartTag } from "tuff-core/parts"
|
|
2
|
+
import { ColumnDef, ModelDef, SchemaDef } from "../../terrier/schema"
|
|
3
|
+
import { TableRef, TableView } from "./tables"
|
|
4
|
+
import { Logger } from "tuff-core/logging"
|
|
5
|
+
import Forms, { FormFields, SelectOptions } from "tuff-core/forms"
|
|
6
6
|
import Objects from "tuff-core/objects"
|
|
7
|
-
import {ModalPart} from "../../terrier/modals"
|
|
8
|
-
import {Dropdown} from "../../terrier/dropdowns"
|
|
7
|
+
import { ModalPart } from "../../terrier/modals"
|
|
8
|
+
import { Dropdown } from "../../terrier/dropdowns"
|
|
9
9
|
import DiveEditor from "../dives/dive-editor"
|
|
10
10
|
import Messages from "tuff-core/messages"
|
|
11
11
|
import Arrays from "tuff-core/arrays"
|
|
12
12
|
import Dom from "tuff-core/dom"
|
|
13
|
-
import Validation, {ColumnValidationError} from "./validation"
|
|
14
|
-
import Queries, {Query} from "./queries"
|
|
15
|
-
import {TerrierFormFields} from "../../terrier/forms"
|
|
13
|
+
import Validation, { ColumnValidationError } from "./validation"
|
|
14
|
+
import Queries, { Query } from "./queries"
|
|
15
|
+
import { TerrierFormFields } from "../../terrier/forms"
|
|
16
16
|
import TerrierPart from "../../terrier/parts/terrier-part"
|
|
17
17
|
|
|
18
18
|
const log = new Logger("Columns")
|
|
@@ -115,7 +115,7 @@ export type ColumnsEditorState = {
|
|
|
115
115
|
const saveKey = Messages.untypedKey()
|
|
116
116
|
const addKey = Messages.untypedKey()
|
|
117
117
|
const addSingleKey = Messages.typedKey<{ name: string }>()
|
|
118
|
-
const removeKey = Messages.typedKey<{id: string}>()
|
|
118
|
+
const removeKey = Messages.typedKey<{ id: string }>()
|
|
119
119
|
const valueChangedKey = Messages.untypedKey()
|
|
120
120
|
|
|
121
121
|
/**
|
|
@@ -131,11 +131,11 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
131
131
|
tableFields!: FormFields<TableRef>
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
async init
|
|
134
|
+
async init() {
|
|
135
135
|
this.table = this.state.tableView.table
|
|
136
136
|
this.modelDef = this.state.tableView.modelDef
|
|
137
137
|
|
|
138
|
-
this.tableFields = new FormFields(this, {...this.table})
|
|
138
|
+
this.tableFields = new FormFields(this, { ...this.table })
|
|
139
139
|
|
|
140
140
|
// initialize the columns states
|
|
141
141
|
const columns: ColumnRef[] = this.table.columns || []
|
|
@@ -149,13 +149,13 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
149
149
|
this.addAction({
|
|
150
150
|
title: 'Apply',
|
|
151
151
|
icon: 'glyp-checkmark',
|
|
152
|
-
click: {key: saveKey}
|
|
152
|
+
click: { key: saveKey }
|
|
153
153
|
}, 'primary')
|
|
154
154
|
|
|
155
155
|
this.addAction({
|
|
156
156
|
title: 'Add Columns',
|
|
157
157
|
icon: 'glyp-plus',
|
|
158
|
-
click: {key: addKey}
|
|
158
|
+
click: { key: addKey }
|
|
159
159
|
}, 'secondary')
|
|
160
160
|
|
|
161
161
|
this.onClick(saveKey, _ => {
|
|
@@ -171,7 +171,7 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
171
171
|
})
|
|
172
172
|
|
|
173
173
|
this.onClick(addKey, m => {
|
|
174
|
-
this.toggleDropdown(SelectColumnsDropdown, {editor: this as ColumnsEditorModal}, m.event.target)
|
|
174
|
+
this.toggleDropdown(SelectColumnsDropdown, { editor: this as ColumnsEditorModal }, m.event.target)
|
|
175
175
|
})
|
|
176
176
|
|
|
177
177
|
this.onChange(valueChangedKey, m => {
|
|
@@ -189,7 +189,7 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
189
189
|
|
|
190
190
|
addEditor(col: ColumnRef) {
|
|
191
191
|
this.columnCount += 1
|
|
192
|
-
const state = {schema: this.state.schema, columnsEditor: this, id: `column-${this.columnCount}`, column: col}
|
|
192
|
+
const state = { schema: this.state.schema, columnsEditor: this, id: `column-${this.columnCount}`, column: col }
|
|
193
193
|
this.columnEditors[state.id] = this.makePart(ColumnEditor, state)
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -227,10 +227,10 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
227
227
|
// the table of column editors
|
|
228
228
|
parent.div('.dd-columns-editor-table', table => {
|
|
229
229
|
table.div('.dd-editor-header', header => {
|
|
230
|
-
header.div('.name').label({text: "Name"})
|
|
231
|
-
header.div('.alias').label({text: "Alias"})
|
|
232
|
-
header.div('.function').label({text: "Function"})
|
|
233
|
-
header.div('.group-by').label({text: "Group By?"})
|
|
230
|
+
header.div('.name').label({ text: "Name" })
|
|
231
|
+
header.div('.alias').label({ text: "Alias" })
|
|
232
|
+
header.div('.function').label({ text: "Function" })
|
|
233
|
+
header.div('.group-by').label({ text: "Group By?" })
|
|
234
234
|
})
|
|
235
235
|
table.div('dd-editor-row-container', container => {
|
|
236
236
|
for (const id of Object.keys(this.columnEditors)) {
|
|
@@ -261,7 +261,7 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
261
261
|
tr.td('.description').text(colDef.metadata?.description || '')
|
|
262
262
|
tr.td().a('.add-column.tt-button.secondary.circle.compact.inline', a => {
|
|
263
263
|
a.i('.glyp-plus')
|
|
264
|
-
}).emitClick(addSingleKey, {name: colDef.name})
|
|
264
|
+
}).emitClick(addSingleKey, { name: colDef.name })
|
|
265
265
|
})
|
|
266
266
|
}
|
|
267
267
|
})
|
|
@@ -288,12 +288,12 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
288
288
|
// make a deep copy of the query and update this table's columns and settings
|
|
289
289
|
this.table._id = this.id // we need this to identify the table after the deep copy
|
|
290
290
|
const query = Objects.deepCopy(this.state.query)
|
|
291
|
-
Queries.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
291
|
+
const tables = Queries.tables(query).
|
|
292
|
+
filter(table => table._id == this.id)
|
|
293
|
+
for (const table of tables) {
|
|
294
|
+
table.columns = columns
|
|
295
|
+
table.prefix = tableData.prefix
|
|
296
|
+
}
|
|
297
297
|
|
|
298
298
|
// validate the temporary query
|
|
299
299
|
log.info(`Validating temporary query with column changes`, query)
|
|
@@ -358,10 +358,10 @@ class ColumnEditor extends TerrierPart<ColumnState> {
|
|
|
358
358
|
|
|
359
359
|
render(parent: PartTag) {
|
|
360
360
|
parent.div('.name', col => {
|
|
361
|
-
col.div('.tt-readonly-field', {text: this.columnRef.name})
|
|
361
|
+
col.div('.tt-readonly-field', { text: this.columnRef.name })
|
|
362
362
|
})
|
|
363
363
|
parent.div('.alias', col => {
|
|
364
|
-
this.fields.textInput(col, "alias", {placeholder: "Alias"})
|
|
364
|
+
this.fields.textInput(col, "alias", { placeholder: "Alias" })
|
|
365
365
|
.emitChange(valueChangedKey)
|
|
366
366
|
})
|
|
367
367
|
parent.div('.function', col => {
|
|
@@ -375,7 +375,7 @@ class ColumnEditor extends TerrierPart<ColumnState> {
|
|
|
375
375
|
parent.div('.actions', actions => {
|
|
376
376
|
actions.a(a => {
|
|
377
377
|
a.i('.glyp-close')
|
|
378
|
-
}).emitClick(removeKey, {id: this.state.id})
|
|
378
|
+
}).emitClick(removeKey, { id: this.state.id })
|
|
379
379
|
})
|
|
380
380
|
if (this.columnRef.errors?.length) {
|
|
381
381
|
for (const error of this.columnRef.errors) {
|
|
@@ -387,7 +387,7 @@ class ColumnEditor extends TerrierPart<ColumnState> {
|
|
|
387
387
|
async serialize() {
|
|
388
388
|
return await this.fields.serialize()
|
|
389
389
|
}
|
|
390
|
-
|
|
390
|
+
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
|
|
@@ -406,7 +406,7 @@ type SelectableColumn = {
|
|
|
406
406
|
/**
|
|
407
407
|
* Shows a dropdown that allows the user to select one or more columns from the given model.
|
|
408
408
|
*/
|
|
409
|
-
class SelectColumnsDropdown extends Dropdown<{editor: ColumnsEditorModal}> {
|
|
409
|
+
class SelectColumnsDropdown extends Dropdown<{ editor: ColumnsEditorModal }> {
|
|
410
410
|
|
|
411
411
|
addAllKey = Messages.untypedKey()
|
|
412
412
|
addKey = Messages.typedKey<ColumnRef>()
|
|
@@ -431,9 +431,11 @@ class SelectColumnsDropdown extends Dropdown<{editor: ColumnsEditorModal}> {
|
|
|
431
431
|
const description = colDef.metadata?.description || ''
|
|
432
432
|
return {
|
|
433
433
|
def: colDef,
|
|
434
|
-
ref: {name: colDef.name},
|
|
434
|
+
ref: { name: colDef.name },
|
|
435
435
|
included,
|
|
436
|
-
sortOrder,
|
|
436
|
+
sortOrder,
|
|
437
|
+
description
|
|
438
|
+
}
|
|
437
439
|
})
|
|
438
440
|
this.columns = Arrays.sortBy(this.columns, 'sortOrder')
|
|
439
441
|
|
|
@@ -527,7 +529,7 @@ class SelectColumnsDropdown extends Dropdown<{editor: ColumnsEditorModal}> {
|
|
|
527
529
|
|
|
528
530
|
parent.a('.primary', a => {
|
|
529
531
|
a.i('.glyp-check_all')
|
|
530
|
-
a.span({text: "Add All"})
|
|
532
|
+
a.span({ text: "Add All" })
|
|
531
533
|
}).emitClick(this.addAllKey)
|
|
532
534
|
}
|
|
533
535
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { Filter } from "./filters"
|
|
2
|
-
import {TableRef} from "./tables"
|
|
3
|
-
import api, {ApiResponse} from "../../terrier/api"
|
|
4
|
-
import {PartTag} from "tuff-core/parts"
|
|
5
|
-
import {TableCellTag} from "tuff-core/html"
|
|
2
|
+
import { TableRef } from "./tables"
|
|
3
|
+
import api, { ApiResponse } from "../../terrier/api"
|
|
4
|
+
import { PartTag } from "tuff-core/parts"
|
|
5
|
+
import { TableCellTag } from "tuff-core/html"
|
|
6
6
|
import dayjs from "dayjs"
|
|
7
7
|
import QueryEditor from "./query-editor"
|
|
8
8
|
import TerrierPart from "../../terrier/parts/terrier-part"
|
|
9
|
-
import Schema, {ModelDef, SchemaDef} from "../../terrier/schema"
|
|
10
|
-
import {Logger} from "tuff-core/logging"
|
|
9
|
+
import Schema, { ModelDef, SchemaDef } from "../../terrier/schema"
|
|
10
|
+
import { Logger } from "tuff-core/logging"
|
|
11
11
|
import * as inflection from "inflection"
|
|
12
12
|
import Messages from "tuff-core/messages"
|
|
13
13
|
import Strings from "tuff-core/strings"
|
|
14
14
|
import Arrays from "tuff-core/arrays"
|
|
15
|
-
import {ColumnRef} from "./columns"
|
|
15
|
+
import { ColumnRef } from "./columns"
|
|
16
16
|
import Ids from "../../terrier/ids";
|
|
17
17
|
import Objects from "tuff-core/objects";
|
|
18
18
|
|
|
@@ -42,74 +42,46 @@ export type Query = {
|
|
|
42
42
|
// Utilities
|
|
43
43
|
////////////////////////////////////////////////////////////////////////////////
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
function* childTables(table: TableRef): Generator<TableRef, void, void> {
|
|
46
|
+
if (!table.joins) return
|
|
46
47
|
|
|
47
|
-
function eachChildTable(table: TableRef, fn: TableFunction) {
|
|
48
|
-
if (!table.joins) {
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
48
|
for (const joinedTable of Object.values(table.joins)) {
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
yield joinedTable
|
|
50
|
+
yield* childTables(joinedTable)
|
|
54
51
|
}
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* @param fn
|
|
61
|
-
*/
|
|
62
|
-
function eachTable(query: Query, fn: TableFunction) {
|
|
63
|
-
fn(query.from)
|
|
64
|
-
eachChildTable(query.from, fn)
|
|
54
|
+
function* tables(query: Query): Generator<TableRef, void, void> {
|
|
55
|
+
yield query.from
|
|
56
|
+
yield* childTables(query.from)
|
|
65
57
|
}
|
|
66
58
|
|
|
67
|
-
|
|
59
|
+
function* tableColumns(table: TableRef): Generator<{ table: TableRef, column: ColumnRef }, void, void> {
|
|
60
|
+
if (table.columns)
|
|
61
|
+
for (const column of table.columns)
|
|
62
|
+
yield { table, column }
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
fn(table, col)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (table.joins) {
|
|
76
|
-
for (const joinedTable of Object.values(table.joins)) {
|
|
77
|
-
eachColumnForTable(joinedTable, fn)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
64
|
+
if (table.joins)
|
|
65
|
+
for (const joinedTable of Object.values(table.joins))
|
|
66
|
+
yield* tableColumns(joinedTable)
|
|
80
67
|
}
|
|
81
68
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* @param query
|
|
85
|
-
* @param fn a function to evaluate for each column in the query
|
|
86
|
-
*/
|
|
87
|
-
function eachColumn(query: Query, fn: ColumnFunction) {
|
|
88
|
-
eachColumnForTable(query.from, fn)
|
|
69
|
+
function columns(query: Query) {
|
|
70
|
+
return tableColumns(query.from)
|
|
89
71
|
}
|
|
90
72
|
|
|
91
|
-
|
|
73
|
+
function* tableFilters(table: TableRef): Generator<{ table: TableRef, filter: Filter }, void, void> {
|
|
74
|
+
if (table.filters)
|
|
75
|
+
for (const filter of table.filters)
|
|
76
|
+
yield ({ table, filter })
|
|
92
77
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
fn(table, filter)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (table.joins) {
|
|
100
|
-
for (const joinedTable of Object.values(table.joins)) {
|
|
101
|
-
eachFilterForTable(joinedTable, fn)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
78
|
+
if (table.joins)
|
|
79
|
+
for (const joinedTable of Object.values(table.joins))
|
|
80
|
+
yield* tableFilters(joinedTable)
|
|
104
81
|
}
|
|
105
82
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
* @param query
|
|
109
|
-
* @param fn a function to evaluate on each filter
|
|
110
|
-
*/
|
|
111
|
-
function eachFilter(query: Query, fn: FilterFunction) {
|
|
112
|
-
eachFilterForTable(query.from, fn)
|
|
83
|
+
function filters(query: Query) {
|
|
84
|
+
return tableFilters(query.from)
|
|
113
85
|
}
|
|
114
86
|
|
|
115
87
|
/**
|
|
@@ -122,9 +94,8 @@ function duplicate(query: Query): Query {
|
|
|
122
94
|
newQuery.id = Ids.makeUuid()
|
|
123
95
|
|
|
124
96
|
// filters need new IDs, otherwise they won't be able to be set differently than the original query's filters
|
|
125
|
-
|
|
126
|
-
filter.id = Ids.makeUuid()
|
|
127
|
-
})
|
|
97
|
+
filters(query)
|
|
98
|
+
.forEach(({ filter }) => filter.id = Ids.makeUuid())
|
|
128
99
|
|
|
129
100
|
return newQuery
|
|
130
101
|
}
|
|
@@ -151,7 +122,7 @@ export type QueryServerValidation = ApiResponse & {
|
|
|
151
122
|
* @param query
|
|
152
123
|
*/
|
|
153
124
|
async function validate(query: Query): Promise<QueryServerValidation> {
|
|
154
|
-
return await api.post<QueryServerValidation>("/data_dive/validate_query.json", {query})
|
|
125
|
+
return await api.post<QueryServerValidation>("/data_dive/validate_query.json", { query })
|
|
155
126
|
}
|
|
156
127
|
|
|
157
128
|
|
|
@@ -184,7 +155,7 @@ export type QueryResult = ApiResponse & {
|
|
|
184
155
|
* @param query
|
|
185
156
|
*/
|
|
186
157
|
async function preview(query: Query): Promise<QueryResult> {
|
|
187
|
-
return await api.post<QueryResult>("/data_dive/preview_query.json", {query})
|
|
158
|
+
return await api.post<QueryResult>("/data_dive/preview_query.json", { query })
|
|
188
159
|
}
|
|
189
160
|
|
|
190
161
|
|
|
@@ -211,15 +182,15 @@ function renderCell(td: TableCellTag, col: QueryResultColumn, val: any): any {
|
|
|
211
182
|
return td.div('.dollars').text(`\$${dollars}`)
|
|
212
183
|
case 'cents':
|
|
213
184
|
const cents = parseInt(val)
|
|
214
|
-
const d = (cents/100.0).toFixed(2)
|
|
185
|
+
const d = (cents / 100.0).toFixed(2)
|
|
215
186
|
return td.div('.dollars').text(`\$${d}`)
|
|
216
187
|
case 'string':
|
|
217
188
|
if (col.select_name.endsWith('id')) {
|
|
218
189
|
const id = val.toString()
|
|
219
190
|
td.a('.id')
|
|
220
|
-
.data({tooltip: id})
|
|
221
|
-
.text(`...${id.substring(id.length-6)}`)
|
|
222
|
-
.emitClick(QueryEditor.copyToClipboardKey, {value: id})
|
|
191
|
+
.data({ tooltip: id })
|
|
192
|
+
.text(`...${id.substring(id.length - 6)}`)
|
|
193
|
+
.emitClick(QueryEditor.copyToClipboardKey, { value: id })
|
|
223
194
|
}
|
|
224
195
|
else {
|
|
225
196
|
td.text(val.toString())
|
|
@@ -307,8 +278,8 @@ export class QueryModelPicker extends TerrierPart<QueryModelPickerState> {
|
|
|
307
278
|
|
|
308
279
|
renderModelOption(parent: PartTag, model: ModelDef) {
|
|
309
280
|
parent.label('.model-option', label => {
|
|
310
|
-
label.input({type: 'radio', name: `new-query-model-${this.id}`, value: model.name})
|
|
311
|
-
.emitChange(this.pickedKey, {model: model.name})
|
|
281
|
+
label.input({ type: 'radio', name: `new-query-model-${this.id}`, value: model.name })
|
|
282
|
+
.emitChange(this.pickedKey, { model: model.name })
|
|
312
283
|
label.div(col => {
|
|
313
284
|
const name = inflection.pluralize(Strings.titleize(model.name))
|
|
314
285
|
col.div('.name').text(name)
|
|
@@ -364,10 +335,16 @@ const Queries = {
|
|
|
364
335
|
eachColumn,
|
|
365
336
|
eachTable,
|
|
366
337
|
eachFilter,
|
|
338
|
+
childTables,
|
|
339
|
+
tables,
|
|
340
|
+
tableColumns,
|
|
341
|
+
columns,
|
|
342
|
+
tableFilters,
|
|
343
|
+
filters,
|
|
367
344
|
duplicate,
|
|
368
345
|
validate,
|
|
369
346
|
preview,
|
|
370
347
|
renderPreview
|
|
371
348
|
}
|
|
372
349
|
|
|
373
|
-
export default Queries
|
|
350
|
+
export default Queries
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {PartTag} from "tuff-core/parts"
|
|
2
|
-
import Queries, {Query, QueryResult, QueryServerValidation} from "./queries"
|
|
3
|
-
import Tables, {FromTableView} from "./tables"
|
|
4
|
-
import {Logger} from "tuff-core/logging"
|
|
5
|
-
import QueryForm, {QuerySettings, QuerySettingsColumns} from "./query-form"
|
|
6
|
-
import DiveEditor, {DiveEditorState} from "../dives/dive-editor"
|
|
1
|
+
import { PartTag } from "tuff-core/parts"
|
|
2
|
+
import Queries, { Query, QueryResult, QueryServerValidation } from "./queries"
|
|
3
|
+
import Tables, { FromTableView } from "./tables"
|
|
4
|
+
import { Logger } from "tuff-core/logging"
|
|
5
|
+
import QueryForm, { QuerySettings, QuerySettingsColumns } from "./query-form"
|
|
6
|
+
import DiveEditor, { DiveEditorState } from "../dives/dive-editor"
|
|
7
7
|
import Objects from "tuff-core/objects"
|
|
8
8
|
import Html from "tuff-core/html"
|
|
9
9
|
import ContentPart from "../../terrier/parts/content-part"
|
|
10
|
-
import {TabContainerPart} from "../../terrier/tabs"
|
|
10
|
+
import { TabContainerPart } from "../../terrier/tabs"
|
|
11
11
|
import Messages from "tuff-core/messages"
|
|
12
|
-
import Validation, {QueryClientValidation} from "./validation"
|
|
12
|
+
import Validation, { QueryClientValidation } from "./validation"
|
|
13
13
|
import ColumnOrderModal from "./column-order-modal"
|
|
14
14
|
import RowOrderModal from "./row-order-modal"
|
|
15
|
+
import Columns from "./columns"
|
|
15
16
|
|
|
16
17
|
const log = new Logger("QueryEditor")
|
|
17
18
|
|
|
@@ -32,7 +33,7 @@ class SettingsPart extends ContentPart<SubEditorState> {
|
|
|
32
33
|
form!: QueryForm
|
|
33
34
|
|
|
34
35
|
async init() {
|
|
35
|
-
this.form = this.makePart(QueryForm, {query: Objects.slice(this.state.query, ...QuerySettingsColumns)})
|
|
36
|
+
this.form = this.makePart(QueryForm, { query: Objects.slice(this.state.query, ...QuerySettingsColumns) })
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
|
|
@@ -48,8 +49,8 @@ class SettingsPart extends ContentPart<SubEditorState> {
|
|
|
48
49
|
})
|
|
49
50
|
row.a('.alert.tt-flex', a => {
|
|
50
51
|
a.i('.glyp-delete')
|
|
51
|
-
a.span({text: "Delete"})
|
|
52
|
-
}).emitClick(DiveEditor.deleteQueryKey, {id: this.state.query.id})
|
|
52
|
+
a.span({ text: "Delete" })
|
|
53
|
+
}).emitClick(DiveEditor.deleteQueryKey, { id: this.state.query.id })
|
|
53
54
|
})
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -68,9 +69,11 @@ class SortingPart extends ContentPart<SubEditorState> {
|
|
|
68
69
|
async init() {
|
|
69
70
|
this.onClick(this.sortColumnsKey, _ => {
|
|
70
71
|
log.info("Sorting columns")
|
|
72
|
+
this.state.query.columns = Array.from(Queries.tableColumns(this.state.query.from)).
|
|
73
|
+
map(({ table, column }) => Columns.computeSelectName(table, column))
|
|
71
74
|
this.app.showModal(ColumnOrderModal, {
|
|
72
75
|
query: this.state.query,
|
|
73
|
-
onSorted: (newColumns) =>
|
|
76
|
+
onSorted: (newColumns) => {
|
|
74
77
|
this.state.query.columns = newColumns
|
|
75
78
|
this.state.editor.dirty()
|
|
76
79
|
this.emitMessage(DiveEditor.diveChangedKey, {})
|
|
@@ -82,7 +85,7 @@ class SortingPart extends ContentPart<SubEditorState> {
|
|
|
82
85
|
log.info("Sorting rows")
|
|
83
86
|
this.app.showModal(RowOrderModal, {
|
|
84
87
|
query: this.state.query,
|
|
85
|
-
onSorted: (newOrderBys) =>
|
|
88
|
+
onSorted: (newOrderBys) => {
|
|
86
89
|
log.info(`New row sort order`, newOrderBys)
|
|
87
90
|
this.state.query.order_by = newOrderBys
|
|
88
91
|
this.state.editor.dirty()
|
|
@@ -170,7 +173,7 @@ class SqlPart extends ContentPart<SubEditorState> {
|
|
|
170
173
|
})
|
|
171
174
|
}
|
|
172
175
|
else {
|
|
173
|
-
row.div({text: 'SQL Goes Here'})
|
|
176
|
+
row.div({ text: 'SQL Goes Here' })
|
|
174
177
|
}
|
|
175
178
|
})
|
|
176
179
|
}
|
|
@@ -266,12 +269,12 @@ export default class QueryEditor extends ContentPart<QueryEditorState> {
|
|
|
266
269
|
|
|
267
270
|
log.info("Initializing query editor", query)
|
|
268
271
|
|
|
269
|
-
this.tabs = this.makePart(TabContainerPart, {side: 'left'})
|
|
270
|
-
this.settingsPart = this.tabs.upsertTab({key: 'settings', title: 'Settings', icon: 'glyp-settings'},
|
|
271
|
-
SettingsPart, {editor: this, query})
|
|
272
|
+
this.tabs = this.makePart(TabContainerPart, { side: 'left' })
|
|
273
|
+
this.settingsPart = this.tabs.upsertTab({ key: 'settings', title: 'Settings', icon: 'glyp-settings' },
|
|
274
|
+
SettingsPart, { editor: this, query })
|
|
272
275
|
|
|
273
|
-
this.sortingPart = this.tabs.upsertTab({key: 'sorting', title: 'Sorting', icon: 'glyp-sort'},
|
|
274
|
-
SortingPart, {editor: this, query})
|
|
276
|
+
this.sortingPart = this.tabs.upsertTab({ key: 'sorting', title: 'Sorting', icon: 'glyp-sort' },
|
|
277
|
+
SortingPart, { editor: this, query })
|
|
275
278
|
|
|
276
279
|
|
|
277
280
|
this.listenMessage(QueryForm.settingsChangedKey, m => {
|
|
@@ -279,13 +282,13 @@ export default class QueryEditor extends ContentPart<QueryEditorState> {
|
|
|
279
282
|
this.updateSettings(m.data)
|
|
280
283
|
})
|
|
281
284
|
|
|
282
|
-
this.sqlPart = this.tabs.upsertTab({key: 'sql', title: 'SQL', icon: 'glyp-code'},
|
|
283
|
-
SqlPart, {editor: this, query})
|
|
285
|
+
this.sqlPart = this.tabs.upsertTab({ key: 'sql', title: 'SQL', icon: 'glyp-code' },
|
|
286
|
+
SqlPart, { editor: this, query })
|
|
284
287
|
|
|
285
|
-
this.previewPart = this.tabs.upsertTab({key: 'preview', title: 'Preview', icon: 'glyp-table', classes: ['no-padding'], click: {key: this.updatePreviewKey}},
|
|
286
|
-
PreviewPart, {editor: this, query})
|
|
288
|
+
this.previewPart = this.tabs.upsertTab({ key: 'preview', title: 'Preview', icon: 'glyp-table', classes: ['no-padding'], click: { key: this.updatePreviewKey } },
|
|
289
|
+
PreviewPart, { editor: this, query })
|
|
287
290
|
|
|
288
|
-
this.tableEditor = this.makePart(FromTableView, {schema: this.state.schema, queryEditor: this, table: this.state.query.from})
|
|
291
|
+
this.tableEditor = this.makePart(FromTableView, { schema: this.state.schema, queryEditor: this, table: this.state.query.from })
|
|
289
292
|
|
|
290
293
|
this.listenMessage(Tables.updatedKey, m => {
|
|
291
294
|
log.info(`Table ${m.data.model} updated`, m.data)
|
|
@@ -300,7 +303,7 @@ export default class QueryEditor extends ContentPart<QueryEditorState> {
|
|
|
300
303
|
this.onClick(QueryEditor.copyToClipboardKey, async m => {
|
|
301
304
|
log.info(`Copy value to clipboard: ${m.data.value}`)
|
|
302
305
|
await navigator.clipboard.writeText(m.data.value)
|
|
303
|
-
this.showToast(`Copied '${m.data.value}' to clipboard`, {color: 'primary'})
|
|
306
|
+
this.showToast(`Copied '${m.data.value}' to clipboard`, { color: 'primary' })
|
|
304
307
|
})
|
|
305
308
|
|
|
306
309
|
this.validate().then()
|
|
@@ -341,4 +344,4 @@ export default class QueryEditor extends ContentPart<QueryEditorState> {
|
|
|
341
344
|
}
|
|
342
345
|
|
|
343
346
|
static readonly copyToClipboardKey = Messages.typedKey<{ value: string }>()
|
|
344
|
-
}
|
|
347
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {ModalPart} from "../../terrier/modals"
|
|
1
|
+
import { ModalPart } from "../../terrier/modals"
|
|
2
2
|
import Columns from "./columns"
|
|
3
|
-
import Queries, {OrderBy, Query} from "./queries"
|
|
3
|
+
import Queries, { OrderBy, Query } from "./queries"
|
|
4
4
|
import Messages from "tuff-core/messages"
|
|
5
|
-
import {PartTag} from "tuff-core/parts"
|
|
6
|
-
import {optionsForSelect, SelectOption} from "tuff-core/forms"
|
|
7
|
-
import {Logger} from "tuff-core/logging"
|
|
5
|
+
import { PartTag } from "tuff-core/parts"
|
|
6
|
+
import { optionsForSelect, SelectOption } from "tuff-core/forms"
|
|
7
|
+
import { Logger } from "tuff-core/logging"
|
|
8
8
|
import Forms from "../../terrier/forms"
|
|
9
9
|
import SortablePlugin from "tuff-sortable/sortable-plugin";
|
|
10
10
|
|
|
@@ -22,7 +22,7 @@ export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
|
22
22
|
changedKey = Messages.untypedKey()
|
|
23
23
|
orderBys: OrderBy[] = []
|
|
24
24
|
columnOptions: SelectOption[] = []
|
|
25
|
-
removeClauseKey = Messages.typedKey<{index: number}>()
|
|
25
|
+
removeClauseKey = Messages.typedKey<{ index: number }>()
|
|
26
26
|
|
|
27
27
|
async init() {
|
|
28
28
|
this.setTitle("Row Order")
|
|
@@ -31,7 +31,7 @@ export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
|
31
31
|
this.addAction({
|
|
32
32
|
title: "Apply",
|
|
33
33
|
icon: "glyp-checkmark",
|
|
34
|
-
click: {key: this.submitKey}
|
|
34
|
+
click: { key: this.submitKey }
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
this.onClick(this.submitKey, _ => {
|
|
@@ -42,7 +42,7 @@ export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
|
42
42
|
this.addAction({
|
|
43
43
|
title: "New Clause",
|
|
44
44
|
icon: "glyp-plus",
|
|
45
|
-
click: {key: this.newClauseKey}
|
|
45
|
+
click: { key: this.newClauseKey }
|
|
46
46
|
}, 'secondary')
|
|
47
47
|
|
|
48
48
|
this.onClick(this.newClauseKey, _ => {
|
|
@@ -56,11 +56,12 @@ export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
|
56
56
|
// collect the column options
|
|
57
57
|
const query = this.state.query
|
|
58
58
|
const existingColumns = new Set<string>()
|
|
59
|
-
|
|
60
|
-
const name = Columns.computeSelectName(table,
|
|
59
|
+
for (const { table, column } of Queries.columns(query)) {
|
|
60
|
+
const name = Columns.computeSelectName(table, column)
|
|
61
61
|
existingColumns.add(name)
|
|
62
|
-
this.columnOptions.push({title: name, value: name})
|
|
63
|
-
|
|
62
|
+
this.columnOptions.push({ title: name, value: name })
|
|
63
|
+
|
|
64
|
+
}
|
|
64
65
|
|
|
65
66
|
// initialize the order-bys from the query, if present
|
|
66
67
|
if (query.order_by?.length) {
|
|
@@ -88,7 +89,7 @@ export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
addClause() {
|
|
91
|
-
this.orderBys.push({column: this.columnOptions[0]?.value || '', dir: 'asc'})
|
|
92
|
+
this.orderBys.push({ column: this.columnOptions[0]?.value || '', dir: 'asc' })
|
|
92
93
|
log.info(`Added a line, orderBys is now ${this.orderBys.length} long`, this.orderBys)
|
|
93
94
|
this.dirty()
|
|
94
95
|
}
|
|
@@ -107,7 +108,7 @@ export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
|
107
108
|
this.element.querySelectorAll<HTMLElement>(".order-by").forEach(line => {
|
|
108
109
|
const column = line.querySelector<HTMLSelectElement>("select.column")?.value!!
|
|
109
110
|
const dir = Forms.getRadioValue(line, "input.dir") || "asc"
|
|
110
|
-
this.orderBys.push({column, dir})
|
|
111
|
+
this.orderBys.push({ column, dir })
|
|
111
112
|
})
|
|
112
113
|
log.info("Serialized", this.orderBys)
|
|
113
114
|
}
|
|
@@ -119,9 +120,9 @@ export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
|
119
120
|
container.div(".dive-row-sort-zone", zone => {
|
|
120
121
|
let index = 0 // for making unique radio names
|
|
121
122
|
for (const orderBy of this.orderBys) {
|
|
122
|
-
zone.div(".order-by", {data: {index: index.toString()}}, line => {
|
|
123
|
+
zone.div(".order-by", { data: { index: index.toString() } }, line => {
|
|
123
124
|
line.a(".drag.glyp-navicon")
|
|
124
|
-
.data({tooltip: "Re-order this clause"})
|
|
125
|
+
.data({ tooltip: "Re-order this clause" })
|
|
125
126
|
line.select('.column', colSelect => {
|
|
126
127
|
optionsForSelect(colSelect, this.columnOptions, orderBy.column)
|
|
127
128
|
}).emitChange(this.changedKey)
|
|
@@ -144,8 +145,8 @@ export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
|
144
145
|
label.span().text("descending")
|
|
145
146
|
})
|
|
146
147
|
line.a(".remove.glyp-close")
|
|
147
|
-
.data({tooltip: "Remove this clause"})
|
|
148
|
-
.emitClick(this.removeClauseKey, {index})
|
|
148
|
+
.data({ tooltip: "Remove this clause" })
|
|
149
|
+
.emitClick(this.removeClauseKey, { index })
|
|
149
150
|
})
|
|
150
151
|
index += 1
|
|
151
152
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import {PartTag} from "tuff-core/parts"
|
|
2
|
-
import Schema, {BelongsToDef, ModelDef, SchemaDef} from "../../terrier/schema"
|
|
1
|
+
import { PartTag } from "tuff-core/parts"
|
|
2
|
+
import Schema, { BelongsToDef, ModelDef, SchemaDef } from "../../terrier/schema"
|
|
3
3
|
import * as inflection from "inflection"
|
|
4
|
-
import Filters, {Filter, FilterInput, FiltersEditorModal} from "./filters"
|
|
5
|
-
import Columns, {ColumnRef, ColumnsEditorModal} from "./columns"
|
|
6
|
-
import {Logger} from "tuff-core/logging"
|
|
4
|
+
import Filters, { Filter, FilterInput, FiltersEditorModal } from "./filters"
|
|
5
|
+
import Columns, { ColumnRef, ColumnsEditorModal } from "./columns"
|
|
6
|
+
import { Logger } from "tuff-core/logging"
|
|
7
7
|
import ContentPart from "../../terrier/parts/content-part"
|
|
8
|
-
import {ActionsDropdown} from "../../terrier/dropdowns"
|
|
9
|
-
import {ModalPart} from "../../terrier/modals"
|
|
8
|
+
import { ActionsDropdown } from "../../terrier/dropdowns"
|
|
9
|
+
import { ModalPart } from "../../terrier/modals"
|
|
10
10
|
import TerrierFormPart from "../../terrier/parts/terrier-form-part"
|
|
11
11
|
import DiveEditor from "../dives/dive-editor"
|
|
12
12
|
import Messages from "tuff-core/messages"
|
|
13
13
|
import Arrays from "tuff-core/arrays"
|
|
14
14
|
import QueryEditor from "./query-editor"
|
|
15
|
-
import {Query} from "./queries";
|
|
15
|
+
import { Query } from "./queries";
|
|
16
16
|
|
|
17
17
|
const log = new Logger("Tables")
|
|
18
18
|
|
|
@@ -85,7 +85,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
85
85
|
editColumnsKey = Messages.untypedKey()
|
|
86
86
|
editFiltersKey = Messages.untypedKey()
|
|
87
87
|
newJoinedKey = Messages.untypedKey()
|
|
88
|
-
createJoinedKey = Messages.typedKey<{name: string}>()
|
|
88
|
+
createJoinedKey = Messages.typedKey<{ name: string }>()
|
|
89
89
|
|
|
90
90
|
async init() {
|
|
91
91
|
this.schema = this.state.schema
|
|
@@ -102,12 +102,12 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
102
102
|
|
|
103
103
|
this.onClick(this.editColumnsKey, _ => {
|
|
104
104
|
log.info(`Edit ${this.displayName} Columns`)
|
|
105
|
-
this.app.showModal(ColumnsEditorModal, {schema: this.schema, query: this.query, tableView: this as TableView<TableRef>})
|
|
105
|
+
this.app.showModal(ColumnsEditorModal, { schema: this.schema, query: this.query, tableView: this as TableView<TableRef> })
|
|
106
106
|
})
|
|
107
107
|
|
|
108
108
|
this.onClick(this.editFiltersKey, _ => {
|
|
109
109
|
log.info(`Edit ${this.displayName} Filters`)
|
|
110
|
-
this.app.showModal(FiltersEditorModal, {schema: this.schema, tableView: this as TableView<TableRef>})
|
|
110
|
+
this.app.showModal(FiltersEditorModal, { schema: this.schema, tableView: this as TableView<TableRef> })
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
// show the new join dropdown
|
|
@@ -127,19 +127,19 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
127
127
|
const common = model.metadata?.visibility == 'common' ? '0' : '1'
|
|
128
128
|
return `${common}${bt.name}`
|
|
129
129
|
})
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
130
|
+
.map(bt => {
|
|
131
|
+
const model = this.schema.models[bt.model]
|
|
132
|
+
const isCommon = model.metadata?.visibility == 'common'
|
|
133
|
+
// put a border between the common and uncommon
|
|
134
|
+
const classes = showingCommon && !isCommon ? ['border-top'] : []
|
|
135
|
+
showingCommon = isCommon
|
|
136
|
+
return {
|
|
137
|
+
title: Schema.belongsToDisplay(bt),
|
|
138
|
+
subtitle: model.metadata?.description,
|
|
139
|
+
classes,
|
|
140
|
+
click: { key: this.createJoinedKey, data: { name: bt.name } }
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
// don't show the dropdown if there are no more belongs-tos left
|
|
@@ -147,7 +147,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
147
147
|
this.toggleDropdown(ActionsDropdown, actions, m.event.target)
|
|
148
148
|
}
|
|
149
149
|
else {
|
|
150
|
-
this.showToast(`No more possible joins for ${this.displayName}`, {color: 'pending'})
|
|
150
|
+
this.showToast(`No more possible joins for ${this.displayName}`, { color: 'pending' })
|
|
151
151
|
}
|
|
152
152
|
})
|
|
153
153
|
|
|
@@ -170,13 +170,13 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
170
170
|
this.dirty()
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
-
this.app.showModal(JoinedTableEditorModal, {table, belongsTo, callback, parentTable: this.state.table as TableRef})
|
|
173
|
+
this.app.showModal(JoinedTableEditorModal, { table, belongsTo, callback, parentTable: this.state.table as TableRef })
|
|
174
174
|
}
|
|
175
175
|
})
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
addJoinedPart(joinedTable: JoinedTableRef) {
|
|
179
|
-
const state = {schema: this.schema, queryEditor: this.state.queryEditor, table: joinedTable}
|
|
179
|
+
const state = { schema: this.schema, queryEditor: this.state.queryEditor, table: joinedTable }
|
|
180
180
|
const part = this.makePart(JoinedTableView, state)
|
|
181
181
|
part.parentView = this
|
|
182
182
|
this.joinParts[joinedTable.belongs_to] = part
|
|
@@ -241,8 +241,8 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
241
241
|
panel.a('.dd-hint.joins.arrow-top.glyp-hint', hint => {
|
|
242
242
|
hint.div('.title').text("Join More Tables")
|
|
243
243
|
})
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
.emitClick(this.newJoinedKey)
|
|
245
|
+
.data({ tooltip: "Include data from other tables that are related to this one" })
|
|
246
246
|
}
|
|
247
247
|
})
|
|
248
248
|
|
|
@@ -252,7 +252,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
252
252
|
parent.section(section => {
|
|
253
253
|
section.div('.title', title => {
|
|
254
254
|
title.i(".glyp-columns")
|
|
255
|
-
title.span({text: "Columns"})
|
|
255
|
+
title.span({ text: "Columns" })
|
|
256
256
|
if (this.table.prefix?.length) {
|
|
257
257
|
title.span('.prefix').text(`${this.table.prefix}*`)
|
|
258
258
|
}
|
|
@@ -285,7 +285,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
285
285
|
parent.section('.filters', section => {
|
|
286
286
|
section.div('.title', title => {
|
|
287
287
|
title.i(".glyp-filter")
|
|
288
|
-
title.span({text: "Filters"})
|
|
288
|
+
title.span({ text: "Filters" })
|
|
289
289
|
})
|
|
290
290
|
if (this.table.filters?.length) {
|
|
291
291
|
for (const filter of this.table.filters) {
|
|
@@ -439,7 +439,7 @@ class JoinedTableEditorModal extends ModalPart<JoinedTableEditorState> {
|
|
|
439
439
|
this.addAction({
|
|
440
440
|
title: "Apply",
|
|
441
441
|
icon: 'glyp-checkmark',
|
|
442
|
-
click: {key: this.applyKey}
|
|
442
|
+
click: { key: this.applyKey }
|
|
443
443
|
}, 'primary')
|
|
444
444
|
|
|
445
445
|
this.onClick(this.applyKey, async _ => {
|
|
@@ -455,7 +455,7 @@ class JoinedTableEditorModal extends ModalPart<JoinedTableEditorState> {
|
|
|
455
455
|
this.addAction({
|
|
456
456
|
title: "Delete",
|
|
457
457
|
icon: "glyp-delete",
|
|
458
|
-
click: {key: this.deleteKey},
|
|
458
|
+
click: { key: this.deleteKey },
|
|
459
459
|
classes: ['alert']
|
|
460
460
|
}, 'secondary')
|
|
461
461
|
|
|
@@ -483,4 +483,4 @@ const Tables = {
|
|
|
483
483
|
computeFilterInputs
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
-
export default Tables
|
|
486
|
+
export default Tables
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import Queries, {Query} from "./queries"
|
|
2
|
-
import Columns, {ColumnRef} from "./columns"
|
|
3
|
-
import {TableRef} from "./tables"
|
|
1
|
+
import Queries, { Query } from "./queries"
|
|
2
|
+
import Columns, { ColumnRef } from "./columns"
|
|
3
|
+
import { TableRef } from "./tables"
|
|
4
4
|
|
|
5
5
|
export type ColumnValidationError = {
|
|
6
6
|
message: string
|
|
@@ -24,7 +24,7 @@ function validateQuery(query: Query): QueryClientValidation {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function addColumnError(col: ColumnRef, message: string) {
|
|
27
|
-
const error = {message}
|
|
27
|
+
const error = { message }
|
|
28
28
|
col.errors ||= []
|
|
29
29
|
col.errors.push(error)
|
|
30
30
|
validation.columns.push(error)
|
|
@@ -35,40 +35,41 @@ function validateQuery(query: Query): QueryClientValidation {
|
|
|
35
35
|
const aggCols: ColumnRef[] = [] // keep track of columns with an aggregate function
|
|
36
36
|
let isGrouped = false
|
|
37
37
|
const groupedTables: Set<TableRef> = new Set()
|
|
38
|
-
|
|
38
|
+
for (const { table, column } of Queries.columns(query)) {
|
|
39
39
|
// clear the errors
|
|
40
|
-
|
|
40
|
+
column.errors = undefined
|
|
41
41
|
|
|
42
42
|
// determine if there's an aggregate function
|
|
43
|
-
if (
|
|
44
|
-
aggCols.push(
|
|
43
|
+
if (column.function?.length && Columns.functionType(column.function) == 'aggregate') {
|
|
44
|
+
aggCols.push(column)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// determine if there's a _group by_ in the query
|
|
48
|
-
if (
|
|
48
|
+
if (column.grouped) {
|
|
49
49
|
isGrouped = true
|
|
50
|
-
if (
|
|
50
|
+
if (column.name == 'id') {
|
|
51
51
|
// ungrouped columns on this table are okay
|
|
52
52
|
groupedTables.add(table)
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// each select name should only be used once
|
|
57
|
-
const selectName = Columns.computeSelectName(table,
|
|
57
|
+
const selectName = Columns.computeSelectName(table, column)
|
|
58
58
|
if (usedNames.has(selectName)) {
|
|
59
|
-
addColumnError(
|
|
59
|
+
addColumnError(column, `<strong>${selectName}</strong> has already been selected for a different column`)
|
|
60
60
|
}
|
|
61
61
|
usedNames.add(selectName)
|
|
62
|
-
}
|
|
62
|
+
}
|
|
63
63
|
|
|
64
64
|
if (isGrouped) {
|
|
65
65
|
// if the query is grouped, ensure that all other column refs
|
|
66
66
|
// are either grouped, have an aggregate function, or are on a grouped table
|
|
67
|
-
Queries.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
const columns = Queries.columns(query)
|
|
68
|
+
.filter(({ table, column }) =>
|
|
69
|
+
!column.grouped && Columns.functionType(column.function) != 'aggregate' && !groupedTables.has(table))
|
|
70
|
+
for (const { column } of columns) {
|
|
71
|
+
addColumnError(column, `<strong>${column.name}</strong> must be grouped or have an aggregate function`)
|
|
72
|
+
}
|
|
72
73
|
}
|
|
73
74
|
else if (aggCols.length) {
|
|
74
75
|
// if the query isn't grouped, aggregate functions are an error
|
package/package.json
CHANGED
package/terrier/modals.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {Logger} from "tuff-core/logging"
|
|
1
|
+
import { Logger } from "tuff-core/logging"
|
|
2
2
|
import TerrierPart from "./parts/terrier-part"
|
|
3
|
-
import {PartConstructor, PartTag} from "tuff-core/parts"
|
|
3
|
+
import { PartConstructor, PartTag } from "tuff-core/parts"
|
|
4
4
|
import ContentPart from "./parts/content-part"
|
|
5
5
|
import Messages from "tuff-core/messages"
|
|
6
6
|
|
|
@@ -27,13 +27,13 @@ export abstract class ModalPart<TState> extends ContentPart<TState> {
|
|
|
27
27
|
this.emitMessage(modalPopKey, {}, { scope: 'bubble' })
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
render(parent: PartTag)
|
|
30
|
+
render(parent: PartTag) {
|
|
31
31
|
parent.div('.modal-header', header => {
|
|
32
32
|
if (this._icon) {
|
|
33
33
|
this.theme.renderIcon(header, this._icon, 'secondary')
|
|
34
34
|
}
|
|
35
|
-
header.h2({text: this._title || 'Call setTitle()'})
|
|
36
|
-
this.theme.renderActions(header, this.getActions('tertiary'), {defaultClass: 'secondary'})
|
|
35
|
+
header.h2({ text: this._title || 'Call setTitle()' })
|
|
36
|
+
this.theme.renderActions(header, this.getActions('tertiary'), { defaultClass: 'secondary' })
|
|
37
37
|
header.a('.close-modal', closeButton => {
|
|
38
38
|
this.theme.renderCloseIcon(closeButton)
|
|
39
39
|
}).emitClick(modalPopKey)
|
|
@@ -165,7 +165,7 @@ export class ModalStackPart extends TerrierPart<{}> {
|
|
|
165
165
|
if (this.displayClass == 'show') {
|
|
166
166
|
classes.push(this.displayClass)
|
|
167
167
|
}
|
|
168
|
-
parent.div('.tt-modal-stack', {classes}, stack => {
|
|
168
|
+
parent.div('.tt-modal-stack', { classes }, stack => {
|
|
169
169
|
stack.div('.modal-container', container => {
|
|
170
170
|
this.eachChild(part => {
|
|
171
171
|
container.part(part)
|