Skip to content

Commit

Permalink
Collection info tab (#103)
Browse files Browse the repository at this point in the history
* wip collection info tab

* wip collection info tab

* wip collection info tab, some refactoring, status indicator

* added collection cluster info

* snackbars

* configs upd for tests, test for CollectionClusterInfo

* cluster info refactoring

* variant heading for CardHeader

* format

* bug fix
  • Loading branch information
trean authored Aug 30, 2023
1 parent fdb3c44 commit d53b2d0
Show file tree
Hide file tree
Showing 14 changed files with 423 additions and 61 deletions.
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ rules:
},
],
}
ignorePatterns: ['node_modules/', 'build/', '*.test.js', '*.test.jsx']
68 changes: 68 additions & 0 deletions src/components/Collections/CollectionCluster/ClusterInfo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Card, CardHeader, Table, TableBody } from '@mui/material';
import { CopyButton } from '../../Common/CopyButton';
import ClusterInfoHead from './ClusterInfoHead';
import ClusterShardRow from './ClusterShardRow';

const ClusterInfo = ({ collectionCluster, ...other }) => {
const shards = [
...(collectionCluster.result?.local_shards || []),
...(collectionCluster.result?.remote_shards || []),
];

const shardRows = shards.map((shard) => (
<ClusterShardRow
shard={shard}
clusterPeerId={collectionCluster.result?.peer_id}
key={shard.shard_id.toString() + (shard.peer_id || '')}
/>
));

return (
<Card variant="dual" {...other}>
<CardHeader
title={'Collection Cluster Info'}
variant="heading"
sx={{
flexGrow: 1,
}}
action={<CopyButton text={JSON.stringify(collectionCluster)} />}
/>
<Table>
<ClusterInfoHead />
<TableBody>{shardRows}</TableBody>
</Table>
</Card>
);
};

ClusterInfo.defaultProps = {
collectionCluster: {
result: {},
},
};

ClusterInfo.propTypes = {
collectionCluster: PropTypes.shape({
result: PropTypes.shape({
peer_id: PropTypes.number,
local_shards: PropTypes.arrayOf(
PropTypes.shape({
shard_id: PropTypes.number,
state: PropTypes.string,
})
),
remote_shards: PropTypes.arrayOf(
PropTypes.shape({
shard_id: PropTypes.number,
peer_id: PropTypes.number,
state: PropTypes.string,
})
),
}),
}).isRequired,
other: PropTypes.object,
};

export default ClusterInfo;
28 changes: 28 additions & 0 deletions src/components/Collections/CollectionCluster/ClusterInfoHead.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TableCell, TableHead, TableRow, Typography } from '@mui/material';
import React from 'react';

const ClusterInfoHead = () => {
return (
<TableHead>
<TableRow>
<TableCell>
<Typography variant="subtitle1" fontWeight={600}>
Shard ID
</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle1" fontWeight={600}>
Location
</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle1" fontWeight={600}>
Status
</Typography>
</TableCell>
</TableRow>
</TableHead>
);
};

export default ClusterInfoHead;
36 changes: 36 additions & 0 deletions src/components/Collections/CollectionCluster/ClusterShardRow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TableCell, TableRow, Typography } from '@mui/material';
import React from 'react';
import PropTypes from 'prop-types';

const ClusterShardRow = ({ shard, clusterPeerId }) => {
return (
<TableRow data-testid="shard-row">
<TableCell>
<Typography variant="subtitle1" component={'span'} color="text.secondary">
{shard.shard_id}
</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle1" component={'span'} color="text.secondary">
{shard.peer_id ? `Remote (${shard.peer_id})` : `Local (${clusterPeerId ?? 'unknown'})`}
</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle1" component={'span'} color="text.secondary">
{shard.state}
</Typography>
</TableCell>
</TableRow>
);
};

ClusterShardRow.propTypes = {
shard: PropTypes.shape({
shard_id: PropTypes.number,
peer_id: PropTypes.number,
state: PropTypes.string,
}).isRequired,
clusterPeerId: PropTypes.number,
};

export default ClusterShardRow;
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import ClusterInfo from './ClusterInfo';
import ClusterShardRow from './ClusterShardRow';

const CLUSTER_INFO = {
result: {
peer_id: 5644950770669488,
shard_count: 3,
local_shards: [
{
shard_id: 0,
points_count: 62223,
state: 'Active',
},
{
shard_id: 2,
points_count: 65999,
state: 'Active',
},
],
remote_shards: [
{
shard_id: 0,
peer_id: 5255497362296823,
state: 'Active',
},
{
shard_id: 1,
peer_id: 5255497362296823,
state: 'Active',
},
{
shard_id: 1,
peer_id: 8741461806010521,
state: 'Active',
},
{
shard_id: 2,
peer_id: 8741461806010521,
state: 'Active',
},
],
shard_transfers: [],
},
status: 'ok',
time: 0.00002203,
};

