Loki: Remove dependecy on core and move to @grafana/o11y-ds-frontend (#81284)

* Loki: Remove dependecy on core and move to o11y-ds-frontend

* Fix ctr -> ctrl

* Remove test helpers
This commit is contained in:
Ivana Huckova 2024-01-29 11:19:34 +01:00 committed by GitHub
parent 34e88077aa
commit a83e01918a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 342 additions and 14 deletions

View File

@ -0,0 +1,49 @@
import React, { useEffect, useState } from 'react';
import { store } from '../store';
export interface Props<T> {
storageKey: string;
defaultValue: T;
children: (value: T, onSaveToStore: (value: T) => void, onDeleteFromStore: () => void) => React.ReactNode;
}
export const LocalStorageValueProvider = <T,>(props: Props<T>) => {
const { children, storageKey, defaultValue } = props;
const [state, setState] = useState({ value: store.getObject(props.storageKey, props.defaultValue) });
useEffect(() => {
const onStorageUpdate = (v: StorageEvent) => {
if (v.key === storageKey) {
setState({ value: store.getObject(props.storageKey, props.defaultValue) });
}
};
window.addEventListener('storage', onStorageUpdate);
return () => {
window.removeEventListener('storage', onStorageUpdate);
};
});
const onSaveToStore = (value: T) => {
try {
store.setObject(storageKey, value);
} catch (error) {
console.error(error);
}
setState({ value });
};
const onDeleteFromStore = () => {
try {
store.delete(storageKey);
} catch (error) {
console.log(error);
}
setState({ value: defaultValue });
};
return <>{children(state.value, onSaveToStore, onDeleteFromStore)}</>;
};

View File

@ -1,7 +1,15 @@
import { DataQueryResponse, LoadingState, PanelData, QueryResultMetaStat, getDefaultTimeRange } from '@grafana/data';
import { getMockFrames } from 'app/plugins/datasource/loki/__mocks__/frames';
import {
DataFrame,
DataFrameType,
DataQueryResponse,
FieldType,
LoadingState,
PanelData,
QueryResultMetaStat,
getDefaultTimeRange,
} from '@grafana/data';
import { cloneQueryResponse, combinePanelData, combineResponses } from './response';
import { cloneQueryResponse, combinePanelData, combineResponses } from './combineResponses';
describe('cloneQueryResponse', () => {
const { logFrameA } = getMockFrames();
@ -489,3 +497,204 @@ describe('combinePanelData', () => {
});
});
});
export function getMockFrames() {
const logFrameA: DataFrame = {
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [3, 4],
},
{
name: 'Line',
type: FieldType.string,
config: {},
values: ['line1', 'line2'],
},
{
name: 'labels',
type: FieldType.other,
config: {},
values: [
{
label: 'value',
},
{
otherLabel: 'other value',
},
],
},
{
name: 'tsNs',
type: FieldType.string,
config: {},
values: ['3000000', '4000000'],
},
{
name: 'id',
type: FieldType.string,
config: {},
values: ['id1', 'id2'],
},
],
meta: {
custom: {
frameType: 'LabeledTimeValues',
},
stats: [
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 11 },
{ displayName: 'Ingester: total reached', value: 1 },
],
},
length: 2,
};
const logFrameB: DataFrame = {
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [1, 2],
},
{
name: 'Line',
type: FieldType.string,
config: {},
values: ['line3', 'line4'],
},
{
name: 'labels',
type: FieldType.other,
config: {},
values: [
{
otherLabel: 'other value',
},
],
},
{
name: 'tsNs',
type: FieldType.string,
config: {},
values: ['1000000', '2000000'],
},
{
name: 'id',
type: FieldType.string,
config: {},
values: ['id3', 'id4'],
},
],
meta: {
custom: {
frameType: 'LabeledTimeValues',
},
stats: [
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 22 },
{ displayName: 'Ingester: total reached', value: 2 },
],
},
length: 2,
};
const metricFrameA: DataFrame = {
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [3000000, 4000000],
},
{
name: 'Value',
type: FieldType.number,
config: {},
values: [5, 4],
labels: {
level: 'debug',
},
},
],
meta: {
type: DataFrameType.TimeSeriesMulti,
stats: [
{ displayName: 'Ingester: total reached', value: 1 },
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 11 },
],
},
length: 2,
};
const metricFrameB: DataFrame = {
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [1000000, 2000000],
},
{
name: 'Value',
type: FieldType.number,
config: {},
values: [6, 7],
labels: {
level: 'debug',
},
},
],
meta: {
type: DataFrameType.TimeSeriesMulti,
stats: [
{ displayName: 'Ingester: total reached', value: 2 },
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 22 },
],
},
length: 2,
};
const metricFrameC: DataFrame = {
refId: 'A',
name: 'some-time-series',
fields: [
{
name: 'Time',
type: FieldType.time,
config: {},
values: [3000000, 4000000],
},
{
name: 'Value',
type: FieldType.number,
config: {},
values: [6, 7],
labels: {
level: 'error',
},
},
],
meta: {
type: DataFrameType.TimeSeriesMulti,
stats: [
{ displayName: 'Ingester: total reached', value: 2 },
{ displayName: 'Summary: total bytes processed', unit: 'decbytes', value: 33 },
],
},
length: 2,
};
return {
logFrameA,
logFrameB,
metricFrameA,
metricFrameB,
metricFrameC,
};
}

