Skip to content

Commit

Permalink
Line filters: Expand on focus (#1113)
Browse files Browse the repository at this point in the history
* feat: expand line filters on focus

---------

Co-authored-by: Matias Chomicki <matyax@gmail.com>
  • Loading branch information
gtk-grafana and matyax authored Feb 27, 2025
1 parent bfbf715 commit b9d6541
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 53 deletions.
65 changes: 65 additions & 0 deletions src/Components/IndexScene/LineFilterVariable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { LineFilterEditor } from '../ServiceScene/LineFilter/LineFilterEditor';
import React, { ChangeEvent, KeyboardEvent, useState } from 'react';
import { IconButton, useStyles2 } from '@grafana/ui';
import { LineFilterCaseSensitive } from '../../services/filterTypes';
import { RegexInputValue } from '../ServiceScene/LineFilter/RegexIconButton';

export interface LineFilterProps {
exclusive: boolean;
lineFilter: string;
caseSensitive: boolean;
regex: boolean;
setExclusive: (exclusive: boolean) => void;
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
onCaseSensitiveToggle: (caseSensitive: LineFilterCaseSensitive) => void;
onRegexToggle: (regex: RegexInputValue) => void;
updateFilter: (lineFilter: string, debounced: boolean) => void;
handleEnter: (e: KeyboardEvent<HTMLInputElement>, lineFilter: string) => void;
onSubmitLineFilter?: () => void;
onClearLineFilter?: () => void;
}

export function LineFilterVariable({ onClick, props }: { onClick: () => void; props: LineFilterProps }) {
const [focus, setFocus] = useState(false);
const styles = useStyles2(getLineFilterStyles);
return (
<>
<span>
<div className={styles.titleWrap}>
<span>Line filter</span>
<IconButton onClick={onClick} name={'times'} size={'xs'} aria-label={'Remove line filter'} />
</div>
<span className={styles.collapseWrap}>
<LineFilterEditor {...props} focus={focus} setFocus={setFocus} type={'variable'} />
{focus && (
<IconButton
className={styles.collapseBtn}
tooltip={'Collapse'}
size={'lg'}
aria-label={'Collapse filter'}
onClick={() => setFocus(false)}
name={'table-collapse-all'}
/>
)}
</span>
</span>
</>
);
}

const getLineFilterStyles = (theme: GrafanaTheme2) => ({
titleWrap: css({
display: 'flex',
fontSize: theme.typography.bodySmall.fontSize,
marginBottom: theme.spacing(0.5),
gap: theme.spacing(1),
}),
collapseWrap: css({
display: 'flex',
}),
collapseBtn: css({
marginLeft: theme.spacing(1),
}),
});
30 changes: 4 additions & 26 deletions src/Components/IndexScene/LineFilterVariablesScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from '..
import { debounce } from 'lodash';
import { GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { IconButton, useStyles2 } from '@grafana/ui';
import { LineFilterEditor, LineFilterEditorProps } from '../ServiceScene/LineFilter/LineFilterEditor';
import { useStyles2 } from '@grafana/ui';
import { LineFilterProps, LineFilterVariable } from './LineFilterVariable';
import { addCurrentUrlToHistory } from '../../services/navigate';

interface LineFilterRendererState extends SceneObjectState {}
Expand All @@ -30,7 +30,7 @@ export class LineFilterVariablesScene extends SceneObjectBase<LineFilterRenderer
return (
<div className={styles.lineFiltersWrap}>
{filters.map((filter) => {
const props: LineFilterEditorProps = {
const props: LineFilterProps = {
lineFilter: filter.value,
regex: filter.operator === LineFilterOp.regex || filter.operator === LineFilterOp.negativeRegex,
caseSensitive: filter.key === LineFilterCaseSensitive.caseSensitive,
Expand All @@ -50,20 +50,7 @@ export class LineFilterVariablesScene extends SceneObjectBase<LineFilterRenderer
onInputChange: (e) => model.onInputChange(e, filter),
onCaseSensitiveToggle: () => model.onCaseSensitiveToggle(filter),
};
return (
<span key={filter.keyLabel} className={styles.wrapper}>
<div className={styles.titleWrap}>
<span>Line filter</span>
<IconButton
onClick={() => model.removeFilter(filter)}
name={'times'}
size={'xs'}
aria-label={'Line filter variable'}
/>{' '}
</div>
<LineFilterEditor {...props} />
</span>
);
return <LineFilterVariable key={filter.keyLabel} onClick={() => model.removeFilter(filter)} props={props} />;
})}
</div>
);
Expand Down Expand Up @@ -261,14 +248,5 @@ function getStyles(theme: GrafanaTheme2) {
flexWrap: 'wrap',
gap: `${theme.spacing(0.25)} ${theme.spacing(2)}`,
}),
wrapper: css({
maxWidth: '300px',
}),
titleWrap: css({
display: 'flex',
fontSize: theme.typography.bodySmall.fontSize,
marginBottom: theme.spacing(0.5),
gap: theme.spacing(1),
}),
};
}
50 changes: 50 additions & 0 deletions src/Components/ServiceScene/Breakdowns/LineFilterInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { css } from '@emotion/css';
import { Icon, IconButton, Input, useStyles2 } from '@grafana/ui';
import React, { HTMLProps } from 'react';
import { GrafanaTheme2 } from '@grafana/data';

