mirror of
https://github.com/grafana/grafana.git
synced 2026-08-26 05:17:26 -05:00
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:
@@ -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,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({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user