Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISPN-14230 Download server report from cluster membership list #334

Merged
merged 1 commit into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/__tests__/services/downloadServerReport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as DownloadServerReport from '@app/services/clusterHook';

dpanshug marked this conversation as resolved.
Show resolved Hide resolved
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++,
downloading: true
};
});

describe('downloadReport', () => {
test('To download server report and check download calls', () => {
const { downloadServerReport } = DownloadServerReport.useDownloadServerReport();
downloadServerReport('node1');
expect(onDownloadReport).toBe(1);
});
});
164 changes: 79 additions & 85 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 @@ -20,109 +21,56 @@ import {
TextVariants,
Title
} from '@patternfly/react-core';
import { CubesIcon, SearchIcon } from '@patternfly/react-icons';
import { Table, TableBody, TableHeader, TableVariant } from '@patternfly/react-table';
import { CubesIcon, SearchIcon, DownloadIcon } from '@patternfly/react-icons';
import { TableComposable, Thead, Tr, Th, Tbody, Td } 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,64 @@ 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: '28%' }} />
</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
dpanshug marked this conversation as resolved.
Show resolved Hide resolved
isLoading={downloading}
icon={!downloading ? <DownloadIcon /> : null}
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
});
});
}
}