Scopes: move scope dashboard list toggle to canvas/page (#115131)

* Scopes: move scope dashboard list toggle to canvas/page

* Updates

* Updates

* Fix test

* Update
This commit is contained in:
Torkel Ödegaard
2025-12-15 14:08:04 +01:00
committed by GitHub
parent ca0c09cb73
commit 00a6e1781f
6 changed files with 78 additions and 37 deletions
@@ -19,6 +19,7 @@ import {
} from '@grafana/scenes';
import { Box, Button, useStyles2 } from '@grafana/ui';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { ContextualNavigationPaneToggle } from 'app/features/scopes/dashboards/ContextualNavigationPaneToggle';
import { PanelEditControls } from '../panel-edit/PanelEditControls';
import { getDashboardSceneFor } from '../utils/utils';
@@ -172,6 +173,9 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
</div>
)}
</div>
{config.featureToggles.scopeFilters && !editPanel && (
<ContextualNavigationPaneToggle className={styles.contextualNavToggle} hideWhenOpen={true} />
)}
{!hideVariableControls && (
<>
<VariableControls dashboard={dashboard} />
@@ -287,5 +291,9 @@ function getStyles(theme: GrafanaTheme2) {
flexWrap: 'wrap',
marginLeft: 'auto',
}),
contextualNavToggle: css({
display: 'inline-flex',
margin: theme.spacing(0, 1, 1, 0),
}),
};
}
@@ -0,0 +1,46 @@
import { t } from '@grafana/i18n';
import { useScopes } from '@grafana/runtime';
import { ToolbarButton } from '@grafana/ui';
import { useScopesServices } from '../ScopesContextProvider';
interface Props {
className?: string;
hideWhenOpen?: boolean;
}
export function ContextualNavigationPaneToggle({ className, hideWhenOpen }: Props) {
const scopes = useScopes();
const services = useScopesServices();
if (!scopes || !services) {
return;
}
const { scopesDashboardsService } = services;
const { readOnly, drawerOpened } = scopes.state;
if (hideWhenOpen && drawerOpened) {
return null;
}
const dashboardsIconLabel = readOnly
? t('scopes.dashboards.toggle.disabled', 'Suggested dashboards list is disabled due to read only mode')
: drawerOpened
? t('scopes.dashboards.toggle.collapse', 'Collapse suggested dashboards list')
: t('scopes.dashboards.toggle.expand', 'Expand suggested dashboards list');
return (
<div className={className}>
<ToolbarButton
icon="web-section-alt"
aria-label={dashboardsIconLabel}
tooltip={dashboardsIconLabel}
data-testid="scopes-dashboards-expand"
disabled={readOnly}
onClick={scopesDashboardsService.toggleDrawer}
variant={'canvas'}
/>
</div>
);
}
@@ -1,4 +1,4 @@
import { css, cx } from '@emotion/css';
import { css } from '@emotion/css';
import { useObservable } from 'react-use';
import { Observable } from 'rxjs';
@@ -34,22 +34,22 @@ export function ScopesDashboards() {
if (!loading) {
if (forScopeNames.length === 0) {
return (
<div
className={cx(styles.container, styles.noResultsContainer)}
data-testid="scopes-dashboards-notFoundNoScopes"
>
<Trans i18nKey="scopes.dashboards.noResultsNoScopes">No scopes selected</Trans>
<div className={styles.container} data-testid="scopes-dashboards-container">
<ScopesDashboardsTreeSearch disabled={loading} query={searchQuery} onChange={changeSearchQuery} />
<div className={styles.noResultsContainer} data-testid="scopes-dashboards-notFoundNoScopes">
<Trans i18nKey="scopes.dashboards.noResultsNoScopes">No scopes selected</Trans>
</div>
</div>
);
} else if (dashboards.length === 0 && scopeNavigations.length === 0) {
return (
<div
className={cx(styles.container, styles.noResultsContainer)}
data-testid="scopes-dashboards-notFoundForScope"
>
<Trans i18nKey="scopes.dashboards.noResultsForScopes">
No dashboards or links found for the selected scopes
</Trans>
<div className={styles.container} data-testid="scopes-dashboards-container">
<div className={styles.noResultsContainer} data-testid="scopes-dashboards-notFoundForScope">
<Trans i18nKey="scopes.dashboards.noResultsForScopes">
No dashboards or links found for the selected scopes
</Trans>
</div>
</div>
);
}
@@ -94,13 +94,14 @@ export function ScopesDashboards() {
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
backgroundColor: theme.colors.background.primary,
backgroundColor: theme.colors.background.canvas,
borderRight: `1px solid ${theme.colors.border.weak}`,
display: 'flex',
flexDirection: 'column',
height: '100%',
gap: theme.spacing(1),
padding: theme.spacing(2),
padding: theme.spacing(0, 2),
margin: theme.spacing(2, 0),
width: theme.spacing(37.5),
}),
noResultsContainer: css({
@@ -6,6 +6,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { FilterInput, useStyles2 } from '@grafana/ui';
import { ContextualNavigationPaneToggle } from './ContextualNavigationPaneToggle';
export interface ScopesDashboardsTreeSearchProps {
disabled: boolean;
query: string;
@@ -42,6 +44,7 @@ export function ScopesDashboardsTreeSearch({ disabled, query, onChange }: Scopes
data-testid="scopes-dashboards-search"
onChange={(value) => setInputState({ value, dirty: true })}
/>
<ContextualNavigationPaneToggle />
</div>
);
}
@@ -49,6 +52,8 @@ export function ScopesDashboardsTreeSearch({ disabled, query, onChange }: Scopes
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
display: 'flex',
gap: theme.spacing(1),
flex: '0 1 auto',
}),
};
@@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { useScopes } from '@grafana/runtime';
import { Button, Drawer, ErrorBoundary, ErrorWithStack, IconButton, Spinner, Text, useStyles2 } from '@grafana/ui';
import { Button, Drawer, ErrorBoundary, ErrorWithStack, Spinner, Text, useStyles2 } from '@grafana/ui';
import { getModKey } from 'app/core/utils/browser';
import { useScopesServices } from '../ScopesContextProvider';
@@ -54,8 +54,8 @@ export const ScopesSelector = () => {
tree,
scopes: scopesMap,
} = selectorServiceState;
const { scopesService, scopesSelectorService, scopesDashboardsService } = services;
const { readOnly, drawerOpened, loading } = scopes.state;
const { scopesService, scopesSelectorService } = services;
const { readOnly, loading } = scopes.state;
const {
open,
removeAllScopes,
@@ -70,24 +70,8 @@ export const ScopesSelector = () => {
const recentScopes = getRecentScopes();
const dashboardsIconLabel = readOnly
? t('scopes.dashboards.toggle.disabled', 'Suggested dashboards list is disabled due to read only mode')
: drawerOpened
? t('scopes.dashboards.toggle.collapse', 'Collapse suggested dashboards list')
: t('scopes.dashboards.toggle.expand', 'Expand suggested dashboards list');
return (
<>
<IconButton
name="web-section-alt"
className={styles.dashboards}
aria-label={dashboardsIconLabel}
tooltip={dashboardsIconLabel}
data-testid="scopes-dashboards-expand"
disabled={readOnly}
onClick={scopesDashboardsService.toggleDrawer}
/>
<ScopesInput
nodes={nodes}
scopes={scopesMap}
@@ -26,7 +26,6 @@ import {
expectNoDashboardsForFilter,
expectNoDashboardsForScope,
expectNoDashboardsNoScopes,
expectNoDashboardsSearch,
} from './utils/assertions';
import {
alternativeDashboardWithRootFolder,
@@ -304,14 +303,12 @@ describe('Dashboards list', () => {
it('Shows a proper message when no scopes are selected', async () => {
await toggleDashboards();
expectNoDashboardsNoScopes();
expectNoDashboardsSearch();
});
it('Does not show the input when there are no dashboards found for scope', async () => {
await updateScopes(scopesService, ['cloud']);
await toggleDashboards();
expectNoDashboardsForScope();
expectNoDashboardsSearch();
});
it('Shows the input and a message when there are no dashboards found for filter', async () => {