Skip to content

Commit

Permalink
Multiple fixes to empty state and user experience (#263)
Browse files Browse the repository at this point in the history
* Fix style for filters bar

* Improve RED metrics tooltip design

* Add textual description for the span list tab

* Update structure tab

* Update link

* Update error title to sync with others

* Make sure empty state is shown for errors with no data

* Consistent padding

* Add remedy message to empty state

* Extract empty state error messages

* Use common error remedy

* Update doc link

* Fix merge

---------

Co-authored-by: André Pereira <[email protected]>
  • Loading branch information
joey-grafana and adrapereira authored Nov 19, 2024
1 parent c3d4207 commit 54a7b94
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 78 deletions.
8 changes: 5 additions & 3 deletions src/components/Explore/ByFrameRepeater.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ErrorStateScene } from 'components/states/ErrorState/ErrorStateScene';
import { debounce } from 'lodash';
import { Search } from './Search';
import { getGroupByVariable } from 'utils/utils';
import { EventTimeseriesDataReceived, GRID_TEMPLATE_COLUMNS } from '../../utils/shared';
import { EMPTY_STATE_ERROR_MESSAGE, EMPTY_STATE_ERROR_REMEDY_MESSAGE, EventTimeseriesDataReceived, GRID_TEMPLATE_COLUMNS } from '../../utils/shared';

