diff --git a/packages/grafana-o11y-ds-frontend/src/LocalStorageValueProvider/LocalStorageValueProvider.tsx b/packages/grafana-o11y-ds-frontend/src/LocalStorageValueProvider/LocalStorageValueProvider.tsx new file mode 100644 index 00000000000..b81935d4337 --- /dev/null +++ b/packages/grafana-o11y-ds-frontend/src/LocalStorageValueProvider/LocalStorageValueProvider.tsx @@ -0,0 +1,49 @@ +import React, { useEffect, useState } from 'react'; + +import { store } from '../store'; + +export interface Props { + storageKey: string; + defaultValue: T; + children: (value: T, onSaveToStore: (value: T) => void, onDeleteFromStore: () => void) => React.ReactNode; +} + +export const LocalStorageValueProvider = (props: Props) => { + 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)}; +}; diff --git a/public/app/features/logs/response.test.ts b/packages/grafana-o11y-ds-frontend/src/combineResponses.test.ts similarity index 75% rename from public/app/features/logs/response.test.ts rename to packages/grafana-o11y-ds-frontend/src/combineResponses.test.ts index 1c96656cea7..9438c1e20f8 100644 --- a/public/app/features/logs/response.test.ts +++ b/packages/grafana-o11y-ds-frontend/src/combineResponses.test.ts @@ -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, + }; +} diff --git a/public/app/features/logs/response.ts b/packages/grafana-o11y-ds-frontend/src/combineResponses.ts similarity index 99% rename from public/app/features/logs/response.ts rename to packages/grafana-o11y-ds-frontend/src/combineResponses.ts index fd09c052d66..f4133a7fc22 100644 --- a/public/app/features/logs/response.ts +++ b/packages/grafana-o11y-ds-frontend/src/combineResponses.ts @@ -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[] diff --git a/packages/grafana-o11y-ds-frontend/src/index.ts b/packages/grafana-o11y-ds-frontend/src/index.ts index 18bd6e95419..c2f7e931a27 100644 --- a/packages/grafana-o11y-ds-frontend/src/index.ts +++ b/packages/grafana-o11y-ds-frontend/src/index.ts @@ -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'; diff --git a/packages/grafana-o11y-ds-frontend/src/store.ts b/packages/grafana-o11y-ds-frontend/src/store.ts new file mode 100644 index 00000000000..3942f408a73 --- /dev/null +++ b/packages/grafana-o11y-ds-frontend/src/store.ts @@ -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(key: string): T | undefined; + getObject(key: string, def: T): T; + getObject(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(); diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index a78152b9d11..47f58b0ebbc 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -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 { }); }); }); + +const getSelectParent = (input: HTMLElement) => + input.parentElement?.parentElement?.parentElement?.parentElement?.parentElement; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx index 2ff57e8f5c0..fd89631554c 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx @@ -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; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx index 7e4fcc2ce3c..5db5313f985 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx @@ -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" /> - + diff --git a/public/app/plugins/datasource/loki/querybuilder/components/QueryPatternsModal.tsx b/public/app/plugins/datasource/loki/querybuilder/components/QueryPatternsModal.tsx index f2dbd821133..385b032b05e 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/QueryPatternsModal.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/QueryPatternsModal.tsx @@ -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 {