ScopesInput: Display parent and use + as separator (#107698)
* Display group and use + as separator * Set/get parent node from URL * Add hook useScopeNode * Add loading state and Skeleton * Add preload and actually commit hook * Add preload * Add feature flag * Update test * Fix test * Fix tree test
This commit is contained in:
@@ -457,6 +457,11 @@ export interface FeatureToggles {
|
|||||||
*/
|
*/
|
||||||
scopeApi?: boolean;
|
scopeApi?: boolean;
|
||||||
/**
|
/**
|
||||||
|
* Use the single node endpoint for the scope api. This is used to fetch the scope parent node.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
useScopeSingleNodeEndpoint?: boolean;
|
||||||
|
/**
|
||||||
* In-development feature that will allow injection of labels into prometheus queries.
|
* In-development feature that will allow injection of labels into prometheus queries.
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -769,6 +769,16 @@ var (
|
|||||||
HideFromAdminPage: true,
|
HideFromAdminPage: true,
|
||||||
Expression: "false",
|
Expression: "false",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "useScopeSingleNodeEndpoint",
|
||||||
|
Description: "Use the single node endpoint for the scope api. This is used to fetch the scope parent node.",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
Owner: grafanaOperatorExperienceSquad,
|
||||||
|
Expression: "false",
|
||||||
|
FrontendOnly: true,
|
||||||
|
HideFromDocs: true,
|
||||||
|
HideFromAdminPage: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "promQLScope",
|
Name: "promQLScope",
|
||||||
Description: "In-development feature that will allow injection of labels into prometheus queries.",
|
Description: "In-development feature that will allow injection of labels into prometheus queries.",
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ secretsManagementAppPlatform,experimental,@grafana/grafana-operator-experience-s
|
|||||||
alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false
|
alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false
|
||||||
alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false
|
alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false
|
||||||
scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||||
|
useScopeSingleNodeEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||||
promQLScope,GA,@grafana/oss-big-tent,false,false,false
|
promQLScope,GA,@grafana/oss-big-tent,false,false,false
|
||||||
logQLScope,privatePreview,@grafana/observability-logs,false,false,false
|
logQLScope,privatePreview,@grafana/observability-logs,false,false,false
|
||||||
sqlExpressions,privatePreview,@grafana/grafana-datasources-core-services,false,false,false
|
sqlExpressions,privatePreview,@grafana/grafana-datasources-core-services,false,false,false
|
||||||
|
|||||||
|
@@ -415,6 +415,10 @@ const (
|
|||||||
// In-development feature flag for the scope api using the app platform.
|
// In-development feature flag for the scope api using the app platform.
|
||||||
FlagScopeApi = "scopeApi"
|
FlagScopeApi = "scopeApi"
|
||||||
|
|
||||||
|
// FlagUseScopeSingleNodeEndpoint
|
||||||
|
// Use the single node endpoint for the scope api. This is used to fetch the scope parent node.
|
||||||
|
FlagUseScopeSingleNodeEndpoint = "useScopeSingleNodeEndpoint"
|
||||||
|
|
||||||
// FlagPromQLScope
|
// FlagPromQLScope
|
||||||
// In-development feature that will allow injection of labels into prometheus queries.
|
// In-development feature that will allow injection of labels into prometheus queries.
|
||||||
FlagPromQLScope = "promQLScope"
|
FlagPromQLScope = "promQLScope"
|
||||||
|
|||||||
@@ -3242,6 +3242,22 @@
|
|||||||
"hideFromDocs": true
|
"hideFromDocs": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "useScopeSingleNodeEndpoint",
|
||||||
|
"resourceVersion": "1753960766702",
|
||||||
|
"creationTimestamp": "2025-07-31T11:19:26Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Use the single node endpoint for the scope api. This is used to fetch the scope parent node.",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/grafana-operator-experience-squad",
|
||||||
|
"frontend": true,
|
||||||
|
"hideFromAdminPage": true,
|
||||||
|
"hideFromDocs": true,
|
||||||
|
"expression": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "useScopesNavigationEndpoint",
|
"name": "useScopesNavigationEndpoint",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data';
|
import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv, config } from '@grafana/runtime';
|
||||||
|
|
||||||
import { getAPINamespace } from '../../api/utils';
|
import { getAPINamespace } from '../../api/utils';
|
||||||
|
|
||||||
@@ -84,4 +84,16 @@ export class ScopesApiClient {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public fetchScopeNode = async (scopeNodeId: string): Promise<ScopeNode | undefined> => {
|
||||||
|
if (!config.featureToggles.useScopeSingleNodeEndpoint) {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await getBackendSrv().get<ScopeNode>(apiUrl + `/scopenodes/${scopeNodeId}`);
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,16 @@ export class ScopesService implements ScopesContextValue {
|
|||||||
|
|
||||||
// Init from the URL when we first load
|
// Init from the URL when we first load
|
||||||
const queryParams = new URLSearchParams(locationService.getLocation().search);
|
const queryParams = new URLSearchParams(locationService.getLocation().search);
|
||||||
this.changeScopes(queryParams.getAll('scopes'));
|
const parentNodeId = queryParams.get('scope_parent');
|
||||||
|
|
||||||
|
this.changeScopes(queryParams.getAll('scopes'), parentNodeId ?? undefined);
|
||||||
|
|
||||||
|
// Pre-load parent node, to prevent UI flickering
|
||||||
|
if (parentNodeId) {
|
||||||
|
this.selectorService.getScopeNode(parentNodeId).catch((error) => {
|
||||||
|
console.error('Failed to pre-load parent node', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update scopes state based on URL.
|
// Update scopes state based on URL.
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
@@ -80,13 +89,16 @@ export class ScopesService implements ScopesContextValue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
// If we have a parent node in the URL, fetch and expand it
|
||||||
|
const parentNode = queryParams.get('scope_parent');
|
||||||
const scopes = queryParams.getAll('scopes');
|
const scopes = queryParams.getAll('scopes');
|
||||||
//const scopesFromState = this.state.value.map((scope) => scope.metadata.name);
|
|
||||||
if (scopes.length) {
|
if (scopes.length) {
|
||||||
// We only update scopes but never delete them. This is to keep the scopes in memory if user navigates to
|
// We only update scopes but never delete them. This is to keep the scopes in memory if user navigates to
|
||||||
// page that does not use scopes (like from dashboard to dashboard list back to dashboard). If user
|
// page that does not use scopes (like from dashboard to dashboard list back to dashboard). If user
|
||||||
// changes the URL directly, it would trigger a reload so scopes would still be reset.
|
// changes the URL directly, it would trigger a reload so scopes would still be reset.
|
||||||
this.changeScopes(scopes);
|
this.changeScopes(scopes, parentNode ?? undefined);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -94,6 +106,12 @@ export class ScopesService implements ScopesContextValue {
|
|||||||
// Update the URL based on change in the scopes state
|
// Update the URL based on change in the scopes state
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
selectorService.subscribeToState((state, prev) => {
|
selectorService.subscribeToState((state, prev) => {
|
||||||
|
const oldParentNode = prev.appliedScopes[0]?.parentNodeId;
|
||||||
|
const newParentNode = state.appliedScopes[0]?.parentNodeId;
|
||||||
|
if (oldParentNode !== newParentNode && newParentNode) {
|
||||||
|
this.locationService.partial({ scope_parent: newParentNode }, true);
|
||||||
|
}
|
||||||
|
|
||||||
const oldScopeNames = prev.appliedScopes.map((scope) => scope.scopeId);
|
const oldScopeNames = prev.appliedScopes.map((scope) => scope.scopeId);
|
||||||
const newScopeNames = state.appliedScopes.map((scope) => scope.scopeId);
|
const newScopeNames = state.appliedScopes.map((scope) => scope.scopeId);
|
||||||
if (!isEqual(oldScopeNames, newScopeNames)) {
|
if (!isEqual(oldScopeNames, newScopeNames)) {
|
||||||
@@ -124,7 +142,8 @@ export class ScopesService implements ScopesContextValue {
|
|||||||
return this._stateObservable;
|
return this._stateObservable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeScopes = (scopeNames: string[]) => this.selectorService.changeScopes(scopeNames);
|
public changeScopes = (scopeNames: string[], parentNodeId?: string) =>
|
||||||
|
this.selectorService.changeScopes(scopeNames, parentNodeId);
|
||||||
|
|
||||||
public setReadOnly = (readOnly: boolean) => {
|
public setReadOnly = (readOnly: boolean) => {
|
||||||
if (this.state.readOnly !== readOnly) {
|
if (this.state.readOnly !== readOnly) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
@@ -7,6 +8,7 @@ import { IconButton, Input, Tooltip, useStyles2 } from '@grafana/ui';
|
|||||||
|
|
||||||
import { getPathOfNode } from './scopesTreeUtils';
|
import { getPathOfNode } from './scopesTreeUtils';
|
||||||
import { NodesMap, ScopesMap, SelectedScope } from './types';
|
import { NodesMap, ScopesMap, SelectedScope } from './types';
|
||||||
|
import { useScopeNode } from './useScopeNode';
|
||||||
|
|
||||||
export interface ScopesInputProps {
|
export interface ScopesInputProps {
|
||||||
nodes: NodesMap;
|
nodes: NodesMap;
|
||||||
@@ -32,6 +34,10 @@ export function ScopesInput({
|
|||||||
}: ScopesInputProps) {
|
}: ScopesInputProps) {
|
||||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||||
|
|
||||||
|
const parentNodeId = appliedScopes[0]?.parentNodeId;
|
||||||
|
const { node: parentNode, isLoading: parentNodeLoading } = useScopeNode(parentNodeId);
|
||||||
|
const parentNodeTitle = parentNode?.spec.title;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTooltipVisible(false);
|
setTooltipVisible(false);
|
||||||
}, [appliedScopes]);
|
}, [appliedScopes]);
|
||||||
@@ -47,10 +53,20 @@ export function ScopesInput({
|
|||||||
// If we are still loading the scope data just show the id
|
// If we are still loading the scope data just show the id
|
||||||
scopes[s.scopeId]?.spec.title || s.scopeId
|
scopes[s.scopeId]?.spec.title || s.scopeId
|
||||||
)
|
)
|
||||||
.join(', '),
|
.join(' + '),
|
||||||
[appliedScopes, scopes]
|
[appliedScopes, scopes]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const parentNodePrefix = useMemo(
|
||||||
|
() =>
|
||||||
|
parentNodeLoading ? (
|
||||||
|
<Skeleton width={30} height={14} />
|
||||||
|
) : parentNodeTitle ? (
|
||||||
|
<span>{parentNodeTitle}:</span>
|
||||||
|
) : undefined,
|
||||||
|
[parentNodeLoading, parentNodeTitle]
|
||||||
|
);
|
||||||
|
|
||||||
const input = useMemo(
|
const input = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<Input
|
<Input
|
||||||
@@ -61,6 +77,7 @@ export function ScopesInput({
|
|||||||
value={scopesTitles}
|
value={scopesTitles}
|
||||||
aria-label={t('scopes.selector.input.placeholder', 'Select scopes...')}
|
aria-label={t('scopes.selector.input.placeholder', 'Select scopes...')}
|
||||||
data-testid="scopes-selector-input"
|
data-testid="scopes-selector-input"
|
||||||
|
prefix={parentNodePrefix}
|
||||||
suffix={
|
suffix={
|
||||||
appliedScopes.length > 0 && !disabled ? (
|
appliedScopes.length > 0 && !disabled ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -80,7 +97,7 @@ export function ScopesInput({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[disabled, loading, onInputClick, onRemoveAllClick, appliedScopes, scopesTitles]
|
[disabled, loading, onInputClick, onRemoveAllClick, appliedScopes, scopesTitles, parentNodePrefix]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -90,6 +107,21 @@ export function ScopesInput({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getScopesPath = (appliedScopes: SelectedScope[], nodes: NodesMap) => {
|
||||||
|
let nicePath: string[] | undefined;
|
||||||
|
|
||||||
|
if (appliedScopes.length > 0 && appliedScopes[0].scopeNodeId) {
|
||||||
|
let path = getPathOfNode(appliedScopes[0].scopeNodeId, nodes);
|
||||||
|
// Get reed of empty root section and the actual scope node
|
||||||
|
path = path.slice(1, -1);
|
||||||
|
|
||||||
|
// We may not have all the nodes in path loaded
|
||||||
|
nicePath = path.map((p) => nodes[p]?.spec.title).filter((p) => p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nicePath;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ScopesTooltipProps {
|
export interface ScopesTooltipProps {
|
||||||
nodes: NodesMap;
|
nodes: NodesMap;
|
||||||
scopes: ScopesMap;
|
scopes: ScopesMap;
|
||||||
@@ -99,16 +131,7 @@ export interface ScopesTooltipProps {
|
|||||||
function ScopesTooltip({ nodes, scopes, appliedScopes }: ScopesTooltipProps) {
|
function ScopesTooltip({ nodes, scopes, appliedScopes }: ScopesTooltipProps) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
let nicePath: string[] | undefined;
|
const nicePath = getScopesPath(appliedScopes, nodes);
|
||||||
|
|
||||||
if (appliedScopes[0].scopeNodeId) {
|
|
||||||
let path = getPathOfNode(appliedScopes[0].scopeNodeId, nodes);
|
|
||||||
// Get reed of empty root section and the actual scope node
|
|
||||||
path = path.slice(1, -1);
|
|
||||||
|
|
||||||
// We may not have all the nodes in path loaded
|
|
||||||
nicePath = path.map((p) => nodes[p]?.spec.title).filter((p) => p);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scopeNames = appliedScopes.map((s) => {
|
const scopeNames = appliedScopes.map((s) => {
|
||||||
if (s.scopeNodeId) {
|
if (s.scopeNodeId) {
|
||||||
|
|||||||
@@ -71,6 +71,24 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loads a node from the API and adds it to the nodes cache
|
||||||
|
public getScopeNode = async (scopeNodeId: string) => {
|
||||||
|
if (this.state.nodes[scopeNodeId]) {
|
||||||
|
return this.state.nodes[scopeNodeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const node = await this.apiClient.fetchScopeNode(scopeNodeId);
|
||||||
|
if (node) {
|
||||||
|
this.updateState({ nodes: { ...this.state.nodes, [node.metadata.name]: node } });
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load node', error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private expandOrFilterNode = async (scopeNodeId: string, query?: string) => {
|
private expandOrFilterNode = async (scopeNodeId: string, query?: string) => {
|
||||||
const path = getPathOfNode(scopeNodeId, this.state.nodes);
|
const path = getPathOfNode(scopeNodeId, this.state.nodes);
|
||||||
|
|
||||||
@@ -160,11 +178,15 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: if we do global search we may not have a prent node loaded. We have the ID but there is not an API that
|
// TODO: if we do global search we may not have a parent node loaded. We have the ID but there is not an API that
|
||||||
// would allow us to load scopeNode by ID right now so this can be undefined which means we skip the
|
// would allow us to load scopeNode by ID right now so this can be undefined which means we skip the
|
||||||
// disableMultiSelect check.
|
// disableMultiSelect check.
|
||||||
const parentNode = this.state.nodes[scopeNode.spec.parentName!];
|
const parentNode = this.state.nodes[scopeNode.spec.parentName!];
|
||||||
const selectedScope = { scopeId: scopeNode.spec.linkId, scopeNodeId: scopeNode.metadata.name };
|
const selectedScope = {
|
||||||
|
scopeId: scopeNode.spec.linkId,
|
||||||
|
scopeNodeId: scopeNode.metadata.name,
|
||||||
|
parentNodeId: parentNode?.metadata.name,
|
||||||
|
};
|
||||||
|
|
||||||
// if something is selected we look at parent and see if we are selecting in the same category or not. As we
|
// if something is selected we look at parent and see if we are selecting in the same category or not. As we
|
||||||
// cannot select in multiple categories we only need to check the first selected node. It is possible we have
|
// cannot select in multiple categories we only need to check the first selected node. It is possible we have
|
||||||
@@ -214,8 +236,8 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
|||||||
return this.collapseNode(scopeNodeId);
|
return this.collapseNode(scopeNodeId);
|
||||||
};
|
};
|
||||||
|
|
||||||
changeScopes = (scopeNames: string[]) => {
|
changeScopes = (scopeNames: string[], parentNodeId?: string) => {
|
||||||
return this.applyScopes(scopeNames.map((id) => ({ scopeId: id })));
|
return this.applyScopes(scopeNames.map((id) => ({ scopeId: id, parentNodeId: parentNodeId })));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export type ScopesMap = Record<string, Scope>;
|
|||||||
export interface SelectedScope {
|
export interface SelectedScope {
|
||||||
scopeId: string;
|
scopeId: string;
|
||||||
scopeNodeId?: string;
|
scopeNodeId?: string;
|
||||||
|
// Used to display title next to selected scope
|
||||||
|
parentNodeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TreeNode {
|
export interface TreeNode {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { ScopeNode } from '@grafana/data';
|
||||||
|
|
||||||
|
import { useScopesServices } from '../ScopesContextProvider';
|
||||||
|
|
||||||
|
// Light wrapper around the scopesSelectorService.getScopeNode to make it easier to use in the UI.
|
||||||
|
export function useScopeNode(scopeNodeId?: string) {
|
||||||
|
const [node, setNode] = useState<ScopeNode | undefined>(undefined);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const scopesSelectorService = useScopesServices()?.scopesSelectorService;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadNode = async () => {
|
||||||
|
if (!scopeNodeId || !scopesSelectorService) {
|
||||||
|
setNode(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const node = await scopesSelectorService.getScopeNode(scopeNodeId);
|
||||||
|
setNode(node);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load node', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadNode();
|
||||||
|
}, [scopeNodeId, scopesSelectorService]);
|
||||||
|
|
||||||
|
return { node, isLoading };
|
||||||
|
}
|
||||||
@@ -115,7 +115,7 @@ describe('Selector', () => {
|
|||||||
expectRecentScopeNotPresent('Mimir');
|
expectRecentScopeNotPresent('Mimir');
|
||||||
await selectRecentScope('Grafana, Mimir');
|
await selectRecentScope('Grafana, Mimir');
|
||||||
|
|
||||||
expectScopesSelectorValue('Grafana, Mimir');
|
expectScopesSelectorValue('Grafana + Mimir');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recent scopes should not be visible when the first scope is selected', async () => {
|
it('recent scopes should not be visible when the first scope is selected', async () => {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ describe('Tree', () => {
|
|||||||
await selectResultApplicationsMimir();
|
await selectResultApplicationsMimir();
|
||||||
await selectResultApplicationsCloud();
|
await selectResultApplicationsCloud();
|
||||||
await applyScopes();
|
await applyScopes();
|
||||||
expectScopesSelectorValue('Grafana, Mimir, Cloud');
|
expectScopesSelectorValue('Grafana + Mimir + Cloud');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can select a node from an inner level', async () => {
|
it('Can select a node from an inner level', async () => {
|
||||||
@@ -225,7 +225,7 @@ describe('Tree', () => {
|
|||||||
|
|
||||||
await selectResultApplicationsGrafana();
|
await selectResultApplicationsGrafana();
|
||||||
await applyScopes();
|
await applyScopes();
|
||||||
expectScopesSelectorValue('Mimir, Grafana');
|
expectScopesSelectorValue('Mimir + Grafana');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Deselects a persisted scope', async () => {
|
it('Deselects a persisted scope', async () => {
|
||||||
@@ -237,7 +237,7 @@ describe('Tree', () => {
|
|||||||
|
|
||||||
await selectResultApplicationsGrafana();
|
await selectResultApplicationsGrafana();
|
||||||
await applyScopes();
|
await applyScopes();
|
||||||
expectScopesSelectorValue('Mimir, Grafana');
|
expectScopesSelectorValue('Mimir + Grafana');
|
||||||
|
|
||||||
await openSelector();
|
await openSelector();
|
||||||
await selectPersistedApplicationsMimir();
|
await selectPersistedApplicationsMimir();
|
||||||
|
|||||||
Reference in New Issue
Block a user