Skip to content

Commit 6a577a8

Browse files
Add advanced settings
Add timeKey, dataKeys, labelKeys Improve time key searching and parsing
1 parent f646616 commit 6a577a8

File tree

4 files changed

+191
-92
lines changed

4 files changed

+191
-92
lines changed

src/SqlSeries.ts

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,30 @@ import { FieldType, MutableDataFrame, DataFrame, TableData, TimeSeries, DateTime
33
// eslint-disable-next-line no-restricted-imports
44
import moment from 'moment';
55

6+
type Meta = {
7+
name: string;
8+
type: string;
9+
};
10+
611
type SqlSeriesOptions = {
712
refId: string;
813
series: any[];
9-
keys: string[];
10-
meta: Array<{
11-
name: string;
12-
type: string;
13-
}>;
14+
meta: Meta[];
15+
dataKeys: string[];
16+
labelKeys: string[];
17+
timeKey: string;
1418
tillNow: boolean;
1519
from: DateTime;
1620
to: DateTime;
1721
};
1822

1923
export default class SqlSeries {
2024
private readonly refId: string;
21-
private readonly series: any[];
22-
private readonly keys: string[];
23-
private readonly meta: Array<{
24-
name: string;
25-
type: string;
26-
}>;
25+
private readonly series: Array<Record<string, any>>;
26+
private readonly meta: Meta[];
27+
private readonly timeKey: string;
28+
private readonly dataKeys: string[];
29+
private readonly labelKeys: string[];
2730
private readonly tillNow: boolean;
2831
private readonly from: number;
2932
private readonly to: number;
@@ -35,7 +38,22 @@ export default class SqlSeries {
3538
this.tillNow = options.tillNow;
3639
this.from = options.from.unix();
3740
this.to = options.to.unix();
38-
this.keys = options.keys || [];
41+
const allKeys = options.meta.map((m) => m.name);
42+
const timeKey =
43+
options.timeKey.length > 0 && allKeys.includes(options.timeKey)
44+
? options.timeKey
45+
: this.findColByFieldType(FieldType.time)?.name ?? this.findEpochCol() ?? '';
46+
const allNumberKeys = options.meta
47+
.filter((m) => this.toJSType(m.type) === 'number' && m.name !== timeKey)
48+
.map((m) => m.name);
49+
const allStringKeys = options.meta
50+
.filter((m) => this.toJSType(m.type) === 'string' && m.name !== timeKey)
51+
.map((m) => m.name);
52+
const dataKeys = options.dataKeys.filter((key) => allNumberKeys.includes(key));
53+
const labelKeys = options.labelKeys.filter((key) => allStringKeys.includes(key));
54+
this.timeKey = timeKey;
55+
this.dataKeys = dataKeys.length > 0 ? dataKeys : allNumberKeys;
56+
this.labelKeys = labelKeys.length === this.dataKeys.length ? labelKeys : [];
3957
}
4058

4159
toTable(): TableData[] {
@@ -125,21 +143,15 @@ export default class SqlSeries {
125143
}
126144

127145
const metrics = {};
128-
const timeCol = this.findColByFieldType(FieldType.time) ?? this.findColByFieldType(FieldType.number);
129146

130-
if (!timeCol) {
131-
throw new Error('Please select time column');
132-
}
133-
134-
let lastTimeStamp = this.formatTimeValue(this.series[0][timeCol.name]);
135-
const keyColumns = this.keys.filter((name) => name !== timeCol.name);
147+
let lastTimeStamp = this.formatTimeValue(this.series[0][this.timeKey]);
136148

137149
this.series.forEach((row) => {
138-
const t = this.formatTimeValue(row[timeCol.name]);
150+
const t = this.formatTimeValue(row[this.timeKey]);
139151
let metricKey: string | null = null;
140152

141-
if (keyColumns.length) {
142-
metricKey = keyColumns.map((name) => row[name]).join(', ');
153+
if (this.labelKeys.length) {
154+
metricKey = this.labelKeys.map((name) => row[name]).join(', ');
143155
}
144156

145157
if (lastTimeStamp < t) {
@@ -152,20 +164,16 @@ export default class SqlSeries {
152164
}
153165

154166
Object.entries(row).forEach(([key, val]) => {
155-
if ((!this.keys.length && timeCol.name === key) || this.keys.includes(key)) {
167+
if (this.timeKey === key || this.labelKeys.includes(key) || !this.dataKeys.includes(key)) {
156168
return;
157169
}
158170

159-
if (metricKey) {
160-
key = metricKey;
161-
}
162-
163171
if (Array.isArray(val)) {
164172
val.forEach((arr) => {
165173
this.pushDatapoint(metrics, t, arr[0], arr[1]);
166174
});
167175
} else {
168-
this.pushDatapoint(metrics, t, key, val as number);
176+
this.pushDatapoint(metrics, t, metricKey ?? key, val as number);
169177
}
170178
});
171179
});
@@ -221,7 +229,7 @@ export default class SqlSeries {
221229
metrics[key].push([this.formatValue(value), timestamp]);
222230
}
223231

224-
private toJSType(type: string): string {
232+
private toJSType(type: string): 'number' | 'string' {
225233
switch (type) {
226234
case 'UInt8':
227235
case 'UInt16':
@@ -309,13 +317,43 @@ export default class SqlSeries {
309317
}
310318
}
311319

320+
private findEpochCol() {
321+
const numCols = this.meta
322+
.filter(({ type }) => !type.includes('Float'))
323+
.filter(({ type }) => this.toFieldType(type) === FieldType.number)
324+
.map(({ name }) => name);
325+
326+
const firstRow = this.series[0];
327+
328+
if (!firstRow) {
329+
return null;
330+
}
331+
332+
return numCols
333+
.slice(1)
334+
.reduce((epochCol, col) => (firstRow[epochCol] < firstRow[col] ? col : epochCol), numCols[0]);
335+
}
336+
312337
private findColByFieldType(fieldType: FieldType) {
313338
return this.meta
314339
.filter(({ type }) => !type.includes('Float'))
315340
.find(({ type }) => this.toFieldType(type) === fieldType);
316341
}
317342

318343
private formatTimeValue(value: any) {
344+
// epoch is string
345+
if (!isNaN(Number(value))) {
346+
value = Number(value);
347+
}
348+
349+
// epoch is in seconds
350+
if (
351+
typeof value === 'number' &&
352+
Math.abs(+Date.now() - +new Date(Number(value))) >= Math.abs(+Date.now() - Number(value) * 1000)
353+
) {
354+
value *= 1000;
355+
}
356+
319357
return moment(value).valueOf();
320358
}
321359

src/components/QueryEditor.tsx

Lines changed: 113 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import React, { useEffect, useMemo, useState } from 'react';
2-
import { Icon, InlineField, InlineFieldRow, InlineSwitch, Select, stylesFactory, Tooltip, useTheme } from '@grafana/ui';
2+
import {
3+
Icon,
4+
IconButton,
5+
InlineField,
6+
InlineFieldRow,
7+
InlineSwitch,
8+
Input,
9+
Select,
10+
stylesFactory,
11+
Tooltip,
12+
useTheme,
13+
} from '@grafana/ui';
314
import { GrafanaTheme, QueryEditorProps, SelectableValue } from '@grafana/data';
415
import { css } from '@emotion/css';
516
import { DataSource } from '../datasource';
@@ -21,6 +32,7 @@ export function QueryEditor({
2132
}: QueryEditorProps<DataSource, TinybirdQuery, TinybirdOptions>) {
2233
const theme = useTheme();
2334
const styles = getStyles(theme);
35+
const [isOptionsOpen, setIsOptionsOpen] = useState(false);
2436
const [pipes, setPipes] = useState<TinybirdPipe[]>([]);
2537
const formatAsOptions: Array<SelectableValue<OutputFormat>> = SUPPORTED_OUTPUT_FORMATS.map((f) => ({
2638
label: capitalize(f),
@@ -85,21 +97,19 @@ export function QueryEditor({
8597
return (
8698
<div className={styles.root}>
8799
<InlineFieldRow>
88-
<InlineFieldRow>
89-
<InlineField label="API Endpoint" labelWidth={14} tooltip="Your published Tinybird API Endpoint">
90-
<Select
91-
width={50}
92-
options={pipeNameOptions}
93-
value={query.pipeName}
94-
onChange={({ value }) => {
95-
if (value) {
96-
onChange({ ...query, pipeName: value });
97-
}
98-
}}
99-
placeholder="ds"
100-
/>
101-
</InlineField>
102-
</InlineFieldRow>
100+
<InlineField label="API Endpoint" labelWidth={14} tooltip="Your published Tinybird API Endpoint">
101+
<Select
102+
width={50}
103+
options={pipeNameOptions}
104+
value={query.pipeName}
105+
onChange={({ value }) => {
106+
if (value) {
107+
onChange({ ...query, pipeName: value });
108+
}
109+
}}
110+
placeholder="ds"
111+
/>
112+
</InlineField>
103113

104114
<InlineField label="Format as" labelWidth={14}>
105115
<Select
@@ -115,7 +125,21 @@ export function QueryEditor({
115125
/>
116126
</InlineField>
117127

118-
{query.format === 'timeseries' && (
128+
<div className={styles.cogIconWrapper}>
129+
{query.format === 'timeseries' ? (
130+
<IconButton
131+
name="cog"
132+
variant={isOptionsOpen ? 'primary' : 'secondary'}
133+
onClick={() => setIsOptionsOpen((value) => !value)}
134+
/>
135+
) : (
136+
<></>
137+
)}
138+
</div>
139+
</InlineFieldRow>
140+
141+
{query.format === 'timeseries' && isOptionsOpen && (
142+
<InlineFieldRow>
119143
<InlineField
120144
label="Extrapolate"
121145
labelWidth={14}
@@ -126,58 +150,87 @@ export function QueryEditor({
126150
onChange={({ currentTarget: { value } }) => onChange({ ...query, extrapolate: !!value })}
127151
/>
128152
</InlineField>
129-
)}
130-
</InlineFieldRow>
153+
<InlineField label="Time key" labelWidth={16} tooltip="Time key of the data">
154+
<Input
155+
value={query.timeKey}
156+
onChange={({ currentTarget: { value } }) => onChange({ ...query, timeKey: value })}
157+
/>
158+
</InlineField>
159+
<InlineField label="Data keys" labelWidth={16} tooltip="Comma-separated keys to access values of the data">
160+
<Input
161+
value={query.dataKeys}
162+
onChange={({ currentTarget: { value } }) => onChange({ ...query, dataKeys: value })}
163+
/>
164+
</InlineField>
165+
<InlineField label="Label keys" labelWidth={16} tooltip="Comma-separated keys to access labels of the data">
166+
<Input
167+
value={query.labelKeys}
168+
onChange={({ currentTarget: { value } }) => onChange({ ...query, labelKeys: value })}
169+
/>
170+
</InlineField>
171+
</InlineFieldRow>
172+
)}
131173

132-
{Object.keys(query.paramOptions ?? {}).length > 0 && (
133-
<table className={styles.table}>
134-
<thead className={styles.thead}>
135-
<tr className={styles.row}>
136-
{['Name', 'Type', 'Value'].map((_, key) => (
137-
<th key={key} className={styles.th}>
138-
{_}
139-
</th>
140-
))}
141-
<th className={styles.th}></th>
142-
</tr>
143-
</thead>
144-
<tbody className={styles.tbody}>
145-
{Object.entries(query.paramOptions).map(([name, { type, description, required }], rowIdx) => (
146-
<tr key={rowIdx} className={styles.row}>
147-
<td className={styles.td}>
148-
<div className={styles.nameCol}>
149-
{name}
150-
{description && (
151-
<Tooltip content={description}>
152-
<Icon name="info-circle" />
153-
</Tooltip>
154-
)}
155-
</div>
156-
</td>
157-
<td className={styles.td}>
158-
{type}
159-
{required ? '*' : ''}
160-
</td>
161-
<td className={styles.td}>
162-
<input
163-
className={styles.input}
164-
value={query.params[name]}
165-
onChange={(e) => onChange({ ...query, params: { ...query.params, [name]: e.currentTarget.value } })}
166-
/>
167-
</td>
174+
<div className={styles.root}>
175+
{Object.keys(query.paramOptions ?? {}).length > 0 && (
176+
<table className={styles.table}>
177+
<thead className={styles.thead}>
178+
<tr className={styles.row}>
179+
{['Name', 'Type', 'Value'].map((_, key) => (
180+
<th key={key} className={styles.th}>
181+
{_}
182+
</th>
183+
))}
184+
<th className={styles.th}></th>
168185
</tr>
169-
))}
170-
</tbody>
171-
</table>
172-
)}
186+
</thead>
187+
<tbody className={styles.tbody}>
188+
{Object.entries(query.paramOptions).map(([name, { type, description, required }], rowIdx) => (
189+
<tr key={rowIdx} className={styles.row}>
190+
<td className={styles.td}>
191+
<div className={styles.nameCol}>
192+
{name}
193+
{description && (
194+
<Tooltip content={description}>
195+
<Icon name="info-circle" />
196+
</Tooltip>
197+
)}
198+
</div>
199+
</td>
200+
<td className={styles.td}>
201+
{type}
202+
{required ? '*' : ''}
203+
</td>
204+
<td className={styles.td}>
205+
<input
206+
className={styles.input}
207+
value={query.params[name]}
208+
onChange={(e) =>
209+
onChange({ ...query, params: { ...query.params, [name]: e.currentTarget.value } })
210+
}
211+
/>
212+
</td>
213+
</tr>
214+
))}
215+
</tbody>
216+
</table>
217+
)}
218+
</div>
173219
</div>
174220
);
175221
}
176222

177223
const getStyles = stylesFactory((theme: GrafanaTheme) => {
178224
return {
179225
root: css`
180-
margin-top: 2rem;
226+
margin-top: 1.5rem;
227+
`,
228+
cogIconWrapper: css`
229+
display: flex;
230+
align-items: center;
231+
position: relative;
232+
flex: 0 0 auto;
233+
margin: 0px 4px 4px 4px;
181234
`,
182235
table: css`
183236
table-layout: auto;

src/datasource.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export class DataSource extends DataSourceApi<TinybirdQuery, TinybirdOptions> {
3434
refId: target.refId,
3535
series: response.data,
3636
meta: response.meta,
37-
keys: ['t', 'job_name'],
37+
timeKey: target.timeKey,
38+
dataKeys: target.dataKeys.split(','),
39+
labelKeys: target.labelKeys.split(','),
3840
tillNow: options.rangeRaw?.to === 'now',
3941
from: options.range.from,
4042
to: options.range.to,

0 commit comments

Comments
 (0)