interface Props extends Omit<HTMLProps<HTMLInputElement>, 'width' | 'prefix'> {
onClear?: () => void;
suffix?: React.ReactNode;
prefix?: React.ReactNode;
width?: number;
}

export const LineFilterInput = ({ value, onChange, placeholder, onClear, suffix, width, ...rest }: Props) => {
const styles = useStyles2(getStyles);
return (
<Input
rows={2}
width={width}
value={value}
onChange={onChange}
suffix={
<span className={styles.suffixWrapper}>
{onClear && value ? (
<IconButton
aria-label={'Clear line filter'}
tooltip={'Clear line filter'}
onClick={onClear}
name="times"
className={styles.clearIcon}
/>
) : undefined}
{suffix && suffix}
</span>
}
prefix={<Icon name="search" />}
placeholder={placeholder}
{...rest}
/>
);
};

const getStyles = (theme: GrafanaTheme2) => ({
suffixWrapper: css({
gap: theme.spacing(0.5),
display: 'inline-flex',
}),
clearIcon: css({
cursor: 'pointer',
}),
});
71 changes: 46 additions & 25 deletions src/Components/ServiceScene/LineFilter/LineFilterEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import React, { ChangeEvent, KeyboardEvent } from 'react';
import { RegexIconButton, RegexInputValue } from './RegexIconButton';
import React, { useEffect, useState } from 'react';
import { RegexIconButton } from './RegexIconButton';
import { Button, Field, Select, useStyles2 } from '@grafana/ui';
import { SearchInput } from '../Breakdowns/SearchInput';
import { testIds } from '../../../services/testIds';
import { css, cx } from '@emotion/css';
import { LineFilterCaseSensitivityButton } from './LineFilterCaseSensitivityButton';
import { GrafanaTheme2 } from '@grafana/data';
import { LineFilterCaseSensitive } from '../../../services/filterTypes';
import { LineFilterInput } from '../Breakdowns/LineFilterInput';
import { LineFilterProps } from '../../IndexScene/LineFilterVariable';