interface ByFrameRepeaterState extends SceneObjectState {
body: SceneLayout;
Expand All @@ -45,8 +45,9 @@ export class ByFrameRepeater extends SceneObjectBase<ByFrameRepeaterState> {
children: [
new SceneFlexItem({
body: new EmptyStateScene({
message: 'No data for selected query',
padding: '48px',
message: EMPTY_STATE_ERROR_MESSAGE,
remedyMessage: EMPTY_STATE_ERROR_REMEDY_MESSAGE,
padding: '32px',
}),
}),
],
Expand Down Expand Up @@ -121,6 +122,7 @@ export class ByFrameRepeater extends SceneObjectBase<ByFrameRepeaterState> {
new SceneFlexItem({
body: new EmptyStateScene({
message: 'No data for search term',
padding: '32px',
}),
}),
],
Expand Down
4 changes: 2 additions & 2 deletions src/components/Explore/TracesByService/MiniREDPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { rateByWithStatus } from '../queries/rateByWithStatus';
import { StepQueryRunner } from '../queries/StepQueryRunner';
import { RadioButtonList, useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { getTraceExplorationScene } from '../../../utils/utils';
import { fieldHasEmptyValues, getTraceExplorationScene } from '../../../utils/utils';
import { MINI_PANEL_HEIGHT } from './TracesByServiceScene';
import { buildHistogramQuery } from '../queries/histogram';
import { histogramPanelConfig } from '../panels/histogram';
Expand All @@ -42,7 +42,7 @@ export class MiniREDPanel extends SceneObjectBase<MiniREDPanelState> {
this._subs.add(
data.subscribeToState((data) => {
if (data.data?.state === LoadingState.Done) {
if (data.data.series.length === 0 || data.data.series[0].length === 0) {
if (data.data.series.length === 0 || data.data.series[0].length === 0 || fieldHasEmptyValues(data)) {
this.setState({
panel: new SceneFlexLayout({
children: [
Expand Down
7 changes: 4 additions & 3 deletions src/components/Explore/TracesByService/REDPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
SceneObjectState,
} from '@grafana/scenes';
import { arrayToDataFrame, GrafanaTheme2, LoadingState } from '@grafana/data';
import { ComparisonSelection, explorationDS, MetricFunction } from 'utils/shared';
import { ComparisonSelection, EMPTY_STATE_ERROR_MESSAGE, explorationDS, MetricFunction } from 'utils/shared';
import { EmptyStateScene } from 'components/states/EmptyState/EmptyStateScene';
import { LoadingStateScene } from 'components/states/LoadingState/LoadingStateScene';
import { SkeletonComponent } from '../ByFrameRepeater';
Expand All @@ -20,6 +20,7 @@ import { StepQueryRunner } from '../queries/StepQueryRunner';
import { css } from '@emotion/css';
import { RadioButtonList, useStyles2 } from '@grafana/ui';
import {
fieldHasEmptyValues,
getLatencyPartialThresholdVariable,
getLatencyThresholdVariable,
getMetricVariable,
Expand Down Expand Up @@ -56,13 +57,13 @@ export class REDPanel extends SceneObjectBase<RateMetricsPanelState> {
this._subs.add(
data.subscribeToState((newData) => {
if (newData.data?.state === LoadingState.Done) {
if (newData.data.series.length === 0 || newData.data.series[0].length === 0) {
if (newData.data.series.length === 0 || newData.data.series[0].length === 0 || fieldHasEmptyValues(newData)) {
this.setState({
panel: new SceneFlexLayout({
children: [
new SceneFlexItem({
body: new EmptyStateScene({
message: 'No data for selected query',
message: EMPTY_STATE_ERROR_MESSAGE,
imgWidth: 150,
}),
}),
Expand Down
76 changes: 42 additions & 34 deletions src/components/Explore/TracesByService/Tabs/Spans/SpanListScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import { LoadingStateScene } from 'components/states/LoadingState/LoadingStateSc
import { EmptyStateScene } from 'components/states/EmptyState/EmptyStateScene';
import { css } from '@emotion/css';
import Skeleton from 'react-loading-skeleton';
import { Icon, Link, TableCellDisplayMode, TableCustomCellOptions, useStyles2 } from '@grafana/ui';
import { Icon, Link, TableCellDisplayMode, TableCustomCellOptions, useStyles2, useTheme2 } from '@grafana/ui';
import { map, Observable } from 'rxjs';
import { getDataSource, getTraceExplorationScene } from '../../../../../utils/utils';
import { EMPTY_STATE_ERROR_MESSAGE, EMPTY_STATE_ERROR_REMEDY_MESSAGE } from '../../../../../utils/shared';

export interface SpanListSceneState extends SceneObjectState {
panel?: SceneFlexLayout;
Expand Down Expand Up @@ -160,7 +161,9 @@ export class SpanListScene extends SceneObjectBase<SpanListSceneState> {
children: [
new SceneFlexItem({
body: new EmptyStateScene({
message: 'No data for selected query',
message: EMPTY_STATE_ERROR_MESSAGE,
remedyMessage: EMPTY_STATE_ERROR_REMEDY_MESSAGE,
padding: '32px',
}),
}),
],
Expand Down Expand Up @@ -195,20 +198,55 @@ export class SpanListScene extends SceneObjectBase<SpanListSceneState> {

public static Component = ({ model }: SceneComponentProps<SpanListScene>) => {
const { panel } = model.useState();
const styles = useStyles2(getTableStyles);
const styles = getStyles(useTheme2());

if (!panel) {
return;
}

return (
<div className={styles.overrideCell}>
<div className={styles.container}>
<div className={styles.description}>View a list of spans for the current set of filters.</div>
<panel.Component model={panel} />
</div>
);
};
}

const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
display: 'contents',

'[role="cell"] > div': {
display: 'flex',
width: '100%',
},

'.cell-link-wrapper': {
display: 'flex',
gap: '4px',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
},

'.cell-link': {
color: theme.colors.text.link,
cursor: 'pointer',

':hover': {
textDecoration: 'underline',
},
},
}),
description: css({
fontSize: theme.typography.h6.fontSize,
padding: `${theme.spacing(1)} 0 ${theme.spacing(2)} 0`,
}),
};
};

const SkeletonComponent = () => {
const styles = useStyles2(getSkeletonStyles);

Expand Down Expand Up @@ -253,33 +291,3 @@ function getSkeletonStyles(theme: GrafanaTheme2) {
}),
};
}

function getTableStyles(theme: GrafanaTheme2) {
return {
overrideCell: css({
display: 'flex',
flexGrow: 1,
'[role="cell"] > div': {
display: 'flex',
width: '100%',
},

'.cell-link-wrapper': {
display: 'flex',
gap: '4px',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
},

'.cell-link': {
color: theme.colors.text.link,
cursor: 'pointer',

':hover': {
textDecoration: 'underline',
},
},
}),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SceneQueryRunner,
} from '@grafana/scenes';
import {
EMPTY_STATE_ERROR_MESSAGE,
explorationDS,
filterStreamingProgressTransformations,
MetricFunction,
Expand All @@ -24,14 +25,13 @@ import { TraceSearchMetadata } from '../../../../../types';
import { mergeTraces } from '../../../../../utils/trace-merge/merge';
import { createDataFrame, Field, FieldType, GrafanaTheme2, LinkModel, LoadingState } from '@grafana/data';
import { TreeNode } from '../../../../../utils/trace-merge/tree-node';
import { Button, Icon, Stack, Text, useTheme2 } from '@grafana/ui';
import { Icon, LinkButton, Stack, Text, useTheme2 } from '@grafana/ui';
import Skeleton from 'react-loading-skeleton';
import { EmptyState } from '../../../../states/EmptyState/EmptyState';
import { css } from '@emotion/css';
import { locationService } from '@grafana/runtime';
import { getTraceExplorationScene } from 'utils/utils';
import { structureDisplayName } from '../TabsBarScene';
import { primarySignalOptions } from 'pages/Explore/primary-signals';

export interface ServicesTabSceneState extends SceneObjectState {
panel?: SceneFlexLayout;
Expand Down Expand Up @@ -279,7 +279,7 @@ export class StructureTabScene extends SceneObjectBase<ServicesTabSceneState> {
const noDataMessage = (
<>
<Text textAlignment={'center'} variant="h3">
No data
{EMPTY_STATE_ERROR_MESSAGE}
</Text>
<Text textAlignment={'center'} variant="body">
<div className={styles.longText}>
Expand All @@ -296,22 +296,20 @@ export class StructureTabScene extends SceneObjectBase<ServicesTabSceneState> {

<div className={styles.actionContainer}>
Read more about
<a
href="https://grafana.com/docs/grafana/next/explore/simplified-exploration/traces/#compare-tracing-data"
className={styles.link}
title={`Read more about ${tabName.toLowerCase()}`}
target="_blank"
rel="noreferrer noopener"
>
{`${tabName.toLowerCase()}`}
</a>
<Button
variant="primary"
fill="solid"
onClick={() => exploration.onChangePrimarySignal(primarySignalOptions[0]?.value || '')}
>
Apply full traces filter
</Button>

<div className={styles.action}>
<LinkButton
icon="external-link-alt"
fill="solid"
size={'sm'}
target={'_blank'}
href={
'https://grafana.com/docs/grafana/next/explore/simplified-exploration/traces/concepts/#trace-structure'
}
>
{`${tabName.toLowerCase()}`}
</LinkButton>
</div>
</div>
</>
);
Expand Down Expand Up @@ -398,13 +396,8 @@ const getStyles = (theme: GrafanaTheme2) => {
maxWidth: '800px',
margin: '0 auto',
}),
link: css({
margin: '6px',
color: theme.colors.text.link,
marginRight: theme.spacing(2),
'&:hover': {
textDecoration: 'underline',
},
action: css({
marginLeft: theme.spacing(1),
}),
actionContainer: css({
display: 'flex',
Expand Down
19 changes: 14 additions & 5 deletions src/components/Explore/TracesByService/TracesByServiceScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,10 @@ const MetricTypeTooltip = () => {
const styles = useStyles2(getStyles);

return (
<Stack direction={'column'} gap={2}>
<Stack direction={'column'} gap={1}>
<div className={styles.tooltip.title}>RED metrics for traces</div>
<span className={styles.tooltip.subtitle}>Explore rate, errors, and duration (RED) metrics generated from traces by Tempo.</span>
<div className={styles.tooltip.text}>
<div>Explore rate, errors, and duration (RED) metrics generated from traces by Tempo.</div>
<div>
<span className={styles.tooltip.emphasize}>Rate</span> - Spans per second that match your filter, useful to
find unusual spikes in activity
Expand All @@ -255,14 +255,14 @@ const MetricTypeTooltip = () => {
</div>
</div>

<div>
<div className={styles.tooltip.button}>
<LinkButton
icon="external-link-alt"
fill="solid"
size={'sm'}
target={'_blank'}
href={
'https://grafana.com/docs/grafana-cloud/visualizations/simplified-exploration/traces/#rate-error-and-duration-metrics'
'https://grafana.com/docs/grafana-cloud/visualizations/simplified-exploration/traces/concepts/#rate-error-and-duration-metrics'
}
onClick={() =>
reportAppInteraction(USER_EVENTS_PAGES.common, USER_EVENTS_ACTIONS.common.metric_docs_link_clicked)
Expand Down Expand Up @@ -291,14 +291,23 @@ function getStyles(theme: GrafanaTheme2) {
title: css({
fontSize: '14px',
fontWeight: 500,
lineHeight: '22px',
}),
subtitle: css({
marginBottom: theme.spacing.x1,
}),
text: css({
color: theme.colors.text.secondary,

'div': {
marginBottom: theme.spacing.x0_5,
}
}),
emphasize: css({
color: theme.colors.text.primary,
}),
button: css({
marginBottom: theme.spacing.x0_5,
}),
},
};
}
Expand Down
19 changes: 17 additions & 2 deletions src/components/states/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { Stack, Text, useStyles2 } from '@grafana/ui';
import { Icon, Stack, Text, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';

import { GrotNotFound } from './GrotNotFound';
Expand All @@ -9,11 +9,12 @@ import { testIds } from 'utils/testIds';

export interface Props {
message?: string | React.ReactNode;
remedyMessage?: string;
imgWidth?: number;
padding?: string;
}

export const EmptyState = ({ message, imgWidth, padding }: Props) => {
export const EmptyState = ({ message, remedyMessage, imgWidth, padding }: Props) => {
const styles = useStyles2(getStyles, padding);

return (
Expand All @@ -22,6 +23,17 @@ export const EmptyState = ({ message, imgWidth, padding }: Props) => {
<GrotNotFound width={imgWidth ?? 300} />
{typeof message === 'string' && <Text textAlignment={'center'} variant="h5">{message}</Text>}
{typeof message !== 'string' && message}

{remedyMessage && (
<div className={styles.remedy}>
<Stack gap={0.5} alignItems={'center'}>
<Icon name="info-circle" />
<Text textAlignment={'center'} variant="body">
{remedyMessage}
</Text>
</Stack>
</div>
)}
</Stack>
</div>
);
Expand All @@ -38,5 +50,8 @@ function getStyles(theme: GrafanaTheme2, padding?: string) {
flexDirection: 'column',
padding: padding ? padding : 0,
}),
remedy: css({
marginBottom: theme.spacing(4),
})
};
}
Loading

0 comments on commit 54a7b94

Please sign in to comment.