mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scopes: Open dashboard list when a scope is selected (#94464)
* Open dashboard list when a scope is selected * refactor * test * remove localstorage key * add checks on open/close methods * remove redundant statement * improve dashboards listing
This commit is contained in:
parent
516e0cf7e2
commit
4600bd2e77
@ -34,7 +34,9 @@ export function AppChrome({ children }: Props) {
|
|||||||
const dockedMenuLocalStorageState = store.getBool(DOCKED_LOCAL_STORAGE_KEY, true);
|
const dockedMenuLocalStorageState = store.getBool(DOCKED_LOCAL_STORAGE_KEY, true);
|
||||||
const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
|
const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
|
||||||
const scopesDashboardsState = useScopesDashboardsState();
|
const scopesDashboardsState = useScopesDashboardsState();
|
||||||
const isScopesDashboardsOpen = Boolean(scopesDashboardsState?.isEnabled && scopesDashboardsState?.isPanelOpened);
|
const isScopesDashboardsOpen = Boolean(
|
||||||
|
scopesDashboardsState?.isEnabled && scopesDashboardsState?.isPanelOpened && !scopesDashboardsState?.isReadOnly
|
||||||
|
);
|
||||||
const isSingleTopNav = config.featureToggles.singleTopNav;
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
useMediaQueryChange({
|
useMediaQueryChange({
|
||||||
breakpoint: dockedMenuBreakpoint,
|
breakpoint: dockedMenuBreakpoint,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
import { finalize, from, Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { GrafanaTheme2, ScopeDashboardBinding } from '@grafana/data';
|
import { GrafanaTheme2, ScopeDashboardBinding } from '@grafana/data';
|
||||||
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
|
||||||
@ -10,7 +11,6 @@ import { ScopesDashboardsTree } from './ScopesDashboardsTree';
|
|||||||
import { ScopesDashboardsTreeSearch } from './ScopesDashboardsTreeSearch';
|
import { ScopesDashboardsTreeSearch } from './ScopesDashboardsTreeSearch';
|
||||||
import { ScopesSelectorScene } from './ScopesSelectorScene';
|
import { ScopesSelectorScene } from './ScopesSelectorScene';
|
||||||
import { fetchDashboards } from './api';
|
import { fetchDashboards } from './api';
|
||||||
import { DASHBOARDS_OPENED_KEY } from './const';
|
|
||||||
import { SuggestedDashboardsFoldersMap } from './types';
|
import { SuggestedDashboardsFoldersMap } from './types';
|
||||||
import { filterFolders, getScopeNamesFromSelectedScopes, groupDashboards } from './utils';
|
import { filterFolders, getScopeNamesFromSelectedScopes, groupDashboards } from './utils';
|
||||||
|
|
||||||
@ -26,6 +26,7 @@ export interface ScopesDashboardsSceneState extends SceneObjectState {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isPanelOpened: boolean;
|
isPanelOpened: boolean;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
isReadOnly: boolean;
|
||||||
scopesSelected: boolean;
|
scopesSelected: boolean;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
}
|
}
|
||||||
@ -36,8 +37,9 @@ export const getInitialDashboardsState: () => Omit<ScopesDashboardsSceneState, '
|
|||||||
filteredFolders: {},
|
filteredFolders: {},
|
||||||
forScopeNames: [],
|
forScopeNames: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isPanelOpened: localStorage.getItem(DASHBOARDS_OPENED_KEY) === 'true',
|
isPanelOpened: false,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
|
isReadOnly: false,
|
||||||
scopesSelected: false,
|
scopesSelected: false,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
});
|
});
|
||||||
@ -45,6 +47,8 @@ export const getInitialDashboardsState: () => Omit<ScopesDashboardsSceneState, '
|
|||||||
export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsSceneState> {
|
export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsSceneState> {
|
||||||
static Component = ScopesDashboardsSceneRenderer;
|
static Component = ScopesDashboardsSceneRenderer;
|
||||||
|
|
||||||
|
private dashboardsFetchingSub: Subscription | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
selector: null,
|
selector: null,
|
||||||
@ -52,35 +56,44 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.addActivationHandler(() => {
|
this.addActivationHandler(() => {
|
||||||
if (this.state.isEnabled && this.state.isPanelOpened) {
|
|
||||||
this.fetchDashboards();
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedSelector = this.state.selector?.resolve();
|
const resolvedSelector = this.state.selector?.resolve();
|
||||||
|
|
||||||
|
if (resolvedSelector?.state.scopes.length ?? 0 > 0) {
|
||||||
|
this.fetchDashboards();
|
||||||
|
this.openPanel();
|
||||||
|
}
|
||||||
|
|
||||||
if (resolvedSelector) {
|
if (resolvedSelector) {
|
||||||
this._subs.add(
|
this._subs.add(
|
||||||
resolvedSelector.subscribeToState((newState, prevState) => {
|
resolvedSelector.subscribeToState((newState, prevState) => {
|
||||||
if (
|
const newScopeNames = getScopeNamesFromSelectedScopes(newState.scopes ?? []);
|
||||||
this.state.isEnabled &&
|
const oldScopeNames = getScopeNamesFromSelectedScopes(prevState.scopes ?? []);
|
||||||
this.state.isPanelOpened &&
|
|
||||||
!newState.isLoadingScopes &&
|
if (!isEqual(newScopeNames, oldScopeNames)) {
|
||||||
(prevState.isLoadingScopes || newState.scopes !== prevState.scopes)
|
|
||||||
) {
|
|
||||||
this.fetchDashboards();
|
this.fetchDashboards();
|
||||||
|
|
||||||
|
if (newState.scopes.length > 0) {
|
||||||
|
this.openPanel();
|
||||||
|
} else {
|
||||||
|
this.closePanel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.dashboardsFetchingSub?.unsubscribe();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchDashboards() {
|
public async fetchDashboards() {
|
||||||
const scopeNames = getScopeNamesFromSelectedScopes(this.state.selector?.resolve().state.scopes ?? []);
|
const scopeNames = getScopeNamesFromSelectedScopes(this.state.selector?.resolve().state.scopes ?? []);
|
||||||
|
|
||||||
if (isEqual(scopeNames, this.state.forScopeNames)) {
|
this.dashboardsFetchingSub?.unsubscribe();
|
||||||
return;
|
|
||||||
}
|
this.setState({ forScopeNames: scopeNames });
|
||||||
|
|
||||||
if (scopeNames.length === 0) {
|
if (scopeNames.length === 0) {
|
||||||
return this.setState({
|
return this.setState({
|
||||||
@ -95,7 +108,13 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
|||||||
|
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
|
|
||||||
const dashboards = await fetchDashboards(scopeNames);
|
this.dashboardsFetchingSub = from(fetchDashboards(scopeNames))
|
||||||
|
.pipe(
|
||||||
|
finalize(() => {
|
||||||
|
this.setState({ isLoading: false });
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((dashboards) => {
|
||||||
const folders = groupDashboards(dashboards);
|
const folders = groupDashboards(dashboards);
|
||||||
const filteredFolders = filterFolders(folders, this.state.searchQuery);
|
const filteredFolders = filterFolders(folders, this.state.searchQuery);
|
||||||
|
|
||||||
@ -103,10 +122,12 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
|||||||
dashboards,
|
dashboards,
|
||||||
folders,
|
folders,
|
||||||
filteredFolders,
|
filteredFolders,
|
||||||
forScopeNames: scopeNames,
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
scopesSelected: scopeNames.length > 0,
|
scopesSelected: scopeNames.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.dashboardsFetchingSub?.unsubscribe();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeSearchQuery(searchQuery: string) {
|
public changeSearchQuery(searchQuery: string) {
|
||||||
@ -148,14 +169,19 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
|||||||
}
|
}
|
||||||
|
|
||||||
public openPanel() {
|
public openPanel() {
|
||||||
this.fetchDashboards();
|
if (this.state.isPanelOpened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ isPanelOpened: true });
|
this.setState({ isPanelOpened: true });
|
||||||
localStorage.setItem(DASHBOARDS_OPENED_KEY, JSON.stringify(true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closePanel() {
|
public closePanel() {
|
||||||
|
if (!this.state.isPanelOpened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ isPanelOpened: false });
|
this.setState({ isPanelOpened: false });
|
||||||
localStorage.setItem(DASHBOARDS_OPENED_KEY, JSON.stringify(false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enable() {
|
public enable() {
|
||||||
@ -165,15 +191,23 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
|||||||
public disable() {
|
public disable() {
|
||||||
this.setState({ isEnabled: false });
|
this.setState({ isEnabled: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enterReadOnly() {
|
||||||
|
this.setState({ isReadOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public exitReadOnly() {
|
||||||
|
this.setState({ isReadOnly: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<ScopesDashboardsScene>) {
|
export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<ScopesDashboardsScene>) {
|
||||||
const { dashboards, filteredFolders, isLoading, isPanelOpened, isEnabled, searchQuery, scopesSelected } =
|
const { dashboards, filteredFolders, isLoading, isPanelOpened, isEnabled, isReadOnly, searchQuery, scopesSelected } =
|
||||||
model.useState();
|
model.useState();
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
if (!isEnabled || !isPanelOpened) {
|
if (!isEnabled || !isPanelOpened || isReadOnly) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export const DASHBOARDS_OPENED_KEY = 'grafana.scopes.dashboards.opened';
|
|
@ -12,7 +12,9 @@ import {
|
|||||||
expectDashboardInDocument,
|
expectDashboardInDocument,
|
||||||
expectDashboardLength,
|
expectDashboardLength,
|
||||||
expectDashboardNotInDocument,
|
expectDashboardNotInDocument,
|
||||||
|
expectDashboardsClosed,
|
||||||
expectDashboardSearchValue,
|
expectDashboardSearchValue,
|
||||||
|
expectDashboardsOpen,
|
||||||
expectDashboardsSearch,
|
expectDashboardsSearch,
|
||||||
expectNoDashboardsForFilter,
|
expectNoDashboardsForFilter,
|
||||||
expectNoDashboardsForScope,
|
expectNoDashboardsForScope,
|
||||||
@ -45,9 +47,20 @@ describe('Dashboards list', () => {
|
|||||||
await resetScenes();
|
await resetScenes();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Does not fetch dashboards list when the list is not expanded', async () => {
|
it('Opens container and fetches dashboards list when a scope is selected', async () => {
|
||||||
|
expectDashboardsClosed();
|
||||||
await updateScopes(['mimir']);
|
await updateScopes(['mimir']);
|
||||||
expect(fetchDashboardsSpy).not.toHaveBeenCalled();
|
expectDashboardsOpen();
|
||||||
|
expect(fetchDashboardsSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Closes container when no scopes are selected', async () => {
|
||||||
|
await updateScopes(['mimir']);
|
||||||
|
expectDashboardsOpen();
|
||||||
|
await updateScopes(['mimir', 'loki']);
|
||||||
|
expectDashboardsOpen();
|
||||||
|
await updateScopes([]);
|
||||||
|
expectDashboardsClosed();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Fetches dashboards list when the list is expanded', async () => {
|
it('Fetches dashboards list when the list is expanded', async () => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { getMock, locationReloadSpy } from './mocks';
|
import { getMock, locationReloadSpy } from './mocks';
|
||||||
import {
|
import {
|
||||||
getDashboard,
|
getDashboard,
|
||||||
|
getDashboardsContainer,
|
||||||
getDashboardsExpand,
|
getDashboardsExpand,
|
||||||
getDashboardsSearch,
|
getDashboardsSearch,
|
||||||
getNotFoundForFilter,
|
getNotFoundForFilter,
|
||||||
@ -62,6 +63,7 @@ export const expectResultCloudOpsNotSelected = () => expectRadioNotChecked(getRe
|
|||||||
|
|
||||||
export const expectDashboardsDisabled = () => expectDisabled(getDashboardsExpand);
|
export const expectDashboardsDisabled = () => expectDisabled(getDashboardsExpand);
|
||||||
export const expectDashboardsClosed = () => expectNotInDocument(queryDashboardsContainer);
|
export const expectDashboardsClosed = () => expectNotInDocument(queryDashboardsContainer);
|
||||||
|
export const expectDashboardsOpen = () => expectInDocument(getDashboardsContainer);
|
||||||
export const expectNoDashboardsSearch = () => expectNotInDocument(queryDashboardsSearch);
|
export const expectNoDashboardsSearch = () => expectNotInDocument(queryDashboardsSearch);
|
||||||
export const expectDashboardsSearch = () => expectInDocument(getDashboardsSearch);
|
export const expectDashboardsSearch = () => expectInDocument(getDashboardsSearch);
|
||||||
export const expectNoDashboardsNoScopes = () => expectInDocument(getNotFoundNoScopes);
|
export const expectNoDashboardsNoScopes = () => expectInDocument(getNotFoundNoScopes);
|
||||||
|
@ -12,7 +12,6 @@ import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types';
|
|||||||
import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from '../../instance';
|
import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from '../../instance';
|
||||||
import { getInitialDashboardsState } from '../../internal/ScopesDashboardsScene';
|
import { getInitialDashboardsState } from '../../internal/ScopesDashboardsScene';
|
||||||
import { initialSelectorState } from '../../internal/ScopesSelectorScene';
|
import { initialSelectorState } from '../../internal/ScopesSelectorScene';
|
||||||
import { DASHBOARDS_OPENED_KEY } from '../../internal/const';
|
|
||||||
|
|
||||||
import { clearMocks } from './actions';
|
import { clearMocks } from './actions';
|
||||||
|
|
||||||
@ -160,7 +159,6 @@ export async function resetScenes() {
|
|||||||
await jest.runOnlyPendingTimersAsync();
|
await jest.runOnlyPendingTimersAsync();
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
scopesSelectorScene?.setState(initialSelectorState);
|
scopesSelectorScene?.setState(initialSelectorState);
|
||||||
localStorage.removeItem(DASHBOARDS_OPENED_KEY);
|
|
||||||
scopesDashboardsScene?.setState(getInitialDashboardsState());
|
scopesDashboardsScene?.setState(getInitialDashboardsState());
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ export const getSelectorApply = () => screen.getByTestId(selectors.selector.appl
|
|||||||
export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel);
|
export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel);
|
||||||
|
|
||||||
export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand);
|
export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand);
|
||||||
|
export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container);
|
||||||
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
|
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
|
||||||
export const queryDashboardsSearch = () => screen.queryByTestId(selectors.dashboards.search);
|
export const queryDashboardsSearch = () => screen.queryByTestId(selectors.dashboards.search);
|
||||||
export const getDashboardsSearch = () => screen.getByTestId<HTMLInputElement>(selectors.dashboards.search);
|
export const getDashboardsSearch = () => screen.getByTestId<HTMLInputElement>(selectors.dashboards.search);
|
||||||
|
@ -25,12 +25,12 @@ export function disableScopes() {
|
|||||||
|
|
||||||
export function exitScopesReadOnly() {
|
export function exitScopesReadOnly() {
|
||||||
scopesSelectorScene?.exitReadOnly();
|
scopesSelectorScene?.exitReadOnly();
|
||||||
scopesDashboardsScene?.enable();
|
scopesDashboardsScene?.exitReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enterScopesReadOnly() {
|
export function enterScopesReadOnly() {
|
||||||
scopesSelectorScene?.enterReadOnly();
|
scopesSelectorScene?.enterReadOnly();
|
||||||
scopesDashboardsScene?.disable();
|
scopesDashboardsScene?.enterReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClosestScopesFacade(scene: SceneObject): ScopesFacade | null {
|
export function getClosestScopesFacade(scene: SceneObject): ScopesFacade | null {
|
||||||
|
Loading…
Reference in New Issue
Block a user