mirror of
https://github.com/grafana/grafana.git
synced 2025-12-20 11:40:21 +08:00
Compare commits
1 Commits
docs/add-t
...
fastfrwrd/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8e8069135 |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
69
public/app/features/datalinks/utils.test.ts
Normal file
69
public/app/features/datalinks/utils.test.ts
Normal 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 }]);
|
||||
});
|
||||
});
|
||||
23
public/app/features/datalinks/utils.ts
Normal file
23
public/app/features/datalinks/utils.ts
Normal 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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user