diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index 812fa8253a6982..4d65424a7b9bdb 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -4,26 +4,45 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import url from 'url'; import archives_metadata from '../../fixtures/es_archiver/archives_metadata'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; + const { start, end } = archives_metadata['apm_8.0.0']; +const servicesPath = '/app/apm/services'; +const baseUrl = url.format({ + pathname: servicesPath, + query: { rangeFrom: start, rangeTo: end }, +}); + describe('Home page', () => { before(() => { esArchiverLoad('apm_8.0.0'); - cy.loginAsReadOnlyUser(); }); after(() => { esArchiverUnload('apm_8.0.0'); }); + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); it('Redirects to service page with rangeFrom and rangeTo added to the URL', () => { - const baseUrl = url.format({ - pathname: '/app/apm', - query: { rangeFrom: start, rangeTo: end }, - }); + cy.visit('/app/apm'); - cy.visit(baseUrl); + cy.url().should( + 'include', + 'app/apm/services?rangeFrom=now-15m&rangeTo=now' + ); cy.get('.euiTabs .euiTab-isSelected').contains('Services'); }); + + it('includes services with only metric documents', () => { + cy.visit( + `${baseUrl}&kuery=not%2520(processor.event%2520%253A%2522transaction%2522%2520)` + ); + cy.contains('opbeans-python'); + cy.contains('opbeans-java'); + cy.contains('opbeans-node'); + }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts new file mode 100644 index 00000000000000..d253a290f4a51b --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts @@ -0,0 +1,145 @@ +/* + * 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 url from 'url'; +import archives_metadata from '../../../fixtures/es_archiver/archives_metadata'; +import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; + +const { start, end } = archives_metadata['apm_8.0.0']; + +const serviceOverviewPath = '/app/apm/services/kibana/overview'; +const baseUrl = url.format({ + pathname: serviceOverviewPath, + query: { rangeFrom: start, rangeTo: end }, +}); + +const apisToIntercept = [ + { + endpoint: '/api/apm/services/kibana/transactions/charts/latency', + as: 'latencyChartRequest', + }, + { + endpoint: '/api/apm/services/kibana/throughput', + as: 'throughputChartRequest', + }, + { + endpoint: '/api/apm/services/kibana/transactions/charts/error_rate', + as: 'errorRateChartRequest', + }, + { + endpoint: + '/api/apm/services/kibana/transactions/groups/detailed_statistics', + as: 'transactionGroupsDetailedRequest', + }, + { + endpoint: + '/api/apm/services/kibana/service_overview_instances/detailed_statistics', + as: 'instancesDetailedRequest', + }, + { + endpoint: + '/api/apm/services/kibana/service_overview_instances/main_statistics', + as: 'instancesMainStatisticsRequest', + }, + { + endpoint: '/api/apm/services/kibana/error_groups/main_statistics', + as: 'errorGroupsMainStatisticsRequest', + }, + { + endpoint: '/api/apm/services/kibana/transaction/charts/breakdown', + as: 'transactonBreakdownRequest', + }, + { + endpoint: '/api/apm/services/kibana/transactions/groups/main_statistics', + as: 'transactionsGroupsMainStatisticsRequest', + }, +]; + +describe('Service overview - header filters', () => { + before(() => { + esArchiverLoad('apm_8.0.0'); + }); + after(() => { + esArchiverUnload('apm_8.0.0'); + }); + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + describe('Filtering by transaction type', () => { + it('changes url when selecting different value', () => { + cy.visit(baseUrl); + cy.contains('Kibana'); + cy.url().should('not.include', 'transactionType'); + cy.get('[data-test-subj="headerFilterTransactionType"]').should( + 'have.value', + 'request' + ); + cy.get('[data-test-subj="headerFilterTransactionType"]').select( + 'taskManager' + ); + cy.url().should('include', 'transactionType=taskManager'); + cy.get('[data-test-subj="headerFilterTransactionType"]').should( + 'have.value', + 'taskManager' + ); + }); + + it('calls APIs with correct transaction type', () => { + apisToIntercept.map(({ endpoint, as }) => { + cy.intercept('GET', endpoint).as(as); + }); + cy.visit(baseUrl); + cy.contains('Kibana'); + cy.get('[data-test-subj="headerFilterTransactionType"]').should( + 'have.value', + 'request' + ); + + cy.expectAPIsToHaveBeenCalledWith({ + apisIntercepted: apisToIntercept.map(({ as }) => `@${as}`), + value: 'transactionType=request', + }); + + cy.get('[data-test-subj="headerFilterTransactionType"]').select( + 'taskManager' + ); + cy.url().should('include', 'transactionType=taskManager'); + cy.get('[data-test-subj="headerFilterTransactionType"]').should( + 'have.value', + 'taskManager' + ); + cy.expectAPIsToHaveBeenCalledWith({ + apisIntercepted: apisToIntercept.map(({ as }) => `@${as}`), + value: 'transactionType=taskManager', + }); + }); + }); + + describe('Filtering by kuerybar', () => { + it('filters by transaction.name', () => { + cy.visit( + url.format({ + pathname: '/app/apm/services/opbeans-java/overview', + query: { rangeFrom: start, rangeTo: end }, + }) + ); + cy.contains('opbeans-java'); + cy.get('[data-test-subj="headerFilterKuerybar"]').type('transaction.n'); + cy.contains('transaction.name'); + cy.get('[data-test-subj="suggestionContainer"]') + .find('li') + .first() + .click(); + cy.get('[data-test-subj="headerFilterKuerybar"]').type(':'); + cy.get('[data-test-subj="suggestionContainer"]') + .find('li') + .first() + .click(); + cy.get('[data-test-subj="suggestionContainer"]').realPress('{enter}'); + cy.url().should('include', '&kuery=transaction.name'); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts new file mode 100644 index 00000000000000..2d76dfe977ef75 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts @@ -0,0 +1,114 @@ +/* + * 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 url from 'url'; +import archives_metadata from '../../../fixtures/es_archiver/archives_metadata'; +import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; + +const { start, end } = archives_metadata['apm_8.0.0']; + +const serviceOverviewPath = '/app/apm/services/opbeans-java/overview'; +const baseUrl = url.format({ + pathname: serviceOverviewPath, + query: { rangeFrom: start, rangeTo: end }, +}); + +const apisToIntercept = [ + { + endpoint: + '/api/apm/services/opbeans-java/service_overview_instances/main_statistics', + as: 'instancesMainRequest', + }, + { + endpoint: + '/api/apm/services/opbeans-java/service_overview_instances/detailed_statistics', + as: 'instancesDetailsRequest', + }, + { + endpoint: + '/api/apm/services/opbeans-java/service_overview_instances/details/02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c', + as: 'instanceDetailsRequest', + }, + { + endpoint: + '/api/apm/services/opbeans-java/service_overview_instances/details/02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c', + as: 'instanceDetailsRequest', + }, +]; + +describe('Instances table', () => { + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + describe('when data is not loaded', () => { + it('shows empty message', () => { + cy.visit(baseUrl); + cy.contains('opbeans-java'); + cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains( + 'No items found' + ); + }); + }); + + describe('when data is loaded', () => { + before(() => { + esArchiverLoad('apm_8.0.0'); + }); + after(() => { + esArchiverUnload('apm_8.0.0'); + }); + const serviceNodeName = + '02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c'; + it('has data in the table', () => { + cy.visit(baseUrl); + cy.contains('opbeans-java'); + cy.contains(serviceNodeName); + }); + it('shows instance details', () => { + apisToIntercept.map(({ endpoint, as }) => { + cy.intercept('GET', endpoint).as(as); + }); + + cy.visit(baseUrl); + cy.contains('opbeans-java'); + + cy.wait('@instancesMainRequest'); + cy.contains(serviceNodeName); + + cy.wait('@instancesDetailsRequest'); + cy.get( + `[data-test-subj="instanceDetailsButton_${serviceNodeName}"]` + ).realClick(); + cy.get('[data-test-subj="loadingSpinner"]').should('be.visible'); + cy.wait('@instanceDetailsRequest').then(() => { + cy.contains('Service'); + }); + }); + it('shows actions available', () => { + apisToIntercept.map(({ endpoint, as }) => { + cy.intercept('GET', endpoint).as(as); + }); + + cy.visit(baseUrl); + cy.contains('opbeans-java'); + + cy.wait('@instancesMainRequest'); + cy.contains(serviceNodeName); + + cy.wait('@instancesDetailsRequest'); + cy.get( + `[data-test-subj="instanceActionsButton_${serviceNodeName}"]` + ).realClick(); + cy.contains('Pod logs'); + cy.contains('Pod metrics'); + cy.contains('Container logs'); + cy.contains('Container metrics'); + cy.contains('Filter overview by instance'); + cy.contains('Metrics'); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 37e498619bdd4f..31eab9507ef5e6 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -5,6 +5,7 @@ * 2.0. */ import 'cypress-real-events/support'; +import { Interception } from 'cypress/types/net-stubbing'; Cypress.Commands.add('loginAsReadOnlyUser', () => { cy.loginAs({ username: 'apm_read_user', password: 'changeme' }); @@ -39,3 +40,24 @@ Cypress.Commands.add('changeTimeRange', (value: string) => { cy.get('[data-test-subj="superDatePickerToggleQuickMenuButton"]').click(); cy.contains(value).click(); }); + +Cypress.Commands.add( + 'expectAPIsToHaveBeenCalledWith', + ({ + apisIntercepted, + value, + }: { + apisIntercepted: string[]; + value: string; + }) => { + cy.wait(apisIntercepted).then((interceptions) => { + if (Array.isArray(interceptions)) { + interceptions.map((interception) => { + expect(interception.request.url).include(value); + }); + } else { + expect((interceptions as Interception).request.url).include(value); + } + }); + } +); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts index fff38c9c6d18b7..b47e664e0a0f8a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts @@ -11,5 +11,9 @@ declare namespace Cypress { loginAsSuperUser(): void; loginAs(params: { username: string; password: string }): void; changeTimeRange(value: string): void; + expectAPIsToHaveBeenCalledWith(params: { + apisIntercepted: string[]; + value: string; + }): void; } } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index 4da5ba5a4ae641..46747e18c44afd 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -234,6 +234,7 @@ export function getColumns({ anchorPosition="leftCenter" button={ toggleRowActionMenu(instanceItem.serviceNodeName) @@ -257,6 +258,7 @@ export function getColumns({ render: (instanceItem: MainStatsServiceInstanceItem) => { return ( toggleRowDetails(instanceItem.serviceNodeName)} aria-label={ itemIdToExpandedRowMap[instanceItem.serviceNodeName] diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx index 909c879b433b03..98443fdf0d0ea5 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -138,12 +138,13 @@ export function ServiceOverviewInstancesTable({ - + (this.parentNode = node)}>{suggestions} + (this.parentNode = node)} + > + {suggestions} + ); } } diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js index 21524a877d4cc3..f441497209c3a9 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js @@ -172,6 +172,7 @@ export class Typeahead extends Component { >