Merge remote-tracking branch 'origin/main' into query-history-app
This commit is contained in:
@@ -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"`
|
||||
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
openScopesSelector,
|
||||
searchScopes,
|
||||
selectScope,
|
||||
TestScope,
|
||||
} from '../utils/scope-helpers';
|
||||
import { testScopes } from '../utils/scopes';
|
||||
|
||||
|
||||
@@ -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<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 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<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
|
||||
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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 (
|
||||
<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 {
|
||||
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 (
|
||||
<div
|
||||
key={treeNode.scopeNodeId}
|
||||
@@ -78,75 +246,57 @@ export function ScopesTreeItem({
|
||||
)}
|
||||
data-testid={`scopes-tree-${treeNode.scopeNodeId}`}
|
||||
>
|
||||
{isSelectable && !treeNode.expanded ? (
|
||||
disableMultiSelect ? (
|
||||
<RadioButtonDot
|
||||
id={treeNode.scopeNodeId}
|
||||
name={treeNode.scopeNodeId}
|
||||
checked={selected}
|
||||
label={
|
||||
isExpandable ? (
|
||||
''
|
||||
) : shouldHighlight ? (
|
||||
<Highlighter textToHighlight={titleText} searchWords={searchWords} autoEscape />
|
||||
) : (
|
||||
titleText
|
||||
)
|
||||
}
|
||||
data-testid={`scopes-tree-${treeNode.scopeNodeId}-radio`}
|
||||
onClick={() => {
|
||||
selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.checkboxWithLabel}>
|
||||
<Checkbox
|
||||
id={treeNode.scopeNodeId}
|
||||
checked={selected}
|
||||
data-testid={`scopes-tree-${treeNode.scopeNodeId}-checkbox`}
|
||||
label=""
|
||||
{isSelectable && !isExpandable && !treeNode.expanded && (
|
||||
<>
|
||||
{disableMultiSelect && (
|
||||
<ScopeLinkLikeButton
|
||||
scopeNodeId={treeNode.scopeNodeId}
|
||||
onClick={() => {
|
||||
selectScope(treeNode.scopeNodeId);
|
||||
closeAndApply?.();
|
||||
}}
|
||||
>
|
||||
<TitleContent shouldHighlight={shouldHighlight} titleText={titleText} searchWords={searchWords} />
|
||||
</ScopeLinkLikeButton>
|
||||
)}
|
||||
{!disableMultiSelect && (
|
||||
<ScopeCheckboxWithLabel
|
||||
scopeNodeId={treeNode.scopeNodeId}
|
||||
selected={selected}
|
||||
showLabel={!isExpandable}
|
||||
onChange={() => {
|
||||
selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId);
|
||||
}}
|
||||
/>
|
||||
{!isExpandable && (
|
||||
<label htmlFor={treeNode.scopeNodeId} className={styles.checkboxLabel}>
|
||||
{shouldHighlight ? (
|
||||
<Highlighter textToHighlight={titleText} searchWords={searchWords} autoEscape />
|
||||
) : (
|
||||
titleText
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
>
|
||||
<TitleContent shouldHighlight={shouldHighlight} titleText={titleText} searchWords={searchWords} />
|
||||
</ScopeCheckboxWithLabel>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isExpandable && (
|
||||
<button
|
||||
className={styles.expand}
|
||||
data-testid={`scopes-tree-${treeNode.scopeNodeId}-expand`}
|
||||
aria-label={
|
||||
treeNode.expanded
|
||||
? t('scopes.tree.collapse', 'Collapse {{title}}', { title: titleText })
|
||||
: t('scopes.tree.expand', 'Expand {{title}}', { title: titleText })
|
||||
}
|
||||
onClick={() => {
|
||||
toggleExpandedNode(treeNode.scopeNodeId);
|
||||
}}
|
||||
<ScopeExpandButton
|
||||
scopeNodeId={treeNode.scopeNodeId}
|
||||
expanded={treeNode.expanded}
|
||||
controlsId={childrenId}
|
||||
onClick={() => toggleExpandedNode(treeNode.scopeNodeId)}
|
||||
onSelect={() => (selected ? deselectScope(treeNode.scopeNodeId) : selectScope(treeNode.scopeNodeId))}
|
||||
isSelectable={isSelectable}
|
||||
disableMultiSelect={disableMultiSelect}
|
||||
selected={selected}
|
||||
>
|
||||
<Icon name={!treeNode.expanded ? 'angle-right' : 'angle-down'} />
|
||||
<TitleContent shouldHighlight={shouldHighlight} titleText={titleText} searchWords={searchWords} />
|
||||
</ScopeExpandButton>
|
||||
)}
|
||||
|
||||
{shouldHighlight ? (
|
||||
<Highlighter textToHighlight={titleText} searchWords={searchWords} autoEscape />
|
||||
) : (
|
||||
titleText
|
||||
)}
|
||||
</button>
|
||||
{scopeNode.spec.subTitle && (
|
||||
<Text truncate variant="body" color="secondary">
|
||||
{scopeNode.spec.subTitle}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.children}>
|
||||
<div id={childrenId} className={styles.children}>
|
||||
{treeNode.expanded && (
|
||||
<ScopesTree
|
||||
tree={treeNode}
|
||||
@@ -227,6 +377,20 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
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',
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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<HTMLInputElement>(selectors.tree.radio('cloud-dev'));
|
||||
export const getResultCloudOpsRadio = () => screen.getByTestId<HTMLInputElement>(selectors.tree.radio('cloud-ops'));
|
||||
export const getResultCloudDevLink = () => screen.getByTestId<HTMLButtonElement>(selectors.tree.link('cloud-dev'));
|
||||
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;
|
||||
|
||||
@@ -12305,8 +12305,6 @@
|
||||
"title": "Select scopes"
|
||||
},
|
||||
"tree": {
|
||||
"collapse": "Collapse {{title}}",
|
||||
"expand": "Expand {{title}}",
|
||||
"headline": {
|
||||
"noResults": "No results found for your query",
|
||||
"recommended": "Recommended",
|
||||
|
||||
Reference in New Issue
Block a user