Merge remote-tracking branch 'origin/main' into query-history-app

This commit is contained in:
Ryan McKinley
2025-10-29 13:25:47 +03:00
16 changed files with 379 additions and 97 deletions
+6 -1
View File
@@ -146,7 +146,12 @@ type ScopeNodeSpec struct {
NodeType NodeType `json:"nodeType"` // container | leaf 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"` Description string `json:"description,omitempty"`
DisableMultiSelect bool `json:"disableMultiSelect"` DisableMultiSelect bool `json:"disableMultiSelect"`
@@ -838,6 +838,13 @@ func schema_pkg_apis_scope_v0alpha1_ScopeNodeSpec(ref common.ReferenceCallback)
Format: "", Format: "",
}, },
}, },
"subTitle": {
SchemaProps: spec.SchemaProps{
Description: "Displays next to the title to provide more context.",
Type: []string{"string"},
Format: "",
},
},
"description": { "description": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Type: []string{"string"}, Type: []string{"string"},
@@ -9,7 +9,6 @@ import {
openScopesSelector, openScopesSelector,
searchScopes, searchScopes,
selectScope, selectScope,
TestScope,
} from '../utils/scope-helpers'; } from '../utils/scope-helpers';
import { testScopes } from '../utils/scopes'; import { testScopes } from '../utils/scopes';
+6 -5
View File
@@ -128,7 +128,7 @@ export async function scopeSelectRequest(page: Page, selectedScope: TestScope):
export async function selectScope(page: Page, scopeName: string, selectedScope?: TestScope) { export async function selectScope(page: Page, scopeName: string, selectedScope?: TestScope) {
const click = async () => { const click = async () => {
const element = page.locator( 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.scrollIntoViewIfNeeded();
await element.click({ force: true }); await element.click({ force: true });
@@ -254,9 +254,9 @@ export async function getScopeTreeName(page: Page, nth: number): Promise<string>
} }
export async function getScopeLeafName(page: Page, nth: number): Promise<string> { export async function getScopeLeafName(page: Page, nth: number): Promise<string> {
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 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) { if (!scopeName) {
throw new Error('There are no scopes in the selector'); throw new Error('There are no scopes in the selector');
@@ -266,10 +266,11 @@ export async function getScopeLeafName(page: Page, nth: number): Promise<string>
} }
export async function getScopeLeafTitle(page: Page, nth: number): Promise<string> { export async function getScopeLeafTitle(page: Page, nth: number): Promise<string> {
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 // Find the closest ancestor element that has the main tree item test id
const titleLocator = leafLocator.locator( 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(); const scopeTitle = await titleLocator.textContent();
if (!scopeTitle) { if (!scopeTitle) {
+1 -1
View File
@@ -70,7 +70,7 @@ export type ScopeNodeLinkType = 'scope';
export interface ScopeNodeSpec { export interface ScopeNodeSpec {
nodeType: ScopeNodeNodeType; nodeType: ScopeNodeNodeType;
title: string; title: string;
subTitle?: string;
description?: string; description?: string;
// If true for a scope category/type, it means only single child can be selected at a time. // If true for a scope category/type, it means only single child can be selected at a time.
+1
View File
@@ -153,6 +153,7 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.
p.runIndexDataHooks(reqCtx, &data) p.runIndexDataHooks(reqCtx, &data)
writer.Header().Set("Content-Type", "text/html; charset=UTF-8") writer.Header().Set("Content-Type", "text/html; charset=UTF-8")
writer.Header().Set("Cache-Control", "no-store")
writer.WriteHeader(200) writer.WriteHeader(200)
if err := p.index.Execute(writer, &data); err != nil { if err := p.index.Execute(writer, &data); err != nil {
if errors.Is(err, syscall.EPIPE) { // Client has stopped listening. if errors.Is(err, syscall.EPIPE) { // Client has stopped listening.
+1
View File
@@ -92,6 +92,7 @@ func TestFrontendService_WebAssets(t *testing.T) {
assert.Equal(t, 200, recorder.Code) assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Header().Get("Content-Type"), "text/html") 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 // The response should contain references to the assets
body := recorder.Body.String() body := recorder.Body.String()
@@ -2,13 +2,177 @@ import { css, cx } from '@emotion/css';
import Highlighter from 'react-highlight-words'; import Highlighter from 'react-highlight-words';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n'; import { Checkbox, Icon, RadioButtonDot, useStyles2, Text } from '@grafana/ui';
import { Checkbox, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui';
import { useScopesServices } from '../ScopesContextProvider';
import { ScopesTree } from './ScopesTree'; import { ScopesTree } from './ScopesTree';
import { isNodeExpandable, isNodeSelectable } from './scopesTreeUtils'; import { isNodeExpandable, isNodeSelectable } from './scopesTreeUtils';
import { NodesMap, SelectedScope, TreeNode } from './types'; 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 (
<RadioButtonDot
id={scopeNodeId}
name={scopeNodeId}
checked={selected}
label={children ?? undefined}
data-testid={`scopes-tree-${scopeNodeId}-radio`}
onChange={onChange}
aria-labelledby={ariaLabelledby ?? undefined}
/>
);
}
interface LinkLikeButtonProps {
scopeNodeId: string;
onClick: () => void;
children: React.ReactNode;
}
function ScopeLinkLikeButton({ scopeNodeId, onClick, children }: LinkLikeButtonProps) {
const styles = useStyles2(getStyles);
return (
<button className={styles.linkLikeItem} data-testid={`scopes-tree-${scopeNodeId}-link`} onClick={onClick}>
{children}
</button>
);
}
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 (
<div className={styles.checkboxWithLabel}>
<Checkbox
id={scopeNodeId}
checked={selected}
data-testid={`scopes-tree-${scopeNodeId}-checkbox`}
label=""
onChange={onChange}
aria-labelledby={ariaLabelledby ?? undefined}
/>
{showLabel && (
<label htmlFor={scopeNodeId} className={styles.checkboxLabel}>
{children}
</label>
)}
</div>
);
}
interface TitleContentProps {
shouldHighlight: boolean;
titleText: string;
searchWords: string[];
}
function TitleContent({ shouldHighlight, titleText, searchWords }: TitleContentProps) {
if (shouldHighlight) {
return <Highlighter textToHighlight={titleText} searchWords={searchWords} autoEscape />;
}
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 (
<ScopeRadioButtonDot
scopeNodeId={scopeNodeId}
selected={selected}
onChange={onSelect}
aria-labelledby={buttonId}
/>
);
}
return (
<ScopeCheckboxWithLabel
scopeNodeId={scopeNodeId}
selected={selected}
showLabel={false}
onChange={onSelect}
aria-labelledby={buttonId}
/>
);
};
return (
<>
<SelectComponent />
<button
id={buttonId}
className={styles.expand}
data-testid={`scopes-tree-${scopeNodeId}-expand`}
onClick={onClick}
aria-expanded={expanded}
aria-controls={controlsId}
>
<Icon name={!expanded ? 'angle-right' : 'angle-down'} />
{children}
</button>
</>
);
}
export interface ScopesTreeItemProps { export interface ScopesTreeItemProps {
anyChildExpanded: boolean; anyChildExpanded: boolean;
loadingNodeName: string | undefined; loadingNodeName: string | undefined;
@@ -38,7 +202,8 @@ export function ScopesTreeItem({
toggleExpandedNode, toggleExpandedNode,
}: ScopesTreeItemProps) { }: ScopesTreeItemProps) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const services = useScopesServices();
const { closeAndApply } = services?.scopesSelectorService || {};
if (anyChildExpanded && !treeNode.expanded) { if (anyChildExpanded && !treeNode.expanded) {
return null; 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. // Should not happen as only way we show a tree is if we also load the nodes.
return null; return null;
} }
const parentNode = scopeNode.spec.parentName ? scopeNodes[scopeNode.spec.parentName] : undefined; const parentNode = scopeNode.spec.parentName ? scopeNodes[scopeNode.spec.parentName] : undefined;
const disableMultiSelect = parentNode?.spec.disableMultiSelect ?? false; const disableMultiSelect = parentNode?.spec.disableMultiSelect ?? false;
@@ -57,9 +223,11 @@ export function ScopesTreeItem({
// Create search words for highlighting if there's a query // 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) // Only highlight if we have a query AND this node is not expanded (not a parent showing children)
const titleText = scopeNode.spec.title; 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 searchWords = shouldHighlight ? getSearchWordsFromQuery(treeNode.query) : [];
const childrenId = getTreeItemElementId(treeNode.scopeNodeId) + '-children';
return ( return (
<div <div
key={treeNode.scopeNodeId} key={treeNode.scopeNodeId}
@@ -78,75 +246,57 @@ export function ScopesTreeItem({
)} )}
data-testid={`scopes-tree-${treeNode.scopeNodeId}`} data-testid={`scopes-tree-${treeNode.scopeNodeId}`}
> >
{isSelectable && !treeNode.expanded ? ( {isSelectable && !isExpandable && !treeNode.expanded && (
disableMultiSelect ? ( <>
<RadioButtonDot {disableMultiSelect && (
id={treeNode.scopeNodeId} <ScopeLinkLikeButton
name={treeNode.scopeNodeId} scopeNodeId={treeNode.scopeNodeId}
checked={selected} onClick={() => {
label={ selectScope(treeNode.scopeNodeId);
isExpandable ? ( closeAndApply?.();
'' }}
) : shouldHighlight ? ( >
<Highlighter textToHighlight={titleText} searchWords={searchWords} autoEscape /> <TitleContent shouldHighlight={shouldHighlight} titleText={titleText} searchWords={searchWords} />
) : ( </ScopeLinkLikeButton>
titleText )}
) {!disableMultiSelect && (
} <ScopeCheckboxWithLabel
data-testid={`scopes-tree-${treeNode.scopeNodeId}-radio`} scopeNodeId={treeNode.scopeNodeId}
onClick={() => { selected={selected}
selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId); showLabel={!isExpandable}
}}
/>
) : (
<div className={styles.checkboxWithLabel}>
<Checkbox
id={treeNode.scopeNodeId}
checked={selected}
data-testid={`scopes-tree-${treeNode.scopeNodeId}-checkbox`}
label=""
onChange={() => { onChange={() => {
selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId); selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId);
}} }}
/> >
{!isExpandable && ( <TitleContent shouldHighlight={shouldHighlight} titleText={titleText} searchWords={searchWords} />
<label htmlFor={treeNode.scopeNodeId} className={styles.checkboxLabel}> </ScopeCheckboxWithLabel>
{shouldHighlight ? ( )}
<Highlighter textToHighlight={titleText} searchWords={searchWords} autoEscape /> </>
) : ( )}
titleText
)}
</label>
)}
</div>
)
) : null}
{isExpandable && ( {isExpandable && (
<button <ScopeExpandButton
className={styles.expand} scopeNodeId={treeNode.scopeNodeId}
data-testid={`scopes-tree-${treeNode.scopeNodeId}-expand`} expanded={treeNode.expanded}
aria-label={ controlsId={childrenId}
treeNode.expanded onClick={() => toggleExpandedNode(treeNode.scopeNodeId)}
? t('scopes.tree.collapse', 'Collapse {{title}}', { title: titleText }) onSelect={() => (selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId))}
: t('scopes.tree.expand', 'Expand {{title}}', { title: titleText }) isSelectable={isSelectable}
} disableMultiSelect={disableMultiSelect}
onClick={() => { selected={selected}
toggleExpandedNode(treeNode.scopeNodeId);
}}
> >
<Icon name={!treeNode.expanded ? 'angle-right' : 'angle-down'} /> <TitleContent shouldHighlight={shouldHighlight} titleText={titleText} searchWords={searchWords} />
</ScopeExpandButton>
)}
{shouldHighlight ? ( {scopeNode.spec.subTitle && (
<Highlighter textToHighlight={titleText} searchWords={searchWords} autoEscape /> <Text truncate variant="body" color="secondary">
) : ( {scopeNode.spec.subTitle}
titleText </Text>
)}
</button>
)} )}
</div> </div>
<div className={styles.children}> <div id={childrenId} className={styles.children}>
{treeNode.expanded && ( {treeNode.expanded && (
<ScopesTree <ScopesTree
tree={treeNode} tree={treeNode}
@@ -227,6 +377,20 @@ const getStyles = (theme: GrafanaTheme2) => {
margin: 0, margin: 0,
padding: 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({ children: css({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -79,7 +79,12 @@ export function ScopesTreeSearch({
setInputState({ value, dirty: true }); setInputState({ value, dirty: true });
}} }}
onFocus={onFocus} 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);
}}
/> />
); );
} }
@@ -1,5 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useScopesServices } from '../ScopesContextProvider';
import { getTreeItemElementId } from './ScopesTreeItem'; import { getTreeItemElementId } from './ScopesTreeItem';
import { isNodeExpandable, isNodeSelectable } from './scopesTreeUtils'; import { isNodeExpandable, isNodeSelectable } from './scopesTreeUtils';
import { NodesMap, SelectedScope, TreeNode } from './types'; import { NodesMap, SelectedScope, TreeNode } from './types';
@@ -28,6 +30,8 @@ export function useScopesHighlighting({
}: UseScopesHighlightingParams) { }: UseScopesHighlightingParams) {
// Enable keyboard highlighting when the search field is focused // Enable keyboard highlighting when the search field is focused
const [highlightEnabled, setHighlightEnabled] = useState(false); const [highlightEnabled, setHighlightEnabled] = useState(false);
const services = useScopesServices();
const { changeScopes } = services?.scopesSelectorService || {};
const items = [...selectedNodes, ...resultNodes]; const items = [...selectedNodes, ...resultNodes];
@@ -52,6 +56,16 @@ export function useScopesHighlighting({
return; 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 // Toggle selection
if (selectedScopes.some((s) => s.scopeNodeId === nodeId)) { if (selectedScopes.some((s) => s.scopeNodeId === nodeId)) {
deselectScope(nodeId); deselectScope(nodeId);
+33 -9
View File
@@ -21,6 +21,9 @@ import {
selectResultCloud, selectResultCloud,
selectResultCloudDev, selectResultCloudDev,
selectResultCloudOps, selectResultCloudOps,
expandResultEnvironments,
selectResultEnvironmentsDev,
selectResultEnvironmentsProd,
updateScopes, updateScopes,
} from './utils/actions'; } from './utils/actions';
import { import {
@@ -33,10 +36,10 @@ import {
expectResultApplicationsMimirNotPresent, expectResultApplicationsMimirNotPresent,
expectResultApplicationsMimirPresent, expectResultApplicationsMimirPresent,
expectResultApplicationsMimirSelected, expectResultApplicationsMimirSelected,
expectResultCloudDevNotSelected, expectResultEnvironmentsDevNotSelected,
expectResultCloudDevSelected, expectResultEnvironmentsDevSelected,
expectResultCloudOpsNotSelected, expectResultEnvironmentsProdNotSelected,
expectResultCloudOpsSelected, expectResultEnvironmentsProdSelected,
expectScopesHeadline, expectScopesHeadline,
expectScopesSelectorValue, expectScopesSelectorValue,
} from './utils/assertions'; } from './utils/assertions';
@@ -133,12 +136,33 @@ describe('Tree', () => {
await openSelector(); await openSelector();
await expandResultCloud(); await expandResultCloud();
await selectResultCloudDev(); 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(); await selectResultCloudOps();
expectResultCloudDevNotSelected(); expectScopesSelectorValue('Ops');
expectResultCloudOpsSelected(); });
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 () => { it('Search works', async () => {
@@ -276,7 +300,7 @@ describe('Tree', () => {
await openSelector(); await openSelector();
// Verify that Cloud is expanded // Verify that Cloud is expanded
expect(screen.getByRole('button', { name: 'Collapse Cloud' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Cloud' })).toBeInTheDocument();
}); });
describe('Keyboard Navigation', () => { describe('Keyboard Navigation', () => {
@@ -22,16 +22,19 @@ import {
getResultApplicationsCloudSelect, getResultApplicationsCloudSelect,
getResultApplicationsGrafanaSelect, getResultApplicationsGrafanaSelect,
getResultApplicationsMimirSelect, getResultApplicationsMimirSelect,
getResultCloudDevRadio,
getResultCloudExpand, getResultCloudExpand,
getResultCloudOpsRadio,
getResultCloudSelect, getResultCloudSelect,
getResultEnvironmentsExpand,
getResultEnvironmentsDevSelect,
getResultEnvironmentsProdSelect,
getSelectorApply, getSelectorApply,
getSelectorCancel, getSelectorCancel,
getSelectorClear, getSelectorClear,
getSelectorInput, getSelectorInput,
getTreeSearch, getTreeSearch,
findResultApplicationsExpand, findResultApplicationsExpand,
getResultCloudDevLink,
getResultCloudOpsLink,
} from './selectors'; } from './selectors';
const click = async (selector: () => HTMLElement) => act(() => userEvent.click(selector())); 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 selectResultApplicationsCloud = async () => click(getResultApplicationsCloudSelect);
export const selectResultApplicationsCloudDev = async () => click(getResultApplicationsCloudDevSelect); export const selectResultApplicationsCloudDev = async () => click(getResultApplicationsCloudDevSelect);
export const selectResultCloud = async () => click(getResultCloudSelect); export const selectResultCloud = async () => click(getResultCloudSelect);
export const selectResultCloudDev = async () => click(getResultCloudDevRadio); export const selectResultCloudDev = async () => click(getResultCloudDevLink);
export const selectResultCloudOps = async () => click(getResultCloudOpsRadio); 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 toggleDashboards = async () => click(getDashboardsExpand);
export const searchDashboards = async (value: string) => type(getDashboardsSearch, value); export const searchDashboards = async (value: string) => type(getDashboardsSearch, value);
@@ -12,8 +12,6 @@ import {
getResultApplicationsCloudSelect, getResultApplicationsCloudSelect,
getResultApplicationsGrafanaSelect, getResultApplicationsGrafanaSelect,
getResultApplicationsMimirSelect, getResultApplicationsMimirSelect,
getResultCloudDevRadio,
getResultCloudOpsRadio,
getSelectorInput, getSelectorInput,
getTreeHeadline, getTreeHeadline,
queryAllDashboard, queryAllDashboard,
@@ -28,14 +26,16 @@ import {
queryResultApplicationsCloudSelect, queryResultApplicationsCloudSelect,
queryResultApplicationsGrafanaSelect, queryResultApplicationsGrafanaSelect,
queryResultApplicationsMimirSelect, queryResultApplicationsMimirSelect,
getResultEnvironmentsDevSelect,
getResultEnvironmentsProdSelect,
querySelectorApply, querySelectorApply,
} from './selectors'; } from './selectors';
const expectInDocument = (selector: () => HTMLElement) => expect(selector()).toBeInTheDocument(); const expectInDocument = (selector: () => HTMLElement) => expect(selector()).toBeInTheDocument();
const expectNotInDocument = (selector: () => HTMLElement | null) => expect(selector()).not.toBeInTheDocument(); const expectNotInDocument = (selector: () => HTMLElement | null) => expect(selector()).not.toBeInTheDocument();
const expectChecked = (selector: () => HTMLInputElement) => expect(selector()).toBeChecked(); const expectChecked = (selector: () => HTMLInputElement) => expect(selector()).toBeChecked();
const expectRadioChecked = (selector: () => HTMLInputElement) => expect(selector().checked).toBe(true); const expectNotChecked = (selector: () => HTMLInputElement) => expect(selector()).not.toBeChecked();
const expectRadioNotChecked = (selector: () => HTMLInputElement) => expect(selector().checked).toBe(false);
const expectValue = (selector: () => HTMLInputElement, value: string) => expect(selector().value).toBe(value); const expectValue = (selector: () => HTMLInputElement, value: string) => expect(selector().value).toBe(value);
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();
@@ -62,10 +62,11 @@ export const expectResultApplicationsMimirPresent = () => expectInDocument(getRe
export const expectResultApplicationsMimirNotPresent = () => expectNotInDocument(queryResultApplicationsMimirSelect); export const expectResultApplicationsMimirNotPresent = () => expectNotInDocument(queryResultApplicationsMimirSelect);
export const expectResultApplicationsCloudPresent = () => expectInDocument(getResultApplicationsCloudSelect); export const expectResultApplicationsCloudPresent = () => expectInDocument(getResultApplicationsCloudSelect);
export const expectResultApplicationsCloudNotPresent = () => expectNotInDocument(queryResultApplicationsCloudSelect); export const expectResultApplicationsCloudNotPresent = () => expectNotInDocument(queryResultApplicationsCloudSelect);
export const expectResultCloudDevSelected = () => expectRadioChecked(getResultCloudDevRadio);
export const expectResultCloudDevNotSelected = () => expectRadioNotChecked(getResultCloudDevRadio); export const expectResultEnvironmentsDevSelected = () => expectChecked(getResultEnvironmentsDevSelect);
export const expectResultCloudOpsSelected = () => expectRadioChecked(getResultCloudOpsRadio); export const expectResultEnvironmentsDevNotSelected = () => expectNotChecked(getResultEnvironmentsDevSelect);
export const expectResultCloudOpsNotSelected = () => expectRadioNotChecked(getResultCloudOpsRadio); export const expectResultEnvironmentsProdSelected = () => expectChecked(getResultEnvironmentsProdSelect);
export const expectResultEnvironmentsProdNotSelected = () => expectNotChecked(getResultEnvironmentsProdSelect);
export const expectDashboardsDisabled = () => expectDisabled(getDashboardsExpand); export const expectDashboardsDisabled = () => expectDisabled(getDashboardsExpand);
export const expectDashboardsClosed = () => expectNotInDocument(queryDashboardsContainer); export const expectDashboardsClosed = () => expectNotInDocument(queryDashboardsContainer);
@@ -59,6 +59,20 @@ export const mocksScopes: Scope[] = [
filters: [{ key: 'app', value: 'tempo', operator: 'equals' }], 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; ] as const;
const dashboardBindingsGenerator = ( const dashboardBindingsGenerator = (
@@ -341,6 +355,38 @@ export const mocksNodes: ScopeNode[] = [
parentName: 'cloud-applications', 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; ] as const;
export const dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard'); export const dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard');
@@ -9,6 +9,7 @@ const selectors = {
headline: 'scopes-tree-headline', headline: 'scopes-tree-headline',
select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`, select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`,
radio: (nodeId: string) => `scopes-tree-${nodeId}-radio`, radio: (nodeId: string) => `scopes-tree-${nodeId}-radio`,
link: (nodeId: string) => `scopes-tree-${nodeId}-link`,
expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`, expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`,
title: (nodeId: string) => `scopes-tree-${nodeId}-title`, 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 getResultCloudSelect = () => screen.getByTestId(selectors.tree.select('cloud'));
export const getResultCloudExpand = () => screen.getByTestId(selectors.tree.expand('cloud')); export const getResultCloudExpand = () => screen.getByTestId(selectors.tree.expand('cloud'));
export const getResultCloudDevRadio = () => screen.getByTestId<HTMLInputElement>(selectors.tree.radio('cloud-dev')); export const getResultCloudDevLink = () => screen.getByTestId<HTMLButtonElement>(selectors.tree.link('cloud-dev'));
export const getResultCloudOpsRadio = () => screen.getByTestId<HTMLInputElement>(selectors.tree.radio('cloud-ops')); export const getResultCloudOpsLink = () => screen.getByTestId<HTMLButtonElement>(selectors.tree.link('cloud-ops'));
export const getResultEnvironmentsExpand = () => screen.getByTestId(selectors.tree.expand('environments'));
export const getResultEnvironmentsDevSelect = () =>
screen.getByTestId<HTMLInputElement>(selectors.tree.radio('environments-dev'));
export const getResultEnvironmentsProdSelect = () =>
screen.getByTestId<HTMLInputElement>(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; export const getListOfScopes = (service: ScopesService) => service.state.value;
-2
View File
@@ -12305,8 +12305,6 @@
"title": "Select scopes" "title": "Select scopes"
}, },
"tree": { "tree": {
"collapse": "Collapse {{title}}",
"expand": "Expand {{title}}",
"headline": { "headline": {
"noResults": "No results found for your query", "noResults": "No results found for your query",
"recommended": "Recommended", "recommended": "Recommended",