diff --git a/pkg/handler/lokiclientmock/loki_client_mock.go b/pkg/handler/lokiclientmock/loki_client_mock.go index 61b5c39bd..0663a7a05 100644 --- a/pkg/handler/lokiclientmock/loki_client_mock.go +++ b/pkg/handler/lokiclientmock/loki_client_mock.go @@ -25,7 +25,7 @@ func (o *LokiClientMock) Get(url string) ([]byte, int, error) { if isLabel { path = "mocks/loki/namespaces.json" } else { - if strings.Contains(url, "query=topk") { + if strings.Contains(url, "query=topk") || strings.Contains(url, "query=bottomk") { path = "mocks/loki/flow_metrics" if strings.Contains(url, "|unwrap%20PktDrop") { @@ -72,8 +72,32 @@ func (o *LokiClientMock) Get(url string) ([]byte, int, error) { mlog.Debugf("Reading file path: %s", path) file, err := os.ReadFile(path) + mlog.Debugf("here") if err != nil { - return nil, 500, err + // return nil, 500, err + emptyResponse := []byte(`{ + "status": "success", + "data": { + "resultType": "matrix", + "result": [] + } + }`) + + var qr model.QueryResponse + err = json.Unmarshal(emptyResponse, &qr) + if err != nil { + return nil, 500, err + } + for _, s := range qr.Data.Result.(model.Streams) { + for i := range s.Entries { + s.Entries[i].Line = decoders.NetworkEventsToString(s.Entries[i].Line) + } + } + emptyResponse, err = json.Marshal(qr) + if err != nil { + return nil, 500, err + } + return emptyResponse, 200, nil } if parseNetEvents { diff --git a/web/cypress/e2e/overview/overview.spec.ts b/web/cypress/e2e/overview/overview.spec.ts index 79dedd36f..27b8e0503 100644 --- a/web/cypress/e2e/overview/overview.spec.ts +++ b/web/cypress/e2e/overview/overview.spec.ts @@ -22,7 +22,11 @@ describe('netflow-overview', () => { cy.get('#overview-panels-modal').contains('Select all').click(); //Save + cy.setupNetworkIdleTracking('GET', '/api/flow/metrics*'); cy.get('#overview-panels-modal').contains('Save').click(); + + cy.waitForNetworkIdle(); + cy.checkPanels(c.availablePanelsCount); //reopen modal diff --git a/web/cypress/e2e/table/table.spec.ts b/web/cypress/e2e/table/table.spec.ts index b7a747876..b224cacbe 100644 --- a/web/cypress/e2e/table/table.spec.ts +++ b/web/cypress/e2e/table/table.spec.ts @@ -25,11 +25,13 @@ describe('netflow-table', () => { cy.addFilter('src_namespace', c.namespace); cy.addFilter('src_name', c.pod); - cy.changeQueryOption('Show duplicates'); cy.changeQueryOption('1000'); + cy.clickShowDuplicates(); cy.changeTimeRange('Last 1 day'); }); + + it('manage columns', () => { //first open modal cy.openColumnsModal(); diff --git a/web/cypress/support/commands.ts b/web/cypress/support/commands.ts index d2a58f340..4f78f3546 100644 --- a/web/cypress/support/commands.ts +++ b/web/cypress/support/commands.ts @@ -164,13 +164,11 @@ Cypress.Commands.add('changeTimeRange', (name, topology) => { cy.checkContent(topology); }); -Cypress.Commands.add('changeMetricFunction', (name) => { +Cypress.Commands.add('clickShowDuplicates', () => { cy.showAdvancedOptions(); cy.showDisplayOptions(); - - cy.get('#metricFunction-dropdown').click(); - cy.get('.pf-v5-c-menu__content').contains(name).click(); - cy.get('[data-layer-id="default"]').children().its('length').should('be.gte', 5); + cy.get('#table-display-dropdown').contains('Show duplicates').click(); + cy.checkContent(); }); Cypress.Commands.add('changeMetricType', (name) => { @@ -191,27 +189,107 @@ Cypress.Commands.add('checkRecordField', (field, name, values) => { }); }); + +Cypress.Commands.add('setupNetworkIdleTracking', (method: string = 'GET', urlPattern: string = '/api/**') => { + cy.wrap({ + requestCount: 0, + lastRequestTime: 0, + startTime: Date.now() + }).as('networkIdleTracker'); + + cy.intercept(method, urlPattern, (req) => { + cy.get('@networkIdleTracker').then((tracker: Cypress.networkIdleTracker) => { + tracker.requestCount++; + tracker.lastRequestTime = Date.now(); + }); + req.continue(); + }).as('networkIdleActivity'); +}); + + +Cypress.Commands.add('waitForNetworkIdle', (idleTime: number = 3000, timeout: number = 120000) => { + cy.get('@networkIdleTracker', { timeout: timeout }) + .then((tracker: Cypress.networkIdleTracker) => { + const startTime = Date.now(); + + const checkIdleCondition = () => { + const now = Date.now(); + + if (tracker.requestCount > 0 && (now - tracker.lastRequestTime) >= idleTime) { + return true; + } + + if (tracker.requestCount === 0 && (now - startTime) >= idleTime) { + return true; + } + + if (now - startTime > timeout) { + throw new Error('Timed out waiting for network idle.'); + } + + return false; + }; + + const pollUntilIdle = () => { + const isIdle = checkIdleCondition(); + + if (isIdle) { + return; + } else { + return cy.wait(1000, { log: false }).then(pollUntilIdle); + } + }; + + return cy.then(pollUntilIdle); + }); +}); + declare global { namespace Cypress { interface Chainable { openNetflowTrafficPage(clearCache?: boolean): Chainable - showAdvancedOptions(): Chainable - showDisplayOptions(): Chainable - checkPanels(panels?: number): Chainable - openPanelsModal(): Chainable - checkColumns(groups?: number, cols?: number): Chainable - openColumnsModal(): Chainable - selectPopupItems(id: string, names: string[]): Chainable - checkPopupItems(id: string, ids: string[]): Chainable - sortColumn(name: string): Chainable - dropdownSelect(id: string, name: string): Chainable - checkContent(topology?: boolean): Chainable - addFilter(filter: string, value: string, topology?: boolean): Chainable - changeQueryOption(name: string, topology?: boolean): Chainable - changeTimeRange(name: string, topology?: boolean): Chainable - changeMetricFunction(name: string): Chainable - changeMetricType(name: string): Chainable - checkRecordField(field: string, name: string, values: string[]) + showAdvancedOptions(): Chainable + showDisplayOptions(): Chainable + checkPanels(panels?: number): Chainable + openPanelsModal(): Chainable + checkColumns(groups?: number, cols?: number): Chainable + openColumnsModal(): Chainable + selectPopupItems(id: string, names: string[]): Chainable + checkPopupItems(id: string, ids: string[]): Chainable + sortColumn(name: string): Chainable + dropdownSelect(id: string, name: string): Chainable + checkContent(topology?: boolean): Chainable + addFilter(filter: string, value: string, topology?: boolean): Chainable + changeQueryOption(name: string, topology?: boolean): Chainable + changeTimeRange(name: string, topology?: boolean): Chainable + changeMetricType(name: string): Chainable + checkRecordField(field: string, name: string, values: string[]): Chainable + clickShowDuplicates():Chainable + + /** + * Sets up network interception to track active requests for idle detection. + * This command *must* be called before `cy.waitForNetworkIdle` + * + * @param method HTTP method to intercept (default: 'GET') + * @param urlPattern URL pattern to intercept (default: '/api/**') + */ + setupNetworkIdleTracking(method?: string, urlPattern?: string): Chainable + + /** + * Waits until no intercepted requests (matching the patterns + * set in `setupNetworkIdleTracking`) have been active for `idleTime`, + * or until the `timeout` is reached. + * + * @param idleTime How long the network must be idle (in ms) + * @param timeout Total time to wait before timing out (in ms) + */ + waitForNetworkIdle(idleTime?: number, timeout?: number): Chainable + + networkIdleTracker: { + requestCount: number; + lastRequestTime: number; + startTime: number; + }; } } -} \ No newline at end of file +}