terrier-engine 4.41.2 → 4.44.0
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/dives/dive-editor.ts +1 -0
- package/data-dive/dives/dive-runs.ts +7 -2
- package/data-dive/dives/dives.ts +1 -1
- package/data-dive/queries/filters.ts +4 -2
- package/data-dive/queries/queries.ts +34 -2
- package/data-dive/queries/row-order-modal.ts +10 -5
- package/data-dive/queries/tables.ts +4 -7
- package/package.json +6 -5
- package/terrier/app.ts +4 -4
- package/terrier/lightbox.ts +2 -2
- package/terrier/modals.ts +4 -1
- package/terrier/overlays.ts +2 -2
- package/terrier/parts/terrier-part.ts +5 -5
- package/tsconfig.json +1 -1
|
@@ -416,6 +416,7 @@ class DuplicateQueryModal extends ModalPart<DuplicateQueryState> {
|
|
|
416
416
|
this.state.editor.queryTabs.showTab(query.id)
|
|
417
417
|
this.pop()
|
|
418
418
|
this.app.successToast("Duplicated Query", 'glyp-copy')
|
|
419
|
+
this.emitMessage(DiveEditor.diveChangedKey, {})
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
renderContent(parent: PartTag): void {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Arrays from "tuff-core/arrays"
|
|
1
2
|
import {ModalPart} from "../../terrier/modals"
|
|
2
3
|
import {PartTag} from "tuff-core/parts"
|
|
3
4
|
import {DdDive, DdDiveRun, UnpersistedDdDiveRun} from "../gen/models"
|
|
@@ -191,8 +192,12 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
191
192
|
col.div('.tt-flex.collapsible.gap.tt-form', row => {
|
|
192
193
|
// inputs
|
|
193
194
|
row.div('.tt-flex.column.dd-dive-run-inputs', col => {
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
const filtersByQuery = Arrays.groupBy(this.filters, 'query_name')
|
|
196
|
+
for (const queryName of Object.keys(filtersByQuery).sort()) {
|
|
197
|
+
col.h3(".glyp-data_dive_query").text(queryName)
|
|
198
|
+
for (const filter of filtersByQuery[queryName]) {
|
|
199
|
+
this.renderInput(col, filter)
|
|
200
|
+
}
|
|
196
201
|
}
|
|
197
202
|
})
|
|
198
203
|
|
package/data-dive/dives/dives.ts
CHANGED
|
@@ -61,7 +61,7 @@ async function list(): Promise<DiveListResult> {
|
|
|
61
61
|
function computeFilterInputs(schema: SchemaDef, dive: DdDive): FilterInput[] {
|
|
62
62
|
const filters: Record<string, FilterInput> = {}
|
|
63
63
|
for (const query of dive.query_data?.queries || []) {
|
|
64
|
-
Tables.computeFilterInputs(schema, query.from, filters)
|
|
64
|
+
Tables.computeFilterInputs(schema, query, query.from, filters)
|
|
65
65
|
}
|
|
66
66
|
return Object.values(filters)
|
|
67
67
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {PartTag} from "tuff-core/parts"
|
|
2
2
|
import Dates, {DateLiteral, VirtualDatePeriod, VirtualDateRange} from "./dates"
|
|
3
3
|
import {ColumnDef, ModelDef, SchemaDef} from "../../terrier/schema"
|
|
4
|
+
import { Query } from "./queries"
|
|
4
5
|
import {TableRef, TableView} from "./tables"
|
|
5
6
|
import {Logger} from "tuff-core/logging"
|
|
6
7
|
import * as inflection from "inflection"
|
|
@@ -683,6 +684,7 @@ class AddFilterDropdown extends Dropdown<{modelDef: ModelDef, callback: AddFilte
|
|
|
683
684
|
////////////////////////////////////////////////////////////////////////////////
|
|
684
685
|
|
|
685
686
|
export type FilterInput = Filter & {
|
|
687
|
+
query_name: string
|
|
686
688
|
input_name: string
|
|
687
689
|
input_value: string
|
|
688
690
|
possible_values?: string[]
|
|
@@ -694,12 +696,12 @@ export type FilterInput = Filter & {
|
|
|
694
696
|
* @param table
|
|
695
697
|
* @param filter
|
|
696
698
|
*/
|
|
697
|
-
function toInput(schema: SchemaDef, table: TableRef, filter: Filter): FilterInput {
|
|
699
|
+
function toInput(schema: SchemaDef, query: Query, table: TableRef, filter: Filter): FilterInput {
|
|
698
700
|
if (!filter.id?.length) {
|
|
699
701
|
filter.id = Ids.makeRandom(8)
|
|
700
702
|
}
|
|
701
703
|
const name = `${table.model}.${filter.column}`
|
|
702
|
-
const filterInput: FilterInput = {...filter, input_name: name, input_value: ''}
|
|
704
|
+
const filterInput: FilterInput = {...filter, query_name: query.name, input_name: name, input_value: ''}
|
|
703
705
|
switch (filter.filter_type) {
|
|
704
706
|
case 'inclusion':
|
|
705
707
|
const modelDef = schema.models[table.model]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Filter } from "./filters"
|
|
1
2
|
import {TableRef} from "./tables"
|
|
2
3
|
import api, {ApiResponse} from "../../terrier/api"
|
|
3
4
|
import {PartTag} from "tuff-core/parts"
|
|
@@ -63,7 +64,7 @@ function eachTable(query: Query, fn: TableFunction) {
|
|
|
63
64
|
eachChildTable(query.from, fn)
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
type ColumnFunction = (table: TableRef, col: ColumnRef) => any
|
|
67
|
+
export type ColumnFunction = (table: TableRef, col: ColumnRef) => any
|
|
67
68
|
|
|
68
69
|
function eachColumnForTable(table: TableRef, fn: ColumnFunction) {
|
|
69
70
|
if (table.columns) {
|
|
@@ -87,14 +88,44 @@ function eachColumn(query: Query, fn: ColumnFunction) {
|
|
|
87
88
|
eachColumnForTable(query.from, fn)
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
export type FilterFunction = (table: TableRef, filter: Filter) => any
|
|
92
|
+
|
|
93
|
+
function eachFilterForTable(table: TableRef, fn: FilterFunction) {
|
|
94
|
+
if (table.filters) {
|
|
95
|
+
for (const filter of table.filters) {
|
|
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
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
/**
|
|
91
|
-
*
|
|
107
|
+
* Recursively iterates over all filters in the query and executes the given function for each.
|
|
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)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Duplicates a query, including all the nested data structures.
|
|
92
117
|
* @param query the query to duplicate
|
|
93
118
|
* @return a completely new query
|
|
94
119
|
*/
|
|
95
120
|
function duplicate(query: Query): Query {
|
|
96
121
|
const newQuery = Objects.deepCopy(query)
|
|
97
122
|
newQuery.id = Ids.makeUuid()
|
|
123
|
+
|
|
124
|
+
// filters need new IDs, otherwise they won't be able to be set differently than the original query's filters
|
|
125
|
+
eachFilter(newQuery, (_, filter) => {
|
|
126
|
+
filter.id = Ids.makeUuid()
|
|
127
|
+
})
|
|
128
|
+
|
|
98
129
|
return newQuery
|
|
99
130
|
}
|
|
100
131
|
|
|
@@ -332,6 +363,7 @@ export class QueryModelPicker extends TerrierPart<QueryModelPickerState> {
|
|
|
332
363
|
const Queries = {
|
|
333
364
|
eachColumn,
|
|
334
365
|
eachTable,
|
|
366
|
+
eachFilter,
|
|
335
367
|
duplicate,
|
|
336
368
|
validate,
|
|
337
369
|
preview,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {ModalPart} from "../../terrier/modals"
|
|
2
|
+
import Columns from "./columns"
|
|
2
3
|
import Queries, {OrderBy, Query} from "./queries"
|
|
3
4
|
import Messages from "tuff-core/messages"
|
|
4
5
|
import {PartTag} from "tuff-core/parts"
|
|
@@ -54,15 +55,19 @@ export default class RowOrderModal extends ModalPart<RowOrderState> {
|
|
|
54
55
|
|
|
55
56
|
// collect the column options
|
|
56
57
|
const query = this.state.query
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const existingColumns = new Set<string>()
|
|
59
|
+
Queries.eachColumn(query, (table, col) => {
|
|
60
|
+
const name = Columns.computeSelectName(table, col)
|
|
61
|
+
existingColumns.add(name)
|
|
62
|
+
this.columnOptions.push({title: name, value: name})
|
|
59
63
|
})
|
|
60
64
|
|
|
61
|
-
// initialize the order
|
|
65
|
+
// initialize the order-bys from the query, if present
|
|
62
66
|
if (query.order_by?.length) {
|
|
63
|
-
|
|
67
|
+
// filter out columns that are no longer present in the query
|
|
68
|
+
this.orderBys = query.order_by.filter(ob => existingColumns.has(ob.column))
|
|
64
69
|
}
|
|
65
|
-
else
|
|
70
|
+
if (!query.order_by?.length) { // this isn't an else because the previous clause might've resulted in no columns
|
|
66
71
|
// start with something by default
|
|
67
72
|
this.addClause()
|
|
68
73
|
}
|
|
@@ -47,22 +47,19 @@ const updatedKey = Messages.typedKey<TableRef>()
|
|
|
47
47
|
////////////////////////////////////////////////////////////////////////////////
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Recursively
|
|
51
|
-
* Only keep one (the last one traversed) per table/column combination.
|
|
52
|
-
* This means that some filters may clobber others, but I think it will yield
|
|
53
|
-
* the desired result most of the time.
|
|
50
|
+
* Recursively collects all the filters for this and all joined tables.
|
|
54
51
|
* @param schema
|
|
55
52
|
* @param table
|
|
56
53
|
* @param filters
|
|
57
54
|
*/
|
|
58
|
-
function computeFilterInputs(schema: SchemaDef, table: TableRef, filters: Record<string, FilterInput>) {
|
|
55
|
+
function computeFilterInputs(schema: SchemaDef, query: Query, table: TableRef, filters: Record<string, FilterInput>) {
|
|
59
56
|
for (const f of table.filters || []) {
|
|
60
|
-
const fi = Filters.toInput(schema, table, f)
|
|
57
|
+
const fi = Filters.toInput(schema, query, table, f)
|
|
61
58
|
filters[fi.id] = fi
|
|
62
59
|
}
|
|
63
60
|
if (table.joins) {
|
|
64
61
|
for (const j of Object.values(table.joins)) {
|
|
65
|
-
computeFilterInputs(schema, j, filters)
|
|
62
|
+
computeFilterInputs(schema, query, j, filters)
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
65
|
}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"files": [
|
|
5
5
|
"*"
|
|
6
6
|
],
|
|
7
|
-
"version": "4.
|
|
7
|
+
"version": "4.44.0",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://github.com/Terrier-Tech/terrier-engine"
|
|
@@ -20,18 +20,19 @@
|
|
|
20
20
|
"inflection": "^2.0.1",
|
|
21
21
|
"mime-types": "^2.1.35",
|
|
22
22
|
"tuff-core": "latest",
|
|
23
|
-
"tuff-plot": "
|
|
23
|
+
"tuff-plot": "latest",
|
|
24
24
|
"tuff-sortable": "latest",
|
|
25
25
|
"turbolinks": "^5.2.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/mime-types": "^2.1.4",
|
|
29
|
-
"@types/node": "^
|
|
29
|
+
"@types/node": "^22.9.0",
|
|
30
30
|
"glyphs2font": "^1.2.4",
|
|
31
31
|
"prettier": "^2.8.8",
|
|
32
32
|
"svgo": "^3.0.2",
|
|
33
|
-
"
|
|
33
|
+
"typescript": "^5.6.3",
|
|
34
|
+
"vite": "^5.4.11",
|
|
34
35
|
"vite-plugin-ruby": "^5.1.0",
|
|
35
|
-
"vitest": "^
|
|
36
|
+
"vitest": "^2.1.5"
|
|
36
37
|
}
|
|
37
38
|
}
|
package/terrier/app.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {Logger} from "tuff-core/logging"
|
|
2
|
-
import {Part, PartConstructor
|
|
2
|
+
import {Part, PartConstructor} from "tuff-core/parts"
|
|
3
3
|
import TerrierPart from "./parts/terrier-part"
|
|
4
4
|
import Tooltips from "./tooltips"
|
|
5
5
|
import Lightbox from "./lightbox"
|
|
@@ -48,8 +48,8 @@ export abstract class TerrierApp<TState> extends TerrierPart<TState> {
|
|
|
48
48
|
/// Overlays
|
|
49
49
|
|
|
50
50
|
addOverlay<OverlayType extends Part<StateType>, StateType extends {}>(
|
|
51
|
-
constructor:
|
|
52
|
-
state: StateType
|
|
51
|
+
constructor: PartConstructor<OverlayType, StateType>,
|
|
52
|
+
state: NoInfer<StateType>,
|
|
53
53
|
type: OverlayLayerType
|
|
54
54
|
) {
|
|
55
55
|
return this.overlayPart.pushLayer(constructor, state, type)
|
|
@@ -85,7 +85,7 @@ export abstract class TerrierApp<TState> extends TerrierPart<TState> {
|
|
|
85
85
|
|
|
86
86
|
showModal<ModalType extends ModalPart<StateType>, StateType>(
|
|
87
87
|
constructor: PartConstructor<ModalType, StateType>,
|
|
88
|
-
state: StateType
|
|
88
|
+
state: NoInfer<StateType>
|
|
89
89
|
): ModalType {
|
|
90
90
|
const modalStack = this.overlayPart.getOrCreateLayer(ModalStackPart, {}, 'modal')
|
|
91
91
|
const modal = modalStack.pushModal(constructor, state)
|
package/terrier/lightbox.ts
CHANGED
|
@@ -70,14 +70,14 @@ class LightboxPart extends Part<LightboxState> {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
update(_elem: HTMLElement) {
|
|
73
|
-
setTimeout(
|
|
73
|
+
setTimeout(() => {
|
|
74
74
|
_elem.classList.add('active') // start css fade in
|
|
75
75
|
}, 10)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
close() {
|
|
79
79
|
this.element?.classList.remove('active')
|
|
80
|
-
setTimeout(
|
|
80
|
+
setTimeout(() => {
|
|
81
81
|
this.state.app.removeOverlay(this.state)
|
|
82
82
|
}, 500)
|
|
83
83
|
}
|
package/terrier/modals.ts
CHANGED
|
@@ -147,7 +147,10 @@ export class ModalStackPart extends TerrierPart<{}> {
|
|
|
147
147
|
* @param constructor the modal class
|
|
148
148
|
* @param state the modal's state
|
|
149
149
|
*/
|
|
150
|
-
pushModal<
|
|
150
|
+
pushModal<ModalType extends ModalPart<StateType>, StateType>(
|
|
151
|
+
constructor: PartConstructor<ModalType, StateType>,
|
|
152
|
+
state: NoInfer<StateType>
|
|
153
|
+
): ModalPart<StateType> {
|
|
151
154
|
log.info(`Making modal`, constructor.name)
|
|
152
155
|
const modal = this.makePart(constructor, state)
|
|
153
156
|
this.modals.push(modal)
|
package/terrier/overlays.ts
CHANGED
|
@@ -33,7 +33,7 @@ export class OverlayPart extends Part<NoState> {
|
|
|
33
33
|
*/
|
|
34
34
|
pushLayer<PartType extends Part<StateType>, StateType extends {}>(
|
|
35
35
|
constructor: PartConstructor<PartType, StateType>,
|
|
36
|
-
state: StateType
|
|
36
|
+
state: NoInfer<StateType>,
|
|
37
37
|
type: OverlayLayerType
|
|
38
38
|
): PartType {
|
|
39
39
|
this.layerStates.push({partClass: constructor, partState: state, type})
|
|
@@ -50,7 +50,7 @@ export class OverlayPart extends Part<NoState> {
|
|
|
50
50
|
*/
|
|
51
51
|
getOrCreateLayer<PartType extends Part<StateType>, StateType extends {}>(
|
|
52
52
|
constructor: PartConstructor<PartType, StateType>,
|
|
53
|
-
state: StateType
|
|
53
|
+
state: NoInfer<StateType>,
|
|
54
54
|
type: OverlayLayerType
|
|
55
55
|
): PartType {
|
|
56
56
|
const layers = this.getCollectionParts('layers')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Part,
|
|
1
|
+
import { Part, PartConstructor, PartTag } from "tuff-core/parts"
|
|
2
2
|
import {TerrierApp} from "../app"
|
|
3
3
|
import Loading from "../loading"
|
|
4
4
|
import Theme, {IconName} from "../theme"
|
|
@@ -171,8 +171,8 @@ export default abstract class TerrierPart<TState> extends Part<TState> {
|
|
|
171
171
|
* @param target the target element around which to show the dropdown
|
|
172
172
|
*/
|
|
173
173
|
makeDropdown<DropdownType extends Dropdown<DropdownStateType>, DropdownStateType extends {}>(
|
|
174
|
-
constructor:
|
|
175
|
-
state: DropdownStateType
|
|
174
|
+
constructor: PartConstructor<DropdownType, DropdownStateType>,
|
|
175
|
+
state: NoInfer<DropdownStateType>,
|
|
176
176
|
target: EventTarget | null) {
|
|
177
177
|
const dropdown = this.app.addOverlay(constructor, state, 'dropdown')
|
|
178
178
|
dropdown.parentPart = this
|
|
@@ -193,8 +193,8 @@ export default abstract class TerrierPart<TState> extends Part<TState> {
|
|
|
193
193
|
* @param target the target element around which to show the dropdown
|
|
194
194
|
*/
|
|
195
195
|
toggleDropdown<DropdownType extends Dropdown<DropdownStateType>, DropdownStateType extends {}>(
|
|
196
|
-
constructor:
|
|
197
|
-
state: DropdownStateType
|
|
196
|
+
constructor: PartConstructor<DropdownType, DropdownStateType>,
|
|
197
|
+
state: NoInfer<DropdownStateType>,
|
|
198
198
|
target: EventTarget | null) {
|
|
199
199
|
if (target && target instanceof HTMLElement && target == this.app.lastDropdownTarget) {
|
|
200
200
|
this.clearDropdowns()
|