mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Data Trails: Actions redesign and overview tab (#80216)
* Use last used datasource or default datasource when starting a trail * Use tabs and move actions bar to same line when possible * Added overview tab with description, type and labels * Clickable labels in overview tab * Show label value counts next to the label name * Fix action bar zIndex * Address PR comments * Refactor * Refactor getLabelOptions to utils * Reuse language provider from state * betterer * testing some refactors * Remove unreachable code * Refactor GROUP_BY var to MetricScene * Fix url by excluding var-groupby * Fix conflicts * Use <Text/> instead of custom styles * Simplify setting overview as default tab --------- Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
@@ -4319,10 +4319,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "11"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "12"]
|
||||
],
|
||||
"public/app/features/trails/SelectMetricTrailView.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@grafana/scenes';
|
||||
import { Button } from '@grafana/ui';
|
||||
|
||||
import { getMetricSceneFor } from './utils';
|
||||
import { getMetricSceneFor } from '../utils';
|
||||
|
||||
export interface AddToFiltersGraphActionState extends SceneObjectState {
|
||||
frame: DataFrame;
|
||||
@@ -3,7 +3,6 @@ import React from 'react';
|
||||
|
||||
import { DataFrame, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
AdHocFiltersVariable,
|
||||
PanelBuilders,
|
||||
QueryVariable,
|
||||
SceneComponentProps,
|
||||
@@ -18,19 +17,20 @@ import {
|
||||
SceneObjectBase,
|
||||
SceneObjectState,
|
||||
SceneQueryRunner,
|
||||
SceneVariableSet,
|
||||
} from '@grafana/scenes';
|
||||
import { Button, Field, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
||||
|
||||
import { getAutoQueriesForMetric } from '../AutomaticMetricQueries/AutoQueryEngine';
|
||||
import { AutoQueryDef } from '../AutomaticMetricQueries/types';
|
||||
import { MetricScene } from '../MetricScene';
|
||||
import { trailDS, VAR_GROUP_BY, VAR_GROUP_BY_EXP } from '../shared';
|
||||
import { getColorByIndex } from '../utils';
|
||||
|
||||
import { AddToFiltersGraphAction } from './AddToFiltersGraphAction';
|
||||
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
|
||||
import { AutoQueryDef } from './AutomaticMetricQueries/types';
|
||||
import { ByFrameRepeater } from './ByFrameRepeater';
|
||||
import { LayoutSwitcher } from './LayoutSwitcher';
|
||||
import { MetricScene } from './MetricScene';
|
||||
import { trailDS, VAR_FILTERS, VAR_GROUP_BY, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from './shared';
|
||||
import { getColorByIndex } from './utils';
|
||||
import { getLabelOptions } from './utils';
|
||||
|
||||
export interface BreakdownSceneState extends SceneObjectState {
|
||||
body?: SceneObject;
|
||||
@@ -39,13 +39,9 @@ export interface BreakdownSceneState extends SceneObjectState {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Just a proof of concept example of a behavior
|
||||
*/
|
||||
export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
|
||||
constructor(state: Partial<BreakdownSceneState>) {
|
||||
super({
|
||||
$variables: state.$variables ?? getVariableSet(),
|
||||
labels: state.labels ?? [],
|
||||
...state,
|
||||
});
|
||||
@@ -84,7 +80,7 @@ export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
|
||||
}
|
||||
|
||||
private updateBody(variable: QueryVariable) {
|
||||
const options = this.getLabelOptions(variable);
|
||||
const options = getLabelOptions(this, variable);
|
||||
|
||||
const stateUpdate: Partial<BreakdownSceneState> = {
|
||||
loading: variable.state.loading,
|
||||
@@ -101,31 +97,11 @@ export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
|
||||
this.setState(stateUpdate);
|
||||
}
|
||||
|
||||
private getLabelOptions(variable: QueryVariable) {
|
||||
const labelFilters = sceneGraph.lookupVariable(VAR_FILTERS, this);
|
||||
const labelOptions: Array<SelectableValue<string>> = [];
|
||||
|
||||
if (!(labelFilters instanceof AdHocFiltersVariable)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filters = labelFilters.state.set.state.filters;
|
||||
|
||||
for (const option of variable.getOptionsForSelect()) {
|
||||
const filterExists = filters.find((f) => f.key === option.value);
|
||||
if (!filterExists) {
|
||||
labelOptions.push({ label: option.label, value: String(option.value) });
|
||||
}
|
||||
}
|
||||
|
||||
return labelOptions;
|
||||
}
|
||||
|
||||
public onChange = (value: string) => {
|
||||
const variable = this.getVariable();
|
||||
|
||||
if (value === ALL_VARIABLE_VALUE) {
|
||||
this.setState({ body: buildAllLayout(this.getLabelOptions(variable), this._query!) });
|
||||
this.setState({ body: buildAllLayout(getLabelOptions(this, variable), this._query!) });
|
||||
} else if (variable.hasAllValue()) {
|
||||
this.setState({ body: buildNormalLayout(this._query!) });
|
||||
}
|
||||
@@ -243,23 +219,6 @@ export function buildAllLayout(options: Array<SelectableValue<string>>, queryDef
|
||||
|
||||
const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(400px, 1fr))';
|
||||
|
||||
function getVariableSet() {
|
||||
return new SceneVariableSet({
|
||||
variables: [
|
||||
new QueryVariable({
|
||||
name: VAR_GROUP_BY,
|
||||
label: 'Group by',
|
||||
datasource: trailDS,
|
||||
includeAll: true,
|
||||
defaultToAll: true,
|
||||
query: { query: `label_names(${VAR_METRIC_EXPR})`, refId: 'A' },
|
||||
value: '',
|
||||
text: '',
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function buildNormalLayout(queryDef: AutoQueryDef) {
|
||||
return new LayoutSwitcher({
|
||||
$data: new SceneQueryRunner({
|
||||
22
public/app/features/trails/ActionTabs/LogsScene.tsx
Normal file
22
public/app/features/trails/ActionTabs/LogsScene.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { PanelBuilders, SceneFlexItem, SceneQueryRunner } from '@grafana/scenes';
|
||||
|
||||
import { SelectMetricAction } from '../SelectMetricAction';
|
||||
import { LOGS_METRIC } from '../shared';
|
||||
|
||||
export function buildLogsScene() {
|
||||
return new SceneFlexItem({
|
||||
$data: new SceneQueryRunner({
|
||||
queries: [
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: { uid: 'gdev-loki' },
|
||||
expr: '{${filters}} | logfmt',
|
||||
},
|
||||
],
|
||||
}),
|
||||
body: PanelBuilders.logs()
|
||||
.setTitle('Logs')
|
||||
.setHeaderActions(new SelectMetricAction({ metric: LOGS_METRIC, title: 'Open' }))
|
||||
.build(),
|
||||
});
|
||||
}
|
||||
133
public/app/features/trails/ActionTabs/MetricOverviewScene.tsx
Normal file
133
public/app/features/trails/ActionTabs/MetricOverviewScene.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
QueryVariable,
|
||||
SceneComponentProps,
|
||||
SceneFlexItem,
|
||||
sceneGraph,
|
||||
SceneObjectBase,
|
||||
SceneObjectState,
|
||||
VariableDependencyConfig,
|
||||
} from '@grafana/scenes';
|
||||
import { Stack, Text, TextLink } from '@grafana/ui';
|
||||
|
||||
import PrometheusLanguageProvider from '../../../plugins/datasource/prometheus/language_provider';
|
||||
import { PromMetricsMetadataItem } from '../../../plugins/datasource/prometheus/types';
|
||||
import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
import { ALL_VARIABLE_VALUE } from '../../variables/constants';
|
||||
import { VAR_DATASOURCE_EXPR, VAR_GROUP_BY } from '../shared';
|
||||
import { getMetricSceneFor } from '../utils';
|
||||
|
||||
import { getLabelOptions } from './utils';
|
||||
|
||||
export interface MetricOverviewSceneState extends SceneObjectState {
|
||||
metadata?: PromMetricsMetadataItem;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneState> {
|
||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||
variableNames: [VAR_DATASOURCE_EXPR],
|
||||
onReferencedVariableValueChanged: this.onReferencedVariableValueChanged.bind(this),
|
||||
});
|
||||
|
||||
constructor(state: Partial<MetricOverviewSceneState>) {
|
||||
super({
|
||||
...state,
|
||||
});
|
||||
|
||||
this.addActivationHandler(this._onActivate.bind(this));
|
||||
}
|
||||
|
||||
private getVariable(): QueryVariable {
|
||||
const variable = sceneGraph.lookupVariable(VAR_GROUP_BY, this)!;
|
||||
if (!(variable instanceof QueryVariable)) {
|
||||
throw new Error('Group by variable not found');
|
||||
}
|
||||
|
||||
return variable;
|
||||
}
|
||||
|
||||
private _onActivate() {
|
||||
this.updateMetadata();
|
||||
}
|
||||
|
||||
private onReferencedVariableValueChanged() {
|
||||
this.updateMetadata();
|
||||
}
|
||||
|
||||
private async updateMetadata() {
|
||||
const ds = await getDatasourceSrv().get(VAR_DATASOURCE_EXPR, { __sceneObject: { value: this } });
|
||||
|
||||
const languageProvider: PrometheusLanguageProvider = ds.languageProvider;
|
||||
|
||||
if (!languageProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metricScene = getMetricSceneFor(this);
|
||||
const metric = metricScene.state.metric;
|
||||
|
||||
if (languageProvider.metricsMetadata) {
|
||||
this.setState({ metadata: languageProvider.metricsMetadata[metric] });
|
||||
return;
|
||||
}
|
||||
|
||||
await languageProvider.start();
|
||||
|
||||
this.setState({ metadata: languageProvider.metricsMetadata?.[metric] });
|
||||
}
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<MetricOverviewScene>) => {
|
||||
const { metadata } = model.useState();
|
||||
const variable = model.getVariable();
|
||||
const { loading } = variable.useState();
|
||||
const labelOptions = getLabelOptions(model, variable).filter((l) => l.value !== ALL_VARIABLE_VALUE);
|
||||
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
{loading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Text weight={'medium'}>Description</Text>
|
||||
{metadata?.help ? <div>{metadata?.help}</div> : <i>No description available</i>}
|
||||
</Stack>
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Text weight={'medium'}>Type</Text>
|
||||
{metadata?.type ? <div>{metadata?.type}</div> : <i>Unknown</i>}
|
||||
</Stack>
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Text weight={'medium'}>Unit</Text>
|
||||
{metadata?.unit ? <div>{metadata?.unit}</div> : <i>Unknown</i>}
|
||||
</Stack>
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Text weight={'medium'}>Labels</Text>
|
||||
{labelOptions.map((l) => (
|
||||
<TextLink
|
||||
key={l.label}
|
||||
href={sceneGraph.interpolate(
|
||||
model,
|
||||
`/data-trails/trail$\{__url.params:exclude:actionView,var-groupby}&actionView=breakdown&var-groupby=${encodeURIComponent(
|
||||
l.value!
|
||||
)}`
|
||||
)}
|
||||
title="View breakdown"
|
||||
>
|
||||
{l.label!}
|
||||
</TextLink>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function buildMetricOverviewScene() {
|
||||
return new SceneFlexItem({
|
||||
body: new MetricOverviewScene({}),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SceneFlexItem } from '@grafana/scenes';
|
||||
|
||||
import { MetricSelectScene } from '../MetricSelectScene';
|
||||
|
||||
export function buildRelatedMetricsScene() {
|
||||
return new SceneFlexItem({
|
||||
body: new MetricSelectScene({}),
|
||||
});
|
||||
}
|
||||
24
public/app/features/trails/ActionTabs/utils.ts
Normal file
24
public/app/features/trails/ActionTabs/utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { AdHocFiltersVariable, QueryVariable, sceneGraph, SceneObject } from '@grafana/scenes';
|
||||
|
||||
import { VAR_FILTERS } from '../shared';
|
||||
|
||||
export function getLabelOptions(scenObject: SceneObject, variable: QueryVariable) {
|
||||
const labelFilters = sceneGraph.lookupVariable(VAR_FILTERS, scenObject);
|
||||
const labelOptions: Array<SelectableValue<string>> = [];
|
||||
|
||||
if (!(labelFilters instanceof AdHocFiltersVariable)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filters = labelFilters.state.set.state.filters;
|
||||
|
||||
for (const option of variable.getOptionsForSelect()) {
|
||||
const filterExists = filters.find((f) => f.key === option.value);
|
||||
if (!filterExists) {
|
||||
labelOptions.push({ label: option.label, value: String(option.value) });
|
||||
}
|
||||
}
|
||||
|
||||
return labelOptions;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { DataTrail } from './DataTrail';
|
||||
import { DataTrailCard } from './DataTrailCard';
|
||||
import { DataTrailsApp } from './DataTrailsApp';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { newMetricsTrail } from './utils';
|
||||
import { getDatasourceForNewTrail, newMetricsTrail } from './utils';
|
||||
|
||||
export interface DataTrailsHomeState extends SceneObjectState {}
|
||||
|
||||
@@ -21,7 +21,7 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
|
||||
|
||||
public onNewMetricsTrail = () => {
|
||||
const app = getAppFor(this);
|
||||
const trail = newMetricsTrail();
|
||||
const trail = newMetricsTrail(getDatasourceForNewTrail());
|
||||
|
||||
getTrailStore().setRecentTrail(trail);
|
||||
app.goToUrlForTrail(trail);
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DashboardCursorSync } from '@grafana/data';
|
||||
import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
SceneObjectState,
|
||||
SceneObjectBase,
|
||||
SceneComponentProps,
|
||||
SceneFlexLayout,
|
||||
SceneFlexItem,
|
||||
SceneQueryRunner,
|
||||
SceneObjectUrlSyncConfig,
|
||||
SceneObjectUrlValues,
|
||||
PanelBuilders,
|
||||
sceneGraph,
|
||||
SceneVariableSet,
|
||||
QueryVariable,
|
||||
behaviors,
|
||||
} from '@grafana/scenes';
|
||||
import { ToolbarButton, Box, Stack, Icon } from '@grafana/ui';
|
||||
import { ToolbarButton, Box, Stack, Icon, TabsBar, Tab, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { buildBreakdownActionScene } from './ActionTabs/BreakdownScene';
|
||||
import { buildLogsScene } from './ActionTabs/LogsScene';
|
||||
import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene';
|
||||
import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene';
|
||||
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
|
||||
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
|
||||
import { buildBreakdownActionScene } from './BreakdownScene';
|
||||
import { MetricSelectScene } from './MetricSelectScene';
|
||||
import { SelectMetricAction } from './SelectMetricAction';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import {
|
||||
ActionViewDefinition,
|
||||
ActionViewType,
|
||||
getVariablesWithMetricConstant,
|
||||
LOGS_METRIC,
|
||||
MakeOptional,
|
||||
OpenEmbeddedTrailEvent,
|
||||
trailDS,
|
||||
VAR_GROUP_BY,
|
||||
VAR_METRIC_EXPR,
|
||||
} from './shared';
|
||||
import { getTrailFor } from './utils';
|
||||
|
||||
@@ -42,7 +47,7 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
|
||||
|
||||
public constructor(state: MakeOptional<MetricSceneState, 'body'>) {
|
||||
super({
|
||||
$variables: state.$variables ?? getVariablesWithMetricConstant(state.metric),
|
||||
$variables: state.$variables ?? getVariableSet(state.metric),
|
||||
body: state.body ?? buildGraphScene(state.metric),
|
||||
...state,
|
||||
});
|
||||
@@ -57,7 +62,7 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
|
||||
if (this.state.actionView !== values.actionView) {
|
||||
const actionViewDef = actionViewsDefinitions.find((v) => v.value === values.actionView);
|
||||
if (actionViewDef) {
|
||||
this.setActionView(actionViewDef);
|
||||
this.setActionView(actionViewDef.value);
|
||||
}
|
||||
}
|
||||
} else if (values.actionView === null) {
|
||||
@@ -65,8 +70,9 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
|
||||
}
|
||||
}
|
||||
|
||||
public setActionView(actionViewDef?: ActionViewDefinition) {
|
||||
public setActionView(actionView?: ActionViewType) {
|
||||
const { body } = this.state;
|
||||
const actionViewDef = actionViewsDefinitions.find((v) => v.value === actionView);
|
||||
|
||||
if (actionViewDef && actionViewDef.value !== this.state.actionView) {
|
||||
// reduce max height for main panel to reduce height flicker
|
||||
@@ -88,6 +94,7 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
|
||||
}
|
||||
|
||||
const actionViewsDefinitions: ActionViewDefinition[] = [
|
||||
{ displayName: 'Overview', value: 'overview', getScene: buildMetricOverviewScene },
|
||||
{ displayName: 'Breakdown', value: 'breakdown', getScene: buildBreakdownActionScene },
|
||||
{ displayName: 'Logs', value: 'logs', getScene: buildLogsScene },
|
||||
{ displayName: 'Related metrics', value: 'related', getScene: buildRelatedMetricsScene },
|
||||
@@ -96,16 +103,13 @@ const actionViewsDefinitions: ActionViewDefinition[] = [
|
||||
export interface MetricActionBarState extends SceneObjectState {}
|
||||
|
||||
export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
|
||||
public getButtonVariant(actionViewName: string, currentView: string | undefined) {
|
||||
return currentView === actionViewName ? 'active' : 'canvas';
|
||||
}
|
||||
|
||||
public onOpenTrail = () => {
|
||||
this.publishEvent(new OpenEmbeddedTrailEvent(), true);
|
||||
};
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<MetricActionBar>) => {
|
||||
const metricScene = sceneGraph.getAncestor(model, MetricScene);
|
||||
const styles = useStyles2(getStyles);
|
||||
const trail = getTrailFor(model);
|
||||
const [isBookmarked, setBookmarked] = useState(false);
|
||||
const { actionView } = metricScene.useState();
|
||||
@@ -115,49 +119,92 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
|
||||
setBookmarked(!isBookmarked);
|
||||
};
|
||||
|
||||
if (!actionView) {
|
||||
metricScene.setActionView('overview');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box paddingY={1}>
|
||||
<Stack gap={2}>
|
||||
{actionViewsDefinitions.map((viewDef) => (
|
||||
<div className={styles.actions}>
|
||||
<Stack gap={2}>
|
||||
<ToolbarButton variant={'canvas'} icon="compass" tooltip="Open in explore (todo)" disabled>
|
||||
Explore
|
||||
</ToolbarButton>
|
||||
<ToolbarButton variant={'canvas'}>Add to dashboard</ToolbarButton>
|
||||
<ToolbarButton variant={'canvas'} icon="share-alt" tooltip="Copy url (todo)" disabled />
|
||||
<ToolbarButton
|
||||
key={viewDef.value}
|
||||
variant={viewDef.value === actionView ? 'active' : 'canvas'}
|
||||
onClick={() => metricScene.setActionView(viewDef)}
|
||||
>
|
||||
{viewDef.displayName}
|
||||
</ToolbarButton>
|
||||
))}
|
||||
<ToolbarButton variant={'canvas'}>Add to dashboard</ToolbarButton>
|
||||
<ToolbarButton variant={'canvas'} icon="compass" tooltip="Open in explore (todo)" disabled />
|
||||
<ToolbarButton
|
||||
variant={'canvas'}
|
||||
icon={
|
||||
isBookmarked ? (
|
||||
<Icon name={'favorite'} type={'mono'} size={'lg'} />
|
||||
) : (
|
||||
<Icon name={'star'} type={'default'} size={'lg'} />
|
||||
)
|
||||
}
|
||||
tooltip={'Bookmark'}
|
||||
onClick={onBookmarkTrail}
|
||||
/>
|
||||
<ToolbarButton variant={'canvas'} icon="share-alt" tooltip="Copy url (todo)" disabled />
|
||||
{trail.state.embedded && (
|
||||
<ToolbarButton variant={'canvas'} onClick={model.onOpenTrail}>
|
||||
Open
|
||||
</ToolbarButton>
|
||||
)}
|
||||
</Stack>
|
||||
variant={'canvas'}
|
||||
icon={
|
||||
isBookmarked ? (
|
||||
<Icon name={'favorite'} type={'mono'} size={'lg'} />
|
||||
) : (
|
||||
<Icon name={'star'} type={'default'} size={'lg'} />
|
||||
)
|
||||
}
|
||||
tooltip={'Bookmark'}
|
||||
onClick={onBookmarkTrail}
|
||||
/>
|
||||
{trail.state.embedded && (
|
||||
<ToolbarButton variant={'canvas'} onClick={model.onOpenTrail}>
|
||||
Open
|
||||
</ToolbarButton>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<TabsBar>
|
||||
{actionViewsDefinitions.map((tab, index) => {
|
||||
return (
|
||||
<Tab
|
||||
key={index}
|
||||
label={tab.displayName}
|
||||
active={actionView === tab.value}
|
||||
onChangeTab={() => metricScene.setActionView(tab.value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabsBar>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
actions: css({
|
||||
[theme.breakpoints.up(theme.breakpoints.values.md)]: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
zIndex: 2,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function getVariableSet(metric: string) {
|
||||
return new SceneVariableSet({
|
||||
variables: [
|
||||
...getVariablesWithMetricConstant(metric),
|
||||
new QueryVariable({
|
||||
name: VAR_GROUP_BY,
|
||||
label: 'Group by',
|
||||
datasource: trailDS,
|
||||
includeAll: true,
|
||||
defaultToAll: true,
|
||||
query: { query: `label_names(${VAR_METRIC_EXPR})`, refId: 'A' },
|
||||
value: '',
|
||||
text: '',
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const MAIN_PANEL_MIN_HEIGHT = 280;
|
||||
const MAIN_PANEL_MAX_HEIGHT = '40%';
|
||||
|
||||
function buildGraphScene(metric: string) {
|
||||
const autoQuery = getAutoQueriesForMetric(metric);
|
||||
const bodyAutoVizPanel = new AutoVizPanel({ autoQuery });
|
||||
|
||||
return new SceneFlexLayout({
|
||||
direction: 'column',
|
||||
@@ -166,7 +213,7 @@ function buildGraphScene(metric: string) {
|
||||
new SceneFlexItem({
|
||||
minHeight: MAIN_PANEL_MIN_HEIGHT,
|
||||
maxHeight: MAIN_PANEL_MAX_HEIGHT,
|
||||
body: new AutoVizPanel({ autoQuery }),
|
||||
body: bodyAutoVizPanel,
|
||||
}),
|
||||
new SceneFlexItem({
|
||||
ySizing: 'content',
|
||||
@@ -175,27 +222,3 @@ function buildGraphScene(metric: string) {
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function buildLogsScene() {
|
||||
return new SceneFlexItem({
|
||||
$data: new SceneQueryRunner({
|
||||
queries: [
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: { uid: 'gdev-loki' },
|
||||
expr: '{${filters}} | logfmt',
|
||||
},
|
||||
],
|
||||
}),
|
||||
body: PanelBuilders.logs()
|
||||
.setTitle('Logs')
|
||||
.setHeaderActions(new SelectMetricAction({ metric: LOGS_METRIC, title: 'Open' }))
|
||||
.build(),
|
||||
});
|
||||
}
|
||||
|
||||
function buildRelatedMetricsScene() {
|
||||
return new SceneFlexItem({
|
||||
body: new MetricSelectScene({}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,7 +181,9 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
children.push(panel);
|
||||
} else {
|
||||
const panel = new SceneCSSGridItem({
|
||||
$variables: getVariablesWithMetricConstant(metric.name),
|
||||
$variables: new SceneVariableSet({
|
||||
variables: getVariablesWithMetricConstant(metric.name),
|
||||
}),
|
||||
body: getCardPanelFor(metric.name),
|
||||
});
|
||||
metric.itemRef = panel.getRef();
|
||||
@@ -263,7 +265,9 @@ function getPreviewPanelFor(metric: string, index: number) {
|
||||
.build();
|
||||
|
||||
return new SceneCSSGridItem({
|
||||
$variables: getVariablesWithMetricConstant(metric),
|
||||
$variables: new SceneVariableSet({
|
||||
variables: getVariablesWithMetricConstant(metric),
|
||||
}),
|
||||
$behaviors: [hideEmptyPreviews(metric)],
|
||||
$data: new SceneQueryRunner({
|
||||
datasource: trailDS,
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
SceneObjectState,
|
||||
SceneObjectBase,
|
||||
VariableDependencyConfig,
|
||||
sceneGraph,
|
||||
SceneComponentProps,
|
||||
SceneVariableSet,
|
||||
SceneVariable,
|
||||
QueryVariable,
|
||||
VariableValueOption,
|
||||
} from '@grafana/scenes';
|
||||
import { VariableHide } from '@grafana/schema';
|
||||
import { Input, Card, Stack } from '@grafana/ui';
|
||||
|
||||
import { trailDS } from './shared';
|
||||
|
||||
export interface SelectMetricTrailViewState extends SceneObjectState {
|
||||
metricNames: VariableValueOption[];
|
||||
}
|
||||
|
||||
export class SelectMetricTrailView extends SceneObjectBase<SelectMetricTrailViewState> {
|
||||
public constructor(state: Partial<SelectMetricTrailViewState>) {
|
||||
super({
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [
|
||||
new QueryVariable({
|
||||
name: 'metricNames',
|
||||
datasource: trailDS,
|
||||
hide: VariableHide.hideVariable,
|
||||
includeAll: true,
|
||||
defaultToAll: true,
|
||||
skipUrlSync: true,
|
||||
query: { query: 'label_values({$filters},__name__)', refId: 'A' },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
metricNames: [],
|
||||
...state,
|
||||
});
|
||||
}
|
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||
variableNames: ['filters', 'metricNames'],
|
||||
onVariableUpdatesCompleted: this._onVariableChanged.bind(this),
|
||||
});
|
||||
|
||||
private _onVariableChanged(changedVariables: Set<SceneVariable>, dependencyChanged: boolean): void {
|
||||
for (const variable of changedVariables) {
|
||||
if (variable.state.name === 'filters') {
|
||||
const variable = sceneGraph.lookupVariable('filters', this)!;
|
||||
// Temp hack
|
||||
(this.state.$variables as any)._handleVariableValueChanged(variable);
|
||||
}
|
||||
|
||||
if (variable.state.name === 'metricNames' && variable instanceof QueryVariable) {
|
||||
this.setState({ metricNames: variable.state.options });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<SelectMetricTrailView>) => {
|
||||
const { metricNames } = model.useState();
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={0}>
|
||||
<Stack direction="column" gap={2}>
|
||||
<Input placeholder="Search metrics" />
|
||||
<div></div>
|
||||
</Stack>
|
||||
{metricNames.map((option, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
href={sceneGraph.interpolate(model, `\${__url.path}\${__url.params}&metric=${option.value}`)}
|
||||
>
|
||||
<Card.Heading>{String(option.value)}</Card.Heading>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BusEventBase, BusEventWithPayload } from '@grafana/data';
|
||||
import { ConstantVariable, SceneObject, SceneVariableSet } from '@grafana/scenes';
|
||||
import { ConstantVariable, SceneObject } from '@grafana/scenes';
|
||||
import { VariableHide } from '@grafana/schema';
|
||||
|
||||
export type ActionViewType = 'overview' | 'breakdown' | 'logs' | 'related';
|
||||
export interface ActionViewDefinition {
|
||||
displayName: string;
|
||||
value: string;
|
||||
value: ActionViewType;
|
||||
getScene: () => SceneObject;
|
||||
}
|
||||
|
||||
@@ -32,15 +33,13 @@ export const BOOKMARKED_TRAILS_KEY = 'grafana.trails.bookmarks';
|
||||
export type MakeOptional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
export function getVariablesWithMetricConstant(metric: string) {
|
||||
return new SceneVariableSet({
|
||||
variables: [
|
||||
new ConstantVariable({
|
||||
name: VAR_METRIC,
|
||||
value: metric,
|
||||
hide: VariableHide.hideVariable,
|
||||
}),
|
||||
],
|
||||
});
|
||||
return [
|
||||
new ConstantVariable({
|
||||
name: VAR_METRIC,
|
||||
value: metric,
|
||||
hide: VariableHide.hideVariable,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export class MetricSelectedEvent extends BusEventWithPayload<string> {
|
||||
|
||||
@@ -2,10 +2,13 @@ import { urlUtil } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { getUrlSyncManager, sceneGraph, SceneObject, SceneObjectUrlValues, SceneTimeRange } from '@grafana/scenes';
|
||||
|
||||
import { getDatasourceSrv } from '../plugins/datasource_srv';
|
||||
|
||||
import { DataTrail } from './DataTrail';
|
||||
import { DataTrailSettings } from './DataTrailSettings';
|
||||
import { MetricScene } from './MetricScene';
|
||||
import { TRAILS_ROUTE } from './shared';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { TRAILS_ROUTE, VAR_DATASOURCE_EXPR } from './shared';
|
||||
|
||||
export function getTrailFor(model: SceneObject): DataTrail {
|
||||
return sceneGraph.getAncestor(model, DataTrail);
|
||||
@@ -15,9 +18,9 @@ export function getTrailSettings(model: SceneObject): DataTrailSettings {
|
||||
return sceneGraph.getAncestor(model, DataTrail).state.settings;
|
||||
}
|
||||
|
||||
export function newMetricsTrail(): DataTrail {
|
||||
export function newMetricsTrail(initialDS?: string): DataTrail {
|
||||
return new DataTrail({
|
||||
//initialDS: 'gdev-prometheus',
|
||||
initialDS,
|
||||
$timeRange: new SceneTimeRange({ from: 'now-1h', to: 'now' }),
|
||||
//initialFilters: [{ key: 'job', operator: '=', value: 'grafana' }],
|
||||
embedded: false,
|
||||
@@ -47,6 +50,21 @@ export function getMetricSceneFor(model: SceneObject): MetricScene {
|
||||
throw new Error('Unable to find trail');
|
||||
}
|
||||
|
||||
export function getDatasourceForNewTrail(): string | undefined {
|
||||
const prevTrail = getTrailStore().recent[0];
|
||||
if (prevTrail) {
|
||||
const prevDataSource = sceneGraph.interpolate(prevTrail.resolve(), VAR_DATASOURCE_EXPR);
|
||||
if (typeof prevDataSource === 'string' && prevDataSource.length > 0) {
|
||||
return prevDataSource;
|
||||
}
|
||||
}
|
||||
const promDatasources = getDatasourceSrv().getList({ type: 'prometheus' });
|
||||
if (promDatasources.length > 0) {
|
||||
return promDatasources.find((mds) => mds.uid === config.defaultDatasource)?.uid ?? promDatasources[0].uid;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getColorByIndex(index: number) {
|
||||
const visTheme = config.theme2.visualization;
|
||||
return visTheme.getColorByName(visTheme.palette[index % 8]);
|
||||
|
||||
Reference in New Issue
Block a user