Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support format dropdown and support for rendering traces #329

Merged
merged 5 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 11 additions & 15 deletions pkg/converters/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,26 +201,26 @@ var Converters = map[string]Converter{
convert: decimalNullConvert,
},
"Tuple()": {
fieldType: data.FieldTypeNullableString,
fieldType: data.FieldTypeNullableJSON,
scanType: reflect.TypeOf((*interface{})(nil)).Elem(),
matchRegex: tupleMatch,
convert: jsonConverter,
},
// NestedConverter currently only supports flatten_nested=0 only which can be marshalled into []map[string]interface{}
"Nested()": {
fieldType: data.FieldTypeNullableString,
fieldType: data.FieldTypeNullableJSON,
scanType: reflect.TypeOf([]map[string]interface{}{}),
matchRegex: nestedMatch,
convert: jsonConverter,
},
"Array()": {
fieldType: data.FieldTypeNullableString,
fieldType: data.FieldTypeNullableJSON,
scanType: reflect.TypeOf((*interface{})(nil)).Elem(),
matchRegex: complexArrayMatch,
convert: jsonConverter,
},
"Map()": {
fieldType: data.FieldTypeNullableString,
fieldType: data.FieldTypeNullableJSON,
scanType: reflect.TypeOf((*interface{})(nil)).Elem(),
matchRegex: mapMatch,
convert: jsonConverter,
Expand Down Expand Up @@ -288,24 +288,20 @@ func createConverter(name string, converter Converter) sqlutil.Converter {
}
}

// MarshalJSON marshals the enum as a quoted json string
func marshalJSON(in interface{}) (string, error) {
jBytes, err := json.Marshal(in)
if err != nil {
return "", err
}
return string(jBytes), nil
}

func jsonConverter(in interface{}) (interface{}, error) {
if in == nil {
return (*string)(nil), nil
}
json, err := marshalJSON(in)
jBytes, err := json.Marshal(in)
if err != nil {
return nil, err
}
var rawJSON json.RawMessage
err = json.Unmarshal(jBytes, &rawJSON)
if err != nil {
return nil, err
}
return &json, nil
return &rawJSON, nil
}

