Skip to content

Commit

Permalink
ISPN-14230 Download server report from cluster membership list
Browse files Browse the repository at this point in the history
* Table changed to TableComposable
* Labels exported
  • Loading branch information
dpanshug committed Jul 4, 2023
1 parent fc0310c commit 2734e9b
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 84 deletions.
25 changes: 25 additions & 0 deletions src/__tests__/services/downloadServerReport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as DownloadServerReport from '@app/services/clusterHook';

jest.mock('@app/services/clusterHook');
const mockedClusterHook = DownloadServerReport as jest.Mocked<typeof DownloadServerReport>;

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);
});
});
161 changes: 77 additions & 84 deletions src/app/ClusterStatus/ClusterStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import {
Button,
Bullseye,
Card,
CardBody,
Expand All @@ -21,108 +22,55 @@ 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<any> = (props) => {
const { addAlert } = useApiAlert();
const { t } = useTranslation();
const brandname = t('brandname.brandname');
const [error, setError] = useState<undefined | string>();
const [loading, setLoading] = useState<boolean>(true);
const [cacheManager, setCacheManager] = useState<undefined | CacheManager>(undefined);
const { downloadServerReport, downloading } = useDownloadServerReport();

const { clusterMembers, cacheManager, loading, error, reload } = useFetchClusterMembers();
const [filteredClusterMembers, setFilteredClusterMembers] = useState<ClusterMember[]>([]);
const [clusterMembersPagination, setClusterMembersPagination] = useState({
page: 1,
perPage: 10
});
const [rows, setRows] = useState<(string | any)[]>([]);
const columns = [
{ title: 'Name' },
{
title: 'Physical address'
}
];

const columnNames = {
name: t('cluster-membership.node-name'),
physicalAdd: t('cluster-membership.physical-address')
};

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 (
<Bullseye>
<EmptyState variant={EmptyStateVariant.small}>
<EmptyStateIcon icon={SearchIcon} />
<Title headingLevel="h2" size="lg">
No cluster members
</Title>
<EmptyStateBody>Add nodes to create a cluster.</EmptyStateBody>
</EmptyState>
</Bullseye>
);
};

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 = () => {
Expand All @@ -142,15 +90,15 @@ const ClusterStatus: React.FunctionComponent<any> = (props) => {
<Flex>
<FlexItem>
<TextContent>
<Text component={TextVariants.h1}>Cluster membership</Text>
<Text component={TextVariants.h1}>{t('cluster-membership.title')}</Text>
</TextContent>
</FlexItem>
</Flex>
<Flex>
<FlexItem>
<Health health={cacheManager.health} />
</FlexItem>
<Divider isVertical></Divider>
<Divider orientation={{ default: 'vertical' }} />
<FlexItem>
<TextContent>
<Text component={TextVariants.p}>{sizeLabel}</Text>
Expand Down Expand Up @@ -186,7 +134,7 @@ const ClusterStatus: React.FunctionComponent<any> = (props) => {
return (
<EmptyState variant={EmptyStateVariant.full}>
<EmptyStateIcon icon={CubesIcon} />
<EmptyStateBody>This cluster is empty</EmptyStateBody>
<EmptyStateBody>{t('cluster-membership.empty-cluster')}</EmptyStateBody>
</EmptyState>
);
}
Expand All @@ -195,18 +143,63 @@ const ClusterStatus: React.FunctionComponent<any> = (props) => {
<Card>
<CardBody>
<Pagination
itemCount={filteredClusterMembers.length}
itemCount={clusterMembers.length}
perPage={clusterMembersPagination.perPage}
page={clusterMembersPagination.page}
onSetPage={onSetPage}
widgetId="pagination-cluster-members"
onPerPageSelect={onPerPageSelect}
isCompact
/>
<Table variant={TableVariant.compact} aria-label="Cluster status table" cells={columns} rows={rows}>
<TableHeader />
<TableBody />
</Table>
<TableComposable
className={'cluster-membership-table'}
aria-label={t('cluster-membership.title')}
variant={'compact'}
>
<Thead>
<Tr>
<Th colSpan={1}>{columnNames.name}</Th>
<Th colSpan={1}>{columnNames.physicalAdd}</Th>
<Th style={{ width: '15%' }} />
</Tr>
</Thead>
<Tbody>
{clusterMembers.length == 0 || filteredClusterMembers.length == 0 ? (
<Tr>
<Td colSpan={6}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.small}>
<EmptyStateIcon icon={SearchIcon} />
<Title headingLevel="h2" size="lg">
{t('cluster-membership.no-cluster-title')}
</Title>
<EmptyStateBody>{t('cluster-membership.no-cluster-body')}</EmptyStateBody>
</EmptyState>
</Bullseye>
</Td>
</Tr>
) : (
filteredClusterMembers.map((row) => {
return (
<Tr key={row.name}>
<Td dataLabel={columnNames.name}>{row.name}</Td>
<Td dataLabel={columnNames.physicalAdd}>{row.physical_address}</Td>
<Td dataLabel={columnNames.physicalAdd}>
<Button
variant="link"
isInline
isLoading={downloading}
onClick={() => downloadServerReport(row.name)}
>
{downloading ? t('cluster-membership.downloading') : t('cluster-membership.download-report')}
</Button>
</Td>
</Tr>
);
})
)}
</Tbody>
</TableComposable>
</CardBody>
</Card>
);
Expand Down
10 changes: 10 additions & 0 deletions src/app/assets/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -732,5 +732,15 @@
"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",
"downloading": "Downloading",
"empty-cluster": "This cluster is empty",
"no-cluster-title": "No cluster members",
"no-cluster-body": "Add nodes to create a cluster."
}
}
67 changes: 67 additions & 0 deletions src/app/services/clusterHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect, useState } from 'react';
import { ConsoleServices } from '@services/ConsoleServices';
import { useApiAlert } from '@app/utils/useApiAlert';

export function useFetchClusterMembers() {
const [cacheManager, setCacheManager] = useState<CacheManager>();
const [clusterMembers, setClusterMembers] = useState<ClusterMember[]>([]);
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 [downloading, setDownloading] = useState(false);

const downloadServerReport = (nodeName) => {
setDownloading(true);
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);
}
})
.finally(() => setDownloading(false));
};
return {
downloadServerReport,
downloading
};
}
27 changes: 27 additions & 0 deletions src/services/serverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,31 @@ export class ServerService {
public async getVersion(): Promise<Either<ActionResponse, string>> {
return this.utils.get(this.endpoint, (data) => data.version);
}

/**
* Get server report for a given nodeName
*/
public async downloadReport(nodeName: string): Promise<Either<ActionResponse, Blob>> {
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<ActionResponse, Blob>;
}
return left(<ActionResponse>{
message: 'Unexpected error retreiving data',
success: false,
data: data
});
});
}
}

0 comments on commit 2734e9b

Please sign in to comment.