Datatrails: Metric as breadcrumb and style improvements (#81661)

* Fixed duplicate trails entry for first time users. Show header with description for first time users

* Renamed trail to history in History component

* Use metric name in breadcrumb

* Style tweaks around search field in metric select

* Address PR comments

* prettier
This commit is contained in:
Andre Pereira
2024-02-01 13:01:24 +00:00
committed by GitHub
parent 874ce14f0c
commit d0ecf863df
8 changed files with 67 additions and 48 deletions
+7 -4
View File
@@ -27,8 +27,9 @@ import { DataTrailSettings } from './DataTrailSettings';
import { DataTrailHistory, DataTrailHistoryStep } from './DataTrailsHistory';
import { MetricScene } from './MetricScene';
import { MetricSelectScene } from './MetricSelectScene';
import { MetricsHeader } from './MetricsHeader';
import { getTrailStore } from './TrailStore/TrailStore';
import { MetricSelectedEvent, trailDS, LOGS_METRIC, VAR_DATASOURCE, VAR_FILTERS } from './shared';
import { LOGS_METRIC, MetricSelectedEvent, trailDS, VAR_DATASOURCE, VAR_FILTERS } from './shared';
import { getUrlForTrail } from './utils';
export interface DataTrailState extends SceneObjectState {
@@ -162,7 +163,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
}
} else if (values.metric === null) {
stateUpdate.metric = undefined;
stateUpdate.topScene = new MetricSelectScene({ showHeading: true });
stateUpdate.topScene = new MetricSelectScene({});
}
this.setState(stateUpdate);
@@ -171,9 +172,11 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
static Component = ({ model }: SceneComponentProps<DataTrail>) => {
const { controls, topScene, history } = model.useState();
const styles = useStyles2(getStyles);
const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2;
return (
<div className={styles.container}>
{showHeaderForFirstTimeUsers && <MetricsHeader />}
<history.Component model={history} />
{controls && (
<div className={styles.controls}>
@@ -192,7 +195,7 @@ function getTopSceneFor(metric?: string) {
if (metric) {
return new MetricScene({ metric: metric });
} else {
return new MetricSelectScene({ showHeading: true });
return new MetricSelectScene({});
}
}
@@ -221,7 +224,7 @@ function getStyles(theme: GrafanaTheme2) {
container: css({
flexGrow: 1,
display: 'flex',
gap: theme.spacing(2),
gap: theme.spacing(1),
minHeight: '100%',
flexDirection: 'column',
}),
+3 -15
View File
@@ -3,11 +3,11 @@ import React from 'react';
import { dateTimeFormat, GrafanaTheme2 } from '@grafana/data';
import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes';
import { useStyles2, Stack, Card, IconButton, Badge } from '@grafana/ui';
import { Badge, Card, IconButton, Stack, useStyles2 } from '@grafana/ui';
import { DataTrail } from './DataTrail';
import { LOGS_METRIC, VAR_FILTERS } from './shared';
import { getDataSource, getDataSourceName } from './utils';
import { VAR_FILTERS } from './shared';
import { getDataSource, getDataSourceName, getMetricName } from './utils';
export interface Props {
trail: DataTrail;
@@ -63,18 +63,6 @@ export function DataTrailCard({ trail, onSelect, onDelete }: Props) {
);
}
function getMetricName(metric?: string) {
if (!metric) {
return 'Select metric';
}
if (metric === LOGS_METRIC) {
return 'Logs';
}
return metric;
}
function getStyles(theme: GrafanaTheme2) {
return {
tag: css({
+6 -2
View File
@@ -11,7 +11,7 @@ import { Page } from 'app/core/components/Page/Page';
import { DataTrail } from './DataTrail';
import { DataTrailsHome } from './DataTrailsHome';
import { getTrailStore } from './TrailStore/TrailStore';
import { getUrlForTrail, newMetricsTrail } from './utils';
import { getMetricName, getUrlForTrail, newMetricsTrail } from './utils';
export interface DataTrailsAppState extends SceneObjectState {
trail: DataTrail;
@@ -49,7 +49,11 @@ export class DataTrailsApp extends SceneObjectBase<DataTrailsAppState> {
exact={true}
path="/explore/metrics/trail"
render={() => (
<Page navId="explore/metrics" pageNav={{ text: 'Trail' }} layout={PageLayoutType.Custom}>
<Page
navId="explore/metrics"
pageNav={{ text: getMetricName(trail.state.metric) }}
layout={PageLayoutType.Custom}
>
<div className={styles.customPage}>
<DataTrailView trail={trail} />
</div>
@@ -139,7 +139,7 @@ export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
return (
<div className={styles.container}>
<div className={styles.heading}>Trail</div>
<div className={styles.heading}>History</div>
{steps.map((step, index) => (
<Tooltip content={() => model.renderStepTooltip(step)} key={index}>
<button
@@ -4,12 +4,13 @@ import { Redirect } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { Button, useStyles2, Stack } from '@grafana/ui';
import { Button, Stack, useStyles2 } from '@grafana/ui';
import { Text } from '@grafana/ui/src/components/Text/Text';
import { DataTrail } from './DataTrail';
import { DataTrailCard } from './DataTrailCard';
import { DataTrailsApp } from './DataTrailsApp';
import { MetricsHeader } from './MetricsHeader';
import { getTrailStore } from './TrailStore/TrailStore';
import { getDatasourceForNewTrail, getUrlForTrail, newMetricsTrail } from './utils';
@@ -47,17 +48,13 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
// If there are no recent trails, don't show home page and create a new trail
if (!getTrailStore().recent.length) {
const trail = newMetricsTrail(getDatasourceForNewTrail());
getTrailStore().setRecentTrail(trail);
return <Redirect to={getUrlForTrail(trail)} />;
}
return (
<div className={styles.container}>
<Stack gap={2} justifyContent={'space-between'} alignItems={'center'}>
<Stack direction="column" gap={1}>
<Text variant="h1">Metrics</Text>
<Text color="secondary">Navigate through your Prometheus-compatible metrics without writing a query</Text>
</Stack>
<MetricsHeader />
<Button icon="plus" size="md" variant="primary" onClick={model.onNewMetricsTrail}>
New metric exploration
</Button>
@@ -4,22 +4,22 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import {
SceneObjectState,
SceneObjectBase,
SceneComponentProps,
PanelBuilders,
SceneFlexItem,
SceneVariableSet,
QueryVariable,
sceneGraph,
VariableDependencyConfig,
SceneCSSGridLayout,
SceneComponentProps,
SceneCSSGridItem,
SceneCSSGridLayout,
SceneFlexItem,
sceneGraph,
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
SceneQueryRunner,
SceneVariableSet,
VariableDependencyConfig,
} from '@grafana/scenes';
import { VariableHide } from '@grafana/schema';
import { Input, Text, useStyles2, InlineSwitch, Field, LoadingPlaceholder } from '@grafana/ui';
import { Field, Icon, InlineSwitch, Input, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
import { MetricCategoryCascader } from './MetricCategory/MetricCategoryCascader';
@@ -40,7 +40,6 @@ interface MetricPanel {
export interface MetricSelectSceneState extends SceneObjectState {
body: SceneCSSGridLayout;
showHeading?: boolean;
searchQuery?: string;
showPreviews?: boolean;
prefixFilter?: string;
@@ -274,8 +273,7 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
};
public static Component = ({ model }: SceneComponentProps<MetricSelectScene>) => {
const { showHeading, searchQuery, showPreviews, body, metricsAfterSearch, metricsAfterFilter, prefixFilter } =
model.useState();
const { searchQuery, showPreviews, body, metricsAfterSearch, metricsAfterFilter, prefixFilter } = model.useState();
const { children } = body.useState();
const styles = useStyles2(getStyles);
@@ -296,13 +294,15 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
return (
<div className={styles.container}>
{showHeading && (
<div className={styles.headingWrapper}>
<Text variant="h4">Select a metric</Text>
</div>
)}
<div className={styles.header}>
<Input placeholder="Search metrics" value={searchQuery} onChange={model.onSearchChange} />
<Field label={'Search metrics'} className={styles.searchField}>
<Input
placeholder="Search metrics"
prefix={<Icon name={'search'} />}
value={searchQuery}
onChange={model.onSearchChange}
/>
</Field>
<InlineSwitch showLabel={true} label="Show previews" value={showPreviews} onChange={model.onTogglePreviews} />
</div>
<div className={styles.header}>
@@ -393,19 +393,24 @@ function getStyles(theme: GrafanaTheme2) {
flexGrow: 1,
}),
headingWrapper: css({
marginTop: theme.spacing(1),
marginBottom: theme.spacing(0.5),
}),
header: css({
flexGrow: 0,
display: 'flex',
gap: theme.spacing(2),
marginBottom: theme.spacing(1),
alignItems: 'flex-end',
}),
statusMessage: css({
fontStyle: 'italic',
marginTop: theme.spacing(7),
textAlign: 'center',
}),
searchField: css({
flexGrow: 1,
marginBottom: 0,
}),
};
}
@@ -0,0 +1,10 @@
import React from 'react';
import { Stack, Text } from '@grafana/ui';
export const MetricsHeader = () => (
<Stack direction="column" gap={1}>
<Text variant="h1">Metrics</Text>
<Text color="secondary">Explore your Prometheus-compatible metrics without writing a query</Text>
</Stack>
);
+13 -1
View File
@@ -8,7 +8,7 @@ import { DataTrail } from './DataTrail';
import { DataTrailSettings } from './DataTrailSettings';
import { MetricScene } from './MetricScene';
import { getTrailStore } from './TrailStore/TrailStore';
import { TRAILS_ROUTE, VAR_DATASOURCE_EXPR } from './shared';
import { LOGS_METRIC, TRAILS_ROUTE, VAR_DATASOURCE_EXPR } from './shared';
export function getTrailFor(model: SceneObject): DataTrail {
return sceneGraph.getAncestor(model, DataTrail);
@@ -58,6 +58,18 @@ export function getDataSourceName(dataSourceUid: string) {
return getDataSourceSrv().getInstanceSettings(dataSourceUid)?.name || dataSourceUid;
}
export function getMetricName(metric?: string) {
if (!metric) {
return 'Select metric';
}
if (metric === LOGS_METRIC) {
return 'Logs';
}
return metric;
}
export function getDatasourceForNewTrail(): string | undefined {
const prevTrail = getTrailStore().recent[0];
if (prevTrail) {