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

Multiple include: Levels & Labels #1074

Merged
merged 36 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0b499c6
chore: wip
gtk-grafana Feb 13, 2025
79b8014
feat: levels support multiple includes
gtk-grafana Feb 13, 2025
9f38319
chore: remove unused vars
gtk-grafana Feb 13, 2025
e4b46d1
Merge remote-tracking branch 'origin/main' into gtk-grafana/multiple-…
gtk-grafana Feb 13, 2025
4eaea3a
chore: clean up and document
gtk-grafana Feb 13, 2025
849e23e
Merge branch 'main' into gtk-grafana/multiple-include-ui__levels
gtk-grafana Feb 18, 2025
9b0b0c9
Merge remote-tracking branch 'origin/main' into gtk-grafana/multiple-…
gtk-grafana Feb 18, 2025
ee22f73
feat: add labels support, invert tag exclusion
gtk-grafana Feb 18, 2025
b95704a
test: fix/update e2e, fix bugs
gtk-grafana Feb 19, 2025
5c079df
chore: clean up
gtk-grafana Feb 19, 2025
8e31ca9
chore: clean up, remove include on exclude, expression builder unit test
gtk-grafana Feb 19, 2025
f85fa0f
test: update e2e test to include/exclude multiple filters
gtk-grafana Feb 19, 2025
9d10d0c
Apply suggestions from code review
gtk-grafana Feb 19, 2025
89035a5
chore: clean up
gtk-grafana Feb 20, 2025
9886122
Merge remote-tracking branch 'origin/main' into gtk-grafana/multiple-…
gtk-grafana Feb 20, 2025
1443bf1
test: e2e flake
gtk-grafana Feb 20, 2025
3fc58b2
Merge remote-tracking branch 'origin/main' into gtk-grafana/multiple-…
gtk-grafana Feb 20, 2025
6eda6ce
Merge remote-tracking branch 'origin/main' into gtk-grafana/multiple-…
gtk-grafana Feb 21, 2025
8f090a0
test: update e2e
gtk-grafana Feb 21, 2025
df09c78
Multiple include: Fields & Metadata (#1091)
gtk-grafana Feb 24, 2025
8c3deb8
chore: clean up optional operator
gtk-grafana Feb 24, 2025
e568e30
chore: refactor subscription
gtk-grafana Feb 24, 2025
9c114c7
chore: refactor type -> fieldType
gtk-grafana Feb 24, 2025
074ff1f
fix: incorrect log counts or partial queries
gtk-grafana Feb 24, 2025
2315e17
chore: spellcheck
gtk-grafana Feb 24, 2025
a5276a9
fix: manually interpolate variables within query runner
gtk-grafana Feb 24, 2025
0062ac2
chore: subscribe to levels and labels changes
gtk-grafana Feb 24, 2025
d950e0a
chore: clean up e2e tests
gtk-grafana Feb 24, 2025
4eb0a5b
chore: e2e flake
gtk-grafana Feb 24, 2025
efefbea
test: fix assertions on variable pod count
gtk-grafana Feb 24, 2025
1612168
chore: clean up test
gtk-grafana Feb 24, 2025
19d5596
test: fix assertion
gtk-grafana Feb 25, 2025
5bab518
test: hypothesising why ci has different req count
gtk-grafana Feb 25, 2025
bf53f77
Update tests/exploreServicesJsonMixedBreakDown.spec.ts
matyax Feb 25, 2025
4a684cb
test: debug
gtk-grafana Feb 25, 2025
38ffa52
test: tweak test assertion
gtk-grafana Feb 25, 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
29 changes: 15 additions & 14 deletions src/Components/FilterButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ type Props = {
include: string;
exclude: string;
};
hideExclude?: boolean
};

