Compare commits

..

1 Commits

6 changed files with 222 additions and 9 deletions

View File

@@ -7394,11 +7394,9 @@ components:
op:
$ref: '#/components/schemas/RuletypesCompareOperator'
recoveryTarget:
format: double
nullable: true
type: number
target:
format: double
nullable: true
type: number
targetUnit:
@@ -7682,7 +7680,6 @@ components:
selectedQueryName:
type: string
target:
format: double
nullable: true
type: number
targetUnit:

View File

@@ -8480,12 +8480,10 @@ export interface RuletypesBasicRuleThresholdDTO {
op: RuletypesCompareOperatorDTO;
/**
* @type number,null
* @format double
*/
recoveryTarget?: number | null;
/**
* @type number,null
* @format double
*/
target: number | null;
/**
@@ -8679,7 +8677,6 @@ export interface RuletypesRuleConditionDTO {
selectedQueryName?: string;
/**
* @type number,null
* @format double
*/
target?: number | null;
/**

View File

@@ -112,7 +112,7 @@ type AlertCompositeQuery struct {
type RuleCondition struct {
CompositeQuery *AlertCompositeQuery `json:"compositeQuery" required:"true"`
CompareOperator CompareOperator `json:"op,omitzero"`
Target *float64 `json:"target,omitempty" format:"double"`
Target *float64 `json:"target,omitempty"`
AlertOnAbsent bool `json:"alertOnAbsent,omitempty"`
AbsentFor uint64 `json:"absentFor,omitempty"`
MatchType MatchType `json:"matchType,omitzero"`
@@ -187,3 +187,4 @@ func (rc *RuleCondition) String() string {
data, _ := json.Marshal(*rc)
return string(data)
}

View File

@@ -134,9 +134,9 @@ type RuleThreshold interface {
type BasicRuleThreshold struct {
Name string `json:"name" required:"true"`
TargetValue *float64 `json:"target" required:"true" format:"double"`
TargetValue *float64 `json:"target" required:"true"`
TargetUnit string `json:"targetUnit"`
RecoveryTarget *float64 `json:"recoveryTarget" format:"double"`
RecoveryTarget *float64 `json:"recoveryTarget"`
MatchType MatchType `json:"matchType" required:"true"`
CompareOperator CompareOperator `json:"op" required:"true"`
Channels []string `json:"channels"`

View File

@@ -0,0 +1,179 @@
import { expect, type Locator, type Page } from "@playwright/test";
import { authToken } from "./common";
export const LOGS_EXPLORER_PATH = "/logs/logs-explorer";
export const TRACES_EXPLORER_PATH = "/traces-explorer";
const SETTINGS_PANEL_TITLE = "Edit quick filters";
const SETTINGS_ICON_TEST_ID = "settings-icon";
const ANNOUNCEMENT_DISMISSED_KEY = "QUICK_FILTERS_SETTINGS_ANNOUNCEMENT";
/**
* Conservative lower bound for the open panel width. The panel is 342px in
* QuickFiltersSettings.styles.scss; 300 leaves slack for sub-pixel rounding.
*/
const SETTINGS_PANEL_MIN_WIDTH_PX = 300;
// Pages that already had the announcement-dismissal init script registered.
// addInitScript re-runs on every navigation, so registering once per Page keeps
// the callback from stacking up across repeated helper calls.
const announcementDismissed = new WeakSet<Page>();
export type QuickFilterSignal = "logs" | "traces";
export interface QuickFilterDefinition {
key: string;
dataType: string;
type: string;
}
export const DEFAULT_CUSTOM_FILTERS: QuickFilterDefinition[] = [
{
key: "service.name",
dataType: "string",
type: "resource",
},
{
key: "deployment.environment",
dataType: "string",
type: "resource",
},
];
function settingsPanel(page: Page): Locator {
return page
.locator(".quick-filters-settings")
.filter({ hasText: SETTINGS_PANEL_TITLE });
}
function settingsGearContainer(page: Page): Locator {
return page.getByTestId(SETTINGS_ICON_TEST_ID).locator("..");
}
/** Minimal filter set so `isDynamicFilters` is true and the settings gear renders. */
export async function seedCustomFiltersViaApi(
page: Page,
signal: QuickFilterSignal,
filters: QuickFilterDefinition[] = DEFAULT_CUSTOM_FILTERS,
): Promise<void> {
const token = await authToken(page);
const res = await page.request.put("/api/v1/orgs/me/filters", {
data: { signal, filters },
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok()) {
throw new Error(
`PUT /api/v1/orgs/me/filters ${res.status()}: ${await res.text()}`,
);
}
}
/**
* Hide the one-off announcement tooltip so it cannot intercept the gear click.
* Registered via addInitScript (not page.evaluate) so it runs in the app's own
* origin on every navigation — evaluate() before goto() would set localStorage
* on the pre-navigation about:blank origin, which the app never reads. Must be
* called before the first goto(). Guarded to once per Page to avoid stacking.
*/
export async function dismissQuickFiltersAnnouncement(
page: Page,
): Promise<void> {
if (announcementDismissed.has(page)) {
return;
}
await page.addInitScript((key) => {
// Runs in the browser; Node's tsc has no DOM globals on this callback.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).localStorage.setItem(key, "false");
}, ANNOUNCEMENT_DISMISSED_KEY);
announcementDismissed.add(page);
}
export async function gotoExplorerWithQuickFilters(
page: Page,
path: string,
): Promise<void> {
await dismissQuickFiltersAnnouncement(page);
await page.goto(path);
await page.getByTestId(SETTINGS_ICON_TEST_ID).waitFor({ state: "visible" });
}
/** Settings drawer is closed — content unmounted and gear not active. */
export async function assertQuickFiltersSettingsDrawerClosed(
page: Page,
): Promise<void> {
await expect(page.getByText(SETTINGS_PANEL_TITLE)).toBeHidden();
await expect(settingsGearContainer(page)).not.toHaveClass(/active/);
// Collapsed panel keeps the `hidden` class (width: 0). Assert the class, not
// toBeVisible() — a zero-width element is never "visible" to Playwright.
await expect(page.locator(".quick-filters-settings")).toHaveClass(/hidden/);
}
/**
* Click the settings gear. Pair with `assertQuickFiltersSettingsDrawerOpen` for
* visibility/layout checks that guard overflow clipping regressions.
*/
export async function openQuickFiltersSettingsPanel(page: Page): Promise<void> {
await assertQuickFiltersSettingsDrawerClosed(page);
await page.getByTestId(SETTINGS_ICON_TEST_ID).click();
}
/**
* Assert the customization drawer is open, painted on screen, and populated with
* the seeded filters — not merely present in the DOM.
*/
export async function assertQuickFiltersSettingsDrawerOpen(
page: Page,
seededFilters: QuickFilterDefinition[] = DEFAULT_CUSTOM_FILTERS,
): Promise<void> {
const panel = settingsPanel(page);
const filtersColumn = page.locator(".quick-filters");
await expect(settingsGearContainer(page)).toHaveClass(/active/);
await expect(panel).toBeVisible();
await expect(panel).not.toHaveClass(/hidden/);
await expect(page.getByText(SETTINGS_PANEL_TITLE)).toBeVisible();
await expect(page.getByPlaceholder("Search for a filter...")).toBeVisible();
await expect(panel.locator(".qf-header-icon")).toBeVisible();
const addedList = panel.locator(".qf-added-filters-list");
await expect(panel.getByText("ADDED FILTERS")).toBeVisible();
for (const filter of seededFilters) {
await expect(
addedList.getByText(filter.key, { exact: true }),
).toBeVisible();
}
// Footer only mounts after edits — absent on first open.
await expect(panel.getByRole("button", { name: "Discard" })).toBeHidden();
await expect(
panel.getByRole("button", { name: "Save changes" }),
).toBeHidden();
// OTHER FILTERS loads asynchronously from attribute/aggregate suggestions.
await expect(
panel.locator(".qf-other-filters-list .ant-skeleton-input"),
).toHaveCount(0);
await expect(panel.getByText("OTHER FILTERS")).toBeVisible();
await expect(
panel.locator(".qf-other-filters-list .other-filters-item").first(),
).toBeVisible();
const [panelBox, columnBox] = await Promise.all([
panel.boundingBox(),
filtersColumn.boundingBox(),
]);
expect(panelBox?.width ?? 0).toBeGreaterThanOrEqual(
SETTINGS_PANEL_MIN_WIDTH_PX,
);
expect(columnBox).not.toBeNull();
expect(panelBox!.x).toBeGreaterThanOrEqual(columnBox!.x);
}
/** Close via the drawer header X and confirm state resets. */
export async function closeQuickFiltersSettingsPanel(
page: Page,
): Promise<void> {
await settingsPanel(page).locator(".qf-header-icon").click();
await assertQuickFiltersSettingsDrawerClosed(page);
}

View File

@@ -0,0 +1,39 @@
import { test, expect } from "../../fixtures/auth";
import {
assertQuickFiltersSettingsDrawerOpen,
closeQuickFiltersSettingsPanel,
DEFAULT_CUSTOM_FILTERS,
gotoExplorerWithQuickFilters,
LOGS_EXPLORER_PATH,
openQuickFiltersSettingsPanel,
seedCustomFiltersViaApi,
TRACES_EXPLORER_PATH,
} from "../../helpers/quick-filters";
// smoke tests for settings drawer in quick filters
test.describe("Quick Filters — settings drawer", () => {
test.beforeEach(async ({ authedPage: page }) => {
await seedCustomFiltersViaApi(page, "logs", DEFAULT_CUSTOM_FILTERS);
await seedCustomFiltersViaApi(page, "traces", DEFAULT_CUSTOM_FILTERS);
});
test("TC-01 logs explorer — settings gear opens a visible, populated drawer", async ({
authedPage: page,
}) => {
await gotoExplorerWithQuickFilters(page, LOGS_EXPLORER_PATH);
await openQuickFiltersSettingsPanel(page);
await assertQuickFiltersSettingsDrawerOpen(page, DEFAULT_CUSTOM_FILTERS);
await closeQuickFiltersSettingsPanel(page);
await expect(page.locator(".quick-filters")).toBeVisible();
});
test("TC-02 traces explorer — settings gear opens a visible, populated drawer", async ({
authedPage: page,
}) => {
await gotoExplorerWithQuickFilters(page, TRACES_EXPLORER_PATH);
await openQuickFiltersSettingsPanel(page);
await assertQuickFiltersSettingsDrawerOpen(page, DEFAULT_CUSTOM_FILTERS);
await closeQuickFiltersSettingsPanel(page);
await expect(page.locator(".quick-filters")).toBeVisible();
});
});