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.
- package/data-dive/dives/dive-delivery.ts +13 -6
- package/data-dive/dives/dive-editor.ts +11 -3
- package/data-dive/dives/dive-runs.ts +14 -15
- package/data-dive/gen/models.ts +4 -2
- package/data-dive/plots/dive-plot-axes.ts +85 -10
- package/data-dive/plots/dive-plot-editor.ts +54 -11
- package/data-dive/plots/dive-plot-layouts.ts +49 -0
- package/data-dive/plots/dive-plot-list.ts +19 -5
- package/data-dive/plots/dive-plot-render-part.ts +68 -1
- package/data-dive/plots/dive-plot-styles.ts +194 -0
- package/data-dive/plots/dive-plot-traces.ts +77 -51
- package/data-dive/plots/dive-plots.ts +11 -14
- package/data-dive/queries/columns.ts +33 -24
- package/data-dive/queries/filters.ts +144 -118
- package/data-dive/queries/tables.ts +1 -1
- package/package.json +2 -2
- package/terrier/forms.ts +1 -22
- package/terrier/ids.ts +13 -1
- package/terrier/schedules.ts +43 -48
|
@@ -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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
123
|
-
|
|
124
|
-
|
|
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<{
|
|
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.
|
|
101
|
-
max: dayjs(this.inputFields.data[`${m.data.
|
|
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.
|
|
107
|
-
this.inputFields.data[`${m.data.
|
|
108
|
-
this.inputFields.data[`${m.data.
|
|
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
|
-
|
|
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.
|
|
282
|
+
this.inputFields.numberInput(field, filter.id)
|
|
284
283
|
break
|
|
285
284
|
case 'number':
|
|
286
|
-
this.inputFields.numberInput(field, filter.
|
|
285
|
+
this.inputFields.numberInput(field, filter.id)
|
|
287
286
|
break
|
|
288
287
|
default:
|
|
289
|
-
this.inputFields.textInput(field, filter.
|
|
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.
|
|
295
|
+
this.inputFields.dateInput(field, `${filter.id}-min`)
|
|
297
296
|
field.label().text('→')
|
|
298
|
-
this.inputFields.dateInput(field, `${filter.
|
|
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, {
|
|
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.
|
|
310
|
+
this.inputFields.checkbox(label, `${filter.id}-${possible}`)
|
|
312
311
|
label.span().text(inflection.titleize(possible))
|
|
313
312
|
})
|
|
314
313
|
}
|
package/data-dive/gen/models.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// This file was automatically generated on 2024-08-
|
|
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-
|
|
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
|
-
|
|
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
|
|
23
|
+
const leftAxisTypeOptions = [
|
|
21
24
|
{value: 'number', title: 'Number'},
|
|
22
|
-
{value: '
|
|
23
|
-
{value: '
|
|
24
|
-
{value: '
|
|
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 =
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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.
|
|
151
|
+
this.traces = Arrays.without(this.traces, trace)
|
|
122
152
|
this.updateTraces()
|
|
123
153
|
}
|
|
124
154
|
|
|
125
155
|
updateTraces() {
|
|
126
|
-
|
|
127
|
-
this.
|
|
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
|
|
193
|
+
async serialize() {
|
|
157
194
|
const plotData = await this.fields.serialize()
|
|
158
|
-
const plot =
|
|
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.
|
|
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.
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
88
|
+
parent.part(this.plotPart)
|
|
22
89
|
}
|
|
23
90
|
}
|