From d017afcf0ea2de3828a1b6df49e4283f435506b8 Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Thu, 19 Sep 2024 09:50:22 +0200 Subject: [PATCH] ISPN-16412 View failed caches config --- cypress/e2e/1_rbac_func.cy.js | 10 +- src/app/CacheManagers/CacheTableDisplay.tsx | 12 ++ src/app/Caches/CacheMetrics.tsx | 98 +++++++--- src/app/Caches/Create/FeaturesSelector.tsx | 1 - src/app/Caches/DetailCache.tsx | 179 +++++++++--------- src/app/Caches/Entries/CacheEntries.tsx | 117 +++++++++--- src/app/Common/Health.tsx | 4 +- src/app/Common/SelectMultiWithChips.tsx | 7 +- src/app/ConnectedClients/ConnectedClients.tsx | 33 +++- src/app/assets/languages/en.json | 4 + src/app/providers/CacheDetailProvider.tsx | 54 +++++- src/app/utils/encodingUtils.ts | 5 +- src/app/utils/getLanguage.ts | 6 +- src/services/cacheService.ts | 30 +++ src/services/fetchCaller.ts | 4 +- src/services/infinispanRefData.ts | 3 +- src/types/InfinispanTypes.ts | 17 +- 17 files changed, 394 insertions(+), 190 deletions(-) diff --git a/cypress/e2e/1_rbac_func.cy.js b/cypress/e2e/1_rbac_func.cy.js index dc581f9b3..bb309db68 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 a3cd283ae..1d5e58f5e 100644 --- a/src/app/CacheManagers/CacheTableDisplay.tsx +++ b/src/app/CacheManagers/CacheTableDisplay.tsx @@ -758,6 +758,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 6469c6881..f0ab80286 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,33 +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); - const loadQueryStats = cache.stats != undefined && cache.stats.enabled && cache.features.indexed; - setDisplayQueryStats(loadQueryStats); - }, [cache, error]); + return StorageType.HEAP; + }; const buildOperationsPerformanceCard = () => { if (!stats) { @@ -181,7 +176,7 @@ const CacheMetrics = (props: { cacheName: string; display: boolean }) => { return ''; } let content; - if (memory === StorageType.OFF_HEAP) { + if (memory() === StorageType.OFF_HEAP) { content = ( @@ -243,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" /> @@ -274,8 +310,8 @@ const CacheMetrics = (props: { cacheName: string; display: boolean }) => { {buildOperationsPerformanceCard()} {displayDataDistribution && {buildDataDistribution()}} - - + + {buildQueryStats()} diff --git a/src/app/Caches/Create/FeaturesSelector.tsx b/src/app/Caches/Create/FeaturesSelector.tsx index 65a31ddeb..3617952d9 100644 --- a/src/app/Caches/Create/FeaturesSelector.tsx +++ b/src/app/Caches/Create/FeaturesSelector.tsx @@ -18,7 +18,6 @@ import { SelectMultiWithChips } from '@app/Common/SelectMultiWithChips'; import { selectOptionProps } from '@utils/selectOptionPropsCreator'; import { useDataContainer } from '@app/services/dataContainerHooks'; - const FeaturesSelector = () => { const { t } = useTranslation(); const { notSecured, connectedUser } = useConnectedUser(); diff --git a/src/app/Caches/DetailCache.tsx b/src/app/Caches/DetailCache.tsx index 338be3500..8209a5dd6 100644 --- a/src/app/Caches/DetailCache.tsx +++ b/src/app/Caches/DetailCache.tsx @@ -1,8 +1,6 @@ import * as React from 'react'; import { useContext, useEffect, useState } from 'react'; import { - Alert, - AlertActionLink, AlertVariant, Button, ButtonVariant, @@ -19,6 +17,8 @@ import { EmptyStateHeader, EmptyStateIcon, EmptyStateVariant, + Flex, + FlexItem, Label, LabelGroup, MenuToggle, @@ -44,8 +44,12 @@ import { CacheEntries } from '@app/Caches/Entries/CacheEntries'; 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, InfoCircleIcon, InfoIcon, RedoIcon } from '@patternfly/react-icons'; +import { + global_BackgroundColor_100, + global_danger_color_200, + global_warning_color_100 +} from '@patternfly/react-tokens'; +import { ExclamationCircleIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons'; import { QueryEntries } from '@app/Caches/Query/QueryEntries'; import { Link } from 'react-router-dom'; import { useCacheDetail } from '@app/services/cachesHook'; @@ -54,18 +58,16 @@ import { ConsoleACL } from '@services/securityService'; import { useConnectedUser } from '@app/services/userManagementHook'; import { useTranslation } from 'react-i18next'; 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'; +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; const { t } = useTranslation(); const navigate = useNavigate(); const { theme } = useContext(ThemeContext); - const brandname = t('brandname.brandname'); - const encodingDocs = t('brandname.encoding-docs-link'); const { connectedUser } = useConnectedUser(); const { loading, error, cache, loadCache } = useCacheDetail(); const [activeTabKey1, setActiveTabKey1] = useState(''); @@ -81,7 +83,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); @@ -90,53 +96,15 @@ const DetailCache = (props: { cacheName: string }) => { } }, [cache]); - const encodingMessageDisplay = () => { - if (!ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cacheName, connectedUser)) { - return ''; - } - const encodingKey = CacheConfigUtils.toEncoding(cache.encoding.key); - const encodingValue = CacheConfigUtils.toEncoding(cache.encoding.value); - if ( - encodingKey == EncodingType.Java || - encodingKey == EncodingType.JBoss || - encodingValue == EncodingType.Java || - encodingValue == EncodingType.JBoss - ) { - return ( - - - window.open(encodingDocs, '_blank')}> - {t('caches.configuration.encoding-docs-message')} - - } - /> - - - ); - } - return ''; - }; - - const buildEntriesTabContent = (queryable: boolean) => { + const buildEntriesTabContent = () => { if (!ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cacheName, connectedUser)) { return ''; } - if (!queryable) { + if (!cache?.queryable) { return ( - {encodingMessageDisplay()} - + ); } @@ -156,18 +124,21 @@ const DetailCache = (props: { cacheName: string }) => { title={{t('caches.tabs.entries-manage')}} data-cy="manageEntriesTab" > - {encodingMessageDisplay()} - + {t('caches.tabs.query-values')}}> - setActiveTabKey1(2)} /> + setActiveTabKey1(2)} + /> ); }; 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 = () => { @@ -211,48 +182,33 @@ const DetailCache = (props: { cacheName: string }) => { } if (activeTabKey1 == 0) { - return buildEntriesTabContent(cache.queryable); + return buildEntriesTabContent(); } if (activeTabKey1 == 1) { return ( cache.configuration && ( - + ) ); } return ; - - return ( - - {`Empty ${cacheName}`}} - icon={} - headingLevel="h2" - /> - {error} - - - - - - - - - ); }; 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 = () => { @@ -263,7 +219,7 @@ const DetailCache = (props: { cacheName: string }) => { value={'backupsManage'} key="manageBackupsLink" data-cy="manageBackupsLink" - onClick={(ev: any) => + onClick={() => navigate({ pathname: '/cache/' + encodeURIComponent(cacheName) + '/backups', search: location.search @@ -281,10 +237,27 @@ const DetailCache = (props: { cacheName: string }) => { } return ( - - - - + + + + + + + + + {t('caches.rebuilding-index')} + + + + + + ); }; @@ -295,7 +268,7 @@ const DetailCache = (props: { cacheName: string }) => { value={'indexManage'} key="manageIndexesLink" data-cy="manageIndexesLink" - onClick={(ev: any) => + onClick={() => navigate({ pathname: '/cache/' + encodeURIComponent(cacheName) + '/indexing', search: location.search @@ -458,6 +431,36 @@ const DetailCache = (props: { cacheName: string }) => { ); } + if (!cache.started) { + // cache is not ok + return ( + + + + + + + + + {cache.name} + + + + + + + + + {displayActions} + + + + {displayConfiguration()} + + + ); + } + return ( @@ -469,7 +472,7 @@ const DetailCache = (props: { cacheName: string }) => { - + {displayActions} diff --git a/src/app/Caches/Entries/CacheEntries.tsx b/src/app/Caches/Entries/CacheEntries.tsx index d7e79bc9d..60050118d 100644 --- a/src/app/Caches/Entries/CacheEntries.tsx +++ b/src/app/Caches/Entries/CacheEntries.tsx @@ -1,5 +1,8 @@ import React, { useContext, useEffect, useState } from 'react'; import { + Alert, + AlertActionLink, + AlertVariant, Bullseye, Button, ButtonVariant, @@ -24,7 +27,7 @@ import { ToolbarToggleGroup, Tooltip } from '@patternfly/react-core'; -import { ActionsColumn, IAction, Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { ActionsColumn, Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import { FilterIcon, HelpIcon, PlusCircleIcon, SearchIcon } from '@patternfly/react-icons'; import { global_spacer_md, global_spacer_sm } from '@patternfly/react-tokens'; import SyntaxHighlighter from 'react-syntax-highlighter'; @@ -35,7 +38,7 @@ import { ConsoleServices } from '@services/ConsoleServices'; import { useConnectedUser } from '@app/services/userManagementHook'; import { ConsoleACL } from '@services/securityService'; import { CacheConfigUtils } from '@services/cacheConfigUtils'; -import { ContentType, EncodingType, StorageType } from '@services/infinispanRefData'; +import { ContentType, EncodingType } from '@services/infinispanRefData'; import { CreateOrUpdateEntryForm } from '@app/Caches/Entries/CreateOrUpdateEntryForm'; import { ClearAllEntries } from '@app/Caches/Entries/ClearAllEntries'; import { DeleteEntry } from '@app/Caches/Entries/DeleteEntry'; @@ -43,7 +46,7 @@ import { ThemeContext } from '@app/providers/ThemeProvider'; import { SelectSingle } from '@app/Common/SelectSingle'; import { selectOptionProps, selectOptionPropsFromArray } from '@utils/selectOptionPropsCreator'; -const CacheEntries = (props: { cacheName: string }) => { +const CacheEntries = () => { const { cacheEntries, totalEntriesCount, @@ -59,6 +62,7 @@ const CacheEntries = (props: { cacheName: string }) => { const { connectedUser } = useConnectedUser(); const { t } = useTranslation(); const brandname = t('brandname.brandname'); + const encodingDocs = t('brandname.encoding-docs-link'); const [isCreateOrUpdateEntryFormOpen, setCreateOrUpdateEntryFormOpen] = useState(false); const [isDeleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false); const [keyToDelete, setKeyToDelete] = useState(''); @@ -77,23 +81,23 @@ const CacheEntries = (props: { cacheName: string }) => { 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); } @@ -109,7 +113,7 @@ const CacheEntries = (props: { cacheName: string }) => { if (filteredEntries) { const initSlice = (entriesPagination.page - 1) * entriesPagination.perPage; const updateRows = filteredEntries.slice(initSlice, initSlice + entriesPagination.perPage); - updateRows.length > 0 ? setRows(updateRows) : setRows([]); + setRows(updateRows); } }, [entriesPagination, filteredEntries]); @@ -138,18 +142,29 @@ const CacheEntries = (props: { cacheName: string }) => { return ; } - const actions = [ - { + const actions = []; + if (cache.updateEntry) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + actions.push({ 'aria-label': 'editEntryAction', title: t('caches.entries.action-edit'), onClick: () => onClickEditEntryButton(row.key, row.keyContentType as ContentType) - }, - { + }); + } + if (cache.deleteEntry) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + actions.push({ 'aria-label': 'deleteEntryAction', title: t('caches.entries.action-delete'), onClick: () => onClickDeleteEntryButton(row.key, row.keyContentType as ContentType) - } - ]; + }); + } + + if (actions.length == 0) { + return undefined; + } return ( @@ -269,7 +284,7 @@ const CacheEntries = (props: { cacheName: string }) => { icon={} headingLevel="h4" /> - {infoEntries ? infoEntries : t('caches.entries.empty-cache-body')} + {infoEntries ? t(infoEntries) : t('caches.entries.empty-cache-body')} {addEntryAction()} @@ -295,7 +310,7 @@ const CacheEntries = (props: { cacheName: string }) => { }; const keyContentTypeOptions = (): SelectOptionProps[] => { - return selectOptionPropsFromArray(CacheConfigUtils.getContentTypeOptions(cache.encoding.key as EncodingType)); + return selectOptionPropsFromArray(CacheConfigUtils.getContentTypeOptions(cache.encoding?.key as EncodingType)); }; const searchEntryByKey = () => { @@ -379,8 +394,54 @@ const CacheEntries = (props: { cacheName: string }) => { return entry.expires ? entry.expires : t('caches.entries.never-expire'); }; + const encodingMessageDisplay = () => { + if (!ConsoleServices.security().hasCacheConsoleACL(ConsoleACL.READ, cache.name, connectedUser)) { + return ''; + } + const encodingKey = CacheConfigUtils.toEncoding(cache.encoding?.key); + const encodingValue = CacheConfigUtils.toEncoding(cache.encoding?.value); + if ( + encodingKey == EncodingType.Java || + encodingKey == EncodingType.JavaSerialized || + encodingKey == EncodingType.JBoss || + encodingKey == EncodingType.Octet || + encodingValue == EncodingType.Java || + encodingValue == EncodingType.JavaSerialized || + encodingValue == EncodingType.JBoss || + encodingValue == EncodingType.Octet + ) { + return ( + + + window.open(encodingDocs, '_blank')}> + {t('caches.configuration.encoding-docs-message')} + + } + /> + + + ); + } + return ''; + }; + + if (!cache.started) { + // Don't display anything if the cache is not started + return <>; + } return ( + {encodingMessageDisplay()} {totalEntriesCount == 0 ? ( emptyPage ) : ( @@ -454,14 +515,14 @@ const CacheEntries = (props: { cacheName: string }) => { {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 )} @@ -481,26 +542,22 @@ const CacheEntries = (props: { cacheName: string }) => { )} - + ); }; diff --git a/src/app/Common/Health.tsx b/src/app/Common/Health.tsx index 75088ba45..2106c1970 100644 --- a/src/app/Common/Health.tsx +++ b/src/app/Common/Health.tsx @@ -6,8 +6,8 @@ import { ComponentHealth } from '@services/infinispanRefData'; import { ThemeContext } from '@app/providers/ThemeProvider'; import { chart_global_label_Fill, global_Color_light_100 } from '@patternfly/react-tokens'; -const Health = (props: { health: string; displayIcon?: boolean; cacheName?: string }) => { - 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/Common/SelectMultiWithChips.tsx b/src/app/Common/SelectMultiWithChips.tsx index b92c739a8..20d797fae 100644 --- a/src/app/Common/SelectMultiWithChips.tsx +++ b/src/app/Common/SelectMultiWithChips.tsx @@ -49,8 +49,10 @@ const SelectMultiWithChips = (props: { ); // When no options are found after filtering, display creation option - if (!newSelectOptions.length) { - newSelectOptions = [{ isDisabled: false, children: `Create new option "${inputValue}"`, value: 'create' }]; + if (!newSelectOptions.length && props.create) { + newSelectOptions = [{ isDisabled: false, children: `Create "${inputValue}"`, value: 'create' }]; + } else if (!newSelectOptions.length) { + newSelectOptions = [{ isDisabled: false, children: 'no results', value: 'no results' }]; } // Open the menu when the input value changes and the new value is not empty @@ -104,6 +106,7 @@ const SelectMultiWithChips = (props: { setIsOpen((prevIsOpen) => !prevIsOpen); } else if (isOpen && focusedItem.value !== 'no results') { onSelect(focusedItem.value as string); + setInputValue(''); } break; case 'Tab': diff --git a/src/app/ConnectedClients/ConnectedClients.tsx b/src/app/ConnectedClients/ConnectedClients.tsx index e577afbdf..ce379ee7f 100644 --- a/src/app/ConnectedClients/ConnectedClients.tsx +++ b/src/app/ConnectedClients/ConnectedClients.tsx @@ -29,7 +29,12 @@ import { ToolbarItem, ToolbarItemVariant, ToolbarGroup, - EmptyStateHeader, Dropdown, MenuToggleElement, MenuToggle, DropdownList, DropdownItem + EmptyStateHeader, + Dropdown, + MenuToggleElement, + MenuToggle, + DropdownList, + DropdownItem } from '@patternfly/react-core'; import { CubesIcon, SearchIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons'; import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; @@ -241,13 +246,17 @@ const ConnectedClients = () => { {t('connected-clients.client-version')} - + {t('connected-clients.local-address')} - + @@ -289,11 +298,9 @@ const ConnectedClients = () => { if (connectedClients.length === 0) { return ( - - {emptyPage} - + {emptyPage} - ) + ); } return ( @@ -365,8 +372,8 @@ const ConnectedClients = () => { - ) - } + ); + }; const displayActions = ( @@ -384,7 +391,13 @@ const ConnectedClients = () => { shouldFocusToggleOnSelect > - setLoading(true)} icon={}> + setLoading(true)} + icon={} + > {t('common.actions.refresh')} diff --git a/src/app/assets/languages/en.json b/src/app/assets/languages/en.json index f44b85de3..cd8368d93 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", @@ -554,6 +555,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 cb8feb010..5242b6f30 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 023fa810b..f6290c764 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/app/utils/getLanguage.ts b/src/app/utils/getLanguage.ts index d9e4304c9..5f64c23f2 100644 --- a/src/app/utils/getLanguage.ts +++ b/src/app/utils/getLanguage.ts @@ -2,15 +2,15 @@ import { ConfigDownloadType } from '@services/infinispanRefData'; import { Language } from '@patternfly/react-code-editor'; export const toCodeEditorLanguage = (lang: ConfigDownloadType) => { - if (ConfigDownloadType.JSON == lang ) { + if (ConfigDownloadType.JSON == lang) { return Language.json; } - if (ConfigDownloadType.YAML == lang ) { + if (ConfigDownloadType.YAML == lang) { return Language.yaml; } - if (ConfigDownloadType.XML == lang ) { + if (ConfigDownloadType.XML == lang) { return Language.xml; } diff --git a/src/services/cacheService.ts b/src/services/cacheService.ts index 09a189188..7b4e137de 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 9f993ddd2..c4173716f 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 d51c7c681..44e1ff029 100644 --- a/src/services/infinispanRefData.ts +++ b/src/services/infinispanRefData.ts @@ -11,7 +11,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 cc5949c98..2928a6693 100644 --- a/src/types/InfinispanTypes.ts +++ b/src/types/InfinispanTypes.ts @@ -119,21 +119,24 @@ 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; - 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; + async?: boolean; } interface CacheMemory {