Skip to content

Commit

Permalink
Panel Menus (#892)
Browse files Browse the repository at this point in the history
feat: Add VizPanelMenu to Explore Logs panels
  • Loading branch information
gtk-grafana authored Nov 26, 2024
1 parent b1d0c26 commit 36186cd
Show file tree
Hide file tree
Showing 14 changed files with 303 additions and 52 deletions.
1 change: 0 additions & 1 deletion config/loki-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ frontend:
pattern_ingester:
enabled: true
metric_aggregation:
enabled: true
loki_address: 127.0.0.1:3100
limits_config:
max_global_streams_per_user: 0
Expand Down
1 change: 0 additions & 1 deletion docker-compose.local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ services:
extra_hosts:
- 'host.docker.internal:host-gateway'
loki:
# image: grafana/loki:main-12cd12a
image: grafana/loki:3.3.0
environment:
LOG_CLUSTER_DEPTH: '8'
Expand Down
192 changes: 192 additions & 0 deletions src/Components/Panels/PanelMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { DataFrame, GrafanaTheme2, PanelMenuItem } from '@grafana/data';
import {
SceneComponentProps,
SceneCSSGridItem,
sceneGraph,
SceneObject,
SceneObjectBase,
SceneObjectState,
SceneQueryRunner,
VizPanelMenu,
} from '@grafana/scenes';
import React from 'react';
import { css } from '@emotion/css';
import { onExploreLinkClick } from '../ServiceScene/GoToExploreButton';
import { IndexScene } from '../IndexScene/IndexScene';
import { getQueryRunnerFromChildren } from '../../services/scenes';
import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from '../../services/analytics';
import { logger } from '../../services/logger';
import { AddToExplorationButton } from '../ServiceScene/Breakdowns/AddToExplorationButton';
import { getPluginLinkExtensions } from '@grafana/runtime';
import { ExtensionPoints } from '../../services/extensions/links';

const ADD_TO_INVESTIGATION_MENU_TEXT = 'Add to investigation';
const ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT = 'Add to investigation div';

interface PanelMenuState extends SceneObjectState {
body?: VizPanelMenu;
frame?: DataFrame;
labelName?: string;
fieldName?: string;
addToExplorations?: AddToExplorationButton;
}

/**
* @todo the VizPanelMenu interface is overly restrictive, doesn't allow any member functions on this class, so everything is currently inlined
*/
export class PanelMenu extends SceneObjectBase<PanelMenuState> implements VizPanelMenu, SceneObject {
constructor(state: Partial<PanelMenuState>) {
super(state);
this.addActivationHandler(() => {
this.setState({
addToExplorations: new AddToExplorationButton({
labelName: this.state.labelName,
fieldName: this.state.fieldName,
frame: this.state.frame,
}),
});

// @todo rewrite the AddToExplorationButton
// Manually activate scene
this.state.addToExplorations?.activate();

const items: PanelMenuItem[] = [
{
text: 'Explore',
iconClassName: 'compass',
href: getExploreLink(this),
onClick: () => onExploreLinkClickTracking(),
},
];

this.setState({
body: new VizPanelMenu({
items,
}),
});

this.state.addToExplorations?.subscribeToState(() => {
subscribeToAddToExploration(this);
});
});
}

addItem(item: PanelMenuItem): void {
if (this.state.body) {
this.state.body.addItem(item);
}
}
setItems(items: PanelMenuItem[]): void {
if (this.state.body) {
this.state.body.setItems(items);
}
}

public static Component = ({ model }: SceneComponentProps<PanelMenu>) => {
const { body } = model.useState();

if (body) {
return <body.Component model={body} />;
}

return <></>;
};
}

const getExploreLink = (sceneRef: SceneObject) => {
const indexScene = sceneGraph.getAncestor(sceneRef, IndexScene);
const $data = sceneGraph.getData(sceneRef);
let queryRunner = getQueryRunnerFromChildren($data)[0];

// If we don't have a query runner, then our panel is within a SceneCSSGridItem, we need to get the query runner from there
if (!queryRunner) {
const sceneGridItem = sceneGraph.getAncestor(sceneRef, SceneCSSGridItem);
const queryProvider = sceneGraph.getData(sceneGridItem);

if (queryProvider instanceof SceneQueryRunner) {
queryRunner = queryProvider;
} else {
logger.error(new Error('query provider not found!'));
}
}
const uninterpolatedExpr: string | undefined = queryRunner.state.queries[0].expr;
const expr = sceneGraph.interpolate(sceneRef, uninterpolatedExpr);

return onExploreLinkClick(indexScene, expr);
};

const onExploreLinkClickTracking = () => {
reportAppInteraction(USER_EVENTS_PAGES.all, USER_EVENTS_ACTIONS.all.open_in_explore_menu_clicked);
};

const getInvestigationLink = (addToExplorations: AddToExplorationButton) => {
const links = getPluginLinkExtensions({
extensionPointId: ExtensionPoints.MetricExploration,
context: addToExplorations.state.context,
});

return links.extensions[0];
};

const onAddToInvestigationClick = (event: React.MouseEvent, addToExplorations: AddToExplorationButton) => {
const link = getInvestigationLink(addToExplorations);
if (link && link.onClick) {
link.onClick(event);
}
};

function subscribeToAddToExploration(exploreLogsVizPanelMenu: PanelMenu) {
const addToExplorationButton = exploreLogsVizPanelMenu.state.addToExplorations;
if (addToExplorationButton) {
const link = getInvestigationLink(addToExplorationButton);

const existingMenuItems = exploreLogsVizPanelMenu.state.body?.state.items ?? [];

const existingAddToExplorationLink = existingMenuItems.find((item) => item.text === ADD_TO_INVESTIGATION_MENU_TEXT);

if (link) {
if (!existingAddToExplorationLink) {
exploreLogsVizPanelMenu.state.body?.addItem({
text: ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT,
type: 'divider',
});
exploreLogsVizPanelMenu.state.body?.addItem({
text: ADD_TO_INVESTIGATION_MENU_TEXT,
iconClassName: 'plus-square',
onClick: (e) => onAddToInvestigationClick(e, addToExplorationButton),
});
} else {
if (existingAddToExplorationLink) {
exploreLogsVizPanelMenu.state.body?.setItems(
existingMenuItems.filter(
(item) =>
item.text !== ADD_TO_INVESTIGATION_MENU_TEXT && item.text !== ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT
)
);
}
}
}
}
}

export const getPanelWrapperStyles = (theme: GrafanaTheme2) => {
return {
panelWrapper: css({
width: '100%',
height: '100%',
label: 'panel-wrapper',
display: 'flex',

// @todo remove this wrapper and styles when core changes are introduced in ???
// Need more specificity to override core style
'button.show-on-hover': {
opacity: 1,
visibility: 'visible',
background: 'none',
'&:hover': {
background: theme.colors.secondary.shade,
},
},
}),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import LokiLogo from '../../../img/logo.svg';

export interface AddToExplorationButtonState extends SceneObjectState {
frame?: DataFrame;
ds?: DataSourceWithBackend<DataQuery, DataSourceJsonData>;
labelName?: string;
fieldName?: string;

ds?: DataSourceWithBackend<DataQuery, DataSourceJsonData>;
context?: ExtensionContext;

queries: DataQuery[];
Expand Down Expand Up @@ -45,9 +46,13 @@ export class AddToExplorationButton extends SceneObjectBase<AddToExplorationButt
});

this._subs.add(
this.subscribeToState(() => {
this.getQueries();
this.getContext();
this.subscribeToState((newState, prevState) => {
if (!this.state.queries.length) {
this.getQueries();
}
if (!this.state.context && this.state.queries.length) {
this.getContext();
}
})
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { DataQueryError, LoadingState } from '@grafana/data';
import { LayoutSwitcher } from './LayoutSwitcher';
import { getQueryRunner } from '../../../services/panel';
import { ByFrameRepeater } from './ByFrameRepeater';
import { Alert, DrawStyle, LoadingPlaceholder } from '@grafana/ui';
import { Alert, DrawStyle, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { buildFieldsQueryString, getFilterBreakdownValueScene, getParserForField } from '../../../services/fields';
import { getLabelValue } from './SortByScene';
import { VAR_FIELDS, VAR_METADATA } from '../../../services/variables';
Expand All @@ -31,6 +31,7 @@ import { getDetectedFieldsFrame, ServiceScene } from '../ServiceScene';
import { DEFAULT_SORT_BY } from '../../../services/sorting';
import { getFieldGroupByVariable, getFieldsVariable } from '../../../services/variableGetters';
import { LokiQuery } from '../../../services/lokiQuery';
import { PanelMenu, getPanelWrapperStyles } from '../../Panels/PanelMenu';

export interface FieldValuesBreakdownSceneState extends SceneObjectState {
body?: (LayoutSwitcher & SceneObject) | (SceneReactObject & SceneObject);
Expand All @@ -55,8 +56,9 @@ export class FieldValuesBreakdownScene extends SceneObjectBase<FieldValuesBreakd

public static Component = ({ model }: SceneComponentProps<FieldValuesBreakdownScene>) => {
const { body } = model.useState();
const styles = useStyles2(getPanelWrapperStyles);
if (body) {
return <>{body && <body.Component model={body} />}</>;
return <span className={styles.panelWrapper}>{body && <body.Component model={body} />}</span>;
}

return <LoadingPlaceholder text={'Loading...'} />;
Expand Down Expand Up @@ -180,7 +182,7 @@ export class FieldValuesBreakdownScene extends SceneObjectBase<FieldValuesBreakd
children: [
new SceneFlexItem({
minHeight: 300,
body: PanelBuilders.timeseries().setTitle(optionValue).build(),
body: PanelBuilders.timeseries().setTitle(optionValue).setMenu(new PanelMenu({})).build(),
}),
],
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { ALL_VARIABLE_VALUE, DetectedFieldType, ParserType } from '../../../services/variables';
import { buildDataQuery } from '../../../services/query';
import { getQueryRunner, setLevelColorOverrides } from '../../../services/panel';
import { DrawStyle, LoadingPlaceholder, StackingMode } from '@grafana/ui';
import { DrawStyle, LoadingPlaceholder, StackingMode, useStyles2 } from '@grafana/ui';
import { LayoutSwitcher } from './LayoutSwitcher';
import { FIELDS_BREAKDOWN_GRID_TEMPLATE_COLUMNS, FieldsBreakdownScene } from './FieldsBreakdownScene';
import {
Expand All @@ -40,7 +40,7 @@ import {
getFieldsVariable,
getValueFromFieldsFilter,
} from '../../../services/variableGetters';
import { AddToExplorationButton } from './AddToExplorationButton';
import { PanelMenu, getPanelWrapperStyles } from '../../Panels/PanelMenu';
import { logger } from '../../../services/logger';

export interface FieldsAggregatedBreakdownSceneState extends SceneObjectState {
Expand Down Expand Up @@ -266,7 +266,10 @@ export class FieldsAggregatedBreakdownScene extends SceneObjectBase<FieldsAggreg

const fieldType = getDetectedFieldType(optionValue, detectedFieldsFrame);
const dataTransformer = this.getDataTransformerForPanel(optionValue, detectedFieldsFrame, fieldType);
let body = PanelBuilders.timeseries().setTitle(optionValue).setData(dataTransformer);
let body = PanelBuilders.timeseries()
.setTitle(optionValue)
.setData(dataTransformer)
.setMenu(new PanelMenu({ labelName: optionValue }));

const headerActions = [];
if (!isAvgField(fieldType)) {
Expand All @@ -287,7 +290,7 @@ export class FieldsAggregatedBreakdownScene extends SceneObjectBase<FieldsAggreg
})
);
}
body.setHeaderActions([...headerActions, new AddToExplorationButton({ labelName: optionValue })]);
body.setHeaderActions(headerActions);

const viz = body.build();
const gridItem = new SceneCSSGridItem({
Expand Down Expand Up @@ -336,8 +339,9 @@ export class FieldsAggregatedBreakdownScene extends SceneObjectBase<FieldsAggreg

public static Component = ({ model }: SceneComponentProps<FieldsAggregatedBreakdownScene>) => {
const { body } = model.useState();
const styles = useStyles2(getPanelWrapperStyles);
if (body) {
return <>{body && <body.Component model={body} />}</>;
return <span className={styles.panelWrapper}>{body && <body.Component model={body} />}</span>;
}

return <LoadingPlaceholder text={'Loading...'} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '@grafana/scenes';
import { LayoutSwitcher } from './LayoutSwitcher';
import { getLabelValue } from './SortByScene';
import { Alert, DrawStyle, LoadingPlaceholder, StackingMode } from '@grafana/ui';
import { Alert, DrawStyle, LoadingPlaceholder, StackingMode, useStyles2 } from '@grafana/ui';
import { getQueryRunner, setLevelColorOverrides } from '../../../services/panel';
import { getSortByPreference } from '../../../services/store';
import { AppEvents, DataQueryError, LoadingState } from '@grafana/data';
Expand All @@ -31,6 +31,7 @@ import { DEFAULT_SORT_BY } from '../../../services/sorting';
import { buildLabelsQuery, LABEL_BREAKDOWN_GRID_TEMPLATE_COLUMNS } from '../../../services/labels';
import { getAppEvents } from '@grafana/runtime';
import { getLabelGroupByVariable } from '../../../services/variableGetters';
import { PanelMenu, getPanelWrapperStyles } from '../../Panels/PanelMenu';

type DisplayError = DataQueryError & { displayed: boolean };
type DisplayErrors = Record<string, DisplayError>;
Expand Down Expand Up @@ -184,6 +185,7 @@ export class LabelValuesBreakdownScene extends SceneObjectBase<LabelValueBreakdo
.setCustomFieldConfig('pointSize', 0)
.setCustomFieldConfig('drawStyle', DrawStyle.Bars)
.setOverrides(setLevelColorOverrides)
.setMenu(new PanelMenu({}))
.setTitle(tagKey);

const body = bodyOpts.build();
Expand Down Expand Up @@ -315,8 +317,9 @@ export class LabelValuesBreakdownScene extends SceneObjectBase<LabelValueBreakdo

public static Component = ({ model }: SceneComponentProps<LabelValuesBreakdownScene>) => {
const { body } = model.useState();
const styles = useStyles2(getPanelWrapperStyles);
if (body) {
return <>{body && <body.Component model={body} />}</>;
return <span className={styles.panelWrapper}>{body && <body.Component model={body} />}</span>;
}

return <LoadingPlaceholder text={'Loading...'} />;
Expand Down
Loading

0 comments on commit 36186cd

Please sign in to comment.