The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/commandPalette/KBarResults.tsx

230 lines
7.6 KiB

import { ActionImpl, getListboxItemId, KBAR_LISTBOX, useKBar } from 'kbar';
import { usePointerMovedSinceMount } from 'kbar/lib/utils';
import * as React from 'react';
import { useVirtual } from 'react-virtual';
// From https://github.com/timc1/kbar/blob/main/src/KBarResults.tsx
// TODO: Go back to KBarResults from kbar when https://github.com/timc1/kbar/issues/281 is fixed
// Remember to remove dependency on react-virtual when removing this file
const START_INDEX = 0;
interface RenderParams<T = ActionImpl | string> {
item: T;
active: boolean;
}
interface KBarResultsProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items: any[];
onRender: (params: RenderParams) => React.ReactElement;
maxHeight?: number;
}
export const KBarResults: React.FC<KBarResultsProps> = (props) => {
const activeRef = React.useRef<HTMLElement>(null);
const parentRef = React.useRef(null);
// store a ref to all items so we do not have to pass
// them as a dependency when setting up event listeners.
const itemsRef = React.useRef(props.items);
itemsRef.current = props.items;
const rowVirtualizer = useVirtual({
size: itemsRef.current.length,
parentRef,
});
const { query, search, currentRootActionId, activeIndex, options } = useKBar((state) => ({
search: state.searchQuery,
currentRootActionId: state.currentRootActionId,
activeIndex: state.activeIndex,
}));
React.useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp' || (event.ctrlKey && event.key === 'p')) {
event.preventDefault();
query.setActiveIndex((index) => {
let nextIndex = index > START_INDEX ? index - 1 : index;
// avoid setting active index on a group
if (typeof itemsRef.current[nextIndex] === 'string') {
if (nextIndex === 0) {
return index;
}
nextIndex -= 1;
}
return nextIndex;
});
} else if (event.key === 'ArrowDown' || (event.ctrlKey && event.key === 'n')) {
event.preventDefault();
query.setActiveIndex((index) => {
let nextIndex = index < itemsRef.current.length - 1 ? index + 1 : index;
// avoid setting active index on a group
if (typeof itemsRef.current[nextIndex] === 'string') {
if (nextIndex === itemsRef.current.length - 1) {
return index;
}
nextIndex += 1;
}
return nextIndex;
});
} else if (event.key === 'Enter') {
event.preventDefault();
// storing the active dom element in a ref prevents us from
// having to calculate the current action to perform based
// on the `activeIndex`, which we would have needed to add
// as part of the dependencies array.
activeRef.current?.click();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [query]);
// destructuring here to prevent linter warning to pass
// entire rowVirtualizer in the dependencies array.
const { scrollToIndex } = rowVirtualizer;
React.useEffect(() => {
scrollToIndex(activeIndex, {
// ensure that if the first item in the list is a group
// name and we are focused on the second item, to not
// scroll past that group, hiding it.
align: activeIndex <= 1 ? 'end' : 'auto',
});
}, [activeIndex, scrollToIndex]);
React.useEffect(() => {
// TODO(tim): fix scenario where async actions load in
// and active index is reset to the first item. i.e. when
// users register actions and bust the `useRegisterActions`
// cache, we won't want to reset their active index as they
// are navigating the list.
query.setActiveIndex(
// avoid setting active index on a group
typeof props.items[START_INDEX] === 'string' ? START_INDEX + 1 : START_INDEX
);
}, [search, currentRootActionId, props.items, query]);
const execute = React.useCallback(
(ev: React.MouseEvent, item: RenderParams['item']) => {
if (typeof item === 'string') {
return;
}
// ActionImpl constructor copies all properties from action onto ActionImpl
// so our url property is secretly there, but completely untyped
// Preferably this change is upstreamed and ActionImpl has this
// eslint-disable-next-line
const url = (item as ActionImpl & { url?: string }).url;
if (item.command) {
item.command.perform(item);
query.toggle();
} else if (url) {
if (!(ev.ctrlKey || ev.metaKey || ev.shiftKey)) {
query.toggle();
}
} else {
query.setSearch('');
query.setCurrentRootAction(item.id);
}
options.callbacks?.onSelectAction?.(item);
},
[query, options]
);
const pointerMoved = usePointerMovedSinceMount();
return (
<div
ref={parentRef}
style={{
maxHeight: props.maxHeight || 400,
position: 'relative',
overflow: 'auto',
}}
>
<div
role="listbox"
id={KBAR_LISTBOX}
style={{
height: `${rowVirtualizer.totalSize}px`,
width: '100%',
}}
>
{rowVirtualizer.virtualItems.map((virtualRow) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const item = itemsRef.current[virtualRow.index] as ActionImpl & {
url?: string;
target?: React.HTMLAttributeAnchorTarget;
};
// ActionImpl constructor copies all properties from action onto ActionImpl
// so our url property is secretly there, but completely untyped
// Preferably this change is upstreamed and ActionImpl has this
const { target, url } = item;
const handlers = typeof item !== 'string' && {
onPointerMove: () =>
pointerMoved && activeIndex !== virtualRow.index && query.setActiveIndex(virtualRow.index),
onPointerDown: () => query.setActiveIndex(virtualRow.index),
onClick: (ev: React.MouseEvent) => execute(ev, item),
};
const active = virtualRow.index === activeIndex;
const childProps = {
id: getListboxItemId(virtualRow.index),
role: 'option',
'aria-selected': active,
style: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
} as const,
...handlers,
};
const renderedItem = React.cloneElement(
props.onRender({
item,
active,
}),
{
ref: virtualRow.measureRef,
}
);
if (url) {
return (
<a
key={virtualRow.index}
href={url}
target={target}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
ref={active ? (activeRef as React.RefObject<HTMLAnchorElement>) : null}
{...childProps}
>
{renderedItem}
</a>
);
}
return (
<div
key={virtualRow.index}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
ref={active ? (activeRef as React.RefObject<HTMLDivElement>) : null}
{...childProps}
>
{renderedItem}
</div>
);
})}
</div>
</div>
);
};