View File

@ -73,7 +73,7 @@ function combineFrames(dest: DataFrame, source: DataFrame) {
}
const TOTAL_BYTES_STAT = 'Summary: total bytes processed';
// This is specific for Loki
function getCombinedMetadataStats(
destStats: QueryResultMetaStat[],
sourceStats: QueryResultMetaStat[]

View File

@ -13,3 +13,6 @@ export * from './TraceToLogs/TraceToLogsSettings';
export * from './TraceToMetrics/TraceToMetricsSettings';
export * from './TraceToProfiles/TraceToProfilesSettings';
export * from './utils';
export * from './store';
export * from './LocalStorageValueProvider/LocalStorageValueProvider';
export * from './combineResponses';

View File

@ -0,0 +1,64 @@
type StoreValue = string | number | boolean | null;
export class Store {
get(key: string) {
return window.localStorage[key];
}
set(key: string, value: StoreValue) {
window.localStorage[key] = value;
}
getBool(key: string, def: boolean): boolean {
if (def !== void 0 && !this.exists(key)) {
return def;
}
return window.localStorage[key] === 'true';
}
getObject<T = unknown>(key: string): T | undefined;
getObject<T = unknown>(key: string, def: T): T;
getObject<T = unknown>(key: string, def?: T) {
let ret = def;
if (this.exists(key)) {
const json = window.localStorage[key];
try {
ret = JSON.parse(json);
} catch (error) {
console.error(`Error parsing store object: ${key}. Returning default: ${def}. [${error}]`);
}
}
return ret;
}
/* Returns true when successfully stored, throws error if not successfully stored */
setObject(key: string, value: unknown) {
let json;
try {
json = JSON.stringify(value);
} catch (error) {
throw new Error(`Could not stringify object: ${key}. [${error}]`);
}
try {
this.set(key, json);
} catch (error) {
// Likely hitting storage quota
const errorToThrow = new Error(`Could not save item in localStorage: ${key}. [${error}]`);
if (error instanceof Error) {
errorToThrow.name = error.name;
}
throw errorToThrow;
}
return true;
}
exists(key: string) {
return window.localStorage[key] !== void 0;
}
delete(key: string) {
window.localStorage.removeItem(key);
}
}
export const store = new Store();

View File

@ -22,6 +22,7 @@ import {
SupplementaryQueryType,
toLegacyResponseData,
} from '@grafana/data';
import { combinePanelData } from '@grafana/o11y-ds-frontend';
import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import store from 'app/core/store';
@ -39,7 +40,6 @@ import {
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils';
import { infiniteScrollRefId } from 'app/features/logs/logsModel';
import { combinePanelData } from 'app/features/logs/response';
import { getFiscalYearStartMonth, getTimeZone } from 'app/features/profile/state/selectors';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import {
@ -764,6 +764,7 @@ export const runLoadMoreLogsQueries = createAsyncThunk<void, RunLoadMoreLogsQuer
return of({ ...queryResponse, state: LoadingState.Loading });
}
return decorateData(
// This shouldn't be needed after https://github.com/grafana/grafana/issues/57327 is fixed
combinePanelData(queryResponse, data),
queryResponse,
absoluteRange,

View File

@ -14,7 +14,7 @@ import {
TimeRange,
LoadingState,
} from '@grafana/data';
import { combineResponses } from 'app/features/logs/response';
import { combineResponses } from '@grafana/o11y-ds-frontend';
import { LokiDatasource } from './datasource';
import { splitTimeRange as splitLogsTimeRange } from './logsTimeSplitting';

View File

@ -2,9 +2,9 @@ import { css } from '@emotion/css';
import React, { useState, useEffect } from 'react';
import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data';
import { LocalStorageValueProvider } from '@grafana/o11y-ds-frontend';
import { reportInteraction } from '@grafana/runtime';
import { LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui';
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
import { LokiLabelBrowser } from '../../components/LokiLabelBrowser';
import { LokiDatasource } from '../../datasource';

View File

@ -1,7 +1,6 @@
import { render, screen, getAllByRole, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { getSelectParent } from 'test/helpers/selectOptionInTest';
import { dateTime } from '@grafana/data';
@ -136,3 +135,6 @@ describe('LokiQueryBuilder', () => {
});
});
});
const getSelectParent = (input: HTMLElement) =>
input.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;

View File

@ -1,7 +1,6 @@
import { render, screen, waitFor, findAllByRole } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { getSelectParent } from 'test/helpers/selectOptionInTest';
import { createLokiDatasource } from '../../__mocks__/datasource';
@ -138,3 +137,6 @@ async function addOperation(section: string, op: string) {
// anywhere when debugging so not sure what style is it picking up.
await userEvent.click(opItem, { pointerEventsCheck: 0 });
}
const getSelectParent = (input: HTMLElement) =>
input.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;

View File

@ -4,7 +4,6 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { useStyles2, HorizontalGroup, IconButton, Tooltip, Icon } from '@grafana/ui';
import { getModKey } from 'app/core/utils/browser';
import { testIds } from '../../components/LokiQueryEditor';
import { LokiQueryField } from '../../components/LokiQueryField';
@ -57,7 +56,7 @@ export function LokiQueryCodeEditor({
size="xs"
tooltip="Format query"
/>
<Tooltip content={`Use ${getModKey()}+z to undo`}>
<Tooltip content={`Use ctrl/cmd + z to undo`}>
<Icon className={styles.hint} name="keyboard" />
</Tooltip>
</HorizontalGroup>

View File

@ -2,10 +2,9 @@ import { css } from '@emotion/css';
import { capitalize } from 'lodash';
import React, { useMemo, useState } from 'react';
import { CoreApp, DataQuery, GrafanaTheme2 } from '@grafana/data';
import { CoreApp, DataQuery, GrafanaTheme2, getNextRefId } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { Button, Collapse, Modal, useStyles2 } from '@grafana/ui';
import { getNextRefIdChar } from 'app/core/utils/query';
import { LokiQuery } from '../../types';
import { lokiQueryModeller } from '../LokiQueryModeller';
@ -52,7 +51,7 @@ export const QueryPatternsModal = (props: Props) => {
if (hasNewQueryOption && selectAsNewQuery) {
onAddQuery({
...query,
refId: getNextRefIdChar(queries ?? [query]),
refId: getNextRefId(queries ?? [query]),
expr: lokiQueryModeller.renderQuery(visualQuery.query),
});
} else {