terrier-engine 4.32.0 → 4.32.2

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.
@@ -1,4 +1,4 @@
1
- // This file was automatically generated on 2024-07-17 13:26:00 -0500, DO NOT EDIT IT MANUALLY!
1
+ // This file was automatically generated on 2024-07-25 17:02:53 -0500, DO NOT EDIT IT MANUALLY!
2
2
 
3
3
  import { Query } from "../queries/queries"
4
4
 
@@ -171,6 +171,7 @@ export type DdDiveRun = {
171
171
  output_data?: object
172
172
  output_file_data?: Attachment | { path: string }
173
173
  status: "initial" | "running" | "success" | "error"
174
+ delivery_mode?: string
174
175
  delivery_recipients?: string[]
175
176
  delivery_data?: object
176
177
  created_by?: DdUser
@@ -194,6 +195,7 @@ export type UnpersistedDdDiveRun = {
194
195
  output_data?: object
195
196
  output_file_data?: Attachment | { path: string }
196
197
  status: "initial" | "running" | "success" | "error"
198
+ delivery_mode?: string
197
199
  delivery_recipients?: string[]
198
200
  delivery_data?: object
199
201
  created_by?: DdUser
@@ -0,0 +1,56 @@
1
+ import {AxisType} from "tuff-plot/axis"
2
+ import {TerrierFormFields} from "../../terrier/forms"
3
+ import {PartTag} from "tuff-core/parts"
4
+ import TerrierPart from "../../terrier/parts/terrier-part"
5
+ import {DbErrors} from "../../terrier/db-client"
6
+ import * as inflection from "inflection"
7
+
8
+
9
+ // let's not worry about a top axis for now
10
+ const axisSides = ['left', 'bottom', 'right'] as const
11
+ export type AxisSide = typeof axisSides[number]
12
+
13
+ export type DivePlotAxisType = AxisType | 'none'
14
+
15
+ export type DivePlotAxis = {
16
+ type: DivePlotAxisType
17
+ title: string
18
+ }
19
+
20
+ const axisTypeOptions = [
21
+ {value: 'number', title: 'Number'},
22
+ {value: 'time', title: 'Date/Time'},
23
+ {value: 'group', title: 'Grouped Bars'},
24
+ {value: 'stack', title: 'Stacked Bars'},
25
+ ]
26
+
27
+ // the right axis isn't required
28
+ const rightAxisTypeOptions = axisTypeOptions.concat([
29
+ {value: 'none', title: 'None'}
30
+ ])
31
+
32
+ export class DivePlotAxisFields extends TerrierFormFields<DivePlotAxis> {
33
+
34
+ constructor(part: TerrierPart<any>, axis: DivePlotAxis, readonly side: AxisSide, errors?: DbErrors<DivePlotAxis>) {
35
+ super(part, axis, errors)
36
+ }
37
+
38
+ render(parent: PartTag) {
39
+ parent.div(`.dd-dive-plot-axis-fields.tt-form.shrink.${this.side}`, container => {
40
+ container.div('.side').text(`${inflection.titleize(this.side)} Axis:`)
41
+
42
+ // title
43
+ this.textInput(container, "title", {placeholder: "Title"})
44
+
45
+ // type
46
+ const typeOptions = this.side == 'right' ? rightAxisTypeOptions : axisTypeOptions
47
+ this.select(container, 'type', typeOptions)
48
+ })
49
+ }
50
+ }
51
+
52
+
53
+ const DivePlotAxes = {
54
+ axisSides
55
+ }
56
+ export default DivePlotAxes
@@ -3,12 +3,19 @@ import {ModalPart} from "../../terrier/modals"
3
3
  import {DiveEditorState} from "../dives/dive-editor"
4
4
  import {UnpersistedDdDivePlot} from "../gen/models"
5
5
  import {TerrierFormFields} from "../../terrier/forms"
6
- import {DivePlotTrace} from "./dive-plots"
7
6
  import Messages from "tuff-core/messages"
8
7
  import {Logger} from "tuff-core/logging"
9
8
  import DivePlotList from "./dive-plot-list"
10
9
  import Db from "../dd-db"
11
10
  import DivePlotRenderPart from "./dive-plot-render-part"
11
+ import {DivePlotAxis, DivePlotAxisFields} from "./dive-plot-axes"
12
+ import DivePlotTraces, {
13
+ DivePlotTrace,
14
+ DivePlotTraceEditor,
15
+ DivePlotTraceRow,
16
+ } from "./dive-plot-traces"
17
+ import Fragments from "../../terrier/fragments"
18
+ import Arrays from "tuff-core/arrays"
12
19
 
13
20
  const log = new Logger("DivePlotList")
14
21
 
@@ -16,13 +23,18 @@ export type DivePlotEditorState = DiveEditorState & {
16
23
  plot: UnpersistedDdDivePlot
17
24
  }
18
25
 
19
-
20
-
21
26
  export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
22
27
 
23
28
  plot!: UnpersistedDdDivePlot
24
29
  fields!: TerrierFormFields<UnpersistedDdDivePlot>
30
+
31
+ leftAxisFields!: DivePlotAxisFields
32
+ rightAxisFields!: DivePlotAxisFields
33
+ bottomAxisFields!: DivePlotAxisFields
34
+
35
+ newTraceKey = Messages.untypedKey()
25
36
  traces: DivePlotTrace[] = []
37
+
26
38
  renderPart!: DivePlotRenderPart
27
39
  saveKey = Messages.untypedKey()
28
40
 
@@ -35,11 +47,48 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
35
47
  else {
36
48
  this.setTitle("New Dive Plot")
37
49
  }
38
- this.setIcon("hub-plot")
50
+ this.setIcon("glyp-differential")
39
51
 
40
52
  this.fields = new TerrierFormFields<UnpersistedDdDivePlot>(this, this.plot)
41
53
 
54
+ // axis fields
55
+ const axes = this.plot.layout.axes || {}
56
+ const leftAxis: DivePlotAxis = axes['left'] || {type: 'number', title: ''}
57
+ this.leftAxisFields = new DivePlotAxisFields(this, leftAxis, 'left')
58
+ const rightAxis: DivePlotAxis = axes['right'] || {type: 'none', title: ''}
59
+ this.rightAxisFields = new DivePlotAxisFields(this, rightAxis, 'right')
60
+ const bottomAxis: DivePlotAxis = axes['bottom'] || {type: 'number', title: ''}
61
+ this.bottomAxisFields = new DivePlotAxisFields(this, bottomAxis, 'bottom')
62
+
63
+ // trace editors
42
64
  this.traces = this.plot.traces || []
65
+ this.updateTraces()
66
+
67
+ this.onClick(this.newTraceKey, _ => {
68
+ log.info("Showing new trace form")
69
+ const state = {
70
+ ...this.state,
71
+ trace: DivePlotTraces.blankTrace(),
72
+ onSave: (newTrace: DivePlotTrace) => this.addTrace(newTrace),
73
+ onDelete: (_: DivePlotTrace) => {}
74
+ }
75
+ this.app.showModal(DivePlotTraceEditor, state)
76
+ })
77
+
78
+ this.onClick(DivePlotTraces.editKey, m => {
79
+ const id = m.data.id
80
+ log.info(`Editing plot trace ${id}`)
81
+ const trace = this.traces.find(t => t.id==id)
82
+ if (trace) {
83
+ const state = {
84
+ ...this.state,
85
+ trace,
86
+ onSave: (newTrace: DivePlotTrace) => this.replaceTrace(newTrace),
87
+ onDelete: (trace: DivePlotTrace) => this.removeTrace(trace),
88
+ }
89
+ this.app.showModal(DivePlotTraceEditor, state)
90
+ }
91
+ })
43
92
 
44
93
  this.renderPart = this.makePart(DivePlotRenderPart, this.state)
45
94
 
@@ -55,23 +104,66 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
55
104
  })
