Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Line filters: Regex support #963

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
a4aad04
chore: add case sensitive line filter state to local storage
gtk-grafana Dec 12, 2024
516929b
chore: cleanup
gtk-grafana Dec 12, 2024
4239206
chore: cleanuop
gtk-grafana Dec 12, 2024
91e2ac0
chore: wip
gtk-grafana Dec 12, 2024
0bb19d0
chore: dont debounce case sensitive toggle
gtk-grafana Dec 12, 2024
e381752
chore: rename local storage
gtk-grafana Dec 12, 2024
e28fa96
chore: wip
gtk-grafana Dec 12, 2024
6c8bb6c
chore: spellcheck
gtk-grafana Dec 12, 2024
3a6c628
Merge branch 'gtk-grafana/issues/952/line-filter-ui-updates__case-sen…
gtk-grafana Dec 12, 2024
073c242
feat: add regex line filter button
gtk-grafana Dec 12, 2024
d60261d
chore: clean up
gtk-grafana Dec 12, 2024
4c2ef58
test: add unit test coverage
gtk-grafana Dec 12, 2024
656c1b9
chore: remove uncessary runtime check
gtk-grafana Dec 13, 2024
2d7540d
Merge branch 'gtk-grafana/issues/952/line-filter-ui-updates__case-sen…
gtk-grafana Dec 13, 2024
e442ad0
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 13, 2024
607fcc2
chore: remove uncessary runtime typecheck
gtk-grafana Dec 13, 2024
1cd6621
chore: rename, make buttons
gtk-grafana Dec 13, 2024
3c144b4
refactor: clean up
gtk-grafana Dec 13, 2024
77f7975
feat: migrate line filter variable to new ad-hoc-variable - PoC - WIP
gtk-grafana Dec 13, 2024
9a435c9
chore: todo list
gtk-grafana Dec 13, 2024
ae042d8
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 13, 2024
86a7e91
wip
gtk-grafana Dec 16, 2024
0854122
test: fix e2e, unit tests
gtk-grafana Dec 16, 2024
e8daca4
chore: prevent duplicate queries by extending AdHocFiltersVariable
gtk-grafana Dec 16, 2024
a32694c
test: update tests
gtk-grafana Dec 16, 2024
6c9c120
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 17, 2024
9545fd4
test: fix test assertions
gtk-grafana Dec 17, 2024
a694d91
chore: fix bug generating logQL, add unit test coverage
gtk-grafana Dec 17, 2024
57f4168
chore: add negative line filter option to logs panel
gtk-grafana Dec 17, 2024
8171385
chore: add tooltips, tweak copy
gtk-grafana Dec 17, 2024
2562fbe
chore: copy tweak
gtk-grafana Dec 18, 2024
7035bea
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 18, 2024
160cdc7
chore: functional wip state
gtk-grafana Dec 19, 2024
10850fc
chore: remove only
gtk-grafana Dec 19, 2024
b172211
chore: hide variable
gtk-grafana Dec 19, 2024
3892b7b
chore: clear pending line filter on nav
gtk-grafana Dec 19, 2024
066ebe3
chore: clean up CustomAdHocFiltersVariable
gtk-grafana Dec 19, 2024
363a07a
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 19, 2024
f36015f
chore: fix clear
gtk-grafana Dec 19, 2024
406a7f9
chore: fix bad interpolation
gtk-grafana Dec 19, 2024
456641c
chore: ui review updates, fix breakdown queries
gtk-grafana Dec 19, 2024
4ad627d
chore: clean up
gtk-grafana Dec 19, 2024
4934aaf
chore: add loading state while debouncing
gtk-grafana Dec 19, 2024
4b2e826
chore: facepalm
gtk-grafana Dec 19, 2024
79f338a
chore: fix debounce bugs, style tweaks, submit on enter
gtk-grafana Dec 20, 2024
fa4aa56
chore: mock debounce cancel
gtk-grafana Dec 20, 2024
b1d7f28
chore: update variable layout to match logQL query
gtk-grafana Dec 20, 2024
2bf483e
chore: clean up variable layout styles
gtk-grafana Dec 20, 2024
8c042a0
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Dec 20, 2024
18258c6
chore: upgrade scenes, fix duplicate query bug, fix layout
gtk-grafana Dec 20, 2024
2c93275
fix: flush debounce on submit
gtk-grafana Dec 20, 2024
cfcf1af
chore: fix migration, add e2e tests
gtk-grafana Dec 20, 2024
cb33cb3
chore: remove stale comment
gtk-grafana Dec 20, 2024
839374d
chore: cancel ongoing debounce updates on clear
gtk-grafana Dec 20, 2024
9993038
chore: clean up & document
gtk-grafana Jan 2, 2025
636cce5
chore: refactor line filters, new directory, split out react component
gtk-grafana Jan 2, 2025
32e9d5c
chore: move RegexIconButton
gtk-grafana Jan 2, 2025
2fe440f
chore: unify sorting
gtk-grafana Jan 2, 2025
482c6d8
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Jan 2, 2025
38b27f0
chore: rename case sensitivity button
gtk-grafana Jan 2, 2025
63117c7
chore: update button active state
gtk-grafana Jan 2, 2025
922b53a
chore: fix spacing between toggle buttons and close button
gtk-grafana Jan 2, 2025
7afc9c6
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Jan 6, 2025
9dbd5af
chore: change copy
gtk-grafana Jan 6, 2025
d93210a
chore: remove only
gtk-grafana Jan 6, 2025
5391f89
test: update copy
gtk-grafana Jan 6, 2025
2588618
Merge branch 'main' into gtk-grafana/issues/952/line-filter-ui-update…
gtk-grafana Jan 6, 2025
613d810
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Jan 8, 2025
0055aad
chore: clean up
gtk-grafana Jan 8, 2025
c1d9c5e
chore: add log panel filters to top level var
gtk-grafana Jan 8, 2025
67ff076
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/95…
gtk-grafana Jan 8, 2025
db09f1a
chore: remove only
gtk-grafana Jan 8, 2025
0c1b45d
fix: remove onChange behavior, only run queries on button submit
gtk-grafana Jan 8, 2025
4178c04
chore: remove include/exclude dropdown, add buttons
gtk-grafana Jan 8, 2025
fbf1b77
Update src/Components/ServiceScene/LogsListScene.tsx
gtk-grafana Jan 9, 2025
511c2eb
chore: run detected_fields on line filters change
gtk-grafana Jan 9, 2025
0948dd3
Merge branch 'main' into gtk-grafana/issues/952/line-filter-ui-update…
gtk-grafana Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"@grafana/data": "^11.3.0",
"@grafana/lezer-logql": "^0.2.6",
"@grafana/runtime": "^11.3.0",
"@grafana/scenes": "5.29.0",
"@grafana/scenes": "5.36.0",
"@grafana/ui": "^11.3.0",
"@hello-pangea/dnd": "^16.6.0",
"@lezer/common": "^1.2.1",
Expand Down
64 changes: 57 additions & 7 deletions src/Components/IndexScene/IndexScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,31 @@ import {
VAR_LABELS,
VAR_LEVELS,
VAR_LINE_FILTER,
VAR_LINE_FILTERS,
VAR_LOGS_FORMAT,
VAR_METADATA,
VAR_PATTERNS,
} from 'services/variables';

