Skip to content

Commit

Permalink
List of collections as a table with more info (#213)
Browse files Browse the repository at this point in the history
* collections list as a table

* collections pagination

* collection search upd

* moved a component

* add an action menu to the collection table

* upd

* moved vectors configuration from DatasetsTableRow to a separate component, used it in the CollectionsList

* some fixes

* CollectionsList render test

* fixes

* display sparse vectors in VectorsConfigChip

* optimizations
  • Loading branch information
trean authored Aug 21, 2024
1 parent 435d5d8 commit 9a53edb
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 80 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

137 changes: 137 additions & 0 deletions src/components/Collections/CollectionsList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Box, MenuItem, TableCell, TableContainer, TableRow, Typography } from '@mui/material';
import { styled, useTheme } from '@mui/material/styles';
import { TableBodyWithGaps, TableHeadWithGaps, TableWithGaps } from '../Common/TableWithGaps';
import { Dot } from '../Common/Dot';
import DeleteDialog from './DeleteDialog';
import ActionsMenu from '../Common/ActionsMenu';
import VectorsConfigChip from '../Common/VectorsConfigChip';

const StyledLink = styled(Link)`
text-decoration: none;
color: inherit;
&:hover {
text-decoration: underline;
}
`;

const CollectionTableRow = ({ collection, getCollectionsCall }) => {
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const theme = useTheme();

return (
<TableRow>
<TableCell>
<Typography component={StyledLink} to={`/collections/${collection.name}`}>
{collection.name}
</Typography>
</TableCell>
<TableCell>
<Box
sx={{ display: 'inline-flex', alignItems: 'center' }}
component={StyledLink}
to={`/collections/${collection.name}#info`}
>
<Dot color={collection.status} />
<Typography sx={{ ml: 1 }}>{collection.status}</Typography>
</Box>
</TableCell>
<TableCell align="center">
<Typography component={StyledLink} to={`/collections/${collection.name}`}>
{collection.points_count}
</Typography>
</TableCell>
<TableCell align="center">
<Typography>{collection.segments_count}</Typography>
</TableCell>
<TableCell align="center">
<Typography>{collection.config.params.shard_number}</Typography>
</TableCell>
<TableCell>
<VectorsConfigChip collectionConfigParams={collection.config.params} sx={{ justifyContent: 'center' }} />
</TableCell>
<TableCell align="right">
<ActionsMenu>
<MenuItem component={Link} to={`/collections/${collection.name}#snapshots`}>
Take Snapshot
</MenuItem>
<MenuItem component={Link} to={`/collections/${collection.name}/visualize`}>
Visualize
</MenuItem>
<MenuItem component={Link} to={`/collections/${collection.name}/graph`}>
Graph
</MenuItem>
<MenuItem onClick={() => setOpenDeleteDialog(true)} sx={{ color: theme.palette.error.main }}>
Delete
</MenuItem>
</ActionsMenu>
<DeleteDialog
open={openDeleteDialog}
setOpen={setOpenDeleteDialog}
collectionName={collection.name}
getCollectionsCall={getCollectionsCall}
/>
</TableCell>
</TableRow>
);
};

CollectionTableRow.propTypes = {
collection: PropTypes.object.isRequired,
getCollectionsCall: PropTypes.func.isRequired,
};

const HeaderTableCell = styled(TableCell)`
font-weight: bold;
`;

const CollectionsList = ({ collections, getCollectionsCall }) => {
return (
<TableContainer>
<TableWithGaps aria-label="simple table">
<TableHeadWithGaps>
<TableRow>
<HeaderTableCell width="25%">Name</HeaderTableCell>
<HeaderTableCell width="12%">Status</HeaderTableCell>
<HeaderTableCell width="12%" align="center">
Points (Approx)
</HeaderTableCell>
<HeaderTableCell width="12%" align="center">
Segments
</HeaderTableCell>
<HeaderTableCell width="12%" align="center">
Shards
</HeaderTableCell>
<HeaderTableCell width="20%" align="center">
Vectors Configuration
<br />
(Name, Size, Distance)
</HeaderTableCell>
<HeaderTableCell width="7%" align="right">
Actions
</HeaderTableCell>
</TableRow>
</TableHeadWithGaps>
<TableBodyWithGaps>
{collections.length > 0 &&
collections.map((collection) => (
<CollectionTableRow
key={collection.name}
collection={collection}
getCollectionsCall={getCollectionsCall}
/>
))}
</TableBodyWithGaps>
</TableWithGaps>
</TableContainer>
);
};