export const FilterButton = (props: Props) => {
const { isExcluded, isIncluded, onInclude, onExclude, onClear, titles, buttonFill } = props;
const styles = useStyles2(getStyles, isIncluded, isExcluded);
const { isExcluded, isIncluded, onInclude, onExclude, onClear, titles, buttonFill, hideExclude } = props;
const styles = useStyles2(getStyles, isIncluded, isExcluded, hideExclude);
return (
<div className={styles.container}>
<Button
Expand All @@ -34,31 +35,31 @@ export const FilterButton = (props: Props) => {
>
Include
</Button>
<Button
variant={isExcluded ? 'primary' : 'secondary'}
fill={buttonFill}
size="sm"
aria-selected={isExcluded}
className={styles.excludeButton}
onClick={isExcluded ? onClear : onExclude}
title={titles?.exclude}
data-testid={testIds.exploreServiceDetails.buttonFilterExclude}
{!hideExclude && <Button
variant={isExcluded ? 'primary' : 'secondary'}
fill={buttonFill}
size="sm"
aria-selected={isExcluded}
className={styles.excludeButton}
onClick={isExcluded ? onClear : onExclude}
title={titles?.exclude}
data-testid={testIds.exploreServiceDetails.buttonFilterExclude}
>
Exclude
</Button>
</Button>}
</div>
);
};

const getStyles = (theme: GrafanaTheme2, isIncluded: boolean, isExcluded: boolean) => {
const getStyles = (theme: GrafanaTheme2, isIncluded: boolean, isExcluded: boolean, hideExclude?: boolean) => {
return {
container: css({
display: 'flex',
justifyContent: 'center',
}),
includeButton: css({
borderRadius: 0,
borderRight: isIncluded ? undefined : 'none',
borderRight: isIncluded || hideExclude ? undefined : 'none',
}),
excludeButton: css({
borderRadius: `0 ${theme.shape.radius.default} ${theme.shape.radius.default} 0`,
Expand Down
73 changes: 9 additions & 64 deletions src/Components/IndexScene/LevelsVariableScene.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
ControlsLabel,
SceneComponentProps,
SceneObjectBase,
SceneObjectState,
SceneVariableValueChangedEvent,
} from '@grafana/scenes';
import { ControlsLabel, SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import React from 'react';
import { getLevelsVariable } from '../../services/variableGetters';
import { GrafanaTheme2, MetricFindValue, SelectableValue } from '@grafana/data';
Expand All @@ -31,21 +25,17 @@ export class LevelsVariableScene extends SceneObjectBase<LevelsVariableSceneStat

onActivate() {
this.onFilterChange();
const levelsVar = getLevelsVariable(this);
levelsVar.subscribeToEvent(SceneVariableValueChangedEvent, () => this.onFilterChange());
}

private onFilterChange() {
public onFilterChange() {
const levelsVar = getLevelsVariable(this);
if (levelsVar.state.filters.length) {
this.setState({
options: levelsVar.state.filters.map((filter) => ({
text: filter.valueLabels?.[0] ?? filter.value,
selected: true,
value: filter.value,
})),
});
}
this.setState({
options: levelsVar.state.filters.map((filter) => ({
text: filter.valueLabels?.[0] ?? filter.value,
selected: true,
value: filter.value,
})),
});
}

getTagValues = () => {
Expand Down Expand Up @@ -156,49 +146,4 @@ const getStyles = (theme: GrafanaTheme2) => ({
flex: css({
flex: '1',
}),
removeButton: css({
marginInline: theme.spacing(0.5),
cursor: 'pointer',
'&:hover': {
color: theme.colors.text.primary,
},
}),
pillText: css({
maxWidth: '200px',
width: '100%',
textOverflow: 'ellipsis',
overflow: 'hidden',
}),
tooltipText: css({
textAlign: 'center',
}),
comboboxWrapper: css({
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
columnGap: theme.spacing(1),
rowGap: theme.spacing(0.5),
minHeight: theme.spacing(4),
backgroundColor: theme.components.input.background,
border: `1px solid ${theme.colors.border.strong}`,
borderRadius: theme.shape.radius.default,
paddingInline: theme.spacing(1),
paddingBlock: theme.spacing(0.5),
flexGrow: 1,
}),
comboboxFocusOutline: css({
'&:focus-within': {
outline: '2px dotted transparent',
outlineOffset: '2px',
boxShadow: `0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main}`,
transitionTimingFunction: `cubic-bezier(0.19, 1, 0.22, 1)`,
transitionDuration: '0.2s',
transitionProperty: 'outline, outline-offset, box-shadow',
zIndex: 2,
},
}),
filterIcon: css({
color: theme.colors.text.secondary,
alignSelf: 'center',
}),
});
121 changes: 85 additions & 36 deletions src/Components/ServiceScene/Breakdowns/AddToFiltersButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React from 'react';

import { AdHocVariableFilter, BusEventBase, DataFrame } from '@grafana/data';
import { SceneComponentProps, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import {
AdHocFiltersVariable,
SceneComponentProps,
SceneObject,
SceneObjectBase,
SceneObjectState,
} from '@grafana/scenes';
import { VariableHide } from '@grafana/schema';
import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from 'services/analytics';
import {
Expand All @@ -24,14 +30,24 @@ import {
import { FilterOp, NumericFilterOp } from '../../../services/filterTypes';

import { addToFavorites } from '../../../services/favorites';
import { areArraysEqual } from '../../../services/comparison';
import { logger } from '../../../services/logger';

export interface AddToFiltersButtonState extends SceneObjectState {
frame: DataFrame;
variableName: InterpolatedFilterType;
hideExclude?: boolean;
isIncluded?: boolean;
isExcluded?: boolean;
}

export class AddFilterEvent extends BusEventBase {
constructor(public operator: FilterType | NumericFilterType, public key: string, public value: string) {
constructor(
public source: 'legend' | 'filterButton' | 'variable',
public operator?: FilterType | NumericFilterType,
public key?: string,
public value?: string
) {
super();
}
public static type = 'add-filter';
Expand Down Expand Up @@ -109,7 +125,7 @@ const getNumericOperatorType = (op: NumericFilterType | string): OperatorType |
return undefined;
};

export function removeFilter(
export function removeNumericFilter(
key: string,
scene: SceneObject,
operator?: NumericFilterType,
Expand Down Expand Up @@ -172,11 +188,11 @@ export function addNumericFilter(
},
];

scene.publishEvent(new AddFilterEvent(operator, key, value), true);

variable.setState({
filters,
});

scene.publishEvent(new AddFilterEvent('filterButton', operator, key, value), true);
}

export function addToFilters(
Expand Down Expand Up @@ -210,7 +226,10 @@ export function addToFilters(

// if we're including, we want to remove all filters that have this key
if (operator === 'include') {
return !(filter.key === key && filter.operator !== FilterOp.Equal);
return !(filter.key === key && filter.operator === FilterOp.NotEqual);
}
if (operator === 'exclude') {
return !(filter.key === key && filter.operator === FilterOp.Equal);
}

return !(filter.key === key && fieldValue.value === value);
Expand All @@ -230,11 +249,12 @@ export function addToFilters(
];
}

scene.publishEvent(new AddFilterEvent(operator, key, value), true);

// Variable needs to be updated before event is published!
variable.setState({
filters,
});

scene.publishEvent(new AddFilterEvent('filterButton', operator, key, value), true);
}

export function replaceFilter(
Expand Down Expand Up @@ -272,6 +292,58 @@ function resolveVariableTypeForField(field: string, scene: SceneObject): Interpo
}

export class AddToFiltersButton extends SceneObjectBase<AddToFiltersButtonState> {
constructor(state: AddToFiltersButtonState) {
super(state);

this.addActivationHandler(this.onActivate.bind(this));
}

onActivate() {
const filter = getFilter(this.state.frame);
if (filter) {
const variable = getUIAdHocVariable(this.state.variableName, filter.name, this);
this.setFilterState(variable);

this._subs.add(
variable.subscribeToState((newState, prevState) => {
if (!areArraysEqual(newState.filters, prevState.filters)) {
this.setFilterState(variable);
}
})
);
}
}

private setFilterState(variable: AdHocFiltersVariable) {
const filter = getFilter(this.state.frame);
if (!filter) {
this.setState({
isIncluded: false,
isExcluded: false,
});
return;
}

// Check if the filter is already there
const filterInSelectedFilters = variable.state.filters.find((f) => {
const value = getValueFromAdHocVariableFilter(variable, f);
return f.key === filter.name && value.value === filter.value;
});

if (!filterInSelectedFilters) {
this.setState({
isIncluded: false,
isExcluded: false,
});
return;
}

this.setState({
isIncluded: filterInSelectedFilters.operator === FilterOp.Equal,
isExcluded: filterInSelectedFilters.operator === FilterOp.NotEqual,
});
}

public onClick = (type: FilterType) => {
const filter = getFilter(this.state.frame);
if (!filter) {
Expand All @@ -293,41 +365,17 @@ export class AddToFiltersButton extends SceneObjectBase<AddToFiltersButtonState>
);
};

isSelected = () => {
const filter = getFilter(this.state.frame);
if (!filter) {
return { isIncluded: false, isExcluded: false };
}

const variable = getUIAdHocVariable(this.state.variableName, filter.name, this);

// Check if the filter is already there
const filterInSelectedFilters = variable.state.filters.find((f) => {
const value = getValueFromAdHocVariableFilter(variable, f);
return f.key === filter.name && value.value === filter.value;
});

if (!filterInSelectedFilters) {
return { isIncluded: false, isExcluded: false };
}

// @todo support regex operators?
return {
isIncluded: filterInSelectedFilters.operator === FilterOp.Equal,
isExcluded: filterInSelectedFilters.operator === FilterOp.NotEqual,
};
};

public static Component = ({ model }: SceneComponentProps<AddToFiltersButton>) => {
const { isIncluded, isExcluded } = model.isSelected();
const { hideExclude, isExcluded, isIncluded } = model.useState();
return (
<FilterButton
buttonFill={'outline'}
isIncluded={isIncluded}
isExcluded={isExcluded}
isIncluded={isIncluded ?? false}
isExcluded={isExcluded ?? false}
onInclude={() => model.onClick('include')}
onClear={() => model.onClick('clear')}
onExclude={() => model.onClick('exclude')}
hideExclude={hideExclude}
/>
);
};
Expand All @@ -338,6 +386,7 @@ const getFilter = (frame: DataFrame) => {
const filterNameAndValueObj = frame.fields[1]?.labels ?? {};
// Sanity check - filter should have only one key-value pair
if (Object.keys(filterNameAndValueObj).length !== 1) {
logger.warn('getFilter: unexpected empty labels');
return;
}
const name = Object.keys(filterNameAndValueObj)[0];
Expand Down
Loading
Loading