import { addLastUsedDataSourceToStorage, getLastUsedDataSourceFromStorage } from 'services/store';
import { ServiceScene } from '../ServiceScene/ServiceScene';
import { LayoutScene } from './LayoutScene';
import {
CONTROLS_VARS_FIELDS_ELSE_KEY,
CONTROLS_VARS_FIRST_ROW_KEY,
CONTROLS_VARS_METADATA_ROW_KEY,
CONTROLS_VARS_REFRESH,
CONTROLS_VARS_TIMEPICKER,
CONTROLS_VARS_TOOLBAR,
LayoutScene,
} from './LayoutScene';
import { getDrilldownSlug, PageSlugs } from '../../services/routing';
import { ServiceSelectionScene } from '../ServiceSelectionScene/ServiceSelectionScene';
import { LoadingPlaceholder } from '@grafana/ui';
import { config, getAppEvents, locationService } from '@grafana/runtime';
import {
renderLogQLFieldFilters,
renderLogQLLabelFilters,
renderLogQLLineFilter,
renderLogQLMetadataFilters,
renderPatternFilters,
} from 'services/query';
Expand All @@ -72,7 +82,7 @@ import { lokiRegularEscape } from '../../services/fields';
import { logger } from '../../services/logger';
import { getLabelsTagKeysProvider } from '../../services/TagKeysProviders';
import { AdHocFilterWithLabels, getLokiDatasource } from '../../services/scenes';
import { FilterOp } from '../../services/filterTypes';
import { FilterOp, LineFilterOp } from '../../services/filterTypes';
import { ShowLogsButtonScene } from './ShowLogsButtonScene';
import { CustomVariableValueSelectors } from './CustomVariableValueSelectors';
import { getCopiedTimeRange, PasteTimeEvent, setupKeyboardShortcuts } from '../../services/keyboardShortcuts';
Expand Down Expand Up @@ -106,25 +116,40 @@ export class IndexScene extends SceneObjectBase<IndexSceneState> {

const controls: SceneObject[] = [
new SceneFlexLayout({
key: CONTROLS_VARS_FIRST_ROW_KEY,
direction: 'row',
children: [
new SceneFlexItem({
body: new CustomVariableValueSelectors({ layout: 'vertical', include: [VAR_LABELS, VAR_DATASOURCE] }),
body: new CustomVariableValueSelectors({
key: 'vars-labels-ds',
layout: 'vertical',
include: [VAR_LABELS, VAR_DATASOURCE],
}),
}),
new ShowLogsButtonScene({
key: showLogsButtonSceneKey,
disabled: true,
}),
],
}),
new CustomVariableValueSelectors({ layout: 'vertical', exclude: [VAR_LABELS, VAR_DATASOURCE] }),
new SceneTimePicker({}),
new SceneRefreshPicker({}),
new CustomVariableValueSelectors({
key: CONTROLS_VARS_METADATA_ROW_KEY,
layout: 'vertical',
include: [VAR_METADATA, VAR_LEVELS],
}),
new CustomVariableValueSelectors({
key: CONTROLS_VARS_FIELDS_ELSE_KEY,
layout: 'vertical',
exclude: [VAR_LABELS, VAR_DATASOURCE, VAR_METADATA, VAR_LEVELS],
}),
new SceneTimePicker({ key: CONTROLS_VARS_TIMEPICKER }),
new SceneRefreshPicker({ key: CONTROLS_VARS_REFRESH }),
];

if (getDrilldownSlug() === 'explore' && config.featureToggles.exploreLogsAggregatedMetrics) {
controls.push(
new ToolbarScene({
key: CONTROLS_VARS_TOOLBAR,
isOpen: false,
})
);
Expand Down Expand Up @@ -429,6 +454,13 @@ const numericOperators = numericOperatorArray.map<SelectableValue<string>>((valu
value,
}));

const lineFilterOperators: SelectableValue[] = [
{ label: 'match', value: LineFilterOp.match },
{ label: 'negativeMatch', value: LineFilterOp.negativeMatch },
{ label: 'regex', value: LineFilterOp.regex },
{ label: 'negativeRegex', value: LineFilterOp.negativeRegex },
];

function getVariableSet(initialDatasourceUid: string, initialFilters?: AdHocVariableFilter[]) {
const labelVariable = new AdHocFiltersVariable({
name: VAR_LABELS,
Expand Down Expand Up @@ -490,6 +522,19 @@ function getVariableSet(initialDatasourceUid: string, initialFilters?: AdHocVari
return operators;
};

const lineFiltersVariable = new AdHocFiltersVariable({
name: VAR_LINE_FILTERS,
hide: VariableHide.hideVariable,
getTagKeysProvider: () => Promise.resolve({ replace: true, values: [] }),
getTagValuesProvider: () => Promise.resolve({ replace: true, values: [] }),
expressionBuilder: renderLogQLLineFilter,
layout: 'horizontal',
});

lineFiltersVariable._getOperators = () => {
return lineFilterOperators;
};

const dsVariable = new DataSourceVariable({
name: VAR_DATASOURCE,
label: 'Data source',
Expand All @@ -515,7 +560,12 @@ function getVariableSet(initialDatasourceUid: string, initialFilters?: AdHocVari
value: '',
hide: VariableHide.hideVariable,
}),
new CustomVariable({ name: VAR_LINE_FILTER, value: '', hide: VariableHide.hideVariable }),
new AdHocFiltersVariable({
name: VAR_LINE_FILTER,
hide: VariableHide.hideVariable,
expressionBuilder: renderLogQLLineFilter,
}),
lineFiltersVariable,

// This variable is a hack to get logs context working, this variable should never be used or updated
new CustomConstantVariable({
Expand Down
162 changes: 110 additions & 52 deletions src/Components/IndexScene/LayoutScene.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,49 @@
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, SceneFlexLayout, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { SceneComponentProps, SceneFlexLayout, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import React from 'react';
import { PatternControls } from './PatternControls';
import { AppliedPattern, IndexSceneState } from './IndexScene';
import { AppliedPattern, IndexScene, IndexSceneState } from './IndexScene';
import { css, cx } from '@emotion/css';
import { GiveFeedbackButton } from './GiveFeedbackButton';
import { InterceptBanner } from './InterceptBanner';

import { PLUGIN_ID } from '../../services/plugin';
import { CustomVariableValueSelectors } from './CustomVariableValueSelectors';
import { logger } from '../../services/logger';
import { LineFilterVariablesScene } from './LineFilterVariablesScene';

interface LayoutSceneState extends SceneObjectState {
interceptDismissed: boolean;
lineFilterRenderer?: LineFilterVariablesScene;
}

const interceptBannerStorageKey = `${PLUGIN_ID}.interceptBannerStorageKey`;

export const CONTROLS_VARS_FIRST_ROW_KEY = 'vars-row__datasource-labels-timepicker-button';
export const CONTROLS_VARS_METADATA_ROW_KEY = 'vars-metadata';
export const CONTROLS_VARS_FIELDS_ELSE_KEY = 'vars-all-else';
export const CONTROLS_VARS_TIMEPICKER = 'vars-timepicker';
export const CONTROLS_VARS_REFRESH = 'vars-refresh';
export const CONTROLS_VARS_TOOLBAR = 'vars-toolbar';

export class LayoutScene extends SceneObjectBase<LayoutSceneState> {
constructor(state: Partial<LayoutSceneState>) {
super({
...state,
interceptDismissed: !!localStorage.getItem(interceptBannerStorageKey),
});
}

public dismiss() {
this.setState({
interceptDismissed: true,
});
localStorage.setItem(interceptBannerStorageKey, 'true');
this.addActivationHandler(this.onActivate.bind(this));
}

static Component = ({ model }: SceneComponentProps<LayoutScene>) => {
if (!model.parent) {
return null;
}
const indexScene = sceneGraph.getAncestor(model, IndexScene);
const { controls, contentScene, patterns } = indexScene.useState();
const { interceptDismissed, lineFilterRenderer } = model.useState();

const { controls, contentScene, patterns } = model.parent.useState() as IndexSceneState;
const { interceptDismissed } = model.useState();
if (!contentScene) {
logger.warn('content scene not defined');
return null;
}

Expand All @@ -54,52 +58,97 @@ export class LayoutScene extends SceneObjectBase<LayoutSceneState> {
}}
/>
)}
{controls && (
<div className={styles.controlsContainer}>
<div className={styles.controlsFirstRowContainer}>
<div className={styles.filtersWrap}>
<div className={cx(styles.filters, styles.firstRowWrapper)}>
{controls.map((control) => {
return control instanceof SceneFlexLayout ? (
<control.Component key={control.state.key} model={control} />
) : null;
})}
<div className={styles.controlsContainer}>
<>
{/* First row - datasource, timepicker, refresh, labels, button */}
{controls && (
<div className={styles.controlsFirstRowContainer}>
<div className={styles.filtersWrap}>
<div className={cx(styles.filters, styles.firstRowWrapper)}>
{controls.map((control) => {
return control instanceof SceneFlexLayout ? (
<control.Component key={control.state.key} model={control} />
) : null;
})}
</div>
</div>
</div>
<div className={styles.controlsWrapper}>
<GiveFeedbackButton />
<div className={styles.controls}>
{controls.map((control) => {
return !(control instanceof CustomVariableValueSelectors) &&
!(control instanceof SceneFlexLayout) ? (
<control.Component key={control.state.key} model={control} />
) : null;
})}
<div className={styles.controlsWrapper}>
<GiveFeedbackButton />
<div className={styles.controls}>
{controls.map((control) => {
return !(control instanceof CustomVariableValueSelectors) &&
!(control instanceof SceneFlexLayout) ? (
<control.Component key={control.state.key} model={control} />
) : null;
})}
</div>
</div>
</div>
)}

{/* Second row - Metadata */}
<div className={styles.controlsRowContainer}>
{controls &&
controls.map((control) => {
return control.state.key === CONTROLS_VARS_METADATA_ROW_KEY ? (
<div className={styles.filtersWrap}>
<div className={styles.filters}>
<control.Component key={control.state.key} model={control} />
</div>
</div>
) : null;
})}
</div>

{/* 3rd row - Patterns */}
<div className={styles.controlsRowContainer}>
<PatternControls
patterns={patterns}
onRemove={(patterns: AppliedPattern[]) => model.parent?.setState({ patterns } as IndexSceneState)}
/>
</div>

{/* 4th row - line filters */}
<div className={styles.controlsRowContainer}>
{lineFilterRenderer && <lineFilterRenderer.Component model={lineFilterRenderer} />}
</div>
<div className={styles.controlsSecondRowContainer}>
<div className={styles.filtersWrap}>
<div className={styles.filters}>
{controls.map((control) => {
return control instanceof CustomVariableValueSelectors ? (
<control.Component key={control.state.key} model={control} />
) : null;
})}

{/* 5th row - Fields */}
<div className={styles.controlsRowContainer}>
{controls && (
<div className={styles.filtersWrap}>
<div className={styles.filters}>
{controls.map((control) => {
return control.state.key === CONTROLS_VARS_FIELDS_ELSE_KEY ? (
<control.Component key={control.state.key} model={control} />
) : null;
})}
</div>
</div>
</div>
)}
</div>
</div>
)}
<PatternControls
patterns={patterns}
onRemove={(patterns: AppliedPattern[]) => model.parent?.setState({ patterns } as IndexSceneState)}
/>
</>
</div>

{/* Final "row" - body */}
<div className={styles.body}>{contentScene && <contentScene.Component model={contentScene} />}</div>
</div>
</div>
);
};

public onActivate() {
this.setState({
lineFilterRenderer: new LineFilterVariablesScene({}),
});
}

public dismiss() {
this.setState({
interceptDismissed: true,
});
localStorage.setItem(interceptBannerStorageKey, 'true');
}
}

function getStyles(theme: GrafanaTheme2) {
Expand Down Expand Up @@ -129,7 +178,6 @@ function getStyles(theme: GrafanaTheme2) {
container: css({
flexGrow: 1,
display: 'flex',
gap: theme.spacing(1),
minHeight: '100%',
flexDirection: 'column',
padding: theme.spacing(2),
Expand All @@ -142,23 +190,33 @@ function getStyles(theme: GrafanaTheme2) {
gap: theme.spacing(1),
}),
controlsFirstRowContainer: css({
label: 'controls-first-row',
display: 'flex',
gap: theme.spacing(2),
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: theme.spacing(2),
}),
controlsSecondRowContainer: css({
controlsRowContainer: css({
'&:empty': {
display: 'none',
},
label: 'controls-row',
display: 'flex',
gap: theme.spacing(2),
// @todo add custom renderers for all variables, this currently results in 2 "empty" rows that always take up space
gap: theme.spacing(1),
justifyContent: 'space-between',
alignItems: 'flex-start',
paddingLeft: theme.spacing(2),
}),
controlsContainer: css({
label: 'controlsContainer',
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
}),
filters: css({
label: 'filters',
display: 'flex',
}),
filtersWrap: css({
label: 'filtersWrap',
Expand Down
Loading
Loading