mirror of
https://github.com/grafana/grafana.git
synced 2024-11-24 09:50:29 -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 menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
|
||||
const scopesDashboardsState = useScopesDashboardsState();
|
||||
const isScopesDashboardsOpen = Boolean(scopesDashboardsState?.isEnabled && scopesDashboardsState?.isPanelOpened);
|
||||
const isScopesDashboardsOpen = Boolean(
|
||||
scopesDashboardsState?.isEnabled && scopesDashboardsState?.isPanelOpened && !scopesDashboardsState?.isReadOnly
|
||||
);
|
||||
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||
useMediaQueryChange({
|
||||
breakpoint: dockedMenuBreakpoint,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { isEqual } from 'lodash';
|
||||
import { finalize, from, Subscription } from 'rxjs';
|
||||
|
||||
import { GrafanaTheme2, ScopeDashboardBinding } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
|
||||
@ -10,7 +11,6 @@ import { ScopesDashboardsTree } from './ScopesDashboardsTree';
|
||||
import { ScopesDashboardsTreeSearch } from './ScopesDashboardsTreeSearch';
|
||||
import { ScopesSelectorScene } from './ScopesSelectorScene';
|
||||
import { fetchDashboards } from './api';
|
||||
import { DASHBOARDS_OPENED_KEY } from './const';
|
||||
import { SuggestedDashboardsFoldersMap } from './types';
|
||||
import { filterFolders, getScopeNamesFromSelectedScopes, groupDashboards } from './utils';
|
||||
|
||||
@ -26,6 +26,7 @@ export interface ScopesDashboardsSceneState extends SceneObjectState {
|
||||
isLoading: boolean;
|
||||
isPanelOpened: boolean;
|
||||
isEnabled: boolean;
|
||||
isReadOnly: boolean;
|
||||
scopesSelected: boolean;
|
||||
searchQuery: string;
|
||||
}
|
||||
@ -36,8 +37,9 @@ export const getInitialDashboardsState: () => Omit<ScopesDashboardsSceneState, '
|
||||
filteredFolders: {},
|
||||
forScopeNames: [],
|
||||
isLoading: false,
|
||||
isPanelOpened: localStorage.getItem(DASHBOARDS_OPENED_KEY) === 'true',
|
||||
isPanelOpened: false,
|
||||
isEnabled: false,
|
||||
isReadOnly: false,
|
||||
scopesSelected: false,
|
||||
searchQuery: '',
|
||||
});
|
||||
@ -45,6 +47,8 @@ export const getInitialDashboardsState: () => Omit<ScopesDashboardsSceneState, '
|
||||
export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsSceneState> {
|
||||
static Component = ScopesDashboardsSceneRenderer;
|
||||
|
||||
private dashboardsFetchingSub: Subscription | undefined;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
selector: null,
|
||||
@ -52,35 +56,44 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
||||
});
|
||||
|
||||
this.addActivationHandler(() => {
|
||||
if (this.state.isEnabled && this.state.isPanelOpened) {
|
||||
this.fetchDashboards();
|
||||
}
|
||||
|
||||
const resolvedSelector = this.state.selector?.resolve();
|
||||
|
||||
if (resolvedSelector?.state.scopes.length ?? 0 > 0) {
|
||||
this.fetchDashboards();
|
||||
this.openPanel();
|
||||
}
|
||||
|
||||
if (resolvedSelector) {
|
||||
this._subs.add(
|
||||
resolvedSelector.subscribeToState((newState, prevState) => {
|
||||
if (
|
||||
this.state.isEnabled &&
|
||||
this.state.isPanelOpened &&
|
||||
!newState.isLoadingScopes &&
|
||||
(prevState.isLoadingScopes || newState.scopes !== prevState.scopes)
|
||||
) {
|
||||
const newScopeNames = getScopeNamesFromSelectedScopes(newState.scopes ?? []);
|
||||
const oldScopeNames = getScopeNamesFromSelectedScopes(prevState.scopes ?? []);
|
||||
|
||||
if (!isEqual(newScopeNames, oldScopeNames)) {
|
||||
this.fetchDashboards();
|
||||
|
||||
if (newState.scopes.length > 0) {
|
||||
this.openPanel();
|
||||
} else {
|
||||
this.closePanel();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.dashboardsFetchingSub?.unsubscribe();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async fetchDashboards() {
|
||||
const scopeNames = getScopeNamesFromSelectedScopes(this.state.selector?.resolve().state.scopes ?? []);
|
||||
|
||||
if (isEqual(scopeNames, this.state.forScopeNames)) {
|
||||
return;
|
||||
}
|
||||
this.dashboardsFetchingSub?.unsubscribe();
|
||||
|
||||
this.setState({ forScopeNames: scopeNames });
|
||||
|
||||
if (scopeNames.length === 0) {
|
||||
return this.setState({
|
||||
@ -95,18 +108,26 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
const dashboards = await fetchDashboards(scopeNames);
|
||||
const folders = groupDashboards(dashboards);
|
||||
const filteredFolders = filterFolders(folders, this.state.searchQuery);
|
||||
this.dashboardsFetchingSub = from(fetchDashboards(scopeNames))
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.setState({ isLoading: false });
|
||||
})
|
||||
)
|
||||
.subscribe((dashboards) => {
|
||||
const folders = groupDashboards(dashboards);
|
||||
const filteredFolders = filterFolders(folders, this.state.searchQuery);
|
||||
|
||||
this.setState({
|
||||
dashboards,
|
||||
folders,
|
||||
filteredFolders,
|
||||
forScopeNames: scopeNames,
|
||||
isLoading: false,
|
||||
scopesSelected: scopeNames.length > 0,
|
||||
});
|
||||
this.setState({
|
||||
dashboards,
|
||||
folders,
|
||||
filteredFolders,
|
||||
isLoading: false,
|
||||
scopesSelected: scopeNames.length > 0,
|
||||
});
|
||||
|
||||
this.dashboardsFetchingSub?.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
public changeSearchQuery(searchQuery: string) {
|
||||
@ -148,14 +169,19 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
||||
}
|
||||
|
||||
public openPanel() {
|
||||
this.fetchDashboards();
|
||||
if (this.state.isPanelOpened) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isPanelOpened: true });
|
||||
localStorage.setItem(DASHBOARDS_OPENED_KEY, JSON.stringify(true));
|
||||
}
|
||||
|
||||
public closePanel() {
|
||||
if (!this.state.isPanelOpened) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isPanelOpened: false });
|
||||
localStorage.setItem(DASHBOARDS_OPENED_KEY, JSON.stringify(false));
|
||||
}
|
||||
|
||||
public enable() {
|
||||
@ -165,15 +191,23 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
|
||||
public disable() {
|
||||
this.setState({ isEnabled: false });
|
||||
}
|
||||
|
||||
public enterReadOnly() {
|
||||
this.setState({ isReadOnly: true });
|
||||
}
|
||||
|
||||
public exitReadOnly() {
|
||||
this.setState({ isReadOnly: false });
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!isEnabled || !isPanelOpened) {
|
||||
if (!isEnabled || !isPanelOpened || isReadOnly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
export const DASHBOARDS_OPENED_KEY = 'grafana.scopes.dashboards.opened';
|
@ -12,7 +12,9 @@ import {
|
||||
expectDashboardInDocument,
|
||||
expectDashboardLength,
|
||||
expectDashboardNotInDocument,
|
||||
expectDashboardsClosed,
|
||||
expectDashboardSearchValue,
|
||||
expectDashboardsOpen,
|
||||
expectDashboardsSearch,
|
||||
expectNoDashboardsForFilter,
|
||||
expectNoDashboardsForScope,
|
||||
@ -45,9 +47,20 @@ describe('Dashboards list', () => {
|
||||
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']);
|
||||
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 () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { getMock, locationReloadSpy } from './mocks';
|
||||
import {
|
||||
getDashboard,
|
||||
getDashboardsContainer,
|
||||
getDashboardsExpand,
|
||||
getDashboardsSearch,
|
||||
getNotFoundForFilter,
|
||||
@ -62,6 +63,7 @@ export const expectResultCloudOpsNotSelected = () => expectRadioNotChecked(getRe
|
||||
|
||||
export const expectDashboardsDisabled = () => expectDisabled(getDashboardsExpand);
|
||||
export const expectDashboardsClosed = () => expectNotInDocument(queryDashboardsContainer);
|
||||
export const expectDashboardsOpen = () => expectInDocument(getDashboardsContainer);
|
||||
export const expectNoDashboardsSearch = () => expectNotInDocument(queryDashboardsSearch);
|
||||
export const expectDashboardsSearch = () => expectInDocument(getDashboardsSearch);
|
||||
export const expectNoDashboardsNoScopes = () => expectInDocument(getNotFoundNoScopes);
|
||||
|
@ -12,7 +12,6 @@ import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types';
|
||||
import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from '../../instance';
|
||||
import { getInitialDashboardsState } from '../../internal/ScopesDashboardsScene';
|
||||
import { initialSelectorState } from '../../internal/ScopesSelectorScene';
|
||||
import { DASHBOARDS_OPENED_KEY } from '../../internal/const';
|
||||
|
||||
import { clearMocks } from './actions';
|
||||
|
||||
@ -160,7 +159,6 @@ export async function resetScenes() {
|
||||
await jest.runOnlyPendingTimersAsync();
|
||||
jest.useRealTimers();
|
||||
scopesSelectorScene?.setState(initialSelectorState);
|
||||
localStorage.removeItem(DASHBOARDS_OPENED_KEY);
|
||||
scopesDashboardsScene?.setState(getInitialDashboardsState());
|
||||
cleanup();
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ export const getSelectorApply = () => screen.getByTestId(selectors.selector.appl
|
||||
export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel);
|
||||
|
||||
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 queryDashboardsSearch = () => screen.queryByTestId(selectors.dashboards.search);
|
||||
export const getDashboardsSearch = () => screen.getByTestId<HTMLInputElement>(selectors.dashboards.search);
|
||||
|
@ -25,12 +25,12 @@ export function disableScopes() {
|
||||
|
||||
export function exitScopesReadOnly() {
|
||||
scopesSelectorScene?.exitReadOnly();
|
||||
scopesDashboardsScene?.enable();
|
||||
scopesDashboardsScene?.exitReadOnly();
|
||||
}
|
||||
|
||||
export function enterScopesReadOnly() {
|
||||
scopesSelectorScene?.enterReadOnly();
|
||||
scopesDashboardsScene?.disable();
|
||||
scopesDashboardsScene?.enterReadOnly();
|
||||
}
|
||||
|
||||
export function getClosestScopesFacade(scene: SceneObject): ScopesFacade | null {
|
||||
|
Loading…
Reference in New Issue
Block a user