Compare commits

...

1 Commits

Author SHA1 Message Date
Paul Marbach
e8e8069135 Utils: Reorganize some global viz utils out of StatusHistory 2025-11-03 09:53:07 -05:00
16 changed files with 200 additions and 74 deletions

1
.github/CODEOWNERS vendored
View File

@@ -937,6 +937,7 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform
/public/app/features/datasources/ @grafana/plugins-platform-frontend
/public/app/features/dimensions/ @grafana/dataviz-squad
/public/app/features/dataframe-import/ @grafana/dataviz-squad
/public/app/features/datalinks/ @grafana/dataviz-squad
/public/app/features/explore/ @grafana/observability-traces-and-profiling
/public/app/features/expressions/ @grafana/grafana-datasources-core-services
/public/app/features/folders/ @grafana/grafana-search-navigate-organise

View File

@@ -7,6 +7,7 @@ import {
DataFrame,
Field,
FieldType,
createDataFrame,
} from '@grafana/data';
import { config } from '@grafana/runtime';
@@ -17,6 +18,7 @@ import {
isInfinityActionWithAuth,
getActions,
INFINITY_DATASOURCE_TYPE,
getFieldActions,
} from './utils';
jest.mock('../query/state/PanelQueryRunner', () => ({
@@ -323,3 +325,58 @@ describe('getActions filtering', () => {
expect(result.map((a) => a.title)).toEqual(expectedActionTitles);
});
});
describe('getFieldActions', () => {
it('does not return actions if scopedVars are not set on the state', () => {
const field: Field = {
name: 'field1',
type: FieldType.string,
values: ['foo', 'bar', 'baz'],
config: {
actions: [
{
title: 'Action',
type: ActionType.Fetch,
[ActionType.Fetch]: { url: '', method: HttpRequestMethod.GET },
},
],
},
state: {},
};
const actions = getFieldActions(createDataFrame({ fields: [field] }), field, (str) => str, 0, 'table');
expect(actions).toHaveLength(0);
});
it('deduplicates actions based on title', () => {
const field: Field = {
name: 'field1',
type: FieldType.string,
values: ['foo', 'bar', 'baz'],
config: {
actions: [
{
title: 'Duplicate Action',
type: ActionType.Fetch,
[ActionType.Fetch]: { url: '', method: HttpRequestMethod.GET },
},
{
title: 'Duplicate Action',
type: ActionType.Infinity,
[ActionType.Infinity]: { url: '', method: HttpRequestMethod.GET, datasourceUid: 'uid' },
},
],
},
state: {
scopedVars: {
var1: { text: 'value1', value: 'value1' },
},
},
};
const actions = getFieldActions(createDataFrame({ fields: [field] }), field, (str) => str, 0, 'table');
expect(actions).toHaveLength(1);
expect(actions[0].title).toBe('Duplicate Action');
expect(actions[0].type).toBe(ActionType.Fetch);
});
});

View File

@@ -318,3 +318,40 @@ export const buildActionProxyRequest = (action: Action, replaceVariables: Interp
const requestBuilder = new InfinityRequestBuilder();
return requestBuilder.buildRequest(infinityConfig, url, data, processedHeaders, processedQueryParams, contentType);
};
/** @internal */
export const getFieldActions = (
dataFrame: DataFrame,
field: Field,
replaceVars: InterpolateFunction,
rowIndex: number,
visualizationType?: string
) => {
const actions: Array<ActionModel<Field>> = [];
if (field.state?.scopedVars) {
const actionLookup = new Set<string>();
const actionsModel = getActions(
dataFrame,
field,
field.state.scopedVars,
replaceVars,
field.config.actions ?? [],
{
valueRowIndex: rowIndex,
},
visualizationType
);
actionsModel.forEach((action) => {
const key = `${action.title}`;
if (!actionLookup.has(key)) {
actions.push(action);
actionLookup.add(key);
}
});
}
return actions;
};

View File

@@ -0,0 +1,69 @@
import { Field, FieldType } from '@grafana/data';
import { getDataLinks } from './utils';
describe('getDataLinks', () => {
it('returns an empty array when there are no links configured', () => {
const field: Field = {
name: 'test',
type: FieldType.number,
values: [1, 2, 3],
config: {},
};
const links = getDataLinks(field, 0);
expect(links).toEqual([]);
});
it('returns an empty array if getLinks is not defined', () => {
const field: Field = {
name: 'test',
type: FieldType.number,
values: [1, 2, 3],
config: {
links: [{ title: 'Link 1', url: 'http://example.com' }],
},
};
const links = getDataLinks(field, 0);
expect(links).toEqual([]);
});
it('returns links from getLinks function', () => {
const field: Field = {
name: 'test',
type: FieldType.number,
values: [1, 2, 3],
config: {
links: [{ title: 'Link 1', url: 'http://example.com' }],
},
display: jest.fn((v) => ({ text: `Value: ${v}`, numeric: Number(v) })),
getLinks: jest.fn(({ calculatedValue }) => [
{ title: `Link ${calculatedValue?.text}`, href: 'http://example.com', target: '_blank', origin: field },
]),
};
expect(getDataLinks(field, 0)).toEqual([
{ title: `Link Value: 1`, href: 'http://example.com', target: '_blank', origin: field },
]);
});
it('deduplicates links based on title and href', () => {
const field: Field = {
name: 'test',
type: FieldType.number,
values: [1, 2, 3],
config: {
links: [{ title: 'Link 1', url: 'http://example.com' }],
},
display: jest.fn((v) => ({ text: `Value: ${v}`, numeric: Number(v) })),
getLinks: jest.fn(() => [
{ title: 'Duplicate Link', href: 'http://example.com', target: '_blank', origin: field },
{ title: 'Duplicate Link', href: 'http://example.com', target: '_blank', origin: field },
]),
};
const links = getDataLinks(field, 0);
expect(links).toEqual([{ title: 'Duplicate Link', href: 'http://example.com', target: '_blank', origin: field }]);
});
});

View File

@@ -0,0 +1,23 @@
import { Field, LinkModel } from '@grafana/data';
/** @internal */
export const getDataLinks = (field: Field, rowIdx: number) => {
const links: Array<LinkModel<Field>> = [];
if ((field.config.links?.length ?? 0) > 0 && field.getLinks != null) {
const v = field.values[rowIdx];
const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v };
const linkLookup = new Set<string>();
field.getLinks({ calculatedValue: disp, valueRowIndex: rowIdx }).forEach((link) => {
const key = `${link.title}/${link.href}`;
if (!linkLookup.has(key)) {
links.push(link);
linkLookup.add(key);
}
});
}
return links;
};

