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'; } from '@grafana/scenes';
import { Box, Button, useStyles2 } from '@grafana/ui'; import { Box, Button, useStyles2 } from '@grafana/ui';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { ContextualNavigationPaneToggle } from 'app/features/scopes/dashboards/ContextualNavigationPaneToggle';
import { PanelEditControls } from '../panel-edit/PanelEditControls'; import { PanelEditControls } from '../panel-edit/PanelEditControls';
import { getDashboardSceneFor } from '../utils/utils'; import { getDashboardSceneFor } from '../utils/utils';
@@ -172,6 +173,9 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
</div> </div>
)} )}
</div> </div>
{config.featureToggles.scopeFilters && !editPanel && (
<ContextualNavigationPaneToggle className={styles.contextualNavToggle} hideWhenOpen={true} />
)}
{!hideVariableControls && ( {!hideVariableControls && (
<> <>
<VariableControls dashboard={dashboard} /> <VariableControls dashboard={dashboard} />
@@ -287,5 +291,9 @@ function getStyles(theme: GrafanaTheme2) {
flexWrap: 'wrap', flexWrap: 'wrap',
marginLeft: 'auto', 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 { useObservable } from 'react-use';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -34,22 +34,22 @@ export function ScopesDashboards() {
if (!loading) { if (!loading) {
if (forScopeNames.length === 0) { if (forScopeNames.length === 0) {
return ( return (
<div <div className={styles.container} data-testid="scopes-dashboards-container">
className={cx(styles.container, styles.noResultsContainer)} <ScopesDashboardsTreeSearch disabled={loading} query={searchQuery} onChange={changeSearchQuery} />
data-testid="scopes-dashboards-notFoundNoScopes"
> <div className={styles.noResultsContainer} data-testid="scopes-dashboards-notFoundNoScopes">
<Trans i18nKey="scopes.dashboards.noResultsNoScopes">No scopes selected</Trans> <Trans i18nKey="scopes.dashboards.noResultsNoScopes">No scopes selected</Trans>
</div>
</div> </div>
); );
} else if (dashboards.length === 0 && scopeNavigations.length === 0) { } else if (dashboards.length === 0 && scopeNavigations.length === 0) {
return ( return (
<div <div className={styles.container} data-testid="scopes-dashboards-container">
className={cx(styles.container, styles.noResultsContainer)} <div className={styles.noResultsContainer} data-testid="scopes-dashboards-notFoundForScope">
data-testid="scopes-dashboards-notFoundForScope" <Trans i18nKey="scopes.dashboards.noResultsForScopes">
> No dashboards or links found for the selected scopes
<Trans i18nKey="scopes.dashboards.noResultsForScopes"> </Trans>
No dashboards or links found for the selected scopes </div>
</Trans>
</div> </div>
); );
} }
@@ -94,13 +94,14 @@ export function ScopesDashboards() {
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
container: css({ container: css({
backgroundColor: theme.colors.background.primary, backgroundColor: theme.colors.background.canvas,
borderRight: `1px solid ${theme.colors.border.weak}`, borderRight: `1px solid ${theme.colors.border.weak}`,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100%', height: '100%',
gap: theme.spacing(1), gap: theme.spacing(1),
padding: theme.spacing(2), padding: theme.spacing(0, 2),
margin: theme.spacing(2, 0),
width: theme.spacing(37.5), width: theme.spacing(37.5),
}), }),
noResultsContainer: css({ noResultsContainer: css({
@@ -6,6 +6,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n'; import { t } from '@grafana/i18n';
import { FilterInput, useStyles2 } from '@grafana/ui'; import { FilterInput, useStyles2 } from '@grafana/ui';
import { ContextualNavigationPaneToggle } from './ContextualNavigationPaneToggle';
export interface ScopesDashboardsTreeSearchProps { export interface ScopesDashboardsTreeSearchProps {
disabled: boolean; disabled: boolean;
query: string; query: string;
@@ -42,6 +44,7 @@ export function ScopesDashboardsTreeSearch({ disabled, query, onChange }: Scopes
data-testid="scopes-dashboards-search" data-testid="scopes-dashboards-search"
onChange={(value) => setInputState({ value, dirty: true })} onChange={(value) => setInputState({ value, dirty: true })}
/> />
<ContextualNavigationPaneToggle />
</div> </div>
); );
} }
@@ -49,6 +52,8 @@ export function ScopesDashboardsTreeSearch({ disabled, query, onChange }: Scopes
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
container: css({ container: css({
display: 'flex',
gap: theme.spacing(1),
flex: '0 1 auto', flex: '0 1 auto',
}), }),
}; };
@@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n'; import { Trans, t } from '@grafana/i18n';
import { useScopes } from '@grafana/runtime'; 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 { getModKey } from 'app/core/utils/browser';
import { useScopesServices } from '../ScopesContextProvider'; import { useScopesServices } from '../ScopesContextProvider';
@@ -54,8 +54,8 @@ export const ScopesSelector = () => {
tree, tree,
scopes: scopesMap, scopes: scopesMap,
} = selectorServiceState; } = selectorServiceState;
const { scopesService, scopesSelectorService, scopesDashboardsService } = services; const { scopesService, scopesSelectorService } = services;
const { readOnly, drawerOpened, loading } = scopes.state; const { readOnly, loading } = scopes.state;
const { const {
open, open,
removeAllScopes, removeAllScopes,
@@ -70,24 +70,8 @@ export const ScopesSelector = () => {
const recentScopes = getRecentScopes(); 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 ( 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 <ScopesInput
nodes={nodes} nodes={nodes}
scopes={scopesMap} scopes={scopesMap}
@@ -26,7 +26,6 @@ import {
expectNoDashboardsForFilter, expectNoDashboardsForFilter,
expectNoDashboardsForScope, expectNoDashboardsForScope,
expectNoDashboardsNoScopes, expectNoDashboardsNoScopes,
expectNoDashboardsSearch,
} from './utils/assertions'; } from './utils/assertions';
import { import {
alternativeDashboardWithRootFolder, alternativeDashboardWithRootFolder,
@@ -304,14 +303,12 @@ describe('Dashboards list', () => {
it('Shows a proper message when no scopes are selected', async () => { it('Shows a proper message when no scopes are selected', async () => {
await toggleDashboards(); await toggleDashboards();
expectNoDashboardsNoScopes(); expectNoDashboardsNoScopes();
expectNoDashboardsSearch();
}); });
it('Does not show the input when there are no dashboards found for scope', async () => { it('Does not show the input when there are no dashboards found for scope', async () => {
await updateScopes(scopesService, ['cloud']); await updateScopes(scopesService, ['cloud']);
await toggleDashboards(); await toggleDashboards();
expectNoDashboardsForScope(); expectNoDashboardsForScope();
expectNoDashboardsSearch();
}); });
it('Shows the input and a message when there are no dashboards found for filter', async () => { it('Shows the input and a message when there are no dashboards found for filter', async () => {