func defaultConvert(in interface{}) (interface{}, error) {
Expand Down
28 changes: 21 additions & 7 deletions pkg/converters/converters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package converters_test

import (
"encoding/json"
"errors"
"github.com/grafana/clickhouse-datasource/pkg/converters"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -443,12 +444,17 @@ func TestNullableUInt256ShouldBeNil(t *testing.T) {
assert.Equal(t, (*float64)(nil), actual)
}

func toJson(obj interface{}) string {
func toJson(obj interface{}) (json.RawMessage, error) {
bytes, err := json.Marshal(obj)
if err != nil {
return "unable to marshal"
return nil, errors.New("unable to marshal")
}
return string(bytes)
var rawJSON json.RawMessage
err = json.Unmarshal(bytes, &rawJSON)
if err != nil {
return nil, errors.New("unable to unmarshal")
}
return rawJSON, nil
}

func TestTuple(t *testing.T) {
Expand All @@ -461,7 +467,9 @@ func TestTuple(t *testing.T) {
sut := converters.GetConverter("Tuple(name String, id Uint16)")
v, err := sut.FrameConverter.ConverterFunc(&value)
assert.Nil(t, err)
assert.JSONEq(t, toJson(value), *v.(*string))
msg, err := toJson(value)
assert.Nil(t, err)
assert.Equal(t, msg, *v.(*json.RawMessage))
}

func TestNested(t *testing.T) {
Expand All @@ -476,7 +484,9 @@ func TestNested(t *testing.T) {
sut := converters.GetConverter("Nested(name String, id Uint16)")
v, err := sut.FrameConverter.ConverterFunc(&value)
assert.Nil(t, err)
assert.JSONEq(t, toJson(value), *v.(*string))
msg, err := toJson(value)
assert.Nil(t, err)
assert.Equal(t, msg, *v.(*json.RawMessage))
}

func TestMap(t *testing.T) {
Expand All @@ -489,7 +499,9 @@ func TestMap(t *testing.T) {
sut := converters.GetConverter("Map(String, Uint16)")
v, err := sut.FrameConverter.ConverterFunc(&value)
assert.Nil(t, err)
assert.JSONEq(t, toJson(value), *v.(*string))
msg, err := toJson(value)
assert.Nil(t, err)
assert.Equal(t, msg, *v.(*json.RawMessage))
}

func TestNullableFixedString(t *testing.T) {
Expand All @@ -505,7 +517,9 @@ func TestArray(t *testing.T) {
ipConverter := converters.GetConverter("Array(String)")
v, err := ipConverter.FrameConverter.ConverterFunc(&value)
assert.Nil(t, err)
assert.JSONEq(t, toJson(value), *v.(*string))
msg, err := toJson(value)
assert.Nil(t, err)
assert.Equal(t, msg, *v.(*json.RawMessage))
}

func TestIPv4(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugin/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ func (h *Clickhouse) MutateQuery(ctx context.Context, req backend.DataQuery) (co
Meta struct {
TimeZone string `json:"timezone"`
} `json:"meta"`
Format int `json:"format"`
}

if err := json.Unmarshal(req.JSON, &dataQuery); err != nil {
Expand All @@ -227,6 +228,5 @@ func (h *Clickhouse) MutateQuery(ctx context.Context, req backend.DataQuery) (co
}

loc, _ := time.LoadLocation(dataQuery.Meta.TimeZone)

return clickhouse.Context(ctx, clickhouse.WithUserLocation(loc)), req
}
16 changes: 12 additions & 4 deletions pkg/plugin/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/tls"
"database/sql"
"encoding/json"
"errors"
"fmt"
"math/big"
"net"
Expand Down Expand Up @@ -209,12 +210,17 @@ func insertData(t *testing.T, conn *sql.DB, data ...interface{}) {
require.NoError(t, scope.Commit())
}

func toJson(obj interface{}) string {
func toJson(obj interface{}) (json.RawMessage, error) {
bytes, err := json.Marshal(obj)
if err != nil {
return "unable to marshal"
return nil, errors.New("unable to marshal")
}
return string(bytes)
var rawJSON json.RawMessage
err = json.Unmarshal(bytes, &rawJSON)
if err != nil {
return nil, errors.New("unable to unmarshal")
}
return rawJSON, nil
}

func checkFieldValue(t *testing.T, field *data.Field, expected ...interface{}) {
Expand All @@ -230,7 +236,9 @@ func checkFieldValue(t *testing.T, field *data.Field, expected ...interface{}) {
default:
switch reflect.ValueOf(eVal).Kind() {
case reflect.Map, reflect.Slice:
assert.JSONEq(t, toJson(tVal), *val.(*string))
jsonRaw, err := toJson(tVal)
assert.Nil(t, err)
assert.Equal(t, jsonRaw, *val.(*json.RawMessage))
return
}
assert.Equal(t, eVal, val)
Expand Down
8 changes: 7 additions & 1 deletion src/__mocks__/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ export const mockDatasource = new Datasource({
},
});
mockDatasource.adHocFiltersStatus = 1; // most tests should skip checking the CH version. We will set ad hoc filters to enabled to avoid running the CH version check
export const mockQuery: CHQuery = { rawSql: 'select * from foo', refId: '', format: 1, queryType: QueryType.SQL };
export const mockQuery: CHQuery = {
rawSql: 'select * from foo',
refId: '',
format: 1,
queryType: QueryType.SQL,
selectedFormat: 4,
};
11 changes: 11 additions & 0 deletions src/components/FormatSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { render } from '@testing-library/react';
import { FormatSelect } from './FormatSelect';
import { Format } from '../types';

describe('FormatSelect', () => {
it('renders a format', () => {
const result = render(<FormatSelect format={Format.TIMESERIES} onChange={() => {}} />);
expect(result.container.firstChild).not.toBeNull();
});
});
48 changes: 48 additions & 0 deletions src/components/FormatSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { InlineFormLabel, Select } from '@grafana/ui';
import { selectors } from './../selectors';
import { Format } from '../types';
import { styles } from '../styles';

export type Props = { format: Format; value?: string; onChange: (format: Format) => void };

export const FormatSelect = (props: Props) => {
const { onChange, format } = props;
const { label, tooltip, options: formatLabels } = selectors.components.QueryEditor.Format;
return (
<div className="gf-form">
<InlineFormLabel width={8} className="query-keyword" tooltip={tooltip}>
{label}
</InlineFormLabel>
<Select<Format>
className={`width-8 ${styles.Common.inlineSelect}`}
onChange={(e) => onChange(e.value!)}
options={[
{
label: formatLabels.AUTO,
value: Format.AUTO,
},
{
label: formatLabels.TABLE,
value: Format.TABLE,
},
{
label: formatLabels.TIME_SERIES,
value: Format.TIMESERIES,
},
{
label: formatLabels.LOGS,
value: Format.LOGS,
},
{
label: formatLabels.TRACE,
value: Format.TRACE,
},
]}
value={format}
menuPlacement={'bottom'}
allowCustomValue={false}
/>
</div>
);
};
2 changes: 1 addition & 1 deletion src/components/QueryTypeSwitcher.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('QueryTypeSwitcher', () => {
it('renders correctly SQL editor', () => {
const result = render(
<QueryTypeSwitcher
query={{ refId: 'A', queryType: QueryType.SQL, rawSql: '', format: Format.TABLE }}
query={{ refId: 'A', queryType: QueryType.SQL, rawSql: '', format: Format.TABLE, selectedFormat: Format.AUTO }}
onChange={() => {}}
onRunQuery={() => {}}
/>
Expand Down
5 changes: 3 additions & 2 deletions src/components/QueryTypeSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { SelectableValue } from '@grafana/data';
import { RadioButtonGroup, ConfirmModal, InlineFormLabel } from '@grafana/ui';
import { getQueryOptionsFromSql, getSQLFromQueryOptions } from './queryBuilder/utils';
import { selectors } from './../selectors';
import { CHQuery, QueryType, defaultCHBuilderQuery, SqlBuilderOptions, CHSQLQuery, Format } from 'types';
import { CHQuery, QueryType, defaultCHBuilderQuery, SqlBuilderOptions, CHSQLQuery } from 'types';
import { isString } from 'lodash';

interface QueryTypeSwitcherProps {
Expand Down Expand Up @@ -56,7 +56,8 @@ export const QueryTypeSwitcher = (props: QueryTypeSwitcherProps) => {
queryType,
rawSql: getSQLFromQueryOptions(builderOptions),
meta: { builderOptions },
format: Format.TABLE,
format: query.format,
selectedFormat: query.selectedFormat,
});
} else if (queryType === QueryType.Builder) {
onChange({ ...query, queryType, rawSql: getSQLFromQueryOptions(builderOptions), builderOptions });
Expand Down
4 changes: 2 additions & 2 deletions src/components/SQLEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('SQL Editor', () => {
const rawSql = 'foo';
render(
<SQLEditor
query={{ rawSql, refId: 'A', format: 1, queryType: QueryType.SQL }}
query={{ rawSql, refId: 'A', format: 1, queryType: QueryType.SQL, selectedFormat: 4 }}
onChange={jest.fn()}
onRunQuery={jest.fn()}
datasource={mockDatasource}
Expand All @@ -41,7 +41,7 @@ describe('SQL Editor', () => {
await act(async () => {
render(
<SQLEditor
query={{ rawSql: 'test', refId: 'A', format: 1, queryType: QueryType.SQL }}
query={{ rawSql: 'test', refId: 'A', format: 1, queryType: QueryType.SQL, selectedFormat: 4 }}
onChange={onChangeValue}
onRunQuery={onRunQuery}
datasource={mockDatasource}
Expand Down
2 changes: 1 addition & 1 deletion src/components/SQLEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const SQLEditor = (props: SQLEditorProps) => {
});

const onSqlChange = (sql: string) => {
const format = getFormat(sql);
const format = getFormat(sql, query.selectedFormat);
onChange({ ...query, rawSql: sql, format, queryType: QueryType.SQL });
onRunQuery();
};
Expand Down
29 changes: 16 additions & 13 deletions src/components/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ import { getFields } from 'data/ast';
import { Format } from 'types';
import { isString } from 'lodash';

export const getFormat = (sql: string): Format => {
// convention to format as time series
// first field as "time" alias and requires at least 2 fields (time and metric)
const selectList = getFields(sql);
// if there are more than 2 fields, index 1 will be a ','
if (selectList.length > 2 && isString(selectList[0])) {
const firstProjection = selectList[0].trim().toLowerCase();
if (firstProjection.endsWith('as time')) {
return Format.TIMESERIES;
}
if (firstProjection.endsWith('as log_time')) {
return Format.LOGS;
export const getFormat = (sql: string, selectedFormat: Format): Format => {
if (selectedFormat === Format.AUTO) {
// convention to format as time series
// first field as "time" alias and requires at least 2 fields (time and metric)
const selectList = getFields(sql);
// if there are more than 2 fields, index 1 will be a ','
if (selectList.length > 2 && isString(selectList[0])) {
const firstProjection = selectList[0].trim().toLowerCase();
if (firstProjection.endsWith('as time')) {
return Format.TIMESERIES;
}
if (firstProjection.endsWith('as log_time')) {
return Format.LOGS;
}
}
return Format.TABLE;
}
return Format.TABLE;
return selectedFormat;
};
11 changes: 11 additions & 0 deletions src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ export const Components = {
container: 'data-testid-code-editor-container',
Expand: 'data-testid-code-editor-expand-button',
},
Format: {
label: 'Format',
tooltip: 'Query Type',
options: {
AUTO: 'Auto',
TABLE: 'Table',
TIME_SERIES: 'Time Series',
LOGS: 'Logs',
TRACE: 'Trace',
},
},
Types: {
label: 'Query Type',
tooltip: 'Query Type',
Expand Down
5 changes: 5 additions & 0 deletions src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,10 @@ export const styles = {
}
`,
},
FormatSelector: {
formatSelector: css`
display: flex;
`,
},
VariablesEditor: {},
};
Loading