mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: New instant query results view in Explore (#60479)
Add new default instant query UI option for prometheus users in Explore. Co-authored-by: Beto Muniz <contato@betomuniz.com> Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
parent
0e7640475f
commit
0e265245eb
@ -3743,9 +3743,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:5381": [
|
"public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/explore/TableContainer.test.tsx:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/features/explore/TraceView/TraceView.test.tsx:5381": [
|
"public/app/features/explore/TraceView/TraceView.test.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
@ -21,7 +21,15 @@ export enum LoadingState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Should be kept in sync with grafana-plugin-sdk-go/data/frame_meta.go
|
// Should be kept in sync with grafana-plugin-sdk-go/data/frame_meta.go
|
||||||
export const preferredVisualizationTypes = ['graph', 'table', 'logs', 'trace', 'nodeGraph', 'flamegraph'] as const;
|
export const preferredVisualizationTypes = [
|
||||||
|
'graph',
|
||||||
|
'table',
|
||||||
|
'logs',
|
||||||
|
'trace',
|
||||||
|
'nodeGraph',
|
||||||
|
'flamegraph',
|
||||||
|
'rawPrometheus',
|
||||||
|
] as const;
|
||||||
export type PreferredVisualisationType = typeof preferredVisualizationTypes[number];
|
export type PreferredVisualisationType = typeof preferredVisualizationTypes[number];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -86,6 +86,7 @@ const dummyProps: Props = {
|
|||||||
splitted: false,
|
splitted: false,
|
||||||
isFromCompactUrl: false,
|
isFromCompactUrl: false,
|
||||||
eventBus: new EventBusSrv(),
|
eventBus: new EventBusSrv(),
|
||||||
|
showRawPrometheus: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
|
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
|
||||||
|
@ -40,6 +40,7 @@ import { NoData } from './NoData';
|
|||||||
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
|
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
|
||||||
import { NodeGraphContainer } from './NodeGraphContainer';
|
import { NodeGraphContainer } from './NodeGraphContainer';
|
||||||
import { QueryRows } from './QueryRows';
|
import { QueryRows } from './QueryRows';
|
||||||
|
import RawPrometheusContainer from './RawPrometheusContainer';
|
||||||
import { ResponseErrorContainer } from './ResponseErrorContainer';
|
import { ResponseErrorContainer } from './ResponseErrorContainer';
|
||||||
import RichHistoryContainer from './RichHistory/RichHistoryContainer';
|
import RichHistoryContainer from './RichHistory/RichHistoryContainer';
|
||||||
import { SecondaryActions } from './SecondaryActions';
|
import { SecondaryActions } from './SecondaryActions';
|
||||||
@ -316,6 +317,21 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderRawPrometheus(width: number) {
|
||||||
|
const { exploreId, datasourceInstance, timeZone } = this.props;
|
||||||
|
return (
|
||||||
|
<RawPrometheusContainer
|
||||||
|
showRawPrometheus={true}
|
||||||
|
ariaLabel={selectors.pages.Explore.General.table}
|
||||||
|
width={width}
|
||||||
|
exploreId={exploreId}
|
||||||
|
onCellFilterAdded={datasourceInstance?.modifyQuery ? this.onCellFilterAdded : undefined}
|
||||||
|
timeZone={timeZone}
|
||||||
|
splitOpenFn={this.onSplitOpen('table')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderLogsPanel(width: number) {
|
renderLogsPanel(width: number) {
|
||||||
const { exploreId, syncedTimes, theme, queryResponse } = this.props;
|
const { exploreId, syncedTimes, theme, queryResponse } = this.props;
|
||||||
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
|
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
|
||||||
@ -388,6 +404,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
|||||||
theme,
|
theme,
|
||||||
showMetrics,
|
showMetrics,
|
||||||
showTable,
|
showTable,
|
||||||
|
showRawPrometheus,
|
||||||
showLogs,
|
showLogs,
|
||||||
showTrace,
|
showTrace,
|
||||||
showNodeGraph,
|
showNodeGraph,
|
||||||
@ -410,6 +427,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
|||||||
queryResponse.nodeGraphFrames,
|
queryResponse.nodeGraphFrames,
|
||||||
queryResponse.flameGraphFrames,
|
queryResponse.flameGraphFrames,
|
||||||
queryResponse.tableFrames,
|
queryResponse.tableFrames,
|
||||||
|
queryResponse.rawPrometheusFrames,
|
||||||
queryResponse.traceFrames,
|
queryResponse.traceFrames,
|
||||||
].every((e) => e.length === 0);
|
].every((e) => e.length === 0);
|
||||||
|
|
||||||
@ -458,6 +476,9 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
|||||||
{showMetrics && graphResult && (
|
{showMetrics && graphResult && (
|
||||||
<ErrorBoundaryAlert>{this.renderGraphPanel(width)}</ErrorBoundaryAlert>
|
<ErrorBoundaryAlert>{this.renderGraphPanel(width)}</ErrorBoundaryAlert>
|
||||||
)}
|
)}
|
||||||
|
{showRawPrometheus && (
|
||||||
|
<ErrorBoundaryAlert>{this.renderRawPrometheus(width)}</ErrorBoundaryAlert>
|
||||||
|
)}
|
||||||
{showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
|
{showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
|
||||||
{showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>}
|
{showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>}
|
||||||
{showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>}
|
{showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>}
|
||||||
@ -518,6 +539,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
showFlameGraph,
|
showFlameGraph,
|
||||||
loading,
|
loading,
|
||||||
isFromCompactUrl,
|
isFromCompactUrl,
|
||||||
|
showRawPrometheus,
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -537,6 +559,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
showTable,
|
showTable,
|
||||||
showTrace,
|
showTrace,
|
||||||
showNodeGraph,
|
showNodeGraph,
|
||||||
|
showRawPrometheus,
|
||||||
showFlameGraph,
|
showFlameGraph,
|
||||||
splitted: isSplit(state),
|
splitted: isSplit(state),
|
||||||
loading,
|
loading,
|
||||||
|
@ -51,9 +51,11 @@ const setup = (propOverrides = {}) => {
|
|||||||
traceFrames: [],
|
traceFrames: [],
|
||||||
nodeGraphFrames: [],
|
nodeGraphFrames: [],
|
||||||
flameGraphFrames: [],
|
flameGraphFrames: [],
|
||||||
|
rawPrometheusFrames: [],
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
|
rawPrometheusResult: null,
|
||||||
},
|
},
|
||||||
runQueries: jest.fn(),
|
runQueries: jest.fn(),
|
||||||
...propOverrides,
|
...propOverrides,
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Field, GrafanaTheme2 } from '@grafana/data/';
|
||||||
|
import { useStyles2 } from '@grafana/ui/';
|
||||||
|
|
||||||
|
import { rawListItemColumnWidth } from './RawListItem';
|
||||||
|
|
||||||
|
const getItemLabelsStyles = (theme: GrafanaTheme2, expanded: boolean) => {
|
||||||
|
return {
|
||||||
|
valueNavigation: css`
|
||||||
|
width: ${rawListItemColumnWidth};
|
||||||
|
font-weight: bold;
|
||||||
|
`,
|
||||||
|
valueNavigationWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
`,
|
||||||
|
itemLabelsWrap: css`
|
||||||
|
${!expanded ? `border-bottom: 1px solid ${theme.colors.border.medium}` : ''};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ItemLabels = ({ valueLabels, expanded }: { valueLabels: Field[]; expanded: boolean }) => {
|
||||||
|
const styles = useStyles2((theme) => getItemLabelsStyles(theme, expanded));
|
||||||
|
return (
|
||||||
|
<div className={styles.itemLabelsWrap}>
|
||||||
|
<div className={styles.valueNavigationWrapper}>
|
||||||
|
{valueLabels.map((value, index) => (
|
||||||
|
<span className={styles.valueNavigation} key={value.name}>
|
||||||
|
{value.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,50 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { RawPrometheusListItemEmptyValue } from '../utils/getRawPrometheusListItemsFromDataFrame';
|
||||||
|
|
||||||
|
import { ItemValues } from './ItemValues';
|
||||||
|
import { RawListValue } from './RawListItem';
|
||||||
|
|
||||||
|
const value1 = 'value 1';
|
||||||
|
const value2 = 'value 2';
|
||||||
|
|
||||||
|
const defaultProps: {
|
||||||
|
totalNumberOfValues: number;
|
||||||
|
values: RawListValue[];
|
||||||
|
hideFieldsWithoutValues: boolean;
|
||||||
|
} = {
|
||||||
|
totalNumberOfValues: 3,
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
key: 'Value #A',
|
||||||
|
value: value1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Value #B',
|
||||||
|
value: value2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Value #C',
|
||||||
|
value: RawPrometheusListItemEmptyValue, // Empty value
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hideFieldsWithoutValues: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ItemValues', () => {
|
||||||
|
it('should render values, with empty values', () => {
|
||||||
|
const itemValues = render(<ItemValues {...defaultProps} />);
|
||||||
|
expect(screen.getByText(value1)).toBeVisible();
|
||||||
|
expect(screen.getByText(value2)).toBeVisible();
|
||||||
|
expect(itemValues?.baseElement?.children?.item(0)?.children?.item(0)?.children.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render values, without empty values', () => {
|
||||||
|
const props = { ...defaultProps, hideFieldsWithoutValues: true };
|
||||||
|
const itemValues = render(<ItemValues {...props} />);
|
||||||
|
expect(screen.getByText(value1)).toBeVisible();
|
||||||
|
expect(screen.getByText(value2)).toBeVisible();
|
||||||
|
expect(itemValues?.baseElement?.children?.item(0)?.children?.item(0)?.children.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,72 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data/';
|
||||||
|
import { useStyles2 } from '@grafana/ui/';
|
||||||
|
|
||||||
|
import { RawPrometheusListItemEmptyValue } from '../utils/getRawPrometheusListItemsFromDataFrame';
|
||||||
|
|
||||||
|
import { rawListItemColumnWidth, rawListPaddingToHoldSpaceForCopyIcon, RawListValue } from './RawListItem';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2, totalNumberOfValues: number) => ({
|
||||||
|
rowWrapper: css`
|
||||||
|
position: relative;
|
||||||
|
min-width: ${rawListItemColumnWidth};
|
||||||
|
padding-right: 5px;
|
||||||
|
`,
|
||||||
|
rowValue: css`
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
display: block;
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari and Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background: linear-gradient(to right, transparent calc(100% - 25px), ${theme.colors.background.primary});
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
rowValuesWrap: css`
|
||||||
|
padding-left: ${rawListPaddingToHoldSpaceForCopyIcon};
|
||||||
|
width: calc(${totalNumberOfValues} * ${rawListItemColumnWidth});
|
||||||
|
display: flex;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ItemValues = ({
|
||||||
|
totalNumberOfValues,
|
||||||
|
values,
|
||||||
|
hideFieldsWithoutValues,
|
||||||
|
}: {
|
||||||
|
totalNumberOfValues: number;
|
||||||
|
values: RawListValue[];
|
||||||
|
hideFieldsWithoutValues: boolean;
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles2((theme) => getStyles(theme, totalNumberOfValues));
|
||||||
|
return (
|
||||||
|
<div role={'cell'} className={styles.rowValuesWrap}>
|
||||||
|
{values?.map((value) => {
|
||||||
|
if (hideFieldsWithoutValues && (value.value === undefined || value.value === RawPrometheusListItemEmptyValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={value.key} className={styles.rowWrapper}>
|
||||||
|
<span className={styles.rowValue}>{value.value}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,70 @@
|
|||||||
|
import { render, screen, within } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FieldType, FormattedValue, toDataFrame } from '@grafana/data/src';
|
||||||
|
|
||||||
|
import RawListContainer, { RawListContainerProps } from './RawListContainer';
|
||||||
|
|
||||||
|
function getList(): HTMLElement {
|
||||||
|
return screen.getByRole('table');
|
||||||
|
}
|
||||||
|
|
||||||
|
const display = (input: string): FormattedValue => {
|
||||||
|
return {
|
||||||
|
text: input,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataFrame = toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'Time',
|
||||||
|
type: FieldType.time,
|
||||||
|
values: [1609459200000, 1609470000000, 1609462800000, 1609466400000],
|
||||||
|
config: {
|
||||||
|
custom: {
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: display,
|
||||||
|
name: 'text',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['test_string_1', 'test_string_2', 'test_string_3', 'test_string_4'],
|
||||||
|
config: {
|
||||||
|
custom: {
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '__name__',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['test_string_1', 'test_string_2', 'test_string_3', 'test_string_4'],
|
||||||
|
config: {
|
||||||
|
custom: {
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultProps: RawListContainerProps = {
|
||||||
|
tableResult: dataFrame,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('RawListContainer', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
render(<RawListContainer {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(getList()).toBeInTheDocument();
|
||||||
|
const rows = within(getList()).getAllByRole('row');
|
||||||
|
expect(rows).toHaveLength(4);
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
expect(screen.getAllByText(`test_string_${index + 1}`)[0]).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,167 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useWindowSize } from 'react-use';
|
||||||
|
import { VariableSizeList as List } from 'react-window';
|
||||||
|
|
||||||
|
import { DataFrame, Field as DataFrameField } from '@grafana/data/';
|
||||||
|
import { Field, Switch } from '@grafana/ui/';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getRawPrometheusListItemsFromDataFrame,
|
||||||
|
RawPrometheusListItemEmptyValue,
|
||||||
|
} from '../utils/getRawPrometheusListItemsFromDataFrame';
|
||||||
|
|
||||||
|
import { ItemLabels } from './ItemLabels';
|
||||||
|
import RawListItem from './RawListItem';
|
||||||
|
|
||||||
|
export type instantQueryRawVirtualizedListData = { Value: string; __name__: string; [index: string]: string };
|
||||||
|
|
||||||
|
export interface RawListContainerProps {
|
||||||
|
tableResult: DataFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
wrapper: css`
|
||||||
|
height: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
`,
|
||||||
|
switchWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 0;
|
||||||
|
`,
|
||||||
|
switchLabel: css`
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
`,
|
||||||
|
switch: css`
|
||||||
|
margin-left: 10px;
|
||||||
|
`,
|
||||||
|
resultCount: css`
|
||||||
|
margin-bottom: 4px;
|
||||||
|
`,
|
||||||
|
header: css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.25;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mobileWidthThreshold = 480;
|
||||||
|
const numberOfColumnsBeforeExpandedViewIsDefault = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The container that provides the virtualized list to the child components
|
||||||
|
* @param props
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
const RawListContainer = (props: RawListContainerProps) => {
|
||||||
|
const { tableResult } = props;
|
||||||
|
const dataFrame = cloneDeep(tableResult);
|
||||||
|
const listRef = useRef<List | null>(null);
|
||||||
|
|
||||||
|
const valueLabels = dataFrame.fields.filter((field) => field.name.includes('Value'));
|
||||||
|
const items = getRawPrometheusListItemsFromDataFrame(dataFrame);
|
||||||
|
const { width } = useWindowSize();
|
||||||
|
const [isExpandedView, setIsExpandedView] = useState(
|
||||||
|
width <= mobileWidthThreshold || valueLabels.length > numberOfColumnsBeforeExpandedViewIsDefault
|
||||||
|
);
|
||||||
|
|
||||||
|
const onContentClick = () => {
|
||||||
|
setIsExpandedView(!isExpandedView);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// After the expanded view has updated, tell the list to re-render
|
||||||
|
listRef.current?.resetAfterIndex(0, true);
|
||||||
|
}, [isExpandedView]);
|
||||||
|
|
||||||
|
const calculateInitialHeight = (length: number): number => {
|
||||||
|
const maxListHeight = 600;
|
||||||
|
const shortListLength = 10;
|
||||||
|
|
||||||
|
if (length < shortListLength) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
sum += getListItemHeight(i, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(maxListHeight, sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxListHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getListItemHeight = (itemIndex: number, isExpandedView: boolean) => {
|
||||||
|
const singleLineHeight = 32;
|
||||||
|
const additionalLineHeight = 22;
|
||||||
|
if (!isExpandedView) {
|
||||||
|
return singleLineHeight;
|
||||||
|
}
|
||||||
|
const item = items[itemIndex];
|
||||||
|
|
||||||
|
// Height of 1.5 lines, plus the number of non-value attributes times the height of additional lines
|
||||||
|
return 1.5 * singleLineHeight + (Object.keys(item).length - valueLabels.length) * additionalLineHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<Field className={styles.switchWrapper} label={`Expand results`} htmlFor={'isExpandedView'}>
|
||||||
|
<div className={styles.switch}>
|
||||||
|
<Switch onChange={onContentClick} id="isExpandedView" value={isExpandedView} label={`Expand results`} />
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div className={styles.resultCount}>Result series: {items.length}</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div role={'table'}>
|
||||||
|
{
|
||||||
|
<>
|
||||||
|
{/* Show the value headings above all the values, but only if we're in the contracted view */}
|
||||||
|
{valueLabels.length > 1 && !isExpandedView && (
|
||||||
|
<ItemLabels valueLabels={valueLabels} expanded={isExpandedView} />
|
||||||
|
)}
|
||||||
|
<List
|
||||||
|
ref={listRef}
|
||||||
|
itemCount={items.length}
|
||||||
|
className={styles.wrapper}
|
||||||
|
itemSize={(index) => getListItemHeight(index, isExpandedView)}
|
||||||
|
height={calculateInitialHeight(items.length)}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
{({ index, style }) => {
|
||||||
|
let filteredValueLabels: DataFrameField[] | undefined;
|
||||||
|
if (isExpandedView) {
|
||||||
|
filteredValueLabels = valueLabels.filter((valueLabel) => {
|
||||||
|
const itemWithValue = items[index][valueLabel.name];
|
||||||
|
return itemWithValue && itemWithValue !== RawPrometheusListItemEmptyValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div role="row" style={{ ...style, overflow: 'hidden' }}>
|
||||||
|
<RawListItem
|
||||||
|
isExpandedView={isExpandedView}
|
||||||
|
valueLabels={filteredValueLabels}
|
||||||
|
totalNumberOfValues={valueLabels.length}
|
||||||
|
listKey={items[index].__name__}
|
||||||
|
listItemData={items[index]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RawListContainer;
|
@ -0,0 +1,35 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import RawListItem, { RawListProps } from './RawListItem';
|
||||||
|
|
||||||
|
function getCopyElement(): HTMLElement {
|
||||||
|
return screen.getByLabelText('Copy to clipboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps: RawListProps = {
|
||||||
|
isExpandedView: false,
|
||||||
|
listItemData: {
|
||||||
|
Value: '1234556677888',
|
||||||
|
__name__: 'metric_name_here',
|
||||||
|
job: 'jobValue',
|
||||||
|
instance: 'instanceValue',
|
||||||
|
},
|
||||||
|
listKey: '0',
|
||||||
|
totalNumberOfValues: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('RawListItem', () => {
|
||||||
|
it('should render', () => {
|
||||||
|
render(<RawListItem {...defaultProps} />);
|
||||||
|
|
||||||
|
const copyElement = getCopyElement();
|
||||||
|
|
||||||
|
expect(copyElement).toBeInTheDocument();
|
||||||
|
expect(copyElement).toBeVisible();
|
||||||
|
expect(screen.getAllByText(`jobValue`)[0]).toBeVisible();
|
||||||
|
expect(screen.getAllByText(`instanceValue`)[0]).toBeVisible();
|
||||||
|
expect(screen.getAllByText(`metric_name_here`)[0]).toBeVisible();
|
||||||
|
expect(screen.getAllByText(`1234556677888`)[0]).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
162
public/app/features/explore/PrometheusListView/RawListItem.tsx
Normal file
162
public/app/features/explore/PrometheusListView/RawListItem.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
|
||||||
|
import { Field, GrafanaTheme2 } from '@grafana/data/';
|
||||||
|
import { IconButton, useStyles2 } from '@grafana/ui/';
|
||||||
|
|
||||||
|
import { ItemLabels } from './ItemLabels';
|
||||||
|
import { ItemValues } from './ItemValues';
|
||||||
|
import { instantQueryRawVirtualizedListData } from './RawListContainer';
|
||||||
|
import RawListItemAttributes from './RawListItemAttributes';
|
||||||
|
|
||||||
|
export interface RawListProps {
|
||||||
|
listItemData: instantQueryRawVirtualizedListData;
|
||||||
|
listKey: string;
|
||||||
|
totalNumberOfValues: number;
|
||||||
|
valueLabels?: Field[];
|
||||||
|
isExpandedView: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RawListValue = { key: string; value: string };
|
||||||
|
export const rawListExtraSpaceAtEndOfLine = '20px';
|
||||||
|
export const rawListItemColumnWidth = '80px';
|
||||||
|
export const rawListPaddingToHoldSpaceForCopyIcon = '25px';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2, totalNumberOfValues: number, isExpandedView: boolean) => ({
|
||||||
|
rowWrapper: css`
|
||||||
|
border-bottom: 1px solid ${theme.colors.border.medium};
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 22px;
|
||||||
|
${!isExpandedView ? 'align-items: center;' : ''}
|
||||||
|
${!isExpandedView ? 'height: 100%;' : ''}
|
||||||
|
`,
|
||||||
|
copyToClipboardWrapper: css`
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
${!isExpandedView ? 'bottom: 0;' : ''}
|
||||||
|
${isExpandedView ? 'top: 4px;' : 'top: 0;'}
|
||||||
|
margin: auto;
|
||||||
|
z-index: 1;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
`,
|
||||||
|
rowLabelWrapWrap: css`
|
||||||
|
position: relative;
|
||||||
|
width: calc(100% - (${totalNumberOfValues} * ${rawListItemColumnWidth}) - ${rawListPaddingToHoldSpaceForCopyIcon});
|
||||||
|
`,
|
||||||
|
rowLabelWrap: css`
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
padding-right: ${rawListExtraSpaceAtEndOfLine};
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari and Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent calc(100% - ${rawListExtraSpaceAtEndOfLine}),
|
||||||
|
${theme.colors.background.primary}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
function getQueryValues(allLabels: Pick<instantQueryRawVirtualizedListData, 'Value' | string | number>) {
|
||||||
|
let attributeValues: RawListValue[] = [];
|
||||||
|
let values: RawListValue[] = [];
|
||||||
|
for (const key in allLabels) {
|
||||||
|
if (key in allLabels && allLabels[key] && !key.includes('Value')) {
|
||||||
|
attributeValues.push({
|
||||||
|
key: key,
|
||||||
|
value: allLabels[key],
|
||||||
|
});
|
||||||
|
} else if (key in allLabels && allLabels[key] && key.includes('Value')) {
|
||||||
|
values.push({
|
||||||
|
key: key,
|
||||||
|
value: allLabels[key],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
values: values,
|
||||||
|
attributeValues: attributeValues,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const RawListItem = ({ listItemData, listKey, totalNumberOfValues, valueLabels, isExpandedView }: RawListProps) => {
|
||||||
|
const { __name__, ...allLabels } = listItemData;
|
||||||
|
const [_, copyToClipboard] = useCopyToClipboard();
|
||||||
|
const displayLength = valueLabels?.length ?? totalNumberOfValues;
|
||||||
|
const styles = useStyles2((theme) => getStyles(theme, displayLength, isExpandedView));
|
||||||
|
const { values, attributeValues } = getQueryValues(allLabels);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the symbols in the dataFrame to uniform strings
|
||||||
|
*/
|
||||||
|
const transformCopyValue = (value: string): string => {
|
||||||
|
if (value === '∞') {
|
||||||
|
return '+Inf';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert the object back into a string
|
||||||
|
const stringRep = `${__name__}{${attributeValues.map((value) => {
|
||||||
|
// For histograms the string representation currently in this object is not directly queryable in all situations, leading to broken copied queries. Omitting the attribute from the copied result gives a query which returns all le values, which I assume to be a more common use case.
|
||||||
|
return value.key !== 'le' ? `${value.key}="${transformCopyValue(value.value)}"` : '';
|
||||||
|
})}}`;
|
||||||
|
|
||||||
|
const hideFieldsWithoutValues = Boolean(valueLabels && valueLabels?.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{valueLabels !== undefined && isExpandedView && (
|
||||||
|
<ItemLabels valueLabels={valueLabels} expanded={isExpandedView} />
|
||||||
|
)}
|
||||||
|
<div key={listKey} className={styles.rowWrapper}>
|
||||||
|
<span className={styles.copyToClipboardWrapper}>
|
||||||
|
<IconButton tooltip="Copy to clipboard" onClick={() => copyToClipboard(stringRep)} name="copy" />
|
||||||
|
</span>
|
||||||
|
<span role={'cell'} className={styles.rowLabelWrapWrap}>
|
||||||
|
<div className={styles.rowLabelWrap}>
|
||||||
|
<span>{__name__}</span>
|
||||||
|
<span>{`{`}</span>
|
||||||
|
<span>
|
||||||
|
{attributeValues.map((value, index) => (
|
||||||
|
<RawListItemAttributes
|
||||||
|
isExpandedView={isExpandedView}
|
||||||
|
value={value}
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
length={attributeValues.length}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<span>{`}`}</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Output the values */}
|
||||||
|
<ItemValues
|
||||||
|
hideFieldsWithoutValues={hideFieldsWithoutValues}
|
||||||
|
totalNumberOfValues={displayLength}
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RawListItem;
|
@ -0,0 +1,48 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { RawListValue } from './RawListItem';
|
||||||
|
import RawListItemAttributes from './RawListItemAttributes';
|
||||||
|
|
||||||
|
const getDefaultProps = (
|
||||||
|
override: Partial<{
|
||||||
|
value: RawListValue;
|
||||||
|
index: number;
|
||||||
|
length: number;
|
||||||
|
isExpandedView: boolean;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const key = 'key';
|
||||||
|
const value = 'value';
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: {
|
||||||
|
key: key,
|
||||||
|
value: value,
|
||||||
|
},
|
||||||
|
index: 0,
|
||||||
|
length: 0,
|
||||||
|
isExpandedView: false,
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('RawListItemAttributes', () => {
|
||||||
|
it('should render collapsed', () => {
|
||||||
|
const props = getDefaultProps({ isExpandedView: false });
|
||||||
|
const attributeRow = render(<RawListItemAttributes {...props} />);
|
||||||
|
|
||||||
|
expect(attributeRow.getByText(props.value.value)).toBeVisible();
|
||||||
|
expect(attributeRow.getByText(props.value.key)).toBeVisible();
|
||||||
|
expect(attributeRow.baseElement.textContent).toEqual(`${props.value.key}="${props.value.value}"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render expanded', () => {
|
||||||
|
const props = getDefaultProps({ isExpandedView: true });
|
||||||
|
const attributeRow = render(<RawListItemAttributes {...props} />);
|
||||||
|
|
||||||
|
expect(attributeRow.getByText(props.value.value)).toBeVisible();
|
||||||
|
expect(attributeRow.getByText(props.value.key)).toBeVisible();
|
||||||
|
expect(attributeRow.baseElement.textContent).toEqual(`${props.value.key}="${props.value.value}"`);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,59 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data/';
|
||||||
|
import { useStyles2 } from '@grafana/ui/';
|
||||||
|
|
||||||
|
import { RawListValue } from './RawListItem';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
// Borrowed from the monaco styles
|
||||||
|
const reddish = theme.isDark ? '#ce9178' : '#a31515';
|
||||||
|
const greenish = theme.isDark ? '#73bf69' : '#56a64b';
|
||||||
|
|
||||||
|
return {
|
||||||
|
metricName: css`
|
||||||
|
color: ${greenish};
|
||||||
|
`,
|
||||||
|
metricValue: css`
|
||||||
|
color: ${reddish};
|
||||||
|
`,
|
||||||
|
expanded: css`
|
||||||
|
display: block;
|
||||||
|
text-indent: 1em;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const RawListItemAttributes = ({
|
||||||
|
value,
|
||||||
|
index,
|
||||||
|
length,
|
||||||
|
isExpandedView,
|
||||||
|
}: {
|
||||||
|
value: RawListValue;
|
||||||
|
index: number;
|
||||||
|
length: number;
|
||||||
|
isExpandedView: boolean;
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
// From the beginning of the string to the start of the `=`
|
||||||
|
const attributeName = value.key;
|
||||||
|
|
||||||
|
// From after the `="` to before the last `"`
|
||||||
|
const attributeValue = value.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={isExpandedView ? styles.expanded : ''} key={index}>
|
||||||
|
<span className={styles.metricName}>{attributeName}</span>
|
||||||
|
<span>=</span>
|
||||||
|
<span>"</span>
|
||||||
|
<span className={styles.metricValue}>{attributeValue}</span>
|
||||||
|
<span>"</span>
|
||||||
|
{index < length - 1 && <span>, </span>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RawListItemAttributes;
|
84
public/app/features/explore/RawPrometheusContainer.test.tsx
Normal file
84
public/app/features/explore/RawPrometheusContainer.test.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame } from '@grafana/data';
|
||||||
|
import { ExploreId, TABLE_RESULTS_STYLE } from 'app/types/explore';
|
||||||
|
|
||||||
|
import { RawPrometheusContainer } from './RawPrometheusContainer';
|
||||||
|
|
||||||
|
function getTable(): HTMLElement {
|
||||||
|
return screen.getAllByRole('table')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableToggle(): HTMLElement {
|
||||||
|
return screen.getAllByRole('radio')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowsData(rows: HTMLElement[]): Object[] {
|
||||||
|
let content = [];
|
||||||
|
for (let i = 1; i < rows.length; i++) {
|
||||||
|
content.push({
|
||||||
|
time: within(rows[i]).getByText(/2021*/).textContent,
|
||||||
|
text: within(rows[i]).getByText(/test_string_*/).textContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataFrame = toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
values: [1609459200000, 1609470000000, 1609462800000, 1609466400000],
|
||||||
|
config: {
|
||||||
|
custom: {
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['test_string_1', 'test_string_2', 'test_string_3', 'test_string_4'],
|
||||||
|
config: {
|
||||||
|
custom: {
|
||||||
|
filterable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
exploreId: ExploreId.left,
|
||||||
|
loading: false,
|
||||||
|
width: 800,
|
||||||
|
onCellFilterAdded: jest.fn(),
|
||||||
|
tableResult: [dataFrame],
|
||||||
|
splitOpenFn: () => {},
|
||||||
|
range: getDefaultTimeRange(),
|
||||||
|
timeZone: InternalTimeZones.utc,
|
||||||
|
resultsStyle: TABLE_RESULTS_STYLE.raw,
|
||||||
|
showRawPrometheus: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('RawPrometheusContainer', () => {
|
||||||
|
it('should render component for prometheus', () => {
|
||||||
|
render(<RawPrometheusContainer {...defaultProps} showRawPrometheus={true} />);
|
||||||
|
|
||||||
|
expect(screen.queryAllByRole('table').length).toBe(1);
|
||||||
|
fireEvent.click(getTableToggle());
|
||||||
|
|
||||||
|
expect(getTable()).toBeInTheDocument();
|
||||||
|
const rows = within(getTable()).getAllByRole('row');
|
||||||
|
expect(rows).toHaveLength(5);
|
||||||
|
expect(getRowsData(rows)).toEqual([
|
||||||
|
{ time: '2021-01-01 00:00:00', text: 'test_string_1' },
|
||||||
|
{ time: '2021-01-01 03:00:00', text: 'test_string_2' },
|
||||||
|
{ time: '2021-01-01 01:00:00', text: 'test_string_3' },
|
||||||
|
{ time: '2021-01-01 02:00:00', text: 'test_string_4' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
168
public/app/features/explore/RawPrometheusContainer.tsx
Normal file
168
public/app/features/explore/RawPrometheusContainer.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
|
import { applyFieldOverrides, DataFrame, SelectableValue, SplitOpen, TimeZone, ValueLinkConfig } from '@grafana/data';
|
||||||
|
import { Collapse, RadioButtonGroup, Table } from '@grafana/ui';
|
||||||
|
import { FilterItem } from '@grafana/ui/src/components/Table/types';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
import { PANEL_BORDER } from 'app/core/constants';
|
||||||
|
import { StoreState, TABLE_RESULTS_STYLE } from 'app/types';
|
||||||
|
import { ExploreId, ExploreItemState, TABLE_RESULTS_STYLES, TableResultsStyle } from 'app/types/explore';
|
||||||
|
|
||||||
|
import { MetaInfoText } from './MetaInfoText';
|
||||||
|
import RawListContainer from './PrometheusListView/RawListContainer';
|
||||||
|
import { getFieldLinksForExplore } from './utils/links';
|
||||||
|
|
||||||
|
interface RawPrometheusContainerProps {
|
||||||
|
ariaLabel?: string;
|
||||||
|
exploreId: ExploreId;
|
||||||
|
width: number;
|
||||||
|
timeZone: TimeZone;
|
||||||
|
onCellFilterAdded?: (filter: FilterItem) => void;
|
||||||
|
showRawPrometheus?: boolean;
|
||||||
|
splitOpenFn: SplitOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PrometheusContainerState {
|
||||||
|
resultsStyle: TableResultsStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: StoreState, { exploreId }: RawPrometheusContainerProps) {
|
||||||
|
const explore = state.explore;
|
||||||
|
const item: ExploreItemState = explore[exploreId]!;
|
||||||
|
const { loading: loadingInState, tableResult, rawPrometheusResult, range } = item;
|
||||||
|
const rawPrometheusFrame: DataFrame[] = rawPrometheusResult ? [rawPrometheusResult] : [];
|
||||||
|
const result = (tableResult?.length ?? false) > 0 && rawPrometheusResult ? tableResult : rawPrometheusFrame;
|
||||||
|
const loading = result && result.length > 0 ? false : loadingInState;
|
||||||
|
|
||||||
|
return { loading, tableResult: result, range };
|
||||||
|
}
|
||||||
|
|
||||||
|
const connector = connect(mapStateToProps, {});
|
||||||
|
|
||||||
|
type Props = RawPrometheusContainerProps & ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
|
export class RawPrometheusContainer extends PureComponent<Props, PrometheusContainerState> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// If resultsStyle is undefined we won't render the toggle, and the default table will be rendered
|
||||||
|
if (props.showRawPrometheus) {
|
||||||
|
this.state = {
|
||||||
|
resultsStyle: TABLE_RESULTS_STYLE.raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMainFrame(frames: DataFrame[] | null) {
|
||||||
|
return frames?.find((df) => df.meta?.custom?.parentRowIndex === undefined) || frames?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeResultsStyle = (resultsStyle: TableResultsStyle) => {
|
||||||
|
this.setState({ resultsStyle });
|
||||||
|
};
|
||||||
|
|
||||||
|
getTableHeight() {
|
||||||
|
const { tableResult } = this.props;
|
||||||
|
const mainFrame = this.getMainFrame(tableResult);
|
||||||
|
|
||||||
|
if (!mainFrame || mainFrame.length === 0) {
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tries to estimate table height
|
||||||
|
return Math.max(Math.min(600, mainFrame.length * 35) + 35);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLabel = () => {
|
||||||
|
const spacing = css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
});
|
||||||
|
const ALL_GRAPH_STYLE_OPTIONS: Array<SelectableValue<TableResultsStyle>> = TABLE_RESULTS_STYLES.map((style) => ({
|
||||||
|
value: style,
|
||||||
|
// capital-case it and switch `_` to ` `
|
||||||
|
label: style[0].toUpperCase() + style.slice(1).replace(/_/, ' '),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={spacing}>
|
||||||
|
{this.state.resultsStyle === TABLE_RESULTS_STYLE.raw ? 'Raw' : 'Table'}
|
||||||
|
<RadioButtonGroup
|
||||||
|
size="sm"
|
||||||
|
options={ALL_GRAPH_STYLE_OPTIONS}
|
||||||
|
value={this.state?.resultsStyle}
|
||||||
|
onChange={this.onChangeResultsStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, onCellFilterAdded, tableResult, width, splitOpenFn, range, ariaLabel, timeZone } = this.props;
|
||||||
|
const height = this.getTableHeight();
|
||||||
|
const tableWidth = width - config.theme.panelPadding * 2 - PANEL_BORDER;
|
||||||
|
|
||||||
|
let dataFrames = tableResult;
|
||||||
|
|
||||||
|
if (dataFrames?.length) {
|
||||||
|
dataFrames = applyFieldOverrides({
|
||||||
|
data: dataFrames,
|
||||||
|
timeZone,
|
||||||
|
theme: config.theme2,
|
||||||
|
replaceVariables: (v: string) => v,
|
||||||
|
fieldConfig: {
|
||||||
|
defaults: {},
|
||||||
|
overrides: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Bit of code smell here. We need to add links here to the frame modifying the frame on every render.
|
||||||
|
// Should work fine in essence but still not the ideal way to pass props. In logs container we do this
|
||||||
|
// differently and sidestep this getLinks API on a dataframe
|
||||||
|
for (const frame of dataFrames) {
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
field.getLinks = (config: ValueLinkConfig) => {
|
||||||
|
return getFieldLinksForExplore({
|
||||||
|
field,
|
||||||
|
rowIndex: config.valueRowIndex!,
|
||||||
|
splitOpenFn,
|
||||||
|
range,
|
||||||
|
dataFrame: frame!,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainFrame = this.getMainFrame(dataFrames);
|
||||||
|
const subFrames = dataFrames?.filter((df) => df.meta?.custom?.parentRowIndex !== undefined);
|
||||||
|
const label = this.state?.resultsStyle !== undefined ? this.renderLabel() : 'Table';
|
||||||
|
|
||||||
|
// Render table as default if resultsStyle is not set.
|
||||||
|
const renderTable = !this.state?.resultsStyle || this.state?.resultsStyle === TABLE_RESULTS_STYLE.table;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapse label={label} loading={loading} isOpen>
|
||||||
|
{mainFrame?.length && (
|
||||||
|
<>
|
||||||
|
{renderTable && (
|
||||||
|
<Table
|
||||||
|
ariaLabel={ariaLabel}
|
||||||
|
data={mainFrame}
|
||||||
|
subData={subFrames}
|
||||||
|
width={tableWidth}
|
||||||
|
height={height}
|
||||||
|
onCellFilterAdded={onCellFilterAdded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{this.state?.resultsStyle === TABLE_RESULTS_STYLE.raw && <RawListContainer tableResult={mainFrame} />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!mainFrame?.length && <MetaInfoText metaItems={[{ value: '0 series returned' }]} />}
|
||||||
|
</Collapse>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connector(RawPrometheusContainer);
|
@ -56,10 +56,12 @@ function setup(error: DataQueryError) {
|
|||||||
tableFrames: [],
|
tableFrames: [],
|
||||||
traceFrames: [],
|
traceFrames: [],
|
||||||
nodeGraphFrames: [],
|
nodeGraphFrames: [],
|
||||||
|
rawPrometheusFrames: [],
|
||||||
flameGraphFrames: [],
|
flameGraphFrames: [],
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
|
rawPrometheusResult: null,
|
||||||
};
|
};
|
||||||
render(
|
render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { screen, render, within } from '@testing-library/react';
|
import { render, screen, within } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DataFrame, toDataFrame, FieldType, InternalTimeZones } from '@grafana/data';
|
import { DataFrame, FieldType, getDefaultTimeRange, InternalTimeZones, toDataFrame } from '@grafana/data';
|
||||||
import { ExploreId } from 'app/types/explore';
|
import { ExploreId } from 'app/types/explore';
|
||||||
|
|
||||||
import { TableContainer } from './TableContainer';
|
import { TableContainer } from './TableContainer';
|
||||||
@ -54,7 +54,7 @@ const defaultProps = {
|
|||||||
onCellFilterAdded: jest.fn(),
|
onCellFilterAdded: jest.fn(),
|
||||||
tableResult: [dataFrame],
|
tableResult: [dataFrame],
|
||||||
splitOpenFn: () => {},
|
splitOpenFn: () => {},
|
||||||
range: {} as any,
|
range: getDefaultTimeRange(),
|
||||||
timeZone: InternalTimeZones.utc,
|
timeZone: InternalTimeZones.utc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -922,9 +922,11 @@ export const processQueryResponse = (
|
|||||||
graphResult,
|
graphResult,
|
||||||
logsResult,
|
logsResult,
|
||||||
tableResult,
|
tableResult,
|
||||||
|
rawPrometheusResult,
|
||||||
traceFrames,
|
traceFrames,
|
||||||
nodeGraphFrames,
|
nodeGraphFrames,
|
||||||
flameGraphFrames,
|
flameGraphFrames,
|
||||||
|
rawPrometheusFrames,
|
||||||
} = response;
|
} = response;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -961,6 +963,7 @@ export const processQueryResponse = (
|
|||||||
queryResponse: response,
|
queryResponse: response,
|
||||||
graphResult,
|
graphResult,
|
||||||
tableResult,
|
tableResult,
|
||||||
|
rawPrometheusResult,
|
||||||
logsResult,
|
logsResult,
|
||||||
loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming,
|
loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming,
|
||||||
showLogs: !!logsResult,
|
showLogs: !!logsResult,
|
||||||
@ -968,6 +971,7 @@ export const processQueryResponse = (
|
|||||||
showTable: !!tableResult?.length,
|
showTable: !!tableResult?.length,
|
||||||
showTrace: !!traceFrames.length,
|
showTrace: !!traceFrames.length,
|
||||||
showNodeGraph: !!nodeGraphFrames.length,
|
showNodeGraph: !!nodeGraphFrames.length,
|
||||||
|
showRawPrometheus: !!rawPrometheusFrames.length,
|
||||||
showFlameGraph: !!flameGraphFrames.length,
|
showFlameGraph: !!flameGraphFrames.length,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -73,6 +73,7 @@ export const makeExplorePaneState = (): ExploreItemState => ({
|
|||||||
tableResult: null,
|
tableResult: null,
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
|
rawPrometheusResult: null,
|
||||||
eventBridge: null as unknown as EventBusExtended,
|
eventBridge: null as unknown as EventBusExtended,
|
||||||
cache: [],
|
cache: [],
|
||||||
richHistory: [],
|
richHistory: [],
|
||||||
@ -92,6 +93,8 @@ export const createEmptyQueryResponse = (): ExplorePanelData => ({
|
|||||||
nodeGraphFrames: [],
|
nodeGraphFrames: [],
|
||||||
flameGraphFrames: [],
|
flameGraphFrames: [],
|
||||||
tableFrames: [],
|
tableFrames: [],
|
||||||
|
rawPrometheusFrames: [],
|
||||||
|
rawPrometheusResult: null,
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
|
@ -97,6 +97,8 @@ const createExplorePanelData = (args: Partial<ExplorePanelData>): ExplorePanelDa
|
|||||||
traceFrames: [],
|
traceFrames: [],
|
||||||
nodeGraphFrames: [],
|
nodeGraphFrames: [],
|
||||||
flameGraphFrames: [],
|
flameGraphFrames: [],
|
||||||
|
rawPrometheusFrames: [],
|
||||||
|
rawPrometheusResult: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...defaults, ...args };
|
return { ...defaults, ...args };
|
||||||
@ -128,6 +130,8 @@ describe('decorateWithGraphLogsTraceTableAndFlameGraph', () => {
|
|||||||
graphResult: null,
|
graphResult: null,
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
|
rawPrometheusFrames: [],
|
||||||
|
rawPrometheusResult: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -153,6 +157,8 @@ describe('decorateWithGraphLogsTraceTableAndFlameGraph', () => {
|
|||||||
graphResult: null,
|
graphResult: null,
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
|
rawPrometheusFrames: [],
|
||||||
|
rawPrometheusResult: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -181,6 +187,8 @@ describe('decorateWithGraphLogsTraceTableAndFlameGraph', () => {
|
|||||||
graphResult: null,
|
graphResult: null,
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
|
rawPrometheusFrames: [],
|
||||||
|
rawPrometheusResult: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -29,6 +29,7 @@ import { preProcessPanelData } from '../../query/state/runRequest';
|
|||||||
export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData => {
|
export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData => {
|
||||||
const graphFrames: DataFrame[] = [];
|
const graphFrames: DataFrame[] = [];
|
||||||
const tableFrames: DataFrame[] = [];
|
const tableFrames: DataFrame[] = [];
|
||||||
|
const rawPrometheusFrames: DataFrame[] = [];
|
||||||
const logsFrames: DataFrame[] = [];
|
const logsFrames: DataFrame[] = [];
|
||||||
const traceFrames: DataFrame[] = [];
|
const traceFrames: DataFrame[] = [];
|
||||||
const nodeGraphFrames: DataFrame[] = [];
|
const nodeGraphFrames: DataFrame[] = [];
|
||||||
@ -48,6 +49,9 @@ export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData
|
|||||||
case 'table':
|
case 'table':
|
||||||
tableFrames.push(frame);
|
tableFrames.push(frame);
|
||||||
break;
|
break;
|
||||||
|
case 'rawPrometheus':
|
||||||
|
rawPrometheusFrames.push(frame);
|
||||||
|
break;
|
||||||
case 'nodeGraph':
|
case 'nodeGraph':
|
||||||
nodeGraphFrames.push(frame);
|
nodeGraphFrames.push(frame);
|
||||||
break;
|
break;
|
||||||
@ -73,9 +77,11 @@ export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData
|
|||||||
traceFrames,
|
traceFrames,
|
||||||
nodeGraphFrames,
|
nodeGraphFrames,
|
||||||
flameGraphFrames,
|
flameGraphFrames,
|
||||||
|
rawPrometheusFrames,
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
|
rawPrometheusResult: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,7 +111,7 @@ export const decorateWithGraphResult = (data: ExplorePanelData): ExplorePanelDat
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This processing returns Observable because it uses Transformer internally which result type is also Observable.
|
* This processing returns Observable because it uses Transformer internally which result type is also Observable.
|
||||||
* In this case the transformer should return single result but it is possible that in the future it could return
|
* In this case the transformer should return single result, but it is possible that in the future it could return
|
||||||
* multiple results and so this should be used with mergeMap or similar to unbox the internal observable.
|
* multiple results and so this should be used with mergeMap or similar to unbox the internal observable.
|
||||||
*/
|
*/
|
||||||
export const decorateWithTableResult = (data: ExplorePanelData): Observable<ExplorePanelData> => {
|
export const decorateWithTableResult = (data: ExplorePanelData): Observable<ExplorePanelData> => {
|
||||||
@ -155,6 +161,56 @@ export const decorateWithTableResult = (data: ExplorePanelData): Observable<Expl
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const decorateWithRawPrometheusResult = (data: ExplorePanelData): Observable<ExplorePanelData> => {
|
||||||
|
// Prometheus has a custom frame visualization alongside the table view, but they both handle the data the same
|
||||||
|
const tableFrames = data.rawPrometheusFrames;
|
||||||
|
|
||||||
|
if (!tableFrames || tableFrames.length === 0) {
|
||||||
|
return of({ ...data, tableResult: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
tableFrames.sort((frameA: DataFrame, frameB: DataFrame) => {
|
||||||
|
const frameARefId = frameA.refId!;
|
||||||
|
const frameBRefId = frameB.refId!;
|
||||||
|
|
||||||
|
if (frameARefId > frameBRefId) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (frameARefId < frameBRefId) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasOnlyTimeseries = tableFrames.every((df) => isTimeSeries(df));
|
||||||
|
|
||||||
|
// If we have only timeseries we do join on default time column which makes more sense. If we are showing
|
||||||
|
// non timeseries or some mix of data we are not trying to join on anything and just try to merge them in
|
||||||
|
// single table, which may not make sense in most cases, but it's up to the user to query something sensible.
|
||||||
|
const transformer = hasOnlyTimeseries
|
||||||
|
? of(tableFrames).pipe(standardTransformers.joinByFieldTransformer.operator({}))
|
||||||
|
: of(tableFrames).pipe(standardTransformers.mergeTransformer.operator({}));
|
||||||
|
|
||||||
|
return transformer.pipe(
|
||||||
|
map((frames) => {
|
||||||
|
const frame = frames[0];
|
||||||
|
|
||||||
|
// set display processor
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
field.display =
|
||||||
|
field.display ??
|
||||||
|
getDisplayProcessor({
|
||||||
|
field,
|
||||||
|
theme: config.theme2,
|
||||||
|
timeZone: data.request?.timezone ?? 'browser',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...data, rawPrometheusResult: frame };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const decorateWithLogsResult =
|
export const decorateWithLogsResult =
|
||||||
(
|
(
|
||||||
options: {
|
options: {
|
||||||
@ -195,7 +251,9 @@ export function decorateData(
|
|||||||
map(decorateWithCorrelations({ queries, correlations })),
|
map(decorateWithCorrelations({ queries, correlations })),
|
||||||
map(decorateWithFrameTypeMetadata),
|
map(decorateWithFrameTypeMetadata),
|
||||||
map(decorateWithGraphResult),
|
map(decorateWithGraphResult),
|
||||||
|
map(decorateWithGraphResult),
|
||||||
map(decorateWithLogsResult({ absoluteRange, refreshInterval, queries, fullRangeLogsVolumeAvailable })),
|
map(decorateWithLogsResult({ absoluteRange, refreshInterval, queries, fullRangeLogsVolumeAvailable })),
|
||||||
|
mergeMap(decorateWithRawPrometheusResult),
|
||||||
mergeMap(decorateWithTableResult)
|
mergeMap(decorateWithTableResult)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import { DataFrame, FieldType, FormattedValue, toDataFrame } from '@grafana/data/src';
|
||||||
|
|
||||||
|
import { getRawPrometheusListItemsFromDataFrame } from './getRawPrometheusListItemsFromDataFrame';
|
||||||
|
|
||||||
|
describe('getRawPrometheusListItemsFromDataFrame', () => {
|
||||||
|
it('Parses empty dataframe', () => {
|
||||||
|
const dataFrame: DataFrame = { fields: [], length: 0 };
|
||||||
|
const result = getRawPrometheusListItemsFromDataFrame(dataFrame);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Parses mock dataframe', () => {
|
||||||
|
const display = (value: string, decimals?: number): FormattedValue => {
|
||||||
|
return { text: value };
|
||||||
|
};
|
||||||
|
const dataFrame = toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{ display, name: 'Time', type: FieldType.time, values: [3000, 4000, 5000, 6000, 7000, 8000] },
|
||||||
|
{
|
||||||
|
display,
|
||||||
|
name: '__name__',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['ALERTS', 'ALERTS', 'ALERTS', 'ALERTS_FOR_STATE', 'ALERTS_FOR_STATE', 'ALERTS_FOR_STATE'],
|
||||||
|
},
|
||||||
|
{ display, name: 'Value', type: FieldType.number, values: [1, 2, 3, 4, 5, 6] },
|
||||||
|
{ display, name: 'attribute', type: FieldType.number, values: [7, 8, 9, 10, 11, 12] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const result = getRawPrometheusListItemsFromDataFrame(dataFrame);
|
||||||
|
const differenceBetweenValueAndAttribute = 6;
|
||||||
|
result.forEach((row) => {
|
||||||
|
expect(parseInt(row.attribute, 10)).toEqual(parseInt(row.Value, 10) + differenceBetweenValueAndAttribute);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,60 @@
|
|||||||
|
import { DataFrame, formattedValueToString } from '@grafana/data/src';
|
||||||
|
|
||||||
|
import { instantQueryRawVirtualizedListData } from '../PrometheusListView/RawListContainer';
|
||||||
|
|
||||||
|
type instantQueryMetricList = { [index: string]: { [index: string]: instantQueryRawVirtualizedListData } };
|
||||||
|
|
||||||
|
export const RawPrometheusListItemEmptyValue = ' ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* transform dataFrame to instantQueryRawVirtualizedListData
|
||||||
|
* @param dataFrame
|
||||||
|
*/
|
||||||
|
export const getRawPrometheusListItemsFromDataFrame = (dataFrame: DataFrame): instantQueryRawVirtualizedListData[] => {
|
||||||
|
const metricList: instantQueryMetricList = {};
|
||||||
|
const outputList: instantQueryRawVirtualizedListData[] = [];
|
||||||
|
|
||||||
|
// Filter out time
|
||||||
|
const newFields = dataFrame.fields.filter((field) => !['Time'].includes(field.name));
|
||||||
|
|
||||||
|
// Get name from each series
|
||||||
|
let metricNames: string[] = newFields.find((field) => field.name === '__name__')?.values.toArray() ?? [];
|
||||||
|
if (!metricNames.length && newFields.length && newFields[0].values.length) {
|
||||||
|
// These results do not have series labels
|
||||||
|
// Matching the native prometheus UI which appears to only show the permutations of the first field in the query result.
|
||||||
|
metricNames = Array(newFields[0].values.length).fill('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get everything that isn't the name from each series
|
||||||
|
const metricLabels = dataFrame.fields.filter((field) => !['__name__'].includes(field.name));
|
||||||
|
|
||||||
|
metricNames.forEach(function (metric: string, i: number) {
|
||||||
|
metricList[metric] = {};
|
||||||
|
const formattedMetric: instantQueryRawVirtualizedListData = metricList[metric][i] ?? {};
|
||||||
|
|
||||||
|
for (const field of metricLabels) {
|
||||||
|
const label = field.name;
|
||||||
|
|
||||||
|
if (label !== 'Time') {
|
||||||
|
// Initialize the objects
|
||||||
|
if (typeof field?.display === 'function') {
|
||||||
|
const stringValue = formattedValueToString(field?.display(field.values.get(i)));
|
||||||
|
if (stringValue) {
|
||||||
|
formattedMetric[label] = stringValue;
|
||||||
|
} else if (label.includes('Value #')) {
|
||||||
|
formattedMetric[label] = RawPrometheusListItemEmptyValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Field display method is missing!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputList.push({
|
||||||
|
...formattedMetric,
|
||||||
|
__name__: metric,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return outputList;
|
||||||
|
};
|
@ -1,4 +1,11 @@
|
|||||||
import { DataFrame, DataQueryRequest, DataQueryResponse, FieldType, MutableDataFrame } from '@grafana/data';
|
import {
|
||||||
|
DataFrame,
|
||||||
|
DataQueryRequest,
|
||||||
|
DataQueryResponse,
|
||||||
|
FieldType,
|
||||||
|
MutableDataFrame,
|
||||||
|
PreferredVisualisationType,
|
||||||
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { parseSampleValue, transform, transformDFToTable, transformV2 } from './result_transformer';
|
import { parseSampleValue, transform, transformDFToTable, transformV2 } from './result_transformer';
|
||||||
import { PromQuery } from './types';
|
import { PromQuery } from './types';
|
||||||
@ -131,7 +138,7 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
expect(series.data[0].fields[1].name).toEqual('label1');
|
expect(series.data[0].fields[1].name).toEqual('label1');
|
||||||
expect(series.data[0].fields[2].name).toEqual('label2');
|
expect(series.data[0].fields[2].name).toEqual('label2');
|
||||||
expect(series.data[0].fields[3].name).toEqual('Value');
|
expect(series.data[0].fields[3].name).toEqual('Value');
|
||||||
expect(series.data[0].meta?.preferredVisualisationType).toEqual('table');
|
expect(series.data[0].meta?.preferredVisualisationType).toEqual('rawPrometheus');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('results with table format and multiple data frames should be transformed to 1 table dataFrame', () => {
|
it('results with table format and multiple data frames should be transformed to 1 table dataFrame', () => {
|
||||||
@ -181,7 +188,7 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
expect(series.data[0].fields[3].name).toEqual('label3');
|
expect(series.data[0].fields[3].name).toEqual('label3');
|
||||||
expect(series.data[0].fields[4].name).toEqual('label4');
|
expect(series.data[0].fields[4].name).toEqual('label4');
|
||||||
expect(series.data[0].fields[5].name).toEqual('Value');
|
expect(series.data[0].fields[5].name).toEqual('Value');
|
||||||
expect(series.data[0].meta?.preferredVisualisationType).toEqual('table');
|
expect(series.data[0].meta?.preferredVisualisationType).toEqual('rawPrometheus' as PreferredVisualisationType);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('results with table and time_series format should be correctly transformed', () => {
|
it('results with table and time_series format should be correctly transformed', () => {
|
||||||
@ -230,7 +237,7 @@ describe('Prometheus Result Transformer', () => {
|
|||||||
expect(series.data[0].fields.length).toEqual(2);
|
expect(series.data[0].fields.length).toEqual(2);
|
||||||
expect(series.data[0].meta?.preferredVisualisationType).toEqual('graph');
|
expect(series.data[0].meta?.preferredVisualisationType).toEqual('graph');
|
||||||
expect(series.data[1].fields.length).toEqual(4);
|
expect(series.data[1].fields.length).toEqual(4);
|
||||||
expect(series.data[1].meta?.preferredVisualisationType).toEqual('table');
|
expect(series.data[1].meta?.preferredVisualisationType).toEqual('rawPrometheus' as PreferredVisualisationType);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('results with heatmap format should be correctly transformed', () => {
|
it('results with heatmap format should be correctly transformed', () => {
|
||||||
|
@ -228,7 +228,8 @@ export function transformDFToTable(dfs: DataFrame[]): DataFrame[] {
|
|||||||
return {
|
return {
|
||||||
refId,
|
refId,
|
||||||
fields,
|
fields,
|
||||||
meta: { ...dfs[0].meta, preferredVisualisationType: 'table' as PreferredVisualisationType },
|
// Prometheus specific UI for instant queries
|
||||||
|
meta: { ...dfs[0].meta, preferredVisualisationType: 'rawPrometheus' as PreferredVisualisationType },
|
||||||
length: timeField.values.length,
|
length: timeField.values.length,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -263,7 +264,7 @@ export function transform(
|
|||||||
valueWithRefId: transformOptions.target.valueWithRefId,
|
valueWithRefId: transformOptions.target.valueWithRefId,
|
||||||
meta: {
|
meta: {
|
||||||
// Fix for showing of Prometheus results in Explore table
|
// Fix for showing of Prometheus results in Explore table
|
||||||
preferredVisualisationType: transformOptions.query.instant ? 'table' : 'graph',
|
preferredVisualisationType: transformOptions.query.instant ? 'rawPrometheus' : 'graph',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const prometheusResult = response.data.data;
|
const prometheusResult = response.data.data;
|
||||||
|
@ -150,6 +150,11 @@ export interface ExploreItemState {
|
|||||||
*/
|
*/
|
||||||
tableResult: DataFrame[] | null;
|
tableResult: DataFrame[] | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple UI that emulates native prometheus UI
|
||||||
|
*/
|
||||||
|
rawPrometheusResult: DataFrame | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React keys for rendering of QueryRows
|
* React keys for rendering of QueryRows
|
||||||
*/
|
*/
|
||||||
@ -177,6 +182,10 @@ export interface ExploreItemState {
|
|||||||
showLogs?: boolean;
|
showLogs?: boolean;
|
||||||
showMetrics?: boolean;
|
showMetrics?: boolean;
|
||||||
showTable?: boolean;
|
showTable?: boolean;
|
||||||
|
/**
|
||||||
|
* If true, the default "raw" prometheus instant query UI will be displayed in addition to table view
|
||||||
|
*/
|
||||||
|
showRawPrometheus?: boolean;
|
||||||
showTrace?: boolean;
|
showTrace?: boolean;
|
||||||
showNodeGraph?: boolean;
|
showNodeGraph?: boolean;
|
||||||
showFlameGraph?: boolean;
|
showFlameGraph?: boolean;
|
||||||
@ -247,8 +256,17 @@ export interface ExplorePanelData extends PanelData {
|
|||||||
logsFrames: DataFrame[];
|
logsFrames: DataFrame[];
|
||||||
traceFrames: DataFrame[];
|
traceFrames: DataFrame[];
|
||||||
nodeGraphFrames: DataFrame[];
|
nodeGraphFrames: DataFrame[];
|
||||||
|
rawPrometheusFrames: DataFrame[];
|
||||||
flameGraphFrames: DataFrame[];
|
flameGraphFrames: DataFrame[];
|
||||||
graphResult: DataFrame[] | null;
|
graphResult: DataFrame[] | null;
|
||||||
tableResult: DataFrame[] | null;
|
tableResult: DataFrame[] | null;
|
||||||
logsResult: LogsModel | null;
|
logsResult: LogsModel | null;
|
||||||
|
rawPrometheusResult: DataFrame | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum TABLE_RESULTS_STYLE {
|
||||||
|
table = 'table',
|
||||||
|
raw = 'raw',
|
||||||
|
}
|
||||||
|
export const TABLE_RESULTS_STYLES = [TABLE_RESULTS_STYLE.table, TABLE_RESULTS_STYLE.raw];
|
||||||
|
export type TableResultsStyle = typeof TABLE_RESULTS_STYLES[number];
|
||||||
|
Loading…
Reference in New Issue
Block a user