terrier-engine 4.5.3 → 4.6.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/dives/dive-runs.ts +25 -3
- package/data-dive/queries/queries.ts +11 -12
- package/package.json +1 -1
- package/terrier/api.ts +3 -13
- package/terrier/dropdowns.ts +1 -1
- package/terrier/logging.ts +75 -0
- package/terrier/overlays.ts +15 -17
- package/terrier/progress.ts +10 -0
|
@@ -16,6 +16,7 @@ import inflection from "inflection"
|
|
|
16
16
|
import Dates, {DateLiteral, DatePeriodPickerPart, DatePeriodPickerState, LiteralDateRange} from "../queries/dates"
|
|
17
17
|
import dayjs from "dayjs"
|
|
18
18
|
import {ProgressBarPart} from "../../terrier/progress";
|
|
19
|
+
import {LogListPart} from "../../terrier/logging";
|
|
19
20
|
|
|
20
21
|
const log = new Logger("DiveRuns")
|
|
21
22
|
|
|
@@ -26,6 +27,10 @@ type RunQueryResult = {
|
|
|
26
27
|
message?: string
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
type InitRunResult = {
|
|
31
|
+
total_steps: number
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
const statusIcons: Record<RunQueryResult['status'], IconName> = {
|
|
30
35
|
pending: 'glyp-pending',
|
|
31
36
|
success: 'glyp-complete',
|
|
@@ -48,6 +53,7 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
48
53
|
fileOutput?: RunFileOutput
|
|
49
54
|
queryResults: Record<string, RunQueryResult> = {}
|
|
50
55
|
progressBar!: ProgressBarPart
|
|
56
|
+
logList!: LogListPart
|
|
51
57
|
|
|
52
58
|
startKey = messages.untypedKey()
|
|
53
59
|
pickDateKey = messages.typedKey<{ input_key: string }>()
|
|
@@ -58,9 +64,8 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
58
64
|
|
|
59
65
|
this.schema = await Schema.get()
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.progressBar = this.makePart(ProgressBarPart, {total})
|
|
67
|
+
this.progressBar = this.makePart(ProgressBarPart, {total: 10})
|
|
68
|
+
this.logList = this.makePart(LogListPart, {})
|
|
64
69
|
|
|
65
70
|
// initialize the inputs
|
|
66
71
|
this.filters = Dives.computeFilterInputs(this.schema, this.state.dive)
|
|
@@ -123,6 +128,7 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
123
128
|
const res = await Db().upsert('dd_dive_run', newRun)
|
|
124
129
|
if (res.status == 'success' && res.record) {
|
|
125
130
|
this.beginStreaming(res.record)
|
|
131
|
+
this.logList.info(`Created dive run`)
|
|
126
132
|
} else {
|
|
127
133
|
this.alertToast(`Error creating dive run: ${res.message}`)
|
|
128
134
|
}
|
|
@@ -133,6 +139,11 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
133
139
|
beginStreaming(run: DdDiveRun) {
|
|
134
140
|
this.run = run
|
|
135
141
|
Api.stream(`/data_dive/stream_run/${this.run.id}`)
|
|
142
|
+
.on<InitRunResult>('init_run', res => {
|
|
143
|
+
this.progressBar.setTotal(res.total_steps)
|
|
144
|
+
log.info(`Total steps for run: ${res.total_steps}`)
|
|
145
|
+
this.dirty()
|
|
146
|
+
})
|
|
136
147
|
.on<RunQueryResult>('query_result', res => {
|
|
137
148
|
this.queryResults[res.id] = res
|
|
138
149
|
this.progressBar.increment()
|
|
@@ -144,6 +155,12 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
144
155
|
this.progressBar.complete('success')
|
|
145
156
|
this.dirty()
|
|
146
157
|
})
|
|
158
|
+
.onLog(evt => {
|
|
159
|
+
this.progressBar.increment()
|
|
160
|
+
log.log(evt.level, evt.message)
|
|
161
|
+
this.logList.push(evt)
|
|
162
|
+
this.dirty()
|
|
163
|
+
})
|
|
147
164
|
.onError(evt => {
|
|
148
165
|
this.error = evt
|
|
149
166
|
this.stopActionLoading()
|
|
@@ -160,6 +177,8 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
160
177
|
renderContent(parent: PartTag): void {
|
|
161
178
|
parent.div('.tt-flex.padded.gap.column', col => {
|
|
162
179
|
col.part(this.progressBar)
|
|
180
|
+
|
|
181
|
+
// inputs and outputs row
|
|
163
182
|
col.div('.tt-flex.collapsible.gap.tt-form', row => {
|
|
164
183
|
// inputs
|
|
165
184
|
row.div('.tt-flex.column.shrink.dd-dive-run-inputs', col => {
|
|
@@ -197,6 +216,9 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
197
216
|
}
|
|
198
217
|
})
|
|
199
218
|
})
|
|
219
|
+
|
|
220
|
+
// log
|
|
221
|
+
col.part(this.logList)
|
|
200
222
|
})
|
|
201
223
|
}
|
|
202
224
|
|
|
@@ -50,24 +50,19 @@ async function validate(query: Query): Promise<QueryValidation> {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
////////////////////////////////////////////////////////////////////////////////
|
|
54
|
-
// Inputs
|
|
55
|
-
////////////////////////////////////////////////////////////////////////////////
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
53
|
////////////////////////////////////////////////////////////////////////////////
|
|
60
54
|
// Preview
|
|
61
55
|
////////////////////////////////////////////////////////////////////////////////
|
|
62
56
|
|
|
63
57
|
export type QueryResultRow = Record<string, any>
|
|
64
58
|
|
|
65
|
-
const ColumnTypes = ['string', 'integer', 'float', 'date', '
|
|
59
|
+
const ColumnTypes = ['string', 'integer', 'float', 'date', 'datetime', 'cents', 'dollars'] as const
|
|
66
60
|
|
|
67
61
|
export type ColumnType = typeof ColumnTypes[number]
|
|
68
62
|
|
|
69
63
|
export type QueryResultColumn = {
|
|
70
|
-
|
|
64
|
+
select_name: string
|
|
65
|
+
column_name: string
|
|
71
66
|
type: ColumnType
|
|
72
67
|
}
|
|
73
68
|
|
|
@@ -102,15 +97,19 @@ function renderCell(td: TableCellTag, col: QueryResultColumn, val: any): any {
|
|
|
102
97
|
case 'date':
|
|
103
98
|
const date = dayjs(val.toString())
|
|
104
99
|
return td.div('.date').text(date.format(DateFormat))
|
|
105
|
-
case '
|
|
100
|
+
case 'datetime':
|
|
106
101
|
const time = dayjs(val.toString())
|
|
107
102
|
td.div('.date').text(time.format(DateFormat))
|
|
108
103
|
return td.div('.time').text(time.format(TimeFormat))
|
|
109
104
|
case 'dollars':
|
|
110
105
|
const dollars = parseFloat(val)
|
|
111
106
|
return td.div('.dollars').text(`\$${dollars}`)
|
|
107
|
+
case 'cents':
|
|
108
|
+
const cents = parseInt(val)
|
|
109
|
+
const d = (cents/100.0).toFixed(2)
|
|
110
|
+
return td.div('.dollars').text(`\$${d}`)
|
|
112
111
|
case 'string':
|
|
113
|
-
if (col.
|
|
112
|
+
if (col.select_name.endsWith('id')) {
|
|
114
113
|
const id = val.toString()
|
|
115
114
|
td.a('.id')
|
|
116
115
|
.data({tooltip: id})
|
|
@@ -134,7 +133,7 @@ function renderTable(parent: PartTag, rows: QueryResultRow[], columns: QueryResu
|
|
|
134
133
|
thead.tr(tr => {
|
|
135
134
|
for (const col of columns) {
|
|
136
135
|
tr.th(col.type, th => {
|
|
137
|
-
th.a().text(col.
|
|
136
|
+
th.a().text(col.select_name)
|
|
138
137
|
})
|
|
139
138
|
}
|
|
140
139
|
})
|
|
@@ -146,7 +145,7 @@ function renderTable(parent: PartTag, rows: QueryResultRow[], columns: QueryResu
|
|
|
146
145
|
tbody.tr(tr => {
|
|
147
146
|
for (const col of columns) {
|
|
148
147
|
tr.td(td => {
|
|
149
|
-
const val = row[col.
|
|
148
|
+
const val = row[col.select_name]
|
|
150
149
|
if (val) {
|
|
151
150
|
renderCell(td, col, val)
|
|
152
151
|
}
|
package/package.json
CHANGED
package/terrier/api.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Logger } from "tuff-core/logging"
|
|
2
2
|
import { QueryParams } from "tuff-core/urls"
|
|
3
|
+
import {LogEntry} from "./logging"
|
|
3
4
|
|
|
4
5
|
const log = new Logger('Api')
|
|
5
6
|
log.level = 'debug'
|
|
@@ -113,17 +114,6 @@ async function post<ResponseType>(url: string, body: Record<string, unknown> | F
|
|
|
113
114
|
// Event Streams
|
|
114
115
|
////////////////////////////////////////////////////////////////////////////////
|
|
115
116
|
|
|
116
|
-
type LogLevel = 'success' | 'info' | 'warn' | 'debug'
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Type of log events from a streaming response.
|
|
120
|
-
*/
|
|
121
|
-
export type LogEvent = {
|
|
122
|
-
level: LogLevel
|
|
123
|
-
prefix?: string
|
|
124
|
-
message: string
|
|
125
|
-
}
|
|
126
|
-
|
|
127
117
|
/**
|
|
128
118
|
* Type of error events from a streaming response.
|
|
129
119
|
*/
|
|
@@ -188,8 +178,8 @@ export class Streamer {
|
|
|
188
178
|
* Listen specifically for log events.
|
|
189
179
|
* @param listener
|
|
190
180
|
*/
|
|
191
|
-
onLog(listener: (event:
|
|
192
|
-
return this.on<
|
|
181
|
+
onLog(listener: (event: LogEntry) => any) {
|
|
182
|
+
return this.on<LogEntry>('_log', listener)
|
|
193
183
|
}
|
|
194
184
|
|
|
195
185
|
/**
|
package/terrier/dropdowns.ts
CHANGED
|
@@ -77,7 +77,7 @@ export abstract class Dropdown<TState> extends TerrierPart<TState> {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
update(_elem: HTMLElement) {
|
|
80
|
-
const content = _elem.querySelector('.tt-dropdown-content')
|
|
80
|
+
const content: HTMLElement | null = _elem.querySelector('.tt-dropdown-content')
|
|
81
81
|
if (content) {
|
|
82
82
|
if (this.anchorTarget) {
|
|
83
83
|
log.info(`Anchoring dropdown`, content, this.anchorTarget)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import TerrierPart from "./parts/terrier-part"
|
|
2
|
+
import {NoState, PartTag} from "tuff-core/parts"
|
|
3
|
+
|
|
4
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
// Types
|
|
6
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
7
|
+
|
|
8
|
+
export type LogLevel = 'success' | 'info' | 'warn' | 'error' | 'debug'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Type of log events from a streaming response.
|
|
12
|
+
*/
|
|
13
|
+
export type LogEntry = {
|
|
14
|
+
level: LogLevel
|
|
15
|
+
prefix?: string
|
|
16
|
+
message: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
21
|
+
// List Part
|
|
22
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
23
|
+
|
|
24
|
+
export class LogListPart extends TerrierPart<NoState> {
|
|
25
|
+
|
|
26
|
+
entries: LogEntry[] = []
|
|
27
|
+
|
|
28
|
+
async init() {
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
render(parent: PartTag): any {
|
|
32
|
+
parent.div('.tt-log-entries', entries => {
|
|
33
|
+
for (const entry of this.entries) {
|
|
34
|
+
this.renderEntry(entries, entry)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pushes an entry to the list and renders it.
|
|
41
|
+
* @todo make this more performant for large lists!
|
|
42
|
+
* @param entry
|
|
43
|
+
*/
|
|
44
|
+
push(entry: LogEntry) {
|
|
45
|
+
this.entries.push(entry)
|
|
46
|
+
this.dirty()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
success(message: string, prefix: string | undefined) {
|
|
50
|
+
this.push({message, prefix, level: "success"})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
info(message: string, prefix?: string) {
|
|
54
|
+
this.push({message, prefix, level: "info"})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
warn(message: string, prefix?: string) {
|
|
58
|
+
this.push({message, prefix, level: "warn"})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
error(message: string, prefix?: string) {
|
|
62
|
+
this.push({message, prefix, level: "error"})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
debug(message: string, prefix?: string) {
|
|
66
|
+
this.push({message, prefix, level: "debug"})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
renderEntry(parent: PartTag, entry: LogEntry) {
|
|
70
|
+
parent.div(`.log-entry.${entry.level}`, row => {
|
|
71
|
+
row.div('.message').text(entry.message)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
}
|
package/terrier/overlays.ts
CHANGED
|
@@ -164,15 +164,24 @@ type AnchorResult = {
|
|
|
164
164
|
valid: boolean
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
function
|
|
167
|
+
function clampAnchorResult(result: AnchorResult, size: Size, container: Size) {
|
|
168
168
|
if (container.width < size.width) {
|
|
169
169
|
result.width = container.width
|
|
170
170
|
}
|
|
171
171
|
const rightOffset = container.width - (result.width ?? size.width) // don't hang off the right side of the viewport
|
|
172
|
-
result.left = Math.max(0, Math.min(result.left, rightOffset))
|
|
172
|
+
result.left = Math.max(0, Math.min(result.left, rightOffset))
|
|
173
173
|
if (result.top < 0 || result.top+size.height > container.height) {
|
|
174
174
|
result.valid = false
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
if (container.height < size.height) {
|
|
178
|
+
result.height = container.height
|
|
179
|
+
}
|
|
180
|
+
const bottomOffset = container.height - (result.height ?? size.height) // don't hang off the bottom of the viewport
|
|
181
|
+
result.top = Math.max(0, Math.min(result.top, bottomOffset))
|
|
182
|
+
if (result.left < 0 || result.left + size.width > container.width) {
|
|
183
|
+
result.valid = false
|
|
184
|
+
}
|
|
176
185
|
}
|
|
177
186
|
|
|
178
187
|
function anchorBoxBottom(size: Size, anchor: Box, container: Size): AnchorResult {
|
|
@@ -181,7 +190,7 @@ function anchorBoxBottom(size: Size, anchor: Box, container: Size): AnchorResult
|
|
|
181
190
|
left: anchor.x + anchor.width / 2 - size.width / 2,
|
|
182
191
|
valid: true
|
|
183
192
|
}
|
|
184
|
-
|
|
193
|
+
clampAnchorResult(result, size, container)
|
|
185
194
|
return result
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -191,28 +200,17 @@ function anchorBoxTop(size: Size, anchor: Box, container: Size): AnchorResult {
|
|
|
191
200
|
left: anchor.x + anchor.width / 2 - size.width / 2,
|
|
192
201
|
valid: true
|
|
193
202
|
}
|
|
194
|
-
|
|
203
|
+
clampAnchorResult(result, size, container)
|
|
195
204
|
return result
|
|
196
205
|
}
|
|
197
206
|
|
|
198
|
-
function clampVertical(result: AnchorResult, size: Size, container: Size) {
|
|
199
|
-
if (container.height < size.height) {
|
|
200
|
-
result.height = container.height
|
|
201
|
-
}
|
|
202
|
-
const bottomOffset = container.height - (result.height ?? size.height) // don't hang off the bottom of the viewport
|
|
203
|
-
result.top = Math.max(0, Math.min(result.top, bottomOffset)) // don't hang off the top of the viewport
|
|
204
|
-
if (result.left < 0 || result.left + size.width > container.width) {
|
|
205
|
-
result.valid = false
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
207
|
function anchorBoxLeft(size: Size, anchor: Box, container: Size): AnchorResult {
|
|
210
208
|
const result = {
|
|
211
209
|
top: anchor.y + anchor.height/2 - size.height / 2,
|
|
212
210
|
left: anchor.x - size.width,
|
|
213
211
|
valid: true
|
|
214
212
|
}
|
|
215
|
-
|
|
213
|
+
clampAnchorResult(result, size, container)
|
|
216
214
|
return result
|
|
217
215
|
}
|
|
218
216
|
|
|
@@ -222,7 +220,7 @@ function anchorBoxRight(size: Size, anchor: Box, container: Size): AnchorResult
|
|
|
222
220
|
left: anchor.x + anchor.width,
|
|
223
221
|
valid: true
|
|
224
222
|
}
|
|
225
|
-
|
|
223
|
+
clampAnchorResult(result, size, container)
|
|
226
224
|
return result
|
|
227
225
|
}
|
|
228
226
|
|
package/terrier/progress.ts
CHANGED
|
@@ -14,6 +14,16 @@ export class ProgressBarPart extends TerrierPart<ProgressState> {
|
|
|
14
14
|
progress = 0
|
|
15
15
|
color: ColorName = 'primary'
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Set the total number of steps.
|
|
19
|
+
* @param total
|
|
20
|
+
*/
|
|
21
|
+
setTotal(total: number) {
|
|
22
|
+
this.state.total = total
|
|
23
|
+
this.progress = Math.min(this.progress, this.state.total)
|
|
24
|
+
this.dirty()
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
/**
|
|
18
28
|
* Sets the progress and (optionally) color, then dirties the part.
|
|
19
29
|
* @param progress
|