mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -06:00
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:
parent
ced0cca27a
commit
8440eadec2
@ -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"]
|
||||
],
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -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}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user