From 3cfb0bc05cbdc60f09ee15c4865813646cf6dd67 Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Wed, 22 Nov 2023 15:20:33 +0100 Subject: [PATCH] ISPN-15324 Reset stats * Global stats * Per cache stats --- cypress/e2e/1_global-stats.cy.js | 16 ++- cypress/e2e/1_rbac_func.cy.js | 14 +++ cypress/e2e/2_cache-detail-search.cy.js | 3 + cypress/e2e/2_cache-metrics.cy.js | 9 ++ src/__tests__/views/GlobalStats.test.tsx | 76 ------------ src/app/AccessManagement/AccessManager.tsx | 12 +- src/app/Caches/CacheMetrics.tsx | 2 +- src/app/Caches/DataAccess.tsx | 49 +++++++- src/app/Caches/DetailCache.tsx | 135 +++++++++++++-------- src/app/Caches/Query/ClearQueryMetrics.tsx | 49 -------- src/app/Caches/Query/QueryMetrics.tsx | 7 +- src/app/ClearMetrics/ClearMetrics.tsx | 44 +++++++ src/app/GlobalStats/GlobalStats.tsx | 96 +++++++++++---- src/app/assets/languages/en.json | 24 ++-- src/app/services/statsHook.ts | 32 +++++ src/services/cacheService.ts | 13 ++ src/services/dataContainerService.ts | 23 +++- src/services/securityService.ts | 5 +- src/types/InfinispanTypes.ts | 1 + 19 files changed, 371 insertions(+), 239 deletions(-) delete mode 100644 src/__tests__/views/GlobalStats.test.tsx delete mode 100644 src/app/Caches/Query/ClearQueryMetrics.tsx create mode 100644 src/app/ClearMetrics/ClearMetrics.tsx diff --git a/cypress/e2e/1_global-stats.cy.js b/cypress/e2e/1_global-stats.cy.js index 9f716fb5b..cca5097ad 100644 --- a/cypress/e2e/1_global-stats.cy.js +++ b/cypress/e2e/1_global-stats.cy.js @@ -12,8 +12,7 @@ describe('Global stats', () => { cy.contains('Cluster distribution'); }); - //View all caches href - it('successfully loads Global stats', () => { + it('successfully links to caches link', () => { //click View all caches should navigate to console page cy.get('[data-cy="viewCachesLink"]').click(); //Verify that page is properly loaded after click; @@ -24,8 +23,7 @@ describe('Global stats', () => { cy.contains('java-serialized-cache'); }); - //View Cluster Status href - it('successfully loads Global stats', () => { + it('successfully links to cluster status', () => { //click View Cluster Status should navigate to cluster-membership page cy.get('[data-cy="viewClustersLink"]').click(); @@ -33,4 +31,14 @@ describe('Global stats', () => { cy.get('h1').should('contain', 'Cluster membership'); cy.contains('Healthy'); }); + + it('successfully resets and refresh global metrics', () => { + cy.get('[data-cy="globalStatsActions"]').click(); + cy.get('[data-cy="clearAccessMetricsButton"]').click(); + cy.contains('Permanently clear global metrics?'); + cy.get('[data-cy="confirmButton"]').click(); + cy.get('[data-cy="globalStatsActions"]').click(); + cy.get('[data-cy="refreshAction"]').click(); + }); + }); diff --git a/cypress/e2e/1_rbac_func.cy.js b/cypress/e2e/1_rbac_func.cy.js index c9441b1cc..0e85e75fc 100644 --- a/cypress/e2e/1_rbac_func.cy.js +++ b/cypress/e2e/1_rbac_func.cy.js @@ -13,6 +13,8 @@ describe('RBAC Functionality Tests', () => { checkMenu(false); cy.login(monitorUserName, Cypress.env('password'), '/cache/default'); checkNoEntriesTabView(false); + cy.login(monitorUserName, Cypress.env('password'), '/global-stats'); + checkGlobalStatsView(false) }); it('successfully logins and performs actions with observer user', () => { @@ -36,6 +38,8 @@ describe('RBAC Functionality Tests', () => { checkCountersPageView(); cy.login(observerUserName, Cypress.env('password'), '/cache/default'); checkNoEntriesTabView(false); + cy.login(observerUserName, Cypress.env('password'), '/global-stats'); + checkGlobalStatsView(false) }); it('successfully logins and performs actions with application user', () => { @@ -53,6 +57,8 @@ describe('RBAC Functionality Tests', () => { checkCountersPageView(); cy.login(applicationUserName, Cypress.env('password'), '/cache/default'); checkNoEntriesTabView(false); + cy.login(applicationUserName, Cypress.env('password'), '/global-stats'); + checkGlobalStatsView(false) }); it('successfully logins and performs actions with deployer user', () => { @@ -70,6 +76,8 @@ describe('RBAC Functionality Tests', () => { checkCountersPageView(); cy.login(deployerUserName, Cypress.env('password'), '/cache/default'); checkNoEntriesTabView(false); + cy.login(deployerUserName, Cypress.env('password'), '/global-stats'); + checkGlobalStatsView(false) }); it('successfully logins and performs actions with admin user', () => { @@ -181,6 +189,7 @@ describe('RBAC Functionality Tests', () => { cy.get('[data-cy=queriesTab]').should('exist'); } + cy.get('[data-cy=detailCacheActions]').click(); cy.get('[data-cy=manageIndexesLink]').click(); if (isSuperAdmin) { cy.get('[data-cy=clearIndexButton]').should('exist'); @@ -277,6 +286,7 @@ describe('RBAC Functionality Tests', () => { cy.get('[data-cy=cacheConfigurationTab]').click(); cy.contains('authorization').should('not.exist'); } + cy.get('[data-cy=detailCacheActions]').click(); cy.get('[data-cy=manageIndexesLink]').click(); if (isSuperAdmin) { cy.get('[data-cy=clearIndexButton]').should('exist'); @@ -410,6 +420,10 @@ describe('RBAC Functionality Tests', () => { // metrics tab is visible cy.contains('Data access').should('exist'); } + } + function checkGlobalStatsView() { + cy.get('[data-cy="globalStatsActions"]').click(); + cy.contains('[data-cy="clearAccessMetricsButton"]').should('not.exist'); } }); diff --git a/cypress/e2e/2_cache-detail-search.cy.js b/cypress/e2e/2_cache-detail-search.cy.js index 43fa6e720..9fd3628fc 100644 --- a/cypress/e2e/2_cache-detail-search.cy.js +++ b/cypress/e2e/2_cache-detail-search.cy.js @@ -49,12 +49,14 @@ describe('Cache Detail Overview', () => { //Opening indexed-cache cache page. cy.login(Cypress.env("username"), Cypress.env("password"), '/cache/indexed-cache'); + cy.get('[data-cy=detailCacheActions]').click(); cy.get("[data-cy=manageIndexesLink]").click(); cy.contains("org.infinispan.Person"); cy.contains("3 k"); cy.get("[data-cy=backButton]").click(); cy.contains("Elaia"); + cy.get('[data-cy=detailCacheActions]').click(); cy.get("[data-cy=manageIndexesLink]").click(); cy.get("[data-cy=clearIndexButton]").click(); cy.contains("Permanently clear index?"); @@ -76,6 +78,7 @@ describe('Cache Detail Overview', () => { cy.get("[data-cy=backButton]").click(); cy.contains("Elaia"); + cy.get('[data-cy=detailCacheActions]').click(); cy.get("[data-cy=manageIndexesLink]").click(); cy.contains("3 k"); }) diff --git a/cypress/e2e/2_cache-metrics.cy.js b/cypress/e2e/2_cache-metrics.cy.js index 5d3a3af81..9ac76fed0 100644 --- a/cypress/e2e/2_cache-metrics.cy.js +++ b/cypress/e2e/2_cache-metrics.cy.js @@ -33,6 +33,15 @@ describe('Cache Metrics Overview', () => { verifyCacheMetrics(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); }); + it('successfully resets metrics', () => { + cy.login(Cypress.env('username'), Cypress.env('password'), '/cache/people'); + cy.get('[data-cy=cacheMetricsTab]').click(); + cy.get('[data-cy=clearAccessMetricsButton]').click(); + cy.contains('Permanently clear data access metrics?'); + cy.get('[data-cy=confirmButton]').click(); + cy.contains('Cache stats people cleared'); + }); + // TODO: Add a test of good stats display with a cache that does not change and provided different metrics function verifyCacheMetrics( diff --git a/src/__tests__/views/GlobalStats.test.tsx b/src/__tests__/views/GlobalStats.test.tsx deleted file mode 100644 index 0126cd67e..000000000 --- a/src/__tests__/views/GlobalStats.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import * as StatsHook from '@app/services/statsHook'; -import { GlobalStats } from '@app/GlobalStats/GlobalStats'; -import { renderWithRouter } from '../../test-utils'; - -jest.mock('@app/services/statsHook'); -jest.mock('@app/GlobalStats/ClusterDistributionChart', () => 'ClusterDistributionChart'); - -const mockedStatsHook = StatsHook as jest.Mocked; - -const statsNotEnabledResponse = { - statistics_enabled: false, - hits: -1, - retrievals: -1, - remove_misses: -1, - remove_hits: -1, - evictions: -1, - stores: -1, - misses: -1 -} as CacheManagerStats; - -const statsEnabledResponse = { - statistics_enabled: true, - hits: -1, - retrievals: -1, - remove_misses: -1, - remove_hits: -1, - evictions: -1, - stores: -1, - misses: -1 -} as CacheManagerStats; - -beforeEach(() => { - mockedStatsHook.useFetchGlobalStats.mockClear(); -}); - -describe('Global stats page', () => { - test('render a non stats enabled message when stats are not enabled', () => { - mockedStatsHook.useFetchGlobalStats.mockImplementationOnce(() => { - return { - loading: false, - stats: statsNotEnabledResponse, - error: '', - reload: () => {} - }; - }); - - render(); - expect(screen.getByRole('heading', { name: 'global-stats.title' })).toBeInTheDocument(); - expect(screen.queryByText('global-stats.global-stats-disable-msg')).toBeInTheDocument(); - expect(screen.queryByText('global-stats.global-stats-disabled-help')).toBeInTheDocument(); - }); - - test('render stats when statistics are enabled', () => { - mockedStatsHook.useFetchGlobalStats.mockImplementationOnce(() => { - return { - loading: false, - stats: statsEnabledResponse, - error: '', - reload: () => {} - }; - }); - - renderWithRouter(); - - expect(screen.getByRole('heading', { name: 'global-stats.title' })).toBeInTheDocument(); - expect(screen.queryByText('global-stats.cluster-wide-stats')).toBeInTheDocument(); - expect(screen.queryByText('global-stats.data-access-stats')).toBeInTheDocument(); - expect(screen.queryByText('global-stats.operation-performance-values')).toBeInTheDocument(); - expect(screen.queryByText('global-stats.cache-manager-lifecycle')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'global-stats.view-caches-link' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'global-stats.view-cluster-membership-link' })).toBeInTheDocument(); - expect(screen.getByText('global-stats.global-stats-enable-msg')).toBeInTheDocument(); - }); -}); diff --git a/src/app/AccessManagement/AccessManager.tsx b/src/app/AccessManagement/AccessManager.tsx index 502da682d..52702098d 100644 --- a/src/app/AccessManagement/AccessManager.tsx +++ b/src/app/AccessManagement/AccessManager.tsx @@ -38,14 +38,6 @@ const AccessManager = () => { const [isOpen, setIsOpen] = useState(false); const [isFlushCache, setIsFlushCache] = useState(false); - const onToggleClick = () => { - setIsOpen(!isOpen); - }; - - const onSelect = (_event: React.MouseEvent | undefined, value: string | number | undefined) => { - setIsOpen(false); - }; - interface AccessTab { key: string; name: string; @@ -100,10 +92,10 @@ const AccessManager = () => { setIsOpen(false)} onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} toggle={(toggleRef: React.Ref) => ( - + setIsOpen(!isOpen)} isExpanded={isOpen}> {t('common.actions.actions')} )} diff --git a/src/app/Caches/CacheMetrics.tsx b/src/app/Caches/CacheMetrics.tsx index b1dd3aead..6469c6881 100644 --- a/src/app/Caches/CacheMetrics.tsx +++ b/src/app/Caches/CacheMetrics.tsx @@ -275,7 +275,7 @@ const CacheMetrics = (props: { cacheName: string; display: boolean }) => { {displayDataDistribution && {buildDataDistribution()}} - + {buildQueryStats()} diff --git a/src/app/Caches/DataAccess.tsx b/src/app/Caches/DataAccess.tsx index 387b156b8..644bf8d7e 100644 --- a/src/app/Caches/DataAccess.tsx +++ b/src/app/Caches/DataAccess.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Card, CardBody, @@ -9,16 +9,26 @@ import { TextList, TextListItem, TextListVariants, - TextListItemVariants + TextListItemVariants, + LevelItem, + Level, + Button, + ButtonVariant } from '@patternfly/react-core'; import { useTranslation } from 'react-i18next'; import { PopoverHelp } from '@app/Common/PopoverHelp'; import displayUtils from '@services/displayUtils'; import { global_spacer_sm } from '@patternfly/react-tokens'; +import { ConsoleServices } from '@services/ConsoleServices'; +import { ConsoleACL } from '@services/securityService'; +import { ClearMetrics } from '@app/ClearMetrics/ClearMetrics'; +import { useConnectedUser } from '@app/services/userManagementHook'; -const DataAccess = (props: { stats: CacheStats }) => { +const DataAccess = (props: { cacheName: string; stats: CacheStats }) => { const { t } = useTranslation(); - const brandname = t('brandname.brandname'); + const { connectedUser } = useConnectedUser(); + const [isClearMetricsModalOpen, setClearMetricsModalOpen] = useState(false); + const all = props.stats.hits + props.stats.retrievals + @@ -41,9 +51,38 @@ const DataAccess = (props: { stats: CacheStats }) => { ); }; + const buildClearStatsButton = () => { + if (!ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser)) { + return ''; + } + + return ( + + + setClearMetricsModalOpen(false)} + type={'cache-metrics'} + /> + + ); + }; + return ( - {t('caches.cache-metrics.data-access-title')} + + + {t('caches.cache-metrics.data-access-title')} + {buildClearStatsButton()} + + diff --git a/src/app/Caches/DetailCache.tsx b/src/app/Caches/DetailCache.tsx index 24a27e264..338be3500 100644 --- a/src/app/Caches/DetailCache.tsx +++ b/src/app/Caches/DetailCache.tsx @@ -9,6 +9,9 @@ import { Card, CardBody, Divider, + Dropdown, + DropdownItem, + DropdownList, EmptyState, EmptyStateActions, EmptyStateBody, @@ -18,6 +21,8 @@ import { EmptyStateVariant, Label, LabelGroup, + MenuToggle, + MenuToggleElement, PageSection, PageSectionVariants, Spinner, @@ -40,14 +45,7 @@ import { CacheConfiguration } from '@app/Caches/Configuration/CacheConfiguration import { CacheTypeBadge } from '@app/Common/CacheTypeBadge'; import { DataContainerBreadcrumb } from '@app/Common/DataContainerBreadcrumb'; import { global_BackgroundColor_100, global_danger_color_200, global_info_color_200 } from '@patternfly/react-tokens'; -import { - ExclamationCircleIcon, - ExclamationTriangleIcon, - InfoCircleIcon, - InfoIcon, - RedoIcon, - ArrowRightIcon -} from '@patternfly/react-icons'; +import { ExclamationCircleIcon, InfoCircleIcon, InfoIcon, RedoIcon } from '@patternfly/react-icons'; import { QueryEntries } from '@app/Caches/Query/QueryEntries'; import { Link } from 'react-router-dom'; import { useCacheDetail } from '@app/services/cachesHook'; @@ -59,10 +57,12 @@ import { RebalancingCache } from '@app/Rebalancing/RebalancingCache'; import { CacheConfigUtils } from '@services/cacheConfigUtils'; import { EncodingType } from '@services/infinispanRefData'; import { ThemeContext } from '@app/providers/ThemeProvider'; +import { useNavigate } from 'react-router'; const DetailCache = (props: { cacheName: string }) => { const cacheName = props.cacheName; const { t } = useTranslation(); + const navigate = useNavigate(); const { theme } = useContext(ThemeContext); const brandname = t('brandname.brandname'); const encodingDocs = t('brandname.encoding-docs-link'); @@ -70,6 +70,7 @@ const DetailCache = (props: { cacheName: string }) => { const { loading, error, cache, loadCache } = useCacheDetail(); const [activeTabKey1, setActiveTabKey1] = useState(''); const [activeTabKey2, setActiveTabKey2] = useState(10); + const [isOpen, setIsOpen] = useState(false); useEffect(() => { loadCache(cacheName); @@ -246,24 +247,31 @@ const DetailCache = (props: { cacheName: string }) => { ); }; + const displayBackupsManagement = () => { + return cache?.features.hasRemoteBackup && ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser); + }; + + const displayIndexManage = () => { + return cache?.features.indexed; + }; + const buildBackupsManage = () => { - if (!cache?.features.hasRemoteBackup) return; + if (!displayBackupsManagement()) return; - if (!ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser)) { - return; - } return ( - - - + navigate({ + pathname: '/cache/' + encodeURIComponent(cacheName) + '/backups', search: location.search - }} - > - - - + }) + } + > + {t('caches.actions.action-manage-backups')} + ); }; @@ -281,40 +289,38 @@ const DetailCache = (props: { cacheName: string }) => { }; const buildIndexManage = () => { - if (!cache?.features.indexed) return; + if (!displayIndexManage()) return; return ( - - - + navigate({ pathname: '/cache/' + encodeURIComponent(cacheName) + '/indexing', search: location.search - }} - > - - - + }) + } + > + {t('caches.actions.action-manage-indexes')} + ); }; - const buildRefreshButton = () => { + const buildRefresh = () => { return ( - - - + + ); }; @@ -342,10 +348,6 @@ const DetailCache = (props: { cacheName: string }) => { {buildDisplayReindexing()} {buildFeaturesChip()} - - {buildBackupsManage()} - {buildIndexManage()} - ); @@ -392,6 +394,37 @@ const DetailCache = (props: { cacheName: string }) => { ); }; + const displayActions = ( + + + setIsOpen(false)} + onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + setIsOpen(!isOpen)} + isExpanded={isOpen} + > + {t('common.actions.actions')} + + )} + ouiaId="detailCacheDropdown" + shouldFocusToggleOnSelect + > + + {buildIndexManage()} + {buildBackupsManage()} + {buildRefresh()} + + + + + ); + const buildCacheHeader = () => { if (loading || !cache) { return ( @@ -439,7 +472,7 @@ const DetailCache = (props: { cacheName: string }) => { - {buildRefreshButton()} + {displayActions} {buildShowMorePanel()} diff --git a/src/app/Caches/Query/ClearQueryMetrics.tsx b/src/app/Caches/Query/ClearQueryMetrics.tsx deleted file mode 100644 index 02d09e775..000000000 --- a/src/app/Caches/Query/ClearQueryMetrics.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { Button, ButtonVariant, Modal, Text, TextContent } from '@patternfly/react-core'; -import { useApiAlert } from '@app/utils/useApiAlert'; -import { useTranslation } from 'react-i18next'; -import { ConsoleServices } from '@services/ConsoleServices'; - -/** - * ClearQueryMetrics entry modal - */ -const ClearQueryMetrics = (props: { cacheName: string; isModalOpen: boolean; closeModal: () => void }) => { - const { addAlert } = useApiAlert(); - const { t } = useTranslation(); - const brandname = t('brandname.brandname'); - - const onClickDeleteButton = () => { - ConsoleServices.search() - .clearQueryStats(props.cacheName) - .then((actionResponse) => { - addAlert(actionResponse); - props.closeModal(); - }); - }; - - return ( - - {t('caches.query.modal-button-query-clear-stats')} - , - - ]} - > - - {t('caches.query.modal-button-query-clear-stats-body')} - - - ); -}; - -export { ClearQueryMetrics }; diff --git a/src/app/Caches/Query/QueryMetrics.tsx b/src/app/Caches/Query/QueryMetrics.tsx index 1beef57cb..1012e1bb3 100644 --- a/src/app/Caches/Query/QueryMetrics.tsx +++ b/src/app/Caches/Query/QueryMetrics.tsx @@ -19,7 +19,7 @@ import { TextVariants } from '@patternfly/react-core'; import { TableErrorState } from '@app/Common/TableErrorState'; -import { ClearQueryMetrics } from '@app/Caches/Query/ClearQueryMetrics'; +import { ClearMetrics } from '@app/ClearMetrics/ClearMetrics'; import { useTranslation } from 'react-i18next'; import { ConsoleServices } from '@services/ConsoleServices'; import { useConnectedUser } from '@app/services/userManagementHook'; @@ -120,10 +120,11 @@ const QueryMetrics = (props: { cacheName: string }) => { > {t('caches.query.button-clear-query-stats')} - ); diff --git a/src/app/ClearMetrics/ClearMetrics.tsx b/src/app/ClearMetrics/ClearMetrics.tsx new file mode 100644 index 000000000..13603e527 --- /dev/null +++ b/src/app/ClearMetrics/ClearMetrics.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Button, ButtonVariant, Modal, Text, TextContent } from '@patternfly/react-core'; +import { useTranslation } from 'react-i18next'; +import { useClearStats } from '@app/services/statsHook'; + +/** + * ClearMetrics entry modal + */ +const ClearMetrics = (props: { + name: string; + type: 'query' | 'cache-metrics' | 'global-stats'; + isModalOpen: boolean; + closeModal: () => void; +}) => { + const { t } = useTranslation(); + const { onClearStats } = useClearStats(props.name, props.type, props.closeModal); + const label = props.type == 'global-stats' ? props.type : 'caches.' + props.type; + + return ( + + {t(label + '.modal-button-clear-stats')} + , + + ]} + > + + {t(label + '.modal-button-clear-stats-body')} + + + ); +}; + +export { ClearMetrics }; diff --git a/src/app/GlobalStats/GlobalStats.tsx b/src/app/GlobalStats/GlobalStats.tsx index 9040401cb..e843b8c6d 100644 --- a/src/app/GlobalStats/GlobalStats.tsx +++ b/src/app/GlobalStats/GlobalStats.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useState } from 'react'; import { Button, ButtonVariant, @@ -6,19 +7,23 @@ import { CardBody, CardTitle, Divider, + Dropdown, + DropdownItem, + DropdownList, EmptyState, EmptyStateBody, + EmptyStateHeader, EmptyStateIcon, EmptyStateVariant, Grid, GridItem, Level, LevelItem, + MenuToggle, + MenuToggleElement, PageSection, PageSectionVariants, Spinner, - Stack, - StackItem, Text, TextContent, TextList, @@ -26,10 +31,9 @@ import { TextListItemVariants, TextListVariants, TextVariants, - EmptyStateHeader, - ToolbarGroup, - ToolbarContent, Toolbar, + ToolbarContent, + ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; import { ArrowIcon, CubesIcon, RedoIcon } from '@patternfly/react-icons'; @@ -40,18 +44,20 @@ import { useFetchGlobalStats } from '@app/services/statsHook'; import { useTranslation } from 'react-i18next'; import { PopoverHelp } from '@app/Common/PopoverHelp'; import ClusterDistributionChart from '@app/GlobalStats/ClusterDistributionChart'; -import { global_spacer_sm } from '@patternfly/react-tokens'; +import { ConsoleServices } from '@services/ConsoleServices'; +import { ConsoleACL } from '@services/securityService'; +import { ClearMetrics } from '@app/ClearMetrics/ClearMetrics'; +import { useConnectedUser } from '@app/services/userManagementHook'; const GlobalStats = () => { const { t } = useTranslation(); - const brandname = t('brandname.brandname'); const { stats, error, loading, reload } = useFetchGlobalStats(); + const { connectedUser } = useConnectedUser(); + const [isClearMetricsModalOpen, setClearMetricsModalOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); const allOps = () => { return stats.hits + stats.misses + stats.remove_hits + stats.remove_misses + stats.stores + stats.evictions; - if (stats?.statistics_enabled) { - } - return 0; }; const clusterStatsCard = () => { @@ -121,6 +127,26 @@ const GlobalStats = () => { ); }; + const buildClearStatsItem = () => { + if (!ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser)) { + return ''; + } + + return ( + + + setClearMetricsModalOpen(true)} + > + {t('caches.cache-metrics.button-clear-access-stats')} + + + ); + }; + const dataAccessCard = () => { return ( @@ -332,18 +358,31 @@ const GlobalStats = () => { } }; - const buildRefreshButton = ( - + const displayActions = ( + + + setIsOpen(false)} + onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + setIsOpen(!isOpen)} isExpanded={isOpen}> + {t('common.actions.actions')} + + )} + ouiaId="globalAccessDropdown" + shouldFocusToggleOnSelect + > + + }> + {t('common.actions.refresh')} + + {buildClearStatsItem()} + + + + ); return ( @@ -359,13 +398,20 @@ const GlobalStats = () => { - - {buildRefreshButton} - + {displayActions} {buildStats()} + { + reload(); + setClearMetricsModalOpen(false); + }} + type={'global-stats'} + /> ); }; diff --git a/src/app/assets/languages/en.json b/src/app/assets/languages/en.json index 020862937..5b5780257 100644 --- a/src/app/assets/languages/en.json +++ b/src/app/assets/languages/en.json @@ -640,16 +640,15 @@ "view-all-query-stats": "View all query statistics", "query-metrics-title": "Query metrics", "query-metrics-tooltip": "Average time, in nanoseconds, for queries on this cache.", - "button-clear-query-stats": "Clear all stats", + "button-clear-query-stats": "Clear all metrics", "stat-max": "Max", "stat-count": "Count", "stat-average": "Average", "stat-slowest": "Slowest", - "modal-clear-query-stats": "Permanently clear query stats?", - "modal-clear-query-stats-label": "Clear query stats modal", - "modal-button-query-clear-stats": "Clear stats", - "modal-button-cancel": "Cancel", - "modal-button-query-clear-stats-body": "All query statistics in this cluster will be removed." + "modal-clear-stats": "Permanently clear query metrics?", + "modal-clear-stats-label": "Clear query metrics modal", + "modal-button-clear-stats": "Clear metrics", + "modal-button-clear-stats-body": "All query metrics in this cluster will be removed." }, "index": { "title": "Index management", @@ -755,7 +754,12 @@ "cache-started-since": "Time since start", "cache-reset-since": "Time since reset", "cache-started-since-info": "Elapsed time in seconds since the cache was started.", - "cache-reset-since-info": "Elapsed time in seconds since the statistics were last reset." + "cache-reset-since-info": "Elapsed time in seconds since the statistics were last reset.", + "button-clear-access-stats": "Clear all metrics", + "modal-clear-stats": "Permanently clear data access metrics?", + "modal-clear-stats-label": "Clear cache access metrics modal", + "modal-button-clear-stats": "Clear metrics", + "modal-button-clear-stats-body": "All data access metrics in this cache will be removed." } }, "schemas": { @@ -837,7 +841,11 @@ "cluster-distribution-option-memory-available": "Memory available", "cluster-distribution-option-no-memory": "No memory", "global-stats-enable-msg": "Global statistics for all caches in the cluster", - "global-stats-disable-msg": "You must enable global statistics in the Cache Manager configuration to display values." + "global-stats-disable-msg": "You must enable global statistics in the Cache Manager configuration to display values.", + "modal-clear-stats": "Permanently clear global metrics?", + "modal-clear-stats-label": "Clear global stats modal", + "modal-button-clear-stats": "Clear metrics", + "modal-button-clear-stats-body": "All data access global metrics in this cluster will be removed." }, "cluster-membership": { "title": "Cluster membership", diff --git a/src/app/services/statsHook.ts b/src/app/services/statsHook.ts index 0b443aa17..487aeea0c 100644 --- a/src/app/services/statsHook.ts +++ b/src/app/services/statsHook.ts @@ -1,8 +1,10 @@ import { useEffect, useState } from 'react'; import { ConsoleServices } from '@services/ConsoleServices'; +import { useApiAlert } from '@utils/useApiAlert'; export function useFetchGlobalStats() { const [stats, setStats] = useState({ + name: '', statistics_enabled: false, hits: -1, retrievals: -1, @@ -81,3 +83,33 @@ export function useSearchStats(cacheName: string) { setLoading }; } + +export function useClearStats(name: string, type: 'query' | 'cache-metrics' | 'global-stats', action: () => void) { + const { addAlert } = useApiAlert(); + const onClearStats = () => { + let actionResponsePromise: undefined | Promise; + if (type == 'query') { + actionResponsePromise = ConsoleServices.search().clearQueryStats(name); + } else if (type == 'cache-metrics') { + actionResponsePromise = ConsoleServices.caches().clearStats(name); + } else if (type == 'global-stats') { + actionResponsePromise = ConsoleServices.dataContainer().clearCacheManagerStats(name); + } else { + console.warn('Requesting a reset type that is not available. Do nothing'); + actionResponsePromise = undefined; + } + + if (actionResponsePromise) { + actionResponsePromise.then((actionResponse) => { + addAlert(actionResponse); + action(); + }); + } else { + action(); + } + }; + + return { + onClearStats + }; +} diff --git a/src/services/cacheService.ts b/src/services/cacheService.ts index d754ab49d..208027d5d 100644 --- a/src/services/cacheService.ts +++ b/src/services/cacheService.ts @@ -608,4 +608,17 @@ export class CacheService { new Map().set('secured', data['secured']).set('non-secured', data['non-secured']) ); } + + /** + * Clear cache stats + * @param cacheName, the name of the cache + */ + public async clearStats(cacheName: string): Promise { + const clearUrl = this.endpoint + '/caches/' + encodeURIComponent(cacheName) + '?action=stats-reset'; + return this.fetchCaller.post({ + url: clearUrl, + successMessage: `Cache stats ${cacheName} cleared.`, + errorMessage: `Unexpected error when clearing the cache ${cacheName} stats.` + }); + } } diff --git a/src/services/dataContainerService.ts b/src/services/dataContainerService.ts index d6212d6ce..e68474736 100644 --- a/src/services/dataContainerService.ts +++ b/src/services/dataContainerService.ts @@ -38,7 +38,7 @@ export class ContainerService { } private getCacheManager(name: string): Promise> { - let healthPromise: Promise> = this.fetchCaller.get( + const healthPromise: Promise> = this.fetchCaller.get( this.endpoint + '/cache-managers/' + name + '/health', (data) => data.cluster_health.health_status ); @@ -85,10 +85,23 @@ export class ContainerService { * @param name, the name of the cache manager */ public async getCacheManagerStats(name: string): Promise> { - return this.fetchCaller.get( - this.endpoint + '/cache-managers/' + name + '/stats', - (data) => data - ); + return this.fetchCaller.get(this.endpoint + '/cache-managers/' + name + '/stats', (data) => { + const stats = data; + stats.name = name; + return stats; + }); + } + + /** + * Clear cache manager stats + */ + public async clearCacheManagerStats(name: string): Promise { + const clearUrl = this.endpoint + '/cache-managers/' + name + '/stats?action=reset'; + return this.fetchCaller.post({ + url: clearUrl, + successMessage: `Global metrics cleared.`, + errorMessage: `Unexpected error when clearing global metrics.` + }); } /** diff --git a/src/services/securityService.ts b/src/services/securityService.ts index 25bd2e4c4..0feaf631a 100644 --- a/src/services/securityService.ts +++ b/src/services/securityService.ts @@ -83,11 +83,12 @@ export class SecurityService { * Console ACL * @param user */ - public hasConsoleACL(consoleACL: ConsoleACL, user: ConnectedUser): boolean { + public hasConsoleACL(consoleACL: ConsoleACL, user: ConnectedUser | undefined): boolean { if (this.authenticationService.isNotSecured()) { return true; } - if (!user.acl) { + + if (!user || !user.acl) { return false; } const globalAcl = user.acl.global; diff --git a/src/types/InfinispanTypes.ts b/src/types/InfinispanTypes.ts index 58541fd9b..0dfb04c68 100644 --- a/src/types/InfinispanTypes.ts +++ b/src/types/InfinispanTypes.ts @@ -29,6 +29,7 @@ interface ClusterMember { } interface CacheManagerStats { + name: string; statistics_enabled: boolean; time_since_start?: number; time_since_reset?: number;