diff --git a/src/app/CacheManagers/CacheTableDisplay.tsx b/src/app/CacheManagers/CacheTableDisplay.tsx index a3cd283a..15a2fb6a 100644 --- a/src/app/CacheManagers/CacheTableDisplay.tsx +++ b/src/app/CacheManagers/CacheTableDisplay.tsx @@ -148,7 +148,7 @@ const CacheTableDisplay = (props: { setCachesCount: (count: number) => void; isV if (filteredCaches) { const initSlice = (cachesPagination.page - 1) * cachesPagination.perPage; const updateRows = filteredCaches.slice(initSlice, initSlice + cachesPagination.perPage); - updateRows.length > 0 ? setRows(updateRows) : setRows([]); + setRows(updateRows); } }, [cachesPagination, filteredCaches]); @@ -218,7 +218,9 @@ const CacheTableDisplay = (props: { setCachesCount: (count: number) => void; isV setTimeout(() => { if (menuRef.current) { const firstElement = menuRef.current.querySelector('li > button:not(:disabled)'); - firstElement && (firstElement as HTMLElement).focus(); + if (firstElement) { + (firstElement as HTMLElement).focus() + } } }, 0); setIsFilterOpen(!isFilterOpen); diff --git a/src/app/CacheManagers/CounterTableDisplay.tsx b/src/app/CacheManagers/CounterTableDisplay.tsx index 08ab570a..ba6c3422 100644 --- a/src/app/CacheManagers/CounterTableDisplay.tsx +++ b/src/app/CacheManagers/CounterTableDisplay.tsx @@ -97,7 +97,7 @@ const CounterTableDisplay = (props: { setCountersCount: (number) => void; isVisi if (filteredCounters) { const initSlice = (countersPagination.page - 1) * countersPagination.perPage; const updateRows = filteredCounters.slice(initSlice, initSlice + countersPagination.perPage); - updateRows.length > 0 ? setRows(updateRows) : setRows([]); + setRows(updateRows); } }, [countersPagination, filteredCounters]); @@ -250,7 +250,9 @@ const CounterTableDisplay = (props: { setCountersCount: (number) => void; isVisi setTimeout(() => { if (menuRef.current) { const firstElement = menuRef.current.querySelector('li > button:not(:disabled)'); - firstElement && (firstElement as HTMLElement).focus(); + if (firstElement) { + (firstElement as HTMLElement).focus(); + } } }, 0); setIsFilterOpen(!isFilterOpen); diff --git a/src/app/Caches/Entries/CacheEntries.tsx b/src/app/Caches/Entries/CacheEntries.tsx index d7e79bc9..85b2250c 100644 --- a/src/app/Caches/Entries/CacheEntries.tsx +++ b/src/app/Caches/Entries/CacheEntries.tsx @@ -109,7 +109,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]); diff --git a/src/app/Common/TableEmptyState.tsx b/src/app/Common/TableEmptyState.tsx index b12829ab..5bac44e1 100644 --- a/src/app/Common/TableEmptyState.tsx +++ b/src/app/Common/TableEmptyState.tsx @@ -39,7 +39,7 @@ const TableEmptyState = (props: { loading: boolean; error: string; empty: string } return ( - + {props.empty}} diff --git a/src/app/IndexManagement/IndexManagement.tsx b/src/app/IndexManagement/IndexManagement.tsx index 5032ac28..60daeb9e 100644 --- a/src/app/IndexManagement/IndexManagement.tsx +++ b/src/app/IndexManagement/IndexManagement.tsx @@ -2,9 +2,11 @@ import * as React from 'react'; import { useState } from 'react'; import { Button, - ButtonVariant, - Card, - CardBody, + ButtonVariant, Card, CardBody, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + EmptyStateVariant, Grid, GridItem, PageSection, @@ -18,16 +20,11 @@ import { TextListVariants, TextVariants, Toolbar, - ToolbarItem, ToolbarContent, - EmptyState, - EmptyStateVariant, - EmptyStateIcon, - EmptyStateBody + ToolbarItem } from '@patternfly/react-core'; import { Link, useParams } from 'react-router-dom'; import { global_spacer_md } from '@patternfly/react-tokens'; -import { useApiAlert } from '@app/utils/useApiAlert'; import { DataContainerBreadcrumb } from '@app/Common/DataContainerBreadcrumb'; import { TableErrorState } from '@app/Common/TableErrorState'; import { PurgeIndex } from '@app/IndexManagement/PurgeIndex'; @@ -39,16 +36,19 @@ import { useConnectedUser } from '@app/services/userManagementHook'; import { useSearchStats } from '@app/services/statsHook'; import { DatabaseIcon } from '@patternfly/react-icons'; import { UpdateSchema } from '@app/IndexManagement/UpdateSchema'; +import { useIndexMetamodel } from '@app/services/searchHook'; +import { ViewMetamodel } from '@app/IndexManagement/ViewMetamodel'; const IndexManagement = () => { const { t } = useTranslation(); - const { addAlert } = useApiAlert(); const { connectedUser } = useConnectedUser(); const cacheName = useParams()['cacheName'] as string; const { stats, loading, error, setLoading } = useSearchStats(cacheName); + const { indexMetamodel, loadingIndexMetamodel, errorIndexMetamodel } = useIndexMetamodel(cacheName); const [purgeModalOpen, setPurgeModalOpen] = useState(false); const [reindexModalOpen, setReindexModalOpen] = useState(false); const [updateSchemaModalOpen, setUpdateSchemaModalOpen] = useState(false); + const [indexMetamodelName, setIndexMetamodelName] = useState(''); const closePurgeModal = () => { setPurgeModalOpen(false); @@ -145,19 +145,21 @@ const IndexManagement = () => { {t('caches.index.class-name')} - {indexData.name} + setIndexMetamodelName(indexData.name)}> + {indexData.name} + {t('caches.index.entities-number')} - {indexData.count} + {indexData.count} {t('caches.index.size')} - {indexData.size} + {indexData.size} @@ -220,6 +222,13 @@ const IndexManagement = () => { + 0} + closeModal={() => setIndexMetamodelName('')} + /> ); }; diff --git a/src/app/IndexManagement/ViewMetamodel.tsx b/src/app/IndexManagement/ViewMetamodel.tsx new file mode 100644 index 00000000..ed219bce --- /dev/null +++ b/src/app/IndexManagement/ViewMetamodel.tsx @@ -0,0 +1,141 @@ +import React, { useState } from 'react'; +import { Icon, Modal } from '@patternfly/react-core'; +import { useTranslation } from 'react-i18next'; +import { CheckCircleIcon, ListIcon } from '@patternfly/react-icons'; +import { TableEmptyState } from '@app/Common/TableEmptyState'; +import { Table, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; + +/** + * Update schema modal + */ +const ViewMetamodel = (props: { + metamodelName: string, + metamodels: Map, + loading: boolean, + error: string, + isModalOpen: boolean, closeModal: () => void }) => { + const { t } = useTranslation(); + const [rows, setRows] = useState([]); + const [loadingFields, setLoadingFields] = useState(props.loading && rows.length == 0) + const [fieldsPagination, setFieldsPagination] = useState({ + page: 1, + perPage: 10 + }); + + const columnNames = { + name: t('caches.index.metamodel.column-name'), + searchable: t('caches.index.metamodel.column-searchable'), + sortable: t('caches.index.metamodel.column-sortable'), + projectable: t('caches.index.metamodel.column-projectable'), + aggregable: t('caches.index.metamodel.column-aggregable'), + multiValued: t('caches.index.metamodel.column-multi-valued'), + multiValuedInRoot: t('caches.index.metamodel.column-multi-valued-root'), + type: t('caches.index.metamodel.column-type'), + projectionType: t('caches.index.metamodel.column-projection-type'), + argumentType: t('caches.index.metamodel.column-argument-type') + }; + + const displayEnabled = (enabled: boolean) => { + if (!enabled) { + return ( + <> + ) + } + + return ( + + + + ) + } + + const buildContent = () => { + if (props.loading || props.error !== '' || !props.metamodels.get(props.metamodelName)) { + return ( + + ) + } + + const metamodel = props.metamodels.get(props.metamodelName) as IndexMetamodel; + return ( + + + + + + + + + + + + + + + {metamodel.valueFields.map((field + ) => ( + + + + + + + + + + + ))} + +
{columnNames.name}{columnNames.type}{columnNames.multiValued}{columnNames.multiValuedInRoot}{columnNames.aggregable}{columnNames.projectable}{columnNames.searchable}{columnNames.sortable}
{field.name}{field.type}{displayEnabled(field.multiValued)}{displayEnabled(field.multiValuedInRoot)}{displayEnabled(field.aggregable)}{displayEnabled(field.projectable)}{displayEnabled(field.searchable)}{displayEnabled(field.sortable)}
+ ); + } + + return ( + + {buildContent()} + + ); +}; + +export { ViewMetamodel }; diff --git a/src/app/assets/languages/en.json b/src/app/assets/languages/en.json index 8c8dbcd6..a86cc236 100644 --- a/src/app/assets/languages/en.json +++ b/src/app/assets/languages/en.json @@ -22,6 +22,7 @@ "clear": "Clear", "update": "Update" }, + "loading-empty-message": "No result found.", "loading-error-message": "There was an error retrieving data. Check your connection and try again.", "tracing": { "enabled": "Tracing is enabled", @@ -696,6 +697,24 @@ "title": "Update schema?", "description1": "Add new fields to the existing schema without having to rebuild the entire index.", "description2": "This process may take a few minutes." + }, + "metamodel": { + "column-name": "Name", + "column-searchable": "Searchable", + "column-searchable-tooltip": "A searchable field is one you can search within to find matching content.", + "column-sortable": "Sortable", + "column-sortable-tooltip": "A sortable field is one you can use to sort search results in order.", + "column-projectable": "Projectable", + "column-projectable-tooltip": "A projectable field is one you can include in the search results to display its value.", + "column-aggregable": "Aggregable", + "column-aggregable-tooltip": "An aggregable field is one you can use to group or calculate summary data, like totals or averages.", + "column-multi-valued": "Multi valued", + "column-multi-valued-tooltip": "A multi-valued field is one that can hold multiple values instead of just one.", + "column-multi-valued-root": "Multi valued in root", + "column-multi-valued-root-tooltip": "A multi-valued in root is a field that can hold multiple values at the top level of a document or object.", + "column-type": "Type", + "column-projection-type": "Projection type", + "column-argument-type": "Argument type" } }, "tracing": { diff --git a/src/app/services/searchHook.ts b/src/app/services/searchHook.ts index 7c282011..6d2fd8c6 100644 --- a/src/app/services/searchHook.ts +++ b/src/app/services/searchHook.ts @@ -35,3 +35,30 @@ export function useSearch(cacheName: string) { return { search, setSearch }; } + +export function useIndexMetamodel(cacheName: string) { + const [indexMetamodel, setIndexMetamodel] = useState>(new Map()); + const [errorIndexMetamodel, setErrorMetamodel] = useState(''); + const [loadingIndexMetamodel, setLoadingIndexMetamodel] = useState(true); + + useEffect(() => { + if (loadingIndexMetamodel) { + ConsoleServices.search() + .retrieveIndexMetamodel(cacheName) + .then((eitherMetamodel) => { + if (eitherMetamodel.isRight()) { + setIndexMetamodel(eitherMetamodel.value); + } else { + setErrorMetamodel(eitherMetamodel.value.message); + } + }) + .then(() => setLoadingIndexMetamodel(false)); + } + }, [loadingIndexMetamodel]); + + return { + loadingIndexMetamodel, + errorIndexMetamodel, + indexMetamodel + }; +} diff --git a/src/services/searchService.ts b/src/services/searchService.ts index f7ff2068..79905751 100644 --- a/src/services/searchService.ts +++ b/src/services/searchService.ts @@ -114,6 +114,39 @@ export class SearchService { }); } + /** + * Retrieve index metamodel + * + * @param cacheName + */ + public async retrieveIndexMetamodel(cacheName: string): Promise>> { + return this.utils.get(this.endpoint + encodeURIComponent(cacheName) + '/search/indexes/metamodel', (data) => { + const metamodels = new Map(); + data.forEach(metamodel => { + const name = metamodel['entity-name']; + metamodels.set(name, + { + indexName: metamodel['index-name'], + entityName: name, + valueFields: Object.keys(metamodel['value-fields']) + .map(valueField => { + name: valueField, + multiValued: metamodel['value-fields'][valueField]['multi-valued'], + multiValuedInRoot: metamodel['value-fields'][valueField]['multi-valued-in-root'], + type: metamodel['value-fields'][valueField]['type'], + projectionType: metamodel['value-fields'][valueField]['projection-type'], + argumentType: metamodel['value-fields'][valueField]['argument-type'], + searchable: metamodel['value-fields'][valueField]['searchable'], + sortable: metamodel['value-fields'][valueField]['sortable'], + projectable: metamodel['value-fields'][valueField]['projectable'], + aggregable: metamodel['value-fields'][valueField]['aggregable'], + analyzer: metamodel['value-fields'][valueField]['analyzer'] + })}); + }) + return metamodels; + }); + } + /** * Purge index for the cache name * diff --git a/src/types/InfinispanTypes.ts b/src/types/InfinispanTypes.ts index 82a1aab9..de1ff14e 100644 --- a/src/types/InfinispanTypes.ts +++ b/src/types/InfinispanTypes.ts @@ -525,3 +525,23 @@ interface Realm { name: string; users: string[]; } + +interface IndexMetamodel { + entityName: string; + indexName: string; + valueFields: IndexValueField[] +} + +interface IndexValueField { + name: string; + multiValued: boolean; + multiValuedInRoot: boolean; + type: string; + projectionType: string; + argumentType: string; + searchable: boolean; + sortable: boolean; + projectable: boolean; + aggregable: boolean; + analyzer: string; +}