Tempo: Improve tags UX (#81166)
* Only show add/remove tag when necessary in span filters * Only show add/remove tag when necessary in traceql * Only show add/remove tag when necessary in aggregateBy * Update styles * Add tests * Show remove tag for all if more than one tag * Also check for value only in search editor and update tests
This commit is contained in:
+1
-2
@@ -3421,8 +3421,7 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
[0, 0, 0, "Styles should be written using objects.", "2"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
[0, 0, 0, "Styles should be written using objects.", "4"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "5"],
|
[0, 0, 0, "Styles should be written using objects.", "5"]
|
||||||
[0, 0, 0, "Styles should be written using objects.", "6"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx:5381": [
|
"public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/CanvasSpanGraph.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||||
|
|||||||
+34
-4
@@ -100,8 +100,6 @@ describe('SpanFilters', () => {
|
|||||||
const tagKey = screen.getByLabelText('Select tag key');
|
const tagKey = screen.getByLabelText('Select tag key');
|
||||||
const tagOperator = screen.getByLabelText('Select tag operator');
|
const tagOperator = screen.getByLabelText('Select tag operator');
|
||||||
const tagValue = screen.getByLabelText('Select tag value');
|
const tagValue = screen.getByLabelText('Select tag value');
|
||||||
const addTag = screen.getByLabelText('Add tag');
|
|
||||||
const removeTag = screen.getByLabelText('Remove tag');
|
|
||||||
|
|
||||||
expect(serviceOperator).toBeInTheDocument();
|
expect(serviceOperator).toBeInTheDocument();
|
||||||
expect(getElemText(serviceOperator)).toBe('=');
|
expect(getElemText(serviceOperator)).toBe('=');
|
||||||
@@ -119,8 +117,6 @@ describe('SpanFilters', () => {
|
|||||||
expect(tagOperator).toBeInTheDocument();
|
expect(tagOperator).toBeInTheDocument();
|
||||||
expect(getElemText(tagOperator)).toBe('=');
|
expect(getElemText(tagOperator)).toBe('=');
|
||||||
expect(tagValue).toBeInTheDocument();
|
expect(tagValue).toBeInTheDocument();
|
||||||
expect(addTag).toBeInTheDocument();
|
|
||||||
expect(removeTag).toBeInTheDocument();
|
|
||||||
|
|
||||||
await user.click(serviceValue);
|
await user.click(serviceValue);
|
||||||
jest.advanceTimersByTime(1000);
|
jest.advanceTimersByTime(1000);
|
||||||
@@ -194,9 +190,41 @@ describe('SpanFilters', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should only show add/remove tag when necessary', async () => {
|
||||||
|
render(<SpanFiltersWithProps />);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
||||||
|
expect(screen.queryAllByLabelText('Remove tag').length).toBe(0); // mot filled in the default tag, so no values to remove
|
||||||
|
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
|
||||||
|
|
||||||
|
await selectAndCheckValue(user, screen.getByLabelText('Select tag key'), 'TagKey0');
|
||||||
|
expect(screen.getAllByLabelText('Add tag').length).toBe(1);
|
||||||
|
expect(screen.getAllByLabelText('Remove tag').length).toBe(1);
|
||||||
|
|
||||||
|
await user.click(screen.getByLabelText('Add tag'));
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the new tag, so no need to add another one
|
||||||
|
expect(screen.getAllByLabelText('Remove tag').length).toBe(2); // one for each tag
|
||||||
|
expect(screen.getAllByLabelText('Select tag key').length).toBe(2);
|
||||||
|
|
||||||
|
await user.click(screen.getAllByLabelText('Remove tag')[1]);
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(1); // filled in the default tag, so can add another one
|
||||||
|
expect(screen.queryAllByLabelText('Remove tag').length).toBe(1); // filled in the default tag, so can remove values
|
||||||
|
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
|
||||||
|
|
||||||
|
await user.click(screen.getAllByLabelText('Remove tag')[0]);
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
||||||
|
expect(screen.queryAllByLabelText('Remove tag').length).toBe(0); // mot filled in the default tag, so no values to remove
|
||||||
|
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('should allow adding/removing tags', async () => {
|
it('should allow adding/removing tags', async () => {
|
||||||
render(<SpanFiltersWithProps />);
|
render(<SpanFiltersWithProps />);
|
||||||
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
|
expect(screen.getAllByLabelText('Select tag key').length).toBe(1);
|
||||||
|
const tagKey = screen.getByLabelText('Select tag key');
|
||||||
|
await selectAndCheckValue(user, tagKey, 'TagKey0');
|
||||||
|
|
||||||
await user.click(screen.getByLabelText('Add tag'));
|
await user.click(screen.getByLabelText('Add tag'));
|
||||||
jest.advanceTimersByTime(1000);
|
jest.advanceTimersByTime(1000);
|
||||||
expect(screen.getAllByLabelText('Select tag key').length).toBe(2);
|
expect(screen.getAllByLabelText('Select tag key').length).toBe(2);
|
||||||
@@ -232,6 +260,8 @@ describe('SpanFilters', () => {
|
|||||||
expect(screen.queryByText('Span0')).not.toBeInTheDocument();
|
expect(screen.queryByText('Span0')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('TagKey0')).not.toBeInTheDocument();
|
expect(screen.queryByText('TagKey0')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('TagValue0')).not.toBeInTheDocument();
|
expect(screen.queryByText('TagValue0')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Add tag')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Remove tag')).not.toBeInTheDocument();
|
||||||
expect(matchesSwitch).not.toBeChecked();
|
expect(matchesSwitch).not.toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+17
-15
@@ -439,24 +439,26 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
|||||||
value={tag.value}
|
value={tag.value}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<AccessoryButton
|
{(tag.key || tag.value || search.tags.length > 1) && (
|
||||||
aria-label="Remove tag"
|
<AccessoryButton
|
||||||
variant="secondary"
|
aria-label="Remove tag"
|
||||||
icon="times"
|
variant="secondary"
|
||||||
onClick={() => removeTag(tag.id)}
|
icon="times"
|
||||||
title="Remove tag"
|
onClick={() => removeTag(tag.id)}
|
||||||
/>
|
tooltip="Remove tag"
|
||||||
<span className={styles.addTag}>
|
/>
|
||||||
{search?.tags?.length && i === search.tags.length - 1 && (
|
)}
|
||||||
|
{(tag.key || tag.value) && i === search.tags.length - 1 && (
|
||||||
|
<span className={styles.addTag}>
|
||||||
<AccessoryButton
|
<AccessoryButton
|
||||||
aria-label="Add tag"
|
aria-label="Add tag"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
onClick={addTag}
|
onClick={addTag}
|
||||||
title="Add tag"
|
tooltip="Add tag"
|
||||||
/>
|
/>
|
||||||
)}
|
</span>
|
||||||
</span>
|
)}
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -508,9 +510,9 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}),
|
}),
|
||||||
addTag: css`
|
addTag: css({
|
||||||
margin: 0 0 0 10px;
|
marginLeft: theme.spacing(1),
|
||||||
`,
|
}),
|
||||||
intervalInput: css`
|
intervalInput: css`
|
||||||
margin: 0 -4px 0 0;
|
margin: 0 -4px 0 0;
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { TraceqlSearchScope } from '../dataquery.gen';
|
import { TraceqlSearchScope } from '../dataquery.gen';
|
||||||
import { TempoDatasource } from '../datasource';
|
import { TempoDatasource } from '../datasource';
|
||||||
@@ -46,6 +46,52 @@ describe('GroupByField', () => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should only show add/remove tag when necessary', async () => {
|
||||||
|
const GroupByWithProps = () => {
|
||||||
|
const [query, setQuery] = useState<TempoQuery>({
|
||||||
|
refId: 'A',
|
||||||
|
queryType: 'traceqlSearch',
|
||||||
|
key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0',
|
||||||
|
filters: [],
|
||||||
|
groupBy: [{ id: 'group-by-id', scope: TraceqlSearchScope.Span }],
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<GroupByField
|
||||||
|
datasource={datasource}
|
||||||
|
query={query}
|
||||||
|
onChange={(q: TempoQuery) => setQuery(q)}
|
||||||
|
isTagsLoading={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render(<GroupByWithProps />);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
||||||
|
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(0); // mot filled in the default tag, so no values to remove
|
||||||
|
expect(screen.getAllByText('Select tag').length).toBe(1);
|
||||||
|
|
||||||
|
await user.click(screen.getByText('Select tag'));
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await user.click(screen.getByText('http.method'));
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(screen.getAllByLabelText('Add tag').length).toBe(1);
|
||||||
|
expect(screen.getAllByLabelText(/Remove tag/).length).toBe(1);
|
||||||
|
|
||||||
|
await user.click(screen.getByLabelText('Add tag'));
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the new tag, so no need to add another one
|
||||||
|
expect(screen.getAllByLabelText(/Remove tag/).length).toBe(2); // one for each tag
|
||||||
|
|
||||||
|
await user.click(screen.getAllByLabelText(/Remove tag/)[1]);
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(1); // filled in the default tag, so can add another one
|
||||||
|
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(1); // filled in the default tag, so can remove values
|
||||||
|
|
||||||
|
await user.click(screen.getAllByLabelText(/Remove tag/)[0]);
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
||||||
|
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(0); // mot filled in the default tag, so no values to remove
|
||||||
|
});
|
||||||
|
|
||||||
it('should update scope when new value is selected in scope input', async () => {
|
it('should update scope when new value is selected in scope input', async () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<GroupByField datasource={datasource} query={query} onChange={onChange} isTagsLoading={false} />
|
<GroupByField datasource={datasource} query={query} onChange={onChange} isTagsLoading={false} />
|
||||||
|
|||||||
@@ -101,16 +101,18 @@ export const GroupByField = (props: Props) => {
|
|||||||
placeholder="Select tag"
|
placeholder="Select tag"
|
||||||
value={f.tag || ''}
|
value={f.tag || ''}
|
||||||
/>
|
/>
|
||||||
<AccessoryButton
|
{(f.tag || (query.groupBy?.length ?? 0) > 1) && (
|
||||||
aria-label={`Remove tag for filter ${i + 1}`}
|
<AccessoryButton
|
||||||
icon="times"
|
aria-label={`Remove tag for filter ${i + 1}`}
|
||||||
onClick={() => removeFilter(f)}
|
icon="times"
|
||||||
tooltip="Remove tag"
|
onClick={() => removeFilter(f)}
|
||||||
variant="secondary"
|
tooltip="Remove tag"
|
||||||
/>
|
title={`Remove tag for filter ${i + 1}`}
|
||||||
|
variant="secondary"
|
||||||
{i === (query.groupBy?.length ?? 0) - 1 && (
|
/>
|
||||||
<span className={styles.addFilter}>
|
)}
|
||||||
|
{f.tag && i === (query.groupBy?.length ?? 0) - 1 && (
|
||||||
|
<span className={styles.addTag}>
|
||||||
<AccessoryButton
|
<AccessoryButton
|
||||||
aria-label="Add tag"
|
aria-label="Add tag"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
@@ -129,7 +131,7 @@ export const GroupByField = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
addFilter: css({
|
addTag: css({
|
||||||
marginLeft: theme.spacing(2),
|
marginLeft: theme.spacing(1),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import React, { useState, useEffect, useMemo } from 'react';
|
|||||||
import useAsync from 'react-use/lib/useAsync';
|
import useAsync from 'react-use/lib/useAsync';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { AccessoryButton } from '@grafana/experimental';
|
|
||||||
import { FetchError, getTemplateSrv, isFetchError } from '@grafana/runtime';
|
import { FetchError, getTemplateSrv, isFetchError } from '@grafana/runtime';
|
||||||
import { Select, HorizontalGroup, useStyles2 } from '@grafana/ui';
|
import { Select, HorizontalGroup, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
@@ -27,28 +26,24 @@ interface Props {
|
|||||||
filter: TraceqlFilter;
|
filter: TraceqlFilter;
|
||||||
datasource: TempoDatasource;
|
datasource: TempoDatasource;
|
||||||
updateFilter: (f: TraceqlFilter) => void;
|
updateFilter: (f: TraceqlFilter) => void;
|
||||||
deleteFilter?: (f: TraceqlFilter) => void;
|
|
||||||
setError: (error: FetchError) => void;
|
setError: (error: FetchError) => void;
|
||||||
isTagsLoading?: boolean;
|
isTagsLoading?: boolean;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
hideScope?: boolean;
|
hideScope?: boolean;
|
||||||
hideTag?: boolean;
|
hideTag?: boolean;
|
||||||
hideValue?: boolean;
|
hideValue?: boolean;
|
||||||
allowDelete?: boolean;
|
|
||||||
query: string;
|
query: string;
|
||||||
}
|
}
|
||||||
const SearchField = ({
|
const SearchField = ({
|
||||||
filter,
|
filter,
|
||||||
datasource,
|
datasource,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
deleteFilter,
|
|
||||||
isTagsLoading,
|
isTagsLoading,
|
||||||
tags,
|
tags,
|
||||||
setError,
|
setError,
|
||||||
hideScope,
|
hideScope,
|
||||||
hideTag,
|
hideTag,
|
||||||
hideValue,
|
hideValue,
|
||||||
allowDelete,
|
|
||||||
query,
|
query,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@@ -207,15 +202,6 @@ const SearchField = ({
|
|||||||
allowCreateWhileLoading
|
allowCreateWhileLoading
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{allowDelete && (
|
|
||||||
<AccessoryButton
|
|
||||||
variant={'secondary'}
|
|
||||||
icon={'times'}
|
|
||||||
onClick={() => deleteFilter?.(filter)}
|
|
||||||
tooltip={'Remove tag'}
|
|
||||||
aria-label={`remove tag with ID ${filter.id}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { AccessoryButton } from '@grafana/experimental';
|
import { AccessoryButton } from '@grafana/experimental';
|
||||||
import { FetchError } from '@grafana/runtime';
|
import { FetchError } from '@grafana/runtime';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
@@ -12,16 +13,19 @@ import { TempoDatasource } from '../datasource';
|
|||||||
import SearchField from './SearchField';
|
import SearchField from './SearchField';
|
||||||
import { getFilteredTags } from './utils';
|
import { getFilteredTags } from './utils';
|
||||||
|
|
||||||
const getStyles = () => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
vertical: css({
|
vertical: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '0.25rem',
|
gap: theme.spacing(0.25),
|
||||||
}),
|
}),
|
||||||
horizontal: css({
|
horizontal: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: '1rem',
|
gap: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
addTag: css({
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,6 +38,7 @@ interface Props {
|
|||||||
staticTags: Array<string | undefined>;
|
staticTags: Array<string | undefined>;
|
||||||
isTagsLoading: boolean;
|
isTagsLoading: boolean;
|
||||||
hideValues?: boolean;
|
hideValues?: boolean;
|
||||||
|
requireTagAndValue?: boolean;
|
||||||
query: string;
|
query: string;
|
||||||
}
|
}
|
||||||
const TagsInput = ({
|
const TagsInput = ({
|
||||||
@@ -45,6 +50,7 @@ const TagsInput = ({
|
|||||||
staticTags,
|
staticTags,
|
||||||
isTagsLoading,
|
isTagsLoading,
|
||||||
hideValues,
|
hideValues,
|
||||||
|
requireTagAndValue,
|
||||||
query,
|
query,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@@ -65,6 +71,11 @@ const TagsInput = ({
|
|||||||
return getFilteredTags(tags, staticTags);
|
return getFilteredTags(tags, staticTags);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validInput = (f: TraceqlFilter) => {
|
||||||
|
// If value is removed from the filter, it can be set as an empty array
|
||||||
|
return requireTagAndValue ? f.tag && f.value && f.value.length > 0 : f.tag;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.vertical}>
|
<div className={styles.vertical}>
|
||||||
{filters?.map((f, i) => (
|
{filters?.map((f, i) => (
|
||||||
@@ -76,13 +87,28 @@ const TagsInput = ({
|
|||||||
updateFilter={updateFilter}
|
updateFilter={updateFilter}
|
||||||
tags={getTags(f)}
|
tags={getTags(f)}
|
||||||
isTagsLoading={isTagsLoading}
|
isTagsLoading={isTagsLoading}
|
||||||
deleteFilter={deleteFilter}
|
|
||||||
allowDelete={true}
|
|
||||||
hideValue={hideValues}
|
hideValue={hideValues}
|
||||||
query={query}
|
query={query}
|
||||||
/>
|
/>
|
||||||
{i === filters.length - 1 && (
|
{(validInput(f) || filters.length > 1) && (
|
||||||
<AccessoryButton variant={'secondary'} icon={'plus'} onClick={handleOnAdd} title={'Add tag'} />
|
<AccessoryButton
|
||||||
|
aria-label={`Remove tag with ID ${f.id}`}
|
||||||
|
variant={'secondary'}
|
||||||
|
icon={'times'}
|
||||||
|
onClick={() => deleteFilter?.(f)}
|
||||||
|
tooltip={'Remove tag'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{validInput(f) && i === filters.length - 1 && (
|
||||||
|
<span className={styles.addTag}>
|
||||||
|
<AccessoryButton
|
||||||
|
aria-label="Add tag"
|
||||||
|
variant={'secondary'}
|
||||||
|
icon={'plus'}
|
||||||
|
onClick={handleOnAdd}
|
||||||
|
tooltip={'Add tag'}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
@@ -89,6 +88,62 @@ describe('TraceQLSearch', () => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should only show add/remove tag when necessary', async () => {
|
||||||
|
const TraceQLSearchWithProps = () => {
|
||||||
|
const [query, setQuery] = useState<TempoQuery>({
|
||||||
|
refId: 'A',
|
||||||
|
queryType: 'traceqlSearch',
|
||||||
|
key: 'Q-595a9bbc-2a25-49a7-9249-a52a0a475d83-0',
|
||||||
|
filters: [],
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<TraceQLSearch
|
||||||
|
datasource={datasource}
|
||||||
|
query={query}
|
||||||
|
onChange={(q: TempoQuery) => setQuery(q)}
|
||||||
|
onClearResults={onClearResults}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render(<TraceQLSearchWithProps />);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
||||||
|
expect(screen.queryAllByLabelText('Remove tag').length).toBe(0); // mot filled in the default tag, so no values to remove
|
||||||
|
expect(screen.getAllByText('Select tag').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.click(screen.getByText('Select tag'));
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await user.click(screen.getByText('foo'));
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await user.click(screen.getAllByText('Select value')[2]);
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await user.click(screen.getByText('driver'));
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await act(async () => {
|
||||||
|
expect(screen.getAllByLabelText('Add tag').length).toBe(1);
|
||||||
|
expect(screen.getAllByLabelText(/Remove tag/).length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.click(screen.getByLabelText('Add tag'));
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the new tag, so no need to add another one
|
||||||
|
expect(screen.getAllByLabelText(/Remove tag/).length).toBe(2); // one for each tag
|
||||||
|
|
||||||
|
await user.click(screen.getAllByLabelText(/Remove tag/)[1]);
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(1); // filled in the default tag, so can add another one
|
||||||
|
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(1); // filled in the default tag, so can remove values
|
||||||
|
|
||||||
|
await user.click(screen.getAllByLabelText(/Remove tag/)[0]);
|
||||||
|
jest.advanceTimersByTime(1000);
|
||||||
|
await act(async () => {
|
||||||
|
expect(screen.queryAllByLabelText('Add tag').length).toBe(0); // not filled in the default tag, so no need to add another one
|
||||||
|
expect(screen.queryAllByLabelText(/Remove tag/).length).toBe(0); // mot filled in the default tag, so no values to remove
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should update operator when new value is selected in operator input', async () => {
|
it('should update operator when new value is selected in operator input', async () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TraceQLSearch datasource={datasource} query={query} onChange={onChange} onClearResults={onClearResults} />
|
<TraceQLSearch datasource={datasource} query={query} onChange={onChange} onClearResults={onClearResults} />
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app }: Pro
|
|||||||
staticTags={staticTags}
|
staticTags={staticTags}
|
||||||
isTagsLoading={isTagsLoading}
|
isTagsLoading={isTagsLoading}
|
||||||
query={traceQlQuery}
|
query={traceQlQuery}
|
||||||
|
requireTagAndValue={true}
|
||||||
/>
|
/>
|
||||||
</InlineSearchField>
|
</InlineSearchField>
|
||||||
{config.featureToggles.metricsSummary && (
|
{config.featureToggles.metricsSummary && (
|
||||||
|
|||||||
Reference in New Issue
Block a user