mirror of https://github.com/grafana/grafana.git
Scopes: Get parent nodes for command palette search results (#111820)
* Add feature flag for multiple scopes endpoint usage * Add method to API client for fetching multiple ScopeNodes at the same time * Add parent title to tree * Get nodes from cache too * Update test with new functionality * Update test * Fix linting issue * Remove unapplied scope parents
This commit is contained in:
parent
7055ba9140
commit
5639ecf711
|
|
@ -479,6 +479,11 @@ export interface FeatureToggles {
|
|||
*/
|
||||
useScopeSingleNodeEndpoint?: boolean;
|
||||
/**
|
||||
* Makes the frontend use the 'names' param for fetching multiple scope nodes at once
|
||||
* @default false
|
||||
*/
|
||||
useMultipleScopeNodesEndpoint?: boolean;
|
||||
/**
|
||||
* In-development feature that will allow injection of labels into prometheus queries.
|
||||
* @default true
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -806,6 +806,16 @@ var (
|
|||
HideFromDocs: true,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
{
|
||||
Name: "useMultipleScopeNodesEndpoint",
|
||||
Description: "Makes the frontend use the 'names' param for fetching multiple scope nodes at once",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaOperatorExperienceSquad,
|
||||
Expression: "false",
|
||||
FrontendOnly: true,
|
||||
HideFromDocs: true,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
{
|
||||
Name: "promQLScope",
|
||||
Description: "In-development feature that will allow injection of labels into prometheus queries.",
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,fal
|
|||
alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false
|
||||
scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
useScopeSingleNodeEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
useMultipleScopeNodesEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
promQLScope,GA,@grafana/oss-big-tent,false,false,false
|
||||
logQLScope,privatePreview,@grafana/observability-logs,false,false,false
|
||||
sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,false
|
||||
|
|
|
|||
|
|
|
@ -435,6 +435,10 @@ const (
|
|||
// Use the single node endpoint for the scope api. This is used to fetch the scope parent node.
|
||||
FlagUseScopeSingleNodeEndpoint = "useScopeSingleNodeEndpoint"
|
||||
|
||||
// FlagUseMultipleScopeNodesEndpoint
|
||||
// Makes the frontend use the 'names' param for fetching multiple scope nodes at once
|
||||
FlagUseMultipleScopeNodesEndpoint = "useMultipleScopeNodesEndpoint"
|
||||
|
||||
// FlagPromQLScope
|
||||
// In-development feature that will allow injection of labels into prometheus queries.
|
||||
FlagPromQLScope = "promQLScope"
|
||||
|
|
|
|||
|
|
@ -3845,6 +3845,22 @@
|
|||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "useMultipleScopeNodesEndpoint",
|
||||
"resourceVersion": "1759237515008",
|
||||
"creationTimestamp": "2025-09-30T13:05:15Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Makes the frontend use the 'names' param for fetching multiple scope nodes at once",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-operator-experience-squad",
|
||||
"frontend": true,
|
||||
"hideFromAdminPage": true,
|
||||
"hideFromDocs": true,
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "useScopeSingleNodeEndpoint",
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ describe('useRegisterScopesActions', () => {
|
|||
// The main difference here is that we map it to a parent if we are in the "scopes" section of the cmdK.
|
||||
// In the previous test the scope actions were mapped to global level to show correctly.
|
||||
parent: 'scopes',
|
||||
subtitle: 'some parent',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ function useScopesRow(onApply: () => void) {
|
|||
* @param parentId
|
||||
*/
|
||||
function useGlobalScopesSearch(searchQuery: string, parentId?: string | null) {
|
||||
const { selectScope, searchAllNodes } = useScopeServicesState();
|
||||
const { selectScope, searchAllNodes, getScopeNodes } = useScopeServicesState();
|
||||
const [actions, setActions] = useState<CommandPaletteAction[] | undefined>(undefined);
|
||||
const searchQueryRef = useRef<string>();
|
||||
|
||||
|
|
@ -151,19 +151,42 @@ function useGlobalScopesSearch(searchQuery: string, parentId?: string | null) {
|
|||
if (searchQueryRef.current === searchQuery) {
|
||||
// Only show leaf nodes because otherwise there are issues with navigating to a category without knowing
|
||||
// where in the tree it is.
|
||||
const leafNodes = nodes.filter((node) => node.spec.nodeType === 'leaf');
|
||||
const actions = [getScopesParentAction()];
|
||||
for (const node of leafNodes) {
|
||||
actions.push(mapScopeNodeToAction(node, selectScope, parentId || undefined));
|
||||
|
||||
const parentNodesMap = new Map<string | undefined, string>();
|
||||
|
||||
if (config.featureToggles.useMultipleScopeNodesEndpoint) {
|
||||
// Make sure we only request unqiue parent node names
|
||||
const uniqueParentNodeNames = [
|
||||
...new Set(nodes.map((node) => node.spec.parentName).filter((name) => name !== undefined)),
|
||||
];
|
||||
getScopeNodes(uniqueParentNodeNames).then((parentNodes) => {
|
||||
for (const parentNode of parentNodes) {
|
||||
parentNodesMap.set(parentNode.metadata.name, parentNode.spec.title);
|
||||
}
|
||||
|
||||
const leafNodes = nodes.filter((node) => node.spec.nodeType === 'leaf');
|
||||
const actions = [getScopesParentAction()];
|
||||
for (const node of leafNodes) {
|
||||
const parentName = parentNodesMap.get(node.spec.parentName);
|
||||
actions.push(mapScopeNodeToAction(node, selectScope, parentId || undefined, parentName || undefined));
|
||||
}
|
||||
setActions(actions);
|
||||
});
|
||||
} else {
|
||||
const leafNodes = nodes.filter((node) => node.spec.nodeType === 'leaf');
|
||||
const actions = [getScopesParentAction()];
|
||||
for (const node of leafNodes) {
|
||||
actions.push(mapScopeNodeToAction(node, selectScope, parentId || undefined));
|
||||
}
|
||||
setActions(actions);
|
||||
}
|
||||
setActions(actions);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
searchQueryRef.current = undefined;
|
||||
setActions(undefined);
|
||||
}
|
||||
}, [searchAllNodes, searchQuery, parentId, selectScope]);
|
||||
}, [searchAllNodes, searchQuery, parentId, selectScope, getScopeNodes]);
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ describe('mapScopeNodeToAction', () => {
|
|||
priority: SCOPES_PRIORITY,
|
||||
parent: 'parent1',
|
||||
perform: expect.any(Function),
|
||||
subtitle: 'Parent Scope',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ describe('mapScopeNodeToAction', () => {
|
|||
keywords: 'Scope 1 scope1',
|
||||
priority: SCOPES_PRIORITY,
|
||||
parent: 'parent1',
|
||||
subtitle: 'Parent Scope',
|
||||
});
|
||||
|
||||
// Non-leaf nodes don't have a perform function
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export function useScopeServicesState() {
|
|||
selectScope: () => {},
|
||||
resetSelection: () => {},
|
||||
searchAllNodes: () => Promise.resolve([]),
|
||||
getScopeNodes: (_: string[]) => Promise.resolve([]),
|
||||
apply: () => {},
|
||||
deselectScope: () => {},
|
||||
nodes: {},
|
||||
|
|
@ -31,7 +32,7 @@ export function useScopeServicesState() {
|
|||
},
|
||||
};
|
||||
}
|
||||
const { updateNode, filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply } =
|
||||
const { updateNode, filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply, getScopeNodes } =
|
||||
services.scopesSelectorService;
|
||||
const selectorServiceState: ScopesSelectorServiceState | undefined = useObservable(
|
||||
services.scopesSelectorService.stateObservable ?? new Observable(),
|
||||
|
|
@ -39,6 +40,7 @@ export function useScopeServicesState() {
|
|||
);
|
||||
|
||||
return {
|
||||
getScopeNodes,
|
||||
filterNode,
|
||||
updateNode,
|
||||
selectScope,
|
||||
|
|
@ -90,7 +92,12 @@ export function mapScopesNodesTreeToActions(
|
|||
if (child.spec.nodeType === 'leaf' && scopeIsSelected) {
|
||||
continue;
|
||||
}
|
||||
let action = mapScopeNodeToAction(child, selectScope, parentId);
|
||||
let action = mapScopeNodeToAction(
|
||||
child,
|
||||
selectScope,
|
||||
parentId,
|
||||
child.spec.parentName ? nodes[child.spec.parentName]?.spec.title : undefined
|
||||
);
|
||||
actions.push(action);
|
||||
traverse(childTreeNode, action.id);
|
||||
}
|
||||
|
|
@ -110,13 +117,16 @@ export function mapScopesNodesTreeToActions(
|
|||
export function mapScopeNodeToAction(
|
||||
scopeNode: ScopeNode,
|
||||
selectScope: (id: string) => void,
|
||||
parentId?: string
|
||||
parentId?: string,
|
||||
parentName?: string
|
||||
): CommandPaletteAction {
|
||||
let action: CommandPaletteAction;
|
||||
const subtitle = parentName || scopeNode.spec.parentName || undefined;
|
||||
if (parentId) {
|
||||
action = {
|
||||
id: `${parentId}/${scopeNode.metadata.name}`,
|
||||
name: scopeNode.spec.title,
|
||||
subtitle: subtitle,
|
||||
keywords: `${scopeNode.spec.title} ${scopeNode.metadata.name}`,
|
||||
priority: SCOPES_PRIORITY,
|
||||
parent: parentId,
|
||||
|
|
@ -135,7 +145,7 @@ export function mapScopeNodeToAction(
|
|||
keywords: `${scopeNode.spec.title} ${scopeNode.metadata.name}`,
|
||||
priority: SCOPES_PRIORITY,
|
||||
section: t('command-palette.action.scopes', 'Scopes'),
|
||||
subtitle: scopeNode.spec.parentName,
|
||||
subtitle: subtitle,
|
||||
perform: () => {
|
||||
selectScope(scopeNode.metadata.name);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,6 +26,21 @@ export class ScopesApiClient {
|
|||
return scopes.filter((scope) => scope !== undefined);
|
||||
}
|
||||
|
||||
async fetchMultipleScopeNodes(names: string[]): Promise<ScopeNode[]> {
|
||||
if (!config.featureToggles.useMultipleScopeNodesEndpoint || names.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getBackendSrv().get<{ items: ScopeNode[] }>(apiUrl + `/find/scope_node_children`, {
|
||||
names: names,
|
||||
});
|
||||
return res?.items ?? [];
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a map of nodes based on the specified options.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -508,6 +508,30 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
|||
this.updateState({ nodes: newNodes });
|
||||
return scopeNodes;
|
||||
};
|
||||
|
||||
public getScopeNodes = async (scopeNodeNames: string[]): Promise<ScopeNode[]> => {
|
||||
const nodesMap: NodesMap = {};
|
||||
// Get nodes that are already in the cache
|
||||
for (const name of scopeNodeNames) {
|
||||
if (this.state.nodes[name]) {
|
||||
nodesMap[name] = this.state.nodes[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Get nodes that are not in the cache
|
||||
const nodesToFetch = scopeNodeNames.filter((name) => !nodesMap[name]);
|
||||
|
||||
const nodes = await this.apiClient.fetchMultipleScopeNodes(nodesToFetch);
|
||||
for (const node of nodes) {
|
||||
nodesMap[node.metadata.name] = node;
|
||||
}
|
||||
|
||||
const newNodes = { ...this.state.nodes, ...nodesMap };
|
||||
|
||||
// Return both caches and fetches nodes in the correct order
|
||||
this.updateState({ nodes: newNodes });
|
||||
return scopeNodeNames.map((name) => nodesMap[name]).filter((node) => node !== undefined);
|
||||
};
|
||||
}
|
||||
|
||||
function isScopeLocalStorageV1(obj: unknown): obj is { scope: Scope } {
|
||||
|
|
|
|||
Loading…
Reference in New Issue