CollectionsList.propTypes = {
collections: PropTypes.array.isRequired,
getCollectionsCall: PropTypes.func.isRequired,
};

export default CollectionsList;
1 change: 1 addition & 0 deletions src/components/Collections/SearchBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function InputWithIcon({ value, setValue, actions }) {
</SvgIcon>
</InputAdornment>
}
size={'small'}
sx={{ maxWidth: 500 }}
/>

Expand Down
81 changes: 81 additions & 0 deletions src/components/Collections/collectionList.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import CollectionsList from './CollectionsList';
import { describe, it, expect } from 'vitest';

vi.mock('../../context/client-context', () => ({
useClient: () => ({
client: {
deleteCollection: vi.fn().mockResolvedValue({}),
},
}),
}));

const COLLECTIONS = [
{
name: 'Collection 1',
status: 'green',
points_count: 1000,
segments_count: 10,
config: {
params: {
shard_number: 2,
vectors: {
size: 128,
distance: 'cosine',
},
},
},
},
{
name: 'Collection 2',
status: 'yellow',
points_count: 500,
segments_count: 5,
config: {
params: {
shard_number: 1,
vectors: {
vector1: {
size: 64,
distance: 'euclidean',
},
vector2: {
size: 32,
distance: 'manhattan',
},
},
},
},
},
];

describe('CollectionsList', () => {
it('should render CollectionsList with given data', () => {
render(
<MemoryRouter>
<CollectionsList collections={COLLECTIONS} getCollectionsCall={() => {}} />
</MemoryRouter>
);
expect(screen.getByText('Collection 1')).toBeInTheDocument();
expect(screen.getByText('Collection 2')).toBeInTheDocument();
});

it('should render CollectionTableRow with given data', () => {
render(
<MemoryRouter>
<CollectionsList collections={COLLECTIONS} getCollectionsCall={() => {}} />
</MemoryRouter>
);
expect(screen.getByText('green')).toBeInTheDocument();
expect(screen.getByText('yellow')).toBeInTheDocument();
expect(screen.getByText('1000')).toBeInTheDocument();
expect(screen.getByText('500')).toBeInTheDocument();
expect(screen.getByText('128')).toBeInTheDocument();
expect(screen.getByText('cosine')).toBeInTheDocument();
expect(screen.getByText('64')).toBeInTheDocument();
expect(screen.getByText('euclidean')).toBeInTheDocument();
expect(screen.getByText('32')).toBeInTheDocument();
expect(screen.getByText('manhattan')).toBeInTheDocument();
});
});
File renamed without changes.
68 changes: 68 additions & 0 deletions src/components/Common/VectorsConfigChip.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, Grid } from '@mui/material';

