mirror of https://github.com/grafana/grafana.git
Scopes: Add recent scopes selectors (#103534)
* Add recent scopes to command palette * Remove parent action * Support updating select set of scopes * Filter out currently selected scope * Add recent scopes to drawer * Add expandable section * Add recent scopes component * Only show recommended for leaf nodes * Small style fixes * Always write to recent after fetching new scopes * Add feature toggle check for command palette * Use i18n * Remove unused prop * Add test cases for recent scopes in selector * Add more test cases * Add clear test action * Remove unused imports
This commit is contained in:
parent
6bdf161865
commit
8021dee6f1
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { t } from 'app/core/internationalization';
|
||||||
|
import { defaultScopesServices } from 'app/features/scopes/ScopesContextProvider';
|
||||||
|
|
||||||
|
import { CommandPaletteAction } from '../types';
|
||||||
|
import { RECENT_SCOPES_PRIORITY } from '../values';
|
||||||
|
|
||||||
|
export function getRecentScopesActions(): CommandPaletteAction[] {
|
||||||
|
if (!config.featureToggles.scopeFilters) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scopesSelectorService } = defaultScopesServices();
|
||||||
|
|
||||||
|
const recentScopes = scopesSelectorService.getRecentScopes();
|
||||||
|
|
||||||
|
return recentScopes.map((recentScope) => {
|
||||||
|
return {
|
||||||
|
id: recentScope.map((scope) => scope.scope.spec.title).join(', '),
|
||||||
|
name: recentScope.map((scope) => scope.scope.spec.title).join(', '),
|
||||||
|
section: t('command-palette.section.recent-scopes', 'Recent scopes'),
|
||||||
|
priority: RECENT_SCOPES_PRIORITY,
|
||||||
|
perform: () => {
|
||||||
|
scopesSelectorService.changeScopes(recentScope.map((scope) => scope.scope.metadata.name));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { useSelector } from 'app/types';
|
||||||
import { CommandPaletteAction } from '../types';
|
import { CommandPaletteAction } from '../types';
|
||||||
|
|
||||||
import { getRecentDashboardActions } from './dashboardActions';
|
import { getRecentDashboardActions } from './dashboardActions';
|
||||||
|
import { getRecentScopesActions } from './recentScopesActions';
|
||||||
import getStaticActions from './staticActions';
|
import getStaticActions from './staticActions';
|
||||||
import useExtensionActions from './useExtensionActions';
|
import useExtensionActions from './useExtensionActions';
|
||||||
|
|
||||||
|
|
@ -14,6 +15,7 @@ export default function useActions(searchQuery: string) {
|
||||||
const extensionActions = useExtensionActions();
|
const extensionActions = useExtensionActions();
|
||||||
|
|
||||||
const navBarTree = useSelector((state) => state.navBarTree);
|
const navBarTree = useSelector((state) => state.navBarTree);
|
||||||
|
const recentScopesActions = getRecentScopesActions();
|
||||||
|
|
||||||
// Load standard static actions
|
// Load standard static actions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -32,5 +34,5 @@ export default function useActions(searchQuery: string) {
|
||||||
}
|
}
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
return searchQuery ? navTreeActions : [...recentDashboardActions, ...navTreeActions];
|
return searchQuery ? navTreeActions : [...recentDashboardActions, ...navTreeActions, ...recentScopesActions];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
export const RECENT_SCOPES_PRIORITY = 7;
|
||||||
export const RECENT_DASHBOARDS_PRIORITY = 6;
|
export const RECENT_DASHBOARDS_PRIORITY = 6;
|
||||||
export const ACTIONS_PRIORITY = 5;
|
export const ACTIONS_PRIORITY = 5;
|
||||||
export const DEFAULT_PRIORITY = 4;
|
export const DEFAULT_PRIORITY = 4;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { useId, useState } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { useStyles2, Stack, Text, Icon, Box } from '@grafana/ui';
|
||||||
|
import { Trans } from 'app/core/internationalization';
|
||||||
|
|
||||||
|
import { SelectedScope } from './types';
|
||||||
|
|
||||||
|
interface RecentScopesProps {
|
||||||
|
recentScopes: SelectedScope[][];
|
||||||
|
onSelect: (scopes: SelectedScope[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RecentScopes = ({ recentScopes, onSelect }: RecentScopesProps) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
const contentId = useId();
|
||||||
|
return (
|
||||||
|
<fieldset>
|
||||||
|
<legend className={styles.legend}>
|
||||||
|
<button
|
||||||
|
className={styles.expandButton}
|
||||||
|
aria-expanded={expanded}
|
||||||
|
aria-controls={contentId}
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
data-testid="scopes-selector-recent-scopes-section"
|
||||||
|
>
|
||||||
|
<Icon name={expanded ? 'angle-down' : 'angle-right'} />
|
||||||
|
<Text variant="body">
|
||||||
|
<Trans i18nKey="command-palette.section.recent-scopes" />
|
||||||
|
</Text>
|
||||||
|
</button>
|
||||||
|
</legend>
|
||||||
|
<Box paddingLeft={3} paddingTop={expanded ? 1 : 0} paddingBottom={expanded ? 1 : 0}>
|
||||||
|
<Stack direction="column" gap={1} id={contentId}>
|
||||||
|
{expanded &&
|
||||||
|
recentScopes.map((recentScopeSet) => (
|
||||||
|
<button
|
||||||
|
className={styles.recentScopeButton}
|
||||||
|
key={recentScopeSet.map((s) => s.scope.metadata.name).join(',')}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(recentScopeSet);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>{recentScopeSet.map((s) => s.scope.spec.title).join(', ')}</Text>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
recentScopeButton: css({
|
||||||
|
textAlign: 'left',
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
padding: 0,
|
||||||
|
cursor: 'pointer',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}),
|
||||||
|
expandButton: css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
padding: 0,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}),
|
||||||
|
legend: css({
|
||||||
|
marginBottom: 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
@ -85,6 +85,7 @@ export function ScopesInput({ nodes, scopes, disabled, loading, onInputClick, on
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('scopes.selector.input.removeAll', 'Remove all scopes')}
|
aria-label={t('scopes.selector.input.removeAll', 'Remove all scopes')}
|
||||||
name="times"
|
name="times"
|
||||||
|
data-testid="scopes-selector-input-clear"
|
||||||
onClick={() => onRemoveAllClick()}
|
onClick={() => onRemoveAllClick()}
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ export const ScopesSelector = () => {
|
||||||
const { nodes, loadingNodeName, selectedScopes, opened, treeScopes } = selectorServiceState;
|
const { nodes, loadingNodeName, selectedScopes, opened, treeScopes } = selectorServiceState;
|
||||||
const { scopesService, scopesSelectorService, scopesDashboardsService } = services;
|
const { scopesService, scopesSelectorService, scopesDashboardsService } = services;
|
||||||
const { readOnly, drawerOpened, loading } = scopes.state;
|
const { readOnly, drawerOpened, loading } = scopes.state;
|
||||||
const { open, removeAllScopes, closeAndApply, closeAndReset, updateNode, toggleNodeSelect } = scopesSelectorService;
|
const { open, removeAllScopes, closeAndApply, closeAndReset, updateNode, toggleNodeSelect, getRecentScopes } =
|
||||||
|
scopesSelectorService;
|
||||||
|
|
||||||
|
const recentScopes = getRecentScopes();
|
||||||
|
|
||||||
const dashboardsIconLabel = readOnly
|
const dashboardsIconLabel = readOnly
|
||||||
? t('scopes.dashboards.toggle.disabled', 'Suggested dashboards list is disabled due to read only mode')
|
? t('scopes.dashboards.toggle.disabled', 'Suggested dashboards list is disabled due to read only mode')
|
||||||
|
|
@ -74,14 +77,21 @@ export const ScopesSelector = () => {
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Spinner data-testid="scopes-selector-loading" />
|
<Spinner data-testid="scopes-selector-loading" />
|
||||||
) : (
|
) : (
|
||||||
<ScopesTree
|
<>
|
||||||
nodes={nodes}
|
<ScopesTree
|
||||||
nodePath={['']}
|
nodes={nodes}
|
||||||
loadingNodeName={loadingNodeName}
|
nodePath={['']}
|
||||||
scopes={treeScopes}
|
loadingNodeName={loadingNodeName}
|
||||||
onNodeUpdate={updateNode}
|
scopes={treeScopes}
|
||||||
onNodeSelectToggle={toggleNodeSelect}
|
onNodeUpdate={updateNode}
|
||||||
/>
|
onNodeSelectToggle={toggleNodeSelect}
|
||||||
|
recentScopes={recentScopes}
|
||||||
|
onRecentScopesSelect={(recentScopeSet) => {
|
||||||
|
scopesSelectorService.changeScopes(recentScopeSet.map((s) => s.scope.metadata.name));
|
||||||
|
scopesSelectorService.closeAndApply();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { getEmptyScopeObject } from '../utils';
|
||||||
|
|
||||||
import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
|
import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
|
||||||
|
|
||||||
|
const RECENT_SCOPES_KEY = 'grafana.scopes.recent';
|
||||||
|
|
||||||
export interface ScopesSelectorServiceState {
|
export interface ScopesSelectorServiceState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|
||||||
|
|
@ -188,11 +190,41 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||||
this.dashboardsService.fetchDashboards(selectedScopes.map(({ scope }) => scope.metadata.name));
|
this.dashboardsService.fetchDashboards(selectedScopes.map(({ scope }) => scope.metadata.name));
|
||||||
|
|
||||||
selectedScopes = await this.apiClient.fetchMultipleScopes(treeScopes);
|
selectedScopes = await this.apiClient.fetchMultipleScopes(treeScopes);
|
||||||
|
if (selectedScopes.length > 0) {
|
||||||
|
this.addRecentScopes(selectedScopes);
|
||||||
|
}
|
||||||
this.updateState({ selectedScopes, loading: false });
|
this.updateState({ selectedScopes, loading: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
public removeAllScopes = () => this.setNewScopes([]);
|
public removeAllScopes = () => this.setNewScopes([]);
|
||||||
|
|
||||||
|
private addRecentScopes = (scopes: SelectedScope[]) => {
|
||||||
|
if (scopes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RECENT_SCOPES_MAX_LENGTH = 5;
|
||||||
|
|
||||||
|
const recentScopes = this.getRecentScopes();
|
||||||
|
recentScopes.unshift(scopes);
|
||||||
|
localStorage.setItem(RECENT_SCOPES_KEY, JSON.stringify(recentScopes.slice(0, RECENT_SCOPES_MAX_LENGTH - 1)));
|
||||||
|
};
|
||||||
|
|
||||||
|
public getRecentScopes = (): SelectedScope[][] => {
|
||||||
|
const recentScopes = JSON.parse(localStorage.getItem(RECENT_SCOPES_KEY) || '[]');
|
||||||
|
// TODO: Make type safe
|
||||||
|
// Filter out the current selection from recent scopes to avoid duplicates
|
||||||
|
const filteredScopes = recentScopes.filter((scopes: SelectedScope[]) => {
|
||||||
|
if (scopes.length !== this.state.selectedScopes.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const scopeSet = new Set(scopes.map((s) => s.scope.metadata.name));
|
||||||
|
return !this.state.selectedScopes.every((s) => scopeSet.has(s.scope.metadata.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredScopes.map((scopes: SelectedScope[]) => scopes);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the scopes selector drawer and loads the root nodes if they are not loaded yet.
|
* Opens the scopes selector drawer and loads the root nodes if they are not loaded yet.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Dictionary, groupBy } from 'lodash';
|
import { Dictionary, groupBy } from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { RecentScopes } from './RecentScopes';
|
||||||
import { ScopesTreeHeadline } from './ScopesTreeHeadline';
|
import { ScopesTreeHeadline } from './ScopesTreeHeadline';
|
||||||
import { ScopesTreeItem } from './ScopesTreeItem';
|
import { ScopesTreeItem } from './ScopesTreeItem';
|
||||||
import { ScopesTreeLoading } from './ScopesTreeLoading';
|
import { ScopesTreeLoading } from './ScopesTreeLoading';
|
||||||
import { ScopesTreeSearch } from './ScopesTreeSearch';
|
import { ScopesTreeSearch } from './ScopesTreeSearch';
|
||||||
import { Node, NodeReason, NodesMap, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types';
|
import { Node, NodeReason, NodesMap, OnNodeSelectToggle, OnNodeUpdate, TreeScope, SelectedScope } from './types';
|
||||||
|
|
||||||
export interface ScopesTreeProps {
|
export interface ScopesTreeProps {
|
||||||
nodes: NodesMap;
|
nodes: NodesMap;
|
||||||
nodePath: string[];
|
nodePath: string[];
|
||||||
|
|
@ -14,6 +14,10 @@ export interface ScopesTreeProps {
|
||||||
scopes: TreeScope[];
|
scopes: TreeScope[];
|
||||||
onNodeUpdate: OnNodeUpdate;
|
onNodeUpdate: OnNodeUpdate;
|
||||||
onNodeSelectToggle: OnNodeSelectToggle;
|
onNodeSelectToggle: OnNodeSelectToggle;
|
||||||
|
|
||||||
|
// Recent scopes are only shown at the root node
|
||||||
|
recentScopes?: SelectedScope[][];
|
||||||
|
onRecentScopesSelect?: (recentScopeSet: SelectedScope[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScopesTree({
|
export function ScopesTree({
|
||||||
|
|
@ -21,6 +25,8 @@ export function ScopesTree({
|
||||||
nodePath,
|
nodePath,
|
||||||
loadingNodeName,
|
loadingNodeName,
|
||||||
scopes,
|
scopes,
|
||||||
|
recentScopes,
|
||||||
|
onRecentScopesSelect,
|
||||||
onNodeUpdate,
|
onNodeUpdate,
|
||||||
onNodeSelectToggle,
|
onNodeSelectToggle,
|
||||||
}: ScopesTreeProps) {
|
}: ScopesTreeProps) {
|
||||||
|
|
@ -41,6 +47,13 @@ export function ScopesTree({
|
||||||
query={node.query}
|
query={node.query}
|
||||||
onNodeUpdate={onNodeUpdate}
|
onNodeUpdate={onNodeUpdate}
|
||||||
/>
|
/>
|
||||||
|
{nodePath.length === 1 &&
|
||||||
|
nodePath[0] === '' &&
|
||||||
|
!anyChildExpanded &&
|
||||||
|
recentScopes &&
|
||||||
|
recentScopes.length > 0 &&
|
||||||
|
onRecentScopesSelect &&
|
||||||
|
!node.query && <RecentScopes recentScopes={recentScopes} onSelect={onRecentScopesSelect} />}
|
||||||
|
|
||||||
<ScopesTreeLoading nodeLoading={nodeLoading}>
|
<ScopesTreeLoading nodeLoading={nodeLoading}>
|
||||||
<ScopesTreeItem
|
<ScopesTreeItem
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ export interface ScopesTreeHeadlineProps {
|
||||||
|
|
||||||
export function ScopesTreeHeadline({ anyChildExpanded, query, resultsNodes }: ScopesTreeHeadlineProps) {
|
export function ScopesTreeHeadline({ anyChildExpanded, query, resultsNodes }: ScopesTreeHeadlineProps) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
if (anyChildExpanded || (resultsNodes.some((n) => n.nodeType === 'container') && !query)) {
|
||||||
if (anyChildExpanded) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,26 @@ import { config, locationService } from '@grafana/runtime';
|
||||||
import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/DashboardScenePageStateManager';
|
import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/DashboardScenePageStateManager';
|
||||||
import { ScopesService } from '../ScopesService';
|
import { ScopesService } from '../ScopesService';
|
||||||
|
|
||||||
import { applyScopes, cancelScopes, openSelector, selectResultCloud, updateScopes } from './utils/actions';
|
import {
|
||||||
import { expectScopesSelectorValue } from './utils/assertions';
|
applyScopes,
|
||||||
|
cancelScopes,
|
||||||
|
selectResultApplicationsMimir,
|
||||||
|
selectResultApplicationsGrafana,
|
||||||
|
openSelector,
|
||||||
|
selectResultCloud,
|
||||||
|
updateScopes,
|
||||||
|
expandRecentScopes,
|
||||||
|
expandResultApplications,
|
||||||
|
selectRecentScope,
|
||||||
|
clearSelector,
|
||||||
|
} from './utils/actions';
|
||||||
|
import {
|
||||||
|
expectRecentScope,
|
||||||
|
expectRecentScopeNotPresent,
|
||||||
|
expectRecentScopeNotPresentInDocument,
|
||||||
|
expectRecentScopesSection,
|
||||||
|
expectScopesSelectorValue,
|
||||||
|
} from './utils/assertions';
|
||||||
import { getDatasource, getInstanceSettings, getMock, mocksScopes } from './utils/mocks';
|
import { getDatasource, getInstanceSettings, getMock, mocksScopes } from './utils/mocks';
|
||||||
import { renderDashboard, resetScenes } from './utils/render';
|
import { renderDashboard, resetScenes } from './utils/render';
|
||||||
import { getListOfScopes } from './utils/selectors';
|
import { getListOfScopes } from './utils/selectors';
|
||||||
|
|
@ -33,6 +51,7 @@ describe('Selector', () => {
|
||||||
scopesService = result.scopesService;
|
scopesService = result.scopesService;
|
||||||
fetchSelectedScopesSpy = jest.spyOn(result.client, 'fetchMultipleScopes');
|
fetchSelectedScopesSpy = jest.spyOn(result.client, 'fetchMultipleScopes');
|
||||||
dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard');
|
dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard');
|
||||||
|
window.localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
@ -65,4 +84,92 @@ describe('Selector', () => {
|
||||||
await updateScopes(scopesService, ['grafana']);
|
await updateScopes(scopesService, ['grafana']);
|
||||||
expect(dashboardReloadSpy).not.toHaveBeenCalled();
|
expect(dashboardReloadSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Recent scopes', () => {
|
||||||
|
it('Recent scopes should appear after selecting a second set of scopes', async () => {
|
||||||
|
await openSelector();
|
||||||
|
await expandResultApplications();
|
||||||
|
await selectResultApplicationsGrafana();
|
||||||
|
await applyScopes();
|
||||||
|
|
||||||
|
await openSelector();
|
||||||
|
await selectResultApplicationsMimir();
|
||||||
|
await applyScopes();
|
||||||
|
|
||||||
|
// Grafana,Mimir currently selected. Grafana is the first recent scope.
|
||||||
|
await openSelector();
|
||||||
|
expectRecentScopesSection();
|
||||||
|
await expandRecentScopes();
|
||||||
|
expectRecentScope('Grafana');
|
||||||
|
expectRecentScopeNotPresent('Mimir');
|
||||||
|
expectRecentScopeNotPresent('Grafana, Mimir');
|
||||||
|
await selectRecentScope('Grafana');
|
||||||
|
|
||||||
|
expectScopesSelectorValue('Grafana');
|
||||||
|
|
||||||
|
await openSelector();
|
||||||
|
await expandRecentScopes();
|
||||||
|
expectRecentScope('Grafana, Mimir');
|
||||||
|
expectRecentScopeNotPresent('Grafana');
|
||||||
|
expectRecentScopeNotPresent('Mimir');
|
||||||
|
await selectRecentScope('Grafana, Mimir');
|
||||||
|
|
||||||
|
expectScopesSelectorValue('Grafana, Mimir');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recent scopes should not be visible when the first scope is selected', async () => {
|
||||||
|
await openSelector();
|
||||||
|
await expandResultApplications();
|
||||||
|
await selectResultApplicationsGrafana();
|
||||||
|
await applyScopes();
|
||||||
|
|
||||||
|
await openSelector();
|
||||||
|
expectRecentScopeNotPresentInDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show recent scopes when no scopes have been previously selected', async () => {
|
||||||
|
await openSelector();
|
||||||
|
expectRecentScopeNotPresentInDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should maintain recent scopes after deselecting all scopes', async () => {
|
||||||
|
// First select some scopes
|
||||||
|
await openSelector();
|
||||||
|
await expandResultApplications();
|
||||||
|
await selectResultApplicationsGrafana();
|
||||||
|
await selectResultApplicationsMimir();
|
||||||
|
await applyScopes();
|
||||||
|
|
||||||
|
// Deselect all scopes
|
||||||
|
await clearSelector();
|
||||||
|
|
||||||
|
// Recent scopes should still be available
|
||||||
|
await openSelector();
|
||||||
|
expectRecentScopesSection();
|
||||||
|
await expandRecentScopes();
|
||||||
|
expectRecentScope('Grafana, Mimir');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update recent scopes when selecting a different combination', async () => {
|
||||||
|
// First select Grafana + Mimir
|
||||||
|
await openSelector();
|
||||||
|
await expandResultApplications();
|
||||||
|
await selectResultApplicationsGrafana();
|
||||||
|
await selectResultApplicationsMimir();
|
||||||
|
await applyScopes();
|
||||||
|
|
||||||
|
// Then select just Grafana
|
||||||
|
await openSelector();
|
||||||
|
await selectResultApplicationsMimir();
|
||||||
|
await applyScopes();
|
||||||
|
|
||||||
|
await clearSelector();
|
||||||
|
|
||||||
|
// Check recent scopes are updated
|
||||||
|
await openSelector();
|
||||||
|
await expandRecentScopes();
|
||||||
|
expectRecentScope('Grafana, Mimir');
|
||||||
|
expectRecentScope('Grafana');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,6 @@ describe('Tree', () => {
|
||||||
|
|
||||||
it('Shows the proper headline', async () => {
|
it('Shows the proper headline', async () => {
|
||||||
await openSelector();
|
await openSelector();
|
||||||
expectScopesHeadline('Recommended');
|
|
||||||
|
|
||||||
await searchScopes('Applications');
|
await searchScopes('Applications');
|
||||||
expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
|
expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
|
||||||
|
|
@ -260,6 +259,13 @@ describe('Tree', () => {
|
||||||
expectScopesHeadline('No results found for your query');
|
expectScopesHeadline('No results found for your query');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should only show Recommended when there are no leaf container nodes visible', async () => {
|
||||||
|
await openSelector();
|
||||||
|
await expandResultApplications();
|
||||||
|
await expandResultApplicationsCloud();
|
||||||
|
expectScopesHeadline('Recommended');
|
||||||
|
});
|
||||||
|
|
||||||
it('Updates the paths for scopes without paths on nodes fetching', async () => {
|
it('Updates the paths for scopes without paths on nodes fetching', async () => {
|
||||||
const selectedScopeName = 'grafana';
|
const selectedScopeName = 'grafana';
|
||||||
const unselectedScopeName = 'mimir';
|
const unselectedScopeName = 'mimir';
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ import {
|
||||||
getDashboardsExpand,
|
getDashboardsExpand,
|
||||||
getDashboardsSearch,
|
getDashboardsSearch,
|
||||||
getNotFoundForFilterClear,
|
getNotFoundForFilterClear,
|
||||||
|
getPersistedApplicationsGrafanaSelect,
|
||||||
getPersistedApplicationsMimirSelect,
|
getPersistedApplicationsMimirSelect,
|
||||||
|
getRecentScopeSet,
|
||||||
|
getRecentScopesSection,
|
||||||
getResultApplicationsCloudDevSelect,
|
getResultApplicationsCloudDevSelect,
|
||||||
getResultApplicationsCloudExpand,
|
getResultApplicationsCloudExpand,
|
||||||
getResultApplicationsCloudSelect,
|
getResultApplicationsCloudSelect,
|
||||||
|
|
@ -25,6 +28,7 @@ import {
|
||||||
getResultCloudSelect,
|
getResultCloudSelect,
|
||||||
getSelectorApply,
|
getSelectorApply,
|
||||||
getSelectorCancel,
|
getSelectorCancel,
|
||||||
|
getSelectorClear,
|
||||||
getSelectorInput,
|
getSelectorInput,
|
||||||
getTreeSearch,
|
getTreeSearch,
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
|
|
@ -38,6 +42,7 @@ const type = async (selector: () => HTMLInputElement, value: string) => {
|
||||||
export const updateScopes = async (service: ScopesService, scopes: string[]) =>
|
export const updateScopes = async (service: ScopesService, scopes: string[]) =>
|
||||||
act(async () => service.changeScopes(scopes));
|
act(async () => service.changeScopes(scopes));
|
||||||
export const openSelector = async () => click(getSelectorInput);
|
export const openSelector = async () => click(getSelectorInput);
|
||||||
|
export const clearSelector = async () => click(getSelectorClear);
|
||||||
export const applyScopes = async () => {
|
export const applyScopes = async () => {
|
||||||
await click(getSelectorApply);
|
await click(getSelectorApply);
|
||||||
await jest.runOnlyPendingTimersAsync();
|
await jest.runOnlyPendingTimersAsync();
|
||||||
|
|
@ -45,11 +50,14 @@ export const applyScopes = async () => {
|
||||||
export const cancelScopes = async () => click(getSelectorCancel);
|
export const cancelScopes = async () => click(getSelectorCancel);
|
||||||
export const searchScopes = async (value: string) => type(getTreeSearch, value);
|
export const searchScopes = async (value: string) => type(getTreeSearch, value);
|
||||||
export const clearScopesSearch = async () => type(getTreeSearch, '');
|
export const clearScopesSearch = async () => type(getTreeSearch, '');
|
||||||
|
export const expandRecentScopes = async () => click(getRecentScopesSection);
|
||||||
export const expandResultApplications = async () => click(getResultApplicationsExpand);
|
export const expandResultApplications = async () => click(getResultApplicationsExpand);
|
||||||
export const expandResultApplicationsCloud = async () => click(getResultApplicationsCloudExpand);
|
export const expandResultApplicationsCloud = async () => click(getResultApplicationsCloudExpand);
|
||||||
export const expandResultCloud = async () => click(getResultCloudExpand);
|
export const expandResultCloud = async () => click(getResultCloudExpand);
|
||||||
|
export const selectRecentScope = async (scope: string) => click(() => getRecentScopeSet(scope));
|
||||||
export const selectResultApplicationsGrafana = async () => click(getResultApplicationsGrafanaSelect);
|
export const selectResultApplicationsGrafana = async () => click(getResultApplicationsGrafanaSelect);
|
||||||
export const selectPersistedApplicationsMimir = async () => click(getPersistedApplicationsMimirSelect);
|
export const selectPersistedApplicationsMimir = async () => click(getPersistedApplicationsMimirSelect);
|
||||||
|
export const selectPersistedApplicationsGrafana = async () => click(getPersistedApplicationsGrafanaSelect);
|
||||||
export const selectResultApplicationsMimir = async () => click(getResultApplicationsMimirSelect);
|
export const selectResultApplicationsMimir = async () => click(getResultApplicationsMimirSelect);
|
||||||
export const selectResultApplicationsCloud = async () => click(getResultApplicationsCloudSelect);
|
export const selectResultApplicationsCloud = async () => click(getResultApplicationsCloudSelect);
|
||||||
export const selectResultApplicationsCloudDev = async () => click(getResultApplicationsCloudDevSelect);
|
export const selectResultApplicationsCloudDev = async () => click(getResultApplicationsCloudDevSelect);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import {
|
||||||
getNotFoundForScope,
|
getNotFoundForScope,
|
||||||
getNotFoundNoScopes,
|
getNotFoundNoScopes,
|
||||||
getPersistedApplicationsMimirSelect,
|
getPersistedApplicationsMimirSelect,
|
||||||
|
getRecentScopeSet,
|
||||||
|
getRecentScopesSection,
|
||||||
getResultApplicationsCloudSelect,
|
getResultApplicationsCloudSelect,
|
||||||
getResultApplicationsGrafanaSelect,
|
getResultApplicationsGrafanaSelect,
|
||||||
getResultApplicationsMimirSelect,
|
getResultApplicationsMimirSelect,
|
||||||
|
|
@ -25,6 +27,8 @@ import {
|
||||||
queryDashboardsSearch,
|
queryDashboardsSearch,
|
||||||
queryPersistedApplicationsGrafanaSelect,
|
queryPersistedApplicationsGrafanaSelect,
|
||||||
queryPersistedApplicationsMimirSelect,
|
queryPersistedApplicationsMimirSelect,
|
||||||
|
queryRecentScopeSet,
|
||||||
|
queryRecentScopesSection,
|
||||||
queryResultApplicationsCloudSelect,
|
queryResultApplicationsCloudSelect,
|
||||||
queryResultApplicationsGrafanaSelect,
|
queryResultApplicationsGrafanaSelect,
|
||||||
queryResultApplicationsMimirSelect,
|
queryResultApplicationsMimirSelect,
|
||||||
|
|
@ -40,6 +44,10 @@ const expectValue = (selector: () => HTMLInputElement, value: string) => expect(
|
||||||
const expectTextContent = (selector: () => HTMLElement, text: string) => expect(selector()).toHaveTextContent(text);
|
const expectTextContent = (selector: () => HTMLElement, text: string) => expect(selector()).toHaveTextContent(text);
|
||||||
const expectDisabled = (selector: () => HTMLElement) => expect(selector()).toBeDisabled();
|
const expectDisabled = (selector: () => HTMLElement) => expect(selector()).toBeDisabled();
|
||||||
|
|
||||||
|
export const expectRecentScopeNotPresent = (scope: string) => expectNotInDocument(() => queryRecentScopeSet(scope));
|
||||||
|
export const expectRecentScope = (scope: string) => expectInDocument(() => getRecentScopeSet(scope));
|
||||||
|
export const expectRecentScopeNotPresentInDocument = () => expectNotInDocument(queryRecentScopesSection);
|
||||||
|
export const expectRecentScopesSection = () => expectInDocument(getRecentScopesSection);
|
||||||
export const expectScopesSelectorClosed = () => expectNotInDocument(querySelectorApply);
|
export const expectScopesSelectorClosed = () => expectNotInDocument(querySelectorApply);
|
||||||
export const expectScopesSelectorDisabled = () => expectDisabled(getSelectorInput);
|
export const expectScopesSelectorDisabled = () => expectDisabled(getSelectorInput);
|
||||||
export const expectScopesSelectorValue = (value: string) => expectValue(getSelectorInput, value);
|
export const expectScopesSelectorValue = (value: string) => expectValue(getSelectorInput, value);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { ScopesSelectorService } from '../../selector/ScopesSelectorService';
|
||||||
|
|
||||||
const selectors = {
|
const selectors = {
|
||||||
tree: {
|
tree: {
|
||||||
|
recentScopesSection: 'scopes-selector-recent-scopes-section',
|
||||||
search: 'scopes-tree-search',
|
search: 'scopes-tree-search',
|
||||||
headline: 'scopes-tree-headline',
|
headline: 'scopes-tree-headline',
|
||||||
select: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-checkbox`,
|
select: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-checkbox`,
|
||||||
|
|
@ -18,6 +19,7 @@ const selectors = {
|
||||||
loading: 'scopes-selector-loading',
|
loading: 'scopes-selector-loading',
|
||||||
apply: 'scopes-selector-apply',
|
apply: 'scopes-selector-apply',
|
||||||
cancel: 'scopes-selector-cancel',
|
cancel: 'scopes-selector-cancel',
|
||||||
|
clear: 'scopes-selector-input-clear',
|
||||||
},
|
},
|
||||||
dashboards: {
|
dashboards: {
|
||||||
expand: 'scopes-dashboards-expand',
|
expand: 'scopes-dashboards-expand',
|
||||||
|
|
@ -34,10 +36,16 @@ const selectors = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSelectorInput = () => screen.getByTestId<HTMLInputElement>(selectors.selector.input);
|
export const getSelectorInput = () => screen.getByTestId<HTMLInputElement>(selectors.selector.input);
|
||||||
|
export const getSelectorClear = () => screen.getByTestId(selectors.selector.clear);
|
||||||
export const querySelectorApply = () => screen.queryByTestId(selectors.selector.apply);
|
export const querySelectorApply = () => screen.queryByTestId(selectors.selector.apply);
|
||||||
export const getSelectorApply = () => screen.getByTestId(selectors.selector.apply);
|
export const getSelectorApply = () => screen.getByTestId(selectors.selector.apply);
|
||||||
export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel);
|
export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel);
|
||||||
|
|
||||||
|
export const getRecentScopesSection = () => screen.getByTestId(selectors.tree.recentScopesSection);
|
||||||
|
export const queryRecentScopesSection = () => screen.queryByTestId(selectors.tree.recentScopesSection);
|
||||||
|
export const getRecentScopeSet = (scope: string) => screen.getByRole('button', { name: scope });
|
||||||
|
export const queryRecentScopeSet = (scope: string) => screen.queryByRole('button', { name: scope });
|
||||||
|
|
||||||
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 getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container);
|
||||||
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
|
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
|
||||||
|
|
@ -63,6 +71,8 @@ export const getResultApplicationsGrafanaSelect = () =>
|
||||||
screen.getByTestId<HTMLInputElement>(selectors.tree.select('applications-grafana', 'result'));
|
screen.getByTestId<HTMLInputElement>(selectors.tree.select('applications-grafana', 'result'));
|
||||||
export const queryPersistedApplicationsGrafanaSelect = () =>
|
export const queryPersistedApplicationsGrafanaSelect = () =>
|
||||||
screen.queryByTestId<HTMLInputElement>(selectors.tree.select('applications-grafana', 'persisted'));
|
screen.queryByTestId<HTMLInputElement>(selectors.tree.select('applications-grafana', 'persisted'));
|
||||||
|
export const getPersistedApplicationsGrafanaSelect = () =>
|
||||||
|
screen.getByTestId(selectors.tree.select('applications-grafana', 'persisted'));
|
||||||
export const queryResultApplicationsMimirSelect = () =>
|
export const queryResultApplicationsMimirSelect = () =>
|
||||||
screen.queryByTestId(selectors.tree.select('applications-mimir', 'result'));
|
screen.queryByTestId(selectors.tree.select('applications-mimir', 'result'));
|
||||||
export const getResultApplicationsMimirSelect = () =>
|
export const getResultApplicationsMimirSelect = () =>
|
||||||
|
|
|
||||||
|
|
@ -2719,7 +2719,8 @@
|
||||||
"folder-search-results": "Folders",
|
"folder-search-results": "Folders",
|
||||||
"pages": "Pages",
|
"pages": "Pages",
|
||||||
"preferences": "Preferences",
|
"preferences": "Preferences",
|
||||||
"recent-dashboards": "Recent dashboards"
|
"recent-dashboards": "Recent dashboards",
|
||||||
|
"recent-scopes": "Recent scopes"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue