Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a6112c71f | |||
| c65a738812 |
@@ -35,7 +35,6 @@ export interface PanelContext {
|
||||
onSeriesColorChange?: (label: string, color: string) => void;
|
||||
|
||||
onToggleSeriesVisibility?: (label: string, mode: SeriesVisibilityChangeMode) => void;
|
||||
onResetAllSeriesVisibility?: () => void;
|
||||
|
||||
canAddAnnotations?: () => boolean;
|
||||
canEditAnnotations?: (dashboardUID?: string) => boolean;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
export enum SeriesVisibilityChangeMode {
|
||||
ToggleSelection = 'select',
|
||||
AppendToSelection = 'append',
|
||||
Show = 'show',
|
||||
}
|
||||
|
||||
export type OnSelectRangeCallback = (selections: RangeSelection2D[]) => void;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DataHoverClearEvent, DataHoverEvent } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { LegendDisplayMode } from '@grafana/schema';
|
||||
|
||||
import { Button } from '../Button/Button';
|
||||
import { SeriesVisibilityChangeMode, usePanelContext } from '../PanelChrome';
|
||||
|
||||
import { VizLegendList } from './VizLegendList';
|
||||
@@ -35,7 +32,7 @@ export function VizLegend<T>({
|
||||
readonly,
|
||||
isSortable,
|
||||
}: LegendProps<T>) {
|
||||
const { eventBus, onResetAllSeriesVisibility, onToggleSeriesVisibility, onToggleLegendSort } = usePanelContext();
|
||||
const { eventBus, onToggleSeriesVisibility, onToggleLegendSort } = usePanelContext();
|
||||
|
||||
const onMouseOver = useCallback(
|
||||
(
|
||||
@@ -108,26 +105,6 @@ export function VizLegend<T>({
|
||||
[className, placement, onMouseOver, onMouseOut, onLegendLabelClick, itemRenderer, readonly]
|
||||
);
|
||||
|
||||
if (onResetAllSeriesVisibility && items.every((item) => (item.fieldName ?? item.label) && item.disabled)) {
|
||||
return (
|
||||
<div className={css({ paddingTop: '0.5em' })}>
|
||||
<Button
|
||||
size="sm"
|
||||
tooltip={t(
|
||||
'grafana-ui.viz-legend.show-all-series-tooltip',
|
||||
'Currently loaded series are hidden by previous selection. Click to show all series.'
|
||||
)}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
onResetAllSeriesVisibility();
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="grafana-ui.viz-legend.show-all-series">Show all series</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (displayMode) {
|
||||
case LegendDisplayMode.Table:
|
||||
return (
|
||||
|
||||
@@ -53,7 +53,7 @@ import { loadSnapshotData } from '../utils/loadSnapshotData';
|
||||
|
||||
import { PanelHeaderMenuWrapper } from './PanelHeader/PanelHeaderMenuWrapper';
|
||||
import { PanelLoadTimeMonitor } from './PanelLoadTimeMonitor';
|
||||
import { isHideSeriesOverride, seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
||||
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
|
||||
import { liveTimer } from './liveTimer';
|
||||
import { PanelOptionsLogger } from './panelOptionsLogger';
|
||||
|
||||
@@ -106,7 +106,6 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
sync: this.getSync,
|
||||
onSeriesColorChange: this.onSeriesColorChange,
|
||||
onToggleSeriesVisibility: this.onSeriesVisibilityChange,
|
||||
onResetAllSeriesVisibility: this.onSeriesVisibilityReset,
|
||||
onAnnotationCreate: this.onAnnotationCreate,
|
||||
onAnnotationUpdate: this.onAnnotationUpdate,
|
||||
onAnnotationDelete: this.onAnnotationDelete,
|
||||
@@ -172,13 +171,6 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
onSeriesVisibilityReset = () => {
|
||||
this.onFieldConfigChange({
|
||||
...this.props.panel.fieldConfig,
|
||||
overrides: this.props.panel.fieldConfig.overrides.filter((rule) => !isHideSeriesOverride(rule)),
|
||||
});
|
||||
};
|
||||
|
||||
onToggleLegendSort = (sortKey: string) => {
|
||||
const legendOptions: VizLegendOptions = this.props.panel.options.legend;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { SeriesVisibilityChangeMode } from '@grafana/ui';
|
||||
|
||||
const displayOverrideRef = 'hideSeriesFrom';
|
||||
export const isHideSeriesOverride = isSystemOverrideWithRef(displayOverrideRef);
|
||||
const isHideSeriesOverride = isSystemOverrideWithRef(displayOverrideRef);
|
||||
|
||||
export function seriesVisibilityConfigFactory(
|
||||
label: string,
|
||||
|
||||
@@ -34,10 +34,7 @@ import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/times
|
||||
import { Options as TimeSeriesOptions } from 'app/plugins/panel/timeseries/panelcfg.gen';
|
||||
import { ExploreGraphStyle } from 'app/types/explore';
|
||||
|
||||
import {
|
||||
isHideSeriesOverride,
|
||||
seriesVisibilityConfigFactory,
|
||||
} from '../../dashboard/dashgrid/SeriesVisibilityConfigFactory';
|
||||
import { seriesVisibilityConfigFactory } from '../../dashboard/dashgrid/SeriesVisibilityConfigFactory';
|
||||
import { useExploreDataLinkPostProcessor } from '../hooks/useExploreDataLinkPostProcessor';
|
||||
|
||||
import { applyGraphStyle, applyThresholdsConfig } from './exploreGraphStyleUtils';
|
||||
@@ -174,12 +171,6 @@ export function ExploreGraph({
|
||||
onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) {
|
||||
setFieldConfig(seriesVisibilityConfigFactory(label, mode, fieldConfig, data));
|
||||
},
|
||||
onResetAllSeriesVisibility: () => {
|
||||
setFieldConfig({
|
||||
...fieldConfig,
|
||||
overrides: fieldConfig.overrides.filter((rule) => !isHideSeriesOverride(rule)),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function toggleLegend(name: string | undefined, mode: SeriesVisibilityChangeMode) {
|
||||
|
||||
@@ -34,8 +34,16 @@ interface ScopesDashboardsServiceState {
|
||||
navScopePath?: string[];
|
||||
}
|
||||
|
||||
export interface NavigationUrlInfo {
|
||||
nearestSubscope?: string;
|
||||
subscopePath?: string[];
|
||||
}
|
||||
|
||||
export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsServiceState> {
|
||||
private locationSubscription: Subscription | undefined;
|
||||
// Index mapping navigation URLs to their subscope info for O(1) lookup
|
||||
private navigationUrlIndex: Map<string, NavigationUrlInfo> = new Map();
|
||||
|
||||
constructor(private apiClient: ScopesApiClient) {
|
||||
super({
|
||||
drawerOpened: false,
|
||||
@@ -199,6 +207,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
}
|
||||
|
||||
let subScopeFolders: SuggestedNavigationsFoldersMap | undefined;
|
||||
let filteredItems: Array<ScopeDashboardBinding | ScopeNavigation> = [];
|
||||
|
||||
try {
|
||||
// Fetch navigations for this subScope
|
||||
@@ -210,7 +219,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
|
||||
// Filter out items that have a subScope matching any subScope already in the path
|
||||
// This prevents infinite loops when a subScope returns items with the same subScope
|
||||
const filteredItems = filterItemsWithSubScopesInPath(subScopeItems, path, subScopeName, this.state.folders);
|
||||
filteredItems = filterItemsWithSubScopesInPath(subScopeItems, path, subScopeName, this.state.folders);
|
||||
|
||||
// Group the items and add them to the subScope folder
|
||||
subScopeFolders = this.groupSuggestedItems(filteredItems);
|
||||
@@ -254,10 +263,14 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
...rootSubScopeFolder.suggestedNavigations,
|
||||
};
|
||||
|
||||
// Build the subscope path for these items and add to the index
|
||||
const subscopePath = this.getSubscopePathFromFolderPath(path, folders);
|
||||
this.addNavigationsToIndex(filteredItems, subscopePath);
|
||||
|
||||
this.updateState({ folders, filteredFolders });
|
||||
|
||||
// Preload children for any newly added folders with preLoadSubScopeChildren
|
||||
this.preloadSubScopeChildren(rootSubScopeFolder.folders, path);
|
||||
await this.preloadSubScopeChildren(rootSubScopeFolder.folders, path);
|
||||
} else {
|
||||
this.updateState({ folders, filteredFolders });
|
||||
}
|
||||
@@ -290,6 +303,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
}
|
||||
|
||||
if (forScopeNames.length === 0) {
|
||||
this.navigationUrlIndex.clear();
|
||||
this.updateState({
|
||||
dashboards: [],
|
||||
filteredFolders: {},
|
||||
@@ -314,6 +328,10 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
const folders = this.groupSuggestedItems(res);
|
||||
const filteredFolders = this.filterFolders(folders, this.state.searchQuery);
|
||||
|
||||
// Build navigation URL index directly from the response (no subscope info for top-level)
|
||||
this.navigationUrlIndex.clear();
|
||||
this.addNavigationsToIndex(res, []);
|
||||
|
||||
this.updateState({
|
||||
scopeNavigations: res,
|
||||
filteredFolders,
|
||||
@@ -323,7 +341,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
});
|
||||
|
||||
// Preload children for folders with preLoadSubScopeChildren set
|
||||
this.preloadSubScopeChildren(folders[''].folders, ['']);
|
||||
await this.preloadSubScopeChildren(folders[''].folders, ['']);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -334,13 +352,17 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
* @param foldersToCheck - The folders to check for preLoadSubScopeChildren
|
||||
* @param basePath - The path to prepend when building the full path for each folder
|
||||
*/
|
||||
private preloadSubScopeChildren = (foldersToCheck: SuggestedNavigationsFoldersMap, basePath: string[]) => {
|
||||
private preloadSubScopeChildren = async (foldersToCheck: SuggestedNavigationsFoldersMap, basePath: string[]) => {
|
||||
const preloadPromises: Array<Promise<void>> = [];
|
||||
|
||||
for (const [folderKey, folder] of Object.entries(foldersToCheck)) {
|
||||
if (folder.preLoadSubScopeChildren && folder.subScopeName) {
|
||||
const path = [...basePath, folderKey];
|
||||
this.fetchSubScopeItems(path, folder.subScopeName);
|
||||
preloadPromises.push(this.fetchSubScopeItems(path, folder.subScopeName));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(preloadPromises);
|
||||
};
|
||||
|
||||
public groupSuggestedItems = (
|
||||
@@ -503,6 +525,78 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
|
||||
};
|
||||
|
||||
public toggleDrawer = () => this.updateState({ drawerOpened: !this.state.drawerOpened });
|
||||
|
||||
/**
|
||||
* Adds navigation URLs to the index with their subscope info.
|
||||
* Called when processing navigation items from API responses.
|
||||
*/
|
||||
private addNavigationsToIndex = (items: Array<ScopeDashboardBinding | ScopeNavigation>, subscopePath: string[]) => {
|
||||
const info: NavigationUrlInfo =
|
||||
subscopePath.length > 0
|
||||
? {
|
||||
nearestSubscope: subscopePath[subscopePath.length - 1],
|
||||
subscopePath: [...subscopePath],
|
||||
}
|
||||
: {};
|
||||
|
||||
for (const item of items) {
|
||||
if ('url' in item.spec && typeof item.spec.url === 'string') {
|
||||
this.navigationUrlIndex.set(item.spec.url, info);
|
||||
} else if ('dashboard' in item.spec) {
|
||||
// Dashboard items have URL format /d/{dashboardId}
|
||||
this.navigationUrlIndex.set('/d/' + item.spec.dashboard, info);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the subscope path by traversing the folder path and collecting subScopeName values.
|
||||
*/
|
||||
private getSubscopePathFromFolderPath = (folderPath: string[], folders: SuggestedNavigationsFoldersMap): string[] => {
|
||||
const subscopePath: string[] = [];
|
||||
let currentLevel = folders;
|
||||
|
||||
for (const key of folderPath) {
|
||||
const folder = currentLevel[key];
|
||||
if (folder?.subScopeName) {
|
||||
subscopePath.push(folder.subScopeName);
|
||||
}
|
||||
if (folder?.folders) {
|
||||
currentLevel = folder.folders;
|
||||
}
|
||||
}
|
||||
|
||||
return subscopePath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds navigation info for the given path using the URL index for O(1) lookup.
|
||||
* Returns subscope information if the navigation was found within a subscope folder.
|
||||
*/
|
||||
public findNavigationInfo = (
|
||||
currentPath: string
|
||||
): { found: boolean; nearestSubscope?: string; subscopePath?: string[] } => {
|
||||
// Check each indexed URL using isCurrentPath for proper path matching
|
||||
for (const [url, info] of this.navigationUrlIndex) {
|
||||
if (isCurrentPath(currentPath, url)) {
|
||||
return {
|
||||
found: true,
|
||||
nearestSubscope: info.nearestSubscope,
|
||||
subscopePath: info.subscopePath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { found: false };
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given path matches any navigation URL in the entire folder structure,
|
||||
* including navigations loaded by subscopes.
|
||||
*/
|
||||
public isPathInNavigations = (currentPath: string): boolean => {
|
||||
return this.findNavigationInfo(currentPath).found;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -371,19 +371,35 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
};
|
||||
|
||||
// Redirect to the scope node's redirect URL if it exists, otherwise redirect to the first scope navigation.
|
||||
// If the current path is within a subscope's navigations, apply the subscope and set navigation scope.
|
||||
private redirectAfterApply = (scopeNode: ScopeNode | undefined) => {
|
||||
// Check if we are currently on an active scope navigation
|
||||
// Check if we are currently on an active scope navigation (including those loaded by subscopes)
|
||||
const currentPath = locationService.getLocation().pathname;
|
||||
const activeScopeNavigation = this.dashboardsService.state.scopeNavigations.find((s) => {
|
||||
if (!('url' in s.spec) || typeof s.spec.url !== 'string') {
|
||||
return false;
|
||||
const navigationInfo = this.dashboardsService.findNavigationInfo(currentPath);
|
||||
|
||||
// If we're on a navigation within a subscope:
|
||||
// - Set the navigation scope to the current applied scope (so drawer keeps showing original items)
|
||||
// - Apply the nearest subscope as the new scope
|
||||
// - Set the navScopePath to expand the folders
|
||||
if (navigationInfo.found && navigationInfo.nearestSubscope && navigationInfo.subscopePath) {
|
||||
const currentAppliedScopeId = this.state.appliedScopes[0]?.scopeId;
|
||||
if (currentAppliedScopeId) {
|
||||
// Set navigation scope to current scope, then apply the subscope
|
||||
this.dashboardsService.setNavigationScope(currentAppliedScopeId, undefined, navigationInfo.subscopePath);
|
||||
// Apply the nearest subscope as the new applied scope (redirectOnApply=false to avoid recursion)
|
||||
this.changeScopes([navigationInfo.nearestSubscope], undefined, undefined, false);
|
||||
}
|
||||
return isCurrentPath(currentPath, s.spec.url);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're on a top-level navigation, no redirect needed
|
||||
if (navigationInfo.found) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only redirect to redirectPath if we are not currently on an active scope navigation
|
||||
if (
|
||||
!activeScopeNavigation &&
|
||||
!navigationInfo.found &&
|
||||
scopeNode &&
|
||||
scopeNode.spec.redirectPath &&
|
||||
typeof scopeNode.spec.redirectPath === 'string' &&
|
||||
@@ -395,7 +411,7 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
}
|
||||
|
||||
// Redirect to first scopeNavigation if current URL isn't a scopeNavigation
|
||||
if (!activeScopeNavigation && this.dashboardsService.state.scopeNavigations.length > 0) {
|
||||
if (!navigationInfo.found && this.dashboardsService.state.scopeNavigations.length > 0) {
|
||||
// Redirect to the first available scopeNavigation
|
||||
const firstScopeNavigation = this.dashboardsService.state.scopeNavigations[0];
|
||||
|
||||
|
||||
@@ -9314,9 +9314,7 @@
|
||||
"remove-button": "Remove {{children}}"
|
||||
},
|
||||
"viz-legend": {
|
||||
"right-axis-indicator": "(right y-axis)",
|
||||
"show-all-series": "Show all series",
|
||||
"show-all-series-tooltip": "Currently loaded series are hidden by previous selection. Click to show all series."
|
||||
"right-axis-indicator": "(right y-axis)"
|
||||
},
|
||||
"viz-tooltip": {
|
||||
"actions-confirmation-input-placeholder": "Are you sure you want to {{ actionTitle }}?",
|
||||
|
||||
Reference in New Issue
Block a user