mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[tags] add performance journey to track CRUD operations on listing page (#164537)
## Summary This PR adds single user performance journey to track CRUD operations on Tags listing page: - get all tags on initial loading - create a new tag - update the tag - delete the tag - bulk delete (first 20 tags) flaky-test-runner 25x: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/2966 Added visualisations to monitor metrics, most operations take <1 second <img width="523" alt="image" src="f4c14e49
-edf6-4fff-9f31-30b8a67970e9"> Since bulk delete takes ~20 sec, I put it on the separate visualisation <img width="523" alt="Screenshot 2023-08-23 at 18 19 46" src="467983f8
-f8eb-486a-8e27-beac0d9b1f37">dd0473ac
-826f-5621-9a10-25319700326e?_g=h@3b0c329 To run locally: `node scripts/functional_tests.js --config x-pack/performance/journeys/tags_listing_page.ts` Note: this journey is compatible to be executed on Serverless project --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4180a1a105
commit
4552c6e3b7
10 changed files with 5997 additions and 15 deletions
|
@ -419,6 +419,7 @@ enabled:
|
|||
- x-pack/performance/journeys/ecommerce_dashboard_saved_search_only.ts
|
||||
- x-pack/performance/journeys/ecommerce_dashboard_tsvb_gauge_only.ts
|
||||
- x-pack/performance/journeys/dashboard_listing_page.ts
|
||||
- x-pack/performance/journeys/tags_listing_page.ts
|
||||
- x-pack/performance/journeys/cloud_security_dashboard.ts
|
||||
- x-pack/performance/journeys/apm_service_inventory.ts
|
||||
- x-pack/test/custom_branding/config.ts
|
||||
|
|
|
@ -72,4 +72,18 @@ export class KibanaPage {
|
|||
checkAttribute: 'data-ech-render-complete',
|
||||
});
|
||||
}
|
||||
|
||||
async clearInput(locator: string) {
|
||||
const textArea = this.page.locator(locator);
|
||||
await textArea.clear();
|
||||
}
|
||||
|
||||
async clickAndWaitFor(
|
||||
locator: string,
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden' | undefined
|
||||
) {
|
||||
const element = this.page.locator(locator);
|
||||
await element.click();
|
||||
await element.waitFor({ state });
|
||||
}
|
||||
}
|
||||
|
|
64
x-pack/performance/journeys/tags_listing_page.ts
Normal file
64
x-pack/performance/journeys/tags_listing_page.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Journey } from '@kbn/journeys';
|
||||
import { subj } from '@kbn/test-subj-selector';
|
||||
|
||||
const TAG_NAME = 'testing';
|
||||
const TAG_DESCRIPTION = 'test description';
|
||||
|
||||
export const journey = new Journey({
|
||||
esArchives: ['x-pack/performance/es_archives/sample_data_flights'],
|
||||
kbnArchives: ['x-pack/performance/kbn_archives/many_tags_and_visualizations'],
|
||||
})
|
||||
.step('Go to Tags Page', async ({ page, kbnUrl }) => {
|
||||
await page.goto(kbnUrl.get(`/app/management/kibana/tags`));
|
||||
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
|
||||
})
|
||||
.step('Delete the first 20 tags', async ({ page }) => {
|
||||
await page.click(subj('checkboxSelectAll'));
|
||||
await page.click(subj('actionBar-contextMenuButton'));
|
||||
await page.click(subj('actionBar-button-delete'));
|
||||
await page.click(subj('confirmModalConfirmButton'));
|
||||
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
|
||||
})
|
||||
.step(`Search for 'stream' tag`, async ({ page, inputDelays }) => {
|
||||
await page.type(subj('tagsManagementSearchBar'), 'stream', {
|
||||
delay: inputDelays.TYPING,
|
||||
});
|
||||
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
|
||||
})
|
||||
.step('Create a new tag', async ({ page, inputDelays, kibanaPage }) => {
|
||||
await kibanaPage.clearInput(subj('tagsManagementSearchBar'));
|
||||
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
|
||||
await page.click(subj('createTagButton'));
|
||||
await page.type(subj('createModalField-name'), TAG_NAME, { delay: inputDelays.TYPING });
|
||||
await kibanaPage.clickAndWaitFor(subj('createModalConfirmButton'), 'detached');
|
||||
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
|
||||
// search for newly created tag
|
||||
await page.type(subj('tagsManagementSearchBar'), TAG_NAME, {
|
||||
delay: inputDelays.TYPING,
|
||||
});
|
||||
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
|
||||
await page.waitForSelector(subj('tagsTableRowName'), { state: 'visible' });
|
||||
})
|
||||
.step('Update tag', async ({ page, inputDelays, kibanaPage }) => {
|
||||
await page.click(subj('tagsTableAction-edit'));
|
||||
await page.type(subj('createModalField-description'), TAG_DESCRIPTION, {
|
||||
delay: inputDelays.TYPING,
|
||||
});
|
||||
await kibanaPage.clickAndWaitFor(subj('createModalConfirmButton'), 'detached');
|
||||
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
|
||||
})
|
||||
.step('Delete tag', async ({ page }) => {
|
||||
const tagRow = page.locator(subj('tagsTableRowName'));
|
||||
await page.click(subj('euiCollapsedItemActionsButton'));
|
||||
await page.click(subj('tagsTableAction-delete'));
|
||||
await page.click(subj('confirmModalConfirmButton'));
|
||||
await page.waitForSelector(subj('tagsManagementTable table-is-ready'));
|
||||
await tagRow.waitFor({ state: 'detached' });
|
||||
});
|
5848
x-pack/performance/kbn_archives/many_tags_and_visualizations.json
Normal file
5848
x-pack/performance/kbn_archives/many_tags_and_visualizations.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -139,9 +139,11 @@ export const TagTable: FC<TagTableProps> = ({
|
|||
: []),
|
||||
];
|
||||
|
||||
const testSubjectState = !loading ? 'table-is-ready' : 'table-is-loading';
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
data-test-subj="tagsManagementTable"
|
||||
data-test-subj={`tagsManagementTable ${testSubjectState}`}
|
||||
ref={tableRef}
|
||||
childrenBetween={actionBar}
|
||||
loading={loading}
|
||||
|
|
|
@ -67,12 +67,12 @@ export class SavedObjectTaggingPlugin
|
|||
return {};
|
||||
}
|
||||
|
||||
public start({ http, application, overlays, theme }: CoreStart) {
|
||||
public start({ http, application, overlays, theme, analytics }: CoreStart) {
|
||||
this.tagCache = new TagsCache({
|
||||
refreshHandler: () => this.tagClient!.getAll({ asSystemRequest: true }),
|
||||
refreshInterval: this.config.cacheRefreshInterval,
|
||||
});
|
||||
this.tagClient = new TagsClient({ http, changeListener: this.tagCache });
|
||||
this.tagClient = new TagsClient({ analytics, http, changeListener: this.tagCache });
|
||||
this.assignmentService = new TagAssignmentService({ http });
|
||||
|
||||
// do not fetch tags on anonymous page
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Tag } from '../../../common/types';
|
|||
import { createTag, createTagAttributes } from '../../../common/test_utils';
|
||||
import { tagsCacheMock } from './tags_cache.mock';
|
||||
import { TagsClient, FindTagsOptions } from './tags_client';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
||||
describe('TagsClient', () => {
|
||||
let tagsClient: TagsClient;
|
||||
|
@ -19,7 +20,9 @@ describe('TagsClient', () => {
|
|||
beforeEach(() => {
|
||||
http = httpServiceMock.createSetupContract();
|
||||
changeListener = tagsCacheMock.create();
|
||||
const { analytics } = coreMock.createStart();
|
||||
tagsClient = new TagsClient({
|
||||
analytics,
|
||||
http,
|
||||
changeListener,
|
||||
});
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { HttpSetup, AnalyticsServiceStart } from '@kbn/core/public';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import {
|
||||
Tag,
|
||||
TagAttributes,
|
||||
|
@ -15,7 +16,15 @@ import {
|
|||
} from '../../../common/types';
|
||||
import { ITagsChangeListener } from './tags_cache';
|
||||
|
||||
const BULK_DELETE_TAG_EVENT = 'bulkDeleteTag';
|
||||
const CREATE_TAG_EVENT = 'createTag';
|
||||
const DELETE_TAG_EVENT = 'deleteTag';
|
||||
const GET_ALL_TAGS_EVENT = 'getAllTag';
|
||||
const FIND_TAG_EVENT = 'findTag';
|
||||
const UPDATE_TAG_EVENT = 'updateTag';
|
||||
|
||||
export interface TagsClientOptions {
|
||||
analytics: AnalyticsServiceStart;
|
||||
http: HttpSetup;
|
||||
changeListener?: ITagsChangeListener;
|
||||
}
|
||||
|
@ -45,10 +54,12 @@ export interface ITagInternalClient extends ITagsClient {
|
|||
}
|
||||
|
||||
export class TagsClient implements ITagInternalClient {
|
||||
private readonly analytics: AnalyticsServiceStart;
|
||||
private readonly http: HttpSetup;
|
||||
private readonly changeListener?: ITagsChangeListener;
|
||||
|
||||
constructor({ http, changeListener }: TagsClientOptions) {
|
||||
constructor({ analytics, http, changeListener }: TagsClientOptions) {
|
||||
this.analytics = analytics;
|
||||
this.http = http;
|
||||
this.changeListener = changeListener;
|
||||
}
|
||||
|
@ -56,9 +67,15 @@ export class TagsClient implements ITagInternalClient {
|
|||
// public APIs from ITagsClient
|
||||
|
||||
public async create(attributes: TagAttributes) {
|
||||
const startTime = window.performance.now();
|
||||
const { tag } = await this.http.post<{ tag: Tag }>('/api/saved_objects_tagging/tags/create', {
|
||||
body: JSON.stringify(attributes),
|
||||
});
|
||||
const duration = window.performance.now() - startTime;
|
||||
reportPerformanceMetricEvent(this.analytics, {
|
||||
eventName: CREATE_TAG_EVENT,
|
||||
duration,
|
||||
});
|
||||
|
||||
trapErrors(() => {
|
||||
if (this.changeListener) {
|
||||
|
@ -70,9 +87,15 @@ export class TagsClient implements ITagInternalClient {
|
|||
}
|
||||
|
||||
public async update(id: string, attributes: TagAttributes) {
|
||||
const startTime = window.performance.now();
|
||||
const { tag } = await this.http.post<{ tag: Tag }>(`/api/saved_objects_tagging/tags/${id}`, {
|
||||
body: JSON.stringify(attributes),
|
||||
});
|
||||
const duration = window.performance.now() - startTime;
|
||||
reportPerformanceMetricEvent(this.analytics, {
|
||||
eventName: UPDATE_TAG_EVENT,
|
||||
duration,
|
||||
});
|
||||
|
||||
trapErrors(() => {
|
||||
if (this.changeListener) {
|
||||
|
@ -90,11 +113,17 @@ export class TagsClient implements ITagInternalClient {
|
|||
}
|
||||
|
||||
public async getAll({ asSystemRequest }: GetAllTagsOptions = {}) {
|
||||
const startTime = window.performance.now();
|
||||
const fetchOptions = { asSystemRequest };
|
||||
const { tags } = await this.http.get<{ tags: Tag[] }>(
|
||||
'/api/saved_objects_tagging/tags',
|
||||
fetchOptions
|
||||
);
|
||||
const duration = window.performance.now() - startTime;
|
||||
reportPerformanceMetricEvent(this.analytics, {
|
||||
eventName: GET_ALL_TAGS_EVENT,
|
||||
duration,
|
||||
});
|
||||
|
||||
trapErrors(() => {
|
||||
if (this.changeListener) {
|
||||
|
@ -106,7 +135,13 @@ export class TagsClient implements ITagInternalClient {
|
|||
}
|
||||
|
||||
public async delete(id: string) {
|
||||
const startTime = window.performance.now();
|
||||
await this.http.delete<{}>(`/api/saved_objects_tagging/tags/${id}`);
|
||||
const duration = window.performance.now() - startTime;
|
||||
reportPerformanceMetricEvent(this.analytics, {
|
||||
eventName: DELETE_TAG_EVENT,
|
||||
duration,
|
||||
});
|
||||
|
||||
trapErrors(() => {
|
||||
if (this.changeListener) {
|
||||
|
@ -118,21 +153,38 @@ export class TagsClient implements ITagInternalClient {
|
|||
// internal APIs from ITagInternalClient
|
||||
|
||||
public async find({ page, perPage, search }: FindTagsOptions) {
|
||||
return await this.http.get<FindTagsResponse>('/internal/saved_objects_tagging/tags/_find', {
|
||||
query: {
|
||||
page,
|
||||
perPage,
|
||||
search,
|
||||
},
|
||||
const startTime = window.performance.now();
|
||||
const response = await this.http.get<FindTagsResponse>(
|
||||
'/internal/saved_objects_tagging/tags/_find',
|
||||
{
|
||||
query: {
|
||||
page,
|
||||
perPage,
|
||||
search,
|
||||
},
|
||||
}
|
||||
);
|
||||
const duration = window.performance.now() - startTime;
|
||||
reportPerformanceMetricEvent(this.analytics, {
|
||||
eventName: FIND_TAG_EVENT,
|
||||
duration,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async bulkDelete(tagIds: string[]) {
|
||||
const startTime = window.performance.now();
|
||||
await this.http.post<{}>('/internal/saved_objects_tagging/tags/_bulk_delete', {
|
||||
body: JSON.stringify({
|
||||
ids: tagIds,
|
||||
}),
|
||||
});
|
||||
const duration = window.performance.now() - startTime;
|
||||
reportPerformanceMetricEvent(this.analytics, {
|
||||
eventName: BULK_DELETE_TAG_EVENT,
|
||||
duration,
|
||||
});
|
||||
|
||||
trapErrors(() => {
|
||||
if (this.changeListener) {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"@kbn/utility-types",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/ebt-tools",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -270,10 +270,7 @@ export class TagManagementPageObject extends FtrService {
|
|||
*/
|
||||
async waitUntilTableIsLoaded() {
|
||||
return this.retry.try(async () => {
|
||||
const isLoaded = await this.find.existsByDisplayedByCssSelector(
|
||||
'*[data-test-subj="tagsManagementTable"]:not(.euiBasicTable-loading)'
|
||||
);
|
||||
|
||||
const isLoaded = await this.testSubjects.exists('tagsManagementTable table-is-ready');
|
||||
if (isLoaded) {
|
||||
return true;
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue