From 2817c29e632020f968f9f50e745102a4edf1d2f1 Mon Sep 17 00:00:00 2001 From: Marc Mignonsin Date: Fri, 12 Jul 2024 18:43:31 +0200 Subject: [PATCH 1/3] feat(*): Various minor improvements --- e2e/config/constants.ts | 9 +++ e2e/fixtures/index.ts | 10 ++- e2e/fixtures/pages/ExploreProfilesPage.ts | 20 +++++ .../explore-profiles/explore-profiles.spec.ts | 23 ++++++ .../SceneProfilesExplorer.tsx | 80 +++++++------------ .../actions/FavAction.tsx | 38 +++++---- .../actions/SelectAction.tsx | 56 ++++++++++--- .../SceneByVariableRepeaterGrid.tsx | 43 ++++++++-- .../components/SceneMainServiceTimeseries.tsx | 14 +++- .../interpolateQueryRunnerVariables.ts | 5 +- .../SceneExploreFavorites.tsx | 5 +- .../SceneGroupByLabels.tsx | 17 ++-- .../SceneExploreServiceProfileTypes.tsx} | 8 +- .../SceneFlameGraph.tsx | 36 +++------ .../helpers/buildtimeSeriesPanelTitle.ts | 10 --- .../helpers/getSceneVariableValue.ts | 8 +- 16 files changed, 242 insertions(+), 140 deletions(-) create mode 100644 e2e/fixtures/pages/ExploreProfilesPage.ts create mode 100644 e2e/tests/explore-profiles/explore-profiles.spec.ts rename src/pages/ProfilesExplorerView/exploration-types/{SceneExploreSingleService/SceneExploreSingleService.tsx => SceneExploreServiceProfileTypes/SceneExploreServiceProfileTypes.tsx} (77%) delete mode 100644 src/pages/ProfilesExplorerView/helpers/buildtimeSeriesPanelTitle.ts diff --git a/e2e/config/constants.ts b/e2e/config/constants.ts index e681c61a..ddd72f2f 100644 --- a/e2e/config/constants.ts +++ b/e2e/config/constants.ts @@ -32,4 +32,13 @@ export const DEFAULT_URL_PARAMS = ENV_VARS.E2E_BASE_URL.startsWith('http://local }) : new URLSearchParams(); +export const DEFAULT_EXPLORE_PROFILES_URL_PARAMS = ENV_VARS.E2E_BASE_URL.startsWith('http://localhost') + ? new URLSearchParams({ + // We use static data in local and PR build (where the host is http://localhost): + from: '2024-03-13T18:00:00.000Z', + to: '2024-03-13T18:50:00.000Z', // + maxNodes: '16384', + }) + : new URLSearchParams(); + export const AUTH_FILE = path.join(process.cwd(), 'e2e', 'auth', 'user.json'); diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts index 2db0fb7c..a60b09e8 100644 --- a/e2e/fixtures/index.ts +++ b/e2e/fixtures/index.ts @@ -1,14 +1,16 @@ import { test as base, expect } from '@playwright/test'; -import { DEFAULT_URL_PARAMS } from '../config/constants'; +import { DEFAULT_EXPLORE_PROFILES_URL_PARAMS, DEFAULT_URL_PARAMS } from '../config/constants'; import { Toolbar } from './components/Toolbar'; import { AdHocViewPage } from './pages/AdHocViewPage'; import { ComparisonDiffViewPage } from './pages/ComparisonDiffViewPage'; import { ComparisonViewPage } from './pages/ComparisonViewPage'; +import { ExploreProfilesPage } from './pages/ExploreProfilesPage'; import { SettingsPage } from './pages/SettingsPage'; type Fixtures = { toolbar: Toolbar; + exploreProfilesPage: ExploreProfilesPage; comparisonViewPage: ComparisonViewPage; comparisonDiffViewPage: ComparisonDiffViewPage; adHocViewPage: AdHocViewPage; @@ -41,6 +43,12 @@ export const test = base.extend({ toolbar: async ({ page }, use) => { await use(new Toolbar(page)); }, + exploreProfilesPage: async ({ page, failOnUncaughtExceptions }, use) => { + await withExceptionsAssertion( + { page, failOnUncaughtExceptions, use }, + new ExploreProfilesPage(page, DEFAULT_EXPLORE_PROFILES_URL_PARAMS) + ); + }, comparisonViewPage: async ({ page, failOnUncaughtExceptions }, use) => { await withExceptionsAssertion( { page, failOnUncaughtExceptions, use }, diff --git a/e2e/fixtures/pages/ExploreProfilesPage.ts b/e2e/fixtures/pages/ExploreProfilesPage.ts new file mode 100644 index 00000000..4e5bda73 --- /dev/null +++ b/e2e/fixtures/pages/ExploreProfilesPage.ts @@ -0,0 +1,20 @@ +import { type Page } from '@playwright/test'; + +import { PyroscopePage } from './PyroscopePage'; + +export class ExploreProfilesPage extends PyroscopePage { + constructor(readonly page: Page, defaultUrlParams: URLSearchParams) { + const urlParams = new URLSearchParams(defaultUrlParams); + + super(page, '/a/grafana-pyroscope-app/profiles-explorer', urlParams.toString()); + } + + getExplorationTypeSelector() { + return this.page.getByTestId('exploration-types'); + } + + async getSelectedExplorationType() { + const label = await this.getExplorationTypeSelector().locator('input[checked] + label').textContent(); + return label?.trim(); + } +} diff --git a/e2e/tests/explore-profiles/explore-profiles.spec.ts b/e2e/tests/explore-profiles/explore-profiles.spec.ts new file mode 100644 index 00000000..af719221 --- /dev/null +++ b/e2e/tests/explore-profiles/explore-profiles.spec.ts @@ -0,0 +1,23 @@ +import { DEFAULT_EXPLORE_PROFILES_URL_PARAMS } from '../../config/constants'; +import { expect, test } from '../../fixtures'; + +test.describe('Explore Profiles', () => { + test.describe('Smoke tests', () => { + for (const { type, label } of [ + { type: 'all', label: 'All services' }, + { type: 'profiles', label: 'Profile types' }, + { type: 'labels', label: 'Labels' }, + { type: 'flame-graph', label: 'Flame graph' }, + { type: 'favorites', label: 'Favorites' }, + ]) { + test(label, async ({ exploreProfilesPage }) => { + const urlParams = new URLSearchParams(DEFAULT_EXPLORE_PROFILES_URL_PARAMS); + + urlParams.set('explorationType', type); + await exploreProfilesPage.goto(urlParams.toString()); + + expect(await exploreProfilesPage.getSelectedExplorationType()).toBe(label); + }); + } + }); +}); diff --git a/src/pages/ProfilesExplorerView/SceneProfilesExplorer.tsx b/src/pages/ProfilesExplorerView/SceneProfilesExplorer.tsx index bca6e3d3..9dffd415 100644 --- a/src/pages/ProfilesExplorerView/SceneProfilesExplorer.tsx +++ b/src/pages/ProfilesExplorerView/SceneProfilesExplorer.tsx @@ -36,7 +36,7 @@ import { EventViewServiceProfiles } from './events/EventViewServiceProfiles'; import { SceneExploreAllServices } from './exploration-types/SceneExploreAllServices/SceneExploreAllServices'; import { SceneExploreFavorites } from './exploration-types/SceneExploreFavorites/SceneExploreFavorites'; import { SceneExploreServiceLabels } from './exploration-types/SceneExploreServiceLabels/SceneExploreServiceLabels'; -import { SceneExploreSingleService } from './exploration-types/SceneExploreSingleService/SceneExploreSingleService'; +import { SceneExploreServiceProfileTypes } from './exploration-types/SceneExploreServiceProfileTypes/SceneExploreServiceProfileTypes'; import { SceneServiceFlameGraph } from './exploration-types/SceneServiceFlameGraph/SceneServiceFlameGraph'; import { findSceneObjectByClass } from './helpers/findSceneObjectByClass'; import { FiltersVariable } from './variables/FiltersVariable/FiltersVariable'; @@ -53,9 +53,9 @@ export interface SceneProfilesExplorerState extends Partial export enum ExplorationType { ALL_SERVICES = 'all', - SINGLE_SERVICE = 'single', - SINGLE_SERVICE_LABELS = 'labels', - SINGLE_SERVICE_FLAME_GRAPH = 'flame-graph', + PROFILE_TYPES = 'profiles', + LABELS = 'labels', + FLAME_GRAPH = 'flame-graph', FAVORITES = 'favorites', } @@ -64,27 +64,27 @@ export class SceneProfilesExplorer extends SceneObjectBase { (findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter)?.clear(); - this.setExplorationType(ExplorationType.SINGLE_SERVICE, event.payload.item); + this.setExplorationType(ExplorationType.PROFILE_TYPES, event.payload.item); }); const labelsSub = this.subscribeToEvent(EventViewServiceLabels, (event) => { (findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter)?.clear(); - this.setExplorationType(ExplorationType.SINGLE_SERVICE_LABELS, event.payload.item); + this.setExplorationType(ExplorationType.LABELS, event.payload.item); }); const flameGraphSub = this.subscribeToEvent(EventViewServiceFlameGraph, (event) => { (findSceneObjectByClass(this, SceneQuickFilter) as SceneQuickFilter)?.clear(); - this.setExplorationType(ExplorationType.SINGLE_SERVICE_FLAME_GRAPH, event.payload.item); + this.setExplorationType(ExplorationType.FLAME_GRAPH, event.payload.item); }); return { @@ -216,17 +216,17 @@ export class SceneProfilesExplorer extends SceneObjectBase
- +
{dataSourceVariable.state.label}
-
- -
Types of exploration
-
-
All services
-
Overview of all services, for any given profile metric
-
Single service
-
Overview of all the profile metrics for a single service
-
Labels
-
Single service label exploration and filtering
-
Flame graph
-
Single service flame graph
-
Favorites
-
Overview of favorited visualizations
-
-
- } - > - Exploration type - - +
+ Exploration type { return FavoritesDataSource.exists(this.buildFavorite()); } - buildFavorite(): Favorite { - const { item, skipVariablesInterpolation } = this.state; - - const queryRunnerParams = ( - skipVariablesInterpolation ? item.queryRunnerParams : interpolateQueryRunnerVariables(this, item) - ) as Favorite['queryRunnerParams']; + static buildFavorite(item: GridItemData): Favorite { + const { index, queryRunnerParams } = item; + + const favorite: Favorite = { + index, + queryRunnerParams: { + serviceName: queryRunnerParams.serviceName as string, + profileMetricId: queryRunnerParams.profileMetricId as string, + }, + }; if (queryRunnerParams.groupBy) { - queryRunnerParams.groupBy = { + favorite.queryRunnerParams.groupBy = { label: queryRunnerParams.groupBy.label, // we don't store values, we'll fetch all timeseries by using the `groupBy` parameter }; - } else { - delete queryRunnerParams.groupBy; } // we don't store filters if empty - if (!queryRunnerParams.filters?.length) { - delete queryRunnerParams.filters; + if (queryRunnerParams.filters?.length) { + favorite.queryRunnerParams.filters = queryRunnerParams.filters; } - return { + return favorite; + } + + buildFavorite(): Favorite { + const { item, skipVariablesInterpolation } = this.state; + + return FavAction.buildFavorite({ index: item.index, - queryRunnerParams, - }; + queryRunnerParams: skipVariablesInterpolation + ? item.queryRunnerParams + : interpolateQueryRunnerVariables(this, item), + } as GridItemData); } public onClick = () => { diff --git a/src/pages/ProfilesExplorerView/actions/SelectAction.tsx b/src/pages/ProfilesExplorerView/actions/SelectAction.tsx index 82ab2f2e..0d483e5a 100644 --- a/src/pages/ProfilesExplorerView/actions/SelectAction.tsx +++ b/src/pages/ProfilesExplorerView/actions/SelectAction.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/css'; -import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; +import { SceneComponentProps, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { Button, IconName, useStyles2 } from '@grafana/ui'; +import { getProfileMetric, ProfileMetricId } from '@shared/infrastructure/profile-metrics/getProfileMetric'; import { merge } from 'lodash'; import React from 'react'; @@ -16,6 +17,7 @@ import { import { EventViewServiceFlameGraph, EventViewServiceFlameGraphPayload } from '../events/EventViewServiceFlameGraph'; import { EventViewServiceLabels, EventViewServiceLabelsPayload } from '../events/EventViewServiceLabels'; import { EventViewServiceProfiles, EventViewServiceProfilesPayload } from '../events/EventViewServiceProfiles'; +import { getSceneVariableValue } from '../helpers/getSceneVariableValue'; type EventContructor = | (new (payload: EventAddLabelToFiltersPayload) => EventAddLabelToFilters) @@ -26,13 +28,19 @@ type EventContructor = | (new (payload: EventViewServiceLabelsPayload) => EventViewServiceLabels) | (new (payload: EventViewServiceProfilesPayload) => EventViewServiceProfiles); -const Events = new Map([ +type EventLookup = { + label?: string; + icon?: IconName; + tooltip?: (item: GridItemData, model: SceneObject) => string; +}; + +const Events = new Map([ [EventAddLabelToFilters, Object.freeze({ label: 'Add to filters' })], [ EventExpandPanel, Object.freeze({ icon: 'expand-arrows', - tooltip: 'Expand this panel to view all the timeseries for the current filters', + tooltip: () => 'Expand this panel to view all the timeseries for the current filters', }), ], [EventSelectLabel, Object.freeze({ label: 'Select' })], @@ -40,12 +48,40 @@ const Events = new Map `View the distribution of all the "${item.label}" values for the current filters`, + }), + ], + [ + EventViewServiceFlameGraph, + Object.freeze({ + label: 'Flame graph', + tooltip: ({ queryRunnerParams }, model) => { + const serviceName = queryRunnerParams.serviceName || getSceneVariableValue(model, 'serviceName'); + const profileMetricId = queryRunnerParams.profileMetricId || getSceneVariableValue(model, 'profileMetricId'); + return `View the "${getProfileMetric(profileMetricId as ProfileMetricId).type}" flame graph of ${serviceName}`; + }, + }), + ], + [ + EventViewServiceLabels, + Object.freeze({ + label: 'Labels', + tooltip: ({ queryRunnerParams }, model) => { + const serviceName = queryRunnerParams.serviceName || getSceneVariableValue(model, 'serviceName'); + return `Explore the labels of ${serviceName}`; + }, + }), + ], + [ + EventViewServiceProfiles, + Object.freeze({ + label: 'Profile types', + tooltip: ({ queryRunnerParams }, model) => { + const serviceName = queryRunnerParams.serviceName || getSceneVariableValue(model, 'serviceName'); + return `View the profile types of ${serviceName}`; + }, }), ], - [EventViewServiceFlameGraph, Object.freeze({ label: 'Flame graph' })], - [EventViewServiceLabels, Object.freeze({ label: 'Labels' })], - [EventViewServiceProfiles, Object.freeze({ label: 'Profiles' })], ]); interface SelectActionState extends SceneObjectState { @@ -53,7 +89,7 @@ interface SelectActionState extends SceneObjectState { item: GridItemData; label?: string; icon?: IconName; - tooltip?: string; + tooltip?: EventLookup['tooltip']; skipVariablesInterpolation?: boolean; } @@ -98,7 +134,7 @@ export class SelectAction extends SceneObjectBase { public static Component = ({ model }: SceneComponentProps) => { const styles = useStyles2(getStyles); - const { label, icon, tooltip } = model.useState(); + const { label, icon, tooltip, item } = model.useState(); return (