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>
pull/81577/head
Andre Pereira 1 year ago committed by GitHub
parent ced0cca27a
commit 8440eadec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      .betterer.results
  2. 38
      public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx
  3. 2
      public/app/features/trails/DataTrail.tsx
  4. 112
      public/app/features/trails/DataTrailCard.tsx
  5. 24
      public/app/features/trails/DataTrailsHome.tsx
  6. 22
      public/app/features/trails/MetricScene.tsx
  7. 4
      public/app/features/trails/TrailStore/TrailStore.ts

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

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

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

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

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

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

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

Loading…
Cancel
Save