View File

@@ -3,8 +3,8 @@ import { css } from '@emotion/css';
import { DataFrame, Field, formattedValueToString, getFieldDisplayName, GrafanaTheme2, LinkModel } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { TextLink, useStyles2 } from '@grafana/ui';
import { getDataLinks } from 'app/features/datalinks/utils';
import { renderValue } from 'app/plugins/panel/geomap/utils/uiUtils';
import { getDataLinks } from 'app/plugins/panel/status-history/utils';
export interface Props {
data?: DataFrame; // source data

View File

@@ -24,8 +24,8 @@ import {
} from '@grafana/ui/internal';
import { getActions, getActionsDefaultField } from 'app/features/actions/utils';
import { Scene } from 'app/features/canvas/runtime/scene';
import { getDataLinks } from 'app/features/datalinks/utils';
import { getDataLinks } from '../../status-history/utils';
import { getElementFields, getRowIndex } from '../utils';
interface Props {

View File

@@ -25,12 +25,13 @@ import {
ColorPlacement,
} from '@grafana/ui/internal';
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
import { getFieldActions } from 'app/features/actions/utils';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { getDataLinks } from 'app/features/datalinks/utils';
import { isHeatmapCellsDense, readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
import { getDisplayValuesAndLinks } from 'app/features/visualization/data-hover/DataHoverView';
import { ExemplarTooltip } from 'app/features/visualization/data-hover/ExemplarTooltip';
import { getDataLinks, getFieldActions } from '../status-history/utils';
import { isTooltipScrollable } from '../timeseries/utils';
import { HeatmapData } from './fields';

View File

@@ -1,7 +1,7 @@
import { ReactNode, useMemo } from 'react';
import { DataFrame, formattedValueToString } from '@grafana/data';
import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen';
import { SortOrder, TooltipDisplayMode } from '@grafana/schema';
import {
VizTooltipContent,
VizTooltipFooter,
@@ -10,8 +10,8 @@ import {
getContentItems,
VizTooltipItem,
} from '@grafana/ui/internal';
import { getDataLinks } from 'app/features/datalinks/utils';
import { getDataLinks } from '../status-history/utils';
import { isTooltipScrollable } from '../timeseries/utils';
export interface HistogramTooltipProps {

View File

@@ -12,8 +12,8 @@ import {
VizTooltipItem,
} from '@grafana/ui/internal';
import { findNextStateIndex, fmtDuration } from 'app/core/components/TimelineChart/utils';
import { getFieldActions } from 'app/features/actions/utils';
import { getFieldActions } from '../status-history/utils';
import { TimeSeriesTooltipProps } from '../timeseries/TimeSeriesTooltip';
import { isTooltipScrollable } from '../timeseries/utils';

View File

@@ -1,59 +0,0 @@
import { DataFrame, ActionModel, Field, InterpolateFunction, LinkModel } from '@grafana/data';
import { getActions } from 'app/features/actions/utils';
export const getDataLinks = (field: Field, rowIdx: number) => {
const links: Array<LinkModel<Field>> = [];
if ((field.config.links?.length ?? 0) > 0 && field.getLinks != null) {
const v = field.values[rowIdx];
const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v };
const linkLookup = new Set<string>();
field.getLinks({ calculatedValue: disp, valueRowIndex: rowIdx }).forEach((link) => {
const key = `${link.title}/${link.href}`;
if (!linkLookup.has(key)) {
links.push(link);
linkLookup.add(key);
}
});
}
return links;
};
export const getFieldActions = (
dataFrame: DataFrame,
field: Field,
replaceVars: InterpolateFunction,
rowIndex: number,
visualizationType?: string
) => {
const actions: Array<ActionModel<Field>> = [];
if (field.state?.scopedVars) {
const actionLookup = new Set<string>();
const actionsModel = getActions(
dataFrame,
field,
field.state.scopedVars,
replaceVars,
field.config.actions ?? [],
{
valueRowIndex: rowIndex,
},
visualizationType
);
actionsModel.forEach((action) => {
const key = `${action.title}`;
if (!actionLookup.has(key)) {
actions.push(action);
actionLookup.add(key);
}
});
}
return actions;
};

View File

@@ -19,8 +19,7 @@ import {
VizTooltipItem,
AdHocFilterModel,
} from '@grafana/ui/internal';
import { getFieldActions } from '../status-history/utils';
import { getFieldActions } from 'app/features/actions/utils';
import { isTooltipScrollable } from './utils';

View File

@@ -9,10 +9,9 @@ import { TimeZone } from '@grafana/schema';
import { floatingUtils, Portal, UPlotConfigBuilder, useStyles2 } from '@grafana/ui';
import { VizTooltipItem } from '@grafana/ui/internal';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { getDataLinks } from 'app/features/datalinks/utils';
import { ExemplarTooltip } from 'app/features/visualization/data-hover/ExemplarTooltip';
import { getDataLinks } from '../../status-history/utils';
interface ExemplarMarkerProps {
timeZone: TimeZone;
dataFrame: DataFrame;

View File

@@ -9,7 +9,8 @@ import { ActionModel, DataFrame, GrafanaTheme2, InterpolateFunction, LinkModel }
import { selectors } from '@grafana/e2e-selectors';
import { TimeZone } from '@grafana/schema';
import { floatingUtils, useStyles2 } from '@grafana/ui';
import { getDataLinks, getFieldActions } from 'app/plugins/panel/status-history/utils';
import { getFieldActions } from 'app/features/actions/utils';
import { getDataLinks } from 'app/features/datalinks/utils';
import { AnnotationEditor2 } from './AnnotationEditor2';
import { AnnotationTooltip2 } from './AnnotationTooltip2';

View File

@@ -15,8 +15,7 @@ import {
usePanelContext,
} from '@grafana/ui';
import { getDisplayValuesForCalcs, TooltipHoverMode } from '@grafana/ui/internal';
import { getDataLinks } from '../status-history/utils';
import { getDataLinks } from 'app/features/datalinks/utils';
import { XYChartTooltip } from './XYChartTooltip';
import { Options } from './panelcfg.gen';

View File

@@ -9,8 +9,7 @@ import {
ColorIndicator,
VizTooltipItem,
} from '@grafana/ui/internal';
import { getFieldActions } from '../status-history/utils';
import { getFieldActions } from 'app/features/actions/utils';
import { XYSeries } from './types2';
import { fmt } from './utils';