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.
- package/data-dive/gen/models.ts +3 -1
- package/data-dive/plots/dive-plot-axes.ts +56 -0
- package/data-dive/plots/dive-plot-editor.ts +102 -10
- package/data-dive/plots/dive-plot-list.ts +1 -1
- package/data-dive/plots/dive-plot-traces.ts +233 -0
- package/data-dive/plots/dive-plots.ts +11 -19
- package/package.json +1 -1
- package/terrier/forms.ts +16 -1
- package/terrier/parts/page-part.ts +2 -4
- package/terrier/theme.ts +4 -0
package/data-dive/gen/models.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// This file was automatically generated on 2024-07-17
|
|
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("
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
this.fields.
|
|
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", "
|
|
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
|
-
//
|
|
23
|
-
export type DivePlotLayout =
|
|
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
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
|
-
|
|
165
|
+
this.addBreadcrumb(titleCrumb)
|
|
168
166
|
}
|
|
169
167
|
|
|
170
|
-
this.app.theme.renderActions(h1,
|
|
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
|
}
|