mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 12:04:45 +08:00
Compare commits
3 Commits
docs/add-t
...
chore/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0f73a275d | ||
|
|
e13c62433c | ||
|
|
01b7034eaf |
@@ -801,10 +801,6 @@ export interface FeatureToggles {
|
|||||||
*/
|
*/
|
||||||
elasticsearchCrossClusterSearch?: boolean;
|
elasticsearchCrossClusterSearch?: boolean;
|
||||||
/**
|
/**
|
||||||
* Displays the navigation history so the user can navigate back to previous pages
|
|
||||||
*/
|
|
||||||
unifiedHistory?: boolean;
|
|
||||||
/**
|
|
||||||
* Defaults to using the Loki `/labels` API instead of `/series`
|
* Defaults to using the Loki `/labels` API instead of `/series`
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1383,13 +1383,6 @@ var (
|
|||||||
Owner: grafanaPartnerPluginsSquad,
|
Owner: grafanaPartnerPluginsSquad,
|
||||||
Expression: "false",
|
Expression: "false",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "unifiedHistory",
|
|
||||||
Description: "Displays the navigation history so the user can navigate back to previous pages",
|
|
||||||
Stage: FeatureStageExperimental,
|
|
||||||
Owner: grafanaFrontendSearchNavOrganise,
|
|
||||||
FrontendOnly: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// Remove this flag once Loki v4 is released and the min supported version is v3.0+,
|
// Remove this flag once Loki v4 is released and the min supported version is v3.0+,
|
||||||
// since users on v2.9 need it to disable the feature, as it doesn't work for them.
|
// since users on v2.9 need it to disable the feature, as it doesn't work for them.
|
||||||
|
|||||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -180,7 +180,6 @@ alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true
|
|||||||
feedbackButton,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
feedbackButton,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||||
unifiedStorageSearchUI,experimental,@grafana/search-and-storage,false,false,false
|
unifiedStorageSearchUI,experimental,@grafana/search-and-storage,false,false,false
|
||||||
elasticsearchCrossClusterSearch,GA,@grafana/partner-datasources,false,false,false
|
elasticsearchCrossClusterSearch,GA,@grafana/partner-datasources,false,false,false
|
||||||
unifiedHistory,experimental,@grafana/grafana-search-navigate-organise,false,false,true
|
|
||||||
lokiLabelNamesQueryApi,GA,@grafana/observability-logs,false,false,false
|
lokiLabelNamesQueryApi,GA,@grafana/observability-logs,false,false,false
|
||||||
investigationsBackend,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
investigationsBackend,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||||
k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false
|
k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false
|
||||||
|
|||||||
|
4
pkg/services/featuremgmt/toggles_gen.go
generated
4
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -731,10 +731,6 @@ const (
|
|||||||
// Enables cross cluster search in the Elasticsearch data source
|
// Enables cross cluster search in the Elasticsearch data source
|
||||||
FlagElasticsearchCrossClusterSearch = "elasticsearchCrossClusterSearch"
|
FlagElasticsearchCrossClusterSearch = "elasticsearchCrossClusterSearch"
|
||||||
|
|
||||||
// FlagUnifiedHistory
|
|
||||||
// Displays the navigation history so the user can navigate back to previous pages
|
|
||||||
FlagUnifiedHistory = "unifiedHistory"
|
|
||||||
|
|
||||||
// FlagLokiLabelNamesQueryApi
|
// FlagLokiLabelNamesQueryApi
|
||||||
// Defaults to using the Loki `/labels` API instead of `/series`
|
// Defaults to using the Loki `/labels` API instead of `/series`
|
||||||
FlagLokiLabelNamesQueryApi = "lokiLabelNamesQueryApi"
|
FlagLokiLabelNamesQueryApi = "lokiLabelNamesQueryApi"
|
||||||
|
|||||||
1
pkg/services/featuremgmt/toggles_gen.json
generated
1
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -4136,6 +4136,7 @@
|
|||||||
"name": "unifiedHistory",
|
"name": "unifiedHistory",
|
||||||
"resourceVersion": "1762958248290",
|
"resourceVersion": "1762958248290",
|
||||||
"creationTimestamp": "2024-12-13T10:41:18Z",
|
"creationTimestamp": "2024-12-13T10:41:18Z",
|
||||||
|
"deletionTimestamp": "2025-11-13T16:25:53Z",
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC"
|
"grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ import { isShallowEqual } from 'app/core/utils/isShallowEqual';
|
|||||||
import { KioskMode } from 'app/types/dashboard';
|
import { KioskMode } from 'app/types/dashboard';
|
||||||
|
|
||||||
import { RouteDescriptor } from '../../navigation/types';
|
import { RouteDescriptor } from '../../navigation/types';
|
||||||
import { buildBreadcrumbs } from '../Breadcrumbs/utils';
|
|
||||||
|
|
||||||
import { logDuplicateUnifiedHistoryEntryEvent } from './History/eventsTracking';
|
|
||||||
import { ReturnToPreviousProps } from './ReturnToPrevious/ReturnToPrevious';
|
import { ReturnToPreviousProps } from './ReturnToPrevious/ReturnToPrevious';
|
||||||
import { HistoryEntry } from './types';
|
|
||||||
|
|
||||||
export interface AppChromeState {
|
export interface AppChromeState {
|
||||||
chromeless?: boolean;
|
chromeless?: boolean;
|
||||||
@@ -34,7 +31,6 @@ export interface AppChromeState {
|
|||||||
|
|
||||||
export const DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.docked';
|
export const DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.docked';
|
||||||
export const DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY = 'grafana.navigation.open';
|
export const DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY = 'grafana.navigation.open';
|
||||||
export const HISTORY_LOCAL_STORAGE_KEY = 'grafana.navigation.history';
|
|
||||||
|
|
||||||
export class AppChromeService {
|
export class AppChromeService {
|
||||||
searchBarStorageKey = 'SearchBar_Hidden';
|
searchBarStorageKey = 'SearchBar_Hidden';
|
||||||
@@ -88,8 +84,6 @@ export class AppChromeService {
|
|||||||
newState.chromeless = newState.kioskMode === KioskMode.Full || this.currentRoute?.chromeless;
|
newState.chromeless = newState.kioskMode === KioskMode.Full || this.currentRoute?.chromeless;
|
||||||
|
|
||||||
if (!this.ignoreStateUpdate(newState, current)) {
|
if (!this.ignoreStateUpdate(newState, current)) {
|
||||||
config.featureToggles.unifiedHistory &&
|
|
||||||
store.setObject(HISTORY_LOCAL_STORAGE_KEY, this.getUpdatedHistory(newState));
|
|
||||||
this.state.next(newState);
|
this.state.next(newState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,40 +112,6 @@ export class AppChromeService {
|
|||||||
window.sessionStorage.removeItem('returnToPrevious');
|
window.sessionStorage.removeItem('returnToPrevious');
|
||||||
};
|
};
|
||||||
|
|
||||||
private getUpdatedHistory(newState: AppChromeState): HistoryEntry[] {
|
|
||||||
const breadcrumbs = buildBreadcrumbs(newState.sectionNav.node, newState.pageNav, { text: 'Home', url: '/' }, true);
|
|
||||||
const newPageNav = newState.pageNav || newState.sectionNav.node;
|
|
||||||
|
|
||||||
let entries = store.getObject<HistoryEntry[]>(HISTORY_LOCAL_STORAGE_KEY, []);
|
|
||||||
const clickedHistory = store.getObject<boolean>('CLICKING_HISTORY');
|
|
||||||
if (clickedHistory) {
|
|
||||||
store.setObject('CLICKING_HISTORY', false);
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
if (!newPageNav) {
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastEntry = entries[0];
|
|
||||||
const newEntry = { name: newPageNav.text, views: [], breadcrumbs, time: Date.now(), url: window.location.href };
|
|
||||||
const isSamePath = lastEntry && newEntry.url.split('?')[0] === lastEntry.url.split('?')[0];
|
|
||||||
|
|
||||||
// To avoid adding an entry with the same path twice, we always use the latest one
|
|
||||||
if (isSamePath) {
|
|
||||||
entries[0] = newEntry;
|
|
||||||
} else {
|
|
||||||
if (lastEntry && lastEntry.name === newEntry.name) {
|
|
||||||
logDuplicateUnifiedHistoryEntryEvent({
|
|
||||||
entryName: newEntry.name,
|
|
||||||
lastEntryURL: lastEntry.url,
|
|
||||||
newEntryURL: newEntry.url,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
entries = [newEntry, ...entries];
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
private ignoreStateUpdate(newState: AppChromeState, current: AppChromeState) {
|
private ignoreStateUpdate(newState: AppChromeState, current: AppChromeState) {
|
||||||
if (isShallowEqual(newState, current)) {
|
if (isShallowEqual(newState, current)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useToggle } from 'react-use';
|
|
||||||
|
|
||||||
import { GrafanaTheme2, store } from '@grafana/data';
|
|
||||||
import { t } from '@grafana/i18n';
|
|
||||||
import { Drawer, ToolbarButton, useStyles2 } from '@grafana/ui';
|
|
||||||
import { appEvents } from 'app/core/app_events';
|
|
||||||
import { RecordHistoryEntryEvent } from 'app/types/events';
|
|
||||||
|
|
||||||
import { HISTORY_LOCAL_STORAGE_KEY } from '../AppChromeService';
|
|
||||||
import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
|
|
||||||
import { HistoryEntry } from '../types';
|
|
||||||
|
|
||||||
import { HistoryWrapper } from './HistoryWrapper';
|
|
||||||
import { logUnifiedHistoryDrawerInteractionEvent } from './eventsTracking';
|
|
||||||
|
|
||||||
export function HistoryContainer() {
|
|
||||||
const [showHistoryDrawer, onToggleShowHistoryDrawer] = useToggle(false);
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const sub = appEvents.subscribe(RecordHistoryEntryEvent, (ev) => {
|
|
||||||
const clickedHistory = store.getObject<boolean>('CLICKING_HISTORY');
|
|
||||||
if (clickedHistory) {
|
|
||||||
store.setObject('CLICKING_HISTORY', false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const history = store.getObject<HistoryEntry[]>(HISTORY_LOCAL_STORAGE_KEY, []);
|
|
||||||
let lastEntry = history[0];
|
|
||||||
const newUrl = ev.payload.url;
|
|
||||||
const lastUrl = lastEntry.views[0]?.url;
|
|
||||||
if (lastUrl !== newUrl) {
|
|
||||||
lastEntry.views = [
|
|
||||||
{
|
|
||||||
name: ev.payload.name,
|
|
||||||
description: ev.payload.description,
|
|
||||||
url: newUrl,
|
|
||||||
time: Date.now(),
|
|
||||||
},
|
|
||||||
...lastEntry.views,
|
|
||||||
];
|
|
||||||
store.setObject(HISTORY_LOCAL_STORAGE_KEY, [...history]);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
sub.unsubscribe();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ToolbarButton
|
|
||||||
onClick={() => {
|
|
||||||
onToggleShowHistoryDrawer();
|
|
||||||
logUnifiedHistoryDrawerInteractionEvent({ type: 'open' });
|
|
||||||
}}
|
|
||||||
iconOnly
|
|
||||||
icon="history"
|
|
||||||
aria-label={t('nav.history-container.drawer-tittle', 'History')}
|
|
||||||
/>
|
|
||||||
<NavToolbarSeparator className={styles.separator} />
|
|
||||||
{showHistoryDrawer && (
|
|
||||||
<Drawer
|
|
||||||
title={t('nav.history-container.drawer-tittle', 'History')}
|
|
||||||
onClose={() => {
|
|
||||||
onToggleShowHistoryDrawer();
|
|
||||||
logUnifiedHistoryDrawerInteractionEvent({ type: 'close' });
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<HistoryWrapper onClose={() => onToggleShowHistoryDrawer(false)} />
|
|
||||||
</Drawer>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
|
||||||
return {
|
|
||||||
separator: css({
|
|
||||||
[theme.breakpoints.down('sm')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { FieldType, GrafanaTheme2, store } from '@grafana/data';
|
|
||||||
import { t } from '@grafana/i18n';
|
|
||||||
import { Box, Button, Card, Icon, IconButton, Space, Sparkline, Stack, Text, useStyles2, useTheme2 } from '@grafana/ui';
|
|
||||||
import { formatDate } from 'app/core/internationalization/dates';
|
|
||||||
|
|
||||||
import { HISTORY_LOCAL_STORAGE_KEY } from '../AppChromeService';
|
|
||||||
import { HistoryEntry } from '../types';
|
|
||||||
|
|
||||||
import { logClickUnifiedHistoryEntryEvent, logUnifiedHistoryShowMoreEvent } from './eventsTracking';
|
|
||||||
|
|
||||||
export function HistoryWrapper({ onClose }: { onClose: () => void }) {
|
|
||||||
const history = store.getObject<HistoryEntry[]>(HISTORY_LOCAL_STORAGE_KEY, []).filter((entry) => {
|
|
||||||
return moment(entry.time).isAfter(moment().subtract(2, 'day').startOf('day'));
|
|
||||||
});
|
|
||||||
const [numItemsToShow, setNumItemsToShow] = useState(5);
|
|
||||||
|
|
||||||
const selectedTime = history.find((entry) => {
|
|
||||||
return entry.url === window.location.href || entry.views.some((view) => view.url === window.location.href);
|
|
||||||
})?.time;
|
|
||||||
|
|
||||||
const hist = history.slice(0, numItemsToShow).reduce((acc: { [key: string]: HistoryEntry[] }, entry) => {
|
|
||||||
const date = moment(entry.time);
|
|
||||||
let key = '';
|
|
||||||
if (date.isSame(moment(), 'day')) {
|
|
||||||
key = t('nav.history-wrapper.today', 'Today');
|
|
||||||
} else if (date.isSame(moment().subtract(1, 'day'), 'day')) {
|
|
||||||
key = t('nav.history-wrapper.yesterday', 'Yesterday');
|
|
||||||
} else {
|
|
||||||
key = date.format('YYYY-MM-DD');
|
|
||||||
}
|
|
||||||
acc[key] = [...(acc[key] || []), entry];
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
return (
|
|
||||||
<Stack direction="column" alignItems="flex-start">
|
|
||||||
<Box width="100%">
|
|
||||||
{Object.keys(hist).map((entries, date) => {
|
|
||||||
return (
|
|
||||||
<Stack key={date} direction="column" gap={1}>
|
|
||||||
<Box paddingLeft={2}>
|
|
||||||
<Text color="secondary">{entries}</Text>
|
|
||||||
</Box>
|
|
||||||
<div className={styles.timeline}>
|
|
||||||
{hist[entries].map((entry, index) => {
|
|
||||||
return (
|
|
||||||
<HistoryEntryAppView
|
|
||||||
key={index}
|
|
||||||
entry={entry}
|
|
||||||
isSelected={entry.time === selectedTime}
|
|
||||||
onClick={() => onClose()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
{history.length > numItemsToShow && (
|
|
||||||
<Box paddingLeft={2}>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
fill="text"
|
|
||||||
onClick={() => {
|
|
||||||
setNumItemsToShow(numItemsToShow + 5);
|
|
||||||
logUnifiedHistoryShowMoreEvent();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('nav.history-wrapper.show-more', 'Show more')}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
interface ItemProps {
|
|
||||||
entry: HistoryEntry;
|
|
||||||
isSelected: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function HistoryEntryAppView({ entry, isSelected, onClick }: ItemProps) {
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
const theme = useTheme2();
|
|
||||||
const [isExpanded, setIsExpanded] = useState(isSelected && entry.views.length > 0);
|
|
||||||
|
|
||||||
const { breadcrumbs, views, time, url, sparklineData } = entry;
|
|
||||||
const expandedLabel = isExpanded
|
|
||||||
? t('nav.history-wrapper.collapse', 'Collapse')
|
|
||||||
: t('nav.history-wrapper.expand', 'Expand');
|
|
||||||
const entryIconLabel = isExpanded
|
|
||||||
? t('nav.history-wrapper.icon-selected', 'Selected Entry')
|
|
||||||
: t('nav.history-wrapper.icon-unselected', 'Normal Entry');
|
|
||||||
const selectedViewTime =
|
|
||||||
isSelected &&
|
|
||||||
entry.views.find((entry) => {
|
|
||||||
return entry.url === window.location.href;
|
|
||||||
})?.time;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box marginBottom={1}>
|
|
||||||
<Stack direction="column" gap={1}>
|
|
||||||
<Stack alignItems="baseline">
|
|
||||||
{views.length > 0 ? (
|
|
||||||
<IconButton
|
|
||||||
name={isExpanded ? 'angle-down' : 'angle-right'}
|
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
|
||||||
aria-label={expandedLabel}
|
|
||||||
className={styles.iconButton}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Space h={2} />
|
|
||||||
)}
|
|
||||||
<Icon
|
|
||||||
size="sm"
|
|
||||||
name={isSelected ? 'circle-mono' : 'circle'}
|
|
||||||
aria-label={entryIconLabel}
|
|
||||||
className={isExpanded ? styles.iconButtonDot : styles.iconButtonCircle}
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
noMargin
|
|
||||||
onClick={() => {
|
|
||||||
store.setObject('CLICKING_HISTORY', true);
|
|
||||||
onClick();
|
|
||||||
logClickUnifiedHistoryEntryEvent({ entryURL: url });
|
|
||||||
}}
|
|
||||||
href={url}
|
|
||||||
isCompact={true}
|
|
||||||
className={isSelected ? styles.card : cx(styles.card, styles.cardSelected)}
|
|
||||||
>
|
|
||||||
<Stack direction="column">
|
|
||||||
<div>
|
|
||||||
{breadcrumbs.map((breadcrumb, index) => (
|
|
||||||
<Text key={index}>
|
|
||||||
{breadcrumb.text}{' '}
|
|
||||||
{index !== breadcrumbs.length - 1
|
|
||||||
? // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
|
||||||
'> '
|
|
||||||
: ''}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Text variant="bodySmall" color="secondary">
|
|
||||||
{formatDate(time, { timeStyle: 'short' })}
|
|
||||||
</Text>
|
|
||||||
{sparklineData && (
|
|
||||||
<Sparkline
|
|
||||||
theme={theme}
|
|
||||||
width={240}
|
|
||||||
height={40}
|
|
||||||
config={{
|
|
||||||
custom: {
|
|
||||||
fillColor: 'rgba(130, 181, 216, 0.1)',
|
|
||||||
lineColor: '#82B5D8',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
sparkline={{
|
|
||||||
y: {
|
|
||||||
type: FieldType.number,
|
|
||||||
name: 'test',
|
|
||||||
config: {},
|
|
||||||
values: sparklineData.values,
|
|
||||||
state: {
|
|
||||||
range: {
|
|
||||||
...sparklineData.range,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
|
||||||
{isExpanded && (
|
|
||||||
<div className={styles.expanded}>
|
|
||||||
{views.map((view, index) => {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
noMargin
|
|
||||||
href={view.url}
|
|
||||||
onClick={() => {
|
|
||||||
store.setObject('CLICKING_HISTORY', true);
|
|
||||||
onClick();
|
|
||||||
logClickUnifiedHistoryEntryEvent({ entryURL: view.url, subEntry: 'timeRange' });
|
|
||||||
}}
|
|
||||||
isCompact={true}
|
|
||||||
className={view.time === selectedViewTime ? undefined : styles.subCard}
|
|
||||||
>
|
|
||||||
<Stack direction="column" gap={0}>
|
|
||||||
<Text variant="bodySmall">{view.name}</Text>
|
|
||||||
{view.description && (
|
|
||||||
<Text color="secondary" variant="bodySmall">
|
|
||||||
{view.description}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
|
||||||
return {
|
|
||||||
card: css({
|
|
||||||
label: 'card',
|
|
||||||
background: 'none',
|
|
||||||
margin: theme.spacing(0.5, 0),
|
|
||||||
}),
|
|
||||||
cardSelected: css({
|
|
||||||
label: 'card-selected',
|
|
||||||
background: 'none',
|
|
||||||
}),
|
|
||||||
subCard: css({
|
|
||||||
label: 'subcard',
|
|
||||||
background: 'none',
|
|
||||||
margin: 0,
|
|
||||||
}),
|
|
||||||
iconButton: css({
|
|
||||||
label: 'expand-button',
|
|
||||||
margin: 0,
|
|
||||||
}),
|
|
||||||
iconButtonCircle: css({
|
|
||||||
label: 'blue-circle-icon',
|
|
||||||
margin: 0,
|
|
||||||
background: theme.colors.background.primary,
|
|
||||||
fill: theme.colors.primary.main,
|
|
||||||
cursor: 'default',
|
|
||||||
'&:hover:before': {
|
|
||||||
background: 'none',
|
|
||||||
},
|
|
||||||
//Need this to place the icon on the line, otherwise the line will appear on top of the icon
|
|
||||||
zIndex: 0,
|
|
||||||
}),
|
|
||||||
iconButtonDot: css({
|
|
||||||
label: 'blue-dot-icon',
|
|
||||||
margin: 0,
|
|
||||||
color: theme.colors.primary.main,
|
|
||||||
border: theme.shape.radius.circle,
|
|
||||||
cursor: 'default',
|
|
||||||
'&:hover:before': {
|
|
||||||
background: 'none',
|
|
||||||
},
|
|
||||||
//Need this to place the icon on the line, otherwise the line will appear on top of the icon
|
|
||||||
zIndex: 0,
|
|
||||||
}),
|
|
||||||
expanded: css({
|
|
||||||
label: 'expanded',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
marginLeft: theme.spacing(6),
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
position: 'relative',
|
|
||||||
'&:before': {
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
height: '100%',
|
|
||||||
width: '1px',
|
|
||||||
background: theme.colors.border.weak,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
timeline: css({
|
|
||||||
label: 'timeline',
|
|
||||||
position: 'relative',
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
'&:before': {
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
left: theme.spacing(5.75),
|
|
||||||
top: 0,
|
|
||||||
height: '100%',
|
|
||||||
width: '1px',
|
|
||||||
borderLeft: `1px dashed ${theme.colors.border.strong}`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { reportInteraction } from '@grafana/runtime';
|
|
||||||
|
|
||||||
const UNIFIED_HISTORY_ENTRY_CLICKED = 'grafana_unified_history_entry_clicked';
|
|
||||||
const UNIFIED_HISTORY_ENTRY_DUPLICATED = 'grafana_unified_history_duplicated_entry_rendered';
|
|
||||||
const UNIFIED_HISTORY_DRAWER_INTERACTION = 'grafana_unified_history_drawer_interaction';
|
|
||||||
const UNIFIED_HISTORY_DRAWER_SHOW_MORE = 'grafana_unified_history_show_more';
|
|
||||||
|
|
||||||
//Currently just 'timeRange' is supported
|
|
||||||
//in short term, we could add 'templateVariables' for example
|
|
||||||
type subEntryTypes = 'timeRange';
|
|
||||||
|
|
||||||
//Whether the user opens or closes the `HistoryDrawer`
|
|
||||||
type UnifiedHistoryDrawerInteraction = 'open' | 'close';
|
|
||||||
|
|
||||||
interface UnifiedHistoryEntryClicked {
|
|
||||||
//We will also work with the current URL but we will get this from Rudderstack data
|
|
||||||
//URL to return to
|
|
||||||
entryURL: string;
|
|
||||||
//In the case we want to go back to a specific query param, currently just a specific time range
|
|
||||||
subEntry?: subEntryTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UnifiedHistoryEntryDuplicated {
|
|
||||||
// Common name of the history entries
|
|
||||||
entryName: string;
|
|
||||||
// URL of the last entry
|
|
||||||
lastEntryURL: string;
|
|
||||||
// URL of the new entry
|
|
||||||
newEntryURL: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Event triggered when a user clicks on an entry of the `HistoryDrawer`
|
|
||||||
export const logClickUnifiedHistoryEntryEvent = ({ entryURL, subEntry }: UnifiedHistoryEntryClicked) => {
|
|
||||||
reportInteraction(UNIFIED_HISTORY_ENTRY_CLICKED, {
|
|
||||||
entryURL,
|
|
||||||
subEntry,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//Event triggered when history entry name matches the previous one
|
|
||||||
//so we keep track of duplicated entries and be able to analyze them
|
|
||||||
export const logDuplicateUnifiedHistoryEntryEvent = ({
|
|
||||||
entryName,
|
|
||||||
lastEntryURL,
|
|
||||||
newEntryURL,
|
|
||||||
}: UnifiedHistoryEntryDuplicated) => {
|
|
||||||
reportInteraction(UNIFIED_HISTORY_ENTRY_DUPLICATED, {
|
|
||||||
entryName,
|
|
||||||
lastEntryURL,
|
|
||||||
newEntryURL,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//We keep track of users open and closing the drawer
|
|
||||||
export const logUnifiedHistoryDrawerInteractionEvent = ({ type }: { type: UnifiedHistoryDrawerInteraction }) => {
|
|
||||||
reportInteraction(UNIFIED_HISTORY_DRAWER_INTERACTION, {
|
|
||||||
type,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//We keep track of users clicking on the `Show more` button
|
|
||||||
export const logUnifiedHistoryShowMoreEvent = () => {
|
|
||||||
reportInteraction(UNIFIED_HISTORY_DRAWER_SHOW_MORE);
|
|
||||||
};
|
|
||||||
@@ -6,7 +6,6 @@ import { Components } from '@grafana/e2e-selectors';
|
|||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
import { ScopesContextValue } from '@grafana/runtime';
|
import { ScopesContextValue } from '@grafana/runtime';
|
||||||
import { Icon, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
|
import { Icon, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
|
||||||
import { MEGA_MENU_TOGGLE_ID } from 'app/core/constants';
|
import { MEGA_MENU_TOGGLE_ID } from 'app/core/constants';
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
@@ -18,7 +17,6 @@ import { Branding } from '../../Branding/Branding';
|
|||||||
import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs';
|
import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs';
|
||||||
import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
|
import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
|
||||||
import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem';
|
import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem';
|
||||||
import { HistoryContainer } from '../History/HistoryContainer';
|
|
||||||
import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
|
import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
|
||||||
import { QuickAdd } from '../QuickAdd/QuickAdd';
|
import { QuickAdd } from '../QuickAdd/QuickAdd';
|
||||||
|
|
||||||
@@ -58,7 +56,6 @@ export const SingleTopBar = memo(function SingleTopBar({
|
|||||||
const profileNode = useSelector((state) => state.navIndex['profile']);
|
const profileNode = useSelector((state) => state.navIndex['profile']);
|
||||||
const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
|
const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
|
||||||
const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav);
|
const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav);
|
||||||
const unifiedHistoryEnabled = config.featureToggles.unifiedHistory;
|
|
||||||
const isSmallScreen = !useMediaQueryMinWidth('sm');
|
const isSmallScreen = !useMediaQueryMinWidth('sm');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -91,7 +88,6 @@ export const SingleTopBar = memo(function SingleTopBar({
|
|||||||
minWidth={{ xs: 'unset', lg: 0 }}
|
minWidth={{ xs: 'unset', lg: 0 }}
|
||||||
>
|
>
|
||||||
<TopSearchBarCommandPaletteTrigger />
|
<TopSearchBarCommandPaletteTrigger />
|
||||||
{unifiedHistoryEnabled && !isSmallScreen && <HistoryContainer />}
|
|
||||||
{!isSmallScreen && <QuickAdd />}
|
{!isSmallScreen && <QuickAdd />}
|
||||||
<HelpTopBarButton isSmallScreen={isSmallScreen} />
|
<HelpTopBarButton isSmallScreen={isSmallScreen} />
|
||||||
<NavToolbarSeparator />
|
<NavToolbarSeparator />
|
||||||
|
|||||||
@@ -4,28 +4,3 @@ export interface ToolbarUpdateProps {
|
|||||||
pageNav?: NavModelItem;
|
pageNav?: NavModelItem;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryEntryView {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
url: string;
|
|
||||||
time: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HistoryEntrySparkline {
|
|
||||||
values: number[];
|
|
||||||
range: {
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
delta: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HistoryEntry {
|
|
||||||
name: string;
|
|
||||||
time: number;
|
|
||||||
breadcrumbs: NavModelItem[];
|
|
||||||
url: string;
|
|
||||||
views: HistoryEntryView[];
|
|
||||||
sparklineData?: HistoryEntrySparkline;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { AnnotationQuery, BusEventBase, BusEventWithPayload, eventFactory } from '@grafana/data';
|
import { AnnotationQuery, BusEventBase, BusEventWithPayload, eventFactory } from '@grafana/data';
|
||||||
import { IconName, ButtonVariant } from '@grafana/ui';
|
import { IconName, ButtonVariant } from '@grafana/ui';
|
||||||
import { HistoryEntryView } from 'app/core/components/AppChrome/types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event Payloads
|
* Event Payloads
|
||||||
@@ -217,7 +216,3 @@ export class PanelEditEnteredEvent extends BusEventWithPayload<number> {
|
|||||||
export class PanelEditExitedEvent extends BusEventWithPayload<number> {
|
export class PanelEditExitedEvent extends BusEventWithPayload<number> {
|
||||||
static type = 'panel-edit-finished';
|
static type = 'panel-edit-finished';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecordHistoryEntryEvent extends BusEventWithPayload<HistoryEntryView> {
|
|
||||||
static type = 'record-history-entry';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10469,18 +10469,6 @@
|
|||||||
"help/documentation": "Documentation",
|
"help/documentation": "Documentation",
|
||||||
"help/keyboard-shortcuts": "Keyboard shortcuts",
|
"help/keyboard-shortcuts": "Keyboard shortcuts",
|
||||||
"help/support": "Support",
|
"help/support": "Support",
|
||||||
"history-container": {
|
|
||||||
"drawer-tittle": "History"
|
|
||||||
},
|
|
||||||
"history-wrapper": {
|
|
||||||
"collapse": "Collapse",
|
|
||||||
"expand": "Expand",
|
|
||||||
"icon-selected": "Selected Entry",
|
|
||||||
"icon-unselected": "Normal Entry",
|
|
||||||
"show-more": "Show more",
|
|
||||||
"today": "Today",
|
|
||||||
"yesterday": "Yesterday"
|
|
||||||
},
|
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Home"
|
"title": "Home"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user