-
Datasource: {getDataSourceName(dsValue)}
+
+
+
+
+ Metric:
- {createdAt && (
-
- Created: {dateTimeFormat(createdAt, { format: 'LL' })}
-
+ {getMetricName(metric)}
+
+
+ {filters.map((f) => (
+
+ {f.key}:
+ {truncateValue(f.key, f.value, 44)}
+
+ ))}
+
+
+
+ Data source:
+
+
{dsValue && getDataSourceName(dsValue)}
+
+
+ {onDelete && (
+
+
+
)}
-
-
- {onDelete && (
-
-
-
- )}
-
+
+
+
+
+ Date created:
+
+
{createdAt && dateTimeFormat(createdAt, { format: 'YYYY-MM-DD' })}
+
+
);
}
-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',
diff --git a/public/app/features/trails/DataTrailsApp.tsx b/public/app/features/trails/DataTrailsApp.tsx
index a3057fa4b66..25115c4e213 100644
--- a/public/app/features/trails/DataTrailsApp.tsx
+++ b/public/app/features/trails/DataTrailsApp.tsx
@@ -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
{
}
+ // Returning null to prevent default behavior which renders a header
+ renderTitle={() => null}
subTitle=""
>
@@ -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]);
diff --git a/public/app/features/trails/DataTrailsHome.tsx b/public/app/features/trails/DataTrailsHome.tsx
index 56ca3b83186..a7c6e9d3859 100644
--- a/public/app/features/trails/DataTrailsHome.tsx
+++ b/public/app/features/trails/DataTrailsHome.tsx
@@ -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 {
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 {
static Component = ({ model }: SceneComponentProps) => {
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 {
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 ;
- }
-
return (
-
-
-
-
-
-
-
Recent metrics explorations
-
- {getTrailStore().recent.map((trail, index) => {
- const resolvedTrail = trail.resolve();
- return (
-
model.onSelectRecentTrail(resolvedTrail)}
- />
- );
- })}
+
+
+ {theme.isDark ? : }
+
+ Start your metrics exploration!
+
+
+
+
+ Explore your Prometheus-compatible metrics without writing a query.
+
+
+ Learn more
+
+
+
+
-
-
-
-
Bookmarks
-
- {getTrailStore().bookmarks.length ? (
- getTrailStore().bookmarks.map((bookmark, index) => {
- return (
- model.onSelectBookmark(index)}
- onDelete={() => onDelete(index)}
- />
- );
- })
- ) : (
-
-
- You haven't created any bookmarks yet. Use the Explore Metrics bookmarks feature to save your
- panels as bookmarks.
-
-
- )}
-
-
-
+
+
+
+
);
};
@@ -123,30 +102,26 @@ function getAppFor(model: SceneObject) {
function getStyles(theme: GrafanaTheme2) {
return {
container: css({
- flexGrow: 1,
display: 'flex',
- flexDirection: 'column',
- gap: theme.spacing(3),
- }),
- column: css({
- display: 'flex',
- flexGrow: 1,
- flexDirection: 'column',
- gap: theme.spacing(2),
- }),
- newTrail: css({
- height: 'auto',
justifyContent: 'center',
- fontSize: theme.typography.h5.fontSize,
- }),
- trailCard: css({}),
- trailList: css({
- display: 'flex',
+ alignItems: 'center',
flexDirection: 'column',
- gap: theme.spacing(2),
+ height: '100%',
+ boxSizing: 'border-box', // Ensure padding doesn't cause overflow
}),
- verticalLine: css({
- borderLeft: `1px solid ${theme.colors.border.weak}`,
+ 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,
+ }),
+ startButton: css({
+ fontWeight: theme.typography.fontWeightLight,
+ }),
+ gap24: css({
+ marginTop: theme.spacing(2), // Adds a 24px gap since there is already a 8px gap from the button
}),
};
}
diff --git a/public/app/features/trails/DataTrailsRecentMetrics.tsx b/public/app/features/trails/DataTrailsRecentMetrics.tsx
new file mode 100644
index 00000000000..4a62124b669
--- /dev/null
+++ b/public/app/features/trails/DataTrailsRecentMetrics.tsx
@@ -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) {
+ 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 (
+ <>
+
+
+ Or view a recent exploration
+
+
+
+ {getTrailStore()
+ .recent.slice(0, showAll ? recentMetrics.length : 3)
+ .map((trail, index) => {
+ const resolvedTrail = trail.resolve();
+ return (
+ model.onSelectRecentTrail(resolvedTrail)}
+ />
+ );
+ })}
+
+ {recentMetrics.length > 3 && (
+
+ )}
+ >
+ );
+}
+
+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),
+ }),
+ };
+}
diff --git a/public/app/features/trails/TrailStore/TrailStore.ts b/public/app/features/trails/TrailStore/TrailStore.ts
index a0373bab200..db9864c6fd6 100644
--- a/public/app/features/trails/TrailStore/TrailStore.ts
+++ b/public/app/features/trails/TrailStore/TrailStore.ts
@@ -36,10 +36,11 @@ export class TrailStore {
private _recent: Array> = [];
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) {
diff --git a/public/app/features/trails/assets/rockets.tsx b/public/app/features/trails/assets/rockets.tsx
new file mode 100644
index 00000000000..5de1b750afa
--- /dev/null
+++ b/public/app/features/trails/assets/rockets.tsx
@@ -0,0 +1,19 @@
+export const LightModeRocket = () => (
+
+);
+
+export const DarkModeRocket = () => (
+
+);
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 138865c1b4f..28c3e75f798 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -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"
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index d048607a940..a2b8a080fd9 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -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ĥş"