export interface LineFilterEditorProps {
exclusive: boolean;
lineFilter: string;
caseSensitive: boolean;
regex: boolean;
setExclusive: (exclusive: boolean) => void;
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
onCaseSensitiveToggle: (caseSensitive: LineFilterCaseSensitive) => void;
onRegexToggle: (regex: RegexInputValue) => void;
updateFilter: (lineFilter: string, debounced: boolean) => void;
handleEnter: (e: KeyboardEvent<HTMLInputElement>, lineFilter: string) => void;
onSubmitLineFilter?: () => void;
onClearLineFilter?: () => void;
export interface LineFilterEditorProps extends LineFilterProps {
focus: boolean;
setFocus: (focus: boolean) => void;
type: 'variable' | 'editor';
}

const INITIAL_INPUT_WIDTH = 30;

export function LineFilterEditor({
exclusive,
lineFilter,
Expand All @@ -35,8 +28,24 @@ export function LineFilterEditor({
handleEnter,
onSubmitLineFilter,
onClearLineFilter,
focus,
setFocus,
type,
}: LineFilterEditorProps) {
const styles = useStyles2(getStyles);
const styles = useStyles2((theme) => getStyles(theme, type));
const [width, setWidth] = useState(INITIAL_INPUT_WIDTH);

function resize(content?: string) {
// The input width roughly corresponds to char count
const width = Math.max(content?.length ?? 0, INITIAL_INPUT_WIDTH);
// We add a few extra because the buttons are absolutely positioned within the input width
setWidth(width + 9);
}

useEffect(() => {
resize(lineFilter);
}, [lineFilter, focus]);

return (
<div className={styles.wrapper}>
{!onSubmitLineFilter && (
Expand All @@ -58,7 +67,11 @@ export function LineFilterEditor({
/>
)}
<Field className={styles.field}>
<SearchInput
<LineFilterInput
// Only set width if focused
width={focus ? width : undefined}
onFocus={() => setFocus(true)}
// onBlur={() => setFocus(false)}
data-testid={testIds.exploreServiceDetails.searchLogs}
value={lineFilter}
className={cx(onSubmitLineFilter ? styles.inputNoBorderRight : undefined, styles.input)}
Expand All @@ -75,7 +88,10 @@ export function LineFilterEditor({
prefix={null}
placeholder="Search in log lines"
onClear={onClearLineFilter}
onKeyUp={(e) => handleEnter(e, lineFilter)}
onKeyUp={(e) => {
handleEnter(e, lineFilter);
resize(lineFilter);
}}
/>
</Field>
{onSubmitLineFilter && (
Expand Down Expand Up @@ -110,7 +126,7 @@ export function LineFilterEditor({
);
}

const getStyles = (theme: GrafanaTheme2) => ({
const getStyles = (theme: GrafanaTheme2, type: 'variable' | 'editor') => ({
inputNoBorderRight: css({
input: {
borderTopRightRadius: 0,
Expand Down Expand Up @@ -157,21 +173,27 @@ const getStyles = (theme: GrafanaTheme2) => ({
borderTopRightRadius: '0',
borderRight: 'none',
minHeight: '30px',
width: '100px',
minWidth: '95px',
maxWidth: '95px',
outline: 'none',
}),
wrapper: css({
display: 'flex',
width: '100%',
maxWidth: '600px',
}),
input: css({
label: 'line-filter-input-wrapper',
width: '100%',
minWidth: '200px',

// Keeps the input from overflowing container on resize
maxWidth: type === 'editor' ? 'calc(100vw - 198px)' : 'calc(100vw - 288px)',

input: {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
fontFamily: 'monospace',
fontSize: theme.typography.bodySmall.fontSize,
width: '100%',
},
}),
exclusiveBtn: css({
Expand All @@ -180,7 +202,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
field: css({
label: 'field',
flex: '0 1 auto',
width: '100%',
marginBottom: 0,
}),
});
6 changes: 5 additions & 1 deletion src/Components/ServiceScene/LineFilter/LineFilterScene.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import debounce from 'lodash/debounce';
import { ChangeEvent, KeyboardEvent } from 'react';
import { ChangeEvent, KeyboardEvent, useState } from 'react';
import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from 'services/analytics';
import { getLineFiltersVariable, getLineFilterVariable } from '../../../services/variableGetters';
import {
Expand Down Expand Up @@ -264,6 +264,7 @@ export class LineFilterScene extends SceneObjectBase<LineFilterState> {

function LineFilterComponent({ model }: SceneComponentProps<LineFilterScene>) {
const { lineFilter, caseSensitive, regex, exclusive } = model.useState();
const [focus, setFocus] = useState(false);
return LineFilterEditor({
exclusive,
lineFilter,
Expand All @@ -277,5 +278,8 @@ function LineFilterComponent({ model }: SceneComponentProps<LineFilterScene>) {
onRegexToggle: model.onRegexToggle,
setExclusive: model.onToggleExclusive,
onClearLineFilter: model.clearFilter,
focus,
setFocus,
type: 'editor',
});
}
2 changes: 1 addition & 1 deletion tests/exploreServicesBreakDown.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1694,7 +1694,7 @@ test.describe('explore services breakdown page', () => {
expect(logsPanelQueryCount).toEqual(3);

// Clear the text - should trigger query
await page.getByLabel('Line filter variable').click();
await page.getByLabel('Remove line filter').click();
// Enable regex - should not trigger empty query
await page.getByLabel('Enable regex').click();
// Enable case - should not trigger empty query
Expand Down

0 comments on commit b9d6541

Please sign in to comment.