diff --git a/src/__tests__/services/downloadServerReport.test.ts b/src/__tests__/services/downloadServerReport.test.ts new file mode 100644 index 00000000..00d553b5 --- /dev/null +++ b/src/__tests__/services/downloadServerReport.test.ts @@ -0,0 +1,25 @@ +import * as DownloadServerReport from '@app/services/clusterHook'; + +jest.mock('@app/services/clusterHook'); +const mockedClusterHook = DownloadServerReport as jest.Mocked; + +let onDownloadReport; + +beforeEach(() => { + onDownloadReport = 0; + mockedClusterHook.useDownloadServerReport.mockClear(); +}); + +mockedClusterHook.useDownloadServerReport.mockImplementation(() => { + return { + downloadServerReport: () => onDownloadReport++ + }; +}); + +describe('downloadReport', () => { + test('To download server report and check download calls', () => { + const { downloadServerReport } = DownloadServerReport.useDownloadServerReport(); + downloadServerReport('node1'); + expect(onDownloadReport).toBe(1); + }); +}); diff --git a/src/app/ClusterStatus/ClusterStatus.tsx b/src/app/ClusterStatus/ClusterStatus.tsx index 7910f9b0..4971edf8 100644 --- a/src/app/ClusterStatus/ClusterStatus.tsx +++ b/src/app/ClusterStatus/ClusterStatus.tsx @@ -21,108 +21,65 @@ import { Title } from '@patternfly/react-core'; import { CubesIcon, SearchIcon } from '@patternfly/react-icons'; -import { Table, TableBody, TableHeader, TableVariant } from '@patternfly/react-table'; +import { TableComposable, Thead, Tr, Th, Tbody, Td, IAction, ActionsColumn } from '@patternfly/react-table'; import { Health } from '@app/Common/Health'; import { useApiAlert } from '@app/utils/useApiAlert'; import { TableErrorState } from '@app/Common/TableErrorState'; import { useTranslation } from 'react-i18next'; -import { ConsoleServices } from '@services/ConsoleServices'; +import { useDownloadServerReport, useFetchClusterMembers } from '@app/services/clusterHook'; const ClusterStatus: React.FunctionComponent = (props) => { - const { addAlert } = useApiAlert(); const { t } = useTranslation(); const brandname = t('brandname.brandname'); - const [error, setError] = useState(); - const [loading, setLoading] = useState(true); - const [cacheManager, setCacheManager] = useState(undefined); + const { downloadServerReport } = useDownloadServerReport(); + + const { clusterMembers, cacheManager, loading, error, reload } = useFetchClusterMembers(); const [filteredClusterMembers, setFilteredClusterMembers] = useState([]); const [clusterMembersPagination, setClusterMembersPagination] = useState({ page: 1, perPage: 10 }); - const [rows, setRows] = useState<(string | any)[]>([]); - const columns = [ - { title: 'Name' }, + + const columnNames = { + name: t('cluster-membership.node-name'), + physicalAdd: t('cluster-membership.physical-address') + }; + + const clusterMemberActions = (row): IAction[] => [ { - title: 'Physical address' + title: t('cluster-membership.download-report'), + onClick: () => { + downloadServerReport(row.name); + } } ]; useEffect(() => { - ConsoleServices.dataContainer() - .getDefaultCacheManager() - .then((eitherDefaultCm) => { - setLoading(false); - if (eitherDefaultCm.isRight()) { - setCacheManager(eitherDefaultCm.value); - setFilteredClusterMembers(eitherDefaultCm.value.cluster_members); - updateRows(eitherDefaultCm.value.cluster_members); - } else { - setError(eitherDefaultCm.value.message); - } - }); - }, []); + if (clusterMembers) { + const initSlice = (clusterMembersPagination.page - 1) * clusterMembersPagination.perPage; + setFilteredClusterMembers(clusterMembers.slice(initSlice, initSlice + clusterMembersPagination.perPage)); + } + }, [loading, clusterMembers, error]); useEffect(() => { - const initSlice = (clusterMembersPagination.page - 1) * clusterMembersPagination.perPage; - updateRows(filteredClusterMembers.slice(initSlice, initSlice + clusterMembersPagination.perPage)); - }, [error, cacheManager]); + if (filteredClusterMembers) { + const initSlice = (clusterMembersPagination.page - 1) * clusterMembersPagination.perPage; + setFilteredClusterMembers(clusterMembers.slice(initSlice, initSlice + clusterMembersPagination.perPage)); + } + }, [clusterMembersPagination]); const onSetPage = (_event, pageNumber) => { setClusterMembersPagination({ - page: pageNumber, - perPage: clusterMembersPagination.perPage + ...clusterMembersPagination, + page: pageNumber }); - const initSlice = (pageNumber - 1) * clusterMembersPagination.perPage; - updateRows(filteredClusterMembers.slice(initSlice, initSlice + clusterMembersPagination.perPage)); }; const onPerPageSelect = (_event, perPage) => { setClusterMembersPagination({ - page: clusterMembersPagination.page, + page: 1, perPage: perPage }); - const initSlice = (clusterMembersPagination.page - 1) * perPage; - updateRows(filteredClusterMembers.slice(initSlice, initSlice + perPage)); - }; - - const buildEmptyState = () => { - return ( - - - - - No cluster members - - Add nodes to create a cluster. - - - ); - }; - - const updateRows = (clusterMembers: ClusterMember[]) => { - let rows: { heightAuto: boolean; cells: (string | any)[] }[]; - if (clusterMembers.length == 0) { - rows = [ - { - heightAuto: true, - cells: [ - { - props: { colSpan: 2 }, - title: buildEmptyState() - } - ] - } - ]; - } else { - rows = clusterMembers.map((member) => { - return { - heightAuto: true, - cells: [{ title: member.name }, { title: member.physical_address }] - }; - }); - } - setRows(rows); }; const buildHeader = () => { @@ -142,7 +99,7 @@ const ClusterStatus: React.FunctionComponent = (props) => { - Cluster membership + {t('cluster-membership.title')} @@ -150,7 +107,7 @@ const ClusterStatus: React.FunctionComponent = (props) => { - + {sizeLabel} @@ -186,7 +143,7 @@ const ClusterStatus: React.FunctionComponent = (props) => { return ( - This cluster is empty + {t('cluster-membership.empty-cluster')} ); } @@ -195,7 +152,7 @@ const ClusterStatus: React.FunctionComponent = (props) => { = (props) => { onPerPageSelect={onPerPageSelect} isCompact /> - - - -
+ + + + {columnNames.name} + {columnNames.physicalAdd} + + + + {clusterMembers.length == 0 || filteredClusterMembers.length == 0 ? ( + + + + + + + {t('cluster-membership.no-cluster-title')} + + {t('cluster-membership.no-cluster-body')} + + + + + ) : ( + filteredClusterMembers.map((row) => { + const rowActions: IAction[] = clusterMemberActions(row); + return ( + + {row.name} + {row.physical_address} + + + + + ); + }) + )} + +
); diff --git a/src/app/assets/languages/en.json b/src/app/assets/languages/en.json index 2fad0bb0..71e6c64c 100644 --- a/src/app/assets/languages/en.json +++ b/src/app/assets/languages/en.json @@ -694,5 +694,14 @@ "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." + }, + "cluster-membership": { + "title": "Cluster membership", + "node-name": "Name", + "physical-address": "Physical address", + "download-report": "Download server report", + "empty-cluster": "This cluster is empty", + "no-cluster-title": "No cluster members", + "no-cluster-body": "Add nodes to create a cluster." } } diff --git a/src/app/services/clusterHook.ts b/src/app/services/clusterHook.ts new file mode 100644 index 00000000..5e258c34 --- /dev/null +++ b/src/app/services/clusterHook.ts @@ -0,0 +1,63 @@ +import { useEffect, useState } from 'react'; +import { ConsoleServices } from '@services/ConsoleServices'; +import { useApiAlert } from '@app/utils/useApiAlert'; + +export function useFetchClusterMembers() { + const [cacheManager, setCacheManager] = useState(); + const [clusterMembers, setClusterMembers] = useState([]); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (loading) { + ConsoleServices.dataContainer() + .getDefaultCacheManager() + .then((eitherDefaultCm) => { + if (eitherDefaultCm.isRight()) { + setCacheManager(eitherDefaultCm.value); + setClusterMembers(eitherDefaultCm.value.cluster_members); + } else { + setError(eitherDefaultCm.value.message); + } + }) + .finally(() => setLoading(false)); + } + }, [loading]); + + const reload = () => { + setLoading(true); + }; + + return { + clusterMembers, + cacheManager, + loading, + error, + reload + }; +} + +export function useDownloadServerReport() { + const { addAlert } = useApiAlert(); + + const downloadServerReport = (nodeName) => { + ConsoleServices.server() + .downloadReport(nodeName) + .then((response) => { + if (response.isRight()) { + const blob = new Blob([response.value], { type: 'application/gzip' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', nodeName + '-report.tar.gz'); + document.body.appendChild(link); + link.click(); + } else { + addAlert(response.value); + } + }); + }; + return { + downloadServerReport + }; +} diff --git a/src/services/serverService.ts b/src/services/serverService.ts index 37ec6890..79454bc5 100644 --- a/src/services/serverService.ts +++ b/src/services/serverService.ts @@ -21,4 +21,31 @@ export class ServerService { public async getVersion(): Promise> { return this.utils.get(this.endpoint, (data) => data.version); } + + /** + * Get server report for a given nodeName + */ + public async downloadReport(nodeName: string): Promise> { + return this.utils + .fetch(this.endpoint + '/report/' + encodeURIComponent(nodeName), 'GET') + .then((response) => { + if (response.ok) { + return response.blob(); + } else { + return response.text().then((text) => { + throw text; + }); + } + }) + .then((data) => { + if (data instanceof Blob) { + return right(data) as Either; + } + return left({ + message: 'Unexpected error retreiving data', + success: false, + data: data + }); + }); + } }