Data trails: Homepage redesign (#81496)

* Rename trails and tweak styles in homepage

* Use design system card and update layout. Add createdAt date to trail

* Small style tweaks

* Move queryDef state to metricScene

* Date format update

* More style tweaks

* betterer update

* Use smaller padding on Card and use Badge istead of Tag

* Increase badge max width

---------

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Andre Pereira 2024-01-30 13:51:31 +00:00 committed by GitHub
parent ced0cca27a
commit 8440eadec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 104 additions and 104 deletions

View File

@ -4188,10 +4188,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/MetricScene.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],

View File

@ -1,52 +1,53 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneObjectState, SceneObjectBase, SceneComponentProps, VizPanel, SceneQueryRunner } from '@grafana/scenes';
import { Field, RadioButtonGroup, useStyles2, Stack } from '@grafana/ui';
import { trailDS } from '../shared';
import { getTrailSettings } from '../utils';
import { getMetricSceneFor, getTrailSettings } from '../utils';
import { AutoQueryInfo, AutoQueryDef } from './types';
import { AutoQueryDef } from './types';
export interface AutoVizPanelState extends SceneObjectState {
panel?: VizPanel;
autoQuery: AutoQueryInfo;
queryDef?: AutoQueryDef;
}
export class AutoVizPanel extends SceneObjectBase<AutoVizPanelState> {
constructor(state: AutoVizPanelState) {
super(state);
if (!state.panel) {
this.setState({
panel: this.getVizPanelFor(state.autoQuery.main),
queryDef: state.autoQuery.main,
});
}
this.addActivationHandler(this.onActivate.bind(this));
}
public onActivate() {
const { autoQuery } = getMetricSceneFor(this).state;
this.setState({
panel: this.getVizPanelFor(autoQuery.main),
});
}
private getQuerySelector(def: AutoQueryDef) {
const variants = this.state.autoQuery.variants;
const { autoQuery } = getMetricSceneFor(this).state;
if (variants.length === 0) {
if (autoQuery.variants.length === 0) {
return;
}
const options = variants.map((q) => ({ label: q.variant, value: q.variant }));
const options = autoQuery.variants.map((q) => ({ label: q.variant, value: q.variant }));
return <RadioButtonGroup size="sm" options={options} value={def.variant} onChange={this.onChangeQuery} />;
}
public onChangeQuery = (variant: string) => {
const def = this.state.autoQuery.variants.find((q) => q.variant === variant)!;
const metricScene = getMetricSceneFor(this);
const def = metricScene.state.autoQuery.variants.find((q) => q.variant === variant)!;
this.setState({
panel: this.getVizPanelFor(def),
queryDef: def,
});
metricScene.setState({ queryDef: def });
};
private getVizPanelFor(def: AutoQueryDef) {
@ -64,7 +65,8 @@ export class AutoVizPanel extends SceneObjectBase<AutoVizPanelState> {
}
public static Component = ({ model }: SceneComponentProps<AutoVizPanel>) => {
const { panel, queryDef } = model.useState();
const { panel } = model.useState();
const { queryDef } = getMetricSceneFor(model).state;
const { showQuery } = getTrailSettings(model).useState();
const styles = useStyles2(getStyles);
@ -91,7 +93,7 @@ export class AutoVizPanel extends SceneObjectBase<AutoVizPanelState> {
};
}
function getStyles(theme: GrafanaTheme2) {
function getStyles() {
return {
wrapper: css({
display: 'flex',

View File

@ -37,6 +37,7 @@ export interface DataTrailState extends SceneObjectState {
controls: SceneObject[];
history: DataTrailHistory;
settings: DataTrailSettings;
createdAt: number;
// just for for the starting data source
initialDS?: string;
@ -61,6 +62,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
],
history: state.history ?? new DataTrailHistory({}),
settings: state.settings ?? new DataTrailSettings({}),
createdAt: state.createdAt ?? new Date().getTime(),
...state,
});

View File

@ -1,9 +1,9 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { dateTimeFormat, GrafanaTheme2 } from '@grafana/data';
import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes';
import { useStyles2, Stack, Tooltip, Button } from '@grafana/ui';
import { useStyles2, Stack, Card, IconButton, Badge } from '@grafana/ui';
import { DataTrail } from './DataTrail';
import { LOGS_METRIC, VAR_FILTERS } from './shared';
@ -27,31 +27,39 @@ export function DataTrailCard({ trail, onSelect, onDelete }: Props) {
const dsValue = getDataSource(trail);
return (
<button className={styles.container} onClick={() => onSelect(trail)}>
<div className={styles.wrapper}>
<div className={styles.heading}>{getMetricName(trail.state.metric)}</div>
{onDelete && (
<Tooltip content={'Remove bookmark'}>
<Button size="sm" icon="trash-alt" variant="destructive" fill="text" onClick={onDelete} />
</Tooltip>
)}
<Card onClick={() => onSelect(trail)} className={styles.card}>
<Card.Heading>{getMetricName(trail.state.metric)}</Card.Heading>
<div className={styles.description}>
<Stack gap={1.5}>
{filters.map((f) => (
<Badge key={f.key} text={`${f.key}: ${f.value}`} color={'blue'} className={styles.tag} />
))}
</Stack>
</div>
<Stack gap={1.5}>
{dsValue && (
<Stack direction="column" gap={0.5}>
<div className={styles.label}>Datasource</div>
<div className={styles.value}>{getDataSourceName(dsValue)}</div>
</Stack>
)}
{filters.map((filter, index) => (
<Stack key={index} direction="column" gap={0.5}>
<div className={styles.label}>{filter.key}</div>
<div className={styles.value}>{filter.value}</div>
</Stack>
))}
</Stack>
</button>
<Card.Actions className={styles.actions}>
<Stack gap={1} justifyContent={'space-between'} grow={1}>
<div className={styles.secondary}>
<b>Datasource:</b> {getDataSourceName(dsValue)}
</div>
{trail.state.createdAt && (
<i className={styles.secondary}>
<b>Created:</b> {dateTimeFormat(trail.state.createdAt, { format: 'LL' })}
</i>
)}
</Stack>
</Card.Actions>
{onDelete && (
<Card.SecondaryActions>
<IconButton
key="delete"
name="trash-alt"
className={styles.secondary}
tooltip="Remove bookmark"
onClick={onDelete}
/>
</Card.SecondaryActions>
)}
</Card>
);
}
@ -69,45 +77,27 @@ function getMetricName(metric?: string) {
function getStyles(theme: GrafanaTheme2) {
return {
container: css({
tag: css({
maxWidth: '260px',
overflow: 'hidden',
textOverflow: 'ellipsis',
}),
card: css({
padding: theme.spacing(1),
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}),
secondary: css({
color: theme.colors.text.secondary,
fontSize: '12px',
}),
description: css({
width: '100%',
border: `1px solid ${theme.colors.border.weak}`,
borderRadius: theme.shape.radius.default,
cursor: 'pointer',
boxShadow: 'none',
background: 'transparent',
textAlign: 'left',
'&:hover': {
background: theme.colors.emphasize(theme.colors.background.primary, 0.03),
},
gridArea: 'Description',
margin: theme.spacing(1, 0, 0),
color: theme.colors.text.secondary,
lineHeight: theme.typography.body.lineHeight,
}),
label: css({
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.bodySmall.fontSize,
}),
value: css({
fontSize: theme.typography.bodySmall.fontSize,
}),
heading: css({
padding: theme.spacing(0),
display: 'flex',
fontWeight: theme.typography.fontWeightMedium,
overflowX: 'hidden',
}),
body: css({
padding: theme.spacing(0),
}),
wrapper: css({
position: 'relative',
display: 'flex',
gap: theme.spacing.x1,
justifyContent: 'space-between',
width: '100%',
actions: css({
marginRight: theme.spacing(1),
}),
};
}

View File

@ -53,18 +53,18 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
return (
<div className={styles.container}>
<Stack direction="column" gap={1}>
<Text variant="h2">Data trails</Text>
<Text color="secondary">Automatically query, explore and navigate your observability data</Text>
</Stack>
<Stack gap={2}>
<Button icon="plus" size="lg" variant="secondary" onClick={model.onNewMetricsTrail}>
New metric trail
<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>
<Button icon="plus" size="md" variant="primary" onClick={model.onNewMetricsTrail}>
New metric exploration
</Button>
</Stack>
<Stack gap={4}>
<Stack gap={5}>
<div className={styles.column}>
<Text variant="h4">Recent trails</Text>
<Text variant="h4">Recent metrics</Text>
<div className={styles.trailList}>
{getTrailStore().recent.map((trail, index) => {
const resolvedTrail = trail.resolve();
@ -78,6 +78,7 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
})}
</div>
</div>
<div className={styles.verticalLine} />
<div className={styles.column}>
<Text variant="h4">Bookmarks</Text>
<div className={styles.trailList}>
@ -114,8 +115,8 @@ function getStyles(theme: GrafanaTheme2) {
gap: theme.spacing(3),
}),
column: css({
width: 500,
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
gap: theme.spacing(2),
}),
@ -130,5 +131,8 @@ function getStyles(theme: GrafanaTheme2) {
flexDirection: 'column',
gap: theme.spacing(2),
}),
verticalLine: css({
borderLeft: `1px solid ${theme.colors.border.weak}`,
}),
};
}

View File

@ -24,6 +24,7 @@ import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene';
import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene';
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types';
import { ShareTrailButton } from './ShareTrailButton';
import { getTrailStore } from './TrailStore/TrailStore';
import {
@ -42,15 +43,21 @@ export interface MetricSceneState extends SceneObjectState {
body: SceneFlexLayout;
metric: string;
actionView?: string;
autoQuery: AutoQueryInfo;
queryDef?: AutoQueryDef;
}
export class MetricScene extends SceneObjectBase<MetricSceneState> {
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['actionView'] });
public constructor(state: MakeOptional<MetricSceneState, 'body'>) {
public constructor(state: MakeOptional<MetricSceneState, 'body' | 'autoQuery'>) {
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric);
super({
$variables: state.$variables ?? getVariableSet(state.metric),
body: state.body ?? buildGraphScene(state.metric),
body: state.body ?? buildGraphScene(),
autoQuery,
queryDef: state.queryDef ?? autoQuery.main,
...state,
});
@ -121,10 +128,8 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
const trail = getTrailFor(this);
const dsValue = getDataSource(trail);
const flexItem = metricScene.state.body.state.children[0] as SceneFlexItem;
const autoVizPanel = flexItem.state.body as AutoVizPanel;
const queries = autoVizPanel.state.queryDef?.queries || [];
const timeRange = sceneGraph.getTimeRange(autoVizPanel);
const queries = metricScene.state.queryDef?.queries || [];
const timeRange = sceneGraph.getTimeRange(this);
return getExploreUrl({
queries,
@ -235,9 +240,8 @@ function getVariableSet(metric: string) {
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 });
function buildGraphScene() {
const bodyAutoVizPanel = new AutoVizPanel({});
return new SceneFlexLayout({
direction: 'column',

View File

@ -16,6 +16,7 @@ export interface SerializedTrail {
parentIndex: number;
}>;
currentStep: number;
createdAt?: number;
}
export class TrailStore {
@ -53,7 +54,7 @@ export class TrailStore {
private _deserializeTrail(t: SerializedTrail): DataTrail {
// reconstruct the trail based on the the serialized history
const trail = new DataTrail({});
const trail = new DataTrail({ createdAt: t.createdAt });
t.history.map((step) => {
this._loadFromUrl(trail, step.urlValues);
@ -82,6 +83,7 @@ export class TrailStore {
return {
history,
currentStep: trail.state.history.state.currentStep,
createdAt: trail.state.createdAt,
};
}