Feat: New homepage design (#93354)

* wip: homepage (new user)

* fix: spacing between button and text; wip refactor: separating bookmarks and recent metrics from home page

* feat: new user homepage; wip: need to clean up code

* fix: change rocket icon to svg

* wip feat: rendering recent metrics

* chore: add comments to understand code, will need to delete / cleanup later / pare down into documentation comments

* wip: new recent metric card design

* wip: display recent metrics cards in rows of 3 (height still incorrect)

* feat: apply conditional styling to remainder recent metrics exploration cards (any cards that are not a complete row of 3)

* fix: render new recent metrics explorations without refresh

* style: render recent metrics explorations in rows of 3 using grid instead of flex; fix: remove remainder card styling

* fix: remove delete button from recent metrics exp cards

* style: make background color for each card take up the entire card/grid space; make height of cards for each row the tallest card

* chore: clean up code

* fix: fix eslint errors

* style: implement recent metrics card header styling

* style: in recent metrics exp cards, format datasource line

* fix: add initial value for _lastModified to fix eslint err

* style: format date correctly; chore: clean up code; wip: get date to render properly on bottom left

* style: make inner card height match outer card height; style: add date footer; style: wrap last metric name; style: wrap labels

* style: adjust font for label name and label value

* style: truncate singular label if value is greater than 35 characters

* style: truncate singular long labels at 44 characters; style: truncate multiple labels at 3 lines; style: correct the border width and radius

* style: make background border radius match the border

* style: correct gap between rows and columns of cards; style: correct padding inside card

* chore: clean up code

* refactor: apply new card UI to DataTrailCard component

* feat: add bookmarks (not formatted correctly), only render section if there are bookmarks, hook up delete functionality

* style: add horizontal line above bookmarks header; style: add bookmarks header

* style: add additional padding above bookmarks divider; chore: delete unused code

* style: add carrot button to bookmarks; style: format heading font style

* refactor: separate bookmarks into functional component; feat: make bookmarks section collapsed by default; feat: allow toggle to expand bookmarks section

* style: position delete button for bookmarks in bottom right of card

* fix: only render recent metrics and bookmarks headings if there are any

* style: add show more button (not functional); style: fix padding around show more button

* chore: delete unused code

* fix: add back gap underneath bookmarks header

* feat: implement show more/less button for recent metrics

* fix: do not show select metric card if user does not actually select a metric

* chore: preliminary code clean up

* chore: delete console.logs, comments

* chore: clean up styling

* Update public/app/features/trails/DataTrailCard.tsx

Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com>

* fix: add i18nKey to Trans tags

* fix: attempt to remove go.work.sum changes that are unrelated to my PR

* fix: add Trans tags

* refactor: sepearate recent metrics into functional component; chore: delete unused code; fix: add Trans tags

* chore: generate translation json

* trigger drone

* trigger drone

* fix: add trans tag to date

* chore: abbreviate descriptive key, regenerate json

* Update public/app/features/trails/DataTrailBookmarks.tsx

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>

* Update public/app/features/trails/DataTrailsRecentMetrics.tsx

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>

* Update public/app/features/trails/DataTrailBookmarks.tsx

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>

* fix: revert trans tag on date created to fix formatting

* chore: return null immediately if no recent metrics

* style: add margin between  bookmarks header and carrot toggle button

* style: adjust margin to 8px between bookmarks header and carrot toggle button

* style: make margins multiples of 4

* Update public/app/features/trails/DataTrailBookmarks.tsx

Co-authored-by: Brendan O'Handley <brendan.ohandley@grafana.com>

* style: fix light mode styles; style: fix border radius

* fix: save select metric view as recent metric card if labels are applied

* Update public/app/features/trails/DataTrailCard.tsx

Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com>

* refactor: move rocket svgs into assets folder

* chore: add back accidentally deleted console log

* Update public/app/features/trails/DataTrail.tsx

Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com>

* Update public/app/features/trails/DataTrailBookmarks.tsx

Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com>

* chore: revert lastModified related changes since behavior appears to remain the same

* fix: add back lastModified changes because they make the recent metrics show more functionality work

---------

Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com>
Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
Co-authored-by: Brendan O'Handley <brendan.ohandley@grafana.com>
pull/95626/head
Kat Yang 7 months ago committed by GitHub
parent 55bcbcef83
commit a2755117ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      .betterer.results
  2. 10
      public/app/features/trails/DataTrail.tsx
  3. 97
      public/app/features/trails/DataTrailBookmarks.tsx
  4. 160
      public/app/features/trails/DataTrailCard.tsx
  5. 8
      public/app/features/trails/DataTrailsApp.tsx
  6. 133
      public/app/features/trails/DataTrailsHome.tsx
  7. 85
      public/app/features/trails/DataTrailsRecentMetrics.tsx
  8. 10
      public/app/features/trails/TrailStore/TrailStore.ts
  9. 19
      public/app/features/trails/assets/rockets.tsx
  10. 16
      public/locales/en-US/grafana.json
  11. 16
      public/locales/pseudo-LOCALE/grafana.json

@ -4335,8 +4335,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/trails/DataTrailCard.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/trails/DataTrailSettings.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
@ -4344,11 +4343,6 @@ exports[`better eslint`] = {
"public/app/features/trails/DataTrailsHistory.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/trails/DataTrailsHome.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
],
"public/app/features/trails/MetricScene.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]

@ -137,8 +137,14 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
);
}
// Save the current trail as a recent if the browser closes or reloads
const saveRecentTrail = () => getTrailStore().setRecentTrail(this);
// Save the current trail as a recent (if the browser closes or reloads) if user selects a metric OR applies filters to metric select view
const saveRecentTrail = () => {
const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, this);
const hasFilters = filtersVariable instanceof AdHocFiltersVariable && filtersVariable.state.filters.length > 0;
if (this.state.metric || hasFilters) {
getTrailStore().setRecentTrail(this);
}
};
window.addEventListener('unload', saveRecentTrail);
return () => {

@ -0,0 +1,97 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { IconButton, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { DataTrailCard } from './DataTrailCard';
import { DataTrailsHome } from './DataTrailsHome';
import { getTrailStore, getBookmarkKey } from './TrailStore/TrailStore';
interface Props extends SceneComponentProps<DataTrailsHome> {
onDelete: (index: number) => void;
}
export function DataTrailsBookmarks({ model, onDelete }: Props) {
const [toggleBookmark, setToggleBookmark] = useState(false);
const styles = useStyles2(getStyles);
if (getTrailStore().bookmarks.length === 0) {
return null;
}
return (
<>
<div className={styles.horizontalLine} />
<div className={css(styles.gap20, styles.bookmarkHeader, styles.bottomGap24)}>
<div className={styles.header} style={{ marginRight: '8px' }}>
<Trans i18nKey="trails.bookmarks.or-view-bookmarks">Or view bookmarks</Trans>
</div>
<IconButton
name={toggleBookmark ? 'angle-up' : 'angle-down'}
size="xxxl"
aria-label="bookmarkCarrot"
variant="secondary"
onClick={() => setToggleBookmark(!toggleBookmark)}
/>
</div>
{toggleBookmark && (
<div className={styles.trailList}>
{getTrailStore().bookmarks.map((bookmark, index) => {
return (
<DataTrailCard
key={getBookmarkKey(bookmark)}
bookmark={bookmark}
onSelect={() => model.onSelectBookmark(index)}
onDelete={() => onDelete(index)}
/>
);
})}
</div>
)}
</>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
trailList: css({
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: `${theme.spacing(4)}`,
alignItems: 'stretch',
justifyItems: 'center',
}),
gap20: css({
marginTop: theme.spacing(3),
}),
bottomGap24: css({
marginBottom: theme.spacing(3),
}),
bookmarkHeader: css({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}),
header: css({
color: theme.colors.text.primary,
textAlign: 'center',
/* H4 */
fontFamily: 'Inter',
fontSize: '18px',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '22px' /* 122.222% */,
letterSpacing: '0.045px',
}),
horizontalLine: css({
width: '400px',
height: '1px',
background: theme.colors.border.weak,
margin: '0 auto', // Center line horizontally
marginTop: '32px',
}),
};
}

@ -3,7 +3,8 @@ import { useMemo } from 'react';
import { dateTimeFormat, GrafanaTheme2 } from '@grafana/data';
import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes';
import { Card, IconButton, Stack, Tag, useStyles2 } from '@grafana/ui';
import { Card, IconButton, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { DataTrail } from './DataTrail';
import { getTrailStore, DataTrailBookmark } from './TrailStore/TrailStore';
@ -17,6 +18,15 @@ export type Props = {
onDelete?: () => void;
};
// Helper function to truncate the value for a single key:value pair
const truncateValue = (key: string, value: string, maxLength: number) => {
const combinedLength = key.length + 2 + value.length; // 2 for ": "
if (combinedLength > maxLength) {
return value.substring(0, maxLength - key.length - 5) + '...'; // 5 for ": " and "..."
}
return value;
};
export function DataTrailCard(props: Props) {
const { onSelect, onDelete, bookmark } = props;
const styles = useStyles2(getStyles);
@ -50,68 +60,140 @@ export function DataTrailCard(props: Props) {
const { dsValue, filters, metric, createdAt } = values;
return (
<Card onClick={onSelect} className={styles.card}>
<Card.Heading className={styles.wordwrap}>{getMetricName(metric)}</Card.Heading>
<div className={styles.description}>
<Stack gap={1.5} wrap="wrap">
<div>
<Card onClick={onSelect} className={styles.card}>
<Card.Heading>
<div className={styles.metricLabel}>
<Trans i18nKey="trails.card.metric">Metric:</Trans>
</div>
<div className={styles.metricValue}>{getMetricName(metric)}</div>
</Card.Heading>
<Card.Meta className={styles.meta}>
{filters.map((f) => (
<Tag key={f.key} name={`${f.key}: ${f.value}`} colorIndex={12} />
<span key={f.key}>
<div className={styles.secondaryFont}>{f.key}: </div>
<div className={styles.primaryFont}>{truncateValue(f.key, f.value, 44)}</div>
</span>
))}
</Stack>
</div>
<Card.Actions className={styles.actions}>
<Stack gap={1} justifyContent={'space-between'} grow={1}>
<div className={styles.secondary}>
<b>Datasource:</b> {getDataSourceName(dsValue)}
</Card.Meta>
<div className={styles.datasource}>
<div className={styles.secondaryFont}>
<Trans i18nKey="trails.card.data-source">Data source: </Trans>
</div>
{createdAt && (
<i className={styles.secondary}>
<b>Created:</b> {dateTimeFormat(createdAt, { format: 'LL' })}
</i>
<div className={styles.primaryFont}>{dsValue && getDataSourceName(dsValue)}</div>
</div>
<div className={styles.deleteButton}>
{onDelete && (
<Card.SecondaryActions>
<IconButton
key="delete"
name="trash-alt"
className={styles.secondary}
tooltip="Remove bookmark"
onClick={onDelete}
/>
</Card.SecondaryActions>
)}
</Stack>
</Card.Actions>
{onDelete && (
<Card.SecondaryActions>
<IconButton
key="delete"
name="trash-alt"
className={styles.secondary}
tooltip="Remove bookmark"
onClick={onDelete}
/>
</Card.SecondaryActions>
)}
</Card>
</div>
</Card>
<div className={styles.date}>
<div className={styles.secondaryFont}>
<Trans i18nKey="trails.card.date-created">Date created: </Trans>
</div>
<div className={styles.primaryFont}>{createdAt && dateTimeFormat(createdAt, { format: 'YYYY-MM-DD' })}</div>
</div>
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
export function getStyles(theme: GrafanaTheme2) {
return {
metricLabel: css({
display: 'inline',
color: theme.colors.text.primary,
fontFamily: 'Inter',
fontSize: '14px',
fontStyle: 'normal',
fontWeight: 400,
}),
metricValue: css({
display: 'inline',
color: theme.colors.text.primary,
fontFamily: 'Inter',
fontSize: '14px',
fontStyle: 'normal',
fontWeight: 500,
marginLeft: '8px', // Add space between the label and the value
wordBreak: 'break-all',
}),
tag: css({
maxWidth: '260px',
overflow: 'hidden',
textOverflow: 'ellipsis',
}),
card: css({
padding: theme.spacing(1),
position: 'relative',
width: '318px',
padding: `12px ${theme.spacing(2)} ${theme.spacing(1)} ${theme.spacing(2)}`,
height: '152px',
alignItems: 'start',
marginBottom: 0,
borderTop: `1px solid ${theme.colors.border.weak}`,
borderRight: `1px solid ${theme.colors.border.weak}`,
borderLeft: `1px solid ${theme.colors.border.weak}`,
borderBottom: 'none', // Remove the bottom border
// eslint-disable-next-line @grafana/no-border-radius-literal
borderRadius: '2px 2px 0 0', // Top-left and top-right corners are 2px, bottom-left and bottom-right are 0; cannot use theme.shape.radius.default because need bottom corners to be 0
}),
secondary: css({
color: theme.colors.text.secondary,
fontSize: '12px',
}),
description: css({
width: '100%',
datasource: css({
gridArea: 'Description',
margin: theme.spacing(1, 0, 0),
color: theme.colors.text.secondary,
lineHeight: theme.typography.body.lineHeight,
}),
date: css({
border: `1px solid ${theme.colors.border.weak}`,
// eslint-disable-next-line @grafana/no-border-radius-literal
borderRadius: '0 0 2px 2px',
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
backgroundColor: theme.colors.background.primary,
}),
meta: css({
flexWrap: 'wrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxHeight: '54px',
width: '100%',
margin: 0,
gridArea: 'Meta',
color: theme.colors.text.secondary,
whiteSpace: 'nowrap',
}),
actions: css({
marginRight: theme.spacing(1),
primaryFont: css({
display: 'inline',
color: theme.colors.text.primary,
fontFamily: 'Inter',
fontSize: '12px',
fontStyle: 'normal',
fontWeight: '500',
lineHeight: '18px' /* 150% */,
letterSpacing: '0.018px',
}),
secondaryFont: css({
display: 'inline',
color: theme.colors.text.secondary,
fontFamily: 'Inter',
fontSize: '12px',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '18px' /* 150% */,
letterSpacing: '0.018px',
}),
deleteButton: css({
position: 'absolute',
bottom: theme.spacing(1),
right: theme.spacing(1),
}),
wordwrap: css({
overflow: 'hidden',

@ -8,7 +8,6 @@ import { Page } from 'app/core/components/Page/Page';
import { DataTrail } from './DataTrail';
import { DataTrailsHome } from './DataTrailsHome';
import { MetricsHeader } from './MetricsHeader';
import { getTrailStore } from './TrailStore/TrailStore';
import { HOME_ROUTE, TRAILS_ROUTE } from './shared';
import { getMetricName, getUrlForTrail, newMetricsTrail } from './utils';
@ -40,7 +39,8 @@ export class DataTrailsApp extends SceneObjectBase<DataTrailsAppState> {
<Page
navId="explore/metrics"
layout={PageLayoutType.Standard}
renderTitle={() => <MetricsHeader />}
// Returning null to prevent default behavior which renders a header
renderTitle={() => null}
subTitle=""
>
<home.Component model={home} />
@ -59,7 +59,9 @@ function DataTrailView({ trail }: { trail: DataTrail }) {
useEffect(() => {
if (!isInitialized) {
getTrailStore().setRecentTrail(trail);
if (trail.state.metric !== undefined) {
getTrailStore().setRecentTrail(trail);
}
setIsInitialized(true);
}
}, [trail, isInitialized]);

@ -1,19 +1,20 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { Navigate } from 'react-router-dom-v5-compat';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { Button, EmptyState, Stack, useStyles2 } from '@grafana/ui';
import { Box, Button, Icon, Stack, TextLink, useStyles2, useTheme2 } from '@grafana/ui';
import { Text } from '@grafana/ui/src/components/Text/Text';
import { Trans } from '@grafana/ui/src/utils/i18n';
import { Trans } from 'app/core/internationalization';
import { DataTrail } from './DataTrail';
import { DataTrailCard } from './DataTrailCard';
import { DataTrailsBookmarks } from './DataTrailBookmarks';
import { DataTrailsApp } from './DataTrailsApp';
import { getBookmarkKey, getTrailStore } from './TrailStore/TrailStore';
import { DataTrailsRecentMetrics } from './DataTrailsRecentMetrics';
import { getTrailStore } from './TrailStore/TrailStore';
import { LightModeRocket, DarkModeRocket } from './assets/rockets';
import { reportExploreMetrics } from './interactions';
import { getDatasourceForNewTrail, getUrlForTrail, newMetricsTrail } from './utils';
import { getDatasourceForNewTrail, newMetricsTrail } from './utils';
export interface DataTrailsHomeState extends SceneObjectState {}
@ -26,7 +27,6 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
const app = getAppFor(this);
const trail = newMetricsTrail(getDatasourceForNewTrail());
reportExploreMetrics('exploration_started', { cause: 'new_clicked' });
getTrailStore().setRecentTrail(trail);
app.goToUrlForTrail(trail);
};
@ -48,6 +48,7 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
static Component = ({ model }: SceneComponentProps<DataTrailsHome>) => {
const [_, setLastDelete] = useState(Date.now());
const styles = useStyles2(getStyles);
const theme = useTheme2();
const onDelete = (index: number) => {
getTrailStore().removeBookmark(index);
@ -55,62 +56,40 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
setLastDelete(Date.now()); // trigger re-render
};
// If there are no recent trails, don't show home page and create a new trail
if (!getTrailStore().recent.length) {
const trail = newMetricsTrail(getDatasourceForNewTrail());
return <Navigate replace to={getUrlForTrail(trail)} />;
}
return (
<div className={styles.container}>
<Stack direction={'column'} gap={1} alignItems={'start'}>
<Button icon="plus" size="md" variant="primary" onClick={model.onNewMetricsTrail}>
New metric exploration
</Button>
</Stack>
<Stack gap={5}>
<div className={styles.column}>
<Text variant="h4">Recent metrics explorations</Text>
<div className={styles.trailList}>
{getTrailStore().recent.map((trail, index) => {
const resolvedTrail = trail.resolve();
return (
<DataTrailCard
key={(resolvedTrail.state.key || '') + index}
trail={resolvedTrail}
onSelect={() => model.onSelectRecentTrail(resolvedTrail)}
/>
);
})}
<div className={styles.homepageBox}>
<Stack direction="column" alignItems="center">
<div>{theme.isDark ? <DarkModeRocket /> : <LightModeRocket />}</div>
<Text element="h1" textAlignment="center" weight="medium">
<Trans i18nKey="trails.home.start-your-metrics-exploration">Start your metrics exploration!</Trans>
</Text>
<Box>
<Text element="p" textAlignment="center" color="secondary">
<Trans i18nKey="trails.home.subtitle">
Explore your Prometheus-compatible metrics without writing a query.
</Trans>
<TextLink
href="https://grafana.com/docs/grafana/latest/explore/explore-metrics/"
external
style={{ marginLeft: '8px' }}
>
<Trans i18nKey="trails.home.learn-more">Learn more</Trans>
</TextLink>
</Text>
</Box>
<div className={styles.gap24}>
<Button size="lg" variant="primary" onClick={model.onNewMetricsTrail}>
<div className={styles.startButton}>
<Trans i18nKey="trails.home.lets-start">Let&apos;s start!</Trans>
</div>
<Icon name="arrow-right" size="lg" style={{ marginLeft: '8px' }} />
</Button>
</div>
</div>
<div className={styles.verticalLine} />
<div className={styles.column}>
<Text variant="h4">Bookmarks</Text>
<div className={styles.trailList}>
{getTrailStore().bookmarks.length ? (
getTrailStore().bookmarks.map((bookmark, index) => {
return (
<DataTrailCard
key={getBookmarkKey(bookmark)}
bookmark={bookmark}
onSelect={() => model.onSelectBookmark(index)}
onDelete={() => onDelete(index)}
/>
);
})
) : (
<EmptyState variant="call-to-action" message="" image={false}>
<Trans i18nKey="trails.bookmarks.empty-state">
You haven&apos;t created any bookmarks yet. Use the Explore Metrics bookmarks feature to save your
panels as bookmarks.
</Trans>
</EmptyState>
)}
</div>
</div>
</Stack>
</Stack>
</div>
<DataTrailsRecentMetrics model={model} />
<DataTrailsBookmarks model={model} onDelete={onDelete} />
</div>
);
};
@ -123,30 +102,26 @@ function getAppFor(model: SceneObject) {
function getStyles(theme: GrafanaTheme2) {
return {
container: css({
flexGrow: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: theme.spacing(3),
}),
column: css({
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
gap: theme.spacing(2),
height: '100%',
boxSizing: 'border-box', // Ensure padding doesn't cause overflow
}),
newTrail: css({
height: 'auto',
justifyContent: 'center',
fontSize: theme.typography.h5.fontSize,
homepageBox: css({
backgroundColor: theme.colors.background.secondary,
width: '725px',
height: '294px',
padding: '40px 32px',
boxSizing: 'border-box', // Ensure padding doesn't cause overflow
flexShrink: 0,
}),
trailCard: css({}),
trailList: css({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
startButton: css({
fontWeight: theme.typography.fontWeightLight,
}),
verticalLine: css({
borderLeft: `1px solid ${theme.colors.border.weak}`,
gap24: css({
marginTop: theme.spacing(2), // Adds a 24px gap since there is already a 8px gap from the button
}),
};
}

@ -0,0 +1,85 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes';
import { Button, useStyles2, useTheme2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { DataTrailCard } from './DataTrailCard';
import { DataTrailsHome } from './DataTrailsHome';
import { getTrailStore } from './TrailStore/TrailStore';
export function DataTrailsRecentMetrics({ model }: SceneComponentProps<DataTrailsHome>) {
const styles = useStyles2(getStyles);
const recentMetrics = getTrailStore().recent;
const theme = useTheme2();
const [showAll, setShowAll] = useState(false);
const handleToggleShow = () => {
setShowAll(!showAll);
};
if (recentMetrics.length === 0) {
return null;
}
return (
<>
<div className={styles.recentExplorationHeader}>
<div className={styles.header}>
<Trans i18nKey="trails.recent-metrics.or-view-a-recent-exploration">Or view a recent exploration</Trans>
</div>
</div>
<div className={css(styles.trailList, styles.bottomGap24)}>
{getTrailStore()
.recent.slice(0, showAll ? recentMetrics.length : 3)
.map((trail, index) => {
const resolvedTrail = trail.resolve();
return (
<DataTrailCard
key={(resolvedTrail.state.key || '') + index}
trail={resolvedTrail}
onSelect={() => model.onSelectRecentTrail(resolvedTrail)}
/>
);
})}
</div>
{recentMetrics.length > 3 && (
<Button variant="secondary" size="sm" onClick={handleToggleShow} fill={theme.isLight ? 'outline' : 'solid'}>
{showAll ? 'Show less' : 'Show more'}
</Button>
)}
</>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
recentExplorationHeader: css({
marginTop: theme.spacing(6),
marginBottom: theme.spacing(3),
}),
header: css({
color: theme.colors.text.primary,
textAlign: 'center',
/* H4 */
fontFamily: 'Inter',
fontSize: '18px',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '22px' /* 122.222% */,
letterSpacing: '0.045px',
}),
trailList: css({
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: `${theme.spacing(4)}`,
alignItems: 'stretch',
justifyItems: 'center',
}),
bottomGap24: css({
marginBottom: theme.spacing(3),
}),
};
}

@ -36,10 +36,11 @@ export class TrailStore {
private _recent: Array<SceneObjectRef<DataTrail>> = [];
private _bookmarks: DataTrailBookmark[] = [];
private _save: () => void;
private _lastModified: number;
constructor() {
this.load();
this._lastModified = Date.now();
const doSave = () => {
const serializedRecent = this._recent
.slice(0, MAX_RECENT_TRAILS)
@ -47,6 +48,7 @@ export class TrailStore {
localStorage.setItem(RECENT_TRAILS_KEY, JSON.stringify(serializedRecent));
localStorage.setItem(TRAIL_BOOKMARKS_KEY, JSON.stringify(this._bookmarks));
this._lastModified = Date.now();
};
this._save = debounce(doSave, 1000);
@ -168,10 +170,16 @@ export class TrailStore {
return this._recent;
}
// Last updated metric
get lastModified() {
return this._lastModified;
}
load() {
this._recent = this._loadRecentTrailsFromStorage();
this._bookmarks = this._loadBookmarksFromStorage();
this._refreshBookmarkIndexMap();
this._lastModified = Date.now();
}
setRecentTrail(recentTrail: DataTrail) {

@ -0,0 +1,19 @@
export const LightModeRocket = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="73" height="72" viewBox="0 0 73 72" fill="none">
<path
d="M65.3 8.09993C65.3 7.49993 64.7 7.19993 64.1 6.89993C52.7 3.89993 40.4 7.79993 32.9 16.7999L29 21.2999L20.9 19.1999C17.6 17.9999 14.3 19.4999 12.8 22.4999L6.49999 33.5999C6.49999 33.5999 6.49999 33.8999 6.19999 33.8999C5.89999 34.7999 6.49999 35.3999 7.39999 35.6999L17.6 37.7999C16.7 40.4999 15.8 43.1999 15.5 45.8999C15.5 46.4999 15.5 46.7999 15.8 47.0999L24.8 55.7999C25.1 56.0999 25.4 56.0999 26 56.0999C28.7 55.7999 31.7 55.1999 34.4 54.2999L36.5 64.1999C36.5 64.7999 37.4 65.3999 38 65.3999C38.3 65.3999 38.6 65.3999 38.6 65.0999L49.7 58.7999C52.4 57.2999 53.6 53.9999 53 50.9999L50.9 42.2999L55.1 38.3999C64.4 31.4999 68.3 19.4999 65.3 8.09993ZM10.1 33.2999L15.2 23.9999C16.1 22.1999 17.9 21.5999 19.7 22.1999L26.6 23.9999L23.6 27.5999C21.8 29.9999 20 32.3999 18.8 35.0999L10.1 33.2999ZM48.5 56.9999L39.2 62.3999L37.4 53.6999C40.1 52.4999 42.5 50.6999 44.9 48.8999L48.8 45.2999L50.6 52.1999C50.6 53.9999 50 56.0999 48.5 56.9999ZM53.3 36.8999L42.8 46.4999C38.3 50.3999 32.6 52.7999 26.6 53.3999L18.8 45.5999C19.7 39.5999 22.1 33.8999 26 29.3999L30.8 23.9999L31.1 23.6999L35.3 18.8999C41.9 11.0999 52.7 7.49993 62.6 9.59993C64.7 19.7999 61.4 30.2999 53.3 36.8999ZM49.7 16.7999C46.4 16.7999 44 19.4999 44 22.4999C44 25.4999 46.7 28.1999 49.7 28.1999C53 28.1999 55.4 25.4999 55.4 22.4999C55.4 19.4999 53 16.7999 49.7 16.7999ZM49.7 25.4999C48.2 25.4999 47 24.2999 47 22.7999C47 21.2999 48.2 20.0999 49.7 20.0999C51.2 20.0999 52.4 21.2999 52.4 22.7999C52.4 24.2999 51.2 25.4999 49.7 25.4999Z"
fill="#24292E"
fillOpacity="0.75"
/>
</svg>
);
export const DarkModeRocket = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="73" height="72" viewBox="0 0 73 72" fill="none">
<path
d="M65.3 8.09993C65.3 7.49993 64.7 7.19993 64.1 6.89993C52.7 3.89993 40.4 7.79993 32.9 16.7999L29 21.2999L20.9 19.1999C17.6 17.9999 14.3 19.4999 12.8 22.4999L6.49999 33.5999C6.49999 33.5999 6.49999 33.8999 6.19999 33.8999C5.89999 34.7999 6.49999 35.3999 7.39999 35.6999L17.6 37.7999C16.7 40.4999 15.8 43.1999 15.5 45.8999C15.5 46.4999 15.5 46.7999 15.8 47.0999L24.8 55.7999C25.1 56.0999 25.4 56.0999 26 56.0999C28.7 55.7999 31.7 55.1999 34.4 54.2999L36.5 64.1999C36.5 64.7999 37.4 65.3999 38 65.3999C38.3 65.3999 38.6 65.3999 38.6 65.0999L49.7 58.7999C52.4 57.2999 53.6 53.9999 53 50.9999L50.9 42.2999L55.1 38.3999C64.4 31.4999 68.3 19.4999 65.3 8.09993ZM10.1 33.2999L15.2 23.9999C16.1 22.1999 17.9 21.5999 19.7 22.1999L26.6 23.9999L23.6 27.5999C21.8 29.9999 20 32.3999 18.8 35.0999L10.1 33.2999ZM48.5 56.9999L39.2 62.3999L37.4 53.6999C40.1 52.4999 42.5 50.6999 44.9 48.8999L48.8 45.2999L50.6 52.1999C50.6 53.9999 50 56.0999 48.5 56.9999ZM53.3 36.8999L42.8 46.4999C38.3 50.3999 32.6 52.7999 26.6 53.3999L18.8 45.5999C19.7 39.5999 22.1 33.8999 26 29.3999L30.8 23.9999L31.1 23.6999L35.3 18.8999C41.9 11.0999 52.7 7.49993 62.6 9.59993C64.7 19.7999 61.4 30.2999 53.3 36.8999ZM49.7 16.7999C46.4 16.7999 44 19.4999 44 22.4999C44 25.4999 46.7 28.1999 49.7 28.1999C53 28.1999 55.4 25.4999 55.4 22.4999C55.4 19.4999 53 16.7999 49.7 16.7999ZM49.7 25.4999C48.2 25.4999 47 24.2999 47 22.7999C47 21.2999 48.2 20.0999 49.7 20.0999C51.2 20.0999 52.4 21.2999 52.4 22.7999C52.4 24.2999 51.2 25.4999 49.7 25.4999Z"
fill="#CCCCDC"
fillOpacity="0.65"
/>
</svg>
);

@ -2884,7 +2884,18 @@
},
"trails": {
"bookmarks": {
"empty-state": "You haven't created any bookmarks yet. Use the Explore Metrics bookmarks feature to save your panels as bookmarks."
"or-view-bookmarks": "Or view bookmarks"
},
"card": {
"data-source": "Data source: ",
"date-created": "Date created: ",
"metric": "Metric:"
},
"home": {
"learn-more": "Learn more",
"lets-start": "Let's start!",
"start-your-metrics-exploration": "Start your metrics exploration!",
"subtitle": "Explore your Prometheus-compatible metrics without writing a query."
},
"metric-overview": {
"description-label": "Description",
@ -2899,6 +2910,9 @@
"filter-by": "Filter by",
"otel-switch": "This switch enables filtering by OTel resources for OTel native data sources."
},
"recent-metrics": {
"or-view-a-recent-exploration": "Or view a recent exploration"
},
"settings": {
"always-keep-selected-metric-graph-in-view": "Always keep selected metric graph in-view",
"show-previews-of-metric-graphs": "Show previews of metric graphs"

@ -2884,7 +2884,18 @@
},
"trails": {
"bookmarks": {
"empty-state": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny þőőĸmäřĸş yęŧ. Ůşę ŧĥę Ēχpľőřę Męŧřįčş þőőĸmäřĸş ƒęäŧūřę ŧő şävę yőūř päʼnęľş äş þőőĸmäřĸş."
"or-view-bookmarks": "Øř vįęŵ þőőĸmäřĸş"
},
"card": {
"data-source": "Đäŧä şőūřčę: ",
"date-created": "Đäŧę čřęäŧęđ: ",
"metric": "Męŧřįč:"
},
"home": {
"learn-more": "Ŀęäřʼn mőřę",
"lets-start": "Ŀęŧ'ş şŧäřŧ!",
"start-your-metrics-exploration": "Ŝŧäřŧ yőūř męŧřįčş ęχpľőřäŧįőʼn!",
"subtitle": "Ēχpľőřę yőūř Přőmęŧĥęūş-čőmpäŧįþľę męŧřįčş ŵįŧĥőūŧ ŵřįŧįʼnģ ä qūęřy."
},
"metric-overview": {
"description-label": "Đęşčřįpŧįőʼn",
@ -2899,6 +2910,9 @@
"filter-by": "Fįľŧęř þy",
"otel-switch": "Ŧĥįş şŵįŧčĥ ęʼnäþľęş ƒįľŧęřįʼnģ þy ØŦęľ řęşőūřčęş ƒőř ØŦęľ ʼnäŧįvę đäŧä şőūřčęş."
},
"recent-metrics": {
"or-view-a-recent-exploration": "Øř vįęŵ ä řęčęʼnŧ ęχpľőřäŧįőʼn"
},
"settings": {
"always-keep-selected-metric-graph-in-view": "Åľŵäyş ĸęęp şęľęčŧęđ męŧřįč ģřäpĥ įʼn-vįęŵ",
"show-previews-of-metric-graphs": "Ŝĥőŵ přęvįęŵş őƒ męŧřįč ģřäpĥş"

Loading…
Cancel
Save