describe('collection cluster info', () => {
it('should render ClusterShardRow with given data', () => {
const shard = CLUSTER_INFO.result.remote_shards[0];
render(
<table>
<tbody>
<ClusterShardRow shard={shard} clusterPeerId={CLUSTER_INFO.result.peer_id} />
</tbody>
</table>
);
expect(screen.getByTestId('shard-row').children[0].children[0].textContent).toBe(shard.shard_id.toString());
expect(screen.getByText(`Remote (${shard.peer_id})`)).toBeTruthy();
expect(screen.getByText(shard.state)).toBeTruthy();
});

it('should render CollectionClusterInfo with given data', () => {
const shardReplicasCount = CLUSTER_INFO.result.local_shards.length + CLUSTER_INFO.result.remote_shards.length;
render(<ClusterInfo collectionCluster={CLUSTER_INFO} />);
expect(screen.getByText('Collection Cluster Info')).toBeTruthy();
expect(screen.getAllByTestId('shard-row').length).toBe(shardReplicasCount);
});
});
79 changes: 79 additions & 0 deletions src/components/Collections/CollectionInfo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { memo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Box, Card, CardContent, CardHeader, Typography } from '@mui/material';
import { useClient } from '../../context/client-context';
import { DataGridList } from '../Points/DataGridList';
import { CopyButton } from '../Common/CopyButton';
import { Dot } from '../Common/Dot';
import ClusterInfo from './CollectionCluster/ClusterInfo';
import { useSnackbar } from 'notistack';
import { getSnackbarOptions } from '../Common/utils/snackbarOptions';

export const CollectionInfo = ({ collectionName }) => {
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const { client: qdrantClient } = useClient();
const [collection, setCollection] = React.useState({});
const [clusterInfo, setClusterInfo] = React.useState(null);

useEffect(() => {
qdrantClient
.getCollection(collectionName)
.then((res) => {
setCollection(() => {
return { ...res };
});
})
.catch((err) => {
enqueueSnackbar(err.message, getSnackbarOptions('error', closeSnackbar));
});

qdrantClient
.api('cluster')
.collectionClusterInfo({ collection_name: collectionName })
.then((res) => {
setClusterInfo(() => {
return { ...res.data };
});
})
.catch((err) => {
enqueueSnackbar(err.message, getSnackbarOptions('error', closeSnackbar));
});
}, [collectionName]);

return (
<Box pt={2}>
<Card variant="dual">
<CardHeader
title={'Collection Info'}
variant="heading"
sx={{
flexGrow: 1,
}}
action={<CopyButton text={JSON.stringify(collection)} />}
/>
<CardContent>
<DataGridList
data={collection}
specialCases={{
status: (
<Typography variant="subtitle1" color="text.secondary">
{collection.status} <Dot color={collection.status} />
</Typography>
),
}}
/>
</CardContent>
</Card>

{clusterInfo && <ClusterInfo sx={{ mt: 5 }} collectionCluster={clusterInfo} />}
</Box>
);
};

CollectionInfo.displayName = 'CollectionInfo';

CollectionInfo.propTypes = {
collectionName: PropTypes.string.isRequired,
};

export default memo(CollectionInfo);
15 changes: 15 additions & 0 deletions src/components/Common/Dot.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { styled } from '@mui/material/styles';

/**
* Status indicator dot.
* @type {StyledComponent<MUIStyledCommonProps<Theme>, JSX.IntrinsicElements[string], {}>}
*/
export const Dot = styled('div')(
({ color }) => `
border-radius: 50%;
background-color: ${color};
width: 10px;
height: 10px;
display: inline-block;
`
);
78 changes: 78 additions & 0 deletions src/components/Points/DataGridList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import { JsonViewer } from '@textea/json-viewer';
import { useTheme } from '@mui/material/styles';
import { Divider, Grid, Typography } from '@mui/material';

/**
* A list of key-value pairs, where the value is either a string or an object.
* if the value is an object, it will be rendered as a JSON tree.
* @param {Object} data - key-value pairs to render
* @param {Object} specialCases - key-value pairs to render, where the value is JSX element
* @return {unknown[]} - array of JSX elements
*/
export const DataGridList = function ({ data = {}, specialCases = {} }) {
const theme = useTheme();
const specialKeys = Object.keys(specialCases) || [];

return Object.keys(data).map((key) => {
return (
<div key={key}>
<Grid container spacing={2}>
<Grid item xs={3} my={1}>
<Typography
variant="subtitle1"
sx={{
display: 'inline',
wordBreak: 'break-word',
fontWeight: 600,
}}
>
{key}
</Typography>
</Grid>

<Grid item xs={9} my={1}>
{/* special cases */}
{specialKeys?.includes(key) && specialCases[key]}

{/* objects */}
{typeof data[key] === 'object' && !specialKeys.includes(key) && (
<Typography variant="subtitle1">
{' '}
<JsonViewer
theme={theme.palette.mode}
value={data[key]}
displayDataTypes={false}
defaultInspectDepth={0}
rootName={false}
/>{' '}
</Typography>
)}

{/* other types of values */}
{typeof data[key] !== 'object' && !specialKeys.includes(key) && (
<Typography variant="subtitle1" color="text.secondary" display={'inline'}>
{'\t'} {data[key].toString()}
</Typography>
)}
</Grid>
</Grid>
<Divider />
</div>
);
});
};

DataGridList.defaultProps = {
data: {},
specialCases: {},
};

DataGridList.propTypes = {
data: PropTypes.object.isRequired,
specialCases: PropTypes.shape({
key: PropTypes.string,
value: PropTypes.element,
}),
};
Loading

0 comments on commit d53b2d0

Please sign in to comment.