Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 44 additions & 1 deletion static/app/utils/discover/fieldRenderers.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ThemeFixture} from 'sentry-fixture/theme';
import {UserFixture} from 'sentry-fixture/user';
import {WidgetFixture} from 'sentry-fixture/widget';

import {initializeOrg} from 'sentry-test/initializeOrg';
import {act, render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
Expand All @@ -8,14 +10,17 @@ import ProjectsStore from 'sentry/stores/projectsStore';
import EventView from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {SPAN_OP_RELATIVE_BREAKDOWN_FIELD} from 'sentry/utils/discover/fields';
import {WidgetType, type DashboardFilters} from 'sentry/views/dashboards/types';

const theme = ThemeFixture();

describe('getFieldRenderer', () => {
let location: any, context: any, project: any, organization: any, data: any, user: any;

beforeEach(() => {
context = initializeOrg();
context = initializeOrg({
organization: OrganizationFixture({features: ['dashboards-drilldown-flow']}),
});
organization = context.organization;
project = context.project;
act(() => ProjectsStore.loadInitialData([project]));
Expand Down Expand Up @@ -113,6 +118,44 @@ describe('getFieldRenderer', () => {
expect(screen.getByText(data.numeric)).toBeInTheDocument();
});

it('can render dashboard links', () => {
const widget = WidgetFixture({
widgetType: WidgetType.SPANS,
queries: [
{
linkedDashboards: [{dashboardId: '123', field: 'transaction'}],
aggregates: [],
columns: [],
conditions: '',
name: '',
orderby: '',
},
],
});
const dashboardFilters: DashboardFilters = {};

const renderer = getFieldRenderer(
'transaction',
{transaction: 'string'},
undefined,
widget,
dashboardFilters
);

render(
renderer(data, {
location,
organization,
theme,
}) as React.ReactElement<any, any>
);

expect(screen.getByRole('link')).toHaveAttribute(
'href',
'/organizations/org-slug/dashboard/123/?globalFilter=%7B%22dataset%22%3A%22spans%22%2C%22tag%22%3A%7B%22key%22%3A%22transaction%22%2C%22name%22%3A%22transaction%22%2C%22kind%22%3A%22tag%22%7D%2C%22value%22%3A%22transaction%3A%5Bapi.do_things%5D%22%2C%22isTemporary%22%3Atrue%7D'
);
});

describe('rate', () => {
it('can render null rate', () => {
const renderer = getFieldRenderer(
Expand Down
9 changes: 7 additions & 2 deletions static/app/utils/discover/fieldRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1430,7 +1430,11 @@ function getDashboardUrl(
if (dashboardLink && dashboardLink.dashboardId !== '-1') {
const newTemporaryFilters: GlobalFilter[] = [
...(dashboardFilters[DashboardFilterKeys.GLOBAL_FILTER] ?? []),
].filter(filter => Boolean(filter.value));
].filter(
filter =>
Boolean(filter.value) &&
!(filter.tag.key === field && filter.dataset === widget.widgetType)
);

// Format the value as a proper filter condition string
const mutableSearch = new MutableSearch('');
Expand All @@ -1440,6 +1444,7 @@ function getDashboardUrl(
dataset: widget.widgetType,
tag: {key: field, name: field, kind: FieldKind.TAG},
value: formattedValue,
isTemporary: true,
});

// Preserve project, environment, and time range query params
Expand All @@ -1455,7 +1460,7 @@ function getDashboardUrl(
const url = `/organizations/${organization.slug}/dashboard/${dashboardLink.dashboardId}/?${qs.stringify(
{
...filterParams,
[DashboardFilterKeys.TEMPORARY_FILTERS]: newTemporaryFilters.map(filter =>
[DashboardFilterKeys.GLOBAL_FILTER]: newTemporaryFilters.map(filter =>
JSON.stringify(filter)
),
}
Expand Down
8 changes: 1 addition & 7 deletions static/app/views/dashboards/detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,6 @@ class DashboardDetail extends Component<Props, State> {
filters={{}} // Default Dashboards don't have filters set
location={location}
hasUnsavedChanges={false}
hasTemporaryFilters={false}
isEditingDashboard={false}
isPreview={false}
onDashboardFilterChange={this.handleChangeFilter}
Expand Down Expand Up @@ -1043,10 +1042,6 @@ class DashboardDetail extends Component<Props, State> {
dashboardState !== DashboardState.CREATE &&
hasUnsavedFilterChanges(dashboard, location);

const hasTemporaryFilters = defined(
location.query?.[DashboardFilterKeys.TEMPORARY_FILTERS]
);

const eventView = generatePerformanceEventView(location, projects, {}, organization);

const isDashboardUsingTransaction = dashboard.widgets.some(
Expand Down Expand Up @@ -1179,15 +1174,14 @@ class DashboardDetail extends Component<Props, State> {
dashboardCreator={dashboard.createdBy}
location={location}
hasUnsavedChanges={!this.isEmbedded && hasUnsavedFilters}
hasTemporaryFilters={hasTemporaryFilters}
isEditingDashboard={
dashboardState !== DashboardState.CREATE &&
this.isEditingDashboard
}
isPreview={this.isPreview}
onDashboardFilterChange={this.handleChangeFilter}
shouldBusySaveButton={this.state.isSavingDashboardFilters}
isPrebuiltDashboard={defined(dashboard.prebuiltId)}
prebuiltDashboardId={dashboard.prebuiltId}
onCancel={() => {
resetPageFilters(dashboard, location);
trackAnalytics('dashboards2.filter.cancel', {
Expand Down
172 changes: 172 additions & 0 deletions static/app/views/dashboards/filtersBar.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// create a basic test for filters bar

import {LocationFixture} from 'sentry-fixture/locationFixture';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ReleaseFixture} from 'sentry-fixture/release';
import {TagsFixture} from 'sentry-fixture/tags';

import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary';

import type {Organization} from 'sentry/types/organization';
import {FieldKind} from 'sentry/utils/fields';
import FiltersBar, {type FiltersBarProps} from 'sentry/views/dashboards/filtersBar';
import {
DashboardFilterKeys,
WidgetType,
type GlobalFilter,
} from 'sentry/views/dashboards/types';
import {PrebuiltDashboardId} from 'sentry/views/dashboards/utils/prebuiltConfigs';

describe('FiltersBar', () => {
let organization: Organization;

beforeEach(() => {
mockNetworkRequests();

organization = OrganizationFixture({
features: ['dashboards-basic', 'dashboards-edit', 'dashboards-global-filters'],
});
});

afterEach(() => {
MockApiClient.clearMockResponses();
jest.clearAllMocks();
});

const renderFilterBar = (overrides: Partial<FiltersBarProps> = {}) => {
const props: FiltersBarProps = {
filters: {},
hasUnsavedChanges: false,
isEditingDashboard: false,
isPreview: false,
location: LocationFixture(),
onDashboardFilterChange: () => {},
...overrides,
};

return render(<FiltersBar {...props} />, {organization});
};

it('should render basic global filter', async () => {
const newLocation = LocationFixture({
query: {
[DashboardFilterKeys.GLOBAL_FILTER]: JSON.stringify({
dataset: WidgetType.SPANS,
tag: {key: 'browser.name', name: 'Browser Name', kind: FieldKind.FIELD},
value: `browser.name:[Chrome]`,
} satisfies GlobalFilter),
},
});
renderFilterBar({location: newLocation});
await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
expect(
screen.getByRole('button', {name: /browser\.name.*Chrome/i})
).toBeInTheDocument();
});

it('should render save button with unsaved changes', async () => {
const newLocation = LocationFixture({
query: {
[DashboardFilterKeys.GLOBAL_FILTER]: JSON.stringify({
dataset: WidgetType.SPANS,
tag: {key: 'browser.name', name: 'Browser Name', kind: FieldKind.FIELD},
value: `browser.name:[Chrome]`,
} satisfies GlobalFilter),
},
});
renderFilterBar({location: newLocation, hasUnsavedChanges: true});
await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Cancel'})).toBeInTheDocument();
});

it('should not render save button with temporary filter', async () => {
const newLocation = LocationFixture({
query: {
[DashboardFilterKeys.GLOBAL_FILTER]: JSON.stringify({
dataset: WidgetType.SPANS,
tag: {key: 'browser.name', name: 'Browser Name', kind: FieldKind.FIELD},
value: `browser.name:[Chrome]`,
isTemporary: true,
} satisfies GlobalFilter),
},
});

renderFilterBar({location: newLocation});
await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
expect(
screen.getByRole('button', {name: /browser\.name.*Chrome/i})
).toBeInTheDocument();
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
expect(screen.queryByRole('button', {name: 'Cancel'})).not.toBeInTheDocument();
});

it('should not render save button on prebuilt dashboard', async () => {
const newLocation = LocationFixture({
query: {
[DashboardFilterKeys.GLOBAL_FILTER]: JSON.stringify({
dataset: WidgetType.SPANS,
tag: {key: 'browser.name', name: 'Browser Name', kind: FieldKind.FIELD},
value: `browser.name:[Chrome]`,
} satisfies GlobalFilter),
},
});
renderFilterBar({
location: newLocation,
prebuiltDashboardId: PrebuiltDashboardId.FRONTEND_SESSION_HEALTH,
});
await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
expect(
screen.getByRole('button', {name: /browser\.name.*Chrome/i})
).toBeInTheDocument();
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
expect(screen.queryByRole('button', {name: 'Cancel'})).not.toBeInTheDocument();
});
});

const mockNetworkRequests = () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [ReleaseFixture()],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/',
body: TagsFixture(),
});
MockApiClient.addMockResponse({
url: `/organizations/org-slug/measurements-meta/`,
body: {
'measurements.custom.measurement': {
functions: ['p99'],
},
'measurements.another.custom.measurement': {
functions: ['p99'],
},
},
});

const mockSearchResponse = [
{
key: 'browser.name',
value: 'Chrome',
name: 'Chrome',
first_seen: null,
last_seen: null,
times_seen: null,
},
{
key: 'browser.name',
value: 'Firefox',
name: 'Firefox',
first_seen: null,
last_seen: null,
times_seen: null,
},
];

MockApiClient.addMockResponse({
url: `/organizations/org-slug/trace-items/attributes/browser.name/values/`,
body: mockSearchResponse,
match: [MockApiClient.matchQuery({attributeType: 'string'})],
});
};
Loading
Loading