diff --git a/cypress/e2e/1_rbac_func.cy.js b/cypress/e2e/1_rbac_func.cy.js index dc581f9b..bb309db6 100644 --- a/cypress/e2e/1_rbac_func.cy.js +++ b/cypress/e2e/1_rbac_func.cy.js @@ -11,7 +11,7 @@ describe('RBAC Functionality Tests', () => { checkNotOwnSecuredCache('a-rbac-test-cache'); checkNonSecuredCacheDetailView(true, false); checkMenu(false); - cy.login(monitorUserName, Cypress.env('password'), '/cache/default'); + cy.login(monitorUserName, Cypress.env('password'), '/cache/indexed-cache'); checkNoEntriesTabView(false); cy.login(monitorUserName, Cypress.env('password'), '/global-stats'); checkGlobalStatsView(false) @@ -36,7 +36,7 @@ describe('RBAC Functionality Tests', () => { //Go to tasks (@TODO at the moment for observer no tasks are shown, add after fix) checkSchemasPageView(false); checkCountersPageView(); - cy.login(observerUserName, Cypress.env('password'), '/cache/default'); + cy.login(observerUserName, Cypress.env('password'), '/cache/not-encoded'); checkNoEntriesTabView(false); cy.login(observerUserName, Cypress.env('password'), '/global-stats'); checkGlobalStatsView(false) @@ -55,7 +55,7 @@ describe('RBAC Functionality Tests', () => { //Go to tasks (@TODO at the moment for observer no tasks are shown, add after fix) checkSchemasPageView(false); checkCountersPageView(); - cy.login(applicationUserName, Cypress.env('password'), '/cache/default'); + cy.login(applicationUserName, Cypress.env('password'), '/cache/not-encoded'); checkNoEntriesTabView(false); cy.login(applicationUserName, Cypress.env('password'), '/global-stats'); checkGlobalStatsView(false) @@ -74,7 +74,7 @@ describe('RBAC Functionality Tests', () => { //Go to tasks (@TODO at the moment for observer no tasks are shown, add after fix) checkSchemasPageView(true); checkCountersPageView(); - cy.login(deployerUserName, Cypress.env('password'), '/cache/default'); + cy.login(deployerUserName, Cypress.env('password'), '/cache/not-encoded'); checkNoEntriesTabView(false); cy.login(deployerUserName, Cypress.env('password'), '/global-stats'); checkGlobalStatsView(false) @@ -94,7 +94,7 @@ describe('RBAC Functionality Tests', () => { checkSchemasPageView(true); checkCountersPageView(); checkTasksPage(); - cy.login(Cypress.env('username'), Cypress.env('password'), '/cache/default'); + cy.login(Cypress.env('username'), Cypress.env('password'), '/cache/not-encoded'); checkNoEntriesTabView(true); }); diff --git a/src/app/CacheManagers/CacheTableDisplay.tsx b/src/app/CacheManagers/CacheTableDisplay.tsx index 31dab7b8..256ae522 100644 --- a/src/app/CacheManagers/CacheTableDisplay.tsx +++ b/src/app/CacheManagers/CacheTableDisplay.tsx @@ -760,6 +760,18 @@ const CacheTableDisplay = (props: { setCachesCount: (count: number) => void; isV ); }; + if (loadingCaches) { + return ( + + {t('cache-managers.loading-caches')}} + icon={} + headingLevel="h4" + /> + + ); + } + return ( {!loadingCaches && !rowsLoading && caches.length == 0 ? ( diff --git a/src/app/Caches/CacheMetrics.tsx b/src/app/Caches/CacheMetrics.tsx index b50b2863..f0ab8028 100644 --- a/src/app/Caches/CacheMetrics.tsx +++ b/src/app/Caches/CacheMetrics.tsx @@ -1,10 +1,14 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { + Button, + ButtonVariant, Card, CardBody, CardTitle, EmptyState, + EmptyStateActions, EmptyStateBody, + EmptyStateFooter, EmptyStateHeader, EmptyStateIcon, EmptyStateVariant, @@ -20,7 +24,7 @@ import { TextVariants } from '@patternfly/react-core'; import displayUtils from '@services/displayUtils'; -import { CubesIcon } from '@patternfly/react-icons'; +import { CubesIcon, ExclamationCircleIcon } from '@patternfly/react-icons'; import { QueryMetrics } from '@app/Caches/Query/QueryMetrics'; import { DataDistributionChart } from './DataDistributionChart'; import { PopoverHelp } from '@app/Common/PopoverHelp'; @@ -32,32 +36,24 @@ import { ConsoleServices } from '@services/ConsoleServices'; import { useConnectedUser } from '@app/services/userManagementHook'; import { ConsoleACL } from '@services/securityService'; import { CacheLifecycle } from '@app/Caches/CacheLifecycle'; +import { global_danger_color_200 } from '@patternfly/react-tokens'; +import { Link } from 'react-router-dom'; const CacheMetrics = (props: { cacheName: string; display: boolean }) => { + const { t } = useTranslation(); const { connectedUser } = useConnectedUser(); const { cache, error, loading } = useCacheDetail(); const [stats, setStats] = useState(cache.stats); - const [displayQueryStats, setDisplayQueryStats] = useState(false); - const [displayDataDistribution, setDisplayDataDistribution] = useState(false); - const [memory, setMemory] = useState(undefined); - const { t } = useTranslation(); - const brandname = t('brandname.brandname'); - - useEffect(() => { - if (ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser)) { - // Data distribution is for admin only - setDisplayDataDistribution(true); - const loadMemory = cache.memory; - if (loadMemory) { - setMemory(loadMemory.storage_type == 'OFF_HEAP' ? StorageType.OFF_HEAP : StorageType.HEAP); - } else { - setMemory(StorageType.HEAP); - } + const [displayQueryStats, setDisplayQueryStats] = useState(cache.queryable!); + const [displayDataDistribution, setDisplayDataDistribution] = useState( + ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser) + ); + const memory = () => { + if (cache.memory) { + return cache.memory.storage_type == 'OFF_HEAP' ? StorageType.OFF_HEAP : StorageType.HEAP; } - - setStats(cache.stats); - setDisplayQueryStats(cache.queryable); - }, [cache, error]); + return StorageType.HEAP; + }; const buildOperationsPerformanceCard = () => { if (!stats) { @@ -180,7 +176,7 @@ const CacheMetrics = (props: { cacheName: string; display: boolean }) => { return ''; } let content; - if (memory === StorageType.OFF_HEAP) { + if (memory() === StorageType.OFF_HEAP) { content = ( @@ -242,18 +238,59 @@ const CacheMetrics = (props: { cacheName: string; display: boolean }) => { }; if (!props.display) { - return ; + return <>; + } + + if (loading && error.length == 0) { + return ( + + + + } + /> + + + + ); } - if (!stats || loading) { - return ; + if (error.length > 0) { + return ( + + + + {`An error occurred while retrieving stats ${props.cacheName}`}} + icon={} + headingLevel="h2" + /> + {error} + + + + + + + + + + + ); } - if (!stats.enabled) { + if (stats && !stats.enabled) { return ( {t('caches.cache-metrics.metrics-title')}} + titleText={t('caches.cache-metrics.metrics-title')} icon={} headingLevel="h5" /> @@ -273,8 +310,8 @@ const CacheMetrics = (props: { cacheName: string; display: boolean }) => { {buildOperationsPerformanceCard()} {displayDataDistribution && {buildDataDistribution()}} - - + + {buildQueryStats()} diff --git a/src/app/Caches/DetailCache.tsx b/src/app/Caches/DetailCache.tsx index a1deab31..ae3af95a 100644 --- a/src/app/Caches/DetailCache.tsx +++ b/src/app/Caches/DetailCache.tsx @@ -63,6 +63,7 @@ import { ThemeContext } from '@app/providers/ThemeProvider'; import { useNavigate } from 'react-router'; import { TracingEnabled } from '@app/Common/TracingEnabled'; import { AlertIcon } from '@patternfly/react-core/dist/js/components/Alert/AlertIcon'; +import { Health } from '@app/Common/Health'; const DetailCache = (props: { cacheName: string }) => { const cacheName = props.cacheName; @@ -84,7 +85,11 @@ const DetailCache = (props: { cacheName: string }) => { return; } - if (cache.editable && ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cacheName, connectedUser)) { + if ( + cache.started && + cache.editable && + ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cacheName, connectedUser) + ) { setActiveTabKey1(0); } else if (ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser)) { setActiveTabKey1(1); @@ -94,7 +99,7 @@ const DetailCache = (props: { cacheName: string }) => { }, [cache]); const buildEntriesTabContent = () => { - if (!ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cacheName, connectedUser)) { + if (cache.started && !ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cacheName, connectedUser)) { return ''; } @@ -127,7 +132,7 @@ const DetailCache = (props: { cacheName: string }) => { }; const entriesTabEnabled = (): boolean => { - return cache.editable && ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cacheName, connectedUser); + return cache.editable! && ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cacheName, connectedUser); }; const buildDetailContent = () => { @@ -177,7 +182,11 @@ const DetailCache = (props: { cacheName: string }) => { if (activeTabKey1 == 1) { return ( cache.configuration && ( - + ) ); } @@ -208,11 +217,15 @@ const DetailCache = (props: { cacheName: string }) => { }; const displayBackupsManagement = () => { - return cache?.features.hasRemoteBackup && ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser); + return ( + cache && + cache?.features?.hasRemoteBackup && + ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser) + ); }; const displayIndexManage = () => { - return cache?.features.indexed; + return cache && cache?.features?.indexed; }; const buildBackupsManage = () => { @@ -266,7 +279,7 @@ const DetailCache = (props: { cacheName: string }) => { }; const buildTracing = () => { - if (!cacheManager || !cacheManager.tracing_enabled) return; + if (!cacheManager || !cacheManager.tracing_enabled || !cache || !cache.started) return; return ( { {cacheManager.tracing_enabled && ( - + @@ -464,6 +477,36 @@ const DetailCache = (props: { cacheName: string }) => { ); } + if (!cache.started) { + // cache is not ok + return ( + + + + + + + + + {cache.name} + + + + + + + + + {displayActions} + + + + {displayConfiguration()} + + + ); + } + return ( @@ -475,7 +518,7 @@ const DetailCache = (props: { cacheName: string }) => { - + {buildFeaturesChip()} diff --git a/src/app/Caches/Entries/CacheEntries.tsx b/src/app/Caches/Entries/CacheEntries.tsx index 36e78c46..60050118 100644 --- a/src/app/Caches/Entries/CacheEntries.tsx +++ b/src/app/Caches/Entries/CacheEntries.tsx @@ -81,23 +81,23 @@ const CacheEntries = () => { const { syntaxHighLighterTheme } = useContext(ThemeContext); useEffect(() => { - if (cache.encoding.key == EncodingType.Protobuf) { + if (cache.encoding?.key == EncodingType.Protobuf) { setSelectSearchOption(ContentType.string); setKeyContentTypeToEdit(ContentType.string); } else if ( - cache.encoding.key == EncodingType.Java || - cache.encoding.key == EncodingType.JBoss || - cache.encoding.key == EncodingType.JavaSerialized + cache.encoding?.key == EncodingType.Java || + cache.encoding?.key == EncodingType.JBoss || + cache.encoding?.key == EncodingType.JavaSerialized ) { setSelectSearchOption(ContentType.StringContentType); setKeyContentTypeToEdit(ContentType.StringContentType); - } else if (cache.encoding.key == EncodingType.XML) { + } else if (cache.encoding?.key == EncodingType.XML) { setSelectSearchOption(ContentType.XML); setKeyContentTypeToEdit(ContentType.XML); - } else if (cache.encoding.key == EncodingType.JSON) { + } else if (cache.encoding?.key == EncodingType.JSON) { setSelectSearchOption(ContentType.JSON); setSelectSearchOption(ContentType.JSON); - } else if (cache.encoding.key == EncodingType.Text) { + } else if (cache.encoding?.key == EncodingType.Text) { setSelectSearchOption(ContentType.StringContentType); setSelectSearchOption(ContentType.StringContentType); } @@ -284,7 +284,7 @@ const CacheEntries = () => { icon={} headingLevel="h4" /> - {infoEntries ? infoEntries : t('caches.entries.empty-cache-body')} + {infoEntries ? t(infoEntries) : t('caches.entries.empty-cache-body')} {addEntryAction()} @@ -310,7 +310,7 @@ const CacheEntries = () => { }; const keyContentTypeOptions = (): SelectOptionProps[] => { - return selectOptionPropsFromArray(CacheConfigUtils.getContentTypeOptions(cache.encoding.key as EncodingType)); + return selectOptionPropsFromArray(CacheConfigUtils.getContentTypeOptions(cache.encoding?.key as EncodingType)); }; const searchEntryByKey = () => { @@ -398,8 +398,8 @@ const CacheEntries = () => { if (!ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cache.name, connectedUser)) { return ''; } - const encodingKey = CacheConfigUtils.toEncoding(cache.encoding.key); - const encodingValue = CacheConfigUtils.toEncoding(cache.encoding.value); + const encodingKey = CacheConfigUtils.toEncoding(cache.encoding?.key); + const encodingValue = CacheConfigUtils.toEncoding(cache.encoding?.value); if ( encodingKey == EncodingType.Java || encodingKey == EncodingType.JavaSerialized || @@ -435,6 +435,10 @@ const CacheEntries = () => { return ''; }; + if (!cache.started) { + // Don't display anything if the cache is not started + return <>; + } return ( {encodingMessageDisplay()} @@ -511,14 +515,14 @@ const CacheEntries = () => { {displayHighlighted( row.key, - cache.encoding.key as EncodingType, + cache.encoding?.key as EncodingType, row.keyContentType as ContentType )} {displayHighlighted( row.value, - cache.encoding.value as EncodingType, + cache.encoding?.value as EncodingType, row.valueContentType as ContentType )} @@ -539,7 +543,7 @@ const CacheEntries = () => { )} { /> { - const health = ComponentHealth[props.health]; +const Health = (props: { health?: string; displayIcon?: boolean; cacheName?: string }) => { + const health = props.health ? ComponentHealth[props.health] : ComponentHealth.UNKNOWN; const displayIcon = props.displayIcon == undefined ? true : props.displayIcon; const { theme } = useContext(ThemeContext); diff --git a/src/app/assets/languages/en.json b/src/app/assets/languages/en.json index d42dfc9c..9aada459 100644 --- a/src/app/assets/languages/en.json +++ b/src/app/assets/languages/en.json @@ -11,6 +11,7 @@ "security-realms-docs-link": "https://infinispan.org/docs/stable/titles/security/security.html#security-realms" }, "common": { + "loading": "Loading...", "actions": { "actions": "Actions", "refresh": "Refresh", @@ -575,6 +576,9 @@ "back": "Back" }, "entries": { + "read-error": "Connected user lacks BULK_READ permission to browse the cache content.", + "read-error-unknown-type": "This cache contains entries that can not be read or edited from the Console.", + "read-error-spring-session": "This cache contains Spring Session entries that can not be read or edited from the Console.", "action-edit": "Edit", "action-delete": "Delete", "action-enter": "Enter", diff --git a/src/app/providers/CacheDetailProvider.tsx b/src/app/providers/CacheDetailProvider.tsx index cb8feb01..5242b6f3 100644 --- a/src/app/providers/CacheDetailProvider.tsx +++ b/src/app/providers/CacheDetailProvider.tsx @@ -70,23 +70,63 @@ const CacheDetailProvider = ({ children }) => { if (eitherDetail.isRight()) { setCache(eitherDetail.value); } else { - setError(eitherDetail.value.message); + // Cache can be unhealthy but existing + ConsoleServices.caches() + .retrieveHealth(cacheName) + .then((eitherHealth) => { + if (eitherHealth.isRight()) { + // We have the health. Get the config + return ConsoleServices.caches() + .retrieveConfig(cacheName) + .then((eitherConfig) => { + if (eitherConfig.isRight()) { + const detail: DetailedInfinispanCache = { + name: cacheName, + configuration: eitherConfig.value, + health: eitherHealth.value, + started: false + }; + setCache(detail); + // we are good; + return ''; + } else { + // return the error + return eitherConfig.value.message; + } + }) + .finally(() => { + // loading is over here + setLoading(false); + }); + // we are good + return ''; + } else { + // return the error + return eitherHealth.value.message; + } + }) + .then((error) => { + if (error.length > 0) { + setError(error); + setLoading(false); + } + }); } }) .finally(() => { - setLoading(false); - isEncodingAvailable(cache) && setLoadingEntries(true); + setLoadingEntries(isEncodingAvailable(cache)); }); } else { setError(maybeCm.value.message); } - }); + }) + .finally(() => setLoading(false)); } }; const fetchEntry = (keyToSearch: string, kct: ContentType) => { ConsoleServices.caches() - .getEntry(cacheName, cache.encoding, keyToSearch, kct) + .getEntry(cacheName, cache.encoding!, keyToSearch, kct) .then((response) => { let entries: CacheEntry[] = []; if (response.isRight()) { @@ -103,7 +143,7 @@ const CacheDetailProvider = ({ children }) => { if (ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.BULK_READ, cacheName, connectedUser)) { if (cache) { ConsoleServices.caches() - .getEntries(cacheName, cache.encoding, limit) + .getEntries(cacheName, cache.encoding!, limit) .then((eitherEntries) => { if (eitherEntries.isRight()) { setCacheEntries(eitherEntries.value); @@ -124,7 +164,7 @@ const CacheDetailProvider = ({ children }) => { } } else { setLoadingEntries(false); - setInfoEntries('Connected user lacks BULK_READ permission to browse the cache content.'); + setInfoEntries('caches.entries.read-error'); } } }; diff --git a/src/app/utils/encodingUtils.ts b/src/app/utils/encodingUtils.ts index 023fa810..f6290c76 100644 --- a/src/app/utils/encodingUtils.ts +++ b/src/app/utils/encodingUtils.ts @@ -1,5 +1,8 @@ import { EncodingType } from '@services/infinispanRefData'; export function isEncodingAvailable(cache: DetailedInfinispanCache): boolean { - return cache?.encoding?.key !== EncodingType.Empty || cache?.encoding?.value !== EncodingType.Empty; + return ( + cache?.encoding !== undefined && + (cache?.encoding?.key !== EncodingType.Empty || cache?.encoding?.value !== EncodingType.Empty) + ); } diff --git a/src/services/cacheService.ts b/src/services/cacheService.ts index e04c9113..53e243a0 100644 --- a/src/services/cacheService.ts +++ b/src/services/cacheService.ts @@ -19,6 +19,36 @@ export class CacheService { this.fetchCaller = fetchCaller; } + /** + * Retrieve cache health + * + * @param cacheName + */ + public retrieveHealth(cacheName: string): Promise> { + return this.fetchCaller.get( + this.endpoint + '/caches/' + encodeURIComponent(cacheName) + '?action=health', + (data) => data, + undefined, + true + ); + } + + /** + * Retrieve cache config + * + * @param cacheName + */ + public retrieveConfig(cacheName: string): Promise> { + return this.fetchCaller.get( + this.endpoint + '/caches/' + encodeURIComponent(cacheName) + '?action=config', + (data) => + { + name: cacheName, + config: JSON.stringify(data, null, 2) + } + ); + } + /** * Retrieves all the properties to be displayed in the cache detail in a single rest call * diff --git a/src/services/fetchCaller.ts b/src/services/fetchCaller.ts index 9f993ddd..c4173716 100644 --- a/src/services/fetchCaller.ts +++ b/src/services/fetchCaller.ts @@ -194,10 +194,10 @@ export class FetchCaller { if (text.includes("missing type id property '_type'")) { message = "You are trying to write a JSON key or value that needs '_type' field in this cache."; } else if (text.includes('Unknown type id : 5901')) { - message = 'This cache contains Spring Session entries that can not be read or edited from the Console.'; + message = 'caches.entries.read-error-spring-session'; success = true; } else if (text.includes('Unknown type id')) { - message = 'This cache contains entries that can not be read or edited from the Console.'; + message = 'caches.entries.read-error-unknown-type'; success = true; } else if (text != '') { message = errorMessage + '\n' + text; diff --git a/src/services/infinispanRefData.ts b/src/services/infinispanRefData.ts index ad8b1dce..1fb7de87 100644 --- a/src/services/infinispanRefData.ts +++ b/src/services/infinispanRefData.ts @@ -13,7 +13,8 @@ export enum ComponentHealth { HEALTHY = 'HEALTHY', HEALTHY_REBALANCING = 'HEALTHY_REBALANCING', DEGRADED = 'DEGRADED', - FAILED = 'FAILED' + FAILED = 'FAILED', + UNKNOWN = 'UNKNOWN' } /** * Cache configuration utils class diff --git a/src/types/InfinispanTypes.ts b/src/types/InfinispanTypes.ts index e4a31602..adc305a9 100644 --- a/src/types/InfinispanTypes.ts +++ b/src/types/InfinispanTypes.ts @@ -121,24 +121,25 @@ interface CacheEncoding { interface DetailedInfinispanCache { name: string; configuration?: CacheConfig; - encoding: CacheEncoding; - type: string; + encoding?: CacheEncoding; + type?: string; started: boolean; + health?: string; size?: number; rehash_in_progress?: boolean; indexing_in_progress?: boolean; rebalancing_enabled?: boolean; - editable: boolean; - updateEntry: boolean; - deleteEntry: boolean; - queryable: boolean; - features: Features; + editable?: boolean; + updateEntry?: boolean; + deleteEntry?: boolean; + queryable?: boolean; + features?: Features; backupSites?: [XSite]; stats?: CacheStats; - mode: string; + mode?: string; memory?: CacheMemory; - async: boolean; - tracing: boolean; + async?: boolean; + tracing?: boolean; } interface CacheMemory {