├── .env
├── .eslintignore
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── config.yml
├── dependabot.yml
├── release-draft-template.yml
├── scripts
│ └── check-release.sh
└── workflows
│ ├── publish-build.yml
│ ├── release-drafter.yml
│ └── tests.yml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── .storybook
├── main.js
└── preview.js
├── .yarn
└── releases
│ └── yarn-4.6.0.cjs
├── .yarnrc.yml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── assets
├── logo.svg
├── storybook.png
└── trumen_quick_loop.gif
├── bors.toml
├── cypress.config.js
├── cypress
├── e2e
│ ├── side-panel.cy.js
│ ├── test-api-key-query-param.cy.js
│ ├── test-api-key-required.cy.js
│ ├── test-interface.cy.js
│ ├── test-no-api-key-required.cy.js
│ ├── test-no-meilisearch.cy.js
│ ├── test-search.cy.js
│ └── test-select-indexes.cy.js
├── fixtures
│ ├── movies.json
│ └── pokemon.json
└── support
│ ├── commands.js
│ └── e2e.js
├── jsconfig.json
├── package.json
├── public
├── favicon-32x32.png
├── fonts
│ ├── Barlow
│ │ └── regular.woff2
│ └── Work_Sans
│ │ ├── bold.woff2
│ │ ├── light.woff2
│ │ ├── medium.woff2
│ │ └── regular.woff2
├── index.html
├── logo.svg
├── manifest.json
└── robots.txt
├── src
├── App.js
├── GlobalStyle.js
├── components
│ ├── ApiKeyAwarenessBanner.js
│ ├── ApiKeyModalContent.js
│ ├── Badge.js
│ ├── Body.js
│ ├── BodyWrapper.js
│ ├── Box.js
│ ├── Button.js
│ ├── Card.js
│ ├── Checkbox.js
│ ├── Container.js
│ ├── EmptyView.js
│ ├── Header
│ │ ├── HelpCenter.js
│ │ └── index.js
│ ├── IconButton.js
│ ├── Input.js
│ ├── Link.js
│ ├── Modal.js
│ ├── NewsletterForm.jsx
│ ├── NoMeilisearchRunning.js
│ ├── NoSelectOption.js
│ ├── OnBoarding.js
│ ├── Results
│ │ ├── Highlight.js
│ │ ├── Hit.js
│ │ ├── InfiniteHits.js
│ │ ├── NoResultForRequest.js
│ │ └── index.js
│ ├── RightPanel.jsx
│ ├── ScrollToTop.js
│ ├── SearchBox.js
│ ├── Select.js
│ ├── Sidebar.js
│ ├── Stats.js
│ ├── Toggle.js
│ ├── Typography.js
│ └── icons
│ │ ├── AlertCircle.js
│ │ ├── ArrowDown.js
│ │ ├── Cross.js
│ │ ├── DiscordLogo.js
│ │ ├── DocumentBig.js
│ │ ├── DocumentMedium.js
│ │ ├── GithubLogo.js
│ │ ├── Indexes.js
│ │ ├── InterrogationMark.js
│ │ ├── Key.js
│ │ ├── KeyBig.js
│ │ ├── LogoText.js
│ │ ├── MeilisearchLogo.js
│ │ ├── Picture.js
│ │ ├── SearchMedium.js
│ │ ├── SearchSmall.js
│ │ ├── SettingsBig.js
│ │ ├── SettingsMedium.js
│ │ ├── Speed.js
│ │ ├── heroicons
│ │ ├── AcademicHatIcon.jsx
│ │ ├── ArrowPathIcon.jsx
│ │ ├── ChatBubbleIcon.jsx
│ │ ├── CheckIcon.jsx
│ │ ├── CloseIcon.jsx
│ │ ├── LifebuoyIcon.jsx
│ │ └── MenuBarsIcon.jsx
│ │ ├── index.js
│ │ └── svg
│ │ ├── alert-circle.svg
│ │ ├── arrow_down.svg
│ │ ├── cross.svg
│ │ ├── discord-logo.svg
│ │ ├── document_big.svg
│ │ ├── document_medium.svg
│ │ ├── github_logo.svg
│ │ ├── indexes.svg
│ │ ├── interrogation_mark.svg
│ │ ├── key.svg
│ │ ├── key_big.svg
│ │ ├── logo_text.svg
│ │ ├── meilisearch_logo.svg
│ │ ├── picture.svg
│ │ ├── search_medium.svg
│ │ ├── search_small.svg
│ │ ├── settings_big.svg
│ │ ├── settings_medium.svg
│ │ └── speed.svg
├── context
│ ├── ApiKeyContext.js
│ └── MeilisearchClientContext.js
├── hooks
│ ├── useLocalStorage.js
│ └── useNewsletter.js
├── index.js
├── stories
│ ├── Badge.stories.js
│ ├── Button.stories.js
│ ├── Card.stories.js
│ ├── Checkbox.stories.js
│ ├── Container.stories.js
│ ├── EmptyView.stories.js
│ ├── IconButton.stories.js
│ ├── Icons.stories.js
│ ├── Input.stories.js
│ ├── Link.stories.js
│ ├── Modal.stories.js
│ ├── Select.stories.js
│ ├── Sidebar.stories.js
│ ├── Stats.stories.js
│ ├── Toggle.stories.js
│ └── Typography.stories.js
├── theme.js
├── utils
│ ├── getIndexesListWithStats.js
│ ├── hasAnApiKeySet.js
│ ├── isCloudBannerEnabled.js
│ └── shouldDisplayApiKeyModal.js
└── version
│ ├── client-agents.js
│ └── version.js
├── validate-env.js
├── version-script.js
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | # Hubspot newsletter
2 | REACT_APP_HUBSPOT_PORTAL_ID=25945010
3 | REACT_APP_HUBSPOT_FORM_GUID=991e2a09-77c2-4428-9242-ebf26bfc6c64
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | data.ms/
3 | public/
4 | node_modules/
5 | build/*
6 | src/components/icons/*
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es2021": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 2020
9 | },
10 | "extends": [
11 | "react-app",
12 | "airbnb",
13 | "plugin:jsx-a11y/recommended",
14 | "plugin:cypress/recommended",
15 | "eslint-config-prettier"
16 | ],
17 | "plugins": ["jsx-a11y", "prettier"],
18 | "settings": {
19 | "import/resolver": {
20 | "node": {
21 | "paths": ["src"]
22 | }
23 | }
24 | },
25 | "rules": {
26 | "semi": 0,
27 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
28 | "prettier/prettier": [
29 | "error",
30 | {
31 | "semi": false
32 | }
33 | ],
34 | "react/prop-types": 0,
35 | "jsx-indent-props": 0,
36 | "react/jsx-props-no-spreading": 0,
37 | "react/no-array-index-key": 0,
38 | "no-underscore-dangle": 0,
39 | "react-hooks/exhaustive-deps": 0,
40 | "react/function-component-definition": [
41 | 2,
42 | { "namedComponents": "arrow-function" }
43 | ],
44 | "react/no-arrow-function-lifecycle": 0,
45 | "react/no-invalid-html-attribute": 0,
46 | "react/jsx-no-useless-fragment": "off",
47 | "react/no-unused-class-component-methods": 0,
48 | "react/require-default-props": "off"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report 🐞
3 | about: Create a report to help us improve.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 |
10 |
11 | **Description**
12 | Description of what the bug is about.
13 |
14 | **Expected behavior**
15 | What you expected to happen.
16 |
17 | **Current behavior**
18 | What happened.
19 |
20 | **Screenshots or Logs**
21 | If applicable, add screenshots or logs to help explain your problem.
22 |
23 | **Environment (please complete the following information):**
24 | - Meilisearch version: [e.g. v.0.24.0]
25 | - Browser: [e.g. Chrome version 90.0]
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Meilisearch's bug
4 | url: https://github.com/meilisearch/meilisearch/issues
5 | about: Please report issues regarding Meilisearch's code base here
6 | - name: Feature request & feedback about Meilisearch
7 | url: https://github.com/meilisearch/product/discussions/categories/feedback-feature-proposal
8 | about: Please open a discussion or comment an already-existing one in our dedicated product repository
9 | - name: Support questions & other
10 | url: https://discord.meilisearch.com/
11 | about: Support is not handled here but on our Discord
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'npm'
4 | directory: '/'
5 | schedule:
6 | interval: 'monthly'
7 | labels:
8 | - skip-changelog
9 | - dependencies
10 |
--------------------------------------------------------------------------------
/.github/release-draft-template.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION 🌻'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | exclude-labels:
4 | - 'skip-changelog'
5 | version-resolver:
6 | minor:
7 | labels:
8 | - 'breaking-change'
9 | default: patch
10 | categories:
11 | - title: '⚠️ Breaking changes'
12 | label: 'breaking-change'
13 | - title: '🚀 Enhancements'
14 | label: 'enhancement'
15 | - title: '🐛 Bug Fixes'
16 | label: 'bug'
17 | - title: '🔒 Security'
18 | label: 'security'
19 | - title: '⚙️ Maintenance/misc'
20 | label:
21 | - 'maintenance'
22 | - 'documentation'
23 | template: |
24 | ## Changes
25 |
26 | $CHANGES
27 |
28 | Thanks again to $CONTRIBUTORS! 🎉
29 | no-changes-template: 'Changes are coming soon 😎'
30 | sort-direction: 'ascending'
31 | replacers:
32 | - search: '/(?:and )?@dependabot-preview(?:\[bot\])?,?/g'
33 | replace: ''
34 | - search: '/(?:and )?@dependabot(?:\[bot\])?,?/g'
35 | replace: ''
36 | - search: '/(?:and )?@meili-bors(?:\[bot\])?,?/g'
37 | replace: ''
38 | - search: '/(?:and )?@meili-bot,?/g'
39 | replace: ''
40 |
--------------------------------------------------------------------------------
/.github/scripts/check-release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Checking if current tag matches the package version
4 | current_tag=$(echo $GITHUB_REF | tr -d 'refs/tags/v')
5 | file_tag=$(grep '"version":' package.json | cut -d ':' -f 2- | tr -d ' ' | tr -d '"' | tr -d ',')
6 |
7 | package_file_tag=$(grep '"version":' package.json | cut -d ':' -f 2- | tr -d ' ' | tr -d '"' | tr -d ',')
8 | package_file_name='package.json'
9 | version_file_tag=$(grep "export default" src/version/version.js | cut -d " " -f 3- | tr -d " " | tr -d "'")
10 | version_file_name='src/version/version.js'
11 |
12 | if [ "$current_tag" != "$file_tag" ]; then
13 | echo "Error: the current tag does not match the version in package file(s)."
14 | echo "$package_file_name: $current_tag vs $package_file_tag"
15 | echo "$version_file_name: $current_tag vs $version_file_tag"
16 | exit 1
17 | fi
18 |
19 | echo 'OK'
20 | exit 0
21 |
--------------------------------------------------------------------------------
/.github/workflows/publish-build.yml:
--------------------------------------------------------------------------------
1 | name: publish-build
2 | on:
3 | release:
4 | types: [released]
5 |
6 | jobs:
7 | build-project:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | - name: Setup node and cache
12 | uses: actions/setup-node@v4
13 | with:
14 | node-version: 18
15 | cache: "yarn"
16 | cache-dependency-path: yarn.lock
17 | - name: Check release validity
18 | run: sh .github/scripts/check-release.sh
19 | - name: Install project dependencies
20 | run: yarn install
21 | - name: Build project
22 | run: yarn build
23 | - name: Create ZIP folder
24 | uses: thedoctor0/zip-release@master
25 | with:
26 | filename: 'build.zip'
27 | directory: 'build'
28 | - name: Upload build to release
29 | uses: svenstaro/upload-release-action@v2
30 | with:
31 | repo_token: ${{ secrets.GITHUB_TOKEN }}
32 | file: build/build.zip
33 | tag: ${{ github.ref }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | update_release_draft:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: release-drafter/release-drafter@v5
13 | with:
14 | config-name: release-draft-template.yml
15 | env:
16 | GITHUB_TOKEN: ${{ secrets.RELEASE_DRAFTER_TOKEN }}
17 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | pull_request:
5 | push:
6 | # trying and staging branches are for BORS config
7 | branches:
8 | - trying
9 | - staging
10 | - main
11 | merge_group:
12 |
13 | jobs:
14 | linter_check:
15 | runs-on: ubuntu-latest
16 | name: linter-check
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: actions/setup-node@v4
20 | with:
21 | node-version: 18
22 | cache: "yarn"
23 | cache-dependency-path: yarn.lock
24 | - name: Enable Corepack
25 | run: corepack enable
26 | - name: Install dev dependencies
27 | run: yarn --frozen-lockfile
28 | - name: Run style check
29 | run: yarn lint && yarn prettier-check
30 | cypress_no_meilisearch:
31 | runs-on: ubuntu-latest
32 | container:
33 | image: cypress/browsers:latest
34 | options: --user 1001
35 | steps:
36 | - uses: actions/checkout@v4
37 | - name: Setup node and cache
38 | uses: actions/setup-node@v4
39 | with:
40 | node-version: 18
41 | cache: "yarn"
42 | cache-dependency-path: yarn.lock
43 | - name: Enable Corepack
44 | run: corepack enable
45 | - name: Install dependencies
46 | run: yarn --frozen-lockfile
47 | - name: Test no meilisearch running
48 | uses: cypress-io/github-action@v6
49 | with:
50 | start: yarn start:ci
51 | wait-on: 'http://0.0.0.0:3000'
52 | command: yarn cy:run:test-no-meilisearch
53 | config-file: cypress.config.js
54 | - uses: actions/upload-artifact@v4
55 | if: failure()
56 | with:
57 | name: cypress-screenshots
58 | path: cypress/screenshots
59 | - uses: actions/upload-artifact@v4
60 | if: failure()
61 | with:
62 | name: cypress-videos
63 | path: cypress/videos
64 | cypress_meilisearch-no-api-key:
65 | runs-on: ubuntu-latest
66 | container:
67 | image: cypress/browsers:latest
68 | options: --user 1001
69 | services:
70 | meilisearch:
71 | image: getmeili/meilisearch:latest
72 | env:
73 | MEILI_NO_ANALYTICS: 'true'
74 | ports:
75 | - '7700:7700'
76 | steps:
77 | - uses: actions/checkout@v4
78 | - name: Setup node and cache
79 | uses: actions/setup-node@v4
80 | with:
81 | node-version: 18
82 | cache: "yarn"
83 | cache-dependency-path: yarn.lock
84 | - name: Enable Corepack
85 | run: corepack enable
86 | - name: Install dependencies
87 | run: yarn --frozen-lockfile
88 | - name: Test
89 | uses: cypress-io/github-action@v6
90 | env:
91 | CYPRESS_host: http://meilisearch:7700
92 | with:
93 | start: yarn start:ci
94 | wait-on: 'http://0.0.0.0:3000'
95 | wait-on-timeout: 120
96 | command: yarn cy:run
97 | config-file: cypress.config.js
98 | - uses: actions/upload-artifact@v4
99 | if: failure()
100 | with:
101 | name: cypress-screenshots
102 | path: cypress/screenshots
103 | - uses: actions/upload-artifact@v4
104 | if: failure()
105 | with:
106 | name: cypress-videos
107 | path: cypress/videos
108 | cypress_meilisearch-api-key:
109 | runs-on: ubuntu-latest
110 | container:
111 | image: cypress/browsers:latest
112 | options: --user 1001
113 | services:
114 | meilisearch:
115 | image: getmeili/meilisearch:latest
116 | env:
117 | MEILI_MASTER_KEY: 'masterKey'
118 | MEILI_NO_ANALYTICS: 'true'
119 | ports:
120 | - '7700:7700'
121 | steps:
122 | - uses: actions/checkout@v4
123 | - name: Setup node and cache
124 | uses: actions/setup-node@v4
125 | with:
126 | node-version: 18
127 | cache: "yarn"
128 | cache-dependency-path: yarn.lock
129 | - name: Enable Corepack
130 | run: corepack enable
131 | - name: Install dependencies
132 | run: yarn --frozen-lockfile
133 | - name: Test
134 | uses: cypress-io/github-action@v6
135 | env:
136 | CYPRESS_host: http://meilisearch:7700
137 | with:
138 | start: yarn start:ci
139 | wait-on: 'http://0.0.0.0:3000'
140 | command: yarn cy:run:test-api-key-required
141 | config-file: cypress.config.js
142 | - uses: actions/upload-artifact@v4
143 | if: failure()
144 | with:
145 | name: cypress-screenshots
146 | path: cypress/screenshots
147 | - uses: actions/upload-artifact@v4
148 | if: failure()
149 | with:
150 | name: cypress-videos
151 | path: cypress/videos
152 |
--------------------------------------------------------------------------------
/.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 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/sdks
12 | !.yarn/versions
13 |
14 | # testing
15 | /coverage
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | .env.local
23 | .env.development.local
24 | .env.test.local
25 | .env.production.local
26 |
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | # meilisearch
32 | /data.ms
33 | meilisearch
34 |
35 | # Cypress
36 | cypress/screenshots
37 | cypress/videos
38 | cypress/plugins
39 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.20.5
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 | data.ms/
3 | assets/
4 | public/
5 | node_modules/
6 | .github/
7 | .storybook/
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
3 |
4 | addons: [
5 | "@storybook/addon-links",
6 | "@storybook/addon-essentials",
7 | "@storybook/preset-create-react-app"
8 | ],
9 |
10 | staticDirs: ['../public'],
11 |
12 | framework: {
13 | name: "@storybook/react-webpack5",
14 | options: {}
15 | },
16 |
17 | docs: {
18 | autodocs: true
19 | }
20 | }
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import theme from 'theme'
3 | import GlobalStyle from 'GlobalStyle'
4 |
5 | import { ThemeProvider } from 'styled-components'
6 |
7 | export const decorators = [
8 | (Story) => (
9 |
10 |
11 |
12 |
13 | ),
14 | ]
15 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | yarnPath: .yarn/releases/yarn-4.6.0.cjs
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-alpine
2 |
3 | WORKDIR /home/node/app
4 |
5 | RUN chown -R node:node /home/node/app
6 |
7 | USER node
8 |
9 | COPY package*.json ./
10 |
11 | COPY --chown=node:node . .
12 | RUN yarn install
13 |
14 | CMD ["yarn", "start"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-2025 Meili SAS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mini Dashboard
6 |
7 | ---
8 |
9 | 🚨 IMPORTANT NOTICE: Reduced Maintenance & Support 🚨
10 |
11 | *Dear Community,*
12 |
13 | *We'd like to share some updates regarding the future maintenance of this repository:*
14 |
15 | *Our team is small, and our availability will be reduced in the upcoming times. As such, response times might be slower, and we will not be accepting enhancements for this repository moving forward.*
16 |
17 | *If you're looking for reliable alternatives, consider using [Meilisearch Cloud](https://meilisearch.com/cloud?utm_campaign=oss&utm_source=github&utm_medium=minidashboard). For instance, it offers a convenient solution for managing your index settings.*
18 |
19 | *Seeking immediate support? Please join us on [our Discord server](https://discord.meilisearch.com).*
20 |
21 | ---
22 |
23 |
30 |
31 | > Meilisearch is an open-source search engine that offers fast, relevant search out of the box.
32 |
33 | 👉 [Meilisearch repository](https://github.com/meilisearch/meilisearch)
34 |
35 | Meilisearch's mini-dashboard. A web-app served by the engine with a minimal search experience on your data.
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | **Table of Contents**:
44 |
45 | - [Setup](#setup)
46 | - [Run](#run)
47 | - [Build](#build)
48 | - [Generate build](#generate-build)
49 | - [Specify Meilisearch's server URL](#specify-meilisearchs-server-url)
50 | - [Run your build](#run-your-build)
51 | - [Storybook](#storybook)
52 | - [Contributing](#contributing)
53 |
54 |
55 |
56 | This repository uses [Yarn 4.x](https://yarnpkg.com/) to manage dependencies and [NVM](https://github.com/nvm-sh/nvm) to manage Node version. See [current version](.nvmrc).
57 |
58 | ## Setup
59 |
60 | ```bash
61 | yarn
62 | ```
63 |
64 | ## Run
65 |
66 | ```bash
67 | yarn start
68 | ```
69 |
70 | Go to `http://localhost:3000/` and enjoy ! 🎉
71 |
72 | ## Build
73 |
74 | ### Generate build
75 |
76 | You can generate a build of this project with the following command:
77 |
78 | ```bash
79 | yarn build
80 | ```
81 |
82 | ### Specify Meilisearch's server URL
83 |
84 | ⚠️ By default, the application will call Meilisearch at the exact same address as it is running.
85 | Example: if your app is running at `http://localhost:5000`, it will try to call `http://localhost:5000/indexes` to retrieve the list of your indexes.
86 |
87 | If you want to specify the URL where your Meilisearch is running, use the `REACT_APP_MEILI_SERVER_ADDRESS` environment variable.
88 |
89 | Example:
90 |
91 | ```bash
92 | REACT_APP_MEILI_SERVER_ADDRESS=http://0.0.0.0:7700 yarn build
93 | ```
94 |
95 | ### Run your build
96 |
97 | The above commands will generate an optimized version of the app, inside the `build` folder.
98 |
99 | You can then serve it with any web server of your choice.
100 |
101 | Example:
102 |
103 | ```bash
104 | serve build
105 | ```
106 |
107 | ## Storybook
108 |
109 | Storybook is a development environment for UI components. It allows you to browse a component library, view the different states of each component, and interactively test components.
110 |
111 | 
112 |
113 | ```bash
114 | yarn storybook
115 | ```
116 |
117 | ## Docker
118 |
119 | You can also run the mini-dashboard with Docker.
120 |
121 | ```bash
122 | docker build -t meilisearch-mini-dashboard .
123 | docker run -it -e REACT_APP_MEILI_SERVER_ADDRESS=http://localhost:7700 -p 3000:3000 meilisearch-mini-dashboard
124 | ```
125 |
126 | ## Contributing
127 |
128 | If you want to contribute to this project, please make sure to read [the contributing guidelines](./CONTRIBUTING.md)
129 |
130 | ## Compatibility with Meilisearch
131 |
132 | This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/mini-dashboard/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info.
133 |
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/assets/storybook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meilisearch/mini-dashboard/986ec2fd237d6854a6c58e81c8ea024792939433/assets/storybook.png
--------------------------------------------------------------------------------
/assets/trumen_quick_loop.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meilisearch/mini-dashboard/986ec2fd237d6854a6c58e81c8ea024792939433/assets/trumen_quick_loop.gif
--------------------------------------------------------------------------------
/bors.toml:
--------------------------------------------------------------------------------
1 |
2 | status = [
3 | 'linter-check',
4 | 'cypress_no_meilisearch',
5 | 'cypress_meilisearch-no-api-key',
6 | 'cypress_meilisearch-api-key',
7 | ]
8 | # 1 hour timeout
9 | timeout-sec = 3600
10 |
--------------------------------------------------------------------------------
/cypress.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-extraneous-dependencies
2 | const { defineConfig } = require('cypress')
3 |
4 | module.exports = defineConfig({
5 | watchForFileChanges: true,
6 | retries: 2,
7 | viewportWidth: 1440,
8 | viewportHeight: 900,
9 | env: {
10 | host: 'http://0.0.0.0:7700',
11 | apiKey: 'masterKey',
12 | wrongApiKey: 'wrongApiKey',
13 | waitingTime: 1000,
14 | },
15 | e2e: {
16 | baseUrl: 'http://localhost:3000',
17 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
18 | },
19 | })
20 |
--------------------------------------------------------------------------------
/cypress/e2e/side-panel.cy.js:
--------------------------------------------------------------------------------
1 | const WAITING_TIME = Cypress.env('waitingTime')
2 |
3 | describe(`Right side panel`, () => {
4 | beforeEach(() => {
5 | cy.saveApiTokenCookie()
6 | cy.visit('/')
7 | })
8 |
9 | it('Should be opened by default', () => {
10 | cy.get('[data-testid="right-panel"]').should('be.visible')
11 | })
12 |
13 | it('Should be closed when clicking on the close button', () => {
14 | cy.get('button[aria-label="Close Panel"]').click()
15 | cy.get('[data-testid="right-panel"]').should('not.be.visible')
16 | })
17 |
18 | it('Should be opened when clicking on the menu bars button', () => {
19 | cy.get('button[aria-label="Close Panel"]').click()
20 | cy.get('button[aria-label="Open Panel"]').click()
21 | cy.get('[data-testid="right-panel"]').should('be.visible')
22 | })
23 |
24 | it('Should allow subscribing to the newsletter', () => {
25 | cy.get('input[placeholder="Enter your email"]').type('kero@meilisearch.com')
26 | cy.get('button[aria-label="Subscribe"]').click()
27 | cy.wait(WAITING_TIME)
28 | cy.get('[data-testid="right-panel"]').within(() => {
29 | cy.get('p').should('contain', 'Thanks for subscribing!')
30 | })
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/cypress/e2e/test-api-key-query-param.cy.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable cypress/no-unnecessary-waiting */
2 | const API_KEY = Cypress.env('apiKey')
3 | const WAITING_TIME = Cypress.env('waitingTime')
4 |
5 | describe(`Test API key required with query params`, () => {
6 | before(() => {
7 | cy.deleteAllIndexes()
8 |
9 | cy.wait(WAITING_TIME)
10 | cy.createIndex('movies')
11 | cy.wait(WAITING_TIME)
12 | cy.fixture('movies.json').then((movies) => {
13 | cy.addDocuments('movies', movies)
14 | cy.wait(WAITING_TIME)
15 | })
16 | })
17 |
18 | beforeEach(() => {
19 | cy.visit(`/?api_key=${API_KEY}`)
20 | })
21 |
22 | it('Should display the movies', () => {
23 | cy.wait(WAITING_TIME)
24 | cy.get('ul')
25 | .children()
26 | .should(($p) => {
27 | expect($p).to.have.length(20)
28 | })
29 | })
30 |
31 | it('Should have the api key written in the modal', () => {
32 | // Test if the query parameter is written in the modal
33 | // meaning it is added in the local storage
34 | cy.get('span').contains('Api Key').parent().click()
35 | cy.get('div[aria-label=settings-api-key]').within(() => {
36 | cy.get('input[name="apiKey"]').should('have.value', API_KEY)
37 | })
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/cypress/e2e/test-api-key-required.cy.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable cypress/no-unnecessary-waiting */
2 | const API_KEY = Cypress.env('apiKey')
3 | const WRONG_APIKEY = Cypress.env('wrongApiKey')
4 | const WAITING_TIME = Cypress.env('waitingTime')
5 |
6 | describe(`Test API key required`, () => {
7 | before(() => {
8 | cy.deleteAllIndexes()
9 | })
10 |
11 | beforeEach(() => {
12 | cy.visit('/')
13 | })
14 |
15 | it('Should visit the dashboard', () => {
16 | cy.url().should('match', /\//)
17 | })
18 |
19 | it('Should find a text in modal requesting API key', () => {
20 | cy.contains('Enter your admin API key')
21 | })
22 |
23 | it('Should fail on wrong API key triggered with mouse click', () => {
24 | cy.get('div[aria-label=ask-for-api-key]').within(() => {
25 | cy.get('input[name="apiKey"]').as('apiKeyInput')
26 | cy.get('@apiKeyInput').type(WRONG_APIKEY)
27 | cy.get('@apiKeyInput').should('have.value', WRONG_APIKEY)
28 | cy.get('button').contains('Go').click()
29 | cy.wait(WAITING_TIME)
30 | cy.contains('The provided API key is invalid.')
31 | })
32 | })
33 |
34 | it('Should fail on wrong API key triggered with enter key', () => {
35 | cy.get('div[aria-label=ask-for-api-key]').within(() => {
36 | cy.get('input[name="apiKey"]').as('apiKeyInput')
37 | cy.get('@apiKeyInput').type(WRONG_APIKEY)
38 | cy.get('@apiKeyInput').should('have.value', WRONG_APIKEY)
39 | cy.get('@apiKeyInput').type('{enter}')
40 | cy.wait(WAITING_TIME)
41 | cy.contains('The provided API key is invalid.')
42 | })
43 | })
44 |
45 | it('Should accept valid API key', () => {
46 | cy.get('div[aria-label=ask-for-api-key]').within(() => {
47 | cy.get('input[name="apiKey"]').as('apiKeyInput')
48 | cy.get('@apiKeyInput').clear()
49 | cy.get('@apiKeyInput').type(API_KEY)
50 | cy.get('@apiKeyInput').should('have.value', API_KEY)
51 | cy.get('button').contains('Go').click()
52 | cy.wait(WAITING_TIME)
53 | })
54 | cy.contains('Welcome to')
55 | })
56 |
57 | it('Should display a modal with API key inside the API key modal button', () => {
58 | // Fill the first API key request
59 | cy.get('div[aria-label=ask-for-api-key]').within(() => {
60 | cy.get('input[name="apiKey"]').as('apiKeyInput')
61 | cy.get('@apiKeyInput').clear()
62 | cy.get('@apiKeyInput').type(API_KEY)
63 | cy.get('button').contains('Go').click()
64 | })
65 | cy.visit('/')
66 | cy.wait(WAITING_TIME)
67 |
68 | // Test the value of the Api Key Modal
69 | cy.get('span').contains('Api Key').parent().click()
70 | cy.get('div[aria-label=settings-api-key]').within(() => {
71 | cy.get('input[name="apiKey"]').should('have.value', API_KEY)
72 | })
73 | })
74 |
75 | it('Should fail on API Key change inside the API key modal button', () => {
76 | // Fill the first API key request
77 | cy.get('div[aria-label=ask-for-api-key]').within(() => {
78 | cy.get('input[name="apiKey"]').as('apiKeyInput')
79 | cy.get('@apiKeyInput').clear()
80 | cy.get('@apiKeyInput').type(API_KEY)
81 | cy.get('button').contains('Go').click()
82 | })
83 | cy.visit('/')
84 | cy.wait(WAITING_TIME)
85 |
86 | // Test the change of API key inside the API Key Modal
87 | cy.get('span').contains('Api Key').parent().click()
88 | cy.get('div[aria-label=settings-api-key]').within(() => {
89 | cy.get('input[name="apiKey"]').as('apiKeyInput')
90 | cy.get('@apiKeyInput').clear()
91 | cy.get('@apiKeyInput').type(WRONG_APIKEY)
92 | cy.get('@apiKeyInput').should('have.value', WRONG_APIKEY)
93 | cy.get('button').contains('Go').click()
94 | cy.wait(WAITING_TIME)
95 | cy.contains('The provided API key is invalid.')
96 | })
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/cypress/e2e/test-interface.cy.js:
--------------------------------------------------------------------------------
1 | const WAITING_TIME = Cypress.env('waitingTime')
2 |
3 | describe(`Test interface`, () => {
4 | before(() => {
5 | // Recreate the movies index with documents in it
6 | cy.deleteAllIndexes()
7 | cy.wait(WAITING_TIME)
8 | cy.createIndex('movies')
9 | cy.wait(WAITING_TIME)
10 | cy.fixture('movies.json').then((movies) => {
11 | cy.addDocuments('movies', movies)
12 | cy.wait(WAITING_TIME)
13 | })
14 | cy.createIndex('pokemon')
15 | cy.wait(WAITING_TIME)
16 | cy.fixture('pokemon.json').then((pokemon) => {
17 | cy.addDocuments('pokemon', pokemon)
18 | cy.wait(WAITING_TIME)
19 | })
20 | })
21 |
22 | beforeEach(() => {
23 | cy.visit('/')
24 | })
25 |
26 | it('Should contain a "Show more" button if a document has more than 6 fields', () => {
27 | cy.get('ul')
28 | .children()
29 | .first()
30 | .within(() => {
31 | cy.get('button').contains('Show more')
32 | })
33 | })
34 |
35 | it('Shouldn’t contain a "Show more" button if a document has less than 6 fields', () => {
36 | cy.get('button[aria-haspopup=menu]').click()
37 | cy.wait(WAITING_TIME)
38 | cy.get('button[role=menuitem]').contains('pokemon').click()
39 | cy.get('ul')
40 | .children()
41 | .first()
42 | .within(() => {
43 | cy.get('button').contains('Show more').should('not.exist')
44 | })
45 | })
46 |
47 | it('Should display more fields if the user clicks on the "Show more" button', () => {
48 | cy.get('button[aria-haspopup=menu]').click()
49 | cy.wait(WAITING_TIME)
50 | cy.get('button[role=menuitem]').contains('movies').click()
51 | cy.get('ul')
52 | .children()
53 | .first()
54 | .within(() => {
55 | cy.get('button').contains('Show more').click()
56 | cy.get('>div > div').should('have.length', 8)
57 | })
58 | })
59 | it('Should display a json button', () => {
60 | cy.get('ul')
61 | .children()
62 | .first()
63 | .within(() => {
64 | cy.get('button').contains('Show more').click()
65 | cy.get('button').contains('json')
66 | })
67 | })
68 |
69 | it('Should display json on click on the "json" button', () => {
70 | cy.get('ul')
71 | .children()
72 | .first()
73 | .within(() => {
74 | cy.get('button').contains('Show more').click()
75 | cy.get('button').contains('json').click()
76 | cy.get('div').should('have.class', 'react-json-view')
77 | cy.get('span').contains('Apple iTunes')
78 | })
79 | })
80 |
81 | it('Should display an array button', () => {
82 | cy.get('ul')
83 | .children()
84 | .first()
85 | .within(() => {
86 | cy.get('button').contains('Show more').click()
87 | cy.get('button').contains('array')
88 | })
89 | })
90 |
91 | it('Should display an array on click on the "array" button', () => {
92 | cy.get('ul')
93 | .children()
94 | .first()
95 | .within(() => {
96 | cy.get('button').contains('Show more').click()
97 | cy.get('button').contains('array').click()
98 | cy.get('div').should('have.class', 'react-json-view')
99 | cy.get('span').contains('Action')
100 | })
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/cypress/e2e/test-no-api-key-required.cy.js:
--------------------------------------------------------------------------------
1 | const WAITING_TIME = Cypress.env('waitingTime')
2 |
3 | describe(`Test no API key required`, () => {
4 | before(() => {
5 | cy.deleteAllIndexes()
6 | cy.wait(WAITING_TIME)
7 | })
8 |
9 | beforeEach(() => {
10 | cy.visit('/')
11 | })
12 |
13 | it('Should visit the dashboard', () => {
14 | cy.url().should('match', /\//)
15 | })
16 |
17 | it('Should display the help cards view', () => {
18 | cy.contains(
19 | 'This dashboard will help you check the search results with ease.'
20 | )
21 | cy.contains('Set your API key (optional)')
22 | cy.contains('Select an index')
23 | })
24 |
25 | it('Should display a message telling that no api key is required', () => {
26 | cy.get('span').contains('Api Key').parent().click()
27 | cy.get('div[aria-label=settings-api-key]').within(() => {
28 | cy.contains('Enter your admin API key (optional)')
29 | cy.contains(
30 | 'You haven’t set an API key yet, if you want to set one you can read the documentation'
31 | )
32 | })
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/cypress/e2e/test-no-meilisearch.cy.js:
--------------------------------------------------------------------------------
1 | describe(`Test without Meilisearch running`, () => {
2 | beforeEach(() => {
3 | cy.visit('/')
4 | })
5 |
6 | it('Should visit the dashboard', () => {
7 | cy.url().should('match', /\//)
8 | })
9 |
10 | it('Should invite to start Meilisearch', () => {
11 | cy.contains('It seems like Meilisearch isn’t running')
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/cypress/e2e/test-search.cy.js:
--------------------------------------------------------------------------------
1 | const WAITING_TIME = Cypress.env('waitingTime')
2 |
3 | describe(`Test search`, () => {
4 | before(() => {
5 | // Recreate the movies index with documents in it
6 | cy.deleteAllIndexes()
7 | cy.wait(WAITING_TIME)
8 | cy.createIndex('movies')
9 | cy.wait(WAITING_TIME)
10 | cy.fixture('movies.json').then((movies) => {
11 | cy.addDocuments('movies', movies)
12 | cy.wait(WAITING_TIME)
13 | })
14 | })
15 |
16 | beforeEach(() => {
17 | cy.visit('/')
18 | })
19 |
20 | it('Should update the results according to the user’s search', () => {
21 | cy.get('input[type="search"]').type('Fifth Element')
22 | cy.get('ul')
23 | .children()
24 | .should(($p) => {
25 | expect($p).to.have.length(1)
26 | })
27 | })
28 |
29 | it('Should display a message if there are no result for the user’s search', () => {
30 | cy.get('input[type="search"]').type('zz')
31 | cy.contains('Sorry mate, no results matching your request')
32 | })
33 |
34 | it('Should display a "Load more" button if there are more than 20 results', () => {
35 | cy.get('input[type="search"]')
36 | cy.get('ul')
37 | .children()
38 | .should(($p) => {
39 | expect($p).to.have.length(20)
40 | })
41 | cy.contains('Load more')
42 | })
43 |
44 | it('Should load the next documents on click on the "Load more" button', () => {
45 | cy.get('span').contains('Load more').parent().click()
46 | cy.get('ul')
47 | .children()
48 | .should(($p) => {
49 | expect($p).to.have.length(33)
50 | })
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/cypress/e2e/test-select-indexes.cy.js:
--------------------------------------------------------------------------------
1 | const WAITING_TIME = Cypress.env('waitingTime')
2 |
3 | // TODO: refacto tests to get rid of the WAITING_TIME
4 |
5 | describe(`Test indexes`, () => {
6 | before(() => {
7 | cy.deleteAllIndexes()
8 | cy.wait(WAITING_TIME)
9 |
10 | cy.createIndex('empty_index')
11 | cy.wait(WAITING_TIME)
12 | cy.createIndex('movies')
13 | cy.wait(WAITING_TIME)
14 | cy.createIndex('pokemon')
15 | cy.wait(WAITING_TIME)
16 |
17 | cy.fixture('movies.json')
18 | .as('movies')
19 | .then((movies) => {
20 | cy.addDocuments('movies', movies)
21 | cy.wait(WAITING_TIME)
22 | })
23 | cy.wait(WAITING_TIME)
24 |
25 | cy.fixture('pokemon.json')
26 | .as('pokemon')
27 | .then((pokemon) => {
28 | cy.addDocuments('pokemon', pokemon)
29 | cy.wait(WAITING_TIME)
30 | })
31 | cy.wait(WAITING_TIME)
32 | })
33 |
34 | after(() => {
35 | cy.deleteAllIndexes()
36 | cy.wait(WAITING_TIME)
37 | })
38 |
39 | beforeEach(() => {
40 | cy.visit('/')
41 | })
42 |
43 | it('Should display the first index based on localeCompare order on the uid', () => {
44 | cy.get('button[aria-haspopup=menu]').contains('empty_index 0')
45 | })
46 |
47 | it('Should list all the indexes inside the select', () => {
48 | cy.get('button[aria-haspopup=menu]').click()
49 | cy.wait(WAITING_TIME)
50 | cy.get('div[role=menu]')
51 | .children()
52 | .should(($p) => {
53 | expect($p).to.have.length(3)
54 | expect($p).to.contain('empty_index 0')
55 | expect($p).to.contain('movies 33')
56 | expect($p).to.contain('pokemon 3')
57 | })
58 | })
59 |
60 | it('Should inform that the current index is empty', () => {
61 | cy.contains('There’s no document in the selected index')
62 | })
63 |
64 | it('Should display an indexes documents', () => {
65 | cy.get('button[aria-haspopup=menu]').click()
66 | cy.wait(WAITING_TIME)
67 | cy.get('button[role=menuitem]').contains('movies').click()
68 | cy.get('ul')
69 | .children()
70 | .should(($p) => {
71 | expect($p).to.have.length(20)
72 | })
73 | })
74 |
75 | it('Should display the documents of an other index on click on it', () => {
76 | cy.get('button[aria-haspopup=menu]').click()
77 | cy.wait(WAITING_TIME)
78 | cy.get('button[role=menuitem]').contains('pokemon').click()
79 | cy.get('ul').children().should('have.length', 3)
80 | cy.get('ul')
81 | .children()
82 | .first()
83 | .within(() => {
84 | cy.contains('Bulbasaur')
85 | })
86 | })
87 | })
88 |
--------------------------------------------------------------------------------
/cypress/fixtures/pokemon.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "Bulbasaur",
5 | "type": ["Grass", "Poison"],
6 | "sprite": "https://raw.githubusercontent.com/Purukitto/pokemon-data.json/master/images/pokedex/sprites/001.png"
7 | },
8 | {
9 | "id": 2,
10 | "name": "Ivysaur",
11 | "type": ["Grass", "Poison"],
12 | "sprite": "https://raw.githubusercontent.com/Purukitto/pokemon-data.json/master/images/pokedex/sprites/002.png"
13 | },
14 | {
15 | "id": 3,
16 | "name": "Venusaur",
17 | "type": ["Grass", "Poison"],
18 | "sprite": "https://raw.githubusercontent.com/Purukitto/pokemon-data.json/master/images/pokedex/sprites/003.png"
19 | }
20 | ]
21 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const { MeiliSearch: Meilisearch } = require('meilisearch')
3 |
4 | const { apiKey, host } = Cypress.env()
5 |
6 | Cypress.Commands.add('deleteAllIndexes', async () => {
7 | try {
8 | const client = new Meilisearch({
9 | host,
10 | apiKey,
11 | })
12 | const { results: indexes } = await client.getIndexes()
13 | indexes.forEach(async (index) => {
14 | const task = await client.deleteIndex(index.uid)
15 | await client.waitForTask(task.taskUid)
16 | })
17 | } catch (e) {
18 | console.log({ e })
19 | }
20 | })
21 |
22 | Cypress.Commands.add('createIndex', async (uid) => {
23 | try {
24 | const client = new Meilisearch({
25 | host,
26 | apiKey,
27 | })
28 | const task = await client.createIndex(uid)
29 | await client.waitForTask(task.taskUid)
30 | } catch (e) {
31 | console.log({ e })
32 | }
33 | })
34 |
35 | Cypress.Commands.add('addDocuments', async (uid, documents) => {
36 | try {
37 | const client = new Meilisearch({
38 | host,
39 | apiKey,
40 | })
41 | const index = await client.getIndex(uid)
42 | const task = await index.addDocuments(documents)
43 | await client.waitForTask(task.taskUid)
44 | } catch (e) {
45 | console.log({ e })
46 | }
47 | })
48 |
49 | Cypress.Commands.add('saveApiTokenCookie', () => {
50 | cy.window().then((win) => {
51 | win.localStorage.setItem('apiKey', JSON.stringify(apiKey))
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/cypress/support/e2e.js:
--------------------------------------------------------------------------------
1 | import './commands'
2 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "jsx": "react"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-dashboard",
3 | "version": "0.2.19",
4 | "private": true,
5 | "dependencies": {
6 | "@meilisearch/instant-meilisearch": "0.25",
7 | "@styled-system/should-forward-prop": "^5.1.5",
8 | "babel-loader": "9.1.3",
9 | "color": "^5.0.0",
10 | "eslint-plugin-import": "^2.29.1",
11 | "meilisearch": "0.48.2",
12 | "prop-types": "^15.8.1",
13 | "react": "^18.3.1",
14 | "react-dom": "^18.3.1",
15 | "react-instantsearch-dom": "^6.40.4",
16 | "react-json-view": "^1.21.3",
17 | "react-lazy-load-image-component": "^1.6.3",
18 | "react-scripts": "5.0.1",
19 | "reakit": "^1.3.11",
20 | "styled-components": "^5.3.9",
21 | "styled-system": "^5.1.5"
22 | },
23 | "scripts": {
24 | "version-script": "node version-script.js",
25 | "validate-env": "node validate-env.js",
26 | "prestart": "yarn version-script && yarn validate-env",
27 | "prebuild": "yarn version-script && yarn validate-env",
28 | "start": "react-scripts start",
29 | "start:ci": "REACT_APP_MEILI_SERVER_ADDRESS=http://meilisearch:7700 react-scripts start",
30 | "build": "react-scripts build",
31 | "eject": "react-scripts eject",
32 | "lint": "eslint .",
33 | "prettier-check": "prettier --check ./src",
34 | "format:fix": "prettier --write ./src",
35 | "storybook": "storybook dev -p 6006",
36 | "build-storybook": "storybook build",
37 | "cy:open": "cypress open",
38 | "cy:run:test-no-meilisearch": "cypress run --spec '**/*/test-no-meilisearch.cy.js'",
39 | "cy:run:test-api-key-required": "cypress run --spec '**/*/test-api-key-required.cy.js,**/*/test-api-key-query-param.cy.js'",
40 | "cy:run": "cypress run --config excludeSpecPattern=['**/*/test-no-meilisearch.cy.js','**/*/test-api-key-required.cy.js','**/*/test-api-key-query-param.cy.js']",
41 | "icons": "npx @svgr/cli --title-prop --no-dimensions --replace-attr-values \"#39486E=currentColor,#959DB3=currentColor\" -d src/components/icons src/components/icons/svg"
42 | },
43 | "browserslist": [
44 | "defaults"
45 | ],
46 | "devDependencies": {
47 | "@storybook/addon-actions": "^8.4.7",
48 | "@storybook/addon-essentials": "^8.4.7",
49 | "@storybook/addon-links": "^8.4.7",
50 | "@storybook/node-logger": "^8.4.7",
51 | "@storybook/preset-create-react-app": "^8.6.12",
52 | "@storybook/react": "^8.6.12",
53 | "@storybook/react-webpack5": "^8.4.7",
54 | "@svgr/cli": "^8.1.0",
55 | "cypress": "^14.1.0",
56 | "eslint": "8.57.0",
57 | "eslint-config-airbnb": "^19.0.4",
58 | "eslint-config-prettier": "^10.0.2",
59 | "eslint-plugin-cypress": "^4.2.0",
60 | "eslint-plugin-jsx-a11y": "^6.7.0",
61 | "eslint-plugin-prettier": "^5.1.3",
62 | "eslint-plugin-react": "^7.37.0",
63 | "prettier": "^3.3.3",
64 | "storybook": "^8.6.11"
65 | },
66 | "packageManager": "yarn@4.6.0",
67 | "resolutions": {
68 | "css-select/nth-check": "^2.0.1",
69 | "css-minimizer-webpack-plugin/postcss": "^8.4.31",
70 | "react-scripts/postcss": "^8.4.31",
71 | "resolve-url-loader/postcss": "^8.4.31",
72 | "@storybook/core/esbuild": "^0.25.0"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meilisearch/mini-dashboard/986ec2fd237d6854a6c58e81c8ea024792939433/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/fonts/Barlow/regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meilisearch/mini-dashboard/986ec2fd237d6854a6c58e81c8ea024792939433/public/fonts/Barlow/regular.woff2
--------------------------------------------------------------------------------
/public/fonts/Work_Sans/bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meilisearch/mini-dashboard/986ec2fd237d6854a6c58e81c8ea024792939433/public/fonts/Work_Sans/bold.woff2
--------------------------------------------------------------------------------
/public/fonts/Work_Sans/light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meilisearch/mini-dashboard/986ec2fd237d6854a6c58e81c8ea024792939433/public/fonts/Work_Sans/light.woff2
--------------------------------------------------------------------------------
/public/fonts/Work_Sans/medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meilisearch/mini-dashboard/986ec2fd237d6854a6c58e81c8ea024792939433/public/fonts/Work_Sans/medium.woff2
--------------------------------------------------------------------------------
/public/fonts/Work_Sans/regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meilisearch/mini-dashboard/986ec2fd237d6854a6c58e81c8ea024792939433/public/fonts/Work_Sans/regular.woff2
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
26 | Mini-dashboard | Meilisearch
27 |
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon-32x32.png",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-no-constructed-context-values */
2 | /* eslint-disable no-console */
3 | import React, { useState, useEffect, useCallback } from 'react'
4 | import styled from 'styled-components'
5 | import { instantMeiliSearch as instantMeilisearch } from '@meilisearch/instant-meilisearch'
6 | import { useDialogState } from 'reakit/Dialog'
7 | import { MeiliSearch as Meilisearch } from 'meilisearch'
8 |
9 | import ApiKeyContext from 'context/ApiKeyContext'
10 | import { useMeilisearchClientContext } from 'context/MeilisearchClientContext'
11 | import useLocalStorage from 'hooks/useLocalStorage'
12 | import ApiKeyModalContent from 'components/ApiKeyModalContent'
13 | import Body from 'components/Body'
14 | import Modal from 'components/Modal'
15 | import NoMeilisearchRunning from 'components/NoMeilisearchRunning'
16 | import ApiKeyAwarenessBanner from 'components/ApiKeyAwarenessBanner'
17 | import getIndexesListWithStats from 'utils/getIndexesListWithStats'
18 | import shouldDisplayApiKeyModal from 'utils/shouldDisplayApiKeyModal'
19 | import hasAnApiKeySet from 'utils/hasAnApiKeySet'
20 | import clientAgents from './version/client-agents'
21 |
22 | export const baseUrl =
23 | process.env.REACT_APP_MEILI_SERVER_ADDRESS ||
24 | (process.env.NODE_ENV === 'development'
25 | ? 'http://0.0.0.0:7700'
26 | : window.location.origin)
27 |
28 | const Wrapper = styled.div`
29 | background-color: ${(p) => p.theme.colors.gray[11]};
30 | min-height: 100vh;
31 | `
32 |
33 | const App = () => {
34 | const [apiKey, setApiKey] = useLocalStorage('apiKey')
35 | const [indexes, setIndexes] = useState()
36 | const [isMeilisearchRunning, setIsMeilisearchRunning] = useState(false)
37 | const [requireApiKeyToWork, setRequireApiKeyToWork] = useState(false)
38 | const [currentIndex, setCurrentIndex] = useLocalStorage('currentIndex')
39 | const [isApiKeyBannerVisible, setIsApiKeyBannerVisible] = useState(false)
40 | const dialog = useDialogState({ animated: true, visible: false })
41 |
42 | const {
43 | meilisearchJsClient,
44 | setMeilisearchJsClient,
45 | setInstantMeilisearchClient,
46 | } = useMeilisearchClientContext()
47 |
48 | const getIndexesList = useCallback(async () => {
49 | try {
50 | const indexesList = await getIndexesListWithStats(meilisearchJsClient)
51 | setIndexes(indexesList)
52 | if (indexesList && indexesList?.length > 0) {
53 | setCurrentIndex(
54 | currentIndex
55 | ? indexesList.find((option) => option.uid === currentIndex.uid)
56 | : indexesList[0]
57 | )
58 | } else {
59 | setCurrentIndex(null)
60 | }
61 | } catch (error) {
62 | setCurrentIndex(null)
63 | console.log(error)
64 | }
65 | }, [meilisearchJsClient, currentIndex])
66 |
67 | // Check if the API key is present on the url then put it in the local storage
68 | const getApiKeyFromUrl = useCallback(() => {
69 | const urlParams = new URLSearchParams(window.location.search)
70 | const apiKeyParam = urlParams.get('api_key')
71 | if (apiKeyParam) {
72 | setApiKey(apiKeyParam)
73 | setIsApiKeyBannerVisible(true)
74 | }
75 | }, [])
76 |
77 | useEffect(() => {
78 | getApiKeyFromUrl()
79 | }, [])
80 |
81 | useEffect(() => {
82 | setInstantMeilisearchClient(
83 | instantMeilisearch(baseUrl, apiKey, {
84 | primaryKey: 'id',
85 | clientAgents,
86 | }).searchClient
87 | )
88 |
89 | setMeilisearchJsClient(
90 | new Meilisearch({
91 | host: baseUrl,
92 | apiKey,
93 | clientAgents,
94 | })
95 | )
96 | }, [apiKey])
97 |
98 | useEffect(() => {
99 | const onClientUpdate = async () => {
100 | const isInstanceRunning = await meilisearchJsClient.isHealthy()
101 | setIsMeilisearchRunning(isInstanceRunning)
102 | if (isInstanceRunning) {
103 | setRequireApiKeyToWork(await hasAnApiKeySet())
104 | dialog.setVisible(await shouldDisplayApiKeyModal(meilisearchJsClient))
105 | getIndexesList()
106 | }
107 | }
108 | onClientUpdate()
109 | }, [meilisearchJsClient])
110 |
111 | return (
112 |
113 |
114 | {isApiKeyBannerVisible && (
115 | setIsApiKeyBannerVisible(false)}
117 | />
118 | )}
119 | {isMeilisearchRunning ? (
120 |
128 | ) : (
129 |
130 | )}
131 |
138 | dialog.hide()} />
139 |
140 |
141 |
142 | )
143 | }
144 |
145 | export default App
146 |
--------------------------------------------------------------------------------
/src/GlobalStyle.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components'
2 |
3 | const GlobalStyle = createGlobalStyle`
4 | html, body {
5 | margin: 0;
6 | }
7 | * {
8 | font-family: 'Work Sans';
9 | box-sizing: border-box;
10 | }
11 |
12 | mark, ::selection {
13 | background-color: ${(p) => p.theme.colors.main.light};
14 | }
15 |
16 | @font-face {
17 | font-family: 'Work Sans';
18 | src: url("fonts/Work_Sans/light.woff2") format("woff2");
19 | font-weight: 300;
20 | font-style: normal;
21 | }
22 |
23 | @font-face {
24 | font-family: 'Work Sans';
25 | src: url("fonts/Work_Sans/regular.woff2") format("woff2");
26 | font-weight: 400;
27 | font-style: normal;
28 | }
29 |
30 | @font-face {
31 | font-family: 'Work Sans';
32 | src: url("fonts/Work_Sans/medium.woff2") format("woff2");
33 | font-weight: 500;
34 | font-style: normal;
35 | }
36 |
37 | @font-face {
38 | font-family: 'Work Sans';
39 | src: url("fonts/Work_Sans/bold.woff2") format("woff2");
40 | font-weight: 700;
41 | font-style: normal;
42 | }
43 |
44 | @font-face {
45 | font-family: 'Barlow';
46 | src: url("fonts/Barlow/regular.woff2") format("woff2");
47 | font-weight: 400;
48 | font-style: normal;
49 | }
50 |
51 | `
52 |
53 | export default GlobalStyle
54 |
--------------------------------------------------------------------------------
/src/components/ApiKeyAwarenessBanner.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import Color from 'color'
4 | import Typography from 'components/Typography'
5 | import IconButton from 'components/IconButton'
6 | import { AlertCircle, Cross } from 'components/icons'
7 | import Container from './Container'
8 |
9 | const Button = styled(IconButton)`
10 | position: absolute;
11 | right: 16px;
12 | &:hover {
13 | pointer-events: initial;
14 | }
15 | `
16 |
17 | const ApiKeyBannerWrapper = styled.div`
18 | background: #e41359;
19 | display: flex;
20 | position: sticky;
21 | top: 0;
22 | height: 55px;
23 | box-shadow: 0px 0px 30px ${(p) => Color(p.theme.colors.gray[0]).alpha(0.15)};
24 | z-index: 4;
25 | padding: 4px;
26 | overflow: hidden;
27 | `
28 |
29 | const CloudBanner = ({ onClose }) => (
30 |
31 |
32 |
33 |
34 | Please be aware that you are using api_key in the params. Do not share
35 | the url with api_key to any unknown source.
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 |
44 | export default CloudBanner
45 |
--------------------------------------------------------------------------------
/src/components/ApiKeyModalContent.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { MeiliSearch as Meilisearch } from 'meilisearch'
4 |
5 | import { baseUrl } from 'App'
6 | import Box from 'components/Box'
7 | import Button from 'components/Button'
8 | import Input from 'components/Input'
9 | import Link from 'components/Link'
10 | import Typography from 'components/Typography'
11 |
12 | import ApiKeyContext from 'context/ApiKeyContext'
13 |
14 | import clientAgents from '../version/client-agents'
15 |
16 | const ErrorMessage = styled(Typography)`
17 | position: absolute;
18 | left: 0;
19 | top: 32px;
20 | `
21 |
22 | const ApiKeyModalContent = ({ closeModal }) => {
23 | const { apiKey, setApiKey } = React.useContext(ApiKeyContext)
24 | const [value, setValue] = React.useState(apiKey || '')
25 | const [error, setError] = React.useState()
26 |
27 | const updateClient = async () => {
28 | const clientToTry = new Meilisearch({
29 | host: baseUrl,
30 | apiKey: value,
31 | clientAgents,
32 | })
33 | try {
34 | await clientToTry.getIndexes()
35 | setApiKey(value)
36 | closeModal()
37 | setError()
38 | } catch (err) {
39 | setError(err.message)
40 | }
41 | }
42 |
43 | React.useEffect(() => {
44 | setValue(apiKey)
45 | }, [apiKey])
46 |
47 | return (
48 | <>
49 |
50 | setValue(e.target.value)}
56 | onKeyPress={(e) => {
57 | if (e.key === 'Enter') {
58 | updateClient()
59 | }
60 | }}
61 | />
62 | updateClient()}
66 | style={{ minWidth: 'auto', width: 48, marginLeft: 16 }}
67 | >
68 | Go
69 |
70 |
71 |
72 |
73 | An API key that has at least{' '}
74 |
78 | permission
79 | {' '}
80 | to get the indexes, search and get the version of Meilisearch.
81 |
82 | {error && (
83 |
84 | {error}
85 |
86 | )}
87 |
88 | >
89 | )
90 | }
91 |
92 | export default ApiKeyModalContent
93 |
--------------------------------------------------------------------------------
/src/components/Badge.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Badge = styled.span`
4 | background-color: ${(p) => p.theme.colors.main.lighter};
5 | color: ${(p) => p.theme.colors.main.dark};
6 | height: 16px;
7 | border-radius: 5px;
8 | padding: 0 4px;
9 | display: inline-flex;
10 | align-items: center;
11 | justify-content: center;
12 | font-size: 12px;
13 | line-height: 22px;
14 | font-weight: 400;
15 | letter-spacing: 0.03em;
16 | `
17 |
18 | export default Badge
19 |
--------------------------------------------------------------------------------
/src/components/Body.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { InstantSearch } from 'react-instantsearch-dom'
3 | import styled from 'styled-components'
4 | import { useMeilisearchClientContext } from 'context/MeilisearchClientContext'
5 | import useLocalStorage from 'hooks/useLocalStorage'
6 | import Box from 'components/Box'
7 | import Header from 'components/Header/index'
8 | import RightPanel from 'components/RightPanel'
9 | import BodyWrapper from 'components/BodyWrapper'
10 | import EmptyView from 'components/EmptyView'
11 | import OnBoarding from 'components/OnBoarding'
12 | import Results from 'components/Results'
13 | import Typography from 'components/Typography'
14 |
15 | const ContentWrapper = styled.div`
16 | width: ${({ isRightPanelOpen, theme }) =>
17 | isRightPanelOpen ? `calc(100% - ${theme.sizes.rightPanel})` : '100%'};
18 | transition: width 0.3s ease-in-out;
19 | `
20 |
21 | const IndexContent = ({ currentIndex }) => {
22 | if (!currentIndex) return
23 | if (currentIndex?.stats?.numberOfDocuments > 0) return
24 | return (
25 |
26 |
32 | There’s no document in the selected index
33 |
34 |
35 | )
36 | }
37 |
38 | const Body = ({
39 | currentIndex,
40 | indexes,
41 | getIndexesList,
42 | setCurrentIndex,
43 | requireApiKeyToWork,
44 | isApiKeyBannerVisible,
45 | }) => {
46 | const { meilisearchJsClient, instantMeilisearchClient } =
47 | useMeilisearchClientContext()
48 | const [storedIsPanelOpen, setStoredIsPanelOpen] = useLocalStorage(
49 | 'meilisearch-panel-open',
50 | true
51 | )
52 |
53 | // Right-side panel
54 | const [isRightPanelOpen, setIsRightPanelOpen] =
55 | React.useState(storedIsPanelOpen)
56 | const handleTogglePanel = React.useCallback(() => {
57 | setIsRightPanelOpen((isOpen) => !isOpen)
58 | setStoredIsPanelOpen((isOpen) => !isOpen)
59 | }, [])
60 |
61 | return (
62 |
66 |
67 |
78 |
79 | {/* */}
80 |
87 |
88 |
89 |
90 |
91 | {
94 | setIsRightPanelOpen(false)
95 | setStoredIsPanelOpen(false)
96 | }}
97 | />
98 |
99 | )
100 | }
101 |
102 | export default Body
103 |
--------------------------------------------------------------------------------
/src/components/BodyWrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const BodyWrapper = styled.div`
4 | display: flex;
5 | flex: 1;
6 | width: 100%;
7 | min-height: calc(100vh - 120px);
8 | `
9 |
10 | export default BodyWrapper
11 |
--------------------------------------------------------------------------------
/src/components/Box.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { space, layout, color, compose, flexbox, position } from 'styled-system'
3 | import { props as stprops } from '@styled-system/should-forward-prop'
4 |
5 | const regex = new RegExp(`^(${stprops.join('|')})$`)
6 |
7 | const shouldForwardProp = (prop) => !regex.test(prop)
8 |
9 | const Box = styled('div').withConfig({ shouldForwardProp })`
10 | box-sizing: border-box;
11 | min-width: 0;
12 |
13 | && {
14 | ${compose(space, layout, color, flexbox, position)}
15 | }
16 | `
17 | export default Box
18 |
--------------------------------------------------------------------------------
/src/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Color from 'color'
3 | import styled, { css } from 'styled-components'
4 | import PropTypes from 'prop-types'
5 | import { space, color } from 'styled-system'
6 | import { Button as ReakitButton } from 'reakit/Button'
7 |
8 | import Typography from 'components/Typography'
9 | import { ArrowDown } from 'components/icons'
10 |
11 | const Arrow = styled(ArrowDown)`
12 | margin-left: 6px;
13 | `
14 |
15 | const variants = {
16 | default: css`
17 | padding: 0 24px;
18 | min-width: 128px;
19 | background-color: transparent;
20 | border-width: 1px;
21 | border-style: solid;
22 | border-color: ${(p) => p.theme.colors.gray[10]};
23 | box-shadow: 0px 4px 6px ${Color('#000').alpha(0.04)};
24 | color: ${(p) => p.theme.colors.gray[0]};
25 | svg {
26 | color: ${(p) => p.theme.colors.main.default};
27 | }
28 |
29 | &:hover,
30 | &:focus,
31 | &:active,
32 | &:active,
33 | &[aria-expanded='true'] {
34 | box-shadow: none;
35 | border-color: ${(p) => p.theme.colors.main.default};
36 | }
37 | `,
38 | filled: css`
39 | padding: 0 24px;
40 | min-width: 128px;
41 | background-color: ${(p) => p.theme.colors.main.default};
42 | border: 1px solid ${(p) => p.theme.colors.main.default};
43 | color: white;
44 | svg {
45 | color: white;
46 | }
47 |
48 | &:hover,
49 | &:focus,
50 | &:active {
51 | background-color: ${(p) => p.theme.colors.main.hover};
52 | }
53 |
54 | &[aria-disabled='true'] {
55 | background-color: ${(p) => Color(p.theme.colors.gray[2]).alpha(0.4)};
56 | border-color: transparent;
57 | }
58 | `,
59 | bordered: css`
60 | padding: 0 24px;
61 | min-width: 128px;
62 | background-color: transparent;
63 | border: 2px solid ${(p) => p.theme.colors.main.default};
64 | color: ${(p) => p.theme.colors.main.default};
65 | svg {
66 | color: ${(p) => p.theme.colors.main.default};
67 | }
68 |
69 | &:hover,
70 | &:focus,
71 | &:active {
72 | border-color: ${(p) => p.theme.colors.main.hover};
73 | color: ${(p) => p.theme.colors.main.hover};
74 | }
75 | `,
76 | link: css`
77 | border: none;
78 | height: auto !important;
79 | background-color: transparent;
80 | color: ${(p) => p.theme.colors.main.default};
81 | padding: 0 !important;
82 | span {
83 | text-decoration: underline;
84 | text-underline-offset: 3px;
85 | }
86 |
87 | &:hover,
88 | &:focus,
89 | &:active {
90 | color: ${(p) => p.theme.colors.main.hover};
91 | }
92 | `,
93 | grayscale: css`
94 | padding: 8px;
95 | background-color: transparent;
96 | border: none;
97 | color: ${(p) => p.theme.colors.gray[2]};
98 |
99 | svg {
100 | color: ${(p) => p.theme.colors.gray[4]};
101 | }
102 |
103 | &:hover,
104 | &:focus,
105 | &:active {
106 | background-color: ${(p) => p.theme.colors.gray[10]};
107 | }
108 | `,
109 | }
110 |
111 | const sizes = {
112 | medium: css`
113 | height: 48px;
114 | `,
115 | small: css`
116 | height: 34px;
117 | span {
118 | font-size: 14px;
119 | }
120 | `,
121 | }
122 |
123 | const StyledButton = styled(ReakitButton)`
124 | ${(p) => p.$variant};
125 | ${(p) => p.$size};
126 | ${(p) => p.$shape};
127 | ${space};
128 | ${color};
129 |
130 | border-radius: 8px;
131 | display: flex;
132 | justify-content: center;
133 | align-items: center;
134 | outline: none;
135 | transition:
136 | background-color 300ms,
137 | color 200ms,
138 | box-shadow 300ms,
139 | border-color 300ms;
140 |
141 | &:hover {
142 | cursor: pointer;
143 | }
144 | svg {
145 | transition:
146 | color 200ms,
147 | transform 300ms;
148 | margin-right: 8px;
149 | }
150 |
151 | &[aria-expanded='true'] {
152 | ${Arrow} {
153 | transform: rotate(180deg);
154 | }
155 | }
156 | `
157 |
158 | const Button = React.forwardRef(
159 | (
160 | {
161 | as,
162 | variant = 'default',
163 | size = 'medium',
164 | icon,
165 | toggable = false,
166 | children,
167 | ...props
168 | },
169 | ref
170 | ) => {
171 | const safeVariant = variants[variant] || variants.default
172 | const safeSize = sizes[size] || sizes.medium
173 | return (
174 |
181 | {icon}
182 | {children}
183 | {toggable && }
184 |
185 | )
186 | }
187 | )
188 |
189 | Button.propTypes = {
190 | /**
191 | * Custom tag if we don't want a "button" to appear in the DOM
192 | */
193 | as: PropTypes.string,
194 | /**
195 | * Buttons's variant
196 | */
197 | variant: PropTypes.oneOf([
198 | 'default',
199 | 'filled',
200 | 'bordered',
201 | 'clean',
202 | 'link',
203 | 'grayscale',
204 | ]),
205 | /**
206 | * Buttons's size
207 | */
208 | size: PropTypes.oneOf(['medium', 'small']),
209 | /**
210 | * The icon provided to appear on the left
211 | */
212 | icon: PropTypes.node,
213 | /**
214 | * Whether the button is toggable or not
215 | */
216 | toggable: PropTypes.bool,
217 | /**
218 | * Text to be displayed
219 | */
220 | children: PropTypes.node,
221 | }
222 |
223 | export default Button
224 |
--------------------------------------------------------------------------------
/src/components/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import PropTypes from 'prop-types'
4 |
5 | const Wrapper = styled.li`
6 | background-color: white;
7 | list-style-type: none;
8 | box-shadow: 0px 0px 30px rgba(0, 0, 0, 0.05);
9 | border-radius: 20px;
10 | padding: ${(p) => p.theme.space[4]}px;
11 | `
12 |
13 | const Card = ({ children, ...props }) => (
14 | {children}
15 | )
16 |
17 | Card.propTypes = {
18 | /**
19 | * The content to appear inside the Card
20 | */
21 | children: PropTypes.node,
22 | }
23 |
24 | export default Card
25 |
--------------------------------------------------------------------------------
/src/components/Checkbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { Checkbox as ReakitCheckbox } from 'reakit/Checkbox'
4 |
5 | const StyledCheckbox = styled(ReakitCheckbox)`
6 | appearance: none;
7 | position: relative;
8 | border-radius: 4px;
9 | width: 16px;
10 | height: 16px;
11 | margin-right: 8px;
12 | border-width: 2px;
13 | border-style: solid;
14 | border-color: ${(p) => p.theme.colors.gray[7]};
15 | background-color: white;
16 | outline: none;
17 | cursor: pointer;
18 | transition:
19 | background-color 300ms,
20 | border-color 300ms;
21 |
22 | &[aria-checked='true'] {
23 | color: white;
24 | border-color: ${(p) => p.theme.colors.main.default};
25 | background-color: ${(p) => p.theme.colors.main.default};
26 | &:before {
27 | content: '✔';
28 | position: absolute;
29 | top: 50%;
30 | left: 50%;
31 | transform: translate(-50%, -50%);
32 | font-size: 10px;
33 | }
34 | }
35 | `
36 |
37 | const Label = styled.label`
38 | transition: color 300ms;
39 | cursor: pointer;
40 | outline: none;
41 | `
42 | const Container = styled.div`
43 | color: ${(p) => p.theme.colors.gray[3]};
44 |
45 | display: flex;
46 | align-items: center;
47 | &:hover {
48 | color: ${(p) => p.theme.colors.gray[0]};
49 | }
50 |
51 | ${StyledCheckbox}:focus + ${Label} {
52 | color: ${(p) => p.theme.colors.gray[0]};
53 | }
54 | `
55 |
56 | const Checkbox = ({
57 | children,
58 | checked,
59 | onChange,
60 | label = 'checkbox',
61 | ...props
62 | }) => (
63 |
64 |
65 | {children}
66 |
67 | )
68 |
69 | export default Checkbox
70 |
--------------------------------------------------------------------------------
/src/components/Container.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import styled from 'styled-components'
3 | import Box from 'components/Box'
4 |
5 | const Container = styled(Box)`
6 | max-width: ${(p) => p.theme.breakpoints.large}px;
7 | width: 100%;
8 | margin: auto;
9 | `
10 |
11 | Container.propTypes = {
12 | /**
13 | * Container contents
14 | */
15 | children: PropTypes.node,
16 | }
17 |
18 | export default Container
19 |
--------------------------------------------------------------------------------
/src/components/EmptyView.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import Button from 'components/Button'
5 | import Box from 'components/Box'
6 |
7 | const EmptyView = ({ buttonLink, children, ...props }) => (
8 |
18 | {children}
19 |
27 | Need help?
28 |
29 |
30 | )
31 |
32 | EmptyView.propTypes = {
33 | /**
34 | * External link
35 | */
36 | buttonLink: PropTypes.string,
37 | /**
38 | * Children to be displayed
39 | */
40 | children: PropTypes.node,
41 | }
42 |
43 | export default EmptyView
44 |
--------------------------------------------------------------------------------
/src/components/Header/HelpCenter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Color from 'color'
3 | import styled from 'styled-components'
4 | import { DialogDisclosure, useDialogState } from 'reakit/Dialog'
5 |
6 | import {
7 | MeilisearchLogo,
8 | DiscordLogo,
9 | GithubLogo,
10 | InterrogationMark,
11 | } from 'components/icons'
12 | import Card from 'components/Card'
13 | import IconButton from 'components/IconButton'
14 | import Link from 'components/Link'
15 | import Modal from 'components/Modal'
16 | import Typography from 'components/Typography'
17 |
18 | const StyledCard = styled(Card)`
19 | padding: 20px 12px;
20 | width: 200px;
21 | display: flex;
22 | flex-direction: column;
23 | align-items: center;
24 | justify-content: center;
25 | `
26 |
27 | const StyledLink = styled(Link)`
28 | border-radius: 20px;
29 | box-shadow: none;
30 | transition: box-shadow 300ms;
31 |
32 | &:hover,
33 | &:focus {
34 | outline: none;
35 | box-shadow: 0px 0px 30px ${(p) => Color(p.theme.colors.gray[0]).alpha(0.1)};
36 | }
37 | `
38 |
39 | const Logo = styled.div`
40 | height: 62px;
41 | display: flex;
42 | justify-content: center;
43 | align-items: center;
44 | `
45 |
46 | const HelpCard = ({ description, title, logo, href, ...props }) => (
47 |
48 |
49 | {logo}
50 |
51 | {title}
52 |
53 |
58 | {description}
59 |
60 |
61 |
62 | )
63 |
64 | const CardsContainer = styled.div`
65 | margin-top: 48px;
66 | display: flex;
67 | flex-direction: row;
68 | align-items: center;
69 | justify-content: center;
70 |
71 | > a + a {
72 | margin-left: 20px;
73 | }
74 | `
75 |
76 | const HelpCenter = () => {
77 | const dialog = useDialogState()
78 | return (
79 | <>
80 |
81 | {(props) => (
82 |
89 |
90 |
91 | )}
92 |
93 |
99 |
100 | If you need help with anything, here are a few links that can be
101 | useful.
102 |
103 |
104 | }
106 | title="Github"
107 | description="Explore our repositories on Github"
108 | href="https://github.com/meilisearch"
109 | />
110 | }
112 | title="Discord"
113 | description="Join our Discord and find the help you need"
114 | href="https://discord.meilisearch.com"
115 | />
116 | }
118 | title="Documentation"
119 | description="Learn how to tune your Meilisearch"
120 | href="https://docs.meilisearch.com/?utm_campaign=oss&utm_source=integration&utm_medium=minidashboard"
121 | />
122 |
123 |
124 | >
125 | )
126 | }
127 |
128 | export default HelpCenter
129 |
--------------------------------------------------------------------------------
/src/components/IconButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Color from 'color'
3 | import styled, { css } from 'styled-components'
4 | import PropTypes from 'prop-types'
5 | import { space, color } from 'styled-system'
6 | import theme from 'theme'
7 |
8 | const variants = {
9 | default: css`
10 | border: none;
11 | padding: 4px 6px;
12 | &:focus {
13 | svg {
14 | filter: drop-shadow(
15 | 0px 0px 3px ${(p) => Color(p.theme.colors[p.color]).alpha(0.2)}
16 | );
17 | }
18 | }
19 | `,
20 | bordered: css`
21 | border-width: 1px;
22 | border-style: solid;
23 | border-color: inherit;
24 | border-radius: 50%;
25 | padding: 4px;
26 | display: flex;
27 | justify-content: center;
28 | align-items: center;
29 | transition: background-color 300ms;
30 | svg {
31 | transition: color 300ms;
32 | }
33 | &:hover,
34 | &:focus {
35 | background-color: currentColor;
36 | svg {
37 | color: white;
38 | }
39 | }
40 | `,
41 | }
42 |
43 | const StyledButton = styled.button`
44 | ${(p) => p.$variant};
45 | ${space};
46 | ${color};
47 | outline: none;
48 | background-color: transparent;
49 | cursor: pointer;
50 | svg {
51 | display: block;
52 | }
53 | `
54 |
55 | const IconButton = React.forwardRef(
56 | ({ color: iconColor, variant = 'default', children, ...props }, ref) => {
57 | const safeVariant = variants[variant] || variants.default
58 |
59 | return (
60 |
66 | {children}
67 |
68 | )
69 | }
70 | )
71 |
72 | IconButton.propTypes = {
73 | /**
74 | * Color of the icon
75 | */
76 | color: PropTypes.node,
77 | /**
78 | * variant of the button
79 | */
80 | variant: PropTypes.oneOf(['default', 'bordered']),
81 | /**
82 | * Text to be displayed
83 | */
84 | children: PropTypes.node,
85 | }
86 |
87 | export default IconButton
88 |
--------------------------------------------------------------------------------
/src/components/Input.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react'
2 | import styled from 'styled-components'
3 | import Color from 'color'
4 | import PropTypes from 'prop-types'
5 |
6 | import IconButton from 'components/IconButton'
7 | import Cross from 'components/icons/Cross'
8 |
9 | const InputField = styled.input`
10 | height: 48px;
11 | width: 100%;
12 | padding-left: ${(p) => (p.$hasIcon ? '48px' : '8px')};
13 | padding-right: 8px;
14 | background-position: top 50% left 16px;
15 | border-color: ${(p) => p.theme.colors.gray[10]};
16 | border-width: 1px;
17 | border-style: solid;
18 | border-radius: 8px;
19 | box-shadow: 0px 4px 6px ${Color('#000').alpha(0.04)};
20 | transition: border-color 300ms;
21 | outline: none;
22 | color: ${(p) => p.theme.colors.gray[0]};
23 | font-weight: 500;
24 | font-size: 18px;
25 | line-height: 22px;
26 |
27 | ::placeholder {
28 | color: ${(p) => p.theme.colors.gray[7]};
29 | }
30 |
31 | &:hover {
32 | border-color: ${(p) => p.theme.colors.gray[8]};
33 | }
34 |
35 | &:focus {
36 | border-color: ${(p) => p.theme.colors.main.default};
37 | svg {
38 | fill: ${(p) => p.theme.colors.main.default};
39 | }
40 | }
41 |
42 | ::-webkit-search-cancel-button {
43 | -webkit-appearance: none;
44 | }
45 | `
46 |
47 | const ClearButton = styled(IconButton)`
48 | svg {
49 | width: 11px;
50 | height: 11px;
51 | }
52 | position: absolute;
53 | right: 8px;
54 | top: 50%;
55 | transform: translateY(-50%);
56 | color: ${(p) => p.theme.colors.gray[5]};
57 | `
58 |
59 | const InputContainer = styled.div`
60 | position: relative;
61 | display: inline-block;
62 |
63 | > svg {
64 | position: absolute;
65 | top: 50%;
66 | left: 16px;
67 | transform: translateY(-50%);
68 | }
69 | `
70 |
71 | const Input = ({ icon, ref, clear, type, value, ...props }) => {
72 | const input = useRef(null)
73 | return (
74 |
75 | {icon}
76 |
83 | {type === 'search' && (
84 | {
87 | clear()
88 | input.current.focus()
89 | }}
90 | style={{ display: value ? 'block' : 'none' }}
91 | >
92 |
93 |
94 | )}
95 |
96 | )
97 | }
98 |
99 | export default Input
100 |
101 | Input.propTypes = {
102 | /**
103 | * Icon you want to appear inside the input, on the left
104 | */
105 | icon: PropTypes.node,
106 | }
107 |
--------------------------------------------------------------------------------
/src/components/Link.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import PropTypes from 'prop-types'
4 |
5 | const A = styled.a`
6 | color: ${(p) => p.color || p.theme.colors.main.default};
7 | text-decoration: underline;
8 | transition: color 300ms;
9 | outline: none;
10 | &:hover,
11 | &:focus {
12 | color: ${(p) => p.theme.colors.main.hover};
13 | }
14 | `
15 |
16 | const Link = ({ href, target = '_blank', children, ...props }) => (
17 |
18 | {children}
19 |
20 | )
21 |
22 | Link.propTypes = {
23 | /**
24 | * The link where the user should be redirected
25 | */
26 | href: PropTypes.string,
27 | /**
28 | * How the user should be redirected
29 | */
30 | target: PropTypes.string,
31 | /**
32 | * The text that should be cliquable
33 | */
34 | children: PropTypes.node,
35 | }
36 |
37 | export default Link
38 |
--------------------------------------------------------------------------------
/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Color from 'color'
3 | import PropTypes from 'prop-types'
4 | import styled from 'styled-components'
5 | import {
6 | Dialog as ReakitDialog,
7 | DialogBackdrop as ReakitDialogBackdrop,
8 | } from 'reakit/Dialog'
9 |
10 | import IconButton from 'components/IconButton'
11 | import Typography from 'components/Typography'
12 | import { Cross } from 'components/icons'
13 |
14 | const DialogBackdrop = styled(ReakitDialogBackdrop)`
15 | &[data-leave] {
16 | opacity: 0;
17 | }
18 | &[data-enter] {
19 | opacity: 1;
20 | }
21 | transition: opacity 250ms ease-in-out;
22 | position: fixed;
23 | top: 0;
24 | right: 0;
25 | bottom: 0;
26 | left: 0;
27 | z-index: 997;
28 | background-color: ${(p) => Color(p.theme.colors.gray[3]).alpha(0.6)};
29 | `
30 |
31 | const Dialog = styled(ReakitDialog)`
32 | &[data-leave] {
33 | opacity: 0;
34 | }
35 | &[data-enter] {
36 | opacity: 1;
37 | }
38 | transition: opacity 250ms ease-in-out;
39 | max-width: 992px;
40 | position: relative;
41 | width: 70%;
42 | top: 50%;
43 | left: 50%;
44 | transform: translate(-50%, calc(-50% - 48px));
45 | border-radius: 0.25rem;
46 | outline: 0px;
47 | padding: 24px 32px 24px 40px;
48 | box-shadow: 0px 0px 30px ${(p) => Color(p.theme.colors.gray[0]).alpha(0.15)};
49 | background-color: ${(p) => p.theme.colors.gray[11]};
50 | z-index: 999;
51 | `
52 |
53 | const Button = styled(IconButton)`
54 | position: absolute;
55 | top: 16px;
56 | right: 16px;
57 | &:hover {
58 | pointer-events: initial;
59 | }
60 | `
61 |
62 | const Modal = ({
63 | title,
64 | dialog,
65 | children,
66 | ariaLabel = 'Welcome',
67 | ...props
68 | }) => (
69 |
70 |
71 | {title && (
72 |
73 | {title}
74 |
75 | )}
76 | {children}
77 | dialog.hide()} aria-label="close">
78 |
79 |
80 |
81 |
82 | )
83 |
84 | Modal.propTypes = {
85 | /**
86 | * Title of the Modal
87 | */
88 | title: PropTypes.string,
89 | /**
90 | * Modal contents
91 | */
92 | children: PropTypes.node,
93 | }
94 |
95 | export default Modal
96 |
--------------------------------------------------------------------------------
/src/components/NewsletterForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { keyframes } from 'styled-components'
3 | import useNewsletter from 'hooks/useNewsletter'
4 | import ArrowPathIcon from './icons/heroicons/ArrowPathIcon'
5 | import CheckIcon from './icons/heroicons/CheckIcon'
6 |
7 | const spin = keyframes`
8 | from {
9 | transform: rotate(0deg);
10 | }
11 | to {
12 | transform: rotate(360deg);
13 | }
14 | `
15 |
16 | const SpinningIcon = styled.div`
17 | display: inline-flex;
18 | animation: ${spin} 1s linear infinite;
19 | svg {
20 | width: 1.25rem;
21 | height: 1.25rem;
22 | }
23 | `
24 |
25 | const Input = styled.input`
26 | width: 100%;
27 | padding: 0.75rem 1rem;
28 | border: 1px solid ${(p) => p.theme.colors.gray[8]};
29 | border-radius: 8px;
30 | font-size: 0.875rem;
31 | color: ${(p) => p.theme.colors.gray[0]};
32 | background: ${(p) => p.theme.colors.white};
33 | margin: 1rem 0;
34 | &:focus {
35 | outline: none;
36 | border-color: ${(p) => p.theme.colors.main.default};
37 | }
38 | &::placeholder {
39 | color: ${(p) => p.theme.colors.gray[6]};
40 | }
41 | `
42 |
43 | const Button = styled.button`
44 | width: 100%;
45 | padding: 0.75rem 1rem;
46 | background: ${(p) => p.theme.colors.main.default};
47 | color: white;
48 | border: none;
49 | border-radius: 8px;
50 | font-size: 0.875rem;
51 | font-weight: 600;
52 | cursor: pointer;
53 | transition: background-color 0.2s;
54 | display: flex;
55 | align-items: center;
56 | justify-content: center;
57 | gap: 0.5rem;
58 | &:hover {
59 | background: ${(p) => p.theme.colors.main.dark};
60 | }
61 | &:disabled {
62 | opacity: 0.7;
63 | cursor: not-allowed;
64 | }
65 | `
66 |
67 | const ErrorMessage = styled.p`
68 | color: ${(p) => p.theme.colors.error.text};
69 | font-size: 0.875rem;
70 | margin-top: 0.5rem;
71 | `
72 |
73 | const SuccessMessage = styled.div`
74 | display: flex;
75 | text-align: center;
76 | align-items: center;
77 | justify-content: center;
78 | gap: 0.5rem;
79 | background-color: ${(p) => p.theme.colors.success.background};
80 | border: 1px solid ${(p) => p.theme.colors.success.border};
81 | border-radius: 8px;
82 | padding: 0rem;
83 | color: ${(p) => p.theme.colors.success.text};
84 | svg {
85 | width: 1.25rem;
86 | height: 1.25rem;
87 | }
88 | p {
89 | font-size: 0.875rem;
90 | }
91 | `
92 |
93 | const NewsletterForm = () => {
94 | const [email, setEmail] = React.useState('')
95 | const { subscribe } = useNewsletter()
96 | const [status, setStatus] = React.useState('idle')
97 | const [error, setError] = React.useState(null)
98 |
99 | const handleSubmit = (e) => {
100 | e.preventDefault()
101 | setStatus('loading')
102 | subscribe(email)
103 | .then(() => setStatus('success'))
104 | .catch((err) => {
105 | setStatus('error')
106 | setError(err.message)
107 | })
108 | }
109 |
110 | if (status === 'success') {
111 | return (
112 |
113 |
114 | Thanks for subscribing!
115 |
116 | )
117 | }
118 |
119 | return (
120 |
148 | )
149 | }
150 |
151 | export default NewsletterForm
152 |
--------------------------------------------------------------------------------
/src/components/NoMeilisearchRunning.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Typography from 'components/Typography'
3 | import EmptyView from 'components/EmptyView'
4 | import BodyWrapper from 'components/BodyWrapper'
5 | import Box from 'components/Box'
6 |
7 | const NoMeilisearchRunning = () => (
8 |
9 |
10 |
11 |
17 | It seems like Meilisearch isn’t running, did you forget to start it?
18 |
19 |
25 | (Don’t forget to set an API Key if you want one)
26 |
27 |
32 |
33 | 🧐
34 |
35 |
36 |
37 |
38 |
39 | )
40 |
41 | export default NoMeilisearchRunning
42 |
--------------------------------------------------------------------------------
/src/components/NoSelectOption.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Box from 'components/Box'
4 | import Typography from 'components/Typography'
5 | import Button from 'components/Button'
6 |
7 | const NoSelectOption = () => (
8 |
17 |
18 | no index found
19 |
20 |
28 | Need help?
29 |
30 |
31 | )
32 |
33 | export default NoSelectOption
34 |
--------------------------------------------------------------------------------
/src/components/OnBoarding.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import Box from 'components/Box'
5 | import Button from 'components/Button'
6 | import Card from 'components/Card'
7 | import Typography from 'components/Typography'
8 | import { LogoText, KeyBig, DocumentBig } from 'components/icons'
9 |
10 | const OnBoardingCard = ({ number, title, icon, href, ...props }) => (
11 |
23 | {icon}
24 |
25 |
26 | {number}
27 |
28 |
29 | {title}
30 |
31 |
32 |
40 | Learn more
41 |
42 |
43 | )
44 |
45 | const CardsContainer = styled.div`
46 | display: flex;
47 | margin-top: 48px;
48 | section + section {
49 | margin-left: 72px;
50 | }
51 | svg {
52 | color: ${(p) => p.theme.colors.gray[8]};
53 | height: 36px;
54 | }
55 | `
56 |
57 | const OnBoarding = () => (
58 |
65 |
66 | Welcome to
67 |
68 |
69 |
70 | Mini Dashboard
71 |
72 |
78 | This dashboard will help you check the search results with ease.
79 |
80 |
81 | }
86 | />
87 | }
92 | />
93 | {/* TODO: Enable it once facet search is available */}
94 | {/* }
99 | /> */}
100 |
101 |
102 | )
103 |
104 | export default OnBoarding
105 |
--------------------------------------------------------------------------------
/src/components/Results/Highlight.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connectHighlight } from 'react-instantsearch-dom'
3 | import Typography from 'components/Typography'
4 |
5 | const Highlight = connectHighlight(
6 | ({ highlight, attribute, hit, indexContextValue, ...props }) => {
7 | const parsedHit = highlight({
8 | highlightProperty: '_highlightResult',
9 | attribute,
10 | hit,
11 | })
12 |
13 | return (
14 |
15 | {parsedHit.map((part, index) =>
16 | part.isHighlighted ? (
17 | {part.value}
18 | ) : (
19 | {part.value}
20 | )
21 | )}
22 |
23 | )
24 | }
25 | )
26 |
27 | export default Highlight
28 |
--------------------------------------------------------------------------------
/src/components/Results/InfiniteHits.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { connectInfiniteHits } from 'react-instantsearch-dom'
4 | // import ReactJson from 'react-json-view'
5 |
6 | // import { jsonTheme } from 'theme'
7 | import Button from 'components/Button'
8 | // import Card from 'components/Card'
9 | import ScrollToTop from 'components/ScrollToTop'
10 |
11 | import Hit from './Hit'
12 |
13 | const HitsList = styled.ul`
14 | padding: 0;
15 | margin: 0;
16 | > li + li {
17 | margin-top: 16px;
18 | }
19 | `
20 |
21 | const isAnImage = async (elem) => {
22 | // Test the standard way with regex and image extensions
23 | if (
24 | typeof elem === 'string' &&
25 | elem.match(/^(https|http):\/\/.*(jpe?g|png|gif|webp)(\?.*)?$/gi)
26 | )
27 | return true
28 |
29 | if (typeof elem === 'string' && elem.match(/^https?:\/\//)) {
30 | // Tries to load an image that is a valid URL but doesn't have a correct extension
31 | return new Promise((resolve) => {
32 | const img = new Image()
33 | img.src = elem
34 | img.onload = () => resolve(true)
35 | img.onerror = () => resolve(false)
36 | })
37 | }
38 | return false
39 | }
40 |
41 | const findImageKey = async (array) => {
42 | const promises = array.map(async (elem) => isAnImage(elem[1]))
43 | const results = await Promise.all(promises)
44 | const index = results.findIndex((result) => result)
45 | const imageField = array[index]
46 | return imageField?.[0]
47 | }
48 |
49 | const InfiniteHits = connectInfiniteHits(({ hits, hasMore, refineNext }) => {
50 | const [imageKey, setImageKey] = React.useState(false)
51 |
52 | React.useEffect(() => {
53 | const getImageKey = async () => {
54 | setImageKey(hits[0] ? await findImageKey(Object.entries(hits[0])) : null)
55 | }
56 | getImageKey()
57 | }, [hits[0]])
58 | // ({ hits, hasMore, refineNext, mode }) => {
59 | return (
60 |
61 | {/* {mode === 'fancy' ? ( */}
62 |
63 | {hits.map((hit, index) => (
64 |
65 | ))}
66 |
67 | {/* ) : (
68 |
69 |
78 |
79 | )} */}
80 | {hasMore && (
81 |
87 | Load more
88 |
89 | )}
90 |
91 |
92 | )
93 | })
94 |
95 | export default InfiniteHits
96 |
--------------------------------------------------------------------------------
/src/components/Results/NoResultForRequest.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import EmptyView from 'components/EmptyView'
3 | import Typography from 'components/Typography'
4 |
5 | const NoResultForRequest = () => (
6 |
7 |
13 | Sorry mate, no results matching your request
14 |
15 |
16 |
17 | ☹️
18 |
19 |
20 |
21 | )
22 |
23 | export default NoResultForRequest
24 |
--------------------------------------------------------------------------------
/src/components/Results/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import React from 'react'
3 | import { connectStateResults, connectStats } from 'react-instantsearch-dom'
4 |
5 | // import { DocumentMedium, Picture } from 'components/icons'
6 | import Box from 'components/Box'
7 | import Stats from 'components/Stats'
8 | // import Toggle from 'components/Toggle'
9 | // import useLocalStorage from 'hooks/useLocalStorage'
10 | import InfiniteHits from './InfiniteHits'
11 | import NoResultForRequest from './NoResultForRequest'
12 |
13 | // const Label1 = () => (
14 | // <>
15 | //
16 | // Fancy
17 | // >
18 | // )
19 |
20 | // const Label2 = () => (
21 | // <>
22 | //
23 | // Json
24 | // >
25 | // )
26 |
27 | const ConnectedStats = connectStats((props) => )
28 |
29 | const Results = connectStateResults(({ searchResults }) => {
30 | const hasResults = !!searchResults && searchResults?.nbHits !== 0
31 |
32 | return (
33 | <>
34 |
40 |
41 |
42 |
43 | {/* }
45 | offLabel={ }
46 | ariaLabel="toggleMode"
47 | initialValue={mode === 'fancy'}
48 | onChange={(e) => setMode(e.target.checked ? 'fancy' : 'json')}
49 | /> */}
50 |
51 | {/* {hasResults ? : } */}
52 | {hasResults ? : }
53 | >
54 | )
55 | })
56 |
57 | export default Results
58 |
--------------------------------------------------------------------------------
/src/components/ScrollToTop.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Color from 'color'
3 | import styled from 'styled-components'
4 |
5 | import IconButton from 'components/IconButton'
6 | import ArrowDownIcon from 'components/icons/ArrowDown'
7 |
8 | const ArrowDown = styled(ArrowDownIcon)`
9 | transform: rotate(180deg);
10 | color: white;
11 | `
12 |
13 | const ScrollButton = styled(IconButton)`
14 | position: fixed;
15 | bottom: 40px;
16 | right: 40px;
17 | background-color: ${(p) => Color(p.theme.colors.gray[2]).alpha(0.4)};
18 | width: 40px;
19 | height: 40px;
20 | border-radius: 50%;
21 | display: flex;
22 | align-items: center;
23 | justify-content: center;
24 | transition: background-color 300ms;
25 |
26 | &:hover,
27 | &:focus {
28 | background-color: ${(p) => Color(p.theme.colors.gray[4])};
29 | }
30 | `
31 |
32 | const ScrollToTop = () => {
33 | const scrollToTop = () => {
34 | window.scroll({ top: 0, behavior: 'smooth' })
35 | }
36 |
37 | return (
38 | scrollToTop()} aria-label="scroll to top">
39 |
40 |
41 | )
42 | }
43 |
44 | export default ScrollToTop
45 |
--------------------------------------------------------------------------------
/src/components/SearchBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { connectSearchBox } from 'react-instantsearch-dom'
4 |
5 | import Input from 'components/Input'
6 | import { SearchMedium } from 'components/icons'
7 |
8 | const SearchWrapper = styled.div`
9 | width: 100%;
10 | display: flex;
11 | flex: 1;
12 |
13 | > div {
14 | width: 100%;
15 | }
16 | `
17 |
18 | const SearchIcon = styled(SearchMedium)`
19 | max-width: 20px;
20 | color: ${(p) => p.theme.colors.gray[2]};
21 | `
22 |
23 | const SearchBox = connectSearchBox(
24 | ({ currentRefinement, refine, refreshIndexes, currentIndex }) => {
25 | const [value, setValue] = React.useState(currentRefinement)
26 |
27 | React.useEffect(() => {
28 | if (currentIndex?.stats?.numberOfDocuments === 0) {
29 | refreshIndexes()
30 | }
31 | refine(value)
32 | }, [value])
33 |
34 | return (
35 |
36 | setValue(e.target.value)}
40 | clear={() => setValue('')}
41 | placeholder="Search something"
42 | icon={ }
43 | />
44 |
45 | )
46 | }
47 | )
48 |
49 | export default SearchBox
50 |
--------------------------------------------------------------------------------
/src/components/Select.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { css } from 'styled-components'
3 | import Color from 'color'
4 | import PropTypes from 'prop-types'
5 | import { useMenuState, Menu, MenuItem, MenuButton } from 'reakit/Menu'
6 |
7 | import { ArrowDown } from 'components/icons'
8 | import Typography from 'components/Typography'
9 |
10 | const Arrow = styled(ArrowDown)`
11 | position: absolute;
12 | right: 0;
13 | top: calc(50% - 3px);
14 | transition: transform 300ms;
15 | width: 9px;
16 | `
17 |
18 | const SelectIndexesButton = styled(MenuButton)`
19 | position: relative;
20 | padding: 12px 32px 12px 12px;
21 | height: 48px;
22 | background-color: white;
23 | display: flex;
24 | align-items: center;
25 | min-width: 260px;
26 | border-color: ${(p) => p.theme.colors.gray[10]};
27 | border-width: 1px;
28 | border-style: solid;
29 | border-radius: 8px;
30 | transition: border-color 300ms;
31 | outline: none;
32 | color: ${(p) => p.theme.colors.gray[0]};
33 | font-weight: 500;
34 | font-size: 16px;
35 | line-height: 22px;
36 | cursor: pointer;
37 |
38 | ${(p) =>
39 | p.visible &&
40 | css`
41 | ${Arrow} {
42 | transform: rotate(180deg);
43 | }
44 | `};
45 |
46 | &:hover,
47 | &:focus,
48 | &[aria-expanded='true'] {
49 | border-color: ${(p) => p.theme.colors.main.default};
50 | }
51 |
52 | svg {
53 | margin-right: 16px;
54 | color: ${(p) => p.theme.colors.main.default};
55 | flex-shrink: 0;
56 | }
57 | `
58 |
59 | const IndexesListContainer = styled(Menu)`
60 | min-width: 218px;
61 | display: flex;
62 | flex-direction: column;
63 | outline: none;
64 | border-width: 1px;
65 | border-style: solid;
66 | border-color: ${(p) => p.theme.colors.gray[10]};
67 | border-radius: 8px;
68 | box-shadow: 0px 4px 6px ${Color('#000').alpha(0.04)};
69 | overflow: hidden;
70 | max-height: 180px;
71 | overflow: auto;
72 | `
73 |
74 | const IndexItem = styled(MenuItem)`
75 | background-color: white;
76 | height: 40px;
77 | border: 0;
78 | outline: none;
79 | transition: background-color 300ms;
80 | padding: 6px 18px;
81 | text-align: left;
82 | color: ${(p) => p.theme.colors.gray[2]};
83 |
84 | &:hover,
85 | &:focus {
86 | cursor: pointer;
87 | background-color: ${(p) => p.theme.colors.gray[10]};
88 | }
89 |
90 | ${(p) =>
91 | p.$selected &&
92 | css`
93 | span:first-child {
94 | font-weight: 600;
95 | }
96 | span:nth-child(2) {
97 | color: ${p.theme.colors.gray[5]};
98 | }
99 | `}
100 | `
101 |
102 | const IndexId = styled(Typography)`
103 | text-overflow: ellipsis;
104 | white-space: nowrap;
105 | overflow: hidden;
106 | `
107 |
108 | const TextToDisplay = ({ option, currentOption }) => (
109 | <>
110 |
111 | {option ? option.uid : 'Select an index'}
112 | {' '}
113 | {option?.stats && (
114 |
115 | {option.stats.numberOfDocuments.toLocaleString()}
116 |
117 | )}
118 | >
119 | )
120 |
121 | const Select = ({
122 | options,
123 | icon,
124 | currentOption,
125 | onChange,
126 | noOptionComponent,
127 | ...props
128 | }) => {
129 | const menu = useMenuState()
130 | return (
131 | <>
132 |
133 | {icon || null}
134 |
135 |
136 |
137 |
138 | {options?.length
139 | ? options.map((data, index) => (
140 | {
146 | onChange(data)
147 | menu.hide()
148 | }}
149 | $selected={currentOption?.uid === data.uid}
150 | >
151 |
152 |
153 | ))
154 | : noOptionComponent}
155 |
156 | >
157 | )
158 | }
159 |
160 | Select.propTypes = {
161 | /**
162 | * List of options to appear
163 | */
164 | options: PropTypes.arrayOf(
165 | PropTypes.shape({
166 | uid: PropTypes.string,
167 | stats: PropTypes.shape({
168 | numberOfDocuments: PropTypes.number,
169 | }),
170 | })
171 | ),
172 | /**
173 | * Icon you want to appear inside the select button, on the left
174 | */
175 | icon: PropTypes.node,
176 | /**
177 | * The current option to be displayed
178 | */
179 | currentOption: PropTypes.shape({
180 | uid: PropTypes.string,
181 | stats: PropTypes.shape({
182 | numberOfDocuments: PropTypes.number,
183 | }),
184 | }),
185 | /**
186 | * Function used to change the current option, triggered on click on an option
187 | */
188 | onChange: PropTypes.func,
189 | /**
190 | * Component to display if select has no options
191 | */
192 | noOptionComponent: PropTypes.node,
193 | }
194 |
195 | export default Select
196 |
--------------------------------------------------------------------------------
/src/components/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Color from 'color'
3 | import styled from 'styled-components'
4 | import PropTypes from 'prop-types'
5 | import { ArrowDown } from 'components/icons'
6 | import {
7 | useDisclosureState,
8 | Disclosure as ReakitDisclosure,
9 | DisclosureContent as ReakitDisclosureContent,
10 | } from 'reakit/Disclosure'
11 |
12 | const SidebarWrapper = styled.div`
13 | background-color: white;
14 | flex-shrink: 0;
15 | width: 300px;
16 | display: flex;
17 | overflow: auto;
18 | box-shadow: 0px 0px 30px ${(p) => Color(p.theme.colors.gray[0]).alpha(0.07)};
19 | position: relative;
20 | margin-left: -246px;
21 | transition: margin-left 300ms;
22 | position: sticky;
23 | top: 0;
24 | height: 100%;
25 |
26 | &[aria-expanded='true'] {
27 | margin-left: 0px;
28 | }
29 | `
30 |
31 | const Arrow = styled(ArrowDown)`
32 | width: 18px;
33 | height: 9px;
34 | transition: color 300ms;
35 | `
36 |
37 | const OpeningIcon = styled.div`
38 | width: 16px;
39 | height: 16px;
40 | display: flex;
41 | align-items: center;
42 | justify-content: center;
43 | transition: opacity 300ms;
44 | `
45 |
46 | const Disclosure = styled(ReakitDisclosure)`
47 | position: absolute;
48 | top: 24px;
49 | right: 12px;
50 | z-index: 1;
51 | cursor: pointer;
52 | padding: 6px;
53 | background-color: transparent;
54 | border: 0;
55 |
56 | svg {
57 | color: ${(p) => p.theme.colors.gray[7]};
58 | transition: color 300ms;
59 | }
60 | &:hover,
61 | &:focus {
62 | outline: none;
63 |
64 | svg {
65 | color: ${(p) => p.theme.colors.main.default};
66 | }
67 | }
68 | `
69 |
70 | const DisclosureContent = styled(ReakitDisclosureContent)`
71 | transition:
72 | opacity 300ms ease-in-out,
73 | transform 300ms ease-in-out;
74 | opacity: 0;
75 | height: 100%;
76 | width: 100%;
77 | transform: translateX(-100%);
78 | &[data-enter] {
79 | opacity: 1;
80 | transform: translateX(0);
81 | }
82 | `
83 |
84 | const Sidebar = ({
85 | sidebarIcon = ,
86 | visible = true,
87 | onChange = () => {},
88 | children,
89 | ...props
90 | }) => {
91 | const disclosure = useDisclosureState({ animated: true, visible })
92 | const [toggled, setToggled] = React.useState(false)
93 |
94 | const openingIcon = sidebarIcon || (
95 |
96 | )
97 | const openIconRef = React.useRef(null)
98 | const closeIconRef = React.useRef(null)
99 |
100 | React.useEffect(() => {
101 | if (toggled) {
102 | if (!disclosure.visible) {
103 | openIconRef?.current?.focus()
104 | } else {
105 | closeIconRef?.current?.focus()
106 | }
107 | onChange(disclosure.visible)
108 | }
109 | }, [openIconRef, disclosure.visible])
110 |
111 | return (
112 |
113 | {!disclosure.visible && (
114 | setToggled(true)}
117 | {...disclosure}
118 | >
119 | {openingIcon}
120 |
121 | )}
122 |
123 | {disclosure.visible && (
124 | {
127 | disclosure.hide()
128 | setToggled(true)
129 | }}
130 | >
131 |
132 |
133 | )}
134 | {children}
135 |
136 |
137 | )
138 | }
139 |
140 | Sidebar.propTypes = {
141 | /**
142 | * The icon you want to display to open the sidebar. Arrow by default
143 | */
144 | sidebarIcon: PropTypes.node,
145 | /**
146 | * The initial state of the sidebar (open or closed)
147 | */
148 | visible: PropTypes.bool,
149 | /**
150 | * Action to trigger on toggle change
151 | */
152 | onChange: PropTypes.func,
153 | /**
154 | * Content of the sidebar
155 | */
156 | children: PropTypes.node,
157 | }
158 |
159 | export default Sidebar
160 |
--------------------------------------------------------------------------------
/src/components/Stats.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import PropTypes from 'prop-types'
4 |
5 | import { SearchSmall, Speed } from 'components/icons'
6 | import Typography from 'components/Typography'
7 |
8 | const Legend = styled(Typography)`
9 | display: flex;
10 | align-items: center;
11 | margin-bottom: 6px;
12 | svg {
13 | margin-right: 6px;
14 | }
15 | `
16 |
17 | const Stat = ({ icon, legend, value, ...props }) => (
18 |
19 |
20 | {icon}
21 | {legend}
22 |
23 |
24 | {value}
25 |
26 |
27 | )
28 |
29 | const StatsContainer = styled.div`
30 | display: flex;
31 | `
32 |
33 | const Stats = ({ nbHits, processingTimeMS, nbResults, ...props }) => {
34 | const localeNbHits = `${
35 | nbHits !== nbResults ? '~' : ''
36 | } ${nbHits.toLocaleString()}`
37 |
38 | return (
39 |
40 | }
42 | legend="Hits"
43 | value={localeNbHits}
44 | />
45 | }
47 | legend="Time spent"
48 | value={`${processingTimeMS} ms`}
49 | />
50 |
51 | )
52 | }
53 |
54 | Stats.propTypes = {
55 | /**
56 | * Number of hits provided by connectStats
57 | */
58 | nbHits: PropTypes.number,
59 | /**
60 | * Time in ms needed to execute the request
61 | */
62 | processingTimeMS: PropTypes.number,
63 | /**
64 | * Number of results provided by connectStateResults
65 | */
66 | nbResults: PropTypes.number,
67 | }
68 |
69 | export default Stats
70 |
--------------------------------------------------------------------------------
/src/components/Toggle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import PropTypes from 'prop-types'
4 | import Color from 'color'
5 | import { Checkbox } from 'reakit/Checkbox'
6 |
7 | const Label = styled.label`
8 | width: 200px;
9 | height: 40px;
10 | background-color: ${(p) => p.theme.colors.gray[10]};
11 | border-radius: 60px;
12 | display: flex;
13 | align-items: center;
14 | position: relative;
15 | `
16 |
17 | const Input = styled(Checkbox)`
18 | width: 98px;
19 | height: 32px;
20 | margin: 0;
21 | background-color: white;
22 | border-radius: 60px;
23 | position: absolute;
24 | top: 4px;
25 | transform: translate(4px);
26 | transition: transform 300ms;
27 | &[aria-checked='false'] {
28 | transform: translate(98px);
29 | }
30 | &:before {
31 | content: '';
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 | right: 0;
36 | bottom: 0;
37 | background-color: white;
38 | border-radius: 60px;
39 | box-shadow: 0px 4px 6px ${(p) => Color(p.theme.colors.gray[0]).alpha(0.11)};
40 | }
41 | &:focus {
42 | outline: none;
43 | &:before {
44 | box-shadow: 0px 0px 12px
45 | ${(p) => Color(p.theme.colors.gray[0]).alpha(0.2)};
46 | }
47 | }
48 | -moz-appearance: initial;
49 | `
50 |
51 | const Span = styled.span`
52 | height: 100%;
53 |
54 | width: 50%;
55 | display: inline-flex;
56 | align-items: center;
57 | justify-content: center;
58 | z-index: 2;
59 | &: hover {
60 | cursor: pointer;
61 | }
62 | transition: color 300ms;
63 | &:hover {
64 | color: ${(p) => p.theme.colors.gray[0]};
65 | }
66 | color: ${(p) =>
67 | p.checked ? p.theme.colors.gray[0] : p.theme.colors.gray[5]};
68 | `
69 |
70 | const Toggle = ({
71 | onLabel = 'On',
72 | offLabel = 'Off',
73 | ariaLabel,
74 | onChange,
75 | initialValue = true,
76 | ...props
77 | }) => {
78 | const [checked, setChecked] = React.useState(initialValue)
79 | const toggle = () => setChecked(!checked)
80 |
81 | return (
82 |
83 | {
86 | toggle()
87 | onChange(e)
88 | }}
89 | aria-label={ariaLabel}
90 | />
91 | {onLabel}
92 | {offLabel}
93 |
94 | )
95 | }
96 |
97 | Toggle.propTypes = {
98 | /**
99 | * Text displayed when toggle is on
100 | */
101 | onLabel: PropTypes.element,
102 | /**
103 | * Text displayed when toggle is off
104 | */
105 | offLabel: PropTypes.element,
106 | /**
107 | * Aria-label for toggler
108 | */
109 | ariaLabel: PropTypes.string,
110 | /**
111 | * Function to run when a change occurs
112 | */
113 | onChange: PropTypes.func,
114 | /**
115 | * The initial state of the Toggle
116 | */
117 | initialValue: PropTypes.bool,
118 | }
119 |
120 | export default Toggle
121 |
--------------------------------------------------------------------------------
/src/components/Typography.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { css } from 'styled-components'
3 | import PropTypes from 'prop-types'
4 | import { space, color } from 'styled-system'
5 |
6 | const variants = {
7 | default: {
8 | tag: 'span',
9 | style: css``,
10 | },
11 | typo1: {
12 | tag: 'h2',
13 | style: css`
14 | font-size: 22px;
15 | line-height: 22px;
16 | font-weight: 500;
17 | margin: 0;
18 | `,
19 | },
20 | typo2: {
21 | tag: 'span',
22 | style: css`
23 | font-family: Barlow;
24 | font-size: 15px;
25 | line-height: 22px;
26 | font-weight: 500;
27 | `,
28 | },
29 | typo3: {
30 | tag: 'span',
31 | style: css`
32 | font-size: 13px;
33 | line-height: 19px;
34 | font-weight: 500;
35 | `,
36 | },
37 | typo4: {
38 | tag: 'span',
39 | style: css`
40 | font-size: 16px;
41 | line-height: 22px;
42 | font-weight: 500;
43 | `,
44 | },
45 | typo5: {
46 | tag: 'span',
47 | style: css`
48 | font-size: 14px;
49 | line-height: 22px;
50 | font-weight: 500;
51 | `,
52 | },
53 | typo6: {
54 | tag: 'span',
55 | style: css`
56 | font-size: 12px;
57 | line-height: 22px;
58 | font-weight: 400;
59 | letter-spacing: 0.03em;
60 | `,
61 | },
62 | typo7: {
63 | tag: 'span',
64 | style: css`
65 | font-size: 16px;
66 | line-height: 22px;
67 | font-weight: 500;
68 | letter-spacing: 1px;
69 | `,
70 | },
71 | typo8: {
72 | tag: 'span',
73 | style: css`
74 | font-size: 18px;
75 | line-height: 25px;
76 | font-weight: 400;
77 | `,
78 | },
79 | typo9: {
80 | tag: 'span',
81 | style: css`
82 | font-size: 25px;
83 | line-height: 22px;
84 | font-weight: 400;
85 | `,
86 | },
87 | typo10: {
88 | tag: 'span',
89 | style: css`
90 | font-size: 12px;
91 | font-weight: 500;
92 | line-height: 22px;
93 | `,
94 | },
95 | typo11: {
96 | tag: 'span',
97 | style: css`
98 | font-size: 15px;
99 | font-weight: 400;
100 | line-height: 22px;
101 | display: inline-block;
102 | `,
103 | },
104 | typo12: {
105 | tag: 'span',
106 | style: css`
107 | font-size: 46px;
108 | line-height: 54px;
109 | font-weight: 300;
110 | `,
111 | },
112 | typo13: {
113 | tag: 'span',
114 | style: css`
115 | font-size: 26px;
116 | line-height: 30px;
117 | font-weight: 500;
118 | `,
119 | },
120 | // Used in Banner (Cloud banner & api key warning banner)
121 | typo14: {
122 | tag: 'span',
123 | style: css`
124 | font-size: 15px;
125 | line-height: 25px;
126 | font-weight: 500;
127 | `,
128 | },
129 | // Used in Banner
130 | typo15: {
131 | tag: 'span',
132 | style: css`
133 | font-size: 15px;
134 | font-weight: 300;
135 | line-height: 25px;
136 | display: inline-block;
137 | `,
138 | },
139 | }
140 |
141 | const StyledTypography = styled.span`
142 | ${(p) => p.$variant.style};
143 | ${space};
144 | ${color};
145 | `
146 |
147 | const Typography = ({ variant = 'default', children, ...props }) => {
148 | const safeVariant = variants[variant] || variants.default
149 | const { tag = 'span' } = safeVariant
150 | return (
151 |
152 | {children}
153 |
154 | )
155 | }
156 |
157 | Typography.propTypes = {
158 | /**
159 | * Text's variant
160 | */
161 | variant: PropTypes.oneOf([
162 | 'default',
163 | 'typo1',
164 | 'typo2',
165 | 'typo3',
166 | 'typo4',
167 | 'typo5',
168 | 'typo6',
169 | 'typo7',
170 | 'typo8',
171 | 'typo9',
172 | 'typo10',
173 | 'typo11',
174 | 'typo12',
175 | 'typo13',
176 | 'typo14',
177 | 'typo15',
178 | ]),
179 | /**
180 | * Text to be displayed
181 | */
182 | children: PropTypes.node,
183 | }
184 |
185 | export default Typography
186 |
--------------------------------------------------------------------------------
/src/components/icons/AlertCircle.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgAlertCircle = ({ title, titleId, ...props }) => (
3 |
15 | {title ? {title} : null}
16 |
17 |
18 |
19 | )
20 | export default SvgAlertCircle
21 |
--------------------------------------------------------------------------------
/src/components/icons/ArrowDown.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgArrowDown = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
18 |
19 | )
20 | export default SvgArrowDown
21 |
--------------------------------------------------------------------------------
/src/components/icons/Cross.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgCross = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
18 |
25 |
26 | )
27 | export default SvgCross
28 |
--------------------------------------------------------------------------------
/src/components/icons/DiscordLogo.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgDiscordLogo = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
17 |
23 |
24 | )
25 | export default SvgDiscordLogo
26 |
--------------------------------------------------------------------------------
/src/components/icons/DocumentBig.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgDocumentBig = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
15 |
21 |
22 | )
23 | export default SvgDocumentBig
24 |
--------------------------------------------------------------------------------
/src/components/icons/DocumentMedium.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgDocumentMedium = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
15 |
21 |
22 | )
23 | export default SvgDocumentMedium
24 |
--------------------------------------------------------------------------------
/src/components/icons/GithubLogo.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgGithubLogo = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
15 |
16 | )
17 | export default SvgGithubLogo
18 |
--------------------------------------------------------------------------------
/src/components/icons/Indexes.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgIndexes = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
17 |
18 | )
19 | export default SvgIndexes
20 |
--------------------------------------------------------------------------------
/src/components/icons/InterrogationMark.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgInterrogationMark = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
15 |
16 | )
17 | export default SvgInterrogationMark
18 |
--------------------------------------------------------------------------------
/src/components/icons/Key.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgKey = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
17 |
23 |
24 | )
25 | export default SvgKey
26 |
--------------------------------------------------------------------------------
/src/components/icons/KeyBig.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgKeyBig = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
17 |
23 |
24 | )
25 | export default SvgKeyBig
26 |
--------------------------------------------------------------------------------
/src/components/icons/LogoText.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgLogoText = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
15 |
16 | )
17 | export default SvgLogoText
18 |
--------------------------------------------------------------------------------
/src/components/icons/MeilisearchLogo.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgMeilisearchLogo = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
15 |
19 |
23 |
24 |
32 |
33 |
34 |
35 |
43 |
44 |
45 |
46 |
54 |
55 |
56 |
57 |
58 |
59 | )
60 | export default SvgMeilisearchLogo
61 |
--------------------------------------------------------------------------------
/src/components/icons/Picture.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgPicture = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
15 |
16 | )
17 | export default SvgPicture
18 |
--------------------------------------------------------------------------------
/src/components/icons/SearchMedium.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgSearchMedium = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
17 |
18 | )
19 | export default SvgSearchMedium
20 |
--------------------------------------------------------------------------------
/src/components/icons/SearchSmall.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgSearchSmall = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
17 |
18 | )
19 | export default SvgSearchSmall
20 |
--------------------------------------------------------------------------------
/src/components/icons/SettingsBig.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgSettingsBig = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
17 |
18 | )
19 | export default SvgSettingsBig
20 |
--------------------------------------------------------------------------------
/src/components/icons/SettingsMedium.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgSettingsMedium = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
17 |
18 | )
19 | export default SvgSettingsMedium
20 |
--------------------------------------------------------------------------------
/src/components/icons/Speed.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const SvgSpeed = ({ title, titleId, ...props }) => (
3 |
10 | {title ? {title} : null}
11 |
15 |
19 |
20 | )
21 | export default SvgSpeed
22 |
--------------------------------------------------------------------------------
/src/components/icons/heroicons/AcademicHatIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const AcademicHatIcon = () => (
4 |
12 |
17 |
18 | )
19 |
20 | export default AcademicHatIcon
21 |
--------------------------------------------------------------------------------
/src/components/icons/heroicons/ArrowPathIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function ArrowPathIcon() {
4 | return (
5 |
13 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/icons/heroicons/ChatBubbleIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ChatBubbleIcon = () => (
4 |
12 |
17 |
18 | )
19 |
20 | export default ChatBubbleIcon
21 |
--------------------------------------------------------------------------------
/src/components/icons/heroicons/CheckIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function CheckIcon() {
4 | return (
5 |
13 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/icons/heroicons/CloseIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function CloseIcon() {
4 | return (
5 |
13 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/icons/heroicons/LifebuoyIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const LifebuoyIcon = () => (
4 |
12 |
17 |
18 | )
19 |
20 | export default LifebuoyIcon
21 |
--------------------------------------------------------------------------------
/src/components/icons/heroicons/MenuBarsIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const MenuBarsIcon = () => (
4 |
12 |
17 |
18 | )
19 |
20 | export default MenuBarsIcon
21 |
--------------------------------------------------------------------------------
/src/components/icons/index.js:
--------------------------------------------------------------------------------
1 | export { default as AlertCircle } from './AlertCircle'
2 | export { default as ArrowDown } from './ArrowDown'
3 | export { default as Cross } from './Cross'
4 | export { default as DiscordLogo } from './DiscordLogo'
5 | export { default as DocumentBig } from './DocumentBig'
6 | export { default as DocumentMedium } from './DocumentMedium'
7 | export { default as GithubLogo } from './GithubLogo'
8 | export { default as Indexes } from './Indexes'
9 | export { default as InterrogationMark } from './InterrogationMark'
10 | export { default as Key } from './Key'
11 | export { default as KeyBig } from './KeyBig'
12 | export { default as LogoText } from './LogoText'
13 | export { default as MeilisearchLogo } from './MeilisearchLogo'
14 | export { default as Picture } from './Picture'
15 | export { default as SearchMedium } from './SearchMedium'
16 | export { default as SearchSmall } from './SearchSmall'
17 | export { default as SettingsBig } from './SettingsBig'
18 | export { default as SettingsMedium } from './SettingsMedium'
19 | export { default as Speed } from './Speed'
20 |
--------------------------------------------------------------------------------
/src/components/icons/svg/alert-circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/icons/svg/arrow_down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/icons/svg/cross.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/icons/svg/discord-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/icons/svg/document_big.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/components/icons/svg/document_medium.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/components/icons/svg/github_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/icons/svg/indexes.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/icons/svg/interrogation_mark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/icons/svg/key.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/icons/svg/key_big.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/icons/svg/logo_text.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/icons/svg/meilisearch_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/icons/svg/picture.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/icons/svg/search_medium.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/icons/svg/search_small.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/icons/svg/settings_big.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/icons/svg/settings_medium.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/icons/svg/speed.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/context/ApiKeyContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const ApiKeyContext = React.createContext({
4 | apiKey: '',
5 | setApiKey: () => {},
6 | })
7 |
8 | export const ApiKeyProvider = ApiKeyContext.Provider
9 | export const ApiKeyConsumer = ApiKeyContext.Consumer
10 |
11 | export default ApiKeyContext
12 |
--------------------------------------------------------------------------------
/src/context/MeilisearchClientContext.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, createContext, useContext } from 'react'
2 | import { instantMeiliSearch as instantMeilisearch } from '@meilisearch/instant-meilisearch'
3 | import { MeiliSearch as Meilisearch } from 'meilisearch'
4 | import useLocalStorage from 'hooks/useLocalStorage'
5 | import { baseUrl } from 'App'
6 | import clientAgents from 'version/client-agents'
7 |
8 | export const MeilisearchClientContext = createContext({
9 | meilisearchJsClient: '',
10 | setMeilisearchJsClient: () => {},
11 | instantMeilisearchClient: '',
12 | setInstantMeilisearchClient: () => {},
13 | })
14 |
15 | export const MeiliSearchClientProvider = ({ children }) => {
16 | const [apiKey] = useLocalStorage('apiKey')
17 |
18 | const [meilisearchJsClient, setMeilisearchJsClient] = useState(
19 | new Meilisearch({
20 | host: baseUrl,
21 | apiKey,
22 | clientAgents,
23 | })
24 | )
25 | const [instantMeilisearchClient, setInstantMeilisearchClient] = useState(
26 | instantMeilisearch(baseUrl, apiKey, {
27 | primaryKey: 'id',
28 | clientAgents,
29 | }).searchClient
30 | )
31 |
32 | const contextValue = useMemo(
33 | () => ({
34 | meilisearchJsClient,
35 | setMeilisearchJsClient,
36 | instantMeilisearchClient,
37 | setInstantMeilisearchClient,
38 | }),
39 | [
40 | meilisearchJsClient,
41 | setMeilisearchJsClient,
42 | instantMeilisearchClient,
43 | setInstantMeilisearchClient,
44 | ]
45 | )
46 |
47 | return (
48 |
49 | {children}
50 |
51 | )
52 | }
53 |
54 | export const useMeilisearchClientContext = () => {
55 | const context = useContext(MeilisearchClientContext)
56 | if (context === undefined) {
57 | throw new Error(
58 | 'useMeilisearchClientContext must be used within a MeilisearchClientProvider'
59 | )
60 | }
61 | return context
62 | }
63 |
--------------------------------------------------------------------------------
/src/hooks/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { useState } from 'react'
3 |
4 | function useLocalStorage(key, initialValue) {
5 | const [storedValue, setStoredValue] = useState(() => {
6 | try {
7 | const item = window.localStorage.getItem(key)
8 | return item ? JSON.parse(item) : initialValue
9 | } catch (error) {
10 | console.log(error)
11 | return initialValue
12 | }
13 | })
14 |
15 | const setValue = (value) => {
16 | try {
17 | const valueToStore =
18 | value instanceof Function ? value(storedValue) : value
19 | setStoredValue(valueToStore)
20 | window.localStorage.setItem(key, JSON.stringify(valueToStore))
21 | } catch (error) {
22 | console.log(error)
23 | }
24 | }
25 |
26 | return [storedValue, setValue]
27 | }
28 |
29 | export default useLocalStorage
30 |
--------------------------------------------------------------------------------
/src/hooks/useNewsletter.js:
--------------------------------------------------------------------------------
1 | import version from '../version/version'
2 |
3 | const PORTAL_ID = process.env.REACT_APP_HUBSPOT_PORTAL_ID
4 | const FORM_GUID = process.env.REACT_APP_HUBSPOT_FORM_GUID
5 |
6 | const PAGE_NAME =
7 | process.env.NODE_ENV === 'development'
8 | ? `Mini-dashboard (dev)`
9 | : `Mini-dashboard v${version}`
10 |
11 | function getBody({ email, pageName }) {
12 | return {
13 | fields: [
14 | {
15 | objectTypeId: '0-1',
16 | name: 'email',
17 | value: email,
18 | },
19 | ],
20 | context: {
21 | pageName,
22 | },
23 | legalConsentOptions: {
24 | consent: {
25 | consentToProcess: true,
26 | text: 'I agree to allow Meilisearch to store and process my personal data.',
27 | communications: [
28 | {
29 | value: true,
30 | subscriptionTypeId: 999,
31 | text: 'I agree to receive marketing communications from Meilisearch.',
32 | },
33 | ],
34 | },
35 | },
36 | }
37 | }
38 |
39 | export default function useNewsletter() {
40 | const endpoint = `https://api.hsforms.com/submissions/v3/integration/submit/${PORTAL_ID}/${FORM_GUID}`
41 |
42 | const subscribe = (email) =>
43 | fetch(endpoint, {
44 | method: 'POST',
45 | headers: {
46 | 'Content-Type': 'application/json',
47 | },
48 | body: JSON.stringify(
49 | getBody({
50 | email,
51 | pageName: PAGE_NAME,
52 | })
53 | ),
54 | })
55 |
56 | return { subscribe }
57 | }
58 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import { ThemeProvider } from 'styled-components'
4 |
5 | import theme from 'theme'
6 | import App from 'App'
7 | import GlobalStyle from 'GlobalStyle'
8 | import { MeiliSearchClientProvider } from 'context/MeilisearchClientContext'
9 |
10 | const container = document.getElementById('root')
11 | const root = createRoot(container)
12 | root.render(
13 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 |
--------------------------------------------------------------------------------
/src/stories/Badge.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Badge from 'components/Badge'
4 |
5 | export default {
6 | title: 'Components/Badge',
7 | component: Badge,
8 | }
9 |
10 | const Template = (args) =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | children: '4762',
15 | }
16 |
--------------------------------------------------------------------------------
/src/stories/Button.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Button from 'components/Button'
4 | import { DocumentBig, Key } from 'components/icons'
5 |
6 | export default {
7 | title: 'Components/Button',
8 | component: Button,
9 | argTypes: {
10 | variant: {
11 | control: {
12 | type: 'select',
13 | options: ['default', 'filled', 'bordered', 'link', 'grayscale'],
14 | },
15 | },
16 | size: {
17 | control: {
18 | type: 'select',
19 | options: ['medium', 'small'],
20 | },
21 | },
22 | },
23 | }
24 |
25 | const Template = (args) => {
26 | const [toggled, setToggled] = React.useState(false)
27 | return (
28 | setToggled((prevtoggled) => !prevtoggled)}
31 | {...args}
32 | />
33 | )
34 | }
35 |
36 | export const Default = Template.bind({})
37 | Default.args = {
38 | children: 'I’m a Button',
39 | }
40 |
41 | export const WithIcon = Template.bind({})
42 | WithIcon.args = {
43 | children: 'I’m a Button',
44 | icon: ,
45 | }
46 |
47 | export const SizeSmall = Template.bind({})
48 | SizeSmall.args = {
49 | children: 'I’m a Button',
50 | size: 'small',
51 | }
52 |
53 | export const VariantFilled = Template.bind({})
54 | VariantFilled.args = {
55 | children: 'I’m a Button',
56 | size: 'small',
57 | variant: 'filled',
58 | }
59 |
60 | export const VariantBordered = Template.bind({})
61 | VariantBordered.args = {
62 | children: 'I’m a Button',
63 | size: 'small',
64 | variant: 'bordered',
65 | }
66 |
67 | export const VariantLink = Template.bind({})
68 | VariantLink.args = {
69 | children: 'I’m a Button',
70 | size: 'small',
71 | variant: 'link',
72 | }
73 |
74 | export const VariantGrayscale = Template.bind({})
75 | VariantGrayscale.args = {
76 | children: 'I’m a Button',
77 | icon: ,
78 | size: 'small',
79 | variant: 'grayscale',
80 | toggable: true,
81 | }
82 |
--------------------------------------------------------------------------------
/src/stories/Card.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Card from 'components/Card'
4 |
5 | export default {
6 | title: 'Components/Card',
7 | component: Card,
8 | }
9 |
10 | const Template = (args) =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | children: 'I’m a Card',
15 | }
16 |
--------------------------------------------------------------------------------
/src/stories/Checkbox.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Badge from 'components/Badge'
4 | import Checkbox from 'components/Checkbox'
5 | import Typography from 'components/Typography'
6 |
7 | export default {
8 | title: 'Components/Checkbox',
9 | component: Checkbox,
10 | }
11 |
12 | const Template = (args) => {
13 | const [checked, setChecked] = React.useState(false)
14 | return (
15 | setChecked(!checked)}
18 | {...args}
19 | />
20 | )
21 | }
22 |
23 | export const Default = Template.bind({})
24 | Default.args = {
25 | children: (
26 | <>
27 |
28 | Carrot cake
29 |
30 | 12349
31 | >
32 | ),
33 | }
34 |
--------------------------------------------------------------------------------
/src/stories/Container.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Container from 'components/Container'
3 |
4 | export default {
5 | title: 'Components/Container',
6 | component: Container,
7 | decorators: [
8 | (Story) => (
9 |
10 |
11 |
12 | ),
13 | ],
14 | }
15 |
16 | const Template = (args) =>
17 |
18 | export const Default = Template.bind({})
19 | Default.args = {
20 | children: (
21 |
22 | I’m a Container with a max-width
23 |
24 | ),
25 | }
26 |
--------------------------------------------------------------------------------
/src/stories/EmptyView.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import EmptyView from 'components/EmptyView'
3 | import Typography from 'components/Typography'
4 |
5 | export default {
6 | title: 'Components/EmptyView',
7 | component: EmptyView,
8 | }
9 |
10 | const Template = (args) =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | children: (
15 |
21 | There’s no document in the selected index
22 |
23 | ),
24 | buttonLink: 'https://docs.meilisearch.com/reference/api/documents.html',
25 | }
26 |
--------------------------------------------------------------------------------
/src/stories/IconButton.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import IconButton from 'components/IconButton'
4 | import { Cross, InterrogationMark } from 'components/icons'
5 |
6 | export default {
7 | title: 'Components/IconButton',
8 | component: IconButton,
9 | argTypes: {
10 | variant: {
11 | control: {
12 | type: 'select',
13 | options: ['default', 'bordered'],
14 | },
15 | },
16 | },
17 | }
18 |
19 | const Template = (args) =>
20 |
21 | export const Default = Template.bind({})
22 | Default.args = {
23 | children: ,
24 | }
25 |
26 | export const Bordered = Template.bind({})
27 | Bordered.args = {
28 | style: { width: 24, height: 24 },
29 | variant: 'bordered',
30 | children: ,
31 | }
32 |
--------------------------------------------------------------------------------
/src/stories/Icons.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Box from 'components/Box'
4 | import {
5 | MeilisearchLogo as MeilisearchLogoIcon,
6 | ArrowDown as ArrowDownIcon,
7 | Cross as CrossIcon,
8 | DiscordLogo as DiscordLogoIcon,
9 | DocumentBig as DocumentBigIcon,
10 | DocumentMedium as DocumentMediumIcon,
11 | GithubLogo as GithubLogoIcon,
12 | Indexes as IndexesIcon,
13 | InterrogationMark as InterrogationMarkIcon,
14 | Key as KeyIcon,
15 | KeyBig as KeyBigIcon,
16 | LogoText as LogoTextIcon,
17 | Picture as PictureIcon,
18 | SearchMedium as SearchMediumIcon,
19 | SearchSmall as SearchSmallIcon,
20 | SettingsMedium as SettingsMediumIcon,
21 | SettingsBig as SettingsBigIcon,
22 | Speed as SpeedIcon,
23 | } from 'components/icons'
24 |
25 | export default {
26 | title: 'Components/Icons',
27 | }
28 |
29 | const Template = (args) =>
30 |
31 | export const MeilisearchLogo = Template.bind({})
32 | MeilisearchLogo.args = {
33 | children: ,
34 | }
35 |
36 | export const ArrowDown = Template.bind({})
37 | ArrowDown.args = {
38 | children: ,
39 | }
40 |
41 | export const Cross = Template.bind({})
42 | Cross.args = {
43 | children: ,
44 | }
45 |
46 | export const DiscordLogo = Template.bind({})
47 | DiscordLogo.args = {
48 | children: ,
49 | }
50 |
51 | export const DocumentBig = Template.bind({})
52 | DocumentBig.args = {
53 | children: ,
54 | }
55 |
56 | export const DocumentMedium = Template.bind({})
57 | DocumentMedium.args = {
58 | children: ,
59 | }
60 |
61 | export const Github = Template.bind({})
62 | Github.args = {
63 | children: ,
64 | }
65 |
66 | export const Indexes = Template.bind({})
67 | Indexes.args = {
68 | children: ,
69 | }
70 |
71 | export const InterrogationMark = Template.bind({})
72 | InterrogationMark.args = {
73 | children: ,
74 | }
75 |
76 | export const Key = Template.bind({})
77 | Key.args = {
78 | children: ,
79 | }
80 |
81 | export const KeyBig = Template.bind({})
82 | KeyBig.args = {
83 | children: ,
84 | }
85 |
86 | export const LogoText = Template.bind({})
87 | LogoText.args = {
88 | children: ,
89 | }
90 |
91 | export const Picture = Template.bind({})
92 | Picture.args = {
93 | children: ,
94 | }
95 |
96 | export const SearchSmall = Template.bind({})
97 | SearchSmall.args = {
98 | children: ,
99 | }
100 |
101 | export const SearchMedium = Template.bind({})
102 | SearchMedium.args = {
103 | children: ,
104 | }
105 |
106 | export const SettingsMedium = Template.bind({})
107 | SettingsMedium.args = {
108 | children: ,
109 | }
110 |
111 | export const SettingsBig = Template.bind({})
112 | SettingsBig.args = {
113 | children: ,
114 | }
115 |
116 | export const Speed = Template.bind({})
117 | Speed.args = {
118 | children: ,
119 | }
120 |
--------------------------------------------------------------------------------
/src/stories/Input.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Input from 'components/Input'
4 | import { SearchMedium } from 'components/icons'
5 |
6 | export default {
7 | title: 'Components/Input',
8 | component: Input,
9 | }
10 |
11 | const Template = (args) => (
12 |
13 | )
14 |
15 | export const Default = Template.bind({})
16 |
17 | export const WithIcon = Template.bind({})
18 | WithIcon.args = {
19 | icon: ,
20 | }
21 |
--------------------------------------------------------------------------------
/src/stories/Link.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Link from 'components/Link'
4 |
5 | export default {
6 | title: 'Components/Link',
7 | component: Link,
8 | }
9 |
10 | const Template = (args) =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | href: 'https://docs.meilisearch.com/',
15 | children: 'Go to documentation',
16 | }
17 |
--------------------------------------------------------------------------------
/src/stories/Modal.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDialogState, DialogDisclosure } from 'reakit/Dialog'
3 |
4 | import Modal from 'components/Modal'
5 |
6 | export default {
7 | title: 'Components/Modal',
8 | component: Modal,
9 | }
10 |
11 | const Template = (args) => {
12 | const dialog = useDialogState({ animated: true })
13 | return (
14 | <>
15 | Click me
16 |
17 | >
18 | )
19 | }
20 |
21 | export const Default = Template.bind({})
22 | Default.args = {
23 | title: 'I’m a title',
24 | children: I’m the Modal’s content
,
25 | }
26 |
--------------------------------------------------------------------------------
/src/stories/Select.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Select from 'components/Select'
4 | import NoSelectOption from 'components/NoSelectOption'
5 | import { Indexes } from 'components/icons'
6 |
7 | export default {
8 | title: 'Components/Select',
9 | component: Select,
10 | }
11 |
12 | const options = [
13 | {
14 | uid: 'pokemon',
15 | stats: {
16 | numberOfDocuments: 809,
17 | },
18 | },
19 | {
20 | uid: 'movies',
21 | stats: {
22 | numberOfDocuments: 19546,
23 | },
24 | },
25 | ]
26 |
27 | const Template = (args) => {
28 | const [currentOption, setCurrentOption] = React.useState()
29 | return (
30 |
35 | )
36 | }
37 |
38 | export const Default = Template.bind({})
39 | Default.args = {
40 | options,
41 | }
42 |
43 | export const WithIcon = Template.bind({})
44 | WithIcon.args = {
45 | options,
46 | icon: ,
47 | }
48 |
49 | export const WithoutOptions = Template.bind({})
50 | WithoutOptions.args = {
51 | options: null,
52 | icon: ,
53 | noOptionComponent: ,
54 | }
55 |
--------------------------------------------------------------------------------
/src/stories/Sidebar.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Box from 'components/Box'
4 | import Sidebar from 'components/Sidebar'
5 | import { SettingsBig } from 'components/icons'
6 |
7 | export default {
8 | title: 'Components/Sidebar',
9 | component: Sidebar,
10 | parameters: {
11 | layout: 'fullscreen',
12 | },
13 | }
14 |
15 | const Template = (args) => (
16 |
17 |
18 |
19 | )
20 |
21 | export const Default = Template.bind({})
22 | Default.args = {
23 | children: (
24 |
25 | I’m a sidebar
26 |
27 | ),
28 | }
29 |
30 | export const WithIcon = Template.bind({})
31 | WithIcon.args = {
32 | sidebarIcon: ,
33 | children: (
34 |
35 | I’m a sidebar
36 |
37 | ),
38 | }
39 |
40 | export const DefaultClosed = Template.bind({})
41 | DefaultClosed.args = {
42 | visible: false,
43 | children: (
44 |
45 | I’m a sidebar
46 |
47 | ),
48 | }
49 |
--------------------------------------------------------------------------------
/src/stories/Stats.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Stats from 'components/Stats'
4 |
5 | export default {
6 | title: 'Components/Stats',
7 | component: Stats,
8 | }
9 |
10 | const Template = (args) =>
11 |
12 | export const Default = Template.bind({})
13 | Default.args = {
14 | nbHits: 19546,
15 | processingTimeMS: 2,
16 | nbResults: 19546,
17 | }
18 |
19 | export const Imprecise = Template.bind({})
20 | Imprecise.args = {
21 | nbHits: 19546,
22 | processingTimeMS: 2,
23 | nbResults: 19500,
24 | }
25 |
--------------------------------------------------------------------------------
/src/stories/Toggle.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Toggle from 'components/Toggle'
4 | import { DocumentBig, Picture } from 'components/icons'
5 |
6 | export default {
7 | title: 'Components/Toggle',
8 | component: Toggle,
9 | }
10 |
11 | const Template = (args) =>
12 |
13 | export const Default = Template.bind({})
14 | Default.args = {
15 | onLabel: (
16 | <>
17 |
18 | Fancy
19 | >
20 | ),
21 | offLabel: (
22 | <>
23 |
24 | Json
25 | >
26 | ),
27 | onChange: () => {},
28 | }
29 |
--------------------------------------------------------------------------------
/src/stories/Typography.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Typography from 'components/Typography'
4 |
5 | export default {
6 | title: 'Components/Typography',
7 | component: Typography,
8 | argTypes: {
9 | variant: {
10 | control: {
11 | type: 'select',
12 | options: [
13 | 'default',
14 | 'typo1',
15 | 'typo2',
16 | 'typo3',
17 | 'typo4',
18 | 'typo5',
19 | 'typo6',
20 | 'typo7',
21 | 'typo8',
22 | 'typo9',
23 | 'typo10',
24 | 'typo11',
25 | 'typo12',
26 | 'typo13',
27 | ],
28 | },
29 | },
30 | },
31 | }
32 |
33 | const Template = (args) =>
34 |
35 | export const Default = Template.bind({})
36 | Default.args = {
37 | children: 'I’m a Typography with default variant',
38 | }
39 |
40 | export const typo1 = Template.bind({})
41 | typo1.args = {
42 | variant: 'typo1',
43 | children: 'I’m a Typography with typo1 variant',
44 | }
45 |
46 | export const typo2 = Template.bind({})
47 | typo2.args = {
48 | variant: 'typo2',
49 | children: 'I’m a Typography with typo2 variant',
50 | }
51 |
52 | export const Typo3 = Template.bind({})
53 | Typo3.args = {
54 | variant: 'typo3',
55 | children: 'I’m a Typography with typo3 variant',
56 | }
57 |
58 | export const Typo4 = Template.bind({})
59 | Typo4.args = {
60 | variant: 'typo4',
61 | children: 'I’m a Typography with typo4 variant',
62 | }
63 |
64 | export const Typo5 = Template.bind({})
65 | Typo5.args = {
66 | variant: 'typo5',
67 | children: 'I’m a Typography with typo5 variant',
68 | }
69 |
70 | export const Typo6 = Template.bind({})
71 | Typo6.args = {
72 | variant: 'typo6',
73 | children: 'I’m a Typography with typo6 variant',
74 | }
75 |
76 | export const Typo7 = Template.bind({})
77 | Typo7.args = {
78 | variant: 'typo7',
79 | children: 'I’m a Typography with typo7 variant',
80 | }
81 |
82 | export const Typo8 = Template.bind({})
83 | Typo8.args = {
84 | variant: 'typo8',
85 | children: 'I’m a Typography with typo8 variant',
86 | }
87 |
88 | export const Typo9 = Template.bind({})
89 | Typo9.args = {
90 | variant: 'typo9',
91 | children: 'I’m a Typography with typo9 variant',
92 | }
93 |
94 | export const Typo10 = Template.bind({})
95 | Typo10.args = {
96 | variant: 'typo10',
97 | children: 'I’m a Typography with typo10 variant',
98 | }
99 |
100 | export const Typo11 = Template.bind({})
101 | Typo11.args = {
102 | variant: 'typo11',
103 | children: 'I’m a Typography with typo11 variant',
104 | }
105 |
106 | export const Typo12 = Template.bind({})
107 | Typo12.args = {
108 | variant: 'typo12',
109 | children: 'I’m a Typography with typo12 variant',
110 | }
111 |
112 | export const Typo13 = Template.bind({})
113 | Typo13.args = {
114 | variant: 'typo13',
115 | children: 'I’m a Typography with typo13 variant',
116 | }
117 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | const theme = {
2 | colors: {
3 | white: '#FFFFFF',
4 | main: {
5 | default: '#E41359',
6 | hover: '#CA1B53',
7 | dark: '#73313D',
8 | light: '#FFDBE7',
9 | lighter: '#FDEEF3',
10 | },
11 | gray: [
12 | '#39486E', // 0
13 | '#4F5C7E',
14 | '#606C8B', // 2
15 | '#737E99',
16 | '#838DA5', // 4
17 | '#959DB3',
18 | '#A7AEC0', // 6
19 | '#BBC1CF',
20 | '#CBCFDB', // 8
21 | '#E4E7EE',
22 | '#EDEEF7', // 10
23 | '#FAFBFE',
24 | ],
25 | jsonVue: {
26 | string: '#86C3B8',
27 | keys: '#39486E',
28 | badgeBg: '#EDEEF7',
29 | badgeFg: '#82a0bc',
30 | keyNumber: '#39486E',
31 | arrows: '#E41359',
32 | integers: '#86C3B8',
33 | },
34 | error: {
35 | text: '#dc2626',
36 | },
37 | success: {
38 | text: '#15803d',
39 | background: '#f0fdf4',
40 | border: '#86efac',
41 | },
42 | },
43 | // 0, 1, 2, 3, 4, 5, 6, 7, 8
44 | space: [0, 4, 8, 16, 32, 64, 128, 256, 512],
45 | sizes: {
46 | rightPanel: '430px',
47 | },
48 | fontSizes: [12, 14, 16, 20, 24, 32, 48, 64, 72],
49 | breakpoints: {
50 | large: '1440',
51 | },
52 | }
53 |
54 | export const jsonTheme = {
55 | base00: 'white',
56 | base01: '#ddd',
57 | base02: theme.colors.jsonVue.badgeBg,
58 | base03: '#444',
59 | base04: 'purple',
60 | base05: '#444',
61 | base06: '#444',
62 | base07: theme.colors.jsonVue.keys,
63 | base08: '#444',
64 | base09: theme.colors.jsonVue.string,
65 | base0A: theme.colors.jsonVue.badgeFg,
66 | base0B: theme.colors.jsonVue.string,
67 | base0C: theme.colors.jsonVue.keyNumber,
68 | base0D: theme.colors.jsonVue.arrows,
69 | base0E: theme.colors.jsonVue.arrows,
70 | base0F: theme.colors.jsonVue.integers,
71 | }
72 |
73 | export default theme
74 |
--------------------------------------------------------------------------------
/src/utils/getIndexesListWithStats.js:
--------------------------------------------------------------------------------
1 | const getIndexesListWithStats = async (meilisearchJsClient) => {
2 | const res = await meilisearchJsClient.getStats()
3 | const array = Object.entries(res.indexes)
4 | const indexesList = array
5 | .reduce((prev, curr) => {
6 | const currentOption = { uid: curr[0], stats: curr[1] }
7 | return [...prev, currentOption]
8 | }, [])
9 | .sort((a, b) => a.uid.localeCompare(b.uid))
10 | return indexesList
11 | }
12 |
13 | export default getIndexesListWithStats
14 |
--------------------------------------------------------------------------------
/src/utils/hasAnApiKeySet.js:
--------------------------------------------------------------------------------
1 | import { MeiliSearch as Meilisearch } from 'meilisearch'
2 | import { baseUrl } from 'App'
3 | import clientAgents from 'version/client-agents'
4 |
5 | const hasAnApiKeySet = async () => {
6 | try {
7 | const tempClient = new Meilisearch({ host: baseUrl, clientAgents })
8 | await tempClient.getIndexes()
9 | return false
10 | } catch (err) {
11 | return err.cause.code === 'missing_authorization_header'
12 | }
13 | }
14 |
15 | export default hasAnApiKeySet
16 |
--------------------------------------------------------------------------------
/src/utils/isCloudBannerEnabled.js:
--------------------------------------------------------------------------------
1 | /** @const {string} Name of the query parameter that controls banner visibility */
2 | const QUERY_PARAM_NAME = 'cloud_banner'
3 |
4 | /**
5 | * Checks if the cloud banner should be enabled based on URL query parameters
6 | * @returns {boolean} True if banner should be shown, false if explicitly disabled via query param
7 | */
8 | const isBannerEnabled = () => {
9 | const urlParams = new URLSearchParams(window.location.search)
10 | const cloudBannerQueryParam = urlParams.get(QUERY_PARAM_NAME)
11 | return cloudBannerQueryParam !== 'false'
12 | }
13 |
14 | export default isBannerEnabled
15 |
--------------------------------------------------------------------------------
/src/utils/shouldDisplayApiKeyModal.js:
--------------------------------------------------------------------------------
1 | const shouldDisplayApiKeyModal = async (meilisearchJsClient) => {
2 | try {
3 | await meilisearchJsClient.getIndexes()
4 | return false
5 | } catch (err) {
6 | return true
7 | }
8 | }
9 |
10 | export default shouldDisplayApiKeyModal
11 |
--------------------------------------------------------------------------------
/src/version/client-agents.js:
--------------------------------------------------------------------------------
1 | import PACKAGE_VERSION from './version'
2 |
3 | export default [`Meilisearch mini-dashboard (v${PACKAGE_VERSION})`]
4 |
--------------------------------------------------------------------------------
/src/version/version.js:
--------------------------------------------------------------------------------
1 | export default '0.2.19'
2 |
--------------------------------------------------------------------------------
/validate-env.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const requiredEnv = ['REACT_APP_API_URL', 'REACT_APP_ANOTHER_ENV_VAR']
4 |
5 | const missingEnv = requiredEnv.filter((env) => !process.env[env])
6 |
7 | if (missingEnv.length > 0) {
8 | console.error(
9 | `Missing required environment variables: ${missingEnv.join(', ')}`
10 | )
11 | process.exit(1)
12 | }
13 | console.log('All required environment variables are set.')
14 |
--------------------------------------------------------------------------------
/version-script.js:
--------------------------------------------------------------------------------
1 | /*
2 | * The following script changes the version of the mini-dashboard
3 | * in the file `src/version.js
4 | */
5 |
6 | const fs = require('fs')
7 |
8 | // Fetch the current version in the package.json
9 | const { version } = require('./package.json')
10 |
11 | // Creates content of the version.js file
12 | const versionFile = `export default '${version}'\n`
13 | // Write the content inside ./src/version.js
14 | fs.writeFileSync('./src/version/version.js', versionFile)
15 |
--------------------------------------------------------------------------------