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:
Andre Pereira
2024-01-16 18:22:53 +00:00
committed by GitHub
parent 14c82c2725
commit d27e2fec88
15 changed files with 330 additions and 226 deletions

View File

@@ -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"]
],

View File

@@ -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;

View File

@@ -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({

View 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(),
});
}

View 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({}),
});
}

View File

@@ -0,0 +1,9 @@
import { SceneFlexItem } from '@grafana/scenes';
import { MetricSelectScene } from '../MetricSelectScene';
export function buildRelatedMetricsScene() {
return new SceneFlexItem({
body: new MetricSelectScene({}),
});
}

View 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;
}

View File

@@ -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);

View File

@@ -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({}),
});
}

View File

@@ -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,

View File

@@ -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>
);
};
}

View File

@@ -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> {

View File

@@ -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]);