├── .eslintrc.yml
├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ ├── publish-dist-packages.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── logo.png
├── logo192.png
├── logo512.png
├── manifest.json
├── openapi.json
└── robots.txt
├── src
├── App.css
├── App.jsx
├── common
│ ├── axios.js
│ ├── bigIntJSON.js
│ ├── client.js
│ ├── client.test.js
│ ├── urils.test.js
│ └── utils.js
├── components
│ ├── CodeEditorWindow
│ │ ├── Menu
│ │ │ ├── CommandsDrawer
│ │ │ │ ├── CommandDrawer.test.jsx
│ │ │ │ ├── CommandSearch.jsx
│ │ │ │ ├── CommandsDrawer.jsx
│ │ │ │ └── CommandsTable.jsx
│ │ │ ├── SpeedDialMenu.jsx
│ │ │ ├── history.jsx
│ │ │ ├── index.jsx
│ │ │ └── savedCode.jsx
│ │ ├── config
│ │ │ ├── Autocomplete.js
│ │ │ ├── CommandsDrawerUtils.js
│ │ │ ├── ErrorMarker.js
│ │ │ ├── RequesFromCode.js
│ │ │ ├── customSnippets.js
│ │ │ ├── snippetEnhancer.js
│ │ │ └── snippetEnhancer.test.js
│ │ ├── editor.css
│ │ ├── index.jsx
│ │ └── parser.test.js
│ ├── Collections
│ │ ├── CollectionAliases.jsx
│ │ ├── CollectionAliases.test.jsx
│ │ ├── CollectionCard.jsx
│ │ ├── CollectionCluster
│ │ │ ├── ClusterInfo.jsx
│ │ │ ├── ClusterInfoHead.jsx
│ │ │ ├── ClusterShardRow.jsx
│ │ │ └── collectionCluster.test.jsx
│ │ ├── CollectionInfo.jsx
│ │ ├── CollectionsList.jsx
│ │ ├── DeleteDialog.jsx
│ │ ├── SearchBar.jsx
│ │ ├── SearchQuality
│ │ │ ├── SearchQuality.jsx
│ │ │ ├── SearchQualityPanel.jsx
│ │ │ ├── check-index-precision.js
│ │ │ └── searchQualityPannel.test.jsx
│ │ └── collectionList.test.jsx
│ ├── Common
│ │ ├── ActionsMenu.jsx
│ │ ├── CenteredFrame.jsx
│ │ ├── CircularProgressWithLabel.jsx
│ │ ├── CodeBlock.jsx
│ │ ├── ConfirmationDialog.jsx
│ │ ├── CopyButton.jsx
│ │ ├── Dot.jsx
│ │ ├── InfoBanner.jsx
│ │ ├── PointPreview.jsx
│ │ ├── StyledMain.jsx
│ │ ├── TableWithGaps.jsx
│ │ ├── VectorsConfigChip.jsx
│ │ └── utils
│ │ │ └── snackbarOptions.jsx
│ ├── Datasets
│ │ ├── DatasetsTableHeader.jsx
│ │ ├── DatasetsTableRow.jsx
│ │ └── ImportDatasetDialog.jsx
│ ├── EditorCommon
│ │ ├── config
│ │ │ ├── Rules.js
│ │ │ └── theme.js
│ │ ├── index.jsx
│ │ └── tests
│ │ │ └── get-code-blocks.test.js
│ ├── FilterEditorWindow
│ │ ├── config
│ │ │ ├── Autocomplete.js
│ │ │ └── Rules.js
│ │ ├── editor.css
│ │ └── index.jsx
│ ├── GraphVisualisation
│ │ └── GraphVisualisation.jsx
│ ├── InteractiveTutorial
│ │ ├── InteractiveTutorial.jsx
│ │ ├── MdxComponents
│ │ │ ├── MdxCodeBlock.jsx
│ │ │ ├── MdxCodeBlock.test.jsx
│ │ │ └── MdxComponents.jsx
│ │ ├── MdxPages
│ │ │ ├── FilteringAdvanced.mdx
│ │ │ ├── FilteringBeginner.mdx
│ │ │ ├── FilteringFullText.mdx
│ │ │ ├── HybridSearch.mdx
│ │ │ ├── Index.mdx
│ │ │ ├── LoadContent.mdx
│ │ │ ├── Multitenancy.mdx
│ │ │ ├── Multivectors.mdx
│ │ │ ├── Quickstart.mdx
│ │ │ └── SparseVectors.mdx
│ │ ├── TutorialFooter.jsx
│ │ └── TutorialSubpages.jsx
│ ├── JwtSection
│ │ ├── CallScrollRequest.jsx
│ │ ├── CollectionAccessDialog.jsx
│ │ ├── JwtForm.jsx
│ │ ├── JwtResultForm.jsx
│ │ ├── JwtTokenViewer.jsx
│ │ ├── PreviewTokenAccess.jsx
│ │ ├── RbacCollectionSettings.jsx
│ │ └── TokenValidatior.jsx
│ ├── Logo.jsx
│ ├── Notifications.jsx
│ ├── Points
│ │ ├── DataGridList.jsx
│ │ ├── PayloadEditor.jsx
│ │ ├── PointCard.jsx
│ │ ├── PointImage.jsx
│ │ ├── PointVectors.jsx
│ │ ├── PointsTabs.jsx
│ │ └── SimilarSerachfield.jsx
│ ├── ResultEditorWindow
│ │ └── index.jsx
│ ├── Sidebar
│ │ ├── Sidebar.jsx
│ │ └── SidebarTutorialSection.jsx
│ ├── Snapshots
│ │ ├── SnapshotUploadForm.jsx
│ │ ├── SnapshotsTab.jsx
│ │ ├── SnapshotsTableRow.jsx
│ │ └── SnapshotsUpload.jsx
│ ├── ToastNotifications
│ │ ├── ErrorNotifier.jsx
│ │ └── SuccessNotifier.jsx
│ ├── Uploader
│ │ ├── StyledDragDrop.jsx
│ │ └── StyledStatusBar.jsx
│ ├── UseTitle.jsx
│ ├── VisualizeChart
│ │ ├── index.jsx
│ │ ├── renderBy.js
│ │ ├── requestData.js
│ │ └── worker.js
│ └── authDialog
│ │ └── authDialog.jsx
├── config
│ └── restricted-routes.js
├── context
│ ├── client-context.jsx
│ ├── client-context.test.js
│ ├── color-context.jsx
│ ├── max-collections-context.jsx
│ └── tutorial-context.jsx
├── hooks
│ ├── useRouteAccess.js
│ └── windowHooks.js
├── index.css
├── index.jsx
├── lib
│ ├── common-helpers.js
│ ├── get-error-message.js
│ ├── graph-visualization-helpers.js
│ ├── rehype-meta-as-attributes.js
│ ├── tests
│ │ ├── common-helpers.test.js
│ │ └── graph-visualization-helpers.test.js
│ └── update-history.js
├── logo.svg
├── pages
│ ├── Collection.jsx
│ ├── Collections.jsx
│ ├── Console.jsx
│ ├── Datasets.jsx
│ ├── Graph.jsx
│ ├── Home.jsx
│ ├── Homepage.jsx
│ ├── Jwt.jsx
│ ├── Tutorial.jsx
│ ├── Visualize.jsx
│ └── Welcome.jsx
├── reportWebVitals.jsx
├── routes.jsx
├── setupTests.js
└── theme
│ ├── dark-theme.js
│ ├── index.js
│ └── light-theme.js
└── vite.config.js
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | browser: true
3 | es2021: true
4 | node: true
5 | settings:
6 | react:
7 | version: detect
8 | root: true
9 | extends:
10 | - eslint:recommended
11 | - google
12 | - prettier
13 | - plugin:react/recommended
14 | overrides: []
15 | parserOptions:
16 | ecmaVersion: latest
17 | sourceType: module
18 | plugins:
19 | - react
20 | rules:
21 | {
22 | 'max-len': [
23 | 'error',
24 | {
25 | code: 120,
26 | ignoreComments: true,
27 | ignoreTemplateLiterals: true,
28 | ignoreUrls: true,
29 | },
30 | ],
31 | 'no-process-env': 'off',
32 | 'require-jsdoc': 'off',
33 | 'no-unused-vars': [
34 | 'error',
35 | {
36 | varsIgnorePattern: '^React$',
37 | },
38 | ],
39 | }
40 | ignorePatterns: ['node_modules/', 'build/', '*.test.js', '*.test.jsx']
41 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: '/'
5 | open-pull-requests-limit: 0 # security updates only
6 | schedule:
7 | interval: daily
8 |
9 | - package-ecosystem: github-actions
10 | directory: '/'
11 | schedule:
12 | interval: daily
13 |
--------------------------------------------------------------------------------
/.github/workflows/publish-dist-packages.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3 |
4 | name: Dist build
5 |
6 | on:
7 | release:
8 | types: [created]
9 | workflow_dispatch:
10 | push:
11 | # Pattern matched against refs/tags
12 | tags:
13 | - '*' # Push events to every tag not containing /
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 | - uses: actions/setup-node@v4
21 | with:
22 | node-version: 18
23 | - run: npm ci
24 | - run: npm test -- --run
25 |
26 | publish-dist:
27 | needs: build
28 | runs-on: ubuntu-latest
29 | steps:
30 | - uses: actions/checkout@v4
31 | - uses: actions/setup-node@v4
32 | with:
33 | node-version: 18
34 | - run: npm ci
35 | - name: Set npm package version
36 | if: startsWith(github.ref, 'refs/tags/')
37 | run: npm version --allow-same-version --no-git-tag-version ${GITHUB_REF#refs/*/}
38 | - run: npm run build-qdrant
39 | - run: npm sbom --sbom-format spdx > dist/qdrant-web-ui.spdx.json
40 | - name: Archive dist folder
41 | uses: montudor/action-zip@v1
42 | with:
43 | args: zip -r dist-qdrant.zip dist
44 | - name: Release
45 | uses: softprops/action-gh-release@v2
46 | if: startsWith(github.ref, 'refs/tags/')
47 | with:
48 | files: dist-qdrant.zip
49 |
50 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 | # ESLint is a tool for identifying and reporting on patterns
6 | # found in ECMAScript/JavaScript code.
7 | # More details at https://github.com/eslint/eslint
8 | # and https://eslint.org
9 |
10 | name: ESLint
11 |
12 | on:
13 | push:
14 | branches: ["master"]
15 | pull_request:
16 | # The branches below must be a subset of the branches above
17 | branches: ["master"]
18 | schedule:
19 | - cron: "40 23 * * 1"
20 |
21 | jobs:
22 | test:
23 | name: Check the source code
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v4
27 | - uses: actions/setup-node@v4
28 | with:
29 | node-version: 18
30 | - name: Install packages
31 | run: npm ci
32 | - name: Audit
33 | run: npm audit
34 | - name: Lint
35 | run: npm run lint
36 | - name: Lint
37 | run: npm run format:check
38 | - name: Test
39 | run: npm run test
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /dist
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | # ide
27 | .idea
28 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 |
2 | dist
3 | build
4 | coverage
5 | node_modules
6 | **/*.md
7 | **/*.mdx
8 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "printWidth": 120
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Qdrant web-ui
2 |
3 | This is a self-hosted web UI for [Qdrant](https://github.com/qdrant/qdrant) Vector Search Engine.
4 |
5 | This UI is supposed to be served by Qdrant itself, but you can use it as a standalone application.
6 |
7 | Main goal of this UI is to provide a simple way to view and manage your collections.
8 |
9 | Similar to [Kibana](https://www.elastic.co/kibana) for Elasticsearch, but does not require any additional services.
10 |
11 | ## Available Scripts
12 |
13 | In the project directory, you can run:
14 |
15 | ### `npm start`
16 |
17 | Runs the app in the development mode.\
18 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
19 |
20 | Development mode expects that Qdrant is running on [http://localhost:6333](http://localhost:6333).
21 |
22 | ### `npm test`
23 |
24 | Launches the test runner in the interactive watch mode.\
25 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
26 |
27 | ### `npm run build`
28 |
29 | Builds the app for production to the `build` folder.\
30 | It correctly bundles React in production mode and optimizes the build for the best performance.
31 |
32 | The build is minified and the filenames include the hashes.\
33 | Your app is ready to be deployed!
34 |
35 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
36 |
37 | ## Stack used
38 |
39 | - [React](https://reactjs.org/)
40 | - [MUI](https://mui.com/core/)
41 | - [Axios](https://axios-http.com/)
42 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
25 | UI | Qdrant
26 |
27 |
28 |
29 |
30 |
31 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qdrant-web-ui",
3 | "version": "0.1.40",
4 | "license": "Apache-2.0",
5 | "private": true,
6 | "dependencies": {
7 | "@emotion/react": "^11.10.6",
8 | "@emotion/styled": "^11.10.6",
9 | "@mdx-js/react": "^2.3.0",
10 | "@mdx-js/rollup": "^2.3.0",
11 | "@monaco-editor/react": "^4.6.0",
12 | "@mui/icons-material": "^7.0.2",
13 | "@mui/material": "^7.0.2",
14 | "@mui/x-data-grid": "^7.29.1",
15 | "@qdrant/js-client-rest": "^1.14.1",
16 | "@saehrimnir/druidjs": "^0.6.3",
17 | "@testing-library/jest-dom": "^5.16.5",
18 | "@testing-library/react": "^13.4.0",
19 | "@testing-library/user-event": "^13.5.0",
20 | "@textea/json-viewer": "^2.16.2",
21 | "@uppy/core": "^4.4.4",
22 | "@uppy/dashboard": "^4.3.3",
23 | "@uppy/drag-drop": "^4.1.2",
24 | "@uppy/progress-bar": "^4.0.2",
25 | "@uppy/react": "^4.2.3",
26 | "@uppy/xhr-upload": "^4.2.3",
27 | "@vitejs/plugin-react": "^4.4.1",
28 | "autocomplete-openapi": "0.1.6",
29 | "axios": "^1.9.0",
30 | "chart.js": "^4.4.9",
31 | "chroma-js": "^2.4.2",
32 | "force-graph": "^1.43.5",
33 | "jose": "^5.2.3",
34 | "jsonc-parser": "^3.2.0",
35 | "lodash": "^4.17.21",
36 | "monaco-editor": "^0.44.0",
37 | "mui-chips-input": "^7.0.1",
38 | "notistack": "^3.0.1",
39 | "openapi-client-axios": "^7.1.1",
40 | "pretty-bytes": "^6.1.1",
41 | "prism-react-renderer": "^2.0.6",
42 | "prismjs": "^1.29.0",
43 | "prop-types": "^15.8.1",
44 | "react": "^18.2.0",
45 | "react-diff-viewer-continued": "^3.4.0",
46 | "react-dom": "^18.2.0",
47 | "react-resizable-panels": "^0.0.51",
48 | "react-router-dom": "^6.8.1",
49 | "react-simple-code-editor": "^0.13.1",
50 | "unist-util-visit": "^5.0.0",
51 | "vite": "^6.2.0",
52 | "vite-plugin-svgr": "^4.3.0",
53 | "web-vitals": "^2.1.4"
54 | },
55 | "optionalDependencies": {
56 | "@rollup/rollup-linux-x64-gnu": "*"
57 | },
58 | "scripts": {
59 | "test": "vitest",
60 | "eject": "vite eject",
61 | "start": "vite",
62 | "build": "vite build --base './'",
63 | "build-qdrant": "vite build --base '/dashboard/'",
64 | "serve": "vite preview",
65 | "lint": "eslint src --ext .js,.jsx,.ts,.tsx",
66 | "lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
67 | "format": "prettier ./src --write",
68 | "format:check": "prettier ./src --check"
69 | },
70 | "browserslist": {
71 | "production": [
72 | ">0.2%",
73 | "not dead",
74 | "not op_mini all"
75 | ],
76 | "development": [
77 | "last 1 chrome version",
78 | "last 1 firefox version",
79 | "last 1 safari version"
80 | ]
81 | },
82 | "jest": {
83 | "transformIgnorePatterns": [
84 | "/node_modules/(?!(axios|react-day-picker)/)"
85 | ]
86 | },
87 | "devDependencies": {
88 | "eslint": "^8.46.0",
89 | "eslint-config-google": "^0.14.0",
90 | "eslint-config-prettier": "^8.9.0",
91 | "eslint-plugin-react": "^7.32.2",
92 | "jsdom": "^22.1.0",
93 | "prettier": "2.8.7",
94 | "vite-plugin-eslint": "^1.8.1",
95 | "vitest": "^3.1.2"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qdrant/qdrant-web-ui/260e1af70908649054e87eb97c9745ee2d506c9c/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qdrant/qdrant-web-ui/260e1af70908649054e87eb97c9745ee2d506c9c/public/logo.png
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qdrant/qdrant-web-ui/260e1af70908649054e87eb97c9745ee2d506c9c/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qdrant/qdrant-web-ui/260e1af70908649054e87eb97c9745ee2d506c9c/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "UI | Qdrant",
3 | "name": "Qdrant Web UI",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #ffffff;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: rgb(0, 0, 0);
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRoutes } from 'react-router-dom';
3 | import routes from './routes';
4 | import useTitle from './components/UseTitle';
5 | import { ThemeProvider } from '@mui/material/styles';
6 | import { createTheme } from './theme';
7 | import { CssBaseline } from '@mui/material';
8 | import useMediaQuery from '@mui/material/useMediaQuery';
9 | import { ColorModeContext } from './context/color-context';
10 | import StyledMain from './components/Common/StyledMain';
11 |
12 | function NewApp() {
13 | const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
14 | const [customMode, setCustomMode] = React.useState(
15 | localStorage.getItem('qdrant-web-ui-theme') || (prefersDarkMode ? 'dark' : 'light')
16 | );
17 | const [mode, setMode] = React.useState(customMode === 'auto' ? (prefersDarkMode ? 'dark' : 'light') : customMode);
18 | localStorage.setItem('qdrant-web-ui-theme', customMode);
19 | const colorMode = React.useMemo(
20 | () => ({
21 | // The dark mode switch would invoke this method
22 | toggleColorMode: () => {
23 | setCustomMode((prevMode) => (prevMode === 'light' ? 'dark' : prevMode === 'dark' ? 'auto' : 'light'));
24 | },
25 | mode: customMode,
26 | }),
27 | [customMode]
28 | );
29 |
30 | React.useEffect(() => {
31 | if (customMode === 'auto') {
32 | setMode(prefersDarkMode ? 'dark' : 'light');
33 | } else {
34 | setMode(customMode);
35 | }
36 | }, [customMode, prefersDarkMode]);
37 |
38 | const theme = React.useMemo(
39 | () =>
40 | createTheme({
41 | palette: {
42 | mode,
43 | },
44 | }),
45 | [mode]
46 | );
47 |
48 | const routing = useRoutes(routes());
49 | useTitle('UI | Qdrant ');
50 |
51 | return (
52 |
53 |
54 |
55 | {routing}
56 |
57 |
58 | );
59 | }
60 |
61 | export default NewApp;
62 |
--------------------------------------------------------------------------------
/src/common/axios.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { getBaseURL } from './utils';
3 | import { bigIntJSON } from './bigIntJSON';
4 |
5 | export const axiosInstance = axios.create({
6 | baseURL: process.env.NODE_ENV === 'development' ? 'http://localhost:6333' : getBaseURL(),
7 | transformRequest: [
8 | function (data, headers) {
9 | if (data instanceof FormData) {
10 | return data;
11 | }
12 | headers['Content-Type'] = 'application/json';
13 | headers['x-inference-proxy'] = 'true';
14 | return bigIntJSON.stringify(data);
15 | },
16 | ],
17 | transformResponse: [
18 | function (data) {
19 | return bigIntJSON.parse(data);
20 | },
21 | ],
22 | });
23 |
24 | export function setupAxios(axios, { apiKey }) {
25 | if (process.env.NODE_ENV === 'development') {
26 | axios.defaults.baseURL = 'http://localhost:6333';
27 | } else {
28 | axios.defaults.baseURL = getBaseURL();
29 | }
30 | if (apiKey) {
31 | axios.defaults.headers.common['api-key'] = apiKey;
32 | }
33 | axios.defaults.transformRequest = [
34 | function (data, headers) {
35 | if (data instanceof FormData) {
36 | return data;
37 | }
38 | headers['Content-Type'] = 'application/json';
39 | headers['x-inference-proxy'] = 'true';
40 | return bigIntJSON.stringify(data);
41 | },
42 | ];
43 | axios.defaults.transformResponse = [
44 | function (data) {
45 | return bigIntJSON.parse(data);
46 | },
47 | ];
48 | }
49 |
--------------------------------------------------------------------------------
/src/common/bigIntJSON.js:
--------------------------------------------------------------------------------
1 | let bigintReviver;
2 | let bigintReplacer;
3 |
4 | if ('rawJSON' in JSON) {
5 | bigintReviver = function (_key, val, context) {
6 | if (Number.isInteger(val) && !Number.isSafeInteger(val)) {
7 | try {
8 | return BigInt(context?.source);
9 | } catch {
10 | return val;
11 | }
12 | }
13 | return val;
14 | };
15 | bigintReplacer = function (_key, val) {
16 | if (typeof val === 'bigint') {
17 | return JSON.rawJSON(String(val));
18 | }
19 | return val;
20 | };
21 | }
22 |
23 | export const bigIntJSON = {
24 | parse: function (text, reviver) {
25 | return JSON.parse(text, reviver || bigintReviver);
26 | },
27 | stringify: function (value, replacer, space) {
28 | return JSON.stringify(value, replacer || bigintReplacer, space);
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/src/common/client.js:
--------------------------------------------------------------------------------
1 | import { QdrantClient } from '@qdrant/js-client-rest';
2 | import { getBaseURL } from './utils';
3 |
4 | /**
5 | * Extended QdrantClient class with additional methods
6 | * @class
7 | * @extends QdrantClient
8 | */
9 | export class QdrantClientExtended extends QdrantClient {
10 | #downloadController;
11 | constructor({ url, apiKey, port }) {
12 | super({ url, apiKey, port, checkCompatibility: false, headers: { 'x-inference-proxy': 'true' } });
13 |
14 | this.downloadSnapshot = this.downloadSnapshot.bind(this);
15 | this.getSnapshotUploadUrl = this.getSnapshotUploadUrl.bind(this);
16 | this.getApiKey = this.getApiKey.bind(this);
17 | this.abortDownload = this.abortDownload.bind(this);
18 |
19 | this.url = url;
20 | this.apiKey = apiKey;
21 | this.port = port;
22 |
23 | this.#downloadController = new AbortController();
24 | }
25 |
26 | /**
27 | * Download snapshot from the server
28 | * @param {string} collectionName - name of the collection
29 | * @param {string} snapshotName - name of the snapshot
30 | * @param {boolean} blob - return blob instead of response, default false
31 | * @return {Promise} - promise with response or blob
32 | * @example Download snapshot
33 | * const response = await client.downloadSnapshot('collection', 'snapshot');
34 | * const blob = await client.downloadSnapshot('collection', 'snapshot', true);
35 | */
36 | async downloadSnapshot(collectionName, snapshotName, blob = false) {
37 | const headers = {
38 | 'Content-Disposition': `attachment; filename="${snapshotName}"`,
39 | 'Content-Type': 'application/gzip',
40 | };
41 |
42 | if (this.apiKey) {
43 | headers['api-key'] = this.apiKey;
44 | }
45 |
46 | const snapshotUrl = new URL(`/collections/${collectionName}/snapshots/${snapshotName}`, this.url).href;
47 |
48 | const request = new Request(snapshotUrl, {
49 | method: 'GET',
50 | headers,
51 | });
52 |
53 | const response = await fetch(request, { signal: this.#downloadController.signal });
54 |
55 | if (blob) {
56 | return await response.blob();
57 | }
58 |
59 | return response;
60 | }
61 |
62 | abortDownload() {
63 | this.#downloadController.abort();
64 | this.#downloadController = new AbortController();
65 | }
66 |
67 | /**
68 | * Get url for snapshot upload
69 | * @param {string} collectionName
70 | * @return {module:url.URL} - URL object for snapshot upload
71 | */
72 | getSnapshotUploadUrl(collectionName) {
73 | return new URL(`collections/${collectionName}/snapshots/upload`, this.url);
74 | }
75 |
76 | /**
77 | * Get api key
78 | * @return {string} - api key
79 | */
80 | getApiKey() {
81 | return this.apiKey;
82 | }
83 | }
84 |
85 | export default function qdrantClient({ apiKey }) {
86 | let url;
87 | let port = 6333;
88 | if (process.env.NODE_ENV === 'development') {
89 | url = 'http://localhost:6333';
90 | } else {
91 | url = getBaseURL();
92 | if (window.location.port) {
93 | port = window.location.port;
94 | } else {
95 | if (window.location.protocol === 'https:') {
96 | port = 443;
97 | } else {
98 | port = 80;
99 | }
100 | }
101 | }
102 |
103 | const options = {
104 | url,
105 | apiKey,
106 | port,
107 | };
108 |
109 | return new QdrantClientExtended(options);
110 | }
111 |
--------------------------------------------------------------------------------
/src/common/client.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, afterEach, vi } from 'vitest';
2 | import { QdrantClientExtended } from './client.js';
3 |
4 | // tests for QdrantClientExtended
5 | describe('QdrantClientExtended', () => {
6 | describe('downloadSnapshot', () => {
7 | it('fetch should be called with correct arguments', async () => {
8 | const fetch = vi.fn();
9 | vi.stubGlobal('fetch', fetch);
10 | const fetchSpy = vi.spyOn(global, 'fetch');
11 | const controller = new AbortController();
12 |
13 | const apiKey = 'test-api-key';
14 | const url = 'http://localhost';
15 | const port = 3000;
16 |
17 | const collectionName = 'demo';
18 | const snapshotName = 'test-snapshot';
19 |
20 | const client = new QdrantClientExtended({
21 | url,
22 | apiKey,
23 | port,
24 | });
25 | await client.downloadSnapshot(collectionName, snapshotName);
26 |
27 | expect(fetchSpy).toHaveBeenCalledTimes(1);
28 |
29 | // Extract the actual arguments
30 | const [actualRequest, actualOptions] = fetchSpy.mock.calls[0];
31 |
32 | // Check the URL and method
33 | expect(actualRequest.url).toBe(`${url}/collections/${collectionName}/snapshots/${snapshotName}`);
34 | expect(actualRequest.method).toBe('GET');
35 |
36 | // Check the headers
37 | expect(actualRequest.headers.get('Content-Disposition')).toBe(`attachment; filename="${snapshotName}"`);
38 | expect(actualRequest.headers.get('Content-Type')).toBe('application/gzip');
39 | expect(actualRequest.headers.get('api-key')).toBe(apiKey);
40 | });
41 | });
42 |
43 | // test for getSnapshotUploadUrl
44 | describe('getSnapshotUploadUrl', () => {
45 | // method returns url
46 | it('should return url', () => {
47 | const client = new QdrantClientExtended({
48 | url: 'http://localhost',
49 | apiKey: 'test',
50 | port: 3000,
51 | });
52 |
53 | expect(client.getSnapshotUploadUrl('test').href).toBe(
54 | new URL('collections/test/snapshots/upload', 'http://localhost').href
55 | );
56 | });
57 | });
58 |
59 | // test for getApiKey
60 | describe('getApiKey', () => {
61 | // method returns apiKey
62 | it('should return apiKey', () => {
63 | const client = new QdrantClientExtended({
64 | url: 'http://localhost',
65 | apiKey: 'test',
66 | port: 3000,
67 | });
68 |
69 | expect(client.getApiKey()).toEqual('test');
70 | });
71 | });
72 |
73 | // clear fetch mock
74 | afterEach(() => {
75 | vi.unstubAllGlobals();
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/src/common/urils.test.js:
--------------------------------------------------------------------------------
1 | import { afterEach, describe, expect, it, vi } from 'vitest';
2 | import { getBaseURL, pumpFile, updateProgress } from './utils';
3 |
4 | describe('utils', () => {
5 | describe('getBaseURL', () => {
6 | it('should return base url', () => {
7 | const window = {
8 | location: {
9 | href: 'http://localhost:63342/dashboard',
10 | },
11 | };
12 | vi.stubGlobal('window', window);
13 |
14 | const result = 'http://localhost:63342/';
15 |
16 | expect(getBaseURL()).toEqual(result);
17 | });
18 |
19 | it('should return base url with pathname', () => {
20 | const window = {
21 | location: {
22 | href: 'http://localhost:63342/myapp/dashboard',
23 | },
24 | };
25 | vi.stubGlobal('window', window);
26 |
27 | const result = 'http://localhost:63342/myapp/';
28 |
29 | expect(getBaseURL()).toEqual(result);
30 | });
31 | });
32 |
33 | describe('pumpFile', () => {
34 | it('should return chunks', async () => {
35 | let readNumber = 0;
36 | const reader = {
37 | read: () => {
38 | readNumber += 1;
39 | if (readNumber < 3) {
40 | return Promise.resolve({ done: false, value: `test${readNumber}` });
41 | } else {
42 | return Promise.resolve({ done: true });
43 | }
44 | },
45 | };
46 | const callback = () => {};
47 | const chunks = [];
48 | const result = await pumpFile(reader, callback, chunks);
49 | expect(result).toEqual(['test1', 'test2']);
50 | });
51 | });
52 |
53 | describe('updateProgress', () => {
54 | it('should pass progress in percents in callback', () => {
55 | const snapshotSize = 10;
56 | let result = 0;
57 | const callback = (value) => {
58 | result += value;
59 | };
60 | updateProgress(snapshotSize, callback)(1);
61 | expect(result).toEqual(10);
62 | updateProgress(snapshotSize, callback)(1);
63 | expect(result).toEqual(20);
64 | });
65 | });
66 | });
67 |
68 | afterEach(() => {
69 | vi.unstubAllGlobals();
70 | });
71 |
--------------------------------------------------------------------------------
/src/common/utils.js:
--------------------------------------------------------------------------------
1 | export const getBaseURL = function () {
2 | const url = new URL(window.location.href);
3 | const pathname = url.pathname.replace(/dashboard$/, '');
4 | return new URL(pathname, url.href).href;
5 | };
6 |
7 | export const pumpFile = function (reader, callback, chunks = []) {
8 | return reader.read().then(({ done, value }) => {
9 | if (done) {
10 | return chunks;
11 | }
12 | callback(value.length);
13 | chunks.push(value);
14 | return pumpFile(reader, callback, chunks);
15 | });
16 | };
17 |
18 | export const updateProgress = function (snapshotSize, callback) {
19 | let loaded = 0;
20 |
21 | return (chunkSize) => {
22 | loaded += chunkSize;
23 |
24 | const total = snapshotSize ? parseInt(snapshotSize, 10) : null;
25 | const newProgress = Math.round((loaded / total) * 100);
26 | callback(newProgress);
27 | };
28 | };
29 |
30 | const uuidRegex =
31 | /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
32 |
33 | export const validateUuid = function (uuid) {
34 | return typeof uuid === 'string' && uuidRegex.test(uuid);
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/Menu/CommandsDrawer/CommandDrawer.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen, waitFor } from '@testing-library/react';
2 | import CommandsDrawer from './CommandsDrawer';
3 | import { act } from 'react';
4 |
5 | const openApiJson = {
6 | paths: {
7 | '/metrics': {
8 | get: {
9 | summary: 'Collect Prometheus metrics data',
10 | tags: ['service'],
11 | },
12 | },
13 | '/cluster': {
14 | get: {
15 | tags: ['cluster'],
16 | summary: 'Get cluster status info',
17 | },
18 | },
19 | '/cluster/peer/{peer_id}': {
20 | delete: {
21 | tags: ['cluster'],
22 | summary: 'Remove peer from the cluster',
23 | },
24 | },
25 | },
26 | };
27 |
28 | function createFetchResponse(data) {
29 | return { json: () => new Promise((resolve) => resolve(data)) };
30 | }
31 |
32 | const fetch = vi.fn();
33 | fetch.mockResolvedValue(createFetchResponse(openApiJson));
34 | vi.stubGlobal('fetch', fetch);
35 |
36 | describe('CommandsDrawer', () => {
37 | it('should render CommandsDrawer with fetched data', async () => {
38 | const commands = [];
39 |
40 | await act(async () => {
41 | render(
42 | {}}
45 | handleInsertCommand={(command) => {
46 | commands.push(command);
47 | }}
48 | />
49 | );
50 | });
51 |
52 | expect(screen.getByRole('textbox')).toBeInTheDocument();
53 | expect(screen.getByRole('textbox')).toHaveFocus();
54 | expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'GET collections');
55 |
56 | expect(screen.getByTestId('commands-table')).toBeInTheDocument();
57 |
58 | await waitFor(() => expect(screen.getByTestId('commands-table').firstChild.children.length).toBe(3), {
59 | timeout: 1500,
60 | });
61 |
62 | function getNthRow(n) {
63 | const row = screen.getByTestId('commands-table').children[0].children[n];
64 |
65 | return {
66 | method: row.children[1].children[0].children[0].children[0].textContent,
67 | command: row.children[1].children[0].children[1].children[0].textContent,
68 | description: row.children[1].children[0].children[1].children[2].textContent,
69 | };
70 | }
71 |
72 | for (let i = 0; i < 3; i++) {
73 | const { method, command, description } = getNthRow(i);
74 | const openApiCommand = Object.keys(openApiJson.paths)[i];
75 | const openApiJsonPath = openApiJson.paths[openApiCommand];
76 |
77 | expect(method).toBe(Object.keys(openApiJsonPath)[0].toUpperCase());
78 | expect(command).toBe(openApiCommand.replace(/{/g, '<').replace(/}/g, '>'));
79 | expect(description).toBe(openApiJsonPath[Object.keys(openApiJsonPath)[0]].summary);
80 | }
81 | });
82 | });
83 |
84 | afterAll(() => {
85 | vi.unstubAllGlobals();
86 | });
87 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/Menu/CommandsDrawer/CommandSearch.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import _ from 'lodash';
4 | import { InputAdornment, TextField } from '@mui/material';
5 | import { Search } from '@mui/icons-material';
6 |
7 | const CommandSearch = ({ commands, setCommands }) => {
8 | const ref = React.useRef(null);
9 |
10 | const handleSearch = (event) => {
11 | const value = event.target.value;
12 |
13 | if (value === '') {
14 | setCommands(commands);
15 | } else {
16 | const searchTerms = value.split(' ');
17 | const nextCommands = commands
18 | .reduce((acc, command) => {
19 | const commandTerms = [command.method, command.command, command.description];
20 | const matches = searchTerms.reduce((acc, searchTerm) => {
21 | const escapedSearchTerm = _.escapeRegExp(searchTerm);
22 | const regex = new RegExp(`\\b(${escapedSearchTerm})`, 'gmi');
23 | return acc + commandTerms.join(' ').match(regex)?.length;
24 | }, 0);
25 |
26 | if (matches > 0) {
27 | acc.push({ ...command, matches });
28 | }
29 |
30 | return acc;
31 | }, [])
32 | .sort((a, b) => b.matches - a.matches);
33 |
34 | setCommands(nextCommands);
35 | }
36 | };
37 |
38 | // set focus on mount
39 | React.useEffect(() => {
40 | ref.current.focus();
41 | }, []);
42 |
43 | return (
44 |
54 |
55 |
56 | ),
57 | }}
58 | onChange={handleSearch}
59 | />
60 | );
61 | };
62 |
63 | CommandSearch.propTypes = {
64 | commands: PropTypes.array.isRequired,
65 | setCommands: PropTypes.func.isRequired,
66 | };
67 |
68 | export default CommandSearch;
69 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/Menu/CommandsDrawer/CommandsDrawer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Box, Drawer, Typography, IconButton } from '@mui/material';
4 | import CommandsTable from './CommandsTable';
5 | import CommandSearch from './CommandSearch';
6 | import Close from '@mui/icons-material/Close';
7 | import useMediaQuery from '@mui/material/useMediaQuery';
8 | import { useSnackbar } from 'notistack';
9 | import { getSnackbarOptions } from '../../../Common/utils/snackbarOptions';
10 | import { resolveRequiredBodyParams } from '../../config/CommandsDrawerUtils';
11 |
12 | const CommandsDrawer = ({ open, toggleDrawer, handleInsertCommand }) => {
13 | const [allCommands, setAllCommands] = useState([]);
14 | const [commands, setCommands] = useState([]);
15 | const matchesMdMedia = useMediaQuery('(max-width: 992px)');
16 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
17 | const errorSnackbarOptions = getSnackbarOptions('error', closeSnackbar, 6000);
18 |
19 | useEffect(() => {
20 | fetch(import.meta.env.BASE_URL + './openapi.json')
21 | .then((response) => response.json())
22 | .then((data) => {
23 | const nextCommands = Object.keys(data.paths)
24 | .map((path) => {
25 | return Object.keys(data.paths[path]).map((method) => {
26 | const command = path.replace(/{/g, '<').replace(/}/g, '>');
27 | const description = data.paths[path][method].summary;
28 | const tags = data.paths[path][method].tags;
29 | const hasRequestBody = !!data.paths[path][method].requestBody;
30 | let requiredBodyParameters = null;
31 | if (hasRequestBody) {
32 | requiredBodyParameters = resolveRequiredBodyParams(data, data.paths[path][method].requestBody.content);
33 | }
34 |
35 | return {
36 | method: method.toUpperCase(),
37 | command,
38 | description,
39 | hasRequestBody,
40 | tags,
41 | requiredBodyParameters,
42 | };
43 | });
44 | })
45 | .flat();
46 | setAllCommands(nextCommands);
47 | setCommands(nextCommands);
48 | })
49 | .catch((e) => {
50 | enqueueSnackbar('Error fetching commands', errorSnackbarOptions);
51 | console.error(e);
52 | });
53 | }, []);
54 |
55 | return (
56 |
72 |
73 |
74 | Commands
75 |
76 |
77 |
78 |
79 |
80 |
81 | This is a list of commands that can be used in the editor.
82 |
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | CommandsDrawer.propTypes = {
91 | open: PropTypes.bool.isRequired,
92 | toggleDrawer: PropTypes.func.isRequired,
93 | handleInsertCommand: PropTypes.func.isRequired,
94 | };
95 |
96 | export default CommandsDrawer;
97 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/Menu/SpeedDialMenu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import SpeedDial from '@mui/material/SpeedDial';
4 | import Bolt from '@mui/icons-material/Bolt';
5 | import SpeedDialAction from '@mui/material/SpeedDialAction';
6 |
7 | function SpeedDialMenu({ actions }) {
8 | const [open, setOpen] = React.useState(false);
9 | const handleOpen = () => setOpen(true);
10 | const handleClose = () => setOpen(false);
11 |
12 | const actionsList = actions.map((action) => (
13 |
14 | ));
15 |
16 | return (
17 | }
21 | onClose={handleClose}
22 | onOpen={handleOpen}
23 | open={open}
24 | onClick={handleOpen}
25 | FabProps={{
26 | size: 'small',
27 | sx: {
28 | boxShadow: 1,
29 | },
30 | }}
31 | >
32 | {actionsList}
33 |
34 | );
35 | }
36 |
37 | // props validation
38 | SpeedDialMenu.propTypes = {
39 | actions: PropTypes.arrayOf(
40 | PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.element]))
41 | ),
42 | };
43 |
44 | export default SpeedDialMenu;
45 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/Menu/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Stack } from '@mui/material';
3 | import History from './history';
4 | import PropTypes from 'prop-types';
5 | import SavedCode from './savedCode';
6 |
7 | // deprecated
8 | function Menu({ code, handleEditorChange }) {
9 | const [state, setState] = React.useState({
10 | history: false,
11 | savedCode: false,
12 | });
13 |
14 | const toggleDrawer = (name, open) => () => {
15 | setState({ ...state, [name]: open });
16 | };
17 |
18 | return (
19 |
20 | `1px solid ${theme.palette.divider}`,
27 | }}
28 | >
29 |
30 |
31 |
32 |
33 |
39 |
40 | );
41 | }
42 | Menu.propTypes = {
43 | code: PropTypes.string,
44 | handleEditorChange: PropTypes.func,
45 | };
46 |
47 | export default Menu;
48 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/config/Autocomplete.js:
--------------------------------------------------------------------------------
1 | import { OpenapiAutocomplete } from 'autocomplete-openapi/src/autocomplete';
2 | import { enhanceSnippet } from './snippetEnhancer';
3 | import { customSnippets } from './customSnippets';
4 |
5 | export const autocomplete = async (monaco, qdrantClient) => {
6 | const response = await fetch(import.meta.env.BASE_URL + './openapi.json');
7 | const openapi = await response.json();
8 |
9 | let collections = [];
10 | try {
11 | collections = (await qdrantClient.getCollections()).collections.map((c) => c.name);
12 | } catch (e) {
13 | console.error(e);
14 | }
15 |
16 | const autocomplete = new OpenapiAutocomplete(openapi, collections);
17 | let snippets = autocomplete.getSnippets();
18 | snippets = [...snippets, ...customSnippets];
19 | snippets = snippets.map((snippet) => {
20 | snippet.insertText = enhanceSnippet(snippet.insertText, collections);
21 | return snippet;
22 | });
23 |
24 | return {
25 | provideCompletionItems: (model, position) => {
26 | let suggestions = [];
27 | // Reuse parsed code blocks to avoid parsing the same code block multiple times
28 | const selectedCodeBlock = monaco.editor.selectedCodeBlock;
29 |
30 | if (!selectedCodeBlock) {
31 | suggestions = [
32 | ...suggestions,
33 | {
34 | label: 'POST',
35 | kind: 17,
36 | insertText: 'POST',
37 | },
38 | {
39 | label: 'GET',
40 | kind: 17,
41 | insertText: 'GET',
42 | },
43 | {
44 | label: 'PUT',
45 | kind: 17,
46 | insertText: 'PUT',
47 | },
48 | {
49 | label: 'DELETE',
50 | kind: 17,
51 | insertText: 'DELETE',
52 | },
53 | {
54 | label: 'PATCH',
55 | kind: 17,
56 | insertText: 'PATCH',
57 | },
58 | ];
59 | const word = model.getWordUntilPosition(position);
60 | snippets.forEach((snippet) => {
61 | suggestions.push({
62 | label: snippet.documentation,
63 | kind: 1,
64 | documentation: snippet.documentation,
65 | insertText: snippet.insertText,
66 | insertTextRules: 4,
67 | range: {
68 | startLineNumber: position.lineNumber,
69 | endLineNumber: position.lineNumber,
70 | startColumn: 0,
71 | endColumn: word.endColumn,
72 | },
73 | });
74 | });
75 |
76 | return { suggestions };
77 | }
78 |
79 | const relativeLine = position.lineNumber - selectedCodeBlock.blockStartLine;
80 |
81 | if (relativeLine < 0) {
82 | // Something went wrong
83 | return { suggestions: [] };
84 | }
85 |
86 | if (relativeLine === 0) {
87 | // Autocomplete for request headers
88 | const header = selectedCodeBlock.blockText.slice(0, position.column - 1);
89 |
90 | suggestions = autocomplete.completeRequestHeader(header);
91 |
92 | suggestions = suggestions
93 | .filter((s) => s !== '')
94 | .map((s) => {
95 | return {
96 | label: s,
97 | kind: 17,
98 | insertText: s,
99 | };
100 | });
101 |
102 | return { suggestions };
103 | } else {
104 | // Autocomplete for request body
105 | const requestLines = selectedCodeBlock.blockText.split(/\r?\n/);
106 |
107 | const lastLine = requestLines[relativeLine].slice(0, position.column - 1);
108 |
109 | const requestHeader = requestLines.shift();
110 |
111 | const requestBodyLines = requestLines.slice(0, relativeLine - 1);
112 |
113 | requestBodyLines.push(lastLine);
114 |
115 | const requestBody = requestBodyLines.join('\n');
116 |
117 | let suggestions = autocomplete.completeRequestBody(requestHeader, requestBody);
118 |
119 | suggestions = suggestions.reduce((acc, s) => {
120 | if (acc.findIndex((a) => a.label === s) === -1) {
121 | acc.push({
122 | label: s,
123 | kind: 17,
124 | insertText: s,
125 | });
126 | }
127 | return acc;
128 | }, []);
129 |
130 | return { suggestions };
131 | }
132 | },
133 | triggerCharacters: ['/', '"', ': ', ' '],
134 | };
135 | };
136 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/config/CommandsDrawerUtils.js:
--------------------------------------------------------------------------------
1 | export const resolveRequiredBodyParams = function (openapi, requestBodyContent) {
2 | const contentType = Object.keys(requestBodyContent)[0];
3 | if ('$ref' in requestBodyContent[contentType].schema) {
4 | const refPath = requestBodyContent[contentType].schema.$ref;
5 | // parse and navigate to the ref
6 | const pathComponents = refPath.slice(2).split('/');
7 | const schema = pathComponents.reduce((doc, pathComponent) => doc[pathComponent], openapi);
8 | return schema.required;
9 | } else {
10 | return requestBodyContent[contentType].schema.required;
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/config/ErrorMarker.js:
--------------------------------------------------------------------------------
1 | import { getCodeBlocks } from '../../EditorCommon/config/Rules';
2 | const keywords = ['POST', 'GET', 'PUT', 'DELETE', 'HEAD'];
3 |
4 | export let ErrorMarker = [];
5 |
6 | // example format to generate error
7 | // let err = {
8 | // message: 'unknow type',
9 | // line: 4,
10 | // column: 5,
11 | // length: 5
12 | // };
13 |
14 | const err = null;
15 | if (err) {
16 | ErrorMarker.push({
17 | startLineNumber: err.line,
18 | endLineNumber: err.line,
19 | startColumn: err.column,
20 | endColumn: err.column + err.length,
21 | message: err.message,
22 | severity: 'monaco.MarkerSeverity.Error',
23 | });
24 | }
25 | export function errChecker(code) {
26 | const blocksArray = getCodeBlocks(code);
27 | ErrorMarker = [];
28 | for (let i = 0; i < blocksArray.length; ++i) {
29 | const headLineArray = blocksArray[i].blockText.split(/\r?\n/)[0].split(' ');
30 | if (keywords.indexOf(headLineArray[0]) < 0) {
31 | ErrorMarker.push({
32 | startLineNumber: blocksArray[i].blockStartLine,
33 | endLineNumber: blocksArray[i].blockStartLine,
34 | startColumn: 0,
35 | endColumn: headLineArray[0].length,
36 | message: 'Expected one of GET/POST/PUT/DELETE/HEAD',
37 | severity: 'monaco.MarkerSeverity.Error',
38 | });
39 | }
40 | }
41 | return;
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/config/RequesFromCode.js:
--------------------------------------------------------------------------------
1 | import { axiosInstance as axios } from '../../../common/axios';
2 | import { stripComments } from 'jsonc-parser';
3 | import { updateHistory } from '../../../lib/update-history';
4 | import { bigIntJSON } from '../../../common/bigIntJSON';
5 |
6 | export function requestFromCode(text, withHistory = true) {
7 | const data = codeParse(text);
8 | if (data.error) {
9 | return data;
10 | } else {
11 | // Sending request
12 |
13 | return axios({
14 | method: data.method,
15 | url: data.endpoint,
16 | data: data.reqBody,
17 | })
18 | .then((response) => {
19 | if (withHistory) updateHistory(data);
20 | return response.data;
21 | })
22 | .catch((err) => {
23 | console.log(err);
24 | return err.response?.data?.status ? err.response?.data?.status : err;
25 | });
26 | }
27 | }
28 |
29 | export function codeParse(codeText) {
30 | const codeArray = codeText.split(/\r?\n/);
31 | let headerLine = codeArray.shift();
32 | // Remove possible comments
33 | headerLine = headerLine.replace(/\/\/.*$/gm, '');
34 | const body = codeArray.join('\n');
35 | // Extract the header
36 | const method = headerLine.split(' ')[0];
37 | const endpoint = headerLine.split(' ')[1];
38 |
39 | let reqBody = {};
40 | if (body) {
41 | try {
42 | reqBody = body === '\n' ? {} : bigIntJSON.parse(stripComments(body));
43 | } catch (e) {
44 | return {
45 | method: null,
46 | endpoint: null,
47 | reqBody: null,
48 | error: 'Fix the Position brackets to run & check the json',
49 | };
50 | }
51 | }
52 | if (method === '' && endpoint === '') {
53 | return {
54 | method: null,
55 | endpoint: null,
56 | reqBody: reqBody,
57 | error: 'Add Headline or remove the line gap between json and headline (if any)',
58 | };
59 | } else if (method === '') {
60 | return {
61 | method: null,
62 | endpoint: endpoint,
63 | reqBody: reqBody,
64 | error: 'Add method',
65 | };
66 | } else if (endpoint === '') {
67 | return {
68 | method: method,
69 | endpoint: null,
70 | reqBody: reqBody,
71 | error: 'Add endpoint',
72 | };
73 | } else {
74 | return {
75 | method: method,
76 | endpoint: endpoint,
77 | reqBody: reqBody,
78 | error: null,
79 | };
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/config/customSnippets.js:
--------------------------------------------------------------------------------
1 | export const customSnippets = [
2 | {
3 | documentation: 'Create Simple Collection',
4 | insertText: `PUT collections/\${1:new_collection_name}
5 | {
6 | "vectors": {
7 | "size": \${2:768},
8 | "distance": "\${3|Dot,Cosine,Euclid,Manhattan|}"
9 | }
10 | }`,
11 | },
12 | {
13 | documentation: 'Create Hybrid Collection',
14 | insertText: `PUT collections/\${1:new_collection_name}
15 | {
16 | "vectors": {
17 | "\${2:dense_vector_name}": {
18 | "size": \${3:768},
19 | "distance": "\${4|Dot,Cosine,Euclid,Manhattan|}"
20 | }
21 | },
22 | "sparse_vectors": {
23 | "\${5:sparse_vector_name}": {
24 | "modifier": "\${6|idf,none|}",
25 | "index": {
26 | "on_disk": \${7|true,false|}
27 | }
28 | }
29 | }
30 | }`,
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/config/snippetEnhancer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This function take a snippet and looks up for collection placeholders and replaces them with the actual collection names
3 | * Example input:
4 | * PUT /collections/${1:collection_name}/shards
5 | * {
6 | * "shard_key": ${2:shared_key}
7 | * }
8 | *
9 | * Example output:
10 | * PUT /collections/${1|my_collection,second_collection,anotherOne|}/shards
11 | * {
12 | * "shard_key": ${2:shared_key}
13 | * }
14 | * @param {string} snippet - the snippet to enhance
15 | * @param {Array} collections - the collections to use for autocompletion
16 | * @return {string} - the enhanced snippet
17 | */
18 | export const enhanceSnippet = (snippet, collections) => {
19 | let enhancedSnippet = snippet;
20 | const collectionPlaceholders = enhancedSnippet.match(/\$\{(\d+):collection_name\}/g);
21 | if (collectionPlaceholders) {
22 | for (const collectionPlaceholder of collectionPlaceholders) {
23 | const placeholderId = collectionPlaceholder.match(/\d+/)[0];
24 | if (collections.length > 0) {
25 | const collectionAutocomplete = `\${${placeholderId}|${collections.join(',')}|}`;
26 | enhancedSnippet = enhancedSnippet.replace(collectionPlaceholder, collectionAutocomplete);
27 | }
28 | }
29 | }
30 | return enhancedSnippet;
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/config/snippetEnhancer.test.js:
--------------------------------------------------------------------------------
1 | import { enhanceSnippet } from './snippetEnhancer';
2 |
3 | describe('snippetEnhancer', () => {
4 | it('should replace collection placeholders with the actual collection names', () => {
5 | const snippet =
6 | 'PUT /collections/${1:collection_name}/shards\n' +
7 | ' {\n' +
8 | ' "shard_key": ${2:shared_key}\n' +
9 | ' }';
10 | const collections = ['my_collection', 'second_collection', 'anotherOne'];
11 | const enhancedSnippet = enhanceSnippet(snippet, collections);
12 | expect(enhancedSnippet).toBe(
13 | 'PUT /collections/${1|my_collection,second_collection,anotherOne|}/shards\n' +
14 | ' {\n' +
15 | ' "shard_key": ${2:shared_key}\n' +
16 | ' }'
17 | );
18 | });
19 |
20 | it('should replace collection placeholders with the actual collection names', () => {
21 | const snippet =
22 | 'PUT /collections/${9:collection_name}/shards\n' +
23 | ' {\n' +
24 | ' "shard_key": ${2:shared_key}\n' +
25 | ' }';
26 | const collections = ['my_collection', 'second_collection', 'anotherOne'];
27 | const enhancedSnippet = enhanceSnippet(snippet, collections);
28 | expect(enhancedSnippet).toBe(
29 | 'PUT /collections/${9|my_collection,second_collection,anotherOne|}/shards\n' +
30 | ' {\n' +
31 | ' "shard_key": ${2:shared_key}\n' +
32 | ' }'
33 | );
34 | });
35 |
36 | it('should not replace collection placeholders if there are no collections', () => {
37 | const snippet =
38 | 'PUT /collections/${1:collection_name}/shards\n' +
39 | ' {\n' +
40 | ' "shard_key": ${2:shared_key}\n' +
41 | ' }';
42 | const collections = [];
43 | const enhancedSnippet = enhanceSnippet(snippet, collections);
44 | expect(enhancedSnippet).toBe(snippet);
45 | });
46 |
47 | it('should not replace collection placeholders if there are no collection placeholders', () => {
48 | const snippet =
49 | 'PUT /collections/collection_name/shards\n' +
50 | ' {\n' +
51 | ' "shard_key": ${2:shared_key}\n' +
52 | ' }';
53 | const collections = ['my_collection', 'second_collection', 'anotherOne'];
54 | const enhancedSnippet = enhanceSnippet(snippet, collections);
55 | expect(enhancedSnippet).toBe(snippet);
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/src/components/CodeEditorWindow/editor.css:
--------------------------------------------------------------------------------
1 | .light .blockSelector {
2 | background: rgba(163, 161, 161, 0.24);
3 | }
4 |
5 | .light .blockSelectorStrip {
6 | background: rgba(255, 252, 88, 0.87);
7 | }
8 |
9 | .dark .blockSelector {
10 | background: #2d2d3099;
11 | }
12 |
13 | .dark .blockSelectorStrip {
14 | background: #525100cc;
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Collections/CollectionCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link } from 'react-router-dom';
4 | import { Box, Card, CardContent, Divider, Stack, CardActionArea, Typography, Button } from '@mui/material';
5 | import DeleteIcon from '@mui/icons-material/Delete';
6 | import PolylineIcon from '@mui/icons-material/Polyline';
7 | import DeleteDialog from './DeleteDialog';
8 |
9 | const CollectionCard = (props) => {
10 | const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false);
11 | const { collection, getCollectionsCall } = props;
12 |
13 | return (
14 | <>
15 |
23 |
24 |
25 |
26 | {collection.name}
27 |
28 |
29 |
30 |
31 |
32 |
33 | {/* temporary disabled */}
34 | {false && (
35 | }
41 | >
42 | visualize
43 |
44 | )}
45 |
48 |
49 |
50 |
56 | >
57 | );
58 | };
59 |
60 | CollectionCard.propTypes = {
61 | collection: PropTypes.object.isRequired,
62 | getCollectionsCall: PropTypes.func.isRequired,
63 | };
64 |
65 | export default CollectionCard;
66 |
--------------------------------------------------------------------------------
/src/components/Collections/CollectionCluster/ClusterInfo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Card, CardHeader, Table, TableBody } from '@mui/material';
4 | import { CopyButton } from '../../Common/CopyButton';
5 | import ClusterInfoHead from './ClusterInfoHead';
6 | import ClusterShardRow from './ClusterShardRow';
7 | import { bigIntJSON } from '../../../common/bigIntJSON';
8 |
9 | const ClusterInfo = ({ collectionCluster = { result: {} }, ...other }) => {
10 | const shards = [
11 | ...(collectionCluster.result?.local_shards || []),
12 | ...(collectionCluster.result?.remote_shards || []),
13 | ];
14 |
15 | const shardRows = shards.map((shard) => (
16 |
21 | ));
22 |
23 | return (
24 |
25 | }
32 | />
33 |
34 |
35 | {shardRows}
36 |
37 |
38 | );
39 | };
40 |
41 | ClusterInfo.propTypes = {
42 | collectionCluster: PropTypes.shape({
43 | result: PropTypes.shape({
44 | peer_id: PropTypes.number,
45 | local_shards: PropTypes.arrayOf(
46 | PropTypes.shape({
47 | shard_id: PropTypes.number,
48 | state: PropTypes.string,
49 | })
50 | ),
51 | remote_shards: PropTypes.arrayOf(
52 | PropTypes.shape({
53 | shard_id: PropTypes.number,
54 | peer_id: PropTypes.number,
55 | state: PropTypes.string,
56 | })
57 | ),
58 | }),
59 | }).isRequired,
60 | other: PropTypes.object,
61 | };
62 |
63 | export default ClusterInfo;
64 |
--------------------------------------------------------------------------------
/src/components/Collections/CollectionCluster/ClusterInfoHead.jsx:
--------------------------------------------------------------------------------
1 | import { TableCell, TableHead, TableRow, Typography } from '@mui/material';
2 | import React from 'react';
3 |
4 | const ClusterInfoHead = () => {
5 | return (
6 |
7 |
8 |
9 |
10 | Shard ID
11 |
12 |
13 |
14 |
15 | Location
16 |
17 |
18 |
19 |
20 | Status
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default ClusterInfoHead;
29 |
--------------------------------------------------------------------------------
/src/components/Collections/CollectionCluster/ClusterShardRow.jsx:
--------------------------------------------------------------------------------
1 | import { TableCell, TableRow, Typography } from '@mui/material';
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 |
5 | const ClusterShardRow = ({ shard, clusterPeerId }) => {
6 | return (
7 |
8 |
9 |
10 | {shard.shard_id}
11 |
12 |
13 |
14 |
15 | {shard.peer_id ? `Remote (${shard.peer_id})` : `Local (${clusterPeerId ?? 'unknown'})`}
16 |
17 |
18 |
19 |
20 | {shard.state}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | ClusterShardRow.propTypes = {
28 | shard: PropTypes.shape({
29 | shard_id: PropTypes.number,
30 | peer_id: PropTypes.number,
31 | state: PropTypes.string,
32 | }).isRequired,
33 | clusterPeerId: PropTypes.number,
34 | };
35 |
36 | export default ClusterShardRow;
37 |
--------------------------------------------------------------------------------
/src/components/Collections/CollectionCluster/collectionCluster.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import ClusterInfo from './ClusterInfo';
3 | import ClusterShardRow from './ClusterShardRow';
4 |
5 | // Mock client context
6 | vi.mock('../../../context/client-context', () => ({
7 | useClient: () => ({
8 | client: {},
9 | isRestricted: false,
10 | }),
11 | }));
12 |
13 | const CLUSTER_INFO = {
14 | result: {
15 | peer_id: 5644950770669488,
16 | shard_count: 3,
17 | local_shards: [
18 | {
19 | shard_id: 0,
20 | points_count: 62223,
21 | state: 'Active',
22 | },
23 | {
24 | shard_id: 2,
25 | points_count: 65999,
26 | state: 'Active',
27 | },
28 | ],
29 | remote_shards: [
30 | {
31 | shard_id: 0,
32 | peer_id: 5255497362296823,
33 | state: 'Active',
34 | },
35 | {
36 | shard_id: 1,
37 | peer_id: 5255497362296823,
38 | state: 'Active',
39 | },
40 | {
41 | shard_id: 1,
42 | peer_id: 8741461806010521,
43 | state: 'Active',
44 | },
45 | {
46 | shard_id: 2,
47 | peer_id: 8741461806010521,
48 | state: 'Active',
49 | },
50 | ],
51 | shard_transfers: [],
52 | },
53 | status: 'ok',
54 | time: 0.00002203,
55 | };
56 |
57 | describe('collection cluster info', () => {
58 | it('should render ClusterShardRow with given data', () => {
59 | const shard = CLUSTER_INFO.result.remote_shards[0];
60 | render(
61 |
66 | );
67 | expect(screen.getByTestId('shard-row').children[0].children[0].textContent).toBe(shard.shard_id.toString());
68 | expect(screen.getByText(`Remote (${shard.peer_id})`)).toBeTruthy();
69 | expect(screen.getByText(shard.state)).toBeTruthy();
70 | });
71 |
72 | it('should render CollectionClusterInfo with given data', () => {
73 | const shardReplicasCount = CLUSTER_INFO.result.local_shards.length + CLUSTER_INFO.result.remote_shards.length;
74 | render();
75 | expect(screen.getByText('Collection Cluster Info')).toBeTruthy();
76 | expect(screen.getAllByTestId('shard-row').length).toBe(shardReplicasCount);
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/src/components/Collections/CollectionInfo.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Box, Button, Card, CardContent, CardHeader, Typography } from '@mui/material';
4 | import { useClient } from '../../context/client-context';
5 | import { DataGridList } from '../Points/DataGridList';
6 | import { CopyButton } from '../Common/CopyButton';
7 | import { Dot } from '../Common/Dot';
8 | import ClusterInfo from './CollectionCluster/ClusterInfo';
9 | import { useSnackbar } from 'notistack';
10 | import { getSnackbarOptions } from '../Common/utils/snackbarOptions';
11 | import { bigIntJSON } from '../../common/bigIntJSON';
12 | import CollectionAliases from './CollectionAliases';
13 |
14 | export const CollectionInfo = ({ collectionName }) => {
15 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
16 | const { client: qdrantClient, isRestricted } = useClient();
17 | const [collection, setCollection] = React.useState({});
18 | const [clusterInfo, setClusterInfo] = React.useState(null);
19 |
20 | useEffect(() => {
21 | fetchCollection();
22 |
23 | if (isRestricted) {
24 | return;
25 | }
26 |
27 | qdrantClient
28 | .api('cluster')
29 | .collectionClusterInfo({ collection_name: collectionName })
30 | .then((res) => {
31 | setClusterInfo(() => {
32 | return { ...res.data };
33 | });
34 | })
35 | .catch((err) => {
36 | enqueueSnackbar(err.message, getSnackbarOptions('error', closeSnackbar));
37 | });
38 | }, [collectionName]);
39 |
40 | const fetchCollection = () => {
41 | qdrantClient
42 | .getCollection(collectionName)
43 | .then((res) => {
44 | setCollection(() => {
45 | return { ...res };
46 | });
47 | })
48 | .catch((err) => {
49 | enqueueSnackbar(err.message, getSnackbarOptions('error', closeSnackbar));
50 | });
51 | };
52 |
53 | const triggerOptimizers = () => {
54 | qdrantClient
55 | .updateCollection(collectionName, {
56 | optimizers_config: {},
57 | })
58 | .then(() => {
59 | enqueueSnackbar('Optimizers triggered', getSnackbarOptions('success', closeSnackbar));
60 | fetchCollection();
61 | })
62 | .catch((err) => {
63 | enqueueSnackbar(err.message, getSnackbarOptions('error', closeSnackbar));
64 | });
65 | };
66 |
67 | return (
68 |
69 |
70 |
71 | }
78 | />
79 |
80 |
85 |
86 | {collection.status}
87 |
88 | {(collection.status === 'grey' ||
89 | collection.optimizer_status?.error === `optimizations pending, awaiting update operation`) && (
90 |
93 | )}
94 |
95 | ),
96 | }}
97 | />
98 |
99 |
100 |
101 | {clusterInfo && }
102 |
103 | );
104 | };
105 |
106 | CollectionInfo.displayName = 'CollectionInfo';
107 |
108 | CollectionInfo.propTypes = {
109 | collectionName: PropTypes.string.isRequired,
110 | };
111 |
112 | export default memo(CollectionInfo);
113 |
--------------------------------------------------------------------------------
/src/components/Collections/DeleteDialog.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Button from '@mui/material/Button';
3 | import Dialog from '@mui/material/Dialog';
4 | import DialogActions from '@mui/material/DialogActions';
5 | import DialogContent from '@mui/material/DialogContent';
6 | import DialogContentText from '@mui/material/DialogContentText';
7 | import DialogTitle from '@mui/material/DialogTitle';
8 | import PropTypes from 'prop-types';
9 | import { useClient } from '../../context/client-context';
10 |
11 | import ErrorNotifier from '../ToastNotifications/ErrorNotifier';
12 | import Box from '@mui/material/Box';
13 |
14 | export default function DeleteDialog({ open, setOpen, collectionName, getCollectionsCall }) {
15 | const [hasError, setHasError] = useState(false);
16 | const [errorMessage, setErrorMessage] = useState('');
17 |
18 | const { client: qdrantClient } = useClient();
19 |
20 | async function callDelete() {
21 | try {
22 | await qdrantClient.deleteCollection(collectionName);
23 | getCollectionsCall();
24 | setOpen(false);
25 | setHasError(false);
26 | } catch (error) {
27 | setErrorMessage(`Deletion Unsuccessful, error: ${error.message}`);
28 | setHasError(true);
29 | setOpen(false);
30 | }
31 | }
32 |
33 | const handleClose = () => {
34 | setOpen(false);
35 | };
36 |
37 | return (
38 |
39 | {hasError && }
40 |
65 |
66 | );
67 | }
68 |
69 | DeleteDialog.propTypes = {
70 | collectionName: PropTypes.string.isRequired,
71 | setOpen: PropTypes.func.isRequired,
72 | open: PropTypes.bool.isRequired,
73 | getCollectionsCall: PropTypes.func.isRequired,
74 | };
75 |
--------------------------------------------------------------------------------
/src/components/Collections/SearchBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SearchIcon from '@mui/icons-material/Search';
3 | import PropTypes from 'prop-types';
4 | import { Card, InputAdornment, OutlinedInput, SvgIcon } from '@mui/material';
5 |
6 | function InputWithIcon({ value, setValue, actions }) {
7 | return (
8 |
17 | setValue(e.target.value)}
21 | placeholder="Search Collection"
22 | startAdornment={
23 |
24 |
25 |
26 |
27 |
28 | }
29 | size={'small'}
30 | sx={{ maxWidth: 500 }}
31 | />
32 |
33 | {/* additional actions */}
34 | {actions?.length && actions.map((action, index) => {action})}
35 |
36 | );
37 | }
38 | InputWithIcon.propTypes = {
39 | value: PropTypes.string,
40 | setValue: PropTypes.func,
41 | actions: PropTypes.arrayOf(PropTypes.element),
42 | };
43 |
44 | export default InputWithIcon;
45 |
--------------------------------------------------------------------------------
/src/components/Collections/SearchQuality/SearchQuality.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { getSnackbarOptions } from '../../Common/utils/snackbarOptions';
4 | import { useClient } from '../../../context/client-context';
5 | import SearchQualityPanel from './SearchQualityPanel';
6 | import { useSnackbar } from 'notistack';
7 | import { Box, Card, CardHeader } from '@mui/material';
8 | import { CopyButton } from '../../Common/CopyButton';
9 | import { bigIntJSON } from '../../../common/bigIntJSON';
10 | import EditorCommon from '../../EditorCommon';
11 | import _ from 'lodash';
12 |
13 | const SearchQuality = ({ collectionName }) => {
14 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
15 | const { client } = useClient();
16 | const [collection, setCollection] = React.useState(null);
17 | const [log, setLog] = React.useState('');
18 |
19 | const handleLogUpdate = (newLog) => {
20 | const date = new Date().toLocaleString();
21 | newLog = `[${date}] ${newLog}`;
22 | setLog((prevLog) => {
23 | return newLog + '\n' + prevLog;
24 | });
25 | };
26 |
27 | const clearLogs = () => {
28 | setLog('');
29 | };
30 |
31 | useEffect(() => {
32 | client
33 | .getCollection(collectionName)
34 | .then((res) => {
35 | setCollection(() => {
36 | return { ...res };
37 | });
38 | })
39 | .catch((err) => {
40 | enqueueSnackbar(err.message, getSnackbarOptions('error', closeSnackbar));
41 | });
42 | }, []);
43 |
44 | // Check that collection.config.params.vectors?.size exists and integer
45 | const isNamedVectors = !collection?.config?.params.vectors?.size && _.isObject(collection?.config?.params?.vectors);
46 | let vectors = {};
47 | if (collection) {
48 | vectors = isNamedVectors ? collection?.config?.params?.vectors : { '': collection?.config?.params?.vectors };
49 | }
50 |
51 | return (
52 | <>
53 | {collection?.config?.params?.vectors && (
54 |
61 | )}
62 |
63 |
64 | }
71 | />
72 |
73 |
87 |
88 |
89 | >
90 | );
91 | };
92 |
93 | SearchQuality.propTypes = {
94 | collectionName: PropTypes.string,
95 | };
96 |
97 | export default SearchQuality;
98 |
--------------------------------------------------------------------------------
/src/components/Collections/SearchQuality/check-index-precision.js:
--------------------------------------------------------------------------------
1 | export const checkIndexPrecision = async (
2 | client,
3 | collectionName,
4 | pointId,
5 | logFoo,
6 | idx,
7 | total,
8 | filter = null,
9 | params = null,
10 | vectorName = null,
11 | limit = 10,
12 | timeout = 20
13 | ) => {
14 | try {
15 | const exactSearchtartTime = new Date().getTime();
16 |
17 | const exact = await client.query(collectionName, {
18 | limit: limit,
19 | with_payload: false,
20 | with_vectors: false,
21 | query: pointId,
22 | params: {
23 | exact: true,
24 | },
25 | filter: filter,
26 | using: vectorName,
27 | timeout,
28 | });
29 |
30 | const exactSearchElapsed = new Date().getTime() - exactSearchtartTime;
31 |
32 | const searchStartTime = new Date().getTime();
33 |
34 | const hnsw = await client.query(collectionName, {
35 | timeout,
36 | limit: limit,
37 | with_payload: false,
38 | with_vectors: false,
39 | query: pointId,
40 | params: params,
41 | filter: filter,
42 | using: vectorName,
43 | });
44 |
45 | const searchElapsed = new Date().getTime() - searchStartTime;
46 |
47 | const exactIds = exact.points.map((item) => item.id);
48 | const hnswIds = hnsw.points.map((item) => item.id);
49 |
50 | const precision = exactIds.filter((id) => hnswIds.includes(id)).length / exactIds.length;
51 |
52 | logFoo &&
53 | logFoo(
54 | 'Point ID ' +
55 | idx +
56 | '(' +
57 | idx +
58 | '/' +
59 | total +
60 | ') precision@' +
61 | limit +
62 | ': ' +
63 | precision +
64 | ' (search time exact: ' +
65 | exactSearchElapsed +
66 | 'ms, regular: ' +
67 | searchElapsed +
68 | 'ms)'
69 | );
70 |
71 | return precision;
72 | } catch (e) {
73 | console.error('Error: ', e);
74 | console.error('Skipping point: ', idx);
75 | // todo: throw error
76 | return null;
77 | }
78 | };
79 |
--------------------------------------------------------------------------------
/src/components/Collections/SearchQuality/searchQualityPannel.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2 | import { MemoryRouter } from 'react-router-dom';
3 | import SearchQualityPanel from './SearchQualityPanel';
4 | import { useClient } from '../../../context/client-context';
5 |
6 | const mockFilterEditorWindow = vi.fn();
7 | vi.mock('../../FilterEditorWindow', () => ({
8 | default: (props) => {
9 | mockFilterEditorWindow(props);
10 | return FilterEditorWindow
;
11 | },
12 | }));
13 |
14 | vi.mock('../../../context/client-context');
15 |
16 | const COLLECTION_NAME = 'test_collection';
17 |
18 | const VECTORS = {
19 | '': {
20 | size: 512,
21 | distance: 'Cosine',
22 | },
23 | };
24 |
25 | const VECTORS_NAMED = {
26 | text: {
27 | size: 512,
28 | distance: 'Cosine',
29 | },
30 | image: {
31 | size: 512,
32 | distance: 'Cosine',
33 | },
34 | };
35 |
36 | describe('SearchQualityPannel', () => {
37 | beforeEach(() => {
38 | useClient.mockReturnValue({
39 | client: {
40 | scroll: vi.fn().mockResolvedValue({ points: [{ id: 1 }, { id: 2 }] }),
41 | query: vi.fn().mockResolvedValue({ points: [{ id: 1 }, { id: 2 }] }),
42 | },
43 | });
44 | });
45 |
46 | it('should render SearchQualityPannel with given data', () => {
47 | render(
48 |
49 |
50 |
51 | );
52 | expect(screen.getByText('Search Quality')).toBeInTheDocument();
53 | expect(screen.getByText('512')).toBeInTheDocument();
54 | expect(screen.getByText('Cosine')).toBeInTheDocument();
55 | });
56 |
57 | it('should render SearchQualityPannel with named vectors', () => {
58 | render(
59 |
60 |
61 |
62 | );
63 | expect(screen.getByText('Search Quality')).toBeInTheDocument();
64 | expect(screen.getByText('text')).toBeInTheDocument();
65 | expect(screen.getByText('image')).toBeInTheDocument();
66 | expect(screen.getAllByText('512')).toHaveLength(2);
67 | expect(screen.getAllByText('Cosine')).toHaveLength(2);
68 | });
69 |
70 | it('should call onCheckIndexQuality when "Check index quality" button is clicked', async () => {
71 | render(
72 |
73 |
74 |
75 | );
76 | const button = screen.getAllByTestId('index-quality-check-button')[0];
77 | fireEvent.click(button);
78 | await waitFor(() => {
79 | expect(useClient().client.scroll).toHaveBeenCalled();
80 | expect(useClient().client.query).toHaveBeenCalled();
81 | });
82 | });
83 |
84 | it('should toggle advanced mode', () => {
85 | render(
86 |
87 |
88 |
89 | );
90 | const switchButton = screen.getByRole('checkbox');
91 | fireEvent.click(switchButton);
92 | expect(switchButton).toBeChecked();
93 | expect(screen.getByText('FilterEditorWindow')).toBeInTheDocument();
94 | expect(screen.getByTestId('advanced-mod-editor')).toBeInTheDocument();
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/src/components/Collections/collectionList.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { MemoryRouter } from 'react-router-dom';
3 | import CollectionsList from './CollectionsList';
4 | import { describe, it, expect } from 'vitest';
5 |
6 | vi.mock('../../context/client-context', () => ({
7 | useClient: () => ({
8 | client: {
9 | deleteCollection: vi.fn().mockResolvedValue({}),
10 | },
11 | }),
12 | }));
13 |
14 | const COLLECTIONS = [
15 | {
16 | name: 'Collection 1',
17 | status: 'green',
18 | points_count: 1000,
19 | segments_count: 10,
20 | config: {
21 | params: {
22 | shard_number: 2,
23 | vectors: {
24 | size: 128,
25 | distance: 'cosine',
26 | },
27 | },
28 | },
29 | aliases: ['alias1', 'alias2'],
30 | },
31 | {
32 | name: 'Collection 2',
33 | status: 'yellow',
34 | points_count: 500,
35 | segments_count: 5,
36 | config: {
37 | params: {
38 | shard_number: 1,
39 | vectors: {
40 | vector1: {
41 | size: 64,
42 | distance: 'euclidean',
43 | },
44 | vector2: {
45 | size: 32,
46 | distance: 'manhattan',
47 | },
48 | },
49 | },
50 | },
51 | aliases: [],
52 | },
53 | ];
54 |
55 | describe('CollectionsList', () => {
56 | it('should render CollectionsList with given data', () => {
57 | render(
58 |
59 | {}} />
60 |
61 | );
62 | expect(screen.getByText('Collection 1')).toBeInTheDocument();
63 | expect(screen.getByText('Collection 2')).toBeInTheDocument();
64 | });
65 |
66 | it('should render CollectionTableRow with given data', () => {
67 | render(
68 |
69 | {}} />
70 |
71 | );
72 | expect(screen.getByText('green')).toBeInTheDocument();
73 | expect(screen.getByText('yellow')).toBeInTheDocument();
74 | expect(screen.getByText('1000')).toBeInTheDocument();
75 | expect(screen.getByText('500')).toBeInTheDocument();
76 | expect(screen.getByText('128')).toBeInTheDocument();
77 | expect(screen.getByText('cosine')).toBeInTheDocument();
78 | expect(screen.getByText('64')).toBeInTheDocument();
79 | expect(screen.getByText('euclidean')).toBeInTheDocument();
80 | expect(screen.getByText('32')).toBeInTheDocument();
81 | expect(screen.getByText('manhattan')).toBeInTheDocument();
82 | expect(screen.getByText('Aliases: alias1, alias2')).toBeInTheDocument();
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/src/components/Common/ActionsMenu.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Menu from '@mui/material/Menu';
4 | import { IconButton } from '@mui/material';
5 | import MoreVert from '@mui/icons-material/MoreVert';
6 |
7 | // for better result use as children the components acceptable inside Menu component from MUI
8 | // https://mui.com/material-ui/react-menu/
9 | // but in general it can be any node elements
10 | function ActionsMenu({ children, ...props }) {
11 | const [anchorEl, setAnchorEl] = React.useState(null);
12 | const open = Boolean(anchorEl);
13 | const handleClick = (event) => {
14 | setAnchorEl(event.currentTarget);
15 | };
16 | const handleClose = () => {
17 | setAnchorEl(null);
18 | };
19 |
20 | return (
21 |
22 |
29 |
30 |
31 |
32 |
43 |
44 | );
45 | }
46 |
47 | ActionsMenu.propTypes = {
48 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.node])), PropTypes.node]),
49 | };
50 |
51 | export default memo(ActionsMenu);
52 |
--------------------------------------------------------------------------------
/src/components/Common/CenteredFrame.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Box from '@mui/material/Box';
4 |
5 | export const CenteredFrame = ({ children }) => {
6 | return (
7 |
16 | {children}
17 |
18 | );
19 | };
20 |
21 | // props validation
22 | CenteredFrame.propTypes = {
23 | children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.node])), PropTypes.node]),
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Common/CircularProgressWithLabel.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import PropTypes from 'prop-types';
3 | import CircularProgress from '@mui/material/CircularProgress';
4 | import Typography from '@mui/material/Typography';
5 | import Box from '@mui/material/Box';
6 |
7 | function CircularProgressWithLabel({ value, size = 40, sx = {}, circularProgressProps = {}, ...props }) {
8 | return (
9 |
19 |
33 |
45 |
46 | {`${Math.round(value)}%`}
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | CircularProgressWithLabel.propTypes = {
54 | value: PropTypes.number.isRequired,
55 | size: PropTypes.number,
56 | sx: PropTypes.object,
57 | circularProgressProps: PropTypes.object,
58 | };
59 |
60 | export default CircularProgressWithLabel;
61 |
--------------------------------------------------------------------------------
/src/components/Common/ConfirmationDialog.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Box, Button, Dialog, Typography } from '@mui/material';
4 |
5 | const ConfirmationDialog = ({ open, onClose, title, content, warning, actionName, actionHandler }) => {
6 | const handleActionClick = () => {
7 | actionHandler();
8 | onClose();
9 | };
10 |
11 | return (
12 |
68 | );
69 | };
70 |
71 | // propType validation
72 | ConfirmationDialog.propTypes = {
73 | open: PropTypes.bool.isRequired,
74 | onClose: PropTypes.func.isRequired,
75 | title: PropTypes.string.isRequired,
76 | content: PropTypes.string.isRequired,
77 | warning: PropTypes.string,
78 | actionName: PropTypes.string.isRequired,
79 | actionHandler: PropTypes.func.isRequired,
80 | };
81 |
82 | export default ConfirmationDialog;
83 |
--------------------------------------------------------------------------------
/src/components/Common/CopyButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { IconButton, Tooltip } from '@mui/material';
4 | import { CopyAll } from '@mui/icons-material';
5 | import { useSnackbar } from 'notistack';
6 | import { getSnackbarOptions } from './utils/snackbarOptions';
7 |
8 | export const CopyButton = ({
9 | text,
10 | tooltip = 'Copy to clipboard',
11 | tooltipPlacement = 'left',
12 | successMessage = 'Copied to clipboard',
13 | }) => {
14 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
15 | const successSnackbarOptions = getSnackbarOptions('success', closeSnackbar, 1000);
16 | const errorSnackbarOptions = getSnackbarOptions('error', closeSnackbar);
17 |
18 | return (
19 |
20 | {
23 | navigator.clipboard
24 | .writeText(text)
25 | .then(() => {
26 | enqueueSnackbar(successMessage, successSnackbarOptions);
27 | })
28 | .catch((err) => {
29 | enqueueSnackbar(err.message, errorSnackbarOptions);
30 | });
31 | }}
32 | >
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | CopyButton.propTypes = {
40 | text: PropTypes.string.isRequired,
41 | tooltip: PropTypes.string,
42 | tooltipPlacement: PropTypes.string,
43 | successMessage: PropTypes.string,
44 | };
45 |
--------------------------------------------------------------------------------
/src/components/Common/Dot.jsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@mui/material/styles';
2 |
3 | /**
4 | * Status indicator dot.
5 | * @type {StyledComponent, JSX.IntrinsicElements[string], {}>}
6 | */
7 | export const Dot = styled('div')(
8 | ({ color }) => `
9 | border-radius: 50%;
10 | background-color: ${color};
11 | width: 10px;
12 | height: 10px;
13 | display: inline-block;
14 | `
15 | );
16 |
--------------------------------------------------------------------------------
/src/components/Common/InfoBanner.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Alert, Box, Collapse } from '@mui/material';
4 | import IconButton from '@mui/material/IconButton';
5 | import CloseIcon from '@mui/icons-material/Close';
6 |
7 | const InfoBanner = ({ severity, children, onClose }) => {
8 | const [open, setOpen] = useState(true);
9 |
10 | const handleClose = () => {
11 | onClose && onClose();
12 | setOpen(false);
13 | };
14 |
15 | return (
16 |
17 |
18 |
22 |
23 |
24 | }
25 | sx={{ my: 2, lineHeight: 1.7 }}
26 | >
27 | {children}
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | InfoBanner.propTypes = {
35 | severity: PropTypes.string.isRequired,
36 | children: PropTypes.node.isRequired,
37 | onClose: PropTypes.func,
38 | };
39 |
40 | export default InfoBanner;
41 |
--------------------------------------------------------------------------------
/src/components/Common/PointPreview.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useTheme } from '@mui/material/styles';
4 | import { alpha, Box, Card, CardContent, CardHeader, Grid, LinearProgress } from '@mui/material';
5 | import { DataGridList } from '../Points/DataGridList';
6 | import PointImage from '../Points/PointImage';
7 | import Vectors from '../Points/PointVectors';
8 |
9 | const PointPreview = ({ point }) => {
10 | const theme = useTheme();
11 | const [loading] = React.useState(false);
12 | const conditions = [];
13 | const payloadSchema = {};
14 |
15 | if (!point) {
16 | return null;
17 | }
18 |
19 | const sortedPayload = Object.keys(point.payload)
20 | .sort() // Sort the keys alphabetically
21 | .reduce((obj, key) => {
22 | obj[key] = point.payload[key]; // Rebuild the object with sorted keys
23 | return obj;
24 | }, {});
25 |
26 | return (
27 | <>
28 |
37 | {loading && (
38 |
46 |
47 |
48 | )}
49 | {Object.keys(point.payload).length > 0 && (
50 | <>
51 |
52 |
53 | {point.payload && }
54 |
55 |
60 |
61 |
62 |
63 | >
64 | )}
65 | {point?.vector && (
66 | <>
67 |
74 |
75 |
76 |
77 | >
78 | )}
79 |
80 | >
81 | );
82 | };
83 |
84 | // prop types
85 | PointPreview.propTypes = {
86 | point: PropTypes.object,
87 | };
88 |
89 | export default memo(PointPreview);
90 |
--------------------------------------------------------------------------------
/src/components/Common/StyledMain.jsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@mui/material/styles';
2 |
3 | const StyledMain = styled('main')(({ theme }) => ({
4 | height: '100vh',
5 |
6 | scrollBehavior: 'smooth',
7 | scrollbarWidth: 'thin',
8 |
9 | '& *::-webkit-scrollbar': {
10 | width: '6px',
11 | height: '6px',
12 | },
13 |
14 | '& *::-webkit-scrollbar-track': {
15 | background: theme.palette.background.default,
16 | },
17 |
18 | '& *::-webkit-scrollbar-thumb': {
19 | background: theme.palette.divider,
20 | borderRadius: '6px',
21 | },
22 | }));
23 |
24 | export default StyledMain;
25 |
--------------------------------------------------------------------------------
/src/components/Common/VectorsConfigChip.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Card, Grid } from '@mui/material';
4 |
5 | const VectorsConfigChip = ({ collectionConfigParams, sx = {} }) => {
6 | return (
7 | <>
8 | {collectionConfigParams.vectors.size && (
9 |
10 |
11 | default
12 |
13 |
14 | {collectionConfigParams.vectors.size}
15 |
16 |
17 | {collectionConfigParams.vectors.distance}
18 |
19 | {/* model is not always present */}
20 | {collectionConfigParams.vectors.model && {collectionConfigParams.vectors.model}}
21 |
22 | )}
23 | {!collectionConfigParams.vectors.size &&
24 | Object.keys(collectionConfigParams.vectors).map((vector) => (
25 |
26 |
27 | {vector}
28 |
29 |
30 | {collectionConfigParams.vectors[vector].size}
31 |
32 |
33 | {collectionConfigParams.vectors[vector].distance}
34 |
35 | {/* model is not always present */}
36 | {collectionConfigParams.vectors[vector].model && (
37 | {collectionConfigParams.vectors[vector].model}
38 | )}
39 |
40 | ))}
41 | {collectionConfigParams.sparse_vectors &&
42 | Object.keys(collectionConfigParams.sparse_vectors).map((vector) => (
43 |
44 |
45 | {vector}
46 |
47 |
48 | Sparse
49 |
50 |
51 |
52 | ))}
53 | >
54 | );
55 | };
56 |
57 | VectorsConfigChip.propTypes = {
58 | collectionConfigParams: PropTypes.object.isRequired,
59 | sx: PropTypes.object,
60 | };
61 |
62 | export default VectorsConfigChip;
63 |
--------------------------------------------------------------------------------
/src/components/Common/utils/snackbarOptions.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '@mui/material';
3 |
4 | export const getSnackbarOptions = (variant, closeSnackbar, autoHideDuration = null) => ({
5 | variant: variant,
6 | autoHideDuration: autoHideDuration,
7 | preventDuplicate: true,
8 | action: (key) => (
9 |
18 | ),
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/Datasets/DatasetsTableHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TableCell, TableRow } from '@mui/material';
3 | import { TableHeadWithGaps } from '../Common/TableWithGaps';
4 | import PropTypes from 'prop-types';
5 |
6 | export const DatasetsHeader = ({ headers }) => {
7 | return (
8 |
9 |
10 | {headers.map((header, index) => (
11 |
16 | {header}
17 |
18 | ))}
19 |
20 |
21 | );
22 | };
23 |
24 | DatasetsHeader.propTypes = {
25 | headers: PropTypes.array.isRequired,
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/Datasets/DatasetsTableRow.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import prettyBytes from 'pretty-bytes';
4 | import { useTheme } from '@mui/material/styles';
5 | import { Box, CircularProgress, IconButton, TableCell, TableRow, Tooltip, Typography } from '@mui/material';
6 | import { Download, FolderZip } from '@mui/icons-material';
7 | import ImportDatasetDialog from './ImportDatasetDialog';
8 | import VectorsConfigChip from '../Common/VectorsConfigChip';
9 |
10 | export const DatasetsTableRow = ({ dataset, importDataset }) => {
11 | const theme = useTheme();
12 | const [importing, setImporting] = useState(false);
13 | const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
14 |
15 | return (
16 |
17 |
18 |
19 | setIsImportDialogOpen(true)}
33 | >
34 |
35 |
42 | {importing && (
43 |
52 | )}
53 |
54 |
55 | {dataset.name}
56 |
57 | {dataset.description}
58 |
59 |
60 |
61 |
62 |
63 |
64 | {prettyBytes(dataset.size)}
65 |
66 |
67 |
68 |
69 |
70 | {dataset.vectorCount}
71 |
72 |
73 | setIsImportDialogOpen(true)}>
74 |
75 |
76 |
77 |
78 | setIsImportDialogOpen(false)}
81 | content={`Enter collection name for ${dataset.fileName}`}
82 | fileName={dataset.fileName}
83 | actionHandler={importDataset}
84 | setImporting={setImporting}
85 | importing={importing}
86 | />
87 |
88 | );
89 | };
90 |
91 | DatasetsTableRow.propTypes = {
92 | dataset: PropTypes.shape({
93 | name: PropTypes.string,
94 | vectors: PropTypes.object,
95 | description: PropTypes.string,
96 | vectorCount: PropTypes.number,
97 | fileName: PropTypes.string,
98 | size: PropTypes.number,
99 | }),
100 | importDataset: PropTypes.func,
101 | };
102 |
--------------------------------------------------------------------------------
/src/components/Datasets/ImportDatasetDialog.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Box, Button, Dialog, Input, Typography } from '@mui/material';
4 |
5 | const ImportDatasetDialog = ({ open, onClose, content, actionHandler, fileName, setImporting, importing }) => {
6 | const [collectionName, setCollectionName] = useState('');
7 | const handleActionClick = () => {
8 | actionHandler(fileName, collectionName, setImporting, importing);
9 | onClose();
10 | setCollectionName('');
11 | };
12 |
13 | return (
14 |
61 | );
62 | };
63 |
64 | // propType validation
65 | ImportDatasetDialog.propTypes = {
66 | open: PropTypes.bool.isRequired,
67 | onClose: PropTypes.func.isRequired,
68 | content: PropTypes.string.isRequired,
69 | actionHandler: PropTypes.func.isRequired,
70 | fileName: PropTypes.string.isRequired,
71 | setImporting: PropTypes.func.isRequired,
72 | importing: PropTypes.bool.isRequired,
73 | };
74 |
75 | export default ImportDatasetDialog;
76 |
--------------------------------------------------------------------------------
/src/components/EditorCommon/config/theme.js:
--------------------------------------------------------------------------------
1 | export const getEditorTheme = (theme) => {
2 | if (theme.palette.mode === 'dark') {
3 | return {
4 | base: 'vs-dark',
5 | rules: [
6 | { token: 'keyword', foreground: '#007acc' },
7 | { token: 'string.key', foreground: '#f14c4c' },
8 | { token: 'string.value', foreground: '#3794ff' },
9 | { token: 'number', foreground: '#098658' },
10 | { token: 'comment', foreground: '#6A9955' },
11 |
12 | { token: 'string', foreground: '#f14c4c' },
13 | ],
14 | colors: {
15 | 'editor.foreground': '#FFFFFF',
16 | },
17 | };
18 | } else {
19 | return {
20 | base: 'vs',
21 | rules: [
22 | { token: 'keyword', foreground: '#0000FF' },
23 | { token: 'string.key', foreground: '#A31515' },
24 | { token: 'string.value', foreground: '#0451A5' },
25 | { token: 'number', foreground: '#098658' },
26 | { token: 'comment', foreground: '#008000' },
27 |
28 | { token: 'string', foreground: '#A31515' },
29 | ],
30 | colors: {
31 | 'editor.foreground': '#000000',
32 | },
33 | };
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/EditorCommon/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Editor from '@monaco-editor/react';
4 | import { getEditorTheme } from './config/theme';
5 | import { langConfig, Rules } from './config/Rules';
6 | import { useTheme } from '@mui/material/styles';
7 | import { useWindowResize } from '../../hooks/windowHooks';
8 | import * as monaco from 'monaco-editor';
9 | import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
10 | import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
11 | import { loader } from '@monaco-editor/react';
12 |
13 | window.MonacoEnvironment = {
14 | getWorker(_, label) {
15 | if (label === 'json') {
16 | return new JsonWorker();
17 | }
18 | // Additional language-server support should be added here
19 | return new EditorWorker();
20 | },
21 | };
22 |
23 | loader.config({ monaco });
24 |
25 | const EditorCommon = ({ beforeMount, customHeight, ...props }) => {
26 | const monacoRef = useRef(null);
27 | const editorWrapper = useRef(null);
28 | const theme = useTheme();
29 | const { height } = useWindowResize();
30 | const [editorHeight, setEditorHeight] = useState(customHeight || 0);
31 |
32 | function handleEditorWillMount(monaco) {
33 | monacoRef.current = monaco;
34 | // Register Custom Language
35 | monaco.languages.register({ id: 'custom-language' });
36 | // Defining Rules
37 | monaco.languages.setMonarchTokensProvider('custom-language', Rules);
38 | // Defining Theme
39 | monaco.editor.defineTheme('custom-language-theme', getEditorTheme(theme));
40 |
41 | // Defining Language Configuration, e.g. comments, brackets
42 | monaco.languages.setLanguageConfiguration('custom-language', langConfig);
43 |
44 | if (typeof beforeMount === 'function') {
45 | beforeMount(monaco);
46 | }
47 | }
48 |
49 | // Monitor if theme changes
50 | useEffect(() => {
51 | monacoRef.current?.editor.defineTheme('custom-language-theme', getEditorTheme(theme));
52 | }, [theme]);
53 |
54 | useEffect(() => {
55 | if (customHeight) {
56 | return;
57 | }
58 | setEditorHeight(height - editorWrapper.current?.offsetTop);
59 | }, [height, editorWrapper]);
60 |
61 | return (
62 |
63 |
69 |
70 | );
71 | };
72 |
73 | EditorCommon.propTypes = {
74 | height: PropTypes.string,
75 | beforeMount: PropTypes.func,
76 | customHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
77 | ...Editor.propTypes,
78 | };
79 |
80 | export default EditorCommon;
81 |
--------------------------------------------------------------------------------
/src/components/EditorCommon/tests/get-code-blocks.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { getCodeBlocks } from '../config/Rules';
3 |
4 | describe('get-code-blocks', () => {
5 | it('should return 4 code blocks', () => {
6 | const codeText = `
7 | // List all collections
8 | GET collections
9 |
10 | // Get collection info
11 | GET collections/collection_name
12 |
13 | // List points in a collection, using filter
14 | POST collections/collection_name/points/scroll
15 | {
16 | "limit": 10,
17 | "filter": {
18 | "must": [
19 | {
20 | "key": "city",
21 | "match": {
22 | "any": [
23 | "San Francisco",
24 | "New York",
25 | "Berlin"
26 | ]
27 | }
28 | }
29 | ]
30 | }
31 | }
32 |
33 | lalal something unrelated
34 |
35 | GET collections/collection_name/points/{point_id}
36 | `;
37 | let blocks = getCodeBlocks(codeText);
38 |
39 | expect(blocks.length).toBe(4);
40 | });
41 |
42 | it('should return 1 code block with correct start and end lines', () => {
43 | const codeText = `
44 | // List all collections
45 | GET collections
46 |
47 | // Get collection info
48 | THIS IS NOT A CODE BLOCK
49 | {
50 | lalalal
51 | }
52 |
53 | `;
54 |
55 | let blocks = getCodeBlocks(codeText);
56 |
57 | expect(blocks.length).toBe(1);
58 | expect(blocks[0].blockStartLine).toBe(3);
59 | expect(blocks[0].blockEndLine).toBe(3);
60 | });
61 |
62 | it('incomplete block should be ignored', () => {
63 | const codeText = `
64 | // List all collections
65 | GET collections
66 |
67 | // Scrolling through points
68 | POST collections/collection_name/points/scroll
69 | {
70 | "limit": 10
71 |
72 | This is not a complete block
73 |
74 | // Get point info
75 | GET collections/collection_name/points/{point_id}
76 |
77 | `;
78 | let blocks = getCodeBlocks(codeText);
79 |
80 | expect(blocks.length).toBe(2);
81 |
82 | expect(blocks[0].blockStartLine).toBe(3);
83 | expect(blocks[0].blockEndLine).toBe(3);
84 | // The incomplete block should be ignored
85 | // Go straight to the next block
86 | expect(blocks[1].blockStartLine).toBe(13);
87 | expect(blocks[1].blockEndLine).toBe(13);
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/src/components/FilterEditorWindow/config/Autocomplete.js:
--------------------------------------------------------------------------------
1 | import { OpenapiAutocomplete } from 'autocomplete-openapi/src/autocomplete';
2 |
3 | export const autocomplete = async (monaco, qdrantClient, collectionName, customRequestSchema) => {
4 | const response = await fetch(import.meta.env.BASE_URL + './openapi.json');
5 | const openapi = await response.json();
6 |
7 | const vectorNames = [];
8 | try {
9 | const collectionInfo = await qdrantClient.getCollection(collectionName);
10 | Object.keys(collectionInfo.config.params.vectors).map((key) => {
11 | if (typeof collectionInfo.config.params.vectors[key] === 'object') {
12 | vectorNames.push(key);
13 | }
14 | });
15 | } catch (e) {
16 | console.error(e);
17 | }
18 |
19 | openapi.components.schemas.CustomRequest = customRequestSchema(vectorNames);
20 |
21 | const autocomplete = new OpenapiAutocomplete(openapi, []);
22 |
23 | return {
24 | provideCompletionItems: (model, position) => {
25 | // Reuse parsed code blocks to avoid parsing the same code block multiple times
26 | const selectedCodeBlock = monaco.editor.selectedCodeBlock;
27 | if (!selectedCodeBlock) {
28 | return { suggestions: [] };
29 | }
30 | const relativeLine = position.lineNumber - selectedCodeBlock.blockStartLine;
31 |
32 | if (relativeLine < 0) {
33 | // Something went wrong
34 | return { suggestions: [] };
35 | }
36 |
37 | if (relativeLine > 0) {
38 | // Autocomplete for request body
39 | const requestLines = selectedCodeBlock.blockText.split(/\r?\n/);
40 |
41 | const lastLine = requestLines[relativeLine].slice(0, position.column);
42 |
43 | const requestBodyLines = requestLines.slice(0, relativeLine);
44 |
45 | requestBodyLines.push(lastLine);
46 |
47 | const requestBody = requestBodyLines.join('\n');
48 |
49 | let suggestions = autocomplete.completeRequestBodyByDataRef('#/components/schemas/CustomRequest', requestBody);
50 | suggestions = suggestions.map((s) => {
51 | return {
52 | label: s,
53 | kind: 17,
54 | insertText: s,
55 | };
56 | });
57 |
58 | return { suggestions: suggestions };
59 | }
60 | },
61 | };
62 | };
63 |
--------------------------------------------------------------------------------
/src/components/FilterEditorWindow/config/Rules.js:
--------------------------------------------------------------------------------
1 | export const options = {
2 | scrollBeyondLastLine: false,
3 | readOnly: false,
4 | fontSize: 12,
5 | wordWrap: 'on',
6 | minimap: { enabled: false },
7 | automaticLayout: true,
8 | mouseWheelZoom: true,
9 | glyphMargin: true,
10 | wordBasedSuggestions: false,
11 | };
12 |
13 | export function btnconfig(commandId) {
14 | return {
15 | // function takes model and token as arguments
16 | provideCodeLenses: function (model) {
17 | const codeBlocks = getCodeBlocks(model.getValue());
18 | const lenses = [];
19 |
20 | for (let i = 0; i < codeBlocks.length; ++i) {
21 | lenses.push({
22 | range: {
23 | startLineNumber: codeBlocks[i].blockStartLine,
24 | startColumn: 1,
25 | endLineNumber: codeBlocks[i].blockStartLine,
26 | endColumn: 1,
27 | },
28 | id: 'RUN',
29 | command: {
30 | id: commandId,
31 | title: 'RUN',
32 | arguments: [codeBlocks[i].blockText],
33 | },
34 | });
35 | }
36 |
37 | return {
38 | lenses: lenses,
39 | dispose: () => {},
40 | };
41 | },
42 | // function takes model, codeLens and token as arguments
43 | resolveCodeLens: function (model, codeLens) {
44 | return codeLens;
45 | },
46 | };
47 | }
48 |
49 | export function selectBlock(blocks, location) {
50 | for (let i = 0; i < blocks.length; ++i) {
51 | if (blocks[i].blockStartLine <= location && location <= blocks[i].blockEndLine) {
52 | return blocks[i];
53 | }
54 | }
55 | return null;
56 | }
57 |
58 | export function getCodeBlocks(codeText) {
59 | const codeArray = codeText.replace(/\/\/.*$/gm, '').split(/\r?\n/);
60 | const blocksArray = [];
61 | let block = { blockText: '', blockStartLine: null, blockEndLine: null };
62 | let backetcount = 0;
63 | let codeStarLine = 0;
64 | let codeEndline = 0;
65 | for (let i = 0; i < codeArray.length; ++i) {
66 | // dealing for request which have JSON Body
67 | if (codeArray[i].includes('{')) {
68 | if (backetcount === 0) {
69 | codeStarLine = i + 1;
70 | }
71 | backetcount = backetcount + codeArray[i].match(/{/gi).length;
72 | }
73 | if (codeArray[i].includes('}')) {
74 | backetcount = backetcount - codeArray[i].match(/}/gi).length;
75 | if (backetcount === 0) {
76 | codeEndline = i + 1;
77 | }
78 | }
79 | if (codeStarLine) {
80 | block.blockStartLine = codeStarLine;
81 | block.blockText = block.blockText + codeArray[i] + '\n';
82 | if (codeEndline) {
83 | block.blockEndLine = codeEndline;
84 | blocksArray.push(block);
85 | codeEndline = 0;
86 | codeStarLine = 0;
87 | block = { blockText: '', blockStartLine: null, blockEndLine: null };
88 | }
89 | }
90 | }
91 | return blocksArray;
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/FilterEditorWindow/editor.css:
--------------------------------------------------------------------------------
1 | .light .blockSelector {
2 | background: rgba(163, 161, 161, 0.24);
3 | }
4 |
5 | .light .blockSelectorStrip {
6 | background: rgba(255, 252, 88, 0.87);
7 | }
8 |
9 | .dark .blockSelector {
10 | background: #2d2d3099;
11 | }
12 |
13 | .dark .blockSelectorStrip {
14 | background: #525100cc;
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/InteractiveTutorial.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Box from '@mui/material/Box';
4 | import { alpha } from '@mui/material';
5 | import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
6 | import ResultEditorWindow from '../ResultEditorWindow';
7 | import { useTheme } from '@mui/material/styles';
8 | import { mdxComponents } from './MdxComponents/MdxComponents';
9 | import { useTutorial } from '../../context/tutorial-context';
10 | import { TutorialFooter } from './TutorialFooter';
11 | import { tutorialSubPages, tutorialIndexPage } from './TutorialSubpages';
12 | import { useLocation } from 'react-router-dom';
13 | import { Prism } from 'prism-react-renderer';
14 |
15 | const InteractiveTutorial = ({ pageSlug }) => {
16 | const theme = useTheme();
17 | const { result } = useTutorial();
18 | const location = useLocation();
19 | const tutorialPanelRef = React.useRef(null);
20 |
21 | useEffect(() => {
22 | // we need this to use prismjs support for json highlighting
23 | // which is not included in the prism-react-renderer package by default
24 | window.Prism = Prism; // (or check for window is undefined for ssr and use global)
25 | (async () => await import('prismjs/components/prism-json'))();
26 | }, []);
27 |
28 | useEffect(() => {
29 | if (tutorialPanelRef.current) {
30 | tutorialPanelRef.current.scrollTop = 0;
31 | }
32 | }, [location]);
33 |
34 | let TagName;
35 | try {
36 | TagName = tutorialSubPages.find((p) => p[0] === pageSlug)[1].default;
37 | } catch (e) {
38 | TagName = tutorialIndexPage.default;
39 | }
40 |
41 | return (
42 |
43 |
44 |
58 |
59 |
60 |
61 |
62 |
68 |
76 | ⋮
77 |
78 |
79 |
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | InteractiveTutorial.propTypes = {
87 | pageSlug: PropTypes.string,
88 | };
89 |
90 | export default InteractiveTutorial;
91 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/MdxComponents/MdxCodeBlock.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { requestFromCode } from '../../CodeEditorWindow/config/RequesFromCode';
4 | import { useTutorial } from '../../../context/tutorial-context';
5 | import { bigIntJSON } from '../../../common/bigIntJSON';
6 | import { CodeBlock } from '../../Common/CodeBlock';
7 |
8 | /**
9 | * Code block with syntax highlighting
10 | * @param {object} children - code block content from mdx
11 | * @return {JSX.Element}
12 | * @constructor
13 | */
14 | export const MdxCodeBlock = ({ children }) => {
15 | const className = children.props.className || '';
16 | const code = children.props.children.trim();
17 | const language = className.replace(/language-/, '');
18 | const withRunButton = children.props.withRunButton && bigIntJSON.parse(children.props.withRunButton);
19 | const { setResult } = useTutorial();
20 | const [loading, setLoading] = React.useState(false);
21 |
22 | const handleRun = (code) => {
23 | setLoading(true);
24 | setResult('{}');
25 | requestFromCode(code, false)
26 | .then((res) => {
27 | setResult(() => bigIntJSON.stringify(res));
28 | setLoading(false);
29 | })
30 | .catch((err) => {
31 | setResult(() => bigIntJSON.stringify(err));
32 | setLoading(false);
33 | });
34 | };
35 |
36 | return (
37 |
38 | );
39 | };
40 |
41 | MdxCodeBlock.propTypes = {
42 | children: PropTypes.shape({
43 | props: PropTypes.shape({
44 | className: PropTypes.string,
45 | children: PropTypes.string.isRequired,
46 | withRunButton: PropTypes.string,
47 | }),
48 | }),
49 | };
50 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/MdxComponents/MdxCodeBlock.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import { MdxCodeBlock } from './MdxCodeBlock';
3 | import * as requestFromCodeMod from '../../CodeEditorWindow/config/RequesFromCode';
4 | import { TutorialProvider } from '../../../context/tutorial-context';
5 |
6 | const props = {
7 | children: {
8 | props: {
9 | className: 'language-json',
10 | children: '{\n "name": "test"\n}',
11 | withRunButton: 'true',
12 | },
13 | },
14 | };
15 |
16 | const requestFromCodeSpy = vi.spyOn(requestFromCodeMod, 'requestFromCode').mockImplementation(
17 | () =>
18 | new Promise((resolve) => {
19 | setTimeout(() => {
20 | resolve({ status: 'ok' });
21 | }, 100);
22 | })
23 | );
24 |
25 | describe('CodeBlock', () => {
26 | it('should render CodeBlock with given code', () => {
27 | render(
28 |
29 |
30 |
31 | );
32 |
33 | const codeBlock = screen.getByTestId('code-block');
34 |
35 | expect(codeBlock).toBeInTheDocument();
36 | expect(screen.getByTestId('code-block-pre')).toBeInTheDocument();
37 | expect(screen.getByTestId('code-block-run')).toBeInTheDocument();
38 |
39 | const textContent = codeBlock.textContent;
40 | const occurrences = (textContent.match(/"name": "test"/g) || []).length;
41 |
42 | expect(screen.getAllByText(/{/).length).toBe(2);
43 | expect(occurrences).toBe(2);
44 | expect(screen.getAllByText(/}/).length).toBe(2);
45 |
46 | expect(screen.getByText(/Run/)).toBeInTheDocument();
47 | });
48 |
49 | it('should render CodeBlock without run button', () => {
50 | const propsWithoutButton = structuredClone(props);
51 | propsWithoutButton.children.props.withRunButton = 'false';
52 |
53 | render();
54 |
55 | expect(screen.getByTestId('code-block')).toBeInTheDocument();
56 | expect(screen.getByTestId('code-block-pre')).toBeInTheDocument();
57 | expect(screen.queryByTestId('code-block-run')).not.toBeInTheDocument();
58 | });
59 |
60 | it('should render an editor with given code if RunButton is present', () => {
61 | render(
62 |
63 |
64 |
65 | );
66 |
67 | expect(screen.queryByTestId('code-block-run')).toBeInTheDocument();
68 | expect(screen.queryByTestId('code-block-editor')).toBeInTheDocument();
69 | expect(screen.getByRole('textbox')).toBeInTheDocument();
70 | expect(screen.getByRole('textbox')).toHaveValue('{\n "name": "test"\n}');
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/MdxComponents/MdxComponents.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Typography, Link, Alert } from '@mui/material';
4 | import { MdxCodeBlock } from './MdxCodeBlock';
5 | import { useTheme } from '@mui/material/styles';
6 |
7 | // if we will use mdx in other places, then this is better to be done on the App level
8 | // and passed into MDXProvider wrapping the app
9 | export const LIGHT_BACKGROUND = '#fbfbfb';
10 | export const DARK_BACKGROUND = '#1e1e1e';
11 | export const INLINE_CODE_COLOR_LIGHT = 'rgb(170, 9, 130)';
12 | export const INLINE_CODE_COLOR_DARK = 'rgb(206, 145, 120)';
13 |
14 | export const InlineCode = (props) => {
15 | const theme = useTheme();
16 | const background = theme.palette.mode === 'light' ? LIGHT_BACKGROUND : DARK_BACKGROUND;
17 | return (
18 |
29 | );
30 | };
31 |
32 | export const CustomLink = (props) => {
33 | if (props.href.includes('http')) {
34 | return ;
35 | } else {
36 | return ;
37 | }
38 | };
39 |
40 | CustomLink.propTypes = {
41 | href: PropTypes.string,
42 | };
43 |
44 | export const mdxComponents = {
45 | h1: (props) => ,
46 | h2: (props) => ,
47 | h3: (props) => ,
48 | h4: (props) => ,
49 | p: (props) => ,
50 | a: (props) => ,
51 | img: (props) =>
,
52 | pre: (props) => ,
53 | em: (props) => ,
54 | code: (props) => ,
55 | Alert: (props) => ,
56 | };
57 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/MdxPages/Index.mdx:
--------------------------------------------------------------------------------
1 | # Interactive Tutorials
2 | ## Set up your cluster, add data, and start searching!
3 |
4 | **Setup Guide**
5 | - [Quickstart](#/tutorial/quickstart) - Create a collection, upsert vectors, and run a search.
6 | - [Load Data](#/tutorial/loadcontent) - Load a prepared dataset snapshot into your collection.
7 |
8 | **Vector Search**
9 | - [Filtering - Beginner](#/tutorial/filteringbeginner) - Filter search results using basic payload conditions.
10 | - [Filtering - Advanced](#/tutorial/filteringadvanced) - Try advanced filtering based on nested payload conditions.
11 | - [Filtering - Full Text](#/tutorial/filteringfulltext) - Search for substrings, tokens, or phrases within text fields.
12 | - [Multivector Search](#/tutorial/multivectors) - Work with data represented by ColBERT multivectors.
13 | - [Sparse Vector Search](#/tutorial/sparsevectors) - Use sparse vectors to get specific search results.
14 | - [Hybrid Search](#/tutorial/hybridsearch) - Combine dense and sparse vectors for more accurate search results.
15 |
16 | **Manage Data**
17 | - [Multitenancy](#/tutorial/multitenancy) - Manage multiple users within a single collection.
18 |
19 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/MdxPages/LoadContent.mdx:
--------------------------------------------------------------------------------
1 | export const title = 'Load Content';
2 |
3 | # Load Data into a Collection from a Remote Snapshot
4 |
5 | In this tutorial, we will guide you through loading data into a Qdrant collection from a remote snapshot.
6 |
7 | ## Step 1: Import a snapshot to a collection
8 |
9 | To start, create the collection `midjourney` and load vector data into it.
10 | The collection will take on the parameters of the snapshot, with vector size of 512, and similarity measured using the Cosine distance.
11 |
12 | ```json withRunButton=true
13 | PUT /collections/midjourney/snapshots/recover
14 | {
15 | "location": "http://snapshots.qdrant.io/midlib.snapshot"
16 | }
17 | ```
18 |
19 | Wait a few moments while the vectors from the snapshot are added to the `midjourney` collection.
20 |
21 | ## Step 2: Verify the data upload
22 |
23 | After the data has been imported, it's important to verify that it has been successfully uploaded. You can do this by checking the number of vectors (or points) in the collection.
24 |
25 | Run the following request to get the vector count:
26 |
27 | ```json withRunButton=true
28 | POST /collections/midjourney/points/count
29 | ```
30 |
31 | The collection should contain 5,417 data points.
32 |
33 | ### Step 4: Open the collection UI
34 | You can also [inspect your collection](/dashboard#/collections/midjourney/) to review the uploaded data.
35 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/MdxPages/Multivectors.mdx:
--------------------------------------------------------------------------------
1 | export const title = 'Multivector Search';
2 |
3 | # Search with ColBERT Multivectors
4 |
5 | In Qdrant, multivectors allow you to store and search multiple vectors for each point in your collection. Additionally, you can store payloads, which are key-value pairs containing metadata about each point. This tutorial will show you how to create a collection, insert points with multivectors and payloads, and perform a search.
6 |
7 | ## Step 1: Create a collection with multivectors
8 |
9 | To use multivectors, you need to configure your collection to store multiple vectors per point. The collection’s configuration specifies the vector size, distance metric, and multivector settings, such as the comparator function.
10 |
11 | Run the following request to create a collection with multivectors:
12 |
13 | ```json withRunButton=true
14 | PUT collections/multivector_collection
15 | {
16 | "vectors": {
17 | "size": 4,
18 | "distance": "Dot",
19 | "multivector_config": {
20 | "comparator": "max_sim"
21 | }
22 | }
23 | }
24 | ```
25 |
26 | ## Step 2: Insert points with multivectors and payloads
27 |
28 | Now that the collection is set up, you can insert points where each point contains multiple vectors and a payload. Payloads store additional metadata, such as the planet name and its type.
29 |
30 | Run the following request to insert points with multivectors and payloads:
31 |
32 | ```json withRunButton=true
33 | PUT collections/multivector_collection/points
34 | {
35 | "points": [
36 | {
37 | "id": 1,
38 | "vector": [
39 | [-0.013, 0.020, -0.007, -0.111],
40 | [-0.030, -0.015, 0.021, 0.072],
41 | [0.041, -0.004, 0.032, 0.062]
42 | ],
43 | "payload": {
44 | "name": "Mars",
45 | "type": "terrestrial"
46 | }
47 | },
48 | {
49 | "id": 2,
50 | "vector": [
51 | [0.011, -0.050, 0.007, 0.101],
52 | [0.031, 0.014, -0.032, 0.012]
53 | ],
54 | "payload": {
55 | "name": "Jupiter",
56 | "type": "gas giant"
57 | }
58 | },
59 | {
60 | "id": 3,
61 | "vector": [
62 | [0.041, 0.034, -0.012, -0.022],
63 | [0.040, -0.095, 0.021, 0.032],
64 | [-0.030, 0.025, 0.011, 0.082],
65 | [0.021, -0.044, 0.032, -0.032]
66 | ],
67 | "payload": {
68 | "name": "Venus",
69 | "type": "terrestrial"
70 | }
71 | },
72 | {
73 | "id": 4,
74 | "vector": [
75 | [-0.015, 0.020, 0.045, -0.131],
76 | [0.041, -0.024, -0.032, 0.072]
77 | ],
78 | "payload": {
79 | "name": "Neptune",
80 | "type": "ice giant"
81 | }
82 | }
83 | ]
84 | }
85 | ```
86 |
87 | ## Step 3: Query the collection
88 |
89 | To perform a search with multivectors, you can pass multiple query vectors. Qdrant will compare the query vectors against the multivectors and return the most similar results based on the comparator defined for the collection (`max_sim`). You can also request the payloads to be returned along with the search results.
90 |
91 | Run the following request to search with multivectors and retrieve the payloads:
92 |
93 | ```json withRunButton=true
94 | POST collections/multivector_collection/points/query
95 | {
96 | "query": [
97 | [-0.015, 0.020, 0.045, -0.131],
98 | [0.030, -0.005, 0.001, 0.022],
99 | [0.041, -0.024, -0.032, 0.072]
100 | ],
101 | "with_payload": true
102 | }
103 | ```
104 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/MdxPages/Quickstart.mdx:
--------------------------------------------------------------------------------
1 | export const title = 'Quickstart';
2 |
3 | # Quickstart: Vector Search for Beginners
4 |
5 | Qdrant is designed to find the approximate nearest data points in your dataset. In this quickstart guide, you'll create a simple database to track space colonies and perform a search for the nearest colony based on its vector representation.
6 |
7 | Click **RUN** to send the API request. The response will appear on the right.
You can also edit any code block and rerun the request to see different results.
8 |
9 | ## Step 1: Create a collection
10 |
11 | First, we’ll create a collection called `star_charts` to store the colony data. Each location will be represented by a vector of four dimensions, and we'll use the Dot product as the distance metric for similarity search.
12 |
13 | Run this command to create the collection:
14 |
15 | ```json withRunButton=true
16 | PUT collections/star_charts
17 | {
18 | "vectors": {
19 | "size": 4,
20 | "distance": "Dot"
21 | }
22 | }
23 | ```
24 |
25 | ## Step 2: Load data into the collection
26 |
27 | Now that the collection is set up, let’s add some data. Each location will have a vector and additional information (payload), such as its name.
28 |
29 | Run this request to add the data:
30 |
31 | ```json withRunButton=true
32 | PUT collections/star_charts/points
33 | {
34 | "points": [
35 | {
36 | "id": 1,
37 | "vector": [0.05, 0.61, 0.76, 0.74],
38 | "payload": {
39 | "colony": "Mars"
40 | }
41 | },
42 | {
43 | "id": 2,
44 | "vector": [0.19, 0.81, 0.75, 0.11],
45 | "payload": {
46 | "colony": "Jupiter"
47 | }
48 | },
49 | {
50 | "id": 3,
51 | "vector": [0.36, 0.55, 0.47, 0.94],
52 | "payload": {
53 | "colony": "Venus"
54 | }
55 | },
56 | {
57 | "id": 4,
58 | "vector": [0.18, 0.01, 0.85, 0.80],
59 | "payload": {
60 | "colony": "Moon"
61 | }
62 | },
63 | {
64 | "id": 5,
65 | "vector": [0.24, 0.18, 0.22, 0.44],
66 | "payload": {
67 | "colony": "Pluto"
68 | }
69 | }
70 | ]
71 | }
72 | ```
73 |
74 | ## Step 3: Run a search query
75 |
76 | Now, let’s search for the three nearest colonies to a specific vector representing a spatial location. This query will return the colonies along with their payload information.
77 |
78 | Run the query below to find the nearest colonies:
79 |
80 | ```json withRunButton=true
81 | POST collections/star_charts/points/search
82 | {
83 | "vector": [0.2, 0.1, 0.9, 0.7],
84 | "limit": 3,
85 | "with_payload": true
86 | }
87 | ```
88 |
89 | ## Conclusion
90 |
91 | Congratulations! 🎉 You’ve just completed a vector search across galactic coordinates! You've successfully added spatial data into a collection and performed searches to find the nearest locations based on their vector representation.
92 |
93 | ## Next steps
94 |
95 | In the next section, you’ll explore creating complex filter conditions to refine your searches further for interstellar exploration!
96 |
97 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/TutorialFooter.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Button, Divider, Grid } from '@mui/material';
3 | import { useNavigate, useParams } from 'react-router-dom';
4 | import tutorialSubPages from './TutorialSubpages';
5 | import { ArrowBack, ArrowForward } from '@mui/icons-material';
6 |
7 | export const TutorialFooter = () => {
8 | const { pageSlug } = useParams();
9 | const pageKeys = [...tutorialSubPages.map((p) => p[0])];
10 | let currentPageIndex = pageKeys.indexOf(pageSlug);
11 | const navigate = useNavigate();
12 |
13 | const handlePrev = () => {
14 | if (currentPageIndex > 0) {
15 | currentPageIndex = currentPageIndex - 1;
16 | navigate(`/tutorial/${pageKeys[currentPageIndex]}`);
17 | } else {
18 | currentPageIndex = 0;
19 | navigate('/tutorial');
20 | }
21 | };
22 |
23 | const handleNext = () => {
24 | if (currentPageIndex < pageKeys.length - 1) {
25 | currentPageIndex = currentPageIndex + 1;
26 | navigate(`/tutorial/${pageKeys[currentPageIndex]}`);
27 | }
28 | };
29 |
30 | return (
31 |
32 |
33 |
34 |
35 | {currentPageIndex >= 0 && (
36 | }>
37 | Previous
38 |
39 | )}
40 |
41 |
42 | {currentPageIndex >= 0 && }
43 |
44 |
45 | {currentPageIndex < pageKeys.length - 1 && (
46 | }>
47 | Next
48 |
49 | )}
50 |
51 |
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/components/InteractiveTutorial/TutorialSubpages.jsx:
--------------------------------------------------------------------------------
1 | import * as Index from './MdxPages/Index.mdx';
2 | import * as Quickstart from './MdxPages/Quickstart.mdx';
3 | import * as FilteringBeginner from './MdxPages/FilteringBeginner.mdx';
4 | import * as FilteringAdvanced from './MdxPages/FilteringAdvanced.mdx';
5 | import * as FilteringFullText from './MdxPages/FilteringFullText.mdx';
6 | import * as Multivectors from './MdxPages/Multivectors.mdx';
7 | import * as SparseVectors from './MdxPages/SparseVectors.mdx';
8 | import * as HybridSearch from './MdxPages/HybridSearch.mdx';
9 | import * as Multitenancy from './MdxPages/Multitenancy.mdx';
10 | import * as LoadContent from './MdxPages/LoadContent.mdx';
11 | /**
12 | * MDX page object (Index etc.) contains:
13 | * - default: React component
14 | * - exported variables: MDX page metadata,
15 | * check out the MDX files in src/components/InteractiveTutorial/MdxPages
16 | */
17 |
18 | export const tutorialIndexPage = Index;
19 | const tutorialSubPages = [
20 | ['quickstart', Quickstart],
21 | ['loadcontent', LoadContent],
22 | ['filteringbeginner', FilteringBeginner],
23 | ['filteringadvanced', FilteringAdvanced],
24 | ['filteringfulltext', FilteringFullText],
25 | ['multivectors', Multivectors],
26 | ['sparsevectors', SparseVectors],
27 | ['hybridsearch', HybridSearch],
28 | ['multitenancy', Multitenancy],
29 | // add more pages here
30 | ];
31 |
32 | export { tutorialSubPages };
33 |
34 | export default tutorialSubPages;
35 |
--------------------------------------------------------------------------------
/src/components/JwtSection/CallScrollRequest.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Box, Button, Card, CardActionArea, CardContent, Dialog, Grid, Typography } from '@mui/material';
4 | import { getErrorMessage } from '../../lib/get-error-message';
5 | // import PointCard from '../Points/PointCard';
6 | import PointsTabs from '../Points/PointsTabs';
7 | // import ErrorIcon from '@mui/icons-material/Error';
8 |
9 | const CallScrollRequest = (props) => {
10 | const { client, collection } = props;
11 | const [errorMessage, setErrorMessage] = useState(null);
12 | const [points, setPoints] = useState(null);
13 | const [open, setOpen] = useState(false);
14 |
15 | const handleClose = () => {
16 | setOpen(false);
17 | };
18 |
19 | useEffect(() => {
20 | const getPoints = async () => {
21 | try {
22 | const newPoints = await client.scroll(collection, {
23 | limit: 10,
24 | });
25 | setPoints({
26 | points: [...(newPoints?.points || [])],
27 | });
28 | // console.log(newPoints);
29 | // setErrorMessage('Error: You do not have permission to view this collection');
30 | } catch (error) {
31 | const message = getErrorMessage(error, { withApiKey: { apiKey: client.getApiKey() } });
32 | message && setErrorMessage(message);
33 | setPoints({});
34 | }
35 | };
36 | return () => {
37 | getPoints({});
38 | };
39 | }, []);
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 | {collection}
49 |
50 | {errorMessage && (
51 |
52 | {errorMessage}
53 |
54 | )}
55 | {points && points.points && (
56 | <>
57 |
58 | {points.points.length} points
59 |
60 |
61 |
64 | >
65 | )}
66 |
67 |
68 |
69 |
70 |
83 |
84 | );
85 | };
86 |
87 | CallScrollRequest.propTypes = {
88 | client: PropTypes.object,
89 | collection: PropTypes.string,
90 | };
91 |
92 | export default CallScrollRequest;
93 |
--------------------------------------------------------------------------------
/src/components/JwtSection/JwtTokenViewer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Box, Tooltip } from '@mui/material';
5 |
6 | import TextField from '@mui/material/TextField';
7 | import { CodeBlock } from '../Common/CodeBlock';
8 |
9 | import { Visibility, VisibilityOff } from '@mui/icons-material';
10 | import { CopyButton } from '../Common/CopyButton';
11 | import InputAdornment from '@mui/material/InputAdornment';
12 | import IconButton from '@mui/material/IconButton';
13 |
14 | function JwtTokenViewer({ jwt, token, sx }) {
15 | const [isVisible, setIsVisible] = React.useState(false);
16 |
17 | const handleVisibility = () => {
18 | setIsVisible((prev) => !prev);
19 | };
20 |
21 | return (
22 |
23 |
24 |
30 |
31 |
32 |
43 | {isVisible ? : }
44 |
45 |
46 | ),
47 | }}
48 | />
49 |
50 |
51 | );
52 | }
53 |
54 | JwtTokenViewer.propTypes = {
55 | jwt: PropTypes.string.isRequired,
56 | token: PropTypes.object.isRequired,
57 | sx: PropTypes.object,
58 | };
59 |
60 | export default JwtTokenViewer;
61 |
--------------------------------------------------------------------------------
/src/components/JwtSection/PreviewTokenAccess.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { MenuItem, Select } from '@mui/material';
3 | import { Checkbox, Chip, Grid, ListItemText, Typography } from '@mui/material';
4 | import { Box } from '@mui/system';
5 | import CallScrollRequest from './CallScrollRequest';
6 | import PropTypes from 'prop-types';
7 | import qdrantClient from '../../common/client';
8 |
9 | const PreviewTokenAccess = (props) => {
10 | const { token, collections } = props;
11 | const [selectedCollections, setSelectedCollections] = useState([]);
12 | const tokenBasedClient = qdrantClient(token);
13 |
14 | useEffect(() => {
15 | console.log(collections);
16 | }, [token, collections]);
17 |
18 | return (
19 | <>
20 |
21 | Preview of token access
22 |
42 |
43 | {selectedCollections.map((collection) => (
44 |
45 | ))}
46 | >
47 | );
48 | };
49 |
50 | PreviewTokenAccess.propTypes = {
51 | token: PropTypes.string,
52 | collections: PropTypes.array,
53 | };
54 |
55 | export default PreviewTokenAccess;
56 |
--------------------------------------------------------------------------------
/src/components/JwtSection/RbacCollectionSettings.jsx:
--------------------------------------------------------------------------------
1 | function configureCollection({
2 | collectionName,
3 | isAccessible,
4 | isWritable,
5 | payloadFilters,
6 | configuredCollections,
7 | setConfiguredCollections,
8 | }) {
9 | if (isAccessible) {
10 | // Add `selectedCollection` to `configuredCollections` with new settings
11 | const collectionAccess = {
12 | collection: collectionName,
13 | };
14 |
15 | collectionAccess.access = isWritable ? 'rw' : 'r';
16 |
17 | if (Object.keys(payloadFilters).length > 0) {
18 | collectionAccess.payload = payloadFilters;
19 | }
20 |
21 | const newConfiguredCollections = configuredCollections.filter((c) => c.collection !== collectionName);
22 | setConfiguredCollections([...newConfiguredCollections, collectionAccess]);
23 | } else {
24 | // Remove `selectedCollection` from `configuredCollections` if any
25 | const newConfiguredCollections = configuredCollections.filter((c) => c.collection !== collectionName);
26 | setConfiguredCollections(newConfiguredCollections);
27 | }
28 | }
29 |
30 | export default configureCollection;
31 |
--------------------------------------------------------------------------------
/src/components/Logo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Logo = () => {
4 | return
;
5 | };
6 |
--------------------------------------------------------------------------------
/src/components/Points/PointImage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Box, CardMedia, Grid, Modal, Typography } from '@mui/material';
4 |
5 | function PointImage({ data, sx, xs = 3 }) {
6 | const [fullScreenImg, setFullScreenImg] = useState(null);
7 | const renderImages = () => {
8 | const images = [];
9 |
10 | function isImgUrl(string) {
11 | let url;
12 | try {
13 | url = new URL(string);
14 | } catch (_) {
15 | return false;
16 | }
17 | if (url) {
18 | return /\.(jpg|jpeg|png|webp|gif|svg)$/.test(url.pathname);
19 | }
20 | return false;
21 | }
22 |
23 | // Loop through the object's properties
24 | for (const key in data) {
25 | if (typeof data[key] == 'string') {
26 | // Check if the value is an image URL
27 | if (isImgUrl(data[key])) {
28 | images.push(
29 | setFullScreenImg(data[key])}
43 | />
44 | );
45 | }
46 | }
47 | }
48 |
49 | return images;
50 | };
51 |
52 | const images = renderImages();
53 |
54 | if (images.length === 0) {
55 | return null;
56 | }
57 |
58 | return (
59 |
60 | {images}
61 | setFullScreenImg(null)}
64 | componentsProps={{
65 | backdrop: {
66 | sx: { cursor: 'pointer' },
67 | title: 'Click to close',
68 | },
69 | }}
70 | >
71 |
82 | setFullScreenImg(null)}
86 | sx={{
87 | position: 'absolute',
88 | top: '-60px',
89 | right: 0,
90 | padding: 1,
91 | cursor: 'pointer',
92 | color: 'white',
93 | backgroundColor: 'rgba(0,0,0,0.5)',
94 | borderRadius: '5px',
95 | }}
96 | >
97 | Close [ESC]
98 |
99 |
107 |
108 |
109 |
110 | );
111 | }
112 | PointImage.propTypes = {
113 | data: PropTypes.object.isRequired,
114 | sx: PropTypes.object,
115 | xs: PropTypes.number, // Size of the image grid item. Default is 3.
116 | };
117 |
118 | export default PointImage;
119 |
--------------------------------------------------------------------------------
/src/components/Points/PointVectors.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Box, Button, Chip, Grid, Typography } from '@mui/material';
4 | import { CopyButton } from '../Common/CopyButton';
5 | import { bigIntJSON } from '../../common/bigIntJSON';
6 | import { useNavigate, useParams } from 'react-router-dom';
7 |
8 | /**
9 | * Component for displaying vectors of a point
10 | * @param {Object} point
11 | * @param {function} onConditionChange
12 | * @returns {JSX.Element|null}
13 | * @constructor
14 | */
15 | const Vectors = memo(function Vectors({ point, onConditionChange }) {
16 | const { collectionName } = useParams();
17 | const navigate = useNavigate();
18 | if (!Object.getOwnPropertyDescriptor(point, 'vector')) {
19 | return null;
20 | }
21 |
22 | // to unify the code, we will convert the vector to an object
23 | // when there is only one vector in the point
24 | let vectors = {};
25 | if (Array.isArray(point.vector)) {
26 | vectors[''] = point.vector;
27 | } else {
28 | vectors = point.vector;
29 | }
30 |
31 | const handleNavigate = (key) => {
32 | navigate(`/collections/${collectionName}/graph`, { state: { newInitNode: point, vectorName: key } });
33 | };
34 |
35 | return (
36 |
37 | Vectors:
38 | {Object.keys(vectors).map((key) => {
39 | return (
40 |
41 |
42 | {key === '' ? (
43 |
44 | Default vector
45 |
46 | ) : (
47 | <>
48 |
49 | Name:
50 |
51 |
52 | >
53 | )}
54 |
60 |
61 |
62 |
63 | Length:
64 |
65 |
70 |
71 |
80 |
83 | {typeof onConditionChange !== 'function' ? null : (
84 |
93 | )}
94 |
95 |
96 | );
97 | })}
98 |
99 | );
100 | });
101 |
102 | Vectors.propTypes = {
103 | point: PropTypes.object.isRequired,
104 | onConditionChange: PropTypes.func,
105 | };
106 |
107 | export default Vectors;
108 |
--------------------------------------------------------------------------------
/src/components/Points/SimilarSerachfield.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Card } from '@mui/material';
4 | import { MuiChipsInput } from 'mui-chips-input';
5 | import { bigIntJSON } from '../../common/bigIntJSON';
6 | import { validateUuid } from '../../common/utils';
7 | import ErrorNotifier from '../ToastNotifications/ErrorNotifier';
8 |
9 | function SimilarSerachfield({ conditions, onConditionChange, vectors, usingVector }) {
10 | const [errorMessage, setErrorMessage] = useState(null);
11 |
12 | const handleAddChip = (chip) => {
13 | setErrorMessage(null);
14 | const keyValue = chip.split(/:(.*)/);
15 | if (keyValue.length < 2 || !keyValue[0].trim()) {
16 | setErrorMessage('Invalid format of key:value pair');
17 | return;
18 | }
19 | const key = keyValue[0].trim();
20 | const parseToPrimitive = (value) => {
21 | try {
22 | return bigIntJSON.parse(value);
23 | } catch (e) {
24 | return value;
25 | }
26 | };
27 | const value = parseToPrimitive(keyValue[1].trim());
28 | if (key === 'id') {
29 | if (value && (typeof value === 'number' || typeof value === 'bigint' || validateUuid(value))) {
30 | const id = {
31 | key: 'id',
32 | type: 'id',
33 | value: value,
34 | };
35 | if (vectors.length > 0 && usingVector === null) {
36 | onConditionChange([...conditions, id], vectors[0]); // TODO: add vector selection
37 | return;
38 | }
39 | onConditionChange([...conditions, id]);
40 | } else {
41 | setErrorMessage('Invalid id');
42 | return;
43 | }
44 | } else if (key) {
45 | if (typeof value === 'number' && value % 1 !== 0) {
46 | setErrorMessage('Float values are not supported ');
47 | return;
48 | } else if (
49 | typeof value === 'bigint' ||
50 | typeof value === 'number' ||
51 | typeof value === 'boolean' ||
52 | typeof value === 'string' ||
53 | value === null ||
54 | value === undefined
55 | ) {
56 | const payload = {
57 | key: key,
58 | type: 'payload',
59 | value: value,
60 | };
61 | onConditionChange([...conditions, payload]);
62 | } else {
63 | setErrorMessage('Invalid value');
64 | return;
65 | }
66 | }
67 | };
68 |
69 | const handleDeleteChip = (chip) => {
70 | const newValues = conditions.filter(function (x) {
71 | return getChipValue(x) !== chip;
72 | });
73 | onConditionChange(newValues);
74 | };
75 |
76 | const getChipValue = (condition) => {
77 | if (usingVector && condition.type === 'id') {
78 | return condition.key + ': ' + condition.value + ' using ' + usingVector;
79 | }
80 | return condition.key + ': ' + condition.value;
81 | };
82 |
83 | const handleDeleteAllChips = () => {
84 | onConditionChange([]);
85 | };
86 |
87 | return (
88 |
89 | {errorMessage !== null && }
90 |
104 |
105 | );
106 | }
107 |
108 | SimilarSerachfield.propTypes = {
109 | conditions: PropTypes.array.isRequired,
110 | onConditionChange: PropTypes.func.isRequired,
111 | vectors: PropTypes.array.isRequired,
112 | usingVector: PropTypes.string,
113 | };
114 |
115 | export default SimilarSerachfield;
116 |
--------------------------------------------------------------------------------
/src/components/ResultEditorWindow/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import EditorCommon from '../EditorCommon';
4 | import { bigIntJSON } from '../../common/bigIntJSON';
5 |
6 | const ResultEditorWindow = ({ code }) => {
7 | function formatJSON(val = {}) {
8 | try {
9 | const res = bigIntJSON.parse(val);
10 | return bigIntJSON.stringify(res, null, 2);
11 | } catch {
12 | const errorJson = {
13 | error: `HERE ${val}`,
14 | };
15 | return bigIntJSON.stringify(errorJson, null, 2);
16 | }
17 | }
18 | return (
19 |
33 | );
34 | };
35 |
36 | ResultEditorWindow.propTypes = {
37 | code: PropTypes.string.isRequired,
38 | };
39 |
40 | export default ResultEditorWindow;
41 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { styled } from '@mui/material/styles';
4 | import MuiDrawer from '@mui/material/Drawer';
5 | import { List, Typography, Divider, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
6 | import { Link } from 'react-router-dom';
7 | import { LibraryBooks, Terminal, Animation, Key, RocketLaunch } from '@mui/icons-material';
8 | import Tooltip from '@mui/material/Tooltip';
9 | import SidebarTutorialSection from './SidebarTutorialSection';
10 | import { useClient } from '../../context/client-context';
11 |
12 | const drawerWidth = 240;
13 |
14 | const openedMixin = (theme) => ({
15 | width: drawerWidth,
16 | transition: theme.transitions.create('width', {
17 | easing: theme.transitions.easing.sharp,
18 | duration: theme.transitions.duration.enteringScreen,
19 | }),
20 | overflowX: 'hidden',
21 | });
22 |
23 | const closedMixin = (theme) => ({
24 | transition: theme.transitions.create('width', {
25 | easing: theme.transitions.easing.sharp,
26 | duration: theme.transitions.duration.leavingScreen,
27 | }),
28 | overflowX: 'hidden',
29 | width: `calc(${theme.spacing(7)} + 1px)`,
30 | [theme.breakpoints.up('sm')]: {
31 | width: `calc(${theme.spacing(8)} + 1px)`,
32 | },
33 | });
34 |
35 | const DrawerHeader = styled('div')(({ theme }) => ({
36 | display: 'flex',
37 | alignItems: 'center',
38 | justifyContent: 'flex-end',
39 | padding: theme.spacing(0, 1),
40 | // necessary for content to be below app bar
41 | ...theme.mixins.toolbar,
42 | }));
43 |
44 | const Drawer = styled(MuiDrawer, {
45 | shouldForwardProp: (prop) => prop !== 'open',
46 | })(({ theme, open }) => ({
47 | width: drawerWidth,
48 | flexShrink: 0,
49 | whiteSpace: 'nowrap',
50 | boxSizing: 'border-box',
51 | ...(open && {
52 | ...openedMixin(theme),
53 | '& .MuiDrawer-paper': openedMixin(theme),
54 | }),
55 | ...(!open && {
56 | ...closedMixin(theme),
57 | '& .MuiDrawer-paper': closedMixin(theme),
58 | }),
59 | }));
60 |
61 | export default function Sidebar({ open, version, jwtEnabled, jwtVisible }) {
62 | const { isRestricted } = useClient();
63 |
64 | return (
65 |
66 |
67 |
68 |
69 | {!isRestricted && sidebarItem('Welcome', , '/welcome', open)}
70 | {sidebarItem('Console', , '/console', open)}
71 | {sidebarItem('Collections', , '/collections', open)}
72 | {!isRestricted && (
73 |
74 |
75 |
76 | )}
77 |
78 | {!isRestricted && sidebarItem('Datasets', , '/datasets', open)}
79 |
80 | {!isRestricted && jwtVisible && sidebarItem('Access Tokens', , '/jwt', open, jwtEnabled)}
81 |
82 |
83 |
84 |
85 |
86 | {open ? 'Qdrant ' : ''}v{version}
87 |
88 |
89 |
90 |
91 | );
92 | }
93 |
94 | function sidebarItem(title, icon, linkPath, isOpen, enabled = true) {
95 | return (
96 |
97 |
98 |
108 |
115 | {icon}
116 |
117 |
118 |
119 |
120 |
121 | );
122 | }
123 |
124 | Sidebar.propTypes = {
125 | open: PropTypes.bool,
126 | version: PropTypes.string,
127 | jwtEnabled: PropTypes.bool,
128 | jwtVisible: PropTypes.bool,
129 | };
130 |
--------------------------------------------------------------------------------
/src/components/Sidebar/SidebarTutorialSection.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link, useLocation } from 'react-router-dom';
4 | import Tooltip from '@mui/material/Tooltip';
5 | import { ExpandLess, ExpandMore, Lightbulb } from '@mui/icons-material';
6 | import { Collapse, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
7 | import tutorialSubPages from '../InteractiveTutorial/TutorialSubpages';
8 |
9 | const TutorialsList = tutorialSubPages.map((page) => {
10 | const [slug, pageObject] = page;
11 | return (
12 |
13 |
23 |
24 |
25 |
26 | );
27 | });
28 |
29 | export const SidebarTutorialSection = ({ isSidebarOpen }) => {
30 | const [collapsed, setCollapsed] = useState(window.location.hash.includes('tutorial'));
31 | const location = useLocation();
32 |
33 | const handleChange = (e) => {
34 | e.preventDefault();
35 | setCollapsed(!collapsed);
36 | };
37 |
38 | useEffect(() => {
39 | setCollapsed(!window.location.hash.includes('tutorial'));
40 | }, [location]);
41 |
42 | return (
43 | <>
44 |
45 |
54 |
61 |
62 |
63 |
70 | {isSidebarOpen && (collapsed ? : )}
71 |
72 |
73 | {isSidebarOpen && (
74 |
75 | {TutorialsList}
76 |
77 | )}
78 | >
79 | );
80 | };
81 |
82 | SidebarTutorialSection.propTypes = {
83 | isSidebarOpen: PropTypes.bool.isRequired,
84 | };
85 | export default SidebarTutorialSection;
86 |
--------------------------------------------------------------------------------
/src/components/Snapshots/SnapshotsUpload.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useTheme } from '@mui/material/styles';
4 | import useMediaQuery from '@mui/material/useMediaQuery';
5 | import Box from '@mui/material/Box';
6 | import Tooltip from '@mui/material/Tooltip';
7 | import Button from '@mui/material/Button';
8 | import UploadFile from '@mui/icons-material/UploadFile';
9 | import Dialog from '@mui/material/Dialog';
10 | import DialogContent from '@mui/material/DialogContent';
11 | import DialogTitle from '@mui/material/DialogTitle';
12 | import { SnapshotUploadForm } from './SnapshotUploadForm';
13 | import { useClient } from '../../context/client-context';
14 |
15 | export const SnapshotsUpload = ({ onComplete, sx }) => {
16 | const theme = useTheme();
17 | const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
18 | const [open, setOpen] = React.useState(false);
19 | const { isRestricted } = useClient();
20 |
21 | const handleUploadClick = () => {
22 | if (!isRestricted) {
23 | setOpen(true);
24 | }
25 | };
26 |
27 | const handleUpload = () => {
28 | setTimeout(() => {
29 | setOpen(false);
30 | }, 1000);
31 | };
32 |
33 | return (
34 |
35 |
43 |
44 | }
48 | disabled={isRestricted}
49 | >
50 | Upload snapshot
51 |
52 |
53 |
54 |
55 |
69 |
70 | );
71 | };
72 |
73 | // props validation
74 | SnapshotsUpload.propTypes = {
75 | onComplete: PropTypes.func,
76 | sx: PropTypes.object,
77 | };
78 |
--------------------------------------------------------------------------------
/src/components/ToastNotifications/ErrorNotifier.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useSnackbar } from 'notistack';
4 | import { getSnackbarOptions } from '../Common/utils/snackbarOptions';
5 |
6 | export const ErrorNotifier = ({ message = 'Something went wrong', callback }) => {
7 | const { enqueueSnackbar, closeSnackbar } = useSnackbar();
8 | const errorSnackbarOptions = getSnackbarOptions('error', closeSnackbar, 6000);
9 |
10 | useEffect(() => {
11 | enqueueSnackbar(message, errorSnackbarOptions);
12 | typeof callback === 'function' && callback();
13 | }, [enqueueSnackbar, errorSnackbarOptions, message]);
14 |
15 | return null;
16 | };
17 |
18 | ErrorNotifier.propTypes = {
19 | message: PropTypes.string,
20 | callback: PropTypes.func,
21 | };
22 |
23 | export default ErrorNotifier;
24 |
--------------------------------------------------------------------------------
/src/components/ToastNotifications/SuccessNotifier.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState } from 'react';
2 | import Snackbar from '@mui/material/Snackbar';
3 | import MuiAlert from '@mui/material/Alert';
4 | import Slide from '@mui/material/Slide';
5 | import PropTypes from 'prop-types';
6 |
7 | const Alert = React.forwardRef((props, ref) => {
8 | return ;
9 | });
10 | Alert.displayName = 'Alert';
11 |
12 | function SlideTransition(props) {
13 | return ;
14 | }
15 |
16 | export default function SuccessNotifier({ message, setIsSuccess }) {
17 | const [open, setOpen] = useState(true);
18 |
19 | const handleClose = (event, reason) => {
20 | if (reason === 'clickaway') {
21 | return;
22 | }
23 | setIsSuccess(false);
24 | setOpen(false);
25 | };
26 |
27 | return (
28 | <>
29 |
36 |
37 | {message}
38 |
39 |
40 | >
41 | );
42 | }
43 | SuccessNotifier.propTypes = {
44 | message: PropTypes.string,
45 | setIsSuccess: PropTypes.func,
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/Uploader/StyledDragDrop.jsx:
--------------------------------------------------------------------------------
1 | import DragDrop from '@uppy/react/lib/DragDrop';
2 | import { styled } from '@mui/material/styles';
3 |
4 | export const StyledDragDrop = styled(DragDrop)(({ theme }) => ({
5 | '& .uppy-DragDrop-container': {
6 | width: '100%',
7 | backgroundColor: theme.palette.background.paper,
8 | color: theme.palette.text.primary,
9 | },
10 | }));
11 |
--------------------------------------------------------------------------------
/src/components/Uploader/StyledStatusBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import StatusBar from '@uppy/react/lib/StatusBar';
3 | import { styled } from '@mui/material/styles';
4 |
5 | export const StyledStatusBar = styled(StatusBar)(({ theme }) => ({
6 | '& .uppy-StatusBar': {
7 | backgroundColor: theme.palette.background.paper,
8 | },
9 | }));
10 |
--------------------------------------------------------------------------------
/src/components/UseTitle.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function useTitle(title) {
4 | React.useEffect(() => {
5 | const prevTitle = document.title;
6 | document.title = title;
7 | return () => {
8 | document.title = prevTitle;
9 | };
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/VisualizeChart/renderBy.js:
--------------------------------------------------------------------------------
1 | import chroma from 'chroma-js';
2 |
3 | const SCORE_GRADIENT_COLORS = ['#EB5353', '#F9D923', '#36AE7C'];
4 | const BACKGROUND_COLOR = '#36A2EB';
5 |
6 | const PALLETE = [
7 | '#3366CC',
8 | '#DC3912',
9 | '#FF9900',
10 | '#109618',
11 | '#990099',
12 | '#3B3EAC',
13 | '#0099C6',
14 | '#DD4477',
15 | '#66AA00',
16 | '#B82E2E',
17 | '#316395',
18 | '#994499',
19 | '#22AA99',
20 | '#AAAA11',
21 | '#6633CC',
22 | '#E67300',
23 | '#8B0707',
24 | '#329262',
25 | '#5574A6',
26 | '#651067',
27 | ];
28 |
29 | // const SELECTED_BORDER_COLOR = '#881177';
30 |
31 | function colorByPayload(payloadValue, colored) {
32 | if (colored[payloadValue]) {
33 | return colored[payloadValue];
34 | }
35 |
36 | const nextColorIndex = Object.keys(colored).length % PALLETE.length;
37 |
38 | colored[payloadValue] = PALLETE[nextColorIndex];
39 |
40 | return PALLETE[nextColorIndex];
41 | }
42 |
43 | // This function generates an array of colors for each point in the chart.
44 | // There are following options available for colorBy:
45 | //
46 | // - None: all points will have the same color
47 | // - typeof = "string": color points based on the source field
48 | // - {"payload": "field_name"}: color points based on the payload field
49 | // - {"discover_score": { ... } }: color points based on the discover score
50 | // - {"query": { ... }}: color points based on the query score
51 |
52 | export function generateColorBy(points, colorBy = null) {
53 | // Points example:
54 | // [
55 | // { id: 0, payload: { field_name: 1 }, score: 0.5, vector: [0.1, 0.2, ....] },
56 | // { id: 1, payload: { field_name: 2 }, score: 0.6, vector: [0.3, 0.4, ....] },
57 | // ...
58 | // ]
59 |
60 | if (!colorBy) {
61 | return Array.from({ length: points.length }, () => BACKGROUND_COLOR); // Default color
62 | }
63 |
64 | // If `colorBy` is a string, interpret as a field name
65 | if (typeof colorBy === 'string') {
66 | colorBy = { payload: colorBy };
67 | }
68 |
69 | function getNestedValue(obj, path) {
70 | return path.split('.').reduce((acc, part) => acc && acc[part], obj);
71 | }
72 |
73 | if (colorBy.payload) {
74 | const valuesToColor = {};
75 |
76 | return points.map((point) => {
77 | const payloadValue = getNestedValue(point.payload, colorBy.payload);
78 | return colorByPayload(payloadValue, valuesToColor);
79 | });
80 | }
81 |
82 | if (colorBy.query) {
83 | const scores = points.map((point) => point.score);
84 | const minScore = Math.min(...scores);
85 | const maxScore = Math.max(...scores);
86 |
87 | const colorScale = chroma.scale(SCORE_GRADIENT_COLORS);
88 | return scores.map((score) => {
89 | const normalizedScore = (score - minScore) / (maxScore - minScore);
90 | return colorScale(normalizedScore).hex();
91 | });
92 | }
93 | }
94 |
95 | export function generateSizeBy(points) {
96 | // ToDo: Intoroduce size differentiation later
97 | return points.map(() => 3);
98 | }
99 |
--------------------------------------------------------------------------------
/src/components/VisualizeChart/requestData.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | export function requestData(qdrantClient, collectionName, { limit, filter = null, using = null, color_by = null }) {
3 | // Based on the input parameters, we need to decide what kind of request we need to send
4 | // By default we should do scroll request
5 | // But if we have color_by field which uses query, it should be used instead
6 |
7 | if (color_by?.query) {
8 | const query = {
9 | query: color_by.query,
10 | limit: limit,
11 | filter: filter,
12 | with_vector: using ? [using] : true,
13 | with_payload: true,
14 | using: using ?? null,
15 | };
16 |
17 | return qdrantClient.query(collectionName, query);
18 | }
19 |
20 | // It it's not a query, we should do a scroll request
21 |
22 | const scrollQuery = {
23 | limit: limit,
24 | filter: filter,
25 | with_vector: using ? [using] : true,
26 | with_payload: true,
27 | };
28 |
29 | return qdrantClient.scroll(collectionName, scrollQuery);
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/VisualizeChart/worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import * as druid from '@saehrimnir/druidjs';
3 | import get from 'lodash/get';
4 |
5 | const MESSAGE_INTERVAL = 200;
6 |
7 | function getVectorType(vector) {
8 | if (Array.isArray(vector)) {
9 | if (Array.isArray(vector[0])) {
10 | return 'multivector';
11 | }
12 | return 'vector';
13 | }
14 | if (typeof vector === 'object') {
15 | if (vector.indices) {
16 | return 'sparse';
17 | }
18 | return 'named';
19 | }
20 | return 'unknown';
21 | }
22 |
23 | self.onmessage = function (e) {
24 | let now = new Date().getTime();
25 |
26 | const params = e?.data?.params || {};
27 |
28 | const algorithm = params.algorithm || 'TSNE';
29 |
30 | const data = [];
31 |
32 | const points = e.data?.result?.points;
33 | const vectorName = params.using;
34 |
35 | if (!points || points.length === 0) {
36 | self.postMessage({
37 | data: [],
38 | error: 'No data found',
39 | });
40 | return;
41 | }
42 |
43 | if (points.length === 1) {
44 | self.postMessage({
45 | data: [],
46 | error: `cannot perform ${params.algorithm || 'TSNE'} on single point`,
47 | });
48 | return;
49 | }
50 |
51 | for (let i = 0; i < points.length; i++) {
52 | if (!vectorName) {
53 | // Work with default vector
54 | data.push(points[i]?.vector);
55 | } else {
56 | // Work with named vector
57 | data.push(get(points[i]?.vector, vectorName));
58 | }
59 | }
60 |
61 | // Validate data
62 |
63 | for (let i = 0; i < data.length; i++) {
64 | const vector = data[i];
65 | const vectorType = getVectorType(vector);
66 |
67 | if (vectorType === 'vector') {
68 | continue;
69 | }
70 |
71 | if (vectorType === 'named') {
72 | self.postMessage({
73 | data: [],
74 | error: 'Please select a valid vector name (by `using`), default vector is not defined',
75 | });
76 | return;
77 | }
78 |
79 | self.postMessage({
80 | data: [],
81 | error: 'Vector visualization is not supported for vector type: ' + vectorType,
82 | });
83 | return;
84 | }
85 |
86 | if (data.length) {
87 | const D = new druid[algorithm](data, {}); // ex params = { perplexity : 50,epsilon :5}
88 | const next = D.generator(); // default = 500 iterations
89 |
90 | let reducedPoints = [];
91 | for (reducedPoints of next) {
92 | if (Date.now() - now > MESSAGE_INTERVAL) {
93 | now = Date.now();
94 | self.postMessage({ result: getDataset(reducedPoints), error: null });
95 | }
96 | }
97 | self.postMessage({ result: getDataset(reducedPoints), error: null });
98 | }
99 | };
100 |
101 | function getDataset(reducedPoints) {
102 | // Convert [[x1, y1], [x2, y2] ] to [ { x: x1, y: y1 }, { x: x2, y: y2 } ]
103 | return reducedPoints.map((point) => ({ x: point[0], y: point[1] }));
104 | }
105 |
--------------------------------------------------------------------------------
/src/components/authDialog/authDialog.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from '@mui/material/Button';
4 | import TextField from '@mui/material/TextField';
5 | import Dialog from '@mui/material/Dialog';
6 | import DialogActions from '@mui/material/DialogActions';
7 | import DialogContent from '@mui/material/DialogContent';
8 | import DialogContentText from '@mui/material/DialogContentText';
9 | import DialogTitle from '@mui/material/DialogTitle';
10 | import InputAdornment from '@mui/material/InputAdornment';
11 | import IconButton from '@mui/material/IconButton';
12 | import Visibility from '@mui/icons-material/Visibility';
13 | import VisibilityOff from '@mui/icons-material/VisibilityOff';
14 | import { useClient } from '../../context/client-context';
15 |
16 | export function ApiKeyDialog({ open, setOpen, onApply }) {
17 | const { settings, setSettings } = useClient();
18 | const [showApiKey, setShowApiKey] = React.useState(false);
19 |
20 | const handleClickShowApiKey = () => setShowApiKey((show) => !show);
21 |
22 | const handleMouseDown = (event) => {
23 | event.preventDefault();
24 | };
25 |
26 | const [apiKey, setApiKey] = React.useState('');
27 |
28 | const handleClose = () => {
29 | setOpen(false);
30 | };
31 |
32 | const handleApply = () => {
33 | setSettings({ ...settings, apiKey });
34 | setOpen(false);
35 | onApply();
36 | };
37 |
38 | return (
39 |
40 |
74 |
75 | );
76 | }
77 |
78 | ApiKeyDialog.propTypes = {
79 | open: PropTypes.bool.isRequired,
80 | setOpen: PropTypes.func.isRequired,
81 | onApply: PropTypes.func.isRequired,
82 | };
83 |
--------------------------------------------------------------------------------
/src/config/restricted-routes.js:
--------------------------------------------------------------------------------
1 | // add unavailable routes to show message on these pages
2 | // useful if used bookmarked or shared link
3 | export const restrictedRoutes = ['/datasets', '/jwt', '/tutorial'];
4 |
5 | export const isPathRestricted = (path) => {
6 | return restrictedRoutes.some((restrictedPath) => {
7 | if (restrictedPath.includes('*')) {
8 | const regexPath = restrictedPath.replace('*', '.*');
9 | return new RegExp(`^${regexPath}$`).test(path);
10 | }
11 | return path === restrictedPath;
12 | });
13 | };
14 |
15 | export const isTokenRestricted = (token) => {
16 | if (!token) {
17 | return false;
18 | }
19 |
20 | try {
21 | const decodedToken = JSON.parse(atob(token.split('.')[1]));
22 | if (!decodedToken.access || !Array.isArray(decodedToken.access)) {
23 | return false;
24 | }
25 | return decodedToken.access.some(({ access }) => access === 'prw');
26 | } catch (e) {
27 | return false;
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/context/client-context.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, createContext, useState, useEffect } from 'react';
2 | import { axiosInstance, setupAxios } from '../common/axios';
3 | import qdrantClient from '../common/client';
4 | import { bigIntJSON } from '../common/bigIntJSON';
5 | import { isTokenRestricted } from '../config/restricted-routes';
6 |
7 | const DEFAULT_SETTINGS = {
8 | apiKey: '',
9 | };
10 |
11 | // Write settings to local storage
12 | const persistSettings = (settings) => {
13 | localStorage.setItem('settings', bigIntJSON.stringify(settings));
14 | };
15 |
16 | // Get existing Settings from Local Storage or set default values
17 | const getPersistedSettings = () => {
18 | const settings = localStorage.getItem('settings');
19 |
20 | if (settings) return bigIntJSON.parse(settings);
21 |
22 | return DEFAULT_SETTINGS;
23 | };
24 |
25 | // React context to store the settings
26 | const ClientContext = createContext();
27 |
28 | // React hook to access and modify the settings
29 | export const useClient = () => {
30 | const context = useContext(ClientContext);
31 |
32 | if (!context) {
33 | throw new Error('useClient must be used within ClientProvider');
34 | }
35 |
36 | return {
37 | ...context,
38 | isRestricted: isTokenRestricted(context.settings.apiKey),
39 | };
40 | };
41 |
42 | // Client Context Provider
43 | export const ClientProvider = (props) => {
44 | // TODO: Switch to Reducer if we have more settings to track.
45 | const [settings, setSettings] = useState(getPersistedSettings());
46 |
47 | const client = qdrantClient(settings);
48 |
49 | setupAxios(axiosInstance, settings);
50 |
51 | useEffect(() => {
52 | setupAxios(axiosInstance, settings);
53 | persistSettings(settings);
54 | }, [settings]);
55 |
56 | return ;
57 | };
58 |
--------------------------------------------------------------------------------
/src/context/client-context.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi } from 'vitest';
2 | import { renderHook } from '@testing-library/react';
3 | import { useClient, ClientProvider } from './client-context';
4 | import { bigIntJSON } from '../common/bigIntJSON';
5 |
6 | // Mock localStorage
7 | const mockLocalStorage = {
8 | getItem: vi.fn(),
9 | setItem: vi.fn(),
10 | };
11 |
12 | Object.defineProperty(window, 'localStorage', {
13 | value: mockLocalStorage,
14 | });
15 |
16 | // Mock JWT token
17 | const mockRestrictedToken = 'eyJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3MiOlt7ImFjY2VzcyI6InBydyJ9XX0.x';
18 | const mockUnrestrictedToken = 'eyJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3MiOlt7ImFjY2VzcyI6InIifV19.x';
19 |
20 | describe('useClient', () => {
21 | beforeEach(() => {
22 | mockLocalStorage.getItem.mockReset();
23 | mockLocalStorage.setItem.mockReset();
24 | });
25 |
26 | it('should return isRestricted=true for restricted token', () => {
27 | mockLocalStorage.getItem.mockReturnValue(bigIntJSON.stringify({ apiKey: mockRestrictedToken }));
28 |
29 | const { result } = renderHook(() => useClient(), { wrapper: ClientProvider });
30 |
31 | expect(result.current.isRestricted).toBe(true);
32 | });
33 |
34 | it('should return isRestricted=false for unrestricted token', () => {
35 | mockLocalStorage.getItem.mockReturnValue(bigIntJSON.stringify({ apiKey: mockUnrestrictedToken }));
36 |
37 | const { result } = renderHook(() => useClient(), { wrapper: ClientProvider });
38 |
39 | expect(result.current.isRestricted).toBe(false);
40 | });
41 |
42 | it('should return isRestricted=false for no token', () => {
43 | mockLocalStorage.getItem.mockReturnValue(null);
44 |
45 | const { result } = renderHook(() => useClient(), { wrapper: ClientProvider });
46 |
47 | expect(result.current.isRestricted).toBe(false);
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/src/context/color-context.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const ColorModeContext = React.createContext({
4 | mode: 'auto',
5 | toggleColorMode: () => {},
6 | });
7 |
--------------------------------------------------------------------------------
/src/context/max-collections-context.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState, useContext } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const MaxCollectionsContext = createContext();
5 |
6 | export function MaxCollectionsProvider({ children }) {
7 | const [maxCollections, setMaxCollections] = useState(null);
8 |
9 | return (
10 |
11 | {children}
12 |
13 | );
14 | }
15 |
16 | MaxCollectionsProvider.propTypes = {
17 | children: PropTypes.node.isRequired,
18 | };
19 |
20 | export function useMaxCollections() {
21 | const context = useContext(MaxCollectionsContext);
22 | if (context === undefined) {
23 | throw new Error('useMaxCollections must be used within a MaxCollectionsProvider');
24 | }
25 | return context;
26 | }
27 |
--------------------------------------------------------------------------------
/src/context/tutorial-context.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, createContext, useState } from 'react';
2 |
3 | const TutorialContext = createContext({});
4 |
5 | export const useTutorial = () => useContext(TutorialContext);
6 |
7 | export const TutorialProvider = (props) => {
8 | const [result, setResult] = useState('{}');
9 |
10 | return ;
11 | };
12 |
--------------------------------------------------------------------------------
/src/hooks/useRouteAccess.js:
--------------------------------------------------------------------------------
1 | import { useLocation } from 'react-router-dom';
2 | import { useClient } from '../context/client-context';
3 | import { isPathRestricted } from '../config/restricted-routes';
4 |
5 | export const useRouteAccess = () => {
6 | const location = useLocation();
7 | const { isRestricted } = useClient();
8 |
9 | // Extract path from hash route
10 | const path = location.pathname;
11 |
12 | return {
13 | // use this to show restricted message on unavailable routes
14 | isAccessDenied: isRestricted && isPathRestricted(path),
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/src/hooks/windowHooks.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export const useWindowResize = () => {
4 | const [resized, setResized] = useState(false);
5 | const handleResized = () => setResized(!resized);
6 |
7 | useEffect(() => {
8 | window.addEventListener('resize', handleResized);
9 | return () => {
10 | window.removeEventListener('resize', handleResized);
11 | };
12 | });
13 |
14 | return { width: window.innerWidth, height: window.innerHeight };
15 | };
16 |
17 | // add more window hooks here as needed
18 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
4 | 'Droid Sans', 'Helvetica Neue', sans-serif;
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 | }
8 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@mui/material';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom/client';
4 | import './index.css';
5 | import App from './App';
6 | import reportWebVitals from './reportWebVitals';
7 | import { HashRouter } from 'react-router-dom';
8 | import { ClientProvider } from './context/client-context';
9 | import { SnackbarProvider, closeSnackbar } from 'notistack';
10 |
11 | const root = ReactDOM.createRoot(document.getElementById('root'));
12 | root.render(
13 |
14 |
15 |
16 | (
23 |
32 | )}
33 | >
34 |
35 |
36 |
37 |
38 |
39 | );
40 |
41 | // If you want to start measuring performance in your app, pass a function
42 | // to log results (for example: reportWebVitals(console.log))
43 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
44 | reportWebVitals();
45 |
--------------------------------------------------------------------------------
/src/lib/common-helpers.js:
--------------------------------------------------------------------------------
1 | export const resizeObserverWithCallback = (callback) => {
2 | return new ResizeObserver((entries) => {
3 | for (const entry of entries) {
4 | const { target } = entry;
5 | const { width, height } = target.getBoundingClientRect();
6 | if (typeof callback === 'function') callback(width, height);
7 | }
8 | });
9 | };
10 |
11 | /**
12 | * Compare two semver strings.
13 | * @param {string} version1 - The first version string.
14 | * @param {string} version2 - The second version string.
15 | * @return {number} - Returns 0 if the versions are equal, 1 if version1 is greater, and -1 if version2 is greater.
16 | */
17 | export const compareSemver = function (version1, version2) {
18 | const parseVersion = (version) => version.split('.').map(Number);
19 |
20 | const [major1, minor1, patch1] = parseVersion(version1);
21 | const [major2, minor2, patch2] = parseVersion(version2);
22 |
23 | if (major1 !== major2) {
24 | return major1 > major2 ? 1 : -1;
25 | }
26 | if (minor1 !== minor2) {
27 | return minor1 > minor2 ? 1 : -1;
28 | }
29 | if (patch1 !== patch2) {
30 | return patch1 > patch2 ? 1 : -1;
31 | }
32 | return 0;
33 | };
34 |
--------------------------------------------------------------------------------
/src/lib/get-error-message.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Get error message from error object
3 | * @param {Error} e - error object
4 | * @param {?Object} [options] - options: {defaultMessage: string, withApiKey: {apiKey: string}}
5 | * @param {?string} [options.fallbackMessage] - fallback error message
6 | * @param {?Object} [options.withApiKey] - object with apiKey
7 | * @param {?string} [options.withApiKey.apiKey] - apiKey
8 | * @return {null|string}
9 | */
10 | export const getErrorMessage = (e, options = {}) => {
11 | const { fallbackMessage = 'Something went wrong.', withApiKey = null } = options;
12 | const { apiKey } = withApiKey || {};
13 | let message;
14 |
15 | try {
16 | // error is instance of ApiError
17 | const error = e.getActualType();
18 | if ((error.status === 401 || error.status === 403) && withApiKey) {
19 | if (!apiKey) {
20 | return null;
21 | } else {
22 | return 'Your API key is invalid. Please, set a new one.';
23 | }
24 | }
25 | message = error.data?.status?.error || e.message || fallbackMessage;
26 | } catch (err) {
27 | // error is not instance of ApiError
28 | message = e?.message || fallbackMessage;
29 | }
30 | return message;
31 | };
32 |
--------------------------------------------------------------------------------
/src/lib/rehype-meta-as-attributes.js:
--------------------------------------------------------------------------------
1 | /** A plugin for Rehype to convert meta tags of code blocks to attributes in MDX files. */
2 |
3 | const re = /\b([-\w]+)(?:=(?:"([^"]*)"|'([^']*)'|([^"'\s]+)))?/g;
4 | const transformer = async (tree) => {
5 | const visit = await import('unist-util-visit');
6 |
7 | visit.visit(tree, `element`, (node) => {
8 | let match;
9 |
10 | if (node.tagName === `code` && node.data && node.data.meta) {
11 | re.lastIndex = 0; // Reset regex.
12 |
13 | while ((match = re.exec(node.data.meta))) {
14 | node.properties[match[1]] = match[2] || match[3] || match[4] || true;
15 | }
16 | }
17 | });
18 | };
19 |
20 | export const rehypeMetaAsAttributes = () => transformer;
21 |
--------------------------------------------------------------------------------
/src/lib/tests/common-helpers.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { compareSemver } from '../common-helpers';
3 |
4 | describe('compareSemver', () => {
5 | it('should return 0 if the versions are equal', () => {
6 | const version1 = '1.0.0';
7 | const version2 = '1.0.0';
8 | const result = compareSemver(version1, version2);
9 | expect(result).toEqual(0);
10 | });
11 |
12 | it('should return 1 if version1 is greater', () => {
13 | const version1 = '1.0.1';
14 | const version2 = '1.0.0';
15 | const result = compareSemver(version1, version2);
16 | expect(result).toEqual(1);
17 | });
18 |
19 | it('should return -1 if version2 is greater', () => {
20 | const version1 = '1.0.0';
21 | const version2 = '1.0.1';
22 | const result = compareSemver(version1, version2);
23 | expect(result).toEqual(-1);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/lib/tests/graph-visualization-helpers.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { getMinimalSpanningTree } from '../graph-visualization-helpers';
3 |
4 | describe('getMinimalSpanningTree', () => {
5 | it('should return the minimal spanning tree for a given set of links (ascending order)', () => {
6 | const links = [
7 | { source: 'A', target: 'B', score: 1 },
8 | { source: 'B', target: 'C', score: 2 },
9 | { source: 'A', target: 'C', score: 3 },
10 | { source: 'C', target: 'D', score: 4 },
11 | { source: 'B', target: 'D', score: 5 },
12 | ];
13 |
14 | const expectedMST = [
15 | { source: 'B', target: 'D', score: 5 },
16 | { source: 'C', target: 'D', score: 4 },
17 | { source: 'A', target: 'C', score: 3 },
18 | ];
19 |
20 | const result = getMinimalSpanningTree(links, true);
21 | expect(result).toEqual(expectedMST);
22 | });
23 |
24 | it('should return the minimal spanning tree for a given set of links (descending order)', () => {
25 | const links = [
26 | { source: 'A', target: 'B', score: 1 },
27 | { source: 'B', target: 'C', score: 2 },
28 | { source: 'A', target: 'C', score: 3 },
29 | { source: 'C', target: 'D', score: 4 },
30 | { source: 'B', target: 'D', score: 5 },
31 | ];
32 |
33 | const expectedMST = [
34 | { source: 'A', target: 'B', score: 1 },
35 | { source: 'B', target: 'C', score: 2 },
36 | { source: 'C', target: 'D', score: 4 },
37 | ];
38 |
39 | const result = getMinimalSpanningTree(links, false);
40 | expect(result).toEqual(expectedMST);
41 | });
42 |
43 | it('should return an empty array if no links are provided', () => {
44 | const links = [];
45 | const expectedMST = [];
46 | const result = getMinimalSpanningTree(links, true);
47 | expect(result).toEqual(expectedMST);
48 | });
49 |
50 | it('should handle a single link correctly', () => {
51 | const links = [{ source: 'A', target: 'B', score: 1 }];
52 | const expectedMST = [{ source: 'A', target: 'B', score: 1 }];
53 | const result = getMinimalSpanningTree(links, true);
54 | expect(result).toEqual(expectedMST);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/lib/update-history.js:
--------------------------------------------------------------------------------
1 | import { bigIntJSON } from '../common/bigIntJSON';
2 |
3 | /**
4 | * Update history in local storage
5 | * @param {object} data
6 | * @return {array} history
7 | */
8 | export function updateHistory(data) {
9 | const history = localStorage.getItem('history') ? bigIntJSON.parse(localStorage.getItem('history')) : [];
10 |
11 | // Prevent using whole quota of local storage
12 | if (history.length >= 25) {
13 | history.pop();
14 | }
15 | history.unshift({
16 | idx: data.method + data.endpoint + Date.now(),
17 | code: data,
18 | time: new Date().toLocaleTimeString(),
19 | date: new Date().toLocaleDateString(),
20 | });
21 | localStorage.setItem('history', bigIntJSON.stringify(history));
22 | return history;
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/Collection.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
3 | import { Typography, Grid, Tabs, Tab } from '@mui/material';
4 | import { CenteredFrame } from '../components/Common/CenteredFrame';
5 | import Box from '@mui/material/Box';
6 | import { SnapshotsTab } from '../components/Snapshots/SnapshotsTab';
7 | import CollectionInfo from '../components/Collections/CollectionInfo';
8 | import PointsTabs from '../components/Points/PointsTabs';
9 | import SearchQuality from '../components/Collections/SearchQuality/SearchQuality';
10 | import { useClient } from '../context/client-context';
11 |
12 | function Collection() {
13 | const { collectionName } = useParams();
14 | const navigate = useNavigate();
15 | const location = useLocation();
16 | const [currentTab, setCurrentTab] = useState(location.hash.slice(1) || 'points');
17 | const { isRestricted } = useClient();
18 |
19 | const handleTabChange = (event, newValue) => {
20 | if (typeof newValue !== 'string') {
21 | return;
22 | }
23 | setCurrentTab(newValue);
24 | navigate(`#${newValue}`);
25 | };
26 | return (
27 | <>
28 |
29 |
30 |
31 | {collectionName}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {!isRestricted && }
40 | {!isRestricted && }
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {currentTab === 'info' && }
49 | {!isRestricted && currentTab === 'quality' && }
50 | {currentTab === 'points' && }
51 | {!isRestricted && currentTab === 'snapshots' && }
52 |
53 |
54 |
55 | >
56 | );
57 | }
58 |
59 | export default Collection;
60 |
--------------------------------------------------------------------------------
/src/pages/Homepage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import LinearProgress from '@mui/material/LinearProgress';
4 | import { useClient } from '../context/client-context';
5 |
6 | const Homepage = () => {
7 | const { client } = useClient();
8 | const navigate = useNavigate();
9 | const [loading, setLoading] = useState(false);
10 |
11 | useEffect(() => {
12 | setLoading(true);
13 | client
14 | .getCollections()
15 | .then((data) => {
16 | setLoading(false);
17 | if (data.collections.length > 0) {
18 | navigate('/collections');
19 | } else {
20 | navigate('/welcome');
21 | }
22 | })
23 | .catch((error) => {
24 | console.error(error);
25 | });
26 | }, [client]);
27 |
28 | return <>{loading ? : null}>;
29 | };
30 |
31 | export default Homepage;
32 |
--------------------------------------------------------------------------------
/src/pages/Tutorial.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InteractiveTutorial from '../components/InteractiveTutorial/InteractiveTutorial';
3 | import { TutorialProvider } from '../context/tutorial-context';
4 | import { useParams } from 'react-router-dom';
5 | import { Alert, Box, Grid } from '@mui/material';
6 | import { useClient } from '../context/client-context';
7 |
8 | export const Tutorial = () => {
9 | const { pageSlug } = useParams();
10 | const { isRestricted } = useClient();
11 |
12 | if (isRestricted) {
13 | return (
14 |
15 |
16 |
17 | Access Denied: Because of the serverless mode, tutorial will not work here properly. Please contact your
18 | administrator.
19 |
20 |
21 |
22 | );
23 | }
24 | return (
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default Tutorial;
32 |
--------------------------------------------------------------------------------
/src/pages/Welcome.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Box, Button, Typography } from '@mui/material';
4 | import { styled } from '@mui/material/styles';
5 |
6 | const ButtonsContainer = styled(Box)`
7 | display: flex;
8 | justify-content: flex-start;
9 | gap: 1rem;
10 | margin: 2rem 0;
11 | `;
12 |
13 | const StyledButton = styled((props) => )`
14 | background-color: #333;
15 | color: white;
16 | font-size: 1rem;
17 | text-transform: capitalize;
18 | &:hover {
19 | background-color: #555;
20 | }
21 | `;
22 |
23 | const StyledAbstract = styled(Typography)`
24 | max-width: 600px;
25 | margin-bottom: 2rem;
26 | `;
27 |
28 | const Welcome = () => {
29 | return (
30 |
31 |
32 |
33 | Welcome to Qdrant!
34 |
35 |
36 |
37 |
38 |
39 | Begin by setting up your collection
40 |
41 |
42 | Start building your app by creating a collection and inserting your vectors. Interactive tutorials will show
43 | you how to organize data and perform searches.
44 |
45 |
46 |
47 | Quickstart
48 |
49 |
50 | Load Sample Data
51 |
52 |
53 | Vector Search Tutorials
54 |
55 |
56 |
57 |
58 |
59 |
60 | Connect to your new project
61 |
62 |
63 | Easily interact with your database using Qdrant SDKs and our REST API. Use these libraries to connect, query,
64 | and manage your vector data from the app.
65 |
66 |
67 |
74 | API Reference
75 |
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default Welcome;
83 |
--------------------------------------------------------------------------------
/src/reportWebVitals.jsx:
--------------------------------------------------------------------------------
1 | const reportWebVitals = (onPerfEntry) => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/routes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Home from './pages/Home';
3 | import Console from './pages/Console';
4 | import Collections from './pages/Collections';
5 | import Collection from './pages/Collection';
6 | import Visualize from './pages/Visualize';
7 | import Tutorial from './pages/Tutorial';
8 | import Datasets from './pages/Datasets';
9 | import Jwt from './pages/Jwt';
10 | import Graph from './pages/Graph';
11 | import Welcome from './pages/Welcome';
12 | import Homepage from './pages/Homepage';
13 |
14 | const routes = () => [
15 | {
16 | path: '/',
17 | element: ,
18 | children: [
19 | { path: '/', element: },
20 | { path: '/welcome', element: },
21 | { path: '/console', element: },
22 | { path: '/datasets', element: },
23 | { path: '/collections', element: },
24 | { path: '/collections/:collectionName', element: },
25 | {
26 | path: '/collections/:collectionName/visualize',
27 | element: ,
28 | },
29 | {
30 | path: '/collections/:collectionName/graph',
31 | element: ,
32 | },
33 | { path: '/tutorial', element: },
34 | { path: '/tutorial/:pageSlug', element: },
35 | { path: '/jwt', element: },
36 | ],
37 | },
38 | ];
39 |
40 | export default routes;
41 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import { expect, afterEach } from 'vitest';
2 | import { cleanup } from '@testing-library/react';
3 | import matchers from '@testing-library/jest-dom/matchers';
4 |
5 | // extends Vitest's expect method with methods from react-testing-library
6 | expect.extend(matchers);
7 |
8 | // runs a cleanup after each test case (e.g. clearing jsdom)
9 | afterEach(() => {
10 | cleanup();
11 | });
12 |
--------------------------------------------------------------------------------
/src/theme/dark-theme.js:
--------------------------------------------------------------------------------
1 | import { alpha } from '@mui/material';
2 |
3 | const background = {
4 | paper: '#252525',
5 | };
6 |
7 | const getVariant = ({ theme, ownerState }) => {
8 | // dual cards have a white background and a 1px border around them
9 | // in the light theme (alike variant="outlined" cards)
10 | // and a dark background and no border in the dark theme
11 | // (alike variant="elevation" elevation={1} cards)
12 | if (ownerState?.variant === 'dual') {
13 | return {
14 | backgroundColor: background.paper,
15 | };
16 | }
17 | if (ownerState?.variant === 'heading') {
18 | return {
19 | backgroundColor: alpha(theme.palette.primary.main, 0.05),
20 | };
21 | }
22 | };
23 |
24 | // these options override the base dark theme
25 | export const darkThemeOptions = {
26 | palette: {
27 | background: {
28 | code: '#1e1e1e',
29 | card: background.paper,
30 | },
31 | },
32 | components: {
33 | MuiCard: {
34 | styleOverrides: {
35 | // this adds variant="dual" and variant="heading" support
36 | // to the Card component
37 | root: getVariant,
38 | },
39 | },
40 | MuiPaper: {
41 | styleOverrides: {
42 | // this adds variant="dual" and variant="heading" support
43 | // to the Paper component
44 | root: getVariant,
45 | },
46 | },
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/theme/index.js:
--------------------------------------------------------------------------------
1 | import { createTheme as createMuiTheme } from '@mui/material/styles';
2 | import { darkThemeOptions } from './dark-theme';
3 | import { lightThemeOptions } from './light-theme';
4 | import { alpha } from '@mui/material';
5 |
6 | const themeOptions = {
7 | components: {
8 | MuiCardHeader: {
9 | styleOverrides: {
10 | // this adds variant="heading" support
11 | // to the CardHeader component
12 | root: ({ theme, ownerState }) => {
13 | if (ownerState?.variant === 'heading') {
14 | return {
15 | backgroundColor: alpha(theme.palette.primary.main, 0.05),
16 | };
17 | }
18 | },
19 | },
20 | },
21 | },
22 | };
23 |
24 | export const createTheme = (config) => {
25 | return createMuiTheme(config, themeOptions, config.palette.mode === 'dark' ? darkThemeOptions : lightThemeOptions);
26 | };
27 |
--------------------------------------------------------------------------------
/src/theme/light-theme.js:
--------------------------------------------------------------------------------
1 | import { alpha } from '@mui/material';
2 |
3 | const getVariant = ({ theme, ownerState }) => {
4 | // this adds variant="dual" support to the Card component
5 | // dual cards have a white background and a 1px border around them
6 | // in the light theme (alike variant="outlined" cards)
7 | // and a dark background and no border in the dark theme
8 | // (alike variant="elevation" elevation={1} cards)
9 | if (ownerState?.variant === 'dual') {
10 | return {
11 | border: `1px solid ${theme.palette.divider}`,
12 | };
13 | }
14 | if (ownerState?.variant === 'heading') {
15 | return {
16 | backgroundColor: alpha(theme.palette.primary.main, 0.05),
17 | };
18 | }
19 | };
20 |
21 | // these options override the base light theme
22 | export const lightThemeOptions = {
23 | palette: {
24 | background: {
25 | code: '#fbfbfb',
26 | },
27 | },
28 | components: {
29 | MuiCard: {
30 | styleOverrides: {
31 | // this adds variant="dual" and variant="heading" support
32 | // to the Card component
33 | root: getVariant,
34 | },
35 | },
36 | MuiPaper: {
37 | styleOverrides: {
38 | // this adds variant="dual" and variant="heading" support
39 | // to the Paper component
40 | root: getVariant,
41 | },
42 | },
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import reactRefresh from '@vitejs/plugin-react';
3 | import svgrPlugin from 'vite-plugin-svgr';
4 | import eslintPlugin from 'vite-plugin-eslint';
5 | import {rehypeMetaAsAttributes} from "./src/lib/rehype-meta-as-attributes";
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig(async () => {
9 | const mdx = await import('@mdx-js/rollup');
10 |
11 | return {
12 | base: './',
13 | // This changes the output dir from dist to build
14 | // comment this out if that isn't relevant for your project
15 | build: {
16 | outDir: 'dist',
17 | },
18 | plugins: [
19 | reactRefresh(),
20 | svgrPlugin({
21 | svgrOptions: {
22 | icon: true,
23 | // ...svgr options (https://react-svgr.com/docs/options/)
24 | },
25 | }),
26 | eslintPlugin({
27 | include: ['src/**/*.jsx', 'src/**/*.js', 'src/**/*.ts', 'src/**/*.tsx'],
28 | exclude: [
29 | 'node_modules/**',
30 | 'dist/**, build/**',
31 | '**/*.mdx',
32 | '**/*.md'],
33 | }),
34 | mdx.default({
35 | rehypePlugins: [
36 | rehypeMetaAsAttributes,
37 | ],
38 | }),
39 | ],
40 | test: {
41 | globals: true,
42 | environment: 'jsdom',
43 | setupFiles: ['./src/setupTests.js'],
44 | },
45 | }
46 | });
47 |
--------------------------------------------------------------------------------