diff --git a/src/components/SqlEditor.tsx b/src/components/SqlEditor.tsx index 937e283c..d03e0cf2 100644 --- a/src/components/SqlEditor.tsx +++ b/src/components/SqlEditor.tsx @@ -110,7 +110,7 @@ export const SqlEditor = (props: SqlEditorProps) => { return ( <> -
+
saveChanges({ queryType })} sqlEditor />
diff --git a/src/components/configEditor/HttpHeadersConfig.tsx b/src/components/configEditor/HttpHeadersConfig.tsx index 165c8321..773b409b 100644 --- a/src/components/configEditor/HttpHeadersConfig.tsx +++ b/src/components/configEditor/HttpHeadersConfig.tsx @@ -72,7 +72,6 @@ export const HttpHeadersConfig = (props: HttpHeadersConfigProps) => { updateForwardGrafanaHeaders(e.currentTarget.checked)} /> @@ -155,7 +154,6 @@ const HttpHeaderEditor = (props: HttpHeaderEditorProps) => { setSecure(e.currentTarget.checked)} onBlur={() => onUpdate()} diff --git a/src/components/configEditor/LabeledInput.tsx b/src/components/configEditor/LabeledInput.tsx index 78650577..a4b688b0 100644 --- a/src/components/configEditor/LabeledInput.tsx +++ b/src/components/configEditor/LabeledInput.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Input, InlineFormLabel } from '@grafana/ui'; +import { styles } from 'styles'; interface LabeledInputProps { label: string; @@ -14,7 +15,7 @@ export function LabeledInput(props: LabeledInputProps) { const { label, tooltip, placeholder, disabled, value, onChange } = props; return ( -
+
{label} diff --git a/src/components/configEditor/LogsConfig.tsx b/src/components/configEditor/LogsConfig.tsx index ca45c9b9..996e2913 100644 --- a/src/components/configEditor/LogsConfig.tsx +++ b/src/components/configEditor/LogsConfig.tsx @@ -9,6 +9,7 @@ import { CHLogsConfig } from 'types/config'; import allLabels from 'labels'; import { columnLabelToPlaceholder } from 'data/utils'; import { Switch } from 'components/queryBuilder/Switch'; +import { styles } from 'styles'; interface LogsConfigProps { logsConfig?: CHLogsConfig; @@ -125,7 +126,7 @@ export const LogsConfig = (props: LogsConfigProps) => { onChange={onSelectContextColumnsChange} wide /> -
+
{labels.contextColumns.columns.label} diff --git a/src/components/configEditor/QuerySettingsConfig.tsx b/src/components/configEditor/QuerySettingsConfig.tsx index e6a14dfa..73c42c51 100644 --- a/src/components/configEditor/QuerySettingsConfig.tsx +++ b/src/components/configEditor/QuerySettingsConfig.tsx @@ -100,7 +100,7 @@ export const QuerySettingsConfig = (props: QuerySettingsConfigProps) => { - + ); diff --git a/src/components/queryBuilder/AggregateEditor.test.tsx b/src/components/queryBuilder/AggregateEditor.test.tsx index d88356bc..fe5e4069 100644 --- a/src/components/queryBuilder/AggregateEditor.test.tsx +++ b/src/components/queryBuilder/AggregateEditor.test.tsx @@ -30,8 +30,8 @@ describe('AggregateEditor', () => { const addButton = result.getByTestId(selectors.components.QueryBuilder.AggregateEditor.addButton); expect(addButton).toBeInTheDocument(); await userEvent.click(addButton); - expect(onAggregatesChange).toBeCalledTimes(1); - expect(onAggregatesChange).toBeCalledWith([expect.anything()]); + expect(onAggregatesChange).toHaveBeenCalledTimes(1); + expect(onAggregatesChange).toHaveBeenCalledWith([expect.anything()]); }); it('should call onAggregatesChange when remove aggregate button is clicked', async () => { @@ -45,7 +45,7 @@ describe('AggregateEditor', () => { const removeButton = result.getByTestId(selectors.components.QueryBuilder.AggregateEditor.itemRemoveButton); expect(removeButton).toBeInTheDocument(); await userEvent.click(removeButton); - expect(onAggregatesChange).toBeCalledWith([]); + expect(onAggregatesChange).toHaveBeenCalledWith([]); }); it('should call onAggregatesChange when aggregate is updated', async () => { @@ -62,6 +62,6 @@ describe('AggregateEditor', () => { fireEvent.keyDown(aggregateSelect, { key: 'ArrowDown' }); fireEvent.keyDown(aggregateSelect, { key: 'ArrowDown' }); fireEvent.keyDown(aggregateSelect, { key: 'Enter' }); - expect(onAggregatesChange).toBeCalledWith([expectedAggregate]); + expect(onAggregatesChange).toHaveBeenCalledWith([expectedAggregate]); }); }); diff --git a/src/components/queryBuilder/AggregateEditor.tsx b/src/components/queryBuilder/AggregateEditor.tsx index 0e57ba3f..846632ff 100644 --- a/src/components/queryBuilder/AggregateEditor.tsx +++ b/src/components/queryBuilder/AggregateEditor.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { InlineFormLabel, Select, Button, Input, HorizontalGroup } from '@grafana/ui'; +import { InlineFormLabel, Select, Button, Input, Stack } from '@grafana/ui'; import { AggregateColumn, AggregateType, TableColumn } from 'types/queryBuilder'; import labels from 'labels'; import { selectors } from 'selectors'; @@ -43,7 +43,7 @@ const Aggregate = (props: AggregateProps) => { } return ( - + setInput(e.currentTarget.value)} diff --git a/src/components/queryBuilder/views/TraceQueryBuilder.tsx b/src/components/queryBuilder/views/TraceQueryBuilder.tsx index ed8d421d..efdc9ec0 100644 --- a/src/components/queryBuilder/views/TraceQueryBuilder.tsx +++ b/src/components/queryBuilder/views/TraceQueryBuilder.tsx @@ -19,6 +19,7 @@ import { OrderByEditor, getOrderByOptions } from '../OrderByEditor'; import { LimitEditor } from '../LimitEditor'; import { LabeledInput } from 'components/configEditor/LabeledInput'; import { Switch } from '../Switch'; +import { styles } from 'styles'; interface TraceQueryBuilderProps { datasource: Datasource; @@ -180,7 +181,7 @@ export const TraceQueryBuilder = (props: TraceQueryBuilderProps) => { onVersionChange={(v) => builderOptionsDispatch(setOtelVersion(v))} wide /> -
+
{ label={labels.columns.spanId.label} tooltip={labels.columns.spanId.tooltip} wide - inline />
-
+
{ label={labels.columns.serviceName.label} tooltip={labels.columns.serviceName.tooltip} wide - inline />
-
+
{ label={labels.columns.startTime.label} tooltip={labels.columns.startTime.tooltip} wide - inline />
-
+
{ disabled={builderState.otelEnabled} unit={builderState.durationUnit} onChange={onOptionChange('durationUnit')} - inline />
-
+
{ label={labels.columns.serviceTags.label} tooltip={labels.columns.serviceTags.tooltip} wide - inline />
-
+
{ label={labels.columns.statusCode.label} tooltip={labels.columns.statusCode.tooltip} wide - inline />
-
+
{ label={labels.columns.state.label} tooltip={labels.columns.state.tooltip} wide - inline />
-
+
{ label={labels.columns.instrumentationLibraryVersion.label} tooltip={labels.columns.instrumentationLibraryVersion.tooltip} wide - inline />
-
+
{ wide />
-
+
{ onChange={onOptionChange('traceEventsColumnPrefix')} />
-
+
{ describe('getDataProvider', () => { it('should not support LogsSample yet', async () => { - expect(datasource.getDataProvider(SupplementaryQueryType.LogsSample, {} as any)).toBeUndefined(); + expect(datasource.getSupplementaryQueryRequest(SupplementaryQueryType.LogsSample, {} as any)).toBeUndefined(); }); it('should do nothing if there are no supplementary queries for targets', async () => { jest.spyOn(Datasource.prototype, 'getSupplementaryLogsVolumeQuery').mockReturnValue(undefined); jest.spyOn(logs, 'getIntervalInfo').mockReturnValue({ interval: '1d' }); expect( - datasource.getDataProvider(SupplementaryQueryType.LogsVolume, { + datasource.getSupplementaryQueryRequest(SupplementaryQueryType.LogsVolume, { scopedVars: { __interval: {}, }, @@ -635,42 +634,26 @@ describe('ClickHouseDatasource', () => { ).toBeUndefined(); }); - it('should call logVolumeQuery if there are supplementary log volume queries for targets', async () => { - const range = ['from', 'to']; - const supplementaryQuery = { - rawSql: 'supplementaryQuery', - } as CHSqlQuery; + it('should build a LogsVolume request when supplementary targets exist', async () => { + const range = ['from', 'to'] as any; + const supplementaryQuery = { rawSql: 'supplementaryQuery' } as CHSqlQuery; + jest.spyOn(Datasource.prototype, 'getSupplementaryLogsVolumeQuery').mockReturnValue(supplementaryQuery); - jest.spyOn(logs, 'getIntervalInfo').mockReturnValue({ interval: '1d' }); - const queryLogsVolumeSpy = jest - .spyOn(logs, 'queryLogsVolume') - .mockReturnValue('queryLogsVolumeResponse' as unknown as Observable); - expect( - datasource.getDataProvider(SupplementaryQueryType.LogsVolume, { - scopedVars: { - __interval: {}, - }, - targets: ['initialTarget'], - range, - } as any) - ).toEqual('queryLogsVolumeResponse'); - expect(queryLogsVolumeSpy).toBeCalledTimes(1); - expect(queryLogsVolumeSpy).toHaveBeenLastCalledWith( - datasource, - { - hideFromInspector: true, - interval: '1d', - scopedVars: { - __interval: { - text: '1d', - value: '1d', - }, - }, - targets: [supplementaryQuery], - range, - }, - { range, targets: ['initialTarget'] } - ); + jest.spyOn(logs, 'getIntervalInfo').mockReturnValue({ interval: '1d' } as any); + + const req = datasource.getSupplementaryQueryRequest(SupplementaryQueryType.LogsVolume, { + scopedVars: { __interval: {} }, + targets: ['initialTarget'] as any, + range, + } as any); + + expect(req).toEqual({ + hideFromInspector: true, + interval: '1d', + scopedVars: { __interval: { text: '1d', value: '1d' } }, + targets: [supplementaryQuery], + range, + }); }); }); }); diff --git a/src/data/CHDatasource.ts b/src/data/CHDatasource.ts index e173c416..658a4b38 100644 --- a/src/data/CHDatasource.ts +++ b/src/data/CHDatasource.ts @@ -202,6 +202,61 @@ export class Datasource }; } + // REMOVE this whole method + // getDataProvider(type: SupplementaryQueryType, request: DataQueryRequest): Observable | undefined { ... } + + // ADD this instead: + getSupplementaryQueryRequest( + type: SupplementaryQueryType, + request: DataQueryRequest + ): DataQueryRequest | undefined { + if (!this.getSupportedSupplementaryQueryTypes().includes(type)) { + return undefined; + } + + switch (type) { + case SupplementaryQueryType.LogsVolume: { + const logsVolumeRequest = cloneDeep(request); + + // derive interval + scoped vars + const intervalInfo = getIntervalInfo(logsVolumeRequest.scopedVars); + logsVolumeRequest.interval = intervalInfo.interval; + logsVolumeRequest.scopedVars = { + ...logsVolumeRequest.scopedVars, + __interval: { value: intervalInfo.interval, text: intervalInfo.interval }, + }; + logsVolumeRequest.hideFromInspector = true; + + if (intervalInfo.intervalMs !== undefined) { + logsVolumeRequest.intervalMs = intervalInfo.intervalMs; + logsVolumeRequest.scopedVars.__interval_ms = { + value: intervalInfo.intervalMs, + text: intervalInfo.intervalMs, + }; + } + + // build supplementary targets + const targets: CHQuery[] = []; + for (const target of logsVolumeRequest.targets) { + const supplementaryQuery = this.getSupplementaryLogsVolumeQuery(logsVolumeRequest, target); + if (supplementaryQuery) { + targets.push(supplementaryQuery); + } + } + + if (!targets.length) { + return undefined; + } + + // return a request Grafana will execute + return { ...logsVolumeRequest, targets }; + } + + default: + return undefined; + } + } + getSupplementaryQuery(options: SupplementaryQueryOptions, originalQuery: CHQuery): CHQuery | undefined { return undefined; } @@ -228,7 +283,7 @@ export class Datasource } // convention - assume the first field is an id field const ids = frame?.fields[0]?.values; - return frame?.fields[1]?.values.map((text, i) => ({ text, value: ids.get(i) })); + return frame?.fields[1]?.values.map((text, i) => ({ text, value: ids[i] })); } applyTemplateVariables(query: CHQuery, scoped: ScopedVars, filters: AdHocVariableFilter[] = []): CHQuery { @@ -940,7 +995,7 @@ export class Datasource continue; } - let value = field.values.get(row.rowIndex); + let value = field.values[row.rowIndex]; if (value && field.type === 'other' && isMapKey) { value = value[keyName]; } diff --git a/src/data/adHocFilter.ts b/src/data/adHocFilter.ts index f4b07a4c..65d3e56a 100644 --- a/src/data/adHocFilter.ts +++ b/src/data/adHocFilter.ts @@ -40,7 +40,7 @@ export class AdHocFilter { .map((f, i) => { const key = escapeKey(f.key); const value = escapeValueBasedOnOperator(f.value, f.operator); - const condition = i !== adHocFilters.length - 1 ? (f.condition ? f.condition : 'AND') : ''; + const condition = i !== adHocFilters.length - 1 ? 'AND' : ''; const operator = convertOperatorToClickHouseOperator(f.operator); return ` ${key} ${operator} ${value} ${condition}`; }) diff --git a/src/data/logs.ts b/src/data/logs.ts index 4466d00d..c4272a36 100644 --- a/src/data/logs.ts +++ b/src/data/logs.ts @@ -9,7 +9,6 @@ import { FieldType, LoadingState, LogLevel, - MutableDataFrame, ScopedVars, TimeRange, toDataFrame, @@ -48,7 +47,7 @@ const LogLevelColor = { }; function getThemeColor(dark: string, light: string): string { - return config.bootData.user.lightTheme ? light : dark; + return config.bootData.user.theme ? light : dark; } /** @@ -89,14 +88,14 @@ export function queryLogsVolume { - const { error } = dataQueryResponse; - if (error !== undefined) { + const { errors } = dataQueryResponse; + if (errors !== undefined) { observer.next({ state: LoadingState.Error, - error, + errors, data: [], }); - observer.error(error); + observer.error(errors); } else { rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame)); } @@ -131,22 +130,23 @@ export function aggregateRawLogsVolume(rawLogsVolume: DataFrame[]): DataFrame[] } const oneLevelDetected = levelFields.length === 1 && levelFields[0].name === DEFAULT_LOGS_ALIAS; - if (oneLevelDetected) { - levelFields[0].name = 'logs'; - } - const totalLength = timeField.values.length; return levelFields.map((field) => { - const logLevel = LogLevel[field.name as keyof typeof LogLevel] || LogLevel.unknown; - const df = new MutableDataFrame(); - df.addField({ name: 'Time', type: FieldType.time, values: timeField.values }, totalLength); - df.addField({ - name: 'Value', - type: FieldType.number, - config: getLogVolumeFieldConfig(logLevel, oneLevelDetected), - values: field.values, + const effectiveName = oneLevelDetected && field.name === DEFAULT_LOGS_ALIAS ? 'logs' : field.name; + + const logLevel = LogLevel[effectiveName as keyof typeof LogLevel] ?? LogLevel.unknown; + + return toDataFrame({ + fields: [ + { name: 'Time', type: FieldType.time, values: timeField.values }, + { + name: 'Value', + type: FieldType.number, + config: getLogVolumeFieldConfig(logLevel, oneLevelDetected), + values: field.values, + }, + ], }); - return df; }); } diff --git a/src/labels.ts b/src/labels.ts index f422e091..e3370ce9 100644 --- a/src/labels.ts +++ b/src/labels.ts @@ -157,7 +157,7 @@ export default { }, validateSql: { label: 'Validate SQL', - tooltip: 'Validate SQL in the editor.', + tooltip: 'Validate SQL in the editor', }, }, TracesConfig: { diff --git a/src/styles.ts b/src/styles.ts index 9d7fe2a6..1f580a01 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -2,67 +2,78 @@ import { css } from '@emotion/css'; export const styles = { Common: { - check: css` - margin-top: 5px; - `, - wrapper: css` - position: relative; - width: 100%; - `, - smallBtn: css` - margin-top: 5px; - margin-inline: 5px; - `, - selectWrapper: css` - width: 100%; - `, - inlineSelect: css` - margin-right: 5px; - `, - firstLabel: css` - margin-right: 5px; - `, - expand: css` - position: absolute; - top: 2px; - left: 6px; - z-index: 100; - color: gray; - `, + check: css({ + marginTop: '5px', + }), + wrapper: css({ + position: 'relative', + width: '100%', + }), + smallBtn: css({ + marginTop: '5px', + marginInline: '5px', + }), + selectWrapper: css({ + width: '100%', + }), + inlineSelect: css({ + marginRight: '5px', + }), + firstLabel: css({ + marginRight: '5px', + }), + expand: css({ + position: 'absolute', + top: '2px', + left: '6px', + zIndex: 100, + color: 'gray', + }), + flexContainer: css({ + display: 'flex', + marginBottom: '4px', + }), + flex: css({ + display: 'flex', + }), + flexColumn: css({ + display: 'flex', + flexDirection: 'column', + }), }, ConfigEditor: { - container: css` - justify-content: space-between; - h5 { - line-height: 34px; - margin-bottom: 5px; - } - button { - margin-right: 5px; - } - `, - wide: css` - width: 75%; - `, - subHeader: css` - padding: 5px 0 5px 0; - `, + container: css({ + justifyContent: 'space-between', + '& h5': { + lineHeight: '34px', + marginBottom: '5px', + }, + '& button': { + marginRight: '5px', + }, + }), + wide: css({ + width: '75%', + }), + subHeader: css({ + padding: '5px 0 5px 0', + }), }, QueryEditor: { - queryType: css` - justify-content: space-between; - span { - display: flex; - } - `, - inlineField: css` - margin-left: 7px; - `, + queryType: css({ + justifyContent: 'space-between', + '& span': { + display: 'flex', + }, + }), + inlineField: css({ + marginLeft: '7px', + }), }, FormatSelector: { - formatSelector: css` - display: flex; - `, + formatSelector: css({ + display: 'flex', + }), }, VariablesEditor: {}, }; diff --git a/src/views/CHConfigEditor.tsx b/src/views/CHConfigEditor.tsx index 015743ca..e587b933 100644 --- a/src/views/CHConfigEditor.tsx +++ b/src/views/CHConfigEditor.tsx @@ -284,7 +284,6 @@ export const ConfigEditor: React.FC = (props) => { { trackingV1.trackClickhouseConfigV1SecureConnectionToggleClicked({ @@ -326,7 +325,6 @@ export const ConfigEditor: React.FC = (props) => { { trackingV1.trackClickhouseConfigV1SkipTLSVerifyToggleClicked({ @@ -338,7 +336,6 @@ export const ConfigEditor: React.FC = (props) => { { trackingV1.trackClickhouseConfigV1TLSClientAuthToggleClicked({ @@ -350,7 +347,6 @@ export const ConfigEditor: React.FC = (props) => { { trackingV1.trackClickhouseConfigV1WithCACertToggleClicked({ caCertToggle: e.currentTarget.checked }); @@ -613,7 +609,6 @@ export const ConfigEditor: React.FC = (props) => { { @@ -625,7 +620,6 @@ export const ConfigEditor: React.FC = (props) => { {config.secureSocksDSProxyEnabled && versionGte(config.buildInfo.version, '10.0.0') && ( onSwitchToggle('enableSecureSocksProxy', e.currentTarget.checked)} /> diff --git a/src/views/CHQueryEditor.tsx b/src/views/CHQueryEditor.tsx index f75d38f2..126b6363 100644 --- a/src/views/CHQueryEditor.tsx +++ b/src/views/CHQueryEditor.tsx @@ -25,7 +25,7 @@ export const CHQueryEditor = (props: CHQueryEditorProps) => { return ( <> -
+