terrier-engine 4.34.0 → 4.35.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.
@@ -1,7 +1,7 @@
1
1
  import {PartTag} from "tuff-core/parts"
2
2
  import DiveEditor, {DiveEditorState} from "./dive-editor"
3
3
  import TerrierPart from "../../terrier/parts/terrier-part"
4
- import {RegularSchedule, RegularScheduleForm} from "../../terrier/schedules"
4
+ import {RegularSchedule, RegularScheduleFields} from "../../terrier/schedules"
5
5
  import {Logger} from "tuff-core/logging"
6
6
  import {EmailListForm} from "../../terrier/emails"
7
7
  import {DdDiveRun} from "../gen/models"
@@ -24,17 +24,17 @@ export type DiveDeliverySettings = {
24
24
 
25
25
  export class DiveDeliveryForm extends TerrierPart<DiveEditorState> {
26
26
 
27
- scheduleForm!: RegularScheduleForm
27
+ scheduleFields!: RegularScheduleFields
28
28
  recipientsForm!: EmailListForm
29
29
  deliveryList!: DiveDeliveryList
30
30
 
31
31
  async init() {
32
32
  const schedule = this.state.dive.delivery_schedule || {schedule_type: 'none'}
33
- this.scheduleForm = this.makePart(RegularScheduleForm, schedule)
33
+ this.scheduleFields = new RegularScheduleFields(this, schedule)
34
34
  this.recipientsForm = this.makePart(EmailListForm, {emails: this.state.dive.delivery_recipients || []})
35
35
  this.deliveryList = this.makePart(DiveDeliveryList, this.state)
36
36
 
37
- this.listen('datachanged', this.scheduleForm.dataChangedKey, m => {
37
+ this.listen('datachanged', this.scheduleFields.dataChangedKey, m => {
38
38
  log.info(`Schedule form data changed`, m.data)
39
39
  this.emitMessage(DiveEditor.diveChangedKey, {})
40
40
  })
@@ -51,7 +51,7 @@ export class DiveDeliveryForm extends TerrierPart<DiveEditorState> {
51
51
 
52
52
  render(parent: PartTag): any {
53
53
  parent.h3(".glyp-setup").text("Schedule")
54
- parent.part(this.scheduleForm)
54
+ this.scheduleFields.render(parent)
55
55
  parent.h3(".glyp-users").text("Recipients")
56
56
  parent.part(this.recipientsForm)
57
57
  parent.div('.deliveries', deliveriesContainer => {
@@ -65,7 +65,14 @@ export class DiveDeliveryForm extends TerrierPart<DiveEditorState> {
65
65
  * Serializes just the fields needed for the delivery settings.
66
66
  */
67
67
  async serialize(): Promise<DiveDeliverySettings> {
68
- const delivery_schedule = await this.scheduleForm.serializeConcrete()
68
+ if (!this.isAttached) {
69
+ // it was never actually rendered
70
+ log.info("Skipping delivery setting serialization since it was never rendered", this)
71
+ const delivery_schedule = this.state.dive.delivery_schedule || {schedule_type: 'none'}
72
+ const delivery_recipients = this.state.dive.delivery_recipients || []
73
+ return {delivery_schedule, delivery_recipients}
74
+ }
75
+ const delivery_schedule = await this.scheduleFields.serializeConcrete()
69
76
  log.info(`Serialized ${delivery_schedule.schedule_type} delivery schedule`, delivery_schedule)
70
77
  const delivery_recipients = this.recipientsForm.state.emails
71
78
  return {delivery_schedule, delivery_recipients}
@@ -119,9 +119,17 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
119
119
  })
120
120
  })
121
121
 
122
- this.deliveryForm = this.settingsTabs.upsertTab({key: 'delivery', title: "Delivery", icon: "glyp-email"}, DiveDeliveryForm, this.state)
123
-
124
- this.plotList = this.settingsTabs.upsertTab({key: 'plots', title: "Plots", icon: "glyp-differential"}, DivePlotList, this.state)
122
+ this.plotList = this.settingsTabs.upsertTab({
123
+ key: 'plots',
124
+ title: "Plots",
125
+ icon: "glyp-differential"
126
+ }, DivePlotList, this.state)
127
+
128
+ this.deliveryForm = this.settingsTabs.upsertTab({
129
+ key: 'delivery',
130
+ title: "Delivery",
131
+ icon: "glyp-email"
132
+ }, DiveDeliveryForm, this.state)
125
133
  }
126
134
 
127
135
  /**
@@ -63,7 +63,7 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
63
63
  logList!: LogListPart
64
64
 
65
65
  startKey = Messages.untypedKey()
66
- pickDateKey = Messages.typedKey<{ input_key: string }>()
66
+ pickDateKey = Messages.typedKey<{ id: string }>()
67
67
 
68
68
  async init() {
69
69
  this.setTitle("Run Dive")
@@ -97,15 +97,15 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
97
97
 
98
98
  this.onClick(this.pickDateKey, m => {
99
99
  const initialRange = {
100
- min: this.inputFields.data[`${m.data.input_key}-min`] as DateLiteral,
101
- max: dayjs(this.inputFields.data[`${m.data.input_key}-max`]).add(1, 'day').format(Dates.literalFormat) as DateLiteral
100
+ min: this.inputFields.data[`${m.data.id}-min`] as DateLiteral,
101
+ max: dayjs(this.inputFields.data[`${m.data.id}-max`]).add(1, 'day').format(Dates.literalFormat) as DateLiteral
102
102
  } as LiteralDateRange
103
103
  this.toggleDropdown(DatePeriodPickerPart, {
104
104
  initial: initialRange,
105
105
  callback: (newRange: LiteralDateRange) => {
106
- log.info(`Picked date range for ${m.data.input_key}`, newRange)
107
- this.inputFields.data[`${m.data.input_key}-min`] = newRange.min
108
- this.inputFields.data[`${m.data.input_key}-max`] = dayjs(newRange.max).subtract(1, 'day').format(Dates.literalFormat)
106
+ log.info(`Picked date range for ${m.data.id}`, newRange)
107
+ this.inputFields.data[`${m.data.id}-min`] = newRange.min
108
+ this.inputFields.data[`${m.data.id}-max`] = dayjs(newRange.max).subtract(1, 'day').format(Dates.literalFormat)
109
109
  this.dirty()
110
110
  }} as DatePeriodPickerState,
111
111
  m.event.target
@@ -258,8 +258,7 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
258
258
 
259
259
  renderInput(parent: HtmlParentTag, filter: FilterInput) {
260
260
  parent.div('.dd-dive-run-input', col => {
261
- // don't show anything after the # and replace periods with spaces
262
- const title = inflection.titleize(filter.input_key.split('#')[0].split('.').join(' '))
261
+ const title = filter.input_name.split(' ')[0].replaceAll('.', ' ') // we don't need the operator
263
262
  col.label('.caption-size').text(title)
264
263
  switch (filter.filter_type) {
265
264
  case 'direct':
@@ -280,26 +279,26 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
280
279
  switch (filter.column_type) {
281
280
  case 'cents':
282
281
  field.label().text('$')
283
- this.inputFields.numberInput(field, filter.input_key)
282
+ this.inputFields.numberInput(field, filter.id)
284
283
  break
285
284
  case 'number':
286
- this.inputFields.numberInput(field, filter.input_key)
285
+ this.inputFields.numberInput(field, filter.id)
287
286
  break
288
287
  default:
289
- this.inputFields.textInput(field, filter.input_key)
288
+ this.inputFields.textInput(field, filter.id)
290
289
  }
291
290
  })
292
291
  }
293
292
 
294
293
  renderDateRangeInput(parent: HtmlParentTag, filter: DateRangeFilter & FilterInput) {
295
294
  parent.div('.tt-compound-field', field => {
296
- this.inputFields.dateInput(field, `${filter.input_key}-min`)
295
+ this.inputFields.dateInput(field, `${filter.id}-min`)
297
296
  field.label().text('→')
298
- this.inputFields.dateInput(field, `${filter.input_key}-max`)
297
+ this.inputFields.dateInput(field, `${filter.id}-max`)
299
298
  field.a('.icon-only', {title: "Pick a common date range"}, a => {
300
299
  a.i('.glyp-pick_date')
301
300
  })
302
- .emitClick(this.pickDateKey, {input_key: filter.input_key})
301
+ .emitClick(this.pickDateKey, {id: filter.id})
303
302
 
304
303
  })
305
304
  }
@@ -308,7 +307,7 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
308
307
  parent.div('.inclusion-radios', container => {
309
308
  for (const possible of filter.possible_values || []) {
310
309
  container.label('.body-size', label => {
311
- this.inputFields.checkbox(label, `${filter.input_key}-${possible}`)
310
+ this.inputFields.checkbox(label, `${filter.id}-${possible}`)
312
311
  label.span().text(inflection.titleize(possible))
313
312
  })
314
313
  }
@@ -1,4 +1,4 @@
1
- // This file was automatically generated on 2024-08-14 09:08:29 -0500, DO NOT EDIT IT MANUALLY!
1
+ // This file was automatically generated on 2024-08-21 07:43:58 -0500, DO NOT EDIT IT MANUALLY!
2
2
 
3
3
  import { Query } from "../queries/queries"
4
4
 
@@ -10,7 +10,7 @@ import { Attachment } from "../../terrier/attachments"
10
10
 
11
11
  import { RegularSchedule } from "../../terrier/schedules"
12
12
 
13
- import { DivePlotLayout } from "../plots/dive-plots"
13
+ import { DivePlotLayout } from "../plots/dive-plot-layouts"
14
14
 
15
15
  import { DivePlotTrace } from "../plots/dive-plot-traces"
16
16
 
@@ -35,6 +35,7 @@ export type DdDive = {
35
35
  sort_order?: number
36
36
  query_data?: { queries: Query[] }
37
37
  dive_types: string[]
38
+ delivery_mode?: string
38
39
  delivery_recipients?: string[]
39
40
  delivery_schedule?: RegularSchedule
40
41
  created_by?: DdUser
@@ -63,6 +64,7 @@ export type UnpersistedDdDive = {
63
64
  sort_order?: number
64
65
  query_data?: { queries: Query[] }
65
66
  dive_types: string[]
67
+ delivery_mode?: string
66
68
  delivery_recipients?: string[]
67
69
  delivery_schedule?: RegularSchedule
68
70
  created_by?: DdUser
@@ -1,34 +1,52 @@
1
- import {AxisType} from "tuff-plot/axis"
1
+ import {AxisType, PlotAxis} from "tuff-plot/axis"
2
2
  import {TerrierFormFields} from "../../terrier/forms"
3
3
  import {PartTag} from "tuff-core/parts"
4
4
  import TerrierPart from "../../terrier/parts/terrier-part"
5
5
  import {DbErrors} from "../../terrier/db-client"
6
6
  import * as inflection from "inflection"
7
+ import DivePlotEditor from "./dive-plot-editor"
7
8
 
8
9
 
9
10
  // let's not worry about a top axis for now
10
11
  const axisSides = ['left', 'bottom', 'right'] as const
11
12
  export type AxisSide = typeof axisSides[number]
12
13
 
13
- export type DivePlotAxisType = AxisType | 'none'
14
+ // we have some additional axis types that affect the tick format
15
+ export type DivePlotAxisType = AxisType | 'dollars' | 'days' | 'months' | 'none'
14
16
 
15
17
  export type DivePlotAxis = {
16
18
  type: DivePlotAxisType
17
19
  title: string
20
+ clampToZero?: boolean
18
21
  }
19
22
 
20
- const axisTypeOptions = [
23
+ const leftAxisTypeOptions = [
21
24
  {value: 'number', title: 'Number'},
22
- {value: 'time', title: 'Date/Time'},
23
- {value: 'group', title: 'Grouped Bars'},
24
- {value: 'stack', title: 'Stacked Bars'},
25
+ {value: 'dollars', title: 'Dollars'},
26
+ {value: 'days', title: 'Dates'},
27
+ {value: 'months', title: 'Months'}
25
28
  ]
26
29
 
30
+ // only allow vertical bars
31
+ const bottomAxisTypeOptions = leftAxisTypeOptions.concat([
32
+ {value: 'group', title: 'Grouped Bars'},
33
+ {value: 'stack', title: 'Stacked Bars'}
34
+ ])
35
+
27
36
  // the right axis isn't required
28
- const rightAxisTypeOptions = axisTypeOptions.concat([
37
+ const rightAxisTypeOptions = [
29
38
  {value: 'none', title: 'None'}
30
- ])
39
+ ].concat(leftAxisTypeOptions)
40
+
41
+ const axisTypeOptions = {
42
+ left: leftAxisTypeOptions,
43
+ right: rightAxisTypeOptions,
44
+ bottom: bottomAxisTypeOptions
45
+ } as const
31
46
 
47
+ /**
48
+ * Fields for a DivePlotAxis.
49
+ */
32
50
  export class DivePlotAxisFields extends TerrierFormFields<DivePlotAxis> {
33
51
 
34
52
  constructor(part: TerrierPart<any>, axis: DivePlotAxis, readonly side: AxisSide, errors?: DbErrors<DivePlotAxis>) {
@@ -41,16 +59,73 @@ export class DivePlotAxisFields extends TerrierFormFields<DivePlotAxis> {
41
59
 
42
60
  // title
43
61
  this.textInput(container, "title", {placeholder: "Title"})
62
+ .emitChange(DivePlotEditor.relayoutKey)
44
63
 
45
64
  // type
46
- const typeOptions = this.side == 'right' ? rightAxisTypeOptions : axisTypeOptions
65
+ const typeOptions = axisTypeOptions[this.side]
47
66
  this.select(container, 'type', typeOptions)
67
+ .emitChange(DivePlotEditor.relayoutKey)
68
+
69
+ if (this.side == 'left' || this.side == 'right') {
70
+ container.label('.caption-size', label => {
71
+ this.checkbox(label, 'clampToZero')
72
+ .emitChange(DivePlotEditor.relayoutKey)
73
+ label.div().text("Clamp to Zero")
74
+ }).data({tooltip: "Ensure that the axis range includes zero"})
75
+
76
+ }
48
77
  })
49
78
  }
50
79
  }
51
80
 
52
81
 
82
+ /**
83
+ * Convert a DivePlotAxis to a tuff-plot PlotAxis.
84
+ * @param diveAxis
85
+ * @return undefined if the axis type is 'none'
86
+ */
87
+ function toPlotAxis(diveAxis: DivePlotAxis): PlotAxis | undefined {
88
+ if (diveAxis.type == 'none') {
89
+ return undefined
90
+ }
91
+ let tickMode: PlotAxis['tickMode'] = 'auto'
92
+ let type: AxisType
93
+ let tickFormat: PlotAxis['tickFormat'] = '0.[0]a'
94
+ let hoverFormat = tickFormat
95
+ switch (diveAxis.type) {
96
+ case 'dollars':
97
+ type = 'number'
98
+ tickFormat = '($0.[0]a)'
99
+ hoverFormat = tickFormat
100
+ break
101
+ case 'days':
102
+ type = 'time'
103
+ tickFormat = 'MM/DD'
104
+ hoverFormat = tickFormat
105
+ break
106
+ case 'months':
107
+ type = 'time'
108
+ tickFormat = 'MMM'
109
+ tickMode = 'months'
110
+ hoverFormat = 'MM/DD' // we probably still want to see days on hover
111
+ break
112
+ default:
113
+ type = diveAxis.type
114
+ }
115
+ return {
116
+ type,
117
+ tickMode,
118
+ tickFormat,
119
+ hoverFormat,
120
+ title: diveAxis.title,
121
+ range: diveAxis.clampToZero ? 'auto_zero' : 'auto',
122
+ tickLength: 6
123
+ }
124
+ }
125
+
126
+
53
127
  const DivePlotAxes = {
54
- axisSides
128
+ axisSides,
129
+ toPlotAxis
55
130
  }
56
131
  export default DivePlotAxes
@@ -1,7 +1,7 @@
1
1
  import { PartTag } from "tuff-core/parts"
2
2
  import {ModalPart} from "../../terrier/modals"
3
3
  import {DiveEditorState} from "../dives/dive-editor"
4
- import {UnpersistedDdDivePlot} from "../gen/models"
4
+ import {DdDivePlot, UnpersistedDdDivePlot} from "../gen/models"
5
5
  import {TerrierFormFields} from "../../terrier/forms"
6
6
  import Messages from "tuff-core/messages"
7
7
  import {Logger} from "tuff-core/logging"
@@ -16,6 +16,7 @@ import DivePlotTraces, {
16
16
  } from "./dive-plot-traces"
17
17
  import Fragments from "../../terrier/fragments"
18
18
  import Arrays from "tuff-core/arrays"
19
+ import DivePlots from "./dive-plots"
19
20
 
20
21
  const log = new Logger("DivePlotList")
21
22
 
@@ -24,6 +25,7 @@ export type DivePlotEditorState = DiveEditorState & {
24
25
  }
25
26
 
26
27
  export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
28
+ static relayoutKey = Messages.untypedKey()
27
29
 
28
30
  plot!: UnpersistedDdDivePlot
29
31
  fields!: TerrierFormFields<UnpersistedDdDivePlot>
@@ -36,6 +38,7 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
36
38
  traces: DivePlotTrace[] = []
37
39
 
38
40
  renderPart!: DivePlotRenderPart
41
+ deleteKey = Messages.untypedKey()
39
42
  saveKey = Messages.untypedKey()
40
43
 
41
44
  async init() {
@@ -64,6 +67,7 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
64
67
  this.traces = this.plot.traces || []
65
68
  this.updateTraces()
66
69
 
70
+ // new trace
67
71
  this.onClick(this.newTraceKey, _ => {
68
72
  log.info("Showing new trace form")
69
73
  const state = {
@@ -75,6 +79,7 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
75
79
  this.app.showModal(DivePlotTraceEditor, state)
76
80
  })
77
81
 
82
+ // edit a trace
78
83
  this.onClick(DivePlotTraces.editKey, m => {
79
84
  const id = m.data.id
80
85
  log.info(`Editing plot trace ${id}`)
@@ -90,41 +95,73 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
90
95
  }
91
96
  })
92
97
 
98
+ // delete a trace
99
+ this.onClick(this.deleteKey, async _ => {
100
+ if (confirm("Are you sure you want to delete this plot?")) {
101
+ log.info("Deleting plot", this.plot)
102
+ await DivePlots.softDelete(this.plot as DdDivePlot)
103
+ this.emitMessage(DivePlotList.reloadKey, {})
104
+ this.successToast("Successfully Deleted Plot")
105
+ this.pop()
106
+ }
107
+ })
108
+
93
109
  this.renderPart = this.makePart(DivePlotRenderPart, this.state)
94
110
 
95
111
  this.addAction({
96
112
  title: "Save",
97
- icon: "hub-checkmark",
113
+ icon: "glyp-checkmark",
98
114
  click: {key: this.saveKey}
99
115
  })
100
116
 
117
+ if (this.plot.id?.length) {
118
+ this.addAction({
119
+ title: "Delete",
120
+ icon: "glyp-delete",
121
+ classes: ['alert'],
122
+ click: {key: this.deleteKey}
123
+ }, 'secondary')
124
+ }
125
+
101
126
  this.onClick(this.saveKey, _ => {
102
127
  log.debug("Saving plot", this.plot)
103
128
  this.save()
104
129
  })
130
+
131
+ this.onChange(DivePlotEditor.relayoutKey, m => {
132
+ log.info("Relayouting plot editor", m)
133
+ this.serialize().then(() => this.renderPart.relayout())
134
+ })
105
135
  }
106
136
 
107
137
  addTrace(trace: DivePlotTrace) {
138
+ log.info(`Adding trace ${trace.id}`, trace)
108
139
  this.traces.push(trace)
109
140
  this.updateTraces()
110
141
  }
111
142
 
112
143
  replaceTrace(trace: DivePlotTrace) {
113
- // TODO: implement this in tuff-core
114
144
  log.info(`Replacing trace ${trace.id}`, trace)
115
- this.traces = this.traces.map(t => t.id === trace.id ? trace : t)
145
+ Arrays.replaceBy(this.traces, trace, 'id')
116
146
  this.updateTraces()
117
147
  }
118
148
 
119
149
  removeTrace(trace: DivePlotTrace) {
120
150
  log.info(`Removing trace ${trace.id}`, trace)
121
- this.traces = Arrays.compact(this.traces.map(t => t.id === trace.id ? null : t))
151
+ this.traces = Arrays.without(this.traces, trace)
122
152
  this.updateTraces()
123
153
  }
124
154
 
125
155
  updateTraces() {
126
- this.assignCollection('traces', DivePlotTraceRow, this.traces || [])
127
- this.dirty()
156
+ let index = -1
157
+ const rowStates = (this.traces || []).map(trace => {
158
+ index += 1
159
+ return {...this.state, trace, index}
160
+ })
161
+ this.assignCollection('traces', DivePlotTraceRow, rowStates)
162
+ if (this.renderPart) {
163
+ this.renderPart.relayout()
164
+ }
128
165
  }
129
166
 
130
167
  renderContent(parent: PartTag): void {
@@ -153,9 +190,11 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
153
190
  })
154
191
  }
155
192
 
156
- async save() {
193
+ async serialize() {
157
194
  const plotData = await this.fields.serialize()
158
- const plot = {...this.plot, title: plotData.title, traces: this.traces}
195
+ const plot = this.plot
196
+ plot.title = plotData.title
197
+ plot.traces = this.traces
159
198
 
160
199
  plot.layout.axes = {
161
200
  left: await this.leftAxisFields.serialize(),
@@ -163,6 +202,12 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
163
202
  right: await this.rightAxisFields.serialize()
164
203
  }
165
204
 
205
+ return plot
206
+ }
207
+
208
+ async save() {
209
+ const plot = await this.serialize()
210
+
166
211
  log.info("Saving plot", plot)
167
212
 
168
213
  const res = await Db().upsert('dd_dive_plot', plot)
@@ -179,5 +224,3 @@ export default class DivePlotEditor extends ModalPart<DivePlotEditorState> {
179
224
 
180
225
  }
181
226
  }
182
-
183
-
@@ -0,0 +1,49 @@
1
+ import DivePlotAxes, {DivePlotAxis} from "./dive-plot-axes"
2
+ import {PlotLayout} from "tuff-plot/layout"
3
+
4
+ /**
5
+ * Same as PlotLayout but with our own types.
6
+ */
7
+ export type DivePlotLayout = {
8
+ axes?: {
9
+ left?: DivePlotAxis
10
+ top?: DivePlotAxis
11
+ right?: DivePlotAxis
12
+ bottom?: DivePlotAxis
13
+ }
14
+ }
15
+
16
+ /// Conversion
17
+
18
+ /**
19
+ * Convert a DivePlotLayout to a tuff-plot PlotLayout.
20
+ * @param layout
21
+ */
22
+ function toPlotLayout(diveLayout: DivePlotLayout): PlotLayout {
23
+ const tuffLayout: PlotLayout = {}
24
+ if (diveLayout.axes) {
25
+ tuffLayout.axes = {}
26
+ if (diveLayout.axes.left && diveLayout.axes.left.type !== 'none') {
27
+ tuffLayout.axes.left = DivePlotAxes.toPlotAxis(diveLayout.axes.left)
28
+ }
29
+ if (diveLayout.axes.top && diveLayout.axes.top.type !== 'none') {
30
+ tuffLayout.axes.top = DivePlotAxes.toPlotAxis(diveLayout.axes.top)
31
+ }
32
+ if (diveLayout.axes.right && diveLayout.axes.right.type !== 'none') {
33
+ tuffLayout.axes.right = DivePlotAxes.toPlotAxis(diveLayout.axes.right)
34
+ }
35
+ if (diveLayout.axes.bottom && diveLayout.axes.bottom.type !== 'none') {
36
+ tuffLayout.axes.bottom = DivePlotAxes.toPlotAxis(diveLayout.axes.bottom)
37
+ }
38
+ }
39
+ return tuffLayout
40
+ }
41
+
42
+
43
+ /// Exports
44
+
45
+ const DivePlotLayouts = {
46
+ toPlotLayout
47
+ }
48
+ export default DivePlotLayouts
49
+
@@ -25,14 +25,25 @@ class DivePlotPreview extends TerrierPart<DivePlotRenderState> {
25
25
  return ['dd-dive-plot-preview']
26
26
  }
27
27
 
28
+ reload() {
29
+ if (this.renderPart) {
30
+ this.renderPart.reload().then()
31
+ }
32
+ }
33
+
34
+ relayout() {
35
+ if (this.renderPart) {
36
+ this.renderPart.relayout()
37
+ }
38
+ }
39
+
28
40
  render(parent: PartTag) {
29
- parent.h3(".plot-title", title => {
41
+ parent.a(".plot-title.tt-flex.gap", title => {
30
42
  title.i('.shrink.icon-only.glyp-differential')
31
43
  title.div('.text-center.stretch').text(this.state.plot.title)
32
- title.a('.glyp-settings.icon-only')
33
- .data({tooltip: "Edit this plot"})
34
- .emitClick(editKey, {id: this.state.plot.id})
35
- })
44
+ title.i('.glyp-settings.shrink.icon-only')
45
+ }).data({tooltip: "Edit this plot"})
46
+ .emitClick(editKey, {id: this.state.plot.id})
36
47
  parent.part(this.renderPart)
37
48
  }
38
49
  }
@@ -93,6 +104,9 @@ export default class DivePlotList extends TerrierPart<DiveEditorState> {
93
104
 
94
105
  const plotStates = this.plots.map((plot) => {return {...this.state, plot}})
95
106
  this.assignCollection("plots", DivePlotPreview, plotStates)
107
+ this.getCollectionParts("plots").forEach(part => {
108
+ (part as DivePlotPreview).reload()
109
+ })
96
110
 
97
111
  this.dirty()
98
112
  }
@@ -2,6 +2,15 @@ import TerrierPart from "../../terrier/parts/terrier-part"
2
2
  import {PartTag} from "tuff-core/parts"
3
3
  import {DivePlotEditorState} from "./dive-plot-editor"
4
4
  import {UnpersistedDdDivePlot} from "../gen/models"
5
+ import Queries, {QueryResult} from "../queries/queries"
6
+ import {Logger} from "tuff-core/logging"
7
+ import Arrays from "tuff-core/arrays"
8
+ import {PlotPart} from "tuff-plot/part"
9
+ import DivePlotLayouts from "./dive-plot-layouts"
10
+ import DivePlotTraces from "./dive-plot-traces"
11
+ import DiveEditor from "../dives/dive-editor";
12
+
13
+ const log = new Logger("DivePlotRenderPart")
5
14
 
6
15
  export type DivePlotRenderState = DivePlotEditorState & {
7
16
  plot: UnpersistedDdDivePlot
@@ -12,12 +21,70 @@ export type DivePlotRenderState = DivePlotEditorState & {
12
21
  */
13
22
  export default class DivePlotRenderPart extends TerrierPart<DivePlotRenderState> {
14
23
 
24
+ previewData: Record<string,QueryResult> = {}
25
+
26
+ plotPart!: PlotPart
27
+
28
+ async init() {
29
+
30
+ this.plotPart = this.makePart(PlotPart, {layout: {}, traces: []})
31
+
32
+
33
+ this.listenMessage(DiveEditor.diveChangedKey, _ => {
34
+ log.info("Dive changed, reloading plot")
35
+ this.reload().then()
36
+ }, {attach: 'passive'})
37
+
38
+ await this.reload()
39
+ }
40
+
41
+ relayout() {
42
+ if (this.plotPart) {
43
+ log.info(`Relayouting plot`, this)
44
+ // convert the layout to its tuff-plot equivalents
45
+ this.plotPart.state.layout = DivePlotLayouts.toPlotLayout(this.state.plot.layout)
46
+
47
+ // convert the traces to tuff-plot traces
48
+ if (this.previewData) {
49
+ this.plotPart.state.traces = Arrays.compact(this.state.plot.traces.map(t => {
50
+ const queryResult = this.previewData[t.query_id]
51
+ if (queryResult == null) {
52
+ return null
53
+ }
54
+ return DivePlotTraces.toPlotTrace(t, queryResult)
55
+ }))
56
+ log.info(`Generated ${this.plotPart.state.traces.length} traces`, this.plotPart.state.traces)
57
+ }
58
+
59
+ // force the plot to update its layout
60
+ this.plotPart.relayout()
61
+ }
62
+ }
63
+
64
+ async reload() {
65
+ // determine which queries are needed
66
+ const queryIds = Arrays.unique(this.state.plot.traces.map(t => t.query_id))
67
+ if (!queryIds.length) {
68
+ return
69
+ }
70
+ const queries = this.state.dive.query_data?.queries.filter(q => queryIds.includes(q.id)) || []
71
+
72
+ // get the preview data
73
+ log.info(`Generating preview for queries`, queries)
74
+ this.startLoading()
75
+ for (const query of queries) {
76
+ this.previewData[query.id] = await Queries.preview(query)
77
+ }
78
+
79
+ this.stopLoading()
80
+ this.relayout()
81
+ }
15
82
 
16
83
  get parentClasses(): Array<string> {
17
84
  return ['dd-dive-plot-render']
18
85
  }
19
86
 
20
87
  render(parent: PartTag) {
21
- parent.div().text(`${this.state.plot.title} Render`)
88
+ parent.part(this.plotPart)
22
89
  }
23
90
  }