Scopes: Move URL values from selector to facade (#96842)

This commit is contained in:
Bogdan Matei
2024-11-26 16:43:36 +02:00
committed by GitHub
parent 76f052e8de
commit 13a4bec96b
5 changed files with 70 additions and 90 deletions
@@ -1,6 +1,12 @@
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import {
SceneObjectBase,
SceneObjectState,
SceneObjectUrlSyncConfig,
SceneObjectUrlValues,
SceneObjectWithUrlSync,
} from '@grafana/scenes';
import { scopesSelectorScene } from './instance'; import { scopesSelectorScene } from './instance';
import { disableScopes, enableScopes, enterScopesReadOnly, exitScopesReadOnly, getSelectedScopes } from './utils'; import { disableScopes, enableScopes, enterScopesReadOnly, exitScopesReadOnly, getSelectedScopes } from './utils';
@@ -8,21 +14,47 @@ import { disableScopes, enableScopes, enterScopesReadOnly, exitScopesReadOnly, g
interface ScopesFacadeState extends SceneObjectState { interface ScopesFacadeState extends SceneObjectState {
// A callback that will be executed when new scopes are set // A callback that will be executed when new scopes are set
handler?: (facade: ScopesFacade) => void; handler?: (facade: ScopesFacade) => void;
// The render count is a workaround to force the URL sync manager to update the URL with the latest scopes
// Basically it starts at 0, and it is increased with every scopes value update
renderCount?: number;
} }
export class ScopesFacade extends SceneObjectBase<ScopesFacadeState> { export class ScopesFacade extends SceneObjectBase<ScopesFacadeState> implements SceneObjectWithUrlSync {
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['scopes'] });
public constructor(state: ScopesFacadeState) { public constructor(state: ScopesFacadeState) {
super(state); super({
...state,
renderCount: 0,
});
this.addActivationHandler(this._activationHandler); this.addActivationHandler(this._activationHandler);
} }
public getUrlState() {
return {
scopes: this.value.map(({ metadata }) => metadata.name),
};
}
public updateFromUrl(values: SceneObjectUrlValues) {
if (!values.scopes && !scopesSelectorScene?.state.isEnabled) {
return;
}
let scopeNames = values.scopes ?? [];
scopeNames = Array.isArray(scopeNames) ? scopeNames : [scopeNames];
scopesSelectorScene?.updateScopes(scopeNames.map((scopeName) => ({ scopeName, path: [] })));
}
private _activationHandler = () => { private _activationHandler = () => {
this.enable(); this.enable();
this._subs.add( this._subs.add(
scopesSelectorScene?.subscribeToState((newState, prevState) => { scopesSelectorScene?.subscribeToState((newState, prevState) => {
if (!newState.isLoadingScopes && (prevState.isLoadingScopes || !isEqual(newState.scopes, prevState.scopes))) { if (!newState.isLoadingScopes && (prevState.isLoadingScopes || !isEqual(newState.scopes, prevState.scopes))) {
this.setState({ renderCount: (this.state.renderCount ?? 0) + 1 });
this.state.handler?.(this); this.state.handler?.(this);
} }
}) })
-4
View File
@@ -1,5 +1,4 @@
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { UrlSyncManager } from '@grafana/scenes';
import { ScopesDashboardsScene } from './internal/ScopesDashboardsScene'; import { ScopesDashboardsScene } from './internal/ScopesDashboardsScene';
import { ScopesSelectorScene } from './internal/ScopesSelectorScene'; import { ScopesSelectorScene } from './internal/ScopesSelectorScene';
@@ -14,8 +13,5 @@ export function initializeScopes() {
scopesSelectorScene.setState({ dashboards: scopesDashboardsScene.getRef() }); scopesSelectorScene.setState({ dashboards: scopesDashboardsScene.getRef() });
scopesDashboardsScene.setState({ selector: scopesSelectorScene.getRef() }); scopesDashboardsScene.setState({ selector: scopesSelectorScene.getRef() });
const urlSyncManager = new UrlSyncManager();
urlSyncManager.initSync(scopesSelectorScene!);
} }
} }
@@ -12,7 +12,7 @@ import { ScopesDashboardsTreeSearch } from './ScopesDashboardsTreeSearch';
import { ScopesSelectorScene } from './ScopesSelectorScene'; import { ScopesSelectorScene } from './ScopesSelectorScene';
import { fetchDashboards } from './api'; import { fetchDashboards } from './api';
import { SuggestedDashboardsFoldersMap } from './types'; import { SuggestedDashboardsFoldersMap } from './types';
import { filterFolders, getScopeNamesFromSelectedScopes, groupDashboards } from './utils'; import { filterFolders, groupDashboards } from './utils';
export interface ScopesDashboardsSceneState extends SceneObjectState { export interface ScopesDashboardsSceneState extends SceneObjectState {
selector: SceneObjectRef<ScopesSelectorScene> | null; selector: SceneObjectRef<ScopesSelectorScene> | null;
@@ -27,7 +27,6 @@ export interface ScopesDashboardsSceneState extends SceneObjectState {
isPanelOpened: boolean; isPanelOpened: boolean;
isEnabled: boolean; isEnabled: boolean;
isReadOnly: boolean; isReadOnly: boolean;
scopesSelected: boolean;
searchQuery: string; searchQuery: string;
} }
@@ -40,7 +39,6 @@ export const getInitialDashboardsState: () => Omit<ScopesDashboardsSceneState, '
isPanelOpened: false, isPanelOpened: false,
isEnabled: false, isEnabled: false,
isReadOnly: false, isReadOnly: false,
scopesSelected: false,
searchQuery: '', searchQuery: '',
}); });
@@ -56,40 +54,16 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
}); });
this.addActivationHandler(() => { this.addActivationHandler(() => {
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) => {
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 () => { return () => {
this.dashboardsFetchingSub?.unsubscribe(); this.dashboardsFetchingSub?.unsubscribe();
}; };
}); });
} }
public async fetchDashboards() { public async fetchDashboards(scopeNames: string[]) {
const scopeNames = getScopeNamesFromSelectedScopes(this.state.selector?.resolve().state.scopes ?? []); if (isEqual(this.state.forScopeNames, scopeNames)) {
return;
}
this.dashboardsFetchingSub?.unsubscribe(); this.dashboardsFetchingSub?.unsubscribe();
@@ -102,7 +76,7 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
filteredFolders: {}, filteredFolders: {},
forScopeNames: [], forScopeNames: [],
isLoading: false, isLoading: false,
scopesSelected: false, isPanelOpened: false,
}); });
} }
@@ -123,7 +97,7 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
folders, folders,
filteredFolders, filteredFolders,
isLoading: false, isLoading: false,
scopesSelected: scopeNames.length > 0, isPanelOpened: scopeNames.length > 0,
}); });
this.dashboardsFetchingSub?.unsubscribe(); this.dashboardsFetchingSub?.unsubscribe();
@@ -202,7 +176,7 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
} }
export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<ScopesDashboardsScene>) { export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<ScopesDashboardsScene>) {
const { dashboards, filteredFolders, isLoading, isPanelOpened, isEnabled, isReadOnly, searchQuery, scopesSelected } = const { dashboards, filteredFolders, forScopeNames, isLoading, isPanelOpened, isEnabled, isReadOnly, searchQuery } =
model.useState(); model.useState();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@@ -212,7 +186,7 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<Sco
} }
if (!isLoading) { if (!isLoading) {
if (!scopesSelected) { if (forScopeNames.length === 0) {
return ( return (
<div <div
className={cx(styles.container, styles.noResultsContainer)} className={cx(styles.container, styles.noResultsContainer)}
@@ -4,15 +4,7 @@ import { finalize, from, Subscription } from 'rxjs';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { import { SceneComponentProps, SceneObjectBase, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
SceneComponentProps,
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
SceneObjectUrlSyncConfig,
SceneObjectUrlValues,
SceneObjectWithUrlSync,
} from '@grafana/scenes';
import { Button, Drawer, IconButton, Spinner, useStyles2 } from '@grafana/ui'; import { Button, Drawer, IconButton, Spinner, useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { t, Trans } from 'app/core/internationalization'; import { t, Trans } from 'app/core/internationalization';
@@ -22,12 +14,7 @@ import { ScopesInput } from './ScopesInput';
import { ScopesTree } from './ScopesTree'; import { ScopesTree } from './ScopesTree';
import { fetchNodes, fetchScope, fetchSelectedScopes } from './api'; import { fetchNodes, fetchScope, fetchSelectedScopes } from './api';
import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types'; import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
import { import { getBasicScope, getScopesAndTreeScopesWithPaths, getTreeScopesFromSelectedScopes } from './utils';
getBasicScope,
getScopeNamesFromSelectedScopes,
getScopesAndTreeScopesWithPaths,
getTreeScopesFromSelectedScopes,
} from './utils';
export interface ScopesSelectorSceneState extends SceneObjectState { export interface ScopesSelectorSceneState extends SceneObjectState {
dashboards: SceneObjectRef<ScopesDashboardsScene> | null; dashboards: SceneObjectRef<ScopesDashboardsScene> | null;
@@ -64,11 +51,9 @@ export const initialSelectorState: Omit<ScopesSelectorSceneState, 'dashboards'>
isEnabled: false, isEnabled: false,
}; };
export class ScopesSelectorScene extends SceneObjectBase<ScopesSelectorSceneState> implements SceneObjectWithUrlSync { export class ScopesSelectorScene extends SceneObjectBase<ScopesSelectorSceneState> {
static Component = ScopesSelectorSceneRenderer; static Component = ScopesSelectorSceneRenderer;
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['scopes'] });
private nodesFetchingSub: Subscription | undefined; private nodesFetchingSub: Subscription | undefined;
constructor() { constructor() {
@@ -78,7 +63,11 @@ export class ScopesSelectorScene extends SceneObjectBase<ScopesSelectorSceneStat
}); });
this.addActivationHandler(() => { this.addActivationHandler(() => {
this.fetchBaseNodes(); // Only fetch base nodes on activation when there are no nodes fetched
// This prevents an issue where base nodes are overwritten upon re-activations
if (Object.keys(this.state.nodes[''].nodes).length === 0) {
this.fetchBaseNodes();
}
return () => { return () => {
this.nodesFetchingSub?.unsubscribe(); this.nodesFetchingSub?.unsubscribe();
@@ -86,19 +75,6 @@ export class ScopesSelectorScene extends SceneObjectBase<ScopesSelectorSceneStat
}); });
} }
public getUrlState() {
return {
scopes: this.state.isEnabled ? getScopeNamesFromSelectedScopes(this.state.scopes) : [],
};
}
public updateFromUrl(values: SceneObjectUrlValues) {
let scopeNames = values.scopes ?? [];
scopeNames = Array.isArray(scopeNames) ? scopeNames : [scopeNames];
this.updateScopes(scopeNames.map((scopeName) => ({ scopeName, path: [] })));
}
public fetchBaseNodes() { public fetchBaseNodes() {
return this.updateNode([''], true, ''); return this.updateNode([''], true, '');
} }
@@ -231,6 +207,8 @@ export class ScopesSelectorScene extends SceneObjectBase<ScopesSelectorSceneStat
isLoadingScopes: true, isLoadingScopes: true,
}); });
this.state.dashboards?.resolve().fetchDashboards(treeScopes.map(({ scopeName }) => scopeName));
const scopes = await fetchSelectedScopes(treeScopes); const scopes = await fetchSelectedScopes(treeScopes);
this.setState({ scopes, isLoadingScopes: false }); this.setState({ scopes, isLoadingScopes: false });
@@ -240,8 +218,8 @@ export class ScopesSelectorScene extends SceneObjectBase<ScopesSelectorSceneStat
this.setState({ treeScopes: getTreeScopesFromSelectedScopes(this.state.scopes) }); this.setState({ treeScopes: getTreeScopesFromSelectedScopes(this.state.scopes) });
} }
public removeAllScopes() { public async removeAllScopes() {
this.setState({ scopes: [], treeScopes: [], isLoadingScopes: false }); return this.updateScopes([]);
} }
public enterReadOnly() { public enterReadOnly() {
+14 -14
View File
@@ -126,16 +126,16 @@ describe('Tree', () => {
await openSelector(); await openSelector();
await expandResultApplications(); await expandResultApplications();
await searchScopes('Cloud'); await searchScopes('Cloud');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3); expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
expectResultApplicationsGrafanaNotPresent(); expectResultApplicationsGrafanaNotPresent();
expectResultApplicationsMimirNotPresent(); expectResultApplicationsMimirNotPresent();
expectResultApplicationsCloudPresent(); expectResultApplicationsCloudPresent();
await clearScopesSearch(); await clearScopesSearch();
expect(fetchNodesSpy).toHaveBeenCalledTimes(4); expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
await searchScopes('Grafana'); await searchScopes('Grafana');
expect(fetchNodesSpy).toHaveBeenCalledTimes(5); expect(fetchNodesSpy).toHaveBeenCalledTimes(4);
expectResultApplicationsGrafanaPresent(); expectResultApplicationsGrafanaPresent();
expectResultApplicationsCloudNotPresent(); expectResultApplicationsCloudNotPresent();
}); });
@@ -156,7 +156,7 @@ describe('Tree', () => {
await expandResultApplications(); await expandResultApplications();
await selectResultApplicationsMimir(); await selectResultApplicationsMimir();
await searchScopes('grafana'); await searchScopes('grafana');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3); expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
expectPersistedApplicationsMimirPresent(); expectPersistedApplicationsMimirPresent();
expectPersistedApplicationsGrafanaNotPresent(); expectPersistedApplicationsGrafanaNotPresent();
expectResultApplicationsMimirNotPresent(); expectResultApplicationsMimirNotPresent();
@@ -168,7 +168,7 @@ describe('Tree', () => {
await expandResultApplications(); await expandResultApplications();
await selectResultApplicationsMimir(); await selectResultApplicationsMimir();
await searchScopes('mimir'); await searchScopes('mimir');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3); expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
expectPersistedApplicationsMimirNotPresent(); expectPersistedApplicationsMimirNotPresent();
expectResultApplicationsMimirPresent(); expectResultApplicationsMimirPresent();
}); });
@@ -178,10 +178,10 @@ describe('Tree', () => {
await expandResultApplications(); await expandResultApplications();
await selectResultApplicationsMimir(); await selectResultApplicationsMimir();
await searchScopes('grafana'); await searchScopes('grafana');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3); expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
await clearScopesSearch(); await clearScopesSearch();
expect(fetchNodesSpy).toHaveBeenCalledTimes(4); expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
expectPersistedApplicationsMimirNotPresent(); expectPersistedApplicationsMimirNotPresent();
expectPersistedApplicationsGrafanaNotPresent(); expectPersistedApplicationsGrafanaNotPresent();
expectResultApplicationsMimirPresent(); expectResultApplicationsMimirPresent();
@@ -192,15 +192,15 @@ describe('Tree', () => {
await openSelector(); await openSelector();
await expandResultApplications(); await expandResultApplications();
await searchScopes('mimir'); await searchScopes('mimir');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3); expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
await selectResultApplicationsMimir(); await selectResultApplicationsMimir();
await searchScopes('unknown'); await searchScopes('unknown');
expect(fetchNodesSpy).toHaveBeenCalledTimes(4); expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
expectPersistedApplicationsMimirPresent(); expectPersistedApplicationsMimirPresent();
await clearScopesSearch(); await clearScopesSearch();
expect(fetchNodesSpy).toHaveBeenCalledTimes(5); expect(fetchNodesSpy).toHaveBeenCalledTimes(4);
expectResultApplicationsMimirPresent(); expectResultApplicationsMimirPresent();
expectResultApplicationsGrafanaPresent(); expectResultApplicationsGrafanaPresent();
}); });
@@ -210,7 +210,7 @@ describe('Tree', () => {
await expandResultApplications(); await expandResultApplications();
await selectResultApplicationsMimir(); await selectResultApplicationsMimir();
await searchScopes('grafana'); await searchScopes('grafana');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3); expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
await selectResultApplicationsGrafana(); await selectResultApplicationsGrafana();
await applyScopes(); await applyScopes();
@@ -222,7 +222,7 @@ describe('Tree', () => {
await expandResultApplications(); await expandResultApplications();
await selectResultApplicationsMimir(); await selectResultApplicationsMimir();
await searchScopes('grafana'); await searchScopes('grafana');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3); expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
await selectResultApplicationsGrafana(); await selectResultApplicationsGrafana();
await applyScopes(); await applyScopes();
@@ -239,11 +239,11 @@ describe('Tree', () => {
expectScopesHeadline('Recommended'); expectScopesHeadline('Recommended');
await searchScopes('Applications'); await searchScopes('Applications');
expect(fetchNodesSpy).toHaveBeenCalledTimes(2); expect(fetchNodesSpy).toHaveBeenCalledTimes(1);
expectScopesHeadline('Results'); expectScopesHeadline('Results');
await searchScopes('unknown'); await searchScopes('unknown');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3); expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
expectScopesHeadline('No results found for your query'); expectScopesHeadline('No results found for your query');
}); });