56
105
  }
57
106
 
107
+ addTrace(trace: DivePlotTrace) {
108
+ this.traces.push(trace)
109
+ this.updateTraces()
110
+ }
111
+
112
+ replaceTrace(trace: DivePlotTrace) {
113
+ // TODO: implement this in tuff-core
114
+ log.info(`Replacing trace ${trace.id}`, trace)
115
+ this.traces = this.traces.map(t => t.id === trace.id ? trace : t)
116
+ this.updateTraces()
117
+ }
118
+
119
+ removeTrace(trace: DivePlotTrace) {
120
+ log.info(`Removing trace ${trace.id}`, trace)
121
+ this.traces = Arrays.compact(this.traces.map(t => t.id === trace.id ? null : t))
122
+ this.updateTraces()
123
+ }
124
+
125
+ updateTraces() {
126
+ this.assignCollection('traces', DivePlotTraceRow, this.traces || [])
127
+ this.dirty()
128
+ }
129
+
58
130
  renderContent(parent: PartTag): void {
59
- parent.div(".tt-flex.column.padded.gap", mainColumn => {
60
- this.fields.compoundField(mainColumn, field => {
61
- field.label(".required").text("Title")
62
- this.fields.textInput(field, 'title')
131
+ parent.div(".tt-flex.column.padded.large-gap", mainColumn => {
132
+
133
+ mainColumn.div(".dd-plot-axes-and-preview.tt-flex.column.gap", axesAndPreview => {
134
+ this.fields.compoundField(axesAndPreview, field => {
135
+ field.label(".required").text("Title")
136
+ this.fields.textInput(field, 'title', {class: 'shrink plot-title'})
137
+ }).class('plot-title-field')
138
+ axesAndPreview.div('.tt-flex.gap', row => {
139
+ this.leftAxisFields.render(row)
140
+ row.part(this.renderPart)
141
+ this.rightAxisFields.render(row)
142
+ })
143
+ this.bottomAxisFields.render(axesAndPreview)
63
144
  })
64
145
 
65
- mainColumn.part(this.renderPart)
66
146
 
67
147
  mainColumn.h3(".glyp-items").text("Traces")
68
148
 
149
+ this.renderCollection(mainColumn, 'traces')
150
+
151
+ mainColumn.div('.tt-flex.justify-center', row => {
152
+ Fragments.button(row, this.theme, "New Trace", 'glyp-plus')
153
+ }).emitClick(this.newTraceKey)
69
154
  })
70
155
  }
71
156
 
72
157
  async save() {
73
158
  const plotData = await this.fields.serialize()
74
- const plot = {...this.plot, title: plotData.title}
159
+ const plot = {...this.plot, title: plotData.title, traces: this.traces}
160
+
161
+ plot.layout.axes = {
162
+ left: await this.leftAxisFields.serialize(),
163
+ bottom: await this.bottomAxisFields.serialize(),
164
+ right: await this.rightAxisFields.serialize()
165
+ }
166
+
75
167
  log.info("Saving plot", plot)
76
168
 
77
169
  const res = await Db().upsert('dd_dive_plot', plot)
@@ -100,7 +100,7 @@ export default class DivePlotList extends TerrierPart<DiveEditorState> {
100
100
  render(parent: PartTag): any {
101
101
  this.renderCollection(parent, "plots")
102
102
 
103
- Fragments.button(parent, this.theme, "New Plot", "hub-plus", "secondary")
103
+ Fragments.button(parent, this.theme, "New Plot", "glyp-plus_outline", "secondary")
104
104
  .emitClick(this.newKey)
105
105
  }
106
106
 
@@ -0,0 +1,233 @@
1
+ import {MarkerStyle, TraceStyle, TraceType, YAxisName} from "tuff-plot/trace"
2
+ import TerrierFormPart from "../../terrier/parts/terrier-form-part"
3
+ import { PartTag } from "tuff-core/parts"
4
+ import {ModalPart} from "../../terrier/modals"
5
+ import Ids from "../../terrier/ids"
6
+ import {DivePlotEditorState} from "./dive-plot-editor"
7
+ import {TerrierFormFields} from "../../terrier/forms"
8
+ import {UnpersistedDdDivePlot} from "../gen/models"
9
+ import {SelectOptions} from "tuff-core/forms"
10
+ import Queries, {Query} from "../queries/queries"
11
+ import {Logger} from "tuff-core/logging"
12
+ import Columns from "../queries/columns";
13
+ import Messages from "tuff-core/messages";
14
+
15
+ const log = new Logger("DivePlotTraces")
16
+
17
+ /**
18
+ * Similar to the tuff-plot Trace but not strongly typed to the data type since it's dynamically assigned to a query.
19
+ */
20
+ export type DivePlotTrace = {
21
+ id: string
22
+ type: TraceType
23
+ title: string
24
+ query_id: string
25
+ x: string
26
+ y: string
27
+ y_axis: YAxisName
28
+ style?: TraceStyle
29
+ marker?: MarkerStyle
30
+ }
31
+
32
+ /**
33
+ * Create a new blank trace.
34
+ */
35
+ function blankTrace(): DivePlotTrace {
36
+ return {
37
+ id: Ids.makeUuid(),
38
+ type: 'scatter',
39
+ title: '',
40
+ query_id: '',
41
+ x: '',
42
+ y: '',
43
+ y_axis: 'left',
44
+ style: {}
45
+ }
46
+ }
47
+
48
+
49
+ /// Trace Style Fields
50
+
51
+ /**
52
+ * Form fields for editing trace style.
53
+ */
54
+ class TraceStyleFields extends TerrierFormFields<TraceStyle> {
55
+
56
+ render(parent: PartTag) {
57
+ parent.div('.tt-form.tt-flex.gap.wrap', container => {
58
+ this.compoundField(container, field => {
59
+ field.label().text("Color")
60
+ this.textInput(field, 'stroke', {placeholder: 'Color'})
61
+ })
62
+ this.compoundField(container, field => {
63
+ field.label().text("Width")
64
+ this.numberInput(field, 'strokeWidth', {placeholder: 'Width'})
65
+ })
66
+ this.compoundField(container, field => {
67
+ field.label().text("Dashes")
68
+ this.textInput(field, 'strokeDasharray', {placeholder: 'Dashes'})
69
+ })
70
+ })
71
+ }
72
+
73
+
74
+ }
75
+
76
+
77
+ /// Editor
78
+
79
+ export type DivePlotTraceState = DivePlotEditorState & {
80
+ trace: DivePlotTrace
81
+ onSave: (trace: DivePlotTrace) => any
82
+ onDelete: (trace: DivePlotTrace) => any
83
+ }
84
+
85
+ const editKey = Messages.typedKey<{ id: string }>()
86
+
87
+ /**
88
+ * Editor for a single plot trace.
89
+ */
90
+ export class DivePlotTraceEditor extends ModalPart<DivePlotTraceState> {
91
+
92
+ fields!: TerrierFormFields<DivePlotTrace>
93
+ queries: Query[] = []
94
+ plot!: UnpersistedDdDivePlot
95
+ trace!: DivePlotTrace
96
+
97
+ queryOptions!: SelectOptions
98
+ axisOptions: string[] = []
99
+
100
+ styleFields!: TraceStyleFields
101
+
102
+ saveKey = Messages.untypedKey()
103
+ deleteKey = Messages.untypedKey()
104
+
105
+ async init() {
106
+ this.setTitle("Plot Trace Editor")
107
+ this.setIcon("glyp-items")
108
+
109
+ this.plot = this.state.plot
110
+ this.trace = this.state.trace
111
+
112
+ this.queries = this.state.dive.query_data?.queries || []
113
+ this.queryOptions = this.queries.map(query => {
114
+ return {value: query.id, title: query.name}
115
+ }) || []
116
+
117
+ this.trace.query_id ||= this.queries.at(0)?.id || ''
118
+ this.updateAxisOptions(this.trace.query_id)
119
+
120
+ this.fields = new TerrierFormFields<DivePlotTrace>(this, this.state.trace)
121
+
122
+ this.styleFields = new TraceStyleFields(this, this.trace.style || {})
123
+
124
+ this.addAction({
125
+ title: "Save",
126
+ icon: "glyp-checkmark",
127
+ click: {key: this.saveKey}
128
+ })
129
+
130
+ this.addAction({
131
+ title: "Delete",
132
+ icon: "glyp-delete",
133
+ click: {key: this.deleteKey},
134
+ classes: ['alert']
135
+ }, 'secondary')
136
+
137
+ this.onClick(this.saveKey, _ => {
138
+ this.save()
139
+ })
140
+
141
+ this.onClick(this.deleteKey, _ => {
142
+ this.state.onDelete(this.trace)
143
+ this.pop()
144
+ })
145
+ }
146
+
147
+ /**
148
+ * Update the axis options based on the query.
149
+ */
150
+ updateAxisOptions(queryId: string) {
151
+ const query = this.queries.find(q => q.id == queryId)
152
+ this.axisOptions = []
153
+ if (query) {
154
+ log.info(`Computing axis options for query`, query)
155
+ Queries.eachColumn(query, (table, column) => {
156
+ this.axisOptions.push(Columns.computeSelectName(table, column))
157
+ })
158
+ }
159
+ else {
160
+ log.warn(`No query with id ${queryId}`)
161
+ }
162
+ this.dirty()
163
+ }
164
+
165
+ renderContent(parent: PartTag): void {
166
+ parent.div(".tt-form.tt-flex.large-gap.column.padded", mainColumn => {
167
+ // query
168
+ this.fields.compoundField(mainColumn, field => {
169
+ field.label().text("Query")
170
+ this.fields.select(field, 'query_id', this.queryOptions)
171
+ })
172
+
173
+ // axes
174
+ mainColumn.div('.tt-flex.gap', row => {
175
+ row.div('.tt-flex.column.gap', col => {
176
+ col.h3().text("X Column")
177
+ for (const c of this.axisOptions) {
178
+ this.fields.radioLabel(col, 'x', c, c)
179
+ }
180
+ })
181
+ row.div('.tt-flex.column.gap', col => {
182
+ col.h3().text("Y Column")
183
+ for (const c of this.axisOptions) {
184
+ this.fields.radioLabel(col, 'y', c, c)
185
+ }
186
+ })
187
+ })
188
+
189
+ // style
190
+ mainColumn.div('.tt-flex.gap.column', styleRow => {
191
+ styleRow.h3().text("Style")
192
+ this.styleFields.render(styleRow)
193
+ })
194
+ })
195
+ }
196
+
197
+ async save() {
198
+ const data = await this.fields.serialize()
199
+ this.trace = {...this.trace, ...data}
200
+ log.info("Saving plot trace", this.trace)
201
+ this.state.onSave(this.trace)
202
+ this.pop()
203
+ }
204
+
205
+ }
206
+
207
+
208
+ /// Row
209
+
210
+ /**
211
+ * Row for displaying a single plot trace.
212
+ */
213
+ export class DivePlotTraceRow extends TerrierFormPart<DivePlotTrace> {
214
+
215
+ render(parent: PartTag) {
216
+ const trace = this.state
217
+ parent.a('.dd-dive-plot-trace-row.tt-panel.padded', panel => {
218
+ panel.div('.panel-content.tt-flex.row.gap', content => {
219
+ content.div('.axes').text(`${trace.x} -> ${trace.y}`)
220
+ })
221
+ }).emitClick(editKey, {id: trace.id})
222
+ }
223
+
224
+ }
225
+
226
+
227
+ /// Export
228
+
229
+ const DivePlotTraces = {
230
+ blankTrace,
231
+ editKey
232
+ }
233
+ export default DivePlotTraces
@@ -1,27 +1,19 @@
1
-
2
- import {MarkerStyle, TraceStyle, TraceType, YAxisName} from "tuff-plot/trace"
3
- import {PlotLayout} from "tuff-plot/layout"
4
1
  import {DdDive, DdDivePlot} from "../gen/models"
5
2
  import Db from "../dd-db"
3
+ import {DivePlotAxis} from "./dive-plot-axes"
6
4
 
7
- /**
8
- * Similar to the tuff-plot Trace but not strongly typed to the data type since it's dynamically assigned to a query.
9
- */
10
- export type DivePlotTrace = {
11
- id: string
12
- type: TraceType
13
- title: string
14
- query_id: string
15
- x: string
16
- y: string
17
- y_axis: YAxisName
18
- style?: TraceStyle
19
- marker?: MarkerStyle
20
- }
21
5
 
22
- // maybe we'll add more in the future
23
- export type DivePlotLayout = PlotLayout
6
+ // mimic PlotLayout but with our own types
7
+ export type DivePlotLayout = {
8
+ pad?: number
9
+ axes?: {
10
+ left?: DivePlotAxis
11
+ top?: DivePlotAxis
12
+ right?: DivePlotAxis
13
+ bottom?: DivePlotAxis
14
+ }
24
15
 
16
+ }
25
17
  /**
26
18
  * Get all plots for the given dive.
27
19
  * @param dive
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.32.0",
7
+ "version": "4.32.2",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
package/terrier/forms.ts CHANGED
@@ -150,11 +150,26 @@ export class TerrierFormFields<T extends FormPartData> extends FormFields<T> {
150
150
  * @param fun a function that accepts the field as an argument, to actually populate the field
151
151
  */
152
152
  compoundField(parent: PartTag, fun: (field: DivTag) => any) {
153
- parent.div(".tt-compound-field", field => {
153
+ return parent.div(".tt-compound-field", field => {
154
154
  fun(field)
155
155
  })
156
156
  }
157
157
 
158
+ /**
159
+ * Makes a label with a radio and title span inside of it.
160
+ * @param parent
161
+ * @param name
162
+ * @param value
163
+ * @param title the title to put in the label span
164
+ * @param attrs attributes for the radio input
165
+ */
166
+ radioLabel<Key extends KeyOfType<T, string> & string>(parent: PartTag, name: Key, value: string, title?: string, attrs: InputTagAttrs = {}) {
167
+ return parent.label('.body-size', label => {
168
+ this.radio(label, name, value, attrs)
169
+ label.span().text(title || value)
170
+ })
171
+ }
172
+
158
173
  }
159
174
 
160
175
 
@@ -150,8 +150,6 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
150
150
  if (!this._breadcrumbs.length && !this._title?.length) return
151
151
 
152
152
  parent.h1('.breadcrumbs', h1 => {
153
- const crumbs = Array.from(this._breadcrumbs)
154
-
155
153
  // add a breadcrumb for the page title
156
154
  if (this._title?.length) {
157
155
  const titleCrumb: Action = {
@@ -164,10 +162,10 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
164
162
  if (this._titleClasses?.length) {
165
163
  titleCrumb.classes = this._titleClasses
166
164
  }
167
- crumbs.push(titleCrumb)
165
+ this.addBreadcrumb(titleCrumb)
168
166
  }
169
167
 
170
- this.app.theme.renderActions(h1, crumbs)
168
+ this.app.theme.renderActions(h1, Array.from(this._breadcrumbs))
171
169
  })
172
170
  }
173
171
 
package/terrier/theme.ts CHANGED
@@ -35,6 +35,7 @@ export type Action = {
35
35
  subtitle?: string
36
36
  tooltip?: string
37
37
  icon?: IconName
38
+ img?: string
38
39
  href?: string
39
40
  classes?: string[]
40
41
  click?: Packet
@@ -101,6 +102,9 @@ export default class Theme {
101
102
  if (action.icon?.length) {
102
103
  this.renderIcon(a, action.icon, iconColor)
103
104
  }
105
+ if (action.img?.length) {
106
+ a.img('.image', {src: action.img})
107
+ }
104
108
  if (action.title?.length) {
105
109
  a.div('.title', {text: action.title})
106
110
  }