diff --git a/apps/scope/pkg/apis/scope/v0alpha1/types.go b/apps/scope/pkg/apis/scope/v0alpha1/types.go index 799195b2832..b0f58fdfd44 100644 --- a/apps/scope/pkg/apis/scope/v0alpha1/types.go +++ b/apps/scope/pkg/apis/scope/v0alpha1/types.go @@ -146,7 +146,12 @@ type ScopeNodeSpec struct { NodeType NodeType `json:"nodeType"` // container | leaf - Title string `json:"title"` + Title string `json:"title"` + //+optional + // Displays next to the title to provide more context. + SubTitle string `json:"subTitle,omitempty"` + + //+optional Description string `json:"description,omitempty"` DisableMultiSelect bool `json:"disableMultiSelect"` diff --git a/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go b/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go index 243b0ff2c2e..a0b0e90b557 100644 --- a/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go +++ b/apps/scope/pkg/apis/scope/v0alpha1/zz_generated.openapi.go @@ -838,6 +838,13 @@ func schema_pkg_apis_scope_v0alpha1_ScopeNodeSpec(ref common.ReferenceCallback) Format: "", }, }, + "subTitle": { + SchemaProps: spec.SchemaProps{ + Description: "Displays next to the title to provide more context.", + Type: []string{"string"}, + Format: "", + }, + }, "description": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, diff --git a/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts b/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts index 919930027b8..7d8d202cf41 100644 --- a/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts +++ b/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts @@ -9,7 +9,6 @@ import { openScopesSelector, searchScopes, selectScope, - TestScope, } from '../utils/scope-helpers'; import { testScopes } from '../utils/scopes'; diff --git a/e2e-playwright/utils/scope-helpers.ts b/e2e-playwright/utils/scope-helpers.ts index 4fb12cc41da..749577ecb05 100644 --- a/e2e-playwright/utils/scope-helpers.ts +++ b/e2e-playwright/utils/scope-helpers.ts @@ -128,7 +128,7 @@ export async function scopeSelectRequest(page: Page, selectedScope: TestScope): export async function selectScope(page: Page, scopeName: string, selectedScope?: TestScope) { const click = async () => { const element = page.locator( - `[data-testid="scopes-tree-${scopeName}-checkbox"], [data-testid="scopes-tree-${scopeName}-radio"]` + `[data-testid="scopes-tree-${scopeName}-checkbox"], [data-testid="scopes-tree-${scopeName}-radio"], [data-testid="scopes-tree-${scopeName}-link"]` ); await element.scrollIntoViewIfNeeded(); await element.click({ force: true }); @@ -254,9 +254,9 @@ export async function getScopeTreeName(page: Page, nth: number): Promise } export async function getScopeLeafName(page: Page, nth: number): Promise { - const locator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio)/).nth(nth); + const locator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio|link)/).nth(nth); const fullTestId = await locator.getAttribute('data-testid'); - const scopeName = fullTestId?.replace(/^scopes-tree-/, '').replace(/-(checkbox|radio)/, ''); + const scopeName = fullTestId?.replace(/^scopes-tree-/, '').replace(/-(checkbox|radio|link)/, ''); if (!scopeName) { throw new Error('There are no scopes in the selector'); @@ -266,10 +266,11 @@ export async function getScopeLeafName(page: Page, nth: number): Promise } export async function getScopeLeafTitle(page: Page, nth: number): Promise { - const leafLocator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio)/).nth(nth); + // Get the nth selectable tree item (checkbox, radio, or link) + const leafLocator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio|link)/).nth(nth); // Find the closest ancestor element that has the main tree item test id const titleLocator = leafLocator.locator( - 'xpath=ancestor::*[@data-testid][starts-with(@data-testid, "scopes-tree-") and not(contains(@data-testid, "-checkbox")) and not(contains(@data-testid, "-radio")) and not(contains(@data-testid, "-expand"))]' + 'xpath=ancestor::*[@data-testid][starts-with(@data-testid, "scopes-tree-") and not(contains(@data-testid, "-checkbox")) and not(contains(@data-testid, "-radio")) and not(contains(@data-testid, "-link")) and not(contains(@data-testid, "-expand"))]' ); const scopeTitle = await titleLocator.textContent(); if (!scopeTitle) { diff --git a/packages/grafana-data/src/types/scopes.ts b/packages/grafana-data/src/types/scopes.ts index 98b32dfa0c7..e5897c137da 100644 --- a/packages/grafana-data/src/types/scopes.ts +++ b/packages/grafana-data/src/types/scopes.ts @@ -70,7 +70,7 @@ export type ScopeNodeLinkType = 'scope'; export interface ScopeNodeSpec { nodeType: ScopeNodeNodeType; title: string; - + subTitle?: string; description?: string; // If true for a scope category/type, it means only single child can be selected at a time. diff --git a/pkg/services/frontend/index.go b/pkg/services/frontend/index.go index 89f7fcaf066..ec70d83468a 100644 --- a/pkg/services/frontend/index.go +++ b/pkg/services/frontend/index.go @@ -153,6 +153,7 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http. p.runIndexDataHooks(reqCtx, &data) writer.Header().Set("Content-Type", "text/html; charset=UTF-8") + writer.Header().Set("Cache-Control", "no-store") writer.WriteHeader(200) if err := p.index.Execute(writer, &data); err != nil { if errors.Is(err, syscall.EPIPE) { // Client has stopped listening. diff --git a/pkg/services/frontend/index_test.go b/pkg/services/frontend/index_test.go index 3f0b76ac4f4..46fdeba4410 100644 --- a/pkg/services/frontend/index_test.go +++ b/pkg/services/frontend/index_test.go @@ -92,6 +92,7 @@ func TestFrontendService_WebAssets(t *testing.T) { assert.Equal(t, 200, recorder.Code) assert.Contains(t, recorder.Header().Get("Content-Type"), "text/html") + assert.Contains(t, recorder.Header().Get("Cache-Control"), "no-store") // The response should contain references to the assets body := recorder.Body.String() diff --git a/public/app/features/scopes/selector/ScopesTreeItem.tsx b/public/app/features/scopes/selector/ScopesTreeItem.tsx index 3dfe68e4209..235a65fb19e 100644 --- a/public/app/features/scopes/selector/ScopesTreeItem.tsx +++ b/public/app/features/scopes/selector/ScopesTreeItem.tsx @@ -2,13 +2,177 @@ import { css, cx } from '@emotion/css'; import Highlighter from 'react-highlight-words'; import { GrafanaTheme2 } from '@grafana/data'; -import { t } from '@grafana/i18n'; -import { Checkbox, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui'; +import { Checkbox, Icon, RadioButtonDot, useStyles2, Text } from '@grafana/ui'; + +import { useScopesServices } from '../ScopesContextProvider'; import { ScopesTree } from './ScopesTree'; import { isNodeExpandable, isNodeSelectable } from './scopesTreeUtils'; import { NodesMap, SelectedScope, TreeNode } from './types'; +// Helper components for rendering different selectable content types +interface RadioButtonDotProps { + scopeNodeId: string; + selected: boolean; + onChange: () => void; + children?: React.ReactNode; + 'aria-labelledby'?: string; +} + +function ScopeRadioButtonDot({ + scopeNodeId, + selected, + onChange, + children, + 'aria-labelledby': ariaLabelledby, +}: RadioButtonDotProps) { + return ( + + ); +} + +interface LinkLikeButtonProps { + scopeNodeId: string; + onClick: () => void; + children: React.ReactNode; +} + +function ScopeLinkLikeButton({ scopeNodeId, onClick, children }: LinkLikeButtonProps) { + const styles = useStyles2(getStyles); + return ( + + ); +} + +interface CheckboxWithLabelProps { + scopeNodeId: string; + selected: boolean; + showLabel: boolean; + onChange: () => void; + children?: React.ReactNode; + 'aria-labelledby'?: string; +} + +function ScopeCheckboxWithLabel({ + scopeNodeId, + selected, + showLabel, + onChange, + children, + 'aria-labelledby': ariaLabelledby, +}: CheckboxWithLabelProps) { + const styles = useStyles2(getStyles); + return ( +
+ + {showLabel && ( + + )} +
+ ); +} + +interface TitleContentProps { + shouldHighlight: boolean; + titleText: string; + searchWords: string[]; +} + +function TitleContent({ shouldHighlight, titleText, searchWords }: TitleContentProps) { + if (shouldHighlight) { + return ; + } + return <>{titleText}; +} + +interface ExpandButtonProps { + scopeNodeId: string; + expanded: boolean; + onClick: () => void; + children: React.ReactNode; + controlsId: string; + isSelectable: boolean; + disableMultiSelect: boolean; + selected: boolean; + onSelect: () => void; +} + +function ScopeExpandButton({ + scopeNodeId, + expanded, + onClick, + children, + controlsId, + isSelectable, + disableMultiSelect, + selected, + onSelect, +}: ExpandButtonProps) { + const styles = useStyles2(getStyles); + + const buttonId = getTreeItemElementId(scopeNodeId) + '-button'; + + const SelectComponent = () => { + if (!isSelectable || expanded) { + return null; + } + if (disableMultiSelect) { + return ( + + ); + } + return ( + + ); + }; + return ( + <> + + + + ); +} + export interface ScopesTreeItemProps { anyChildExpanded: boolean; loadingNodeName: string | undefined; @@ -38,7 +202,8 @@ export function ScopesTreeItem({ toggleExpandedNode, }: ScopesTreeItemProps) { const styles = useStyles2(getStyles); - + const services = useScopesServices(); + const { closeAndApply } = services?.scopesSelectorService || {}; if (anyChildExpanded && !treeNode.expanded) { return null; } @@ -48,6 +213,7 @@ export function ScopesTreeItem({ // Should not happen as only way we show a tree is if we also load the nodes. return null; } + const parentNode = scopeNode.spec.parentName ? scopeNodes[scopeNode.spec.parentName] : undefined; const disableMultiSelect = parentNode?.spec.disableMultiSelect ?? false; @@ -57,9 +223,11 @@ export function ScopesTreeItem({ // Create search words for highlighting if there's a query // Only highlight if we have a query AND this node is not expanded (not a parent showing children) const titleText = scopeNode.spec.title; - const shouldHighlight = treeNode.query && !treeNode.expanded; + const shouldHighlight = Boolean(treeNode.query && !treeNode.expanded); const searchWords = shouldHighlight ? getSearchWordsFromQuery(treeNode.query) : []; + const childrenId = getTreeItemElementId(treeNode.scopeNodeId) + '-children'; + return (
- {isSelectable && !treeNode.expanded ? ( - disableMultiSelect ? ( - - ) : ( - titleText - ) - } - data-testid={`scopes-tree-${treeNode.scopeNodeId}-radio`} - onClick={() => { - selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId); - }} - /> - ) : ( -
- + {disableMultiSelect && ( + { + selectScope(treeNode.scopeNodeId); + closeAndApply?.(); + }} + > + + + )} + {!disableMultiSelect && ( + { selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId); }} - /> - {!isExpandable && ( - - )} -
- ) - ) : null} + > + + + )} + + )} {isExpandable && ( - + {scopeNode.spec.subTitle && ( + + {scopeNode.spec.subTitle} + )}
-
+
{treeNode.expanded && ( { margin: 0, padding: 0, }), + linkLikeItem: css({ + alignItems: 'center', + background: 'none', + border: 0, + display: 'flex', + gap: theme.spacing(1), + margin: 0, + padding: 0, + textDecoration: 'none', + + '&:hover': { + textDecoration: 'underline', + }, + }), children: css({ display: 'flex', flexDirection: 'column', diff --git a/public/app/features/scopes/selector/ScopesTreeSearch.tsx b/public/app/features/scopes/selector/ScopesTreeSearch.tsx index 780165f6074..22e8129ce66 100644 --- a/public/app/features/scopes/selector/ScopesTreeSearch.tsx +++ b/public/app/features/scopes/selector/ScopesTreeSearch.tsx @@ -79,7 +79,12 @@ export function ScopesTreeSearch({ setInputState({ value, dirty: true }); }} onFocus={onFocus} - onBlur={onBlur} + onBlur={() => { + // TODO:Handle weird race condition where the blur event interupts selection of a radio button. This is because disableHighlighting is called, which forces a re-render of the tree. This re-render causes the radio button to lose focus, and the selection to be interrupted. + setTimeout(() => { + onBlur(); + }, 0); + }} /> ); } diff --git a/public/app/features/scopes/selector/useScopesHighlighting.tsx b/public/app/features/scopes/selector/useScopesHighlighting.tsx index 75f573a3422..c0b8aef8002 100644 --- a/public/app/features/scopes/selector/useScopesHighlighting.tsx +++ b/public/app/features/scopes/selector/useScopesHighlighting.tsx @@ -1,5 +1,7 @@ import { useState } from 'react'; +import { useScopesServices } from '../ScopesContextProvider'; + import { getTreeItemElementId } from './ScopesTreeItem'; import { isNodeExpandable, isNodeSelectable } from './scopesTreeUtils'; import { NodesMap, SelectedScope, TreeNode } from './types'; @@ -28,6 +30,8 @@ export function useScopesHighlighting({ }: UseScopesHighlightingParams) { // Enable keyboard highlighting when the search field is focused const [highlightEnabled, setHighlightEnabled] = useState(false); + const services = useScopesServices(); + const { changeScopes } = services?.scopesSelectorService || {}; const items = [...selectedNodes, ...resultNodes]; @@ -52,6 +56,16 @@ export function useScopesHighlighting({ return; } + // If parent has disableMultiSelect, apply scope directly + const parentNode = scopeNodes[nodeId]?.spec.parentName + ? scopeNodes[scopeNodes[nodeId]?.spec.parentName] + : undefined; + + if (parentNode?.spec.disableMultiSelect && changeScopes && scopeNodes[nodeId]?.spec.linkId) { + changeScopes([scopeNodes[nodeId].spec.linkId], parentNode.metadata.name); + return; + } + // Toggle selection if (selectedScopes.some((s) => s.scopeNodeId === nodeId)) { deselectScope(nodeId); diff --git a/public/app/features/scopes/tests/tree.test.ts b/public/app/features/scopes/tests/tree.test.ts index eefacb4ee9c..f0f46e2dd9c 100644 --- a/public/app/features/scopes/tests/tree.test.ts +++ b/public/app/features/scopes/tests/tree.test.ts @@ -21,6 +21,9 @@ import { selectResultCloud, selectResultCloudDev, selectResultCloudOps, + expandResultEnvironments, + selectResultEnvironmentsDev, + selectResultEnvironmentsProd, updateScopes, } from './utils/actions'; import { @@ -33,10 +36,10 @@ import { expectResultApplicationsMimirNotPresent, expectResultApplicationsMimirPresent, expectResultApplicationsMimirSelected, - expectResultCloudDevNotSelected, - expectResultCloudDevSelected, - expectResultCloudOpsNotSelected, - expectResultCloudOpsSelected, + expectResultEnvironmentsDevNotSelected, + expectResultEnvironmentsDevSelected, + expectResultEnvironmentsProdNotSelected, + expectResultEnvironmentsProdSelected, expectScopesHeadline, expectScopesSelectorValue, } from './utils/assertions'; @@ -133,12 +136,33 @@ describe('Tree', () => { await openSelector(); await expandResultCloud(); await selectResultCloudDev(); - expectResultCloudDevSelected(); - expectResultCloudOpsNotSelected(); + // Verify the content of the scopes selector input + expectScopesSelectorValue('Dev'); + + // Single leaf node links always apply the scope, hence we need to open the selector again + await openSelector(); await selectResultCloudOps(); - expectResultCloudDevNotSelected(); - expectResultCloudOpsSelected(); + expectScopesSelectorValue('Ops'); + }); + + it('Can only select one selectable container at a time', async () => { + await openSelector(); + await expandResultEnvironments(); + + // Select the Development environment container + await selectResultEnvironmentsDev(); + expectResultEnvironmentsDevSelected(); // Check selection state before applying + expectResultEnvironmentsProdNotSelected(); // Production should not be selected + + // Select the Production environment container - should replace Development + await selectResultEnvironmentsProd(); + expectResultEnvironmentsProdSelected(); // Check selection state before applying + expectResultEnvironmentsDevNotSelected(); // Development should no longer be selected + + // Apply scopes and verify final state + await applyScopes(); + expectScopesSelectorValue('Production'); }); it('Search works', async () => { @@ -276,7 +300,7 @@ describe('Tree', () => { await openSelector(); // Verify that Cloud is expanded - expect(screen.getByRole('button', { name: 'Collapse Cloud' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cloud' })).toBeInTheDocument(); }); describe('Keyboard Navigation', () => { diff --git a/public/app/features/scopes/tests/utils/actions.ts b/public/app/features/scopes/tests/utils/actions.ts index e5b7eb44a20..b018ab674be 100644 --- a/public/app/features/scopes/tests/utils/actions.ts +++ b/public/app/features/scopes/tests/utils/actions.ts @@ -22,16 +22,19 @@ import { getResultApplicationsCloudSelect, getResultApplicationsGrafanaSelect, getResultApplicationsMimirSelect, - getResultCloudDevRadio, getResultCloudExpand, - getResultCloudOpsRadio, getResultCloudSelect, + getResultEnvironmentsExpand, + getResultEnvironmentsDevSelect, + getResultEnvironmentsProdSelect, getSelectorApply, getSelectorCancel, getSelectorClear, getSelectorInput, getTreeSearch, findResultApplicationsExpand, + getResultCloudDevLink, + getResultCloudOpsLink, } from './selectors'; const click = async (selector: () => HTMLElement) => act(() => userEvent.click(selector())); @@ -68,8 +71,12 @@ export const selectResultApplicationsMimir = async () => click(getResultApplicat export const selectResultApplicationsCloud = async () => click(getResultApplicationsCloudSelect); export const selectResultApplicationsCloudDev = async () => click(getResultApplicationsCloudDevSelect); export const selectResultCloud = async () => click(getResultCloudSelect); -export const selectResultCloudDev = async () => click(getResultCloudDevRadio); -export const selectResultCloudOps = async () => click(getResultCloudOpsRadio); +export const selectResultCloudDev = async () => click(getResultCloudDevLink); +export const selectResultCloudOps = async () => click(getResultCloudOpsLink); + +export const expandResultEnvironments = async () => click(getResultEnvironmentsExpand); +export const selectResultEnvironmentsDev = async () => click(getResultEnvironmentsDevSelect); +export const selectResultEnvironmentsProd = async () => click(getResultEnvironmentsProdSelect); export const toggleDashboards = async () => click(getDashboardsExpand); export const searchDashboards = async (value: string) => type(getDashboardsSearch, value); diff --git a/public/app/features/scopes/tests/utils/assertions.ts b/public/app/features/scopes/tests/utils/assertions.ts index cab6c527c06..92e98715231 100644 --- a/public/app/features/scopes/tests/utils/assertions.ts +++ b/public/app/features/scopes/tests/utils/assertions.ts @@ -12,8 +12,6 @@ import { getResultApplicationsCloudSelect, getResultApplicationsGrafanaSelect, getResultApplicationsMimirSelect, - getResultCloudDevRadio, - getResultCloudOpsRadio, getSelectorInput, getTreeHeadline, queryAllDashboard, @@ -28,14 +26,16 @@ import { queryResultApplicationsCloudSelect, queryResultApplicationsGrafanaSelect, queryResultApplicationsMimirSelect, + getResultEnvironmentsDevSelect, + getResultEnvironmentsProdSelect, querySelectorApply, } from './selectors'; const expectInDocument = (selector: () => HTMLElement) => expect(selector()).toBeInTheDocument(); const expectNotInDocument = (selector: () => HTMLElement | null) => expect(selector()).not.toBeInTheDocument(); const expectChecked = (selector: () => HTMLInputElement) => expect(selector()).toBeChecked(); -const expectRadioChecked = (selector: () => HTMLInputElement) => expect(selector().checked).toBe(true); -const expectRadioNotChecked = (selector: () => HTMLInputElement) => expect(selector().checked).toBe(false); +const expectNotChecked = (selector: () => HTMLInputElement) => expect(selector()).not.toBeChecked(); + const expectValue = (selector: () => HTMLInputElement, value: string) => expect(selector().value).toBe(value); const expectTextContent = (selector: () => HTMLElement, text: string) => expect(selector()).toHaveTextContent(text); const expectDisabled = (selector: () => HTMLElement) => expect(selector()).toBeDisabled(); @@ -62,10 +62,11 @@ export const expectResultApplicationsMimirPresent = () => expectInDocument(getRe export const expectResultApplicationsMimirNotPresent = () => expectNotInDocument(queryResultApplicationsMimirSelect); export const expectResultApplicationsCloudPresent = () => expectInDocument(getResultApplicationsCloudSelect); export const expectResultApplicationsCloudNotPresent = () => expectNotInDocument(queryResultApplicationsCloudSelect); -export const expectResultCloudDevSelected = () => expectRadioChecked(getResultCloudDevRadio); -export const expectResultCloudDevNotSelected = () => expectRadioNotChecked(getResultCloudDevRadio); -export const expectResultCloudOpsSelected = () => expectRadioChecked(getResultCloudOpsRadio); -export const expectResultCloudOpsNotSelected = () => expectRadioNotChecked(getResultCloudOpsRadio); + +export const expectResultEnvironmentsDevSelected = () => expectChecked(getResultEnvironmentsDevSelect); +export const expectResultEnvironmentsDevNotSelected = () => expectNotChecked(getResultEnvironmentsDevSelect); +export const expectResultEnvironmentsProdSelected = () => expectChecked(getResultEnvironmentsProdSelect); +export const expectResultEnvironmentsProdNotSelected = () => expectNotChecked(getResultEnvironmentsProdSelect); export const expectDashboardsDisabled = () => expectDisabled(getDashboardsExpand); export const expectDashboardsClosed = () => expectNotInDocument(queryDashboardsContainer); diff --git a/public/app/features/scopes/tests/utils/mocks.ts b/public/app/features/scopes/tests/utils/mocks.ts index a94093747c9..7b306350eb5 100644 --- a/public/app/features/scopes/tests/utils/mocks.ts +++ b/public/app/features/scopes/tests/utils/mocks.ts @@ -59,6 +59,20 @@ export const mocksScopes: Scope[] = [ filters: [{ key: 'app', value: 'tempo', operator: 'equals' }], }, }, + { + metadata: { name: 'dev-env' }, + spec: { + title: 'Development', + filters: [{ key: 'environment', value: 'dev', operator: 'equals' }], + }, + }, + { + metadata: { name: 'prod-env' }, + spec: { + title: 'Production', + filters: [{ key: 'environment', value: 'prod', operator: 'equals' }], + }, + }, ] as const; const dashboardBindingsGenerator = ( @@ -341,6 +355,38 @@ export const mocksNodes: ScopeNode[] = [ parentName: 'cloud-applications', }, }, + { + metadata: { name: 'environments' }, + spec: { + nodeType: 'container', + title: 'Environments', + description: 'Environment Scopes', + disableMultiSelect: true, + parentName: '', + }, + }, + { + metadata: { name: 'environments-dev' }, + spec: { + nodeType: 'container', + title: 'Development', + description: 'Development Environment', + linkType: 'scope', + linkId: 'dev-env', + parentName: 'environments', + }, + }, + { + metadata: { name: 'environments-prod' }, + spec: { + nodeType: 'container', + title: 'Production', + description: 'Production Environment', + linkType: 'scope', + linkId: 'prod-env', + parentName: 'environments', + }, + }, ] as const; export const dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard'); diff --git a/public/app/features/scopes/tests/utils/selectors.ts b/public/app/features/scopes/tests/utils/selectors.ts index 0f49a10c133..4e3b7ed109a 100644 --- a/public/app/features/scopes/tests/utils/selectors.ts +++ b/public/app/features/scopes/tests/utils/selectors.ts @@ -9,6 +9,7 @@ const selectors = { headline: 'scopes-tree-headline', select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`, radio: (nodeId: string) => `scopes-tree-${nodeId}-radio`, + link: (nodeId: string) => `scopes-tree-${nodeId}-link`, expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`, title: (nodeId: string) => `scopes-tree-${nodeId}-title`, }, @@ -93,7 +94,15 @@ export const getResultApplicationsCloudDevSelect = () => export const getResultCloudSelect = () => screen.getByTestId(selectors.tree.select('cloud')); export const getResultCloudExpand = () => screen.getByTestId(selectors.tree.expand('cloud')); -export const getResultCloudDevRadio = () => screen.getByTestId(selectors.tree.radio('cloud-dev')); -export const getResultCloudOpsRadio = () => screen.getByTestId(selectors.tree.radio('cloud-ops')); +export const getResultCloudDevLink = () => screen.getByTestId(selectors.tree.link('cloud-dev')); +export const getResultCloudOpsLink = () => screen.getByTestId(selectors.tree.link('cloud-ops')); + +export const getResultEnvironmentsExpand = () => screen.getByTestId(selectors.tree.expand('environments')); +export const getResultEnvironmentsDevSelect = () => + screen.getByTestId(selectors.tree.radio('environments-dev')); +export const getResultEnvironmentsProdSelect = () => + screen.getByTestId(selectors.tree.radio('environments-prod')); +export const queryResultEnvironmentsDevSelect = () => screen.queryByTestId(selectors.tree.radio('environments-dev')); +export const queryResultEnvironmentsProdSelect = () => screen.queryByTestId(selectors.tree.radio('environments-prod')); export const getListOfScopes = (service: ScopesService) => service.state.value; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 1beea7c487a..c458e08fe59 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12305,8 +12305,6 @@ "title": "Select scopes" }, "tree": { - "collapse": "Collapse {{title}}", - "expand": "Expand {{title}}", "headline": { "noResults": "No results found for your query", "recommended": "Recommended",