mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Loki: Add fuzzy search to label browser (#36864)
This commit is contained in:
154
packages/grafana-ui/src/components/BrowserLabel/Label.tsx
Normal file
154
packages/grafana-ui/src/components/BrowserLabel/Label.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { forwardRef, HTMLAttributes, useCallback } from 'react';
|
||||
import { cx, css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useTheme2 } from '../../themes';
|
||||
// @ts-ignore
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import { PartialHighlighter } from '../Typeahead/PartialHighlighter';
|
||||
import { HighlightPart } from '../../types';
|
||||
|
||||
type OnLabelClick = (name: string, value: string | undefined, event: React.MouseEvent<HTMLElement>) => void;
|
||||
|
||||
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'onClick'> {
|
||||
name: string;
|
||||
active?: boolean;
|
||||
loading?: boolean;
|
||||
searchTerm?: string;
|
||||
value?: string;
|
||||
facets?: number;
|
||||
title?: string;
|
||||
highlightParts?: HighlightPart[];
|
||||
onClick?: OnLabelClick;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const Label = forwardRef<HTMLElement, Props>(
|
||||
(
|
||||
{
|
||||
name,
|
||||
value,
|
||||
hidden,
|
||||
facets,
|
||||
onClick,
|
||||
className,
|
||||
loading,
|
||||
searchTerm,
|
||||
active,
|
||||
style,
|
||||
title,
|
||||
highlightParts,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const theme = useTheme2();
|
||||
const styles = getLabelStyles(theme);
|
||||
const searchWords = searchTerm ? [searchTerm] : [];
|
||||
|
||||
const onLabelClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLElement>) => {
|
||||
if (onClick && !hidden) {
|
||||
onClick(name, value, event);
|
||||
}
|
||||
},
|
||||
[onClick, name, hidden, value]
|
||||
);
|
||||
|
||||
// Using this component for labels and label values. If value is given use value for display text.
|
||||
let text = value || name;
|
||||
if (facets) {
|
||||
text = `${text} (${facets})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
key={text}
|
||||
ref={ref}
|
||||
onClick={onLabelClick}
|
||||
style={style}
|
||||
title={title || text}
|
||||
role="option"
|
||||
aria-selected={!!active}
|
||||
className={cx(
|
||||
styles.base,
|
||||
active && styles.active,
|
||||
loading && styles.loading,
|
||||
hidden && styles.hidden,
|
||||
className,
|
||||
onClick && !hidden && styles.hover
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{highlightParts !== undefined ? (
|
||||
<PartialHighlighter text={text} highlightClassName={styles.matchHighLight} highlightParts={highlightParts} />
|
||||
) : (
|
||||
<Highlighter
|
||||
textToHighlight={text}
|
||||
searchWords={searchWords}
|
||||
autoEscape
|
||||
highlightClassName={styles.matchHighLight}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Label.displayName = 'Label';
|
||||
|
||||
const getLabelStyles = (theme: GrafanaTheme2) => ({
|
||||
base: css`
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
line-height: ${theme.typography.bodySmall.lineHeight};
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
color: ${theme.colors.text};
|
||||
white-space: nowrap;
|
||||
text-shadow: none;
|
||||
padding: ${theme.spacing(0.5)};
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
margin-right: ${theme.spacing(1)};
|
||||
margin-bottom: ${theme.spacing(0.5)};
|
||||
`,
|
||||
loading: css`
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
background-color: ${theme.colors.primary.shade};
|
||||
color: ${theme.colors.text.primary};
|
||||
animation: pulse 3s ease-out 0s infinite normal forwards;
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
color: ${theme.colors.text.primary};
|
||||
}
|
||||
50% {
|
||||
color: ${theme.colors.text.secondary};
|
||||
}
|
||||
100% {
|
||||
color: ${theme.colors.text.disabled};
|
||||
}
|
||||
}
|
||||
`,
|
||||
active: css`
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
background-color: ${theme.colors.primary.main};
|
||||
color: ${theme.colors.primary.contrastText};
|
||||
`,
|
||||
matchHighLight: css`
|
||||
background: inherit;
|
||||
color: ${theme.colors.primary.text};
|
||||
background-color: ${theme.colors.primary.transparent};
|
||||
`,
|
||||
hidden: css`
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
border: 1px solid transparent;
|
||||
`,
|
||||
hover: css`
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
});
|
||||
@@ -255,3 +255,4 @@ export { preparePlotFrame } from './GraphNG/utils';
|
||||
export { GraphNGLegendEvent } from './GraphNG/types';
|
||||
export * from './PanelChrome/types';
|
||||
export { EmotionPerfTest } from './ThemeDemos/EmotionPerfTest';
|
||||
export { Label as BrowserLabel } from './BrowserLabel/Label';
|
||||
|
||||
@@ -76,4 +76,15 @@ describe('Fuzzy search', () => {
|
||||
found: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores whitespace in needle', () => {
|
||||
expect(fuzzyMatch('bbaarr_bar_bbarr', 'bb bar')).toEqual({
|
||||
ranges: [
|
||||
{ start: 0, end: 1 },
|
||||
{ start: 7, end: 9 },
|
||||
],
|
||||
distance: 5,
|
||||
found: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,10 +20,15 @@ type FuzzyMatch = {
|
||||
*
|
||||
* @param stack - main text to be searched
|
||||
* @param needle - partial text to find in the stack
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function fuzzyMatch(stack: string, needle: string): FuzzyMatch {
|
||||
let distance = 0,
|
||||
searchIndex = stack.indexOf(needle);
|
||||
// Remove whitespace from needle as a temporary solution to treat separate string
|
||||
// queries as 'AND'
|
||||
needle = needle.replace(/\s/g, '');
|
||||
|
||||
const ranges: HighlightPart[] = [];
|
||||
|
||||
@@ -16,3 +16,4 @@ export { renderOrCallToRender } from './renderOrCallToRender';
|
||||
export { createLogger } from './logger';
|
||||
export { attachDebugger } from './debug';
|
||||
export * from './nodeGraph';
|
||||
export { fuzzyMatch } from './fuzzy';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CompletionItem, SearchFunction } from '../types';
|
||||
import { fuzzyMatch } from '../slate-plugins/fuzzy';
|
||||
import { fuzzyMatch } from './fuzzy';
|
||||
|
||||
/**
|
||||
* List of auto-complete search function used by SuggestionsPlugin.handleTypeahead()
|
||||
|
||||
Reference in New Issue
Block a user