diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 3757861f860..6e35e460055 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -782,10 +782,6 @@ export interface FeatureToggles { */ 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` * @default true */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a707f1c115d..885079a5f5a 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1290,13 +1290,6 @@ var ( Owner: grafanaPartnerPluginsSquad, 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+, // since users on v2.9 need it to disable the feature, as it doesn't work for them. diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 419a20f6e47..8ddc448ef52 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -178,7 +178,6 @@ alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false, alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true unifiedStorageSearchUI,experimental,@grafana/search-and-storage,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 k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 2b11a0efe16..09c4d0c9760 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3584,8 +3584,12 @@ { "metadata": { "name": "unifiedHistory", - "resourceVersion": "1764664939750", - "creationTimestamp": "2024-12-13T10:41:18Z" + "resourceVersion": "1762958248290", + "creationTimestamp": "2024-12-13T10:41:18Z", + "deletionTimestamp": "2025-11-13T16:25:53Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-12 14:37:28.29086 +0000 UTC" + } }, "spec": { "description": "Displays the navigation history so the user can navigate back to previous pages", diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index e340e8276b8..131b2f48ea2 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -10,11 +10,8 @@ import { isShallowEqual } from 'app/core/utils/isShallowEqual'; import { KioskMode } from 'app/types/dashboard'; import { RouteDescriptor } from '../../navigation/types'; -import { buildBreadcrumbs } from '../Breadcrumbs/utils'; -import { logDuplicateUnifiedHistoryEntryEvent } from './History/eventsTracking'; import { ReturnToPreviousProps } from './ReturnToPrevious/ReturnToPrevious'; -import { HistoryEntry } from './types'; export interface AppChromeState { chromeless?: boolean; @@ -34,7 +31,6 @@ export interface AppChromeState { export const DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.docked'; export const DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY = 'grafana.navigation.open'; -export const HISTORY_LOCAL_STORAGE_KEY = 'grafana.navigation.history'; export class AppChromeService { searchBarStorageKey = 'SearchBar_Hidden'; @@ -88,8 +84,6 @@ export class AppChromeService { newState.chromeless = newState.kioskMode === KioskMode.Full || this.currentRoute?.chromeless; if (!this.ignoreStateUpdate(newState, current)) { - config.featureToggles.unifiedHistory && - store.setObject(HISTORY_LOCAL_STORAGE_KEY, this.getUpdatedHistory(newState)); this.state.next(newState); } } @@ -118,40 +112,6 @@ export class AppChromeService { window.sessionStorage.removeItem('returnToPrevious'); }; - private getUpdatedHistory(newState: AppChromeState): HistoryEntry[] { - const breadcrumbs = buildBreadcrumbs(newState.sectionNav.node, newState.pageNav, { text: 'Home', url: '/' }); - const newPageNav = newState.pageNav || newState.sectionNav.node; - - let entries = store.getObject(HISTORY_LOCAL_STORAGE_KEY, []); - const clickedHistory = store.getObject('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) { if (isShallowEqual(newState, current)) { return true; diff --git a/public/app/core/components/AppChrome/History/HistoryContainer.tsx b/public/app/core/components/AppChrome/History/HistoryContainer.tsx deleted file mode 100644 index 88cbde02856..00000000000 --- a/public/app/core/components/AppChrome/History/HistoryContainer.tsx +++ /dev/null @@ -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('CLICKING_HISTORY'); - if (clickedHistory) { - store.setObject('CLICKING_HISTORY', false); - return; - } - const history = store.getObject(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 ( - <> - { - onToggleShowHistoryDrawer(); - logUnifiedHistoryDrawerInteractionEvent({ type: 'open' }); - }} - iconOnly - icon="history" - aria-label={t('nav.history-container.drawer-tittle', 'History')} - /> - - {showHistoryDrawer && ( - { - onToggleShowHistoryDrawer(); - logUnifiedHistoryDrawerInteractionEvent({ type: 'close' }); - }} - size="sm" - > - onToggleShowHistoryDrawer(false)} /> - - )} - - ); -} - -const getStyles = (theme: GrafanaTheme2) => { - return { - separator: css({ - [theme.breakpoints.down('sm')]: { - display: 'none', - }, - }), - }; -}; diff --git a/public/app/core/components/AppChrome/History/HistoryWrapper.tsx b/public/app/core/components/AppChrome/History/HistoryWrapper.tsx deleted file mode 100644 index d25c86d6f3d..00000000000 --- a/public/app/core/components/AppChrome/History/HistoryWrapper.tsx +++ /dev/null @@ -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(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 ( - - - {Object.keys(hist).map((entries, date) => { - return ( - - - {entries} - -
- {hist[entries].map((entry, index) => { - return ( - onClose()} - /> - ); - })} -
-
- ); - })} -
- {history.length > numItemsToShow && ( - - - - )} -
- ); -} -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 ( - - - - {views.length > 0 ? ( - setIsExpanded(!isExpanded)} - aria-label={expandedLabel} - className={styles.iconButton} - /> - ) : ( - - )} - - { - store.setObject('CLICKING_HISTORY', true); - onClick(); - logClickUnifiedHistoryEntryEvent({ entryURL: url }); - }} - href={url} - isCompact={true} - className={isSelected ? styles.card : cx(styles.card, styles.cardSelected)} - > - -
- {breadcrumbs.map((breadcrumb, index) => ( - - {breadcrumb.text}{' '} - {index !== breadcrumbs.length - 1 - ? // eslint-disable-next-line @grafana/i18n/no-untranslated-strings - '> ' - : ''} - - ))} -
- - {formatDate(time, { timeStyle: 'short' })} - - {sparklineData && ( - - )} -
-
-
- {isExpanded && ( -
- {views.map((view, index) => { - return ( - { - store.setObject('CLICKING_HISTORY', true); - onClick(); - logClickUnifiedHistoryEntryEvent({ entryURL: view.url, subEntry: 'timeRange' }); - }} - isCompact={true} - className={view.time === selectedViewTime ? undefined : styles.subCard} - > - - {view.name} - {view.description && ( - - {view.description} - - )} - - - ); - })} -
- )} -
-
- ); -} -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}`, - }, - }), - }; -}; diff --git a/public/app/core/components/AppChrome/History/eventsTracking.ts b/public/app/core/components/AppChrome/History/eventsTracking.ts deleted file mode 100644 index 5d49bd1c978..00000000000 --- a/public/app/core/components/AppChrome/History/eventsTracking.ts +++ /dev/null @@ -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); -}; diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index d5179570ef3..23c1624dc05 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -6,7 +6,6 @@ import { Components } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { ScopesContextValue } from '@grafana/runtime'; import { Icon, Stack, ToolbarButton, useStyles2 } from '@grafana/ui'; -import { config } from 'app/core/config'; import { MEGA_MENU_TOGGLE_ID } from 'app/core/constants'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth'; @@ -19,7 +18,6 @@ import { HomeLink } from '../../Branding/Branding'; import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs'; import { buildBreadcrumbs } from '../../Breadcrumbs/utils'; import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem'; -import { HistoryContainer } from '../History/HistoryContainer'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; import { QuickAdd } from '../QuickAdd/QuickAdd'; @@ -60,7 +58,6 @@ export const SingleTopBar = memo(function SingleTopBar({ const profileNode = useSelector((state) => state.navIndex['profile']); const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID]; const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav); - const unifiedHistoryEnabled = config.featureToggles.unifiedHistory; const isSmallScreen = !useMediaQueryMinWidth('sm'); const isLargeScreen = useMediaQueryMinWidth('lg'); const topLevelScopes = !showToolbarLevel && isLargeScreen && scopes?.state.enabled; @@ -96,7 +93,6 @@ export const SingleTopBar = memo(function SingleTopBar({ > - {unifiedHistoryEnabled && !isSmallScreen && } {!isSmallScreen && } diff --git a/public/app/core/components/AppChrome/types.ts b/public/app/core/components/AppChrome/types.ts index 6cf72c936f5..de9183423c3 100644 --- a/public/app/core/components/AppChrome/types.ts +++ b/public/app/core/components/AppChrome/types.ts @@ -4,28 +4,3 @@ export interface ToolbarUpdateProps { pageNav?: NavModelItem; 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; -} diff --git a/public/app/types/events.ts b/public/app/types/events.ts index 5c7e4ba151b..6fb728bb94c 100644 --- a/public/app/types/events.ts +++ b/public/app/types/events.ts @@ -1,6 +1,5 @@ import { AnnotationQuery, BusEventBase, BusEventWithPayload, eventFactory } from '@grafana/data'; import { IconName, ButtonVariant } from '@grafana/ui'; -import { HistoryEntryView } from 'app/core/components/AppChrome/types'; /** * Event Payloads @@ -217,7 +216,3 @@ export class PanelEditEnteredEvent extends BusEventWithPayload { export class PanelEditExitedEvent extends BusEventWithPayload { static type = 'panel-edit-finished'; } - -export class RecordHistoryEntryEvent extends BusEventWithPayload { - static type = 'record-history-entry'; -} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2174ff0adbc..0ee16ef4483 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -10720,18 +10720,6 @@ "help/documentation": "Documentation", "help/keyboard-shortcuts": "Keyboard shortcuts", "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": { "title": "Home" },