From 73a0b486f13756cffa7f4f506998aab54496d4ac Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Thu, 12 Mar 2026 18:48:25 +0100 Subject: [PATCH] [#656] Query history --- cypress/e2e/2_cache-detail-search.cy.js | 20 +- cypress/e2e/2_cache-query-history.cy.js | 144 +++++++++ src/app/AppLayout/AppLayout.tsx | 2 +- src/app/Caches/DetailCache.tsx | 54 ++-- src/app/Caches/Query/DeleteByQueryEntries.tsx | 25 +- src/app/Caches/Query/QueryEntries.tsx | 253 ++++++++------- src/app/Caches/Query/QueryHistory.tsx | 292 ++++++++++++++++++ src/app/assets/languages/en.json | 18 +- src/app/providers/QueryContextProvider.tsx | 105 +++++++ src/app/services/searchHook.ts | 105 +++++-- src/app/utils/localStorage.ts | 2 +- src/services/searchService.ts | 36 +-- src/types/InfinispanTypes.ts | 16 + 13 files changed, 851 insertions(+), 221 deletions(-) create mode 100644 cypress/e2e/2_cache-query-history.cy.js create mode 100644 src/app/Caches/Query/QueryHistory.tsx create mode 100644 src/app/providers/QueryContextProvider.tsx diff --git a/cypress/e2e/2_cache-detail-search.cy.js b/cypress/e2e/2_cache-detail-search.cy.js index dd960e657..6dd13e062 100644 --- a/cypress/e2e/2_cache-detail-search.cy.js +++ b/cypress/e2e/2_cache-detail-search.cy.js @@ -17,13 +17,13 @@ describe('Cache Detail Overview', () => { it('successfully deletes by query', () => { cy.get('[data-cy=manageEntriesTab]').click(); cy.get('[data-cy=queriesTab]').click(); - cy.get('#textSearchByQuery').click().type('from org.infinispan.Person where age = 9'); - cy.get('button[aria-label=searchButton]').click(); + cy.get('#textSearchByQuery').find('textarea').type('from org.infinispan.Person where age = 9', { force: true }); + cy.get('[data-cy=searchButton]').click(); cy.contains('1 - 1 of 1'); cy.contains('Oihana'); cy.get('[data-cy=deleteByQueryButton]').click(); cy.get('[data-cy=deleteButton]').click(); - cy.get('button[aria-label=searchButton]').click(); + cy.get('[data-cy=searchButton]').click(); cy.contains('Values not found.'); cy.get('[data-cy=deleteByQueryButton]').should('be.disabled'); }) @@ -32,16 +32,17 @@ describe('Cache Detail Overview', () => { // Going back to cache entries page cy.get('[data-cy=manageEntriesTab]').click(); cy.get('[data-cy=queriesTab]').click(); - cy.get('#textSearchByQuery').click().type('from org.infinispan.Person where age > 2'); - cy.get('button[aria-label=searchButton]').click(); + cy.get('#textSearchByQuery').find('textarea').type('from org.infinispan.Person where age > 2', { force: true }); + cy.get('[data-cy=searchButton]').click(); cy.contains('1 - 1 of 1'); cy.contains('Elaia'); // Going back to cache entries page cy.get('[data-cy=manageEntriesTab]').click(); cy.get('[data-cy=queriesTab]').click(); - cy.get('#textSearchByQuery').click().clear().type("from org.infinispan.Person where name = 'Elaia'"); - cy.get('button[aria-label=searchButton]').click(); + cy.get('#textSearchByQuery').find('textarea').type('{selectall}{backspace}', { force: true }); + cy.get('#textSearchByQuery').find('textarea').type("from org.infinispan.Person where name = 'Elaia'", { force: true }); + cy.get('[data-cy=searchButton]').click(); cy.contains('1 - 1 of 1'); cy.contains('Elaia'); @@ -49,8 +50,9 @@ describe('Cache Detail Overview', () => { cy.get('[data-cy=manageEntriesTab]').click(); cy.get('[data-cy=queriesTab]').click(); - cy.get('#textSearchByQuery').click().clear().type('from org.infinispan.Person where age=2'); - cy.get('button[aria-label=searchButton]').click(); + cy.get('#textSearchByQuery').find('textarea').type('{selectall}{backspace}', { force: true }); + cy.get('#textSearchByQuery').find('textarea').type('from org.infinispan.Person where age=2', { force: true }); + cy.get('[data-cy=searchButton]').click(); cy.contains('Values not found.'); // Verify query metrics available diff --git a/cypress/e2e/2_cache-query-history.cy.js b/cypress/e2e/2_cache-query-history.cy.js new file mode 100644 index 000000000..f36dadac0 --- /dev/null +++ b/cypress/e2e/2_cache-query-history.cy.js @@ -0,0 +1,144 @@ +describe('Query History', () => { + const username = Cypress.env('username'); + const password = Cypress.env('password'); + + before(() => { + // Ensure test data exists + cy.cleanupTest(username, password, '/caches/indexed-cache/query-history-test', 'DELETE'); + const payload = '{"_type": "org.infinispan.Person", "name": "HistoryTest", "age": "25", "city": "Madrid"}'; + cy.cleanupTest(username, password, '/caches/indexed-cache/query-history-test', 'POST', payload); + }); + + beforeEach(() => { + // Clear localStorage to start with clean query history + cy.clearLocalStorage('cache-query-history'); + cy.login(username, password, '/cache/indexed-cache'); + }); + + it('successfully records a search query in history', () => { + // Navigate to queries tab and execute a search + cy.get('[data-cy=manageEntriesTab]').click(); + cy.get('[data-cy=queriesTab]').click(); + cy.get('#textSearchByQuery').find('textarea').type('from org.infinispan.Person where age > 2', { force: true }); + cy.get('[data-cy=searchButton]').click(); + cy.contains('Elaia'); + + // Navigate to query history tab + cy.get('[data-cy=queryHistoryTab]').click(); + + // Verify the query appears in the history table + cy.get('[data-cy=queryHistoryTable]').should('exist'); + cy.contains('from org.infinispan.Person where age > 2'); + cy.contains('Search'); + }); + + it('successfully records a delete query in history', () => { + // Navigate to queries tab and execute a delete query + cy.get('[data-cy=manageEntriesTab]').click(); + cy.get('[data-cy=queriesTab]').click(); + cy.get('#textSearchByQuery').find('textarea').type("delete from org.infinispan.Person where name = 'HistoryTest'", { force: true }); + cy.get('[data-cy=deleteByQueryButton]').click(); + cy.get('[data-cy=deleteButton]').click(); + + // Navigate to query history tab + cy.get('[data-cy=queryHistoryTab]').click(); + + // Verify the delete query appears in history with Delete type label + cy.get('[data-cy=queryHistoryTable]').should('exist'); + cy.contains('delete from org.infinispan.Person'); + cy.contains('Delete'); + + // Re-create test data for subsequent tests + cy.cleanupTest(username, password, '/caches/indexed-cache/query-history-test', 'DELETE'); + const payload = '{"_type": "org.infinispan.Person", "name": "HistoryTest", "age": "25", "city": "Madrid"}'; + cy.cleanupTest(username, password, '/caches/indexed-cache/query-history-test', 'POST', payload); + }); + + it('successfully deletes a single history item', () => { + // First, create a history entry by running a query + cy.get('[data-cy=manageEntriesTab]').click(); + cy.get('[data-cy=queriesTab]').click(); + cy.get('#textSearchByQuery').find('textarea').type('from org.infinispan.Person where age > 2', { force: true }); + cy.get('[data-cy=searchButton]').click(); + cy.contains('Elaia'); + + // Navigate to query history tab + cy.get('[data-cy=queryHistoryTab]').click(); + cy.get('[data-cy=queryHistoryTable]').should('exist'); + cy.contains('from org.infinispan.Person where age > 2'); + + // Open actions menu on the first row and click delete + cy.get('[data-cy=actions-0]').find('button').click(); + cy.contains('button', 'Delete').click(); + + // Verify the query is removed from history + cy.contains('from org.infinispan.Person where age > 2').should('not.exist'); + }); + + it('successfully clears all query history', () => { + // Create history entries by running queries + cy.get('[data-cy=manageEntriesTab]').click(); + cy.get('[data-cy=queriesTab]').click(); + cy.get('#textSearchByQuery').find('textarea').type('from org.infinispan.Person where age > 2', { force: true }); + cy.get('[data-cy=searchButton]').click(); + cy.contains('Elaia'); + + cy.get('#textSearchByQuery').find('textarea').type('{selectall}{backspace}', { force: true }); + cy.get('#textSearchByQuery').find('textarea').type('from org.infinispan.Person where age > 10', { force: true }); + cy.get('[data-cy=searchButton]').click(); + + // Navigate to query history tab + cy.get('[data-cy=queryHistoryTab]').click(); + cy.get('[data-cy=queryHistoryTable]').should('exist'); + + // Click clear all button + cy.get('[data-cy=removeAllQueryHistory]').click(); + + // Verify all history is cleared + cy.contains('from org.infinispan.Person where age > 2').should('not.exist'); + cy.contains('from org.infinispan.Person where age > 10').should('not.exist'); + }); + + it('successfully executes a query from history', () => { + // Create a history entry + cy.get('[data-cy=manageEntriesTab]').click(); + cy.get('[data-cy=queriesTab]').click(); + cy.get('#textSearchByQuery').find('textarea').type('from org.infinispan.Person where age > 2', { force: true }); + cy.get('[data-cy=searchButton]').click(); + cy.contains('Elaia'); + + // Navigate to query history tab + cy.get('[data-cy=queryHistoryTab]').click(); + cy.get('[data-cy=queryHistoryTable]').should('exist'); + + // Execute the query from history + cy.get('[data-cy=actions-0]').find('button').click(); + cy.contains('button', 'Execute').click(); + + // Verify we are back on the queries tab + cy.get('[data-cy=queriesTab]').should('have.attr', 'aria-selected', 'true'); + }); + + it('successfully records an error query in history', () => { + // Execute a query with an invalid type + cy.get('[data-cy=manageEntriesTab]').click(); + cy.get('[data-cy=queriesTab]').click(); + cy.get('#textSearchByQuery').find('textarea').type('from com.NonExistent', { force: true }); + cy.get('[data-cy=searchButton]').click(); + + // Wait for the error response + cy.contains('Query error'); + + // Navigate to query history tab + cy.get('[data-cy=queryHistoryTab]').click(); + cy.get('[data-cy=queryHistoryTable]').should('exist'); + + // Verify the error query is shown with Error label + cy.contains('from com.NonExistent'); + cy.contains('Error'); + }); + + after(() => { + cy.cleanupTest(username, password, '/caches/indexed-cache/query-history-test', 'DELETE'); + }); +}); diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index 9161a0be4..b94127f6f 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -56,7 +56,7 @@ import { import { ConsoleACL } from '@services/securityService'; import { DARK, LIGHT, ThemeContext } from '@app/providers/ThemeProvider'; import { PopoverHelp } from '@app/Common/PopoverHelp'; -import { LanguageSelector } from `@app/Common/LanguageSelector`; +import { LanguageSelector } from '@app/Common/LanguageSelector'; interface IAppLayout { children: React.ReactNode; diff --git a/src/app/Caches/DetailCache.tsx b/src/app/Caches/DetailCache.tsx index 295397663..8910f6dc7 100644 --- a/src/app/Caches/DetailCache.tsx +++ b/src/app/Caches/DetailCache.tsx @@ -62,6 +62,8 @@ import { InfinispanComponentStatus } from '@app/Common/InfinispanComponentStatus import { PageHeader } from '@patternfly/react-component-groups'; import { UpdateAliasCache } from '@app/Caches/UpdateAliasCache'; import { DeleteCache } from '@app/Caches/DeleteCache'; +import { QueryHistory } from '@app/Caches/Query/QueryHistory'; +import { QueryContextProvider } from '@app/providers/QueryContextProvider'; const DetailCache = (props: { cacheName: string }) => { const cacheName = props.cacheName; @@ -110,26 +112,40 @@ const DetailCache = (props: { cacheName: string }) => { } return ( - setActiveTabKey2(tabIndex)} - > - {t('caches.tabs.entries-manage')}} - data-cy="manageEntriesTab" + + setActiveTabKey2(tabIndex)} > - - - {t('caches.tabs.query-values')}}> - setActiveTabKey1(2)} /> - - + {t('caches.tabs.entries-manage')}} + data-cy="manageEntriesTab" + > + + + {t('caches.tabs.query-values')}}> + setActiveTabKey1(2)} /> + + {t('caches.tabs.query-history')}} + > + { + setActiveTabKey2(11); + }} + /> + + + ); }; diff --git a/src/app/Caches/Query/DeleteByQueryEntries.tsx b/src/app/Caches/Query/DeleteByQueryEntries.tsx index 310e3ae60..76b3bf818 100644 --- a/src/app/Caches/Query/DeleteByQueryEntries.tsx +++ b/src/app/Caches/Query/DeleteByQueryEntries.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { Button, ButtonVariant, Content, Modal, ModalBody, ModalFooter, ModalHeader } from '@patternfly/react-core'; -import { useApiAlert } from '@app/utils/useApiAlert'; import { useTranslation } from 'react-i18next'; -import { ConsoleServices } from '@services/ConsoleServices'; +import { useDeleteByQuery } from '@app/services/searchHook'; const DeleteByQueryEntries = (props: { cacheName: string; @@ -10,22 +9,8 @@ const DeleteByQueryEntries = (props: { isModalOpen: boolean; closeModal: () => void; }) => { - const { addAlert } = useApiAlert(); const { t } = useTranslation(); - - const onClickOnDeleteByQuery = () => { - const match = props.query.match(/FROM\s+.+$/i); - if (match) { - ConsoleServices.search() - .deleteByQuery(props.cacheName, `DELETE ${match[0]}`, t('caches.query.modal-action-entries-success')) - .then((actionResponse) => { - addAlert(actionResponse); - }) - .finally(props.closeModal); - } else { - props.closeModal(); - } - }; + const { setExecute } = useDeleteByQuery(props.cacheName, props.query, props.closeModal); return ( - {t('caches.query.modal-delete-entries-body-line-one', { cacheName: props.cacheName })} + {t('caches.query.modal-delete-entries-body-line-one', { + cacheName: props.cacheName + })} {t('caches.query.modal-delete-entries-body-line-two')} - - - - - - - ); - }; - const toolbarPagination = (dropDirection) => { return ( void }) => { }; const toolbar = ( - - - - onChangeSearch(value)} - onSearch={onSearch} - onClear={onClear} - /> - } - onClick={() => window.open(t('brandname.ickle-query-docs-link'), '_blank')} - > - {t('caches.query.ickle-query-docs')} - - } - > -