const VectorsConfigChip = ({ collectionConfigParams, sx = {} }) => {
return (
<>
{collectionConfigParams.vectors.size && (
<Grid container component={Card} variant={'heading'} p={1} sx={{ ...sx }}>
<Grid item align="center" mr={2}>
default
</Grid>
<Grid item align="center" mr={2}>
{collectionConfigParams.vectors.size}
</Grid>
<Grid item align="center" mr={2}>
{collectionConfigParams.vectors.distance}
</Grid>
{/* model is not always present */}
{collectionConfigParams.vectors.model && (
<Grid item align="center">
{collectionConfigParams.vectors.model}
</Grid>
)}
</Grid>
)}
{!collectionConfigParams.vectors.size &&
Object.keys(collectionConfigParams.vectors).map((vector) => (
<Grid key={vector} container component={Card} variant={'heading'} p={1} sx={{ ...sx }}>
<Grid item align="center" mr={2}>
{vector}
</Grid>
<Grid item align="center" mr={2}>
{collectionConfigParams.vectors[vector].size}
</Grid>
<Grid item align="center" mr={2}>
{collectionConfigParams.vectors[vector].distance}
</Grid>
{/* model is not always present */}
{collectionConfigParams.vectors[vector].model && (
<Grid item align="center">
{collectionConfigParams.vectors[vector].model}
</Grid>
)}
</Grid>
))}
{collectionConfigParams.sparse_vectors &&
Object.keys(collectionConfigParams.sparse_vectors).map((vector) => (
<Grid key={vector} container component={Card} variant={'heading'} p={1} sx={{ ...sx }}>
<Grid item align="center" mr={2}>
{vector}
</Grid>
<Grid item align="center" mr={2}>
Sparse
</Grid>
<Grid item align="center" mr={2}></Grid>
</Grid>
))}
</>
);
};

VectorsConfigChip.propTypes = {
collectionConfigParams: PropTypes.object.isRequired,
sx: PropTypes.object,
};

export default VectorsConfigChip;
37 changes: 3 additions & 34 deletions src/components/Datasets/DatasetsTableRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import prettyBytes from 'pretty-bytes';
import { useTheme } from '@mui/material/styles';
import { Box, Card, CircularProgress, Grid, IconButton, TableCell, TableRow, Tooltip, Typography } from '@mui/material';
import { Box, CircularProgress, IconButton, TableCell, TableRow, Tooltip, Typography } from '@mui/material';
import { Download, FolderZip } from '@mui/icons-material';
import ImportDatasetDialog from './ImportDatasetDialog';
import VectorsConfigChip from '../Common/VectorsConfigChip';

export const DatasetsTableRow = ({ dataset, importDataset }) => {
const theme = useTheme();
Expand Down Expand Up @@ -63,39 +64,7 @@ export const DatasetsTableRow = ({ dataset, importDataset }) => {
<TableCell align="center">{prettyBytes(dataset.size)}</TableCell>

<TableCell>
{dataset.vectors.size && (
<Grid container component={Card} variant={'heading'} p={1}>
<Grid item align="center" mr={2}>
default
</Grid>
<Grid item align="center" mr={2}>
{dataset.vectors.size}
</Grid>
<Grid item align="center" mr={2}>
{dataset.vectors.distance}
</Grid>
<Grid item align="center">
{dataset.vectors.model}
</Grid>
</Grid>
)}
{!dataset.vectors.size &&
Object.keys(dataset.vectors).map((vector) => (
<Grid key={vector} container component={Card} variant={'heading'} p={1}>
<Grid item align="center">
{vector}
</Grid>
<Grid item align="center">
{dataset.vectors[vector].size}
</Grid>
<Grid item align="center">
{dataset.vectors[vector].distance}
</Grid>
<Grid item align="center">
{dataset.vectors[vector].model}
</Grid>
</Grid>
))}
<VectorsConfigChip collectionConfigParams={dataset} />
</TableCell>

<TableCell align="center">{dataset.vectorCount}</TableCell>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Snapshots/SnapshotsTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import PhotoCamera from '@mui/icons-material/PhotoCamera';
import { TableWithGaps, TableHeadWithGaps, TableBodyWithGaps } from '../Common/TableWithGaps';
import { SnapshotsTableRow } from './SnapshotsTableRow';
import { pumpFile, updateProgress } from '../../common/utils';
import InfoBanner from '../InfoBanner';
import InfoBanner from '../Common/InfoBanner';

export const SnapshotsTab = ({ collectionName }) => {
const { client: qdrantClient } = useClient();
Expand Down
Loading

0 comments on commit 9a53edb

Please sign in to comment.