├── .nvmrc ├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .prettierrc ├── CODEOWNERS ├── packages ├── search-ui │ ├── .npmrc │ ├── src │ │ ├── constants.ts │ │ ├── test │ │ │ ├── setupTests.js │ │ │ └── sharedTests.js │ │ ├── __tests__ │ │ │ ├── A11yNotifications.ssr.test.ts │ │ │ ├── actions │ │ │ │ └── trackAutocompleteSuggestionClickThrough.test.ts │ │ │ └── A11yNotifications.test.ts │ │ ├── actions │ │ │ ├── reset.ts │ │ │ ├── setCurrent.ts │ │ │ ├── setResultsPerPage.ts │ │ │ ├── clearFilters.ts │ │ │ ├── setSort.ts │ │ │ ├── a11yNotify.ts │ │ │ ├── trackAutocompleteSuggestionClickThrough.ts │ │ │ ├── setFilter.ts │ │ │ ├── removeFilter.ts │ │ │ ├── trackClickThrough.ts │ │ │ └── trackAutocompleteClickThrough.ts │ │ ├── index.ts │ │ ├── queryString.ts │ │ ├── RequestSequencer.ts │ │ └── preserveTypesEncoder.ts │ ├── NOTICE.txt │ ├── tsconfig.json │ ├── jest.config.js │ └── package.json ├── react-search-ui │ ├── .npmrc │ ├── __mocks__ │ │ └── styleMock.js │ ├── src │ │ ├── hooks │ │ │ └── index.ts │ │ ├── containers │ │ │ ├── __tests__ │ │ │ │ └── __snapshots__ │ │ │ │ │ ├── Result.test.tsx.snap │ │ │ │ │ ├── Paging.test.tsx.snap │ │ │ │ │ ├── SearchBox.test.tsx.snap │ │ │ │ │ ├── Sorting.test.tsx.snap │ │ │ │ │ ├── ErrorBoundary.test.tsx.snap │ │ │ │ │ ├── PagingInfo.test.tsx.snap │ │ │ │ │ ├── Facet.test.tsx.snap │ │ │ │ │ ├── Results.test.tsx.snap │ │ │ │ │ └── ResultsPerPage.test.tsx.snap │ │ │ ├── index.ts │ │ │ ├── Paging.tsx │ │ │ ├── PagingInfo.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── ResultsPerPage.tsx │ │ │ ├── Result.tsx │ │ │ └── Results.tsx │ │ ├── setupTests.js │ │ ├── SearchContext.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── A11yNotifications.ts │ │ ├── __tests__ │ │ │ ├── A11yNotifications.test.ts │ │ │ ├── helpers.test.ts │ │ │ └── SearchContext.test.tsx │ │ ├── WithSearchRenderProps.tsx │ │ └── types │ │ │ └── index.ts │ ├── NOTICE.txt │ ├── tsup.config.ts │ ├── tsconfig.json │ ├── README.md │ ├── jest.config.js │ └── package.json ├── react-search-ui-views │ ├── .npmrc │ ├── __mocks__ │ │ └── styleMock.js │ ├── src │ │ ├── layouts │ │ │ ├── index.ts │ │ │ ├── Layout.tsx │ │ │ └── LayoutSidebar.tsx │ │ ├── styles │ │ │ ├── styles.scss │ │ │ └── themes │ │ │ │ └── reference-ui │ │ │ │ ├── components │ │ │ │ ├── _results-container.scss │ │ │ │ ├── _paging-info.scss │ │ │ │ ├── _sorting.scss │ │ │ │ ├── _results-per-page.scss │ │ │ │ ├── _error-boundary.scss │ │ │ │ ├── _boolean-facet.scss │ │ │ │ ├── _facets.scss │ │ │ │ ├── _paging.scss │ │ │ │ ├── _single-links-facet.scss │ │ │ │ └── _multi-checkbox-facet.scss │ │ │ │ ├── _base.scss │ │ │ │ └── modules │ │ │ │ ├── _mixins.scss │ │ │ │ └── _variables.scss │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── Facets.test.tsx.snap │ │ │ │ ├── Results.test.tsx.snap │ │ │ │ ├── ErrorBoundary.test.tsx.snap │ │ │ │ ├── PagingInfo.test.tsx.snap │ │ │ │ ├── Paging.test.tsx.snap │ │ │ │ ├── BooleanFacet.test.tsx.snap │ │ │ │ └── SearchBox.test.tsx.snap │ │ │ ├── layouts │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── LayoutSidebar.test.tsx.snap │ │ │ │ ├── LayoutSidebar.test.tsx │ │ │ │ └── Layout.test.tsx │ │ │ ├── view-helpers │ │ │ │ └── appendClassName.test.ts │ │ │ ├── Facets.test.tsx │ │ │ ├── Results.test.tsx │ │ │ ├── PagingInfo.test.tsx │ │ │ ├── Paging.test.tsx │ │ │ ├── ResultsPerPage.test.tsx │ │ │ ├── ErrorBoundary.test.tsx │ │ │ └── Sorting.test.tsx │ │ ├── view-helpers │ │ │ ├── index.ts │ │ │ ├── getFilterValueDisplay.ts │ │ │ ├── appendClassName.ts │ │ │ └── getUrlSanitizer.ts │ │ ├── Facets.tsx │ │ ├── setupTests.js │ │ ├── SearchInput.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── index.tsx │ │ ├── Results.tsx │ │ ├── Paging.tsx │ │ └── PagingInfo.tsx │ ├── NOTICE.txt │ ├── .storybook │ │ ├── addons.js │ │ └── config.js │ ├── postcss.config.js │ ├── tsconfig.json │ ├── jest.config.js │ ├── tsup.config.ts │ └── README.md ├── search-ui-analytics-plugin │ ├── .npmrc │ ├── NOTICE.txt │ ├── jest.config.js │ ├── tsconfig.json │ └── package.json ├── search-ui-app-search-connector │ ├── .npmrc │ ├── src │ │ ├── index.ts │ │ └── buildResponseAdapterOptions.ts │ ├── NOTICE.txt │ ├── bin │ │ ├── watch-js │ │ └── build-js │ ├── jest.config.js │ ├── tsconfig-cjs.json │ ├── tsconfig.json │ └── package.json ├── search-ui-site-search-connector │ ├── .npmrc │ ├── src │ │ ├── index.ts │ │ ├── responseAdapter.ts │ │ ├── request.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── responseAdapters.ts │ │ └── __tests__ │ │ │ └── example-response.json │ ├── NOTICE.txt │ ├── jest.config.js │ ├── tsconfig.json │ └── package.json ├── search-ui-workplace-search-connector │ ├── .npmrc │ ├── src │ │ ├── index.ts │ │ └── buildResponseAdapterOptions.ts │ ├── NOTICE.txt │ ├── bin │ │ ├── watch-js │ │ └── build-js │ ├── tsconfig-cjs.json │ ├── jest.config.js │ └── tsconfig.json ├── search-ui-engines-connector │ ├── NOTICE.txt │ ├── bin │ │ ├── watch-js │ │ └── build-js │ ├── jest.config.js │ ├── tsconfig.json │ ├── src │ │ ├── handlers │ │ │ ├── SearchkitModule.ts │ │ │ ├── search │ │ │ │ ├── Query.ts │ │ │ │ └── Response.ts │ │ │ ├── transporter.ts │ │ │ ├── common.ts │ │ │ └── __test__ │ │ │ │ └── transporter.test.ts │ │ └── types.ts │ └── README.md └── search-ui-elasticsearch-connector │ ├── NOTICE.txt │ ├── src │ ├── api-proxy.ts │ ├── index.ts │ ├── __tests__ │ │ ├── index.test.ts │ │ ├── utils.test.ts │ │ └── utils.test.node.ts │ ├── handlers │ │ └── handleSearch.ts │ ├── queryBuilders │ │ ├── __tests__ │ │ │ └── BaseQueryBuilder.test.ts │ │ └── SuggestionsAutocompleteBuilder.ts │ └── types.ts │ ├── tsup.config.ts │ ├── tsconfig.json │ ├── jest.config.js │ └── jest.node.config.js ├── NOTICE.txt ├── docs ├── images │ ├── screenshot.png │ ├── ecommerce │ │ ├── carousel │ │ │ └── carousel.png │ │ ├── overview │ │ │ └── overview.png │ │ ├── category-page │ │ │ ├── facets.png │ │ │ ├── sorting.png │ │ │ ├── facet-views.png │ │ │ └── category-page.png │ │ ├── autocomplete │ │ │ ├── query-results-es-shop.png │ │ │ ├── query-suggestions-amazon.png │ │ │ ├── federated-search-products.png │ │ │ ├── query-suggestions-es-shop.png │ │ │ ├── query-suggestions-multiple-sources.png │ │ │ └── query-suggestions-popular-queries.png │ │ └── product-detail-page │ │ │ ├── product-detail-page.png │ │ │ └── cross-sell-recommendations.png │ ├── app-search-tutorial │ │ ├── completed-ui.png │ │ ├── credentials.png │ │ ├── initial-cra.png │ │ ├── kibana-home.png │ │ ├── create-engine.png │ │ ├── ent-search-home.png │ │ ├── paste-json-data.png │ │ └── configure-schema.png │ ├── elasticsearch-tutorial │ │ ├── api-keys.jpeg │ │ ├── search-ui.jpeg │ │ ├── api-key-view.jpeg │ │ ├── copy-cloud-id.jpg │ │ ├── cors-settings.png │ │ ├── create-index.jpeg │ │ ├── edit-settings.png │ │ ├── copy-es-endpoint.jpeg │ │ └── update-mapping.jpeg │ ├── workplace-search-tutorial │ │ ├── authorize.png │ │ ├── endpoints.png │ │ ├── oauth-application.png │ │ └── search-ui-results.png │ └── guides-analyzing-performance │ │ ├── dashboard.png │ │ ├── server-status.png │ │ ├── events-request.png │ │ └── integrations-server.png ├── reference │ ├── images │ │ ├── facets.png │ │ ├── sorting.png │ │ ├── api-keys.jpeg │ │ ├── authorize.png │ │ ├── carousel.png │ │ ├── dashboard.png │ │ ├── endpoints.png │ │ ├── search-ui.jpeg │ │ ├── completed-ui.png │ │ ├── credentials.png │ │ ├── facet-views.png │ │ ├── initial-cra.png │ │ ├── kibana-home.png │ │ ├── api-key-view.jpeg │ │ ├── category-page.png │ │ ├── copy-cloud-id.jpg │ │ ├── cors-settings.png │ │ ├── create-engine.png │ │ ├── create-index.jpeg │ │ ├── edit-settings.png │ │ ├── ent-search-home.png │ │ ├── events-request.png │ │ ├── server-status.png │ │ ├── update-mapping.jpeg │ │ ├── configure-schema.png │ │ ├── oauth-application.png │ │ ├── search-ui-results.png │ │ ├── integrations-server.png │ │ ├── product-detail-page.png │ │ ├── query-results-es-shop.png │ │ ├── federated-search-products.png │ │ ├── query-suggestions-amazon.png │ │ ├── query-suggestions-es-shop.png │ │ ├── cross-sell-recommendations.png │ │ ├── query-suggestions-multiple-sources.png │ │ └── query-suggestions-popular-queries.png │ ├── known-issues.md │ ├── guides-debugging.md │ ├── api-react-use-search.md │ ├── guides-using-search-as-you-type.md │ ├── guides-adding-search-bar-to-header.md │ ├── advanced-usage.md │ ├── tutorials.md │ ├── solutions-ecommerce-product-detail-page.md │ └── api-core-index.md ├── docset.yml └── README.md ├── examples ├── vue │ ├── public │ │ └── favicon.ico │ ├── .vscode │ │ └── extensions.json │ ├── src │ │ ├── main.js │ │ ├── data │ │ │ └── README.md │ │ ├── assets │ │ │ └── logo.svg │ │ ├── components │ │ │ ├── SearchSort.vue │ │ │ ├── SearchResultsPerPage.vue │ │ │ ├── SearchResults.vue │ │ │ ├── SearchPagingInfo.vue │ │ │ ├── SearchHeader.vue │ │ │ └── SearchFacet.vue │ │ ├── App.vue │ │ └── searchConfig.js │ ├── .eslintrc.cjs │ ├── index.html │ ├── vite.config.js │ ├── .gitignore │ ├── README.md │ ├── cleanseCards.js │ └── package.json └── sandbox │ ├── public │ └── favicon.png │ ├── postcss.config.cjs │ ├── tailwind.config.js │ ├── src │ ├── components │ │ ├── ExampleCard.tsx │ │ ├── UseCaseCard.tsx │ │ ├── ApiCard.tsx │ │ └── TabTrigger.tsx │ ├── main.tsx │ ├── pages │ │ ├── search-bar-in-header │ │ │ ├── index.jsx │ │ │ └── Header.jsx │ │ ├── customizing-styles-and-html │ │ │ ├── ClearFilters.jsx │ │ │ └── custom.css │ │ └── ecommerce │ │ │ ├── styles.scss │ │ │ ├── components │ │ │ └── ProductCarousel │ │ │ │ ├── config.js │ │ │ │ └── index.jsx │ │ │ └── index.jsx │ └── styles.css │ ├── vite.config.js │ ├── .gitignore │ ├── tsconfig.json │ ├── index.html │ ├── .codesandbox │ └── tasks.json │ └── eslint.config.js ├── .vscode └── settings.json ├── .ci ├── jobs │ ├── defaults.yml │ ├── searchui.yml │ └── search-ui.yml └── pipelines │ ├── searchui.groovy │ └── search-ui.groovy ├── .gitignore ├── .github ├── workflows │ ├── docs-cleanup.yml │ ├── docs-build.yml │ └── docs-preview.yml └── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── SECURITY.txt ├── tsconfig.base.json ├── tsup.config.ts ├── pull_request_template.md ├── lerna.json ├── .eslintrc.js └── .circleci └── config.yml /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.16.0 -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none" 3 | } 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @elastic/search-experiences-team 2 | -------------------------------------------------------------------------------- /packages/search-ui/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /packages/react-search-ui/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /packages/search-ui-analytics-plugin/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /packages/react-search-ui/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /packages/search-ui-workplace-search-connector/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useSearch } from "./useSearch"; 2 | -------------------------------------------------------------------------------- /packages/search-ui/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const INVALID_CREDENTIALS = "Invalid credentials"; 2 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Layout } from "./Layout"; 2 | -------------------------------------------------------------------------------- /packages/search-ui/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/screenshot.png -------------------------------------------------------------------------------- /packages/react-search-ui/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./AppSearchAPIConnector"; 2 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SiteSearchAPIConnector"; 2 | -------------------------------------------------------------------------------- /examples/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/examples/vue/public/favicon.ico -------------------------------------------------------------------------------- /packages/search-ui-analytics-plugin/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /docs/reference/images/facets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/facets.png -------------------------------------------------------------------------------- /docs/reference/images/sorting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/sorting.png -------------------------------------------------------------------------------- /examples/vue/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | // Reference UI Theme 2 | @import "themes/reference-ui/base"; 3 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /packages/search-ui-workplace-search-connector/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./WorkplaceSearchAPIConnector"; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true 4 | } 5 | -------------------------------------------------------------------------------- /docs/reference/images/api-keys.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/api-keys.jpeg -------------------------------------------------------------------------------- /docs/reference/images/authorize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/authorize.png -------------------------------------------------------------------------------- /docs/reference/images/carousel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/carousel.png -------------------------------------------------------------------------------- /docs/reference/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/dashboard.png -------------------------------------------------------------------------------- /docs/reference/images/endpoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/endpoints.png -------------------------------------------------------------------------------- /docs/reference/images/search-ui.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/search-ui.jpeg -------------------------------------------------------------------------------- /examples/sandbox/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/examples/sandbox/public/favicon.png -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /packages/search-ui-workplace-search-connector/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Elastic Search UI. 2 | 3 | Copyright 2012-2019 Elasticsearch B.V. 4 | -------------------------------------------------------------------------------- /docs/reference/images/completed-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/completed-ui.png -------------------------------------------------------------------------------- /docs/reference/images/credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/credentials.png -------------------------------------------------------------------------------- /docs/reference/images/facet-views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/facet-views.png -------------------------------------------------------------------------------- /docs/reference/images/initial-cra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/initial-cra.png -------------------------------------------------------------------------------- /docs/reference/images/kibana-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/kibana-home.png -------------------------------------------------------------------------------- /examples/vue/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | createApp(App).mount("#app"); 5 | -------------------------------------------------------------------------------- /docs/reference/images/api-key-view.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/api-key-view.jpeg -------------------------------------------------------------------------------- /docs/reference/images/category-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/category-page.png -------------------------------------------------------------------------------- /docs/reference/images/copy-cloud-id.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/copy-cloud-id.jpg -------------------------------------------------------------------------------- /docs/reference/images/cors-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/cors-settings.png -------------------------------------------------------------------------------- /docs/reference/images/create-engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/create-engine.png -------------------------------------------------------------------------------- /docs/reference/images/create-index.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/create-index.jpeg -------------------------------------------------------------------------------- /docs/reference/images/edit-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/edit-settings.png -------------------------------------------------------------------------------- /docs/reference/images/ent-search-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/ent-search-home.png -------------------------------------------------------------------------------- /docs/reference/images/events-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/events-request.png -------------------------------------------------------------------------------- /docs/reference/images/server-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/server-status.png -------------------------------------------------------------------------------- /docs/reference/images/update-mapping.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/update-mapping.jpeg -------------------------------------------------------------------------------- /docs/images/ecommerce/carousel/carousel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/carousel/carousel.png -------------------------------------------------------------------------------- /docs/images/ecommerce/overview/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/overview/overview.png -------------------------------------------------------------------------------- /docs/reference/images/configure-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/configure-schema.png -------------------------------------------------------------------------------- /docs/reference/images/oauth-application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/oauth-application.png -------------------------------------------------------------------------------- /docs/reference/images/search-ui-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/search-ui-results.png -------------------------------------------------------------------------------- /examples/sandbox/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/bin/watch-js: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/tsc --watch & 2 | ./node_modules/.bin/tsc -p tsconfig-cjs.json --watch & -------------------------------------------------------------------------------- /docs/images/ecommerce/category-page/facets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/category-page/facets.png -------------------------------------------------------------------------------- /docs/reference/images/integrations-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/integrations-server.png -------------------------------------------------------------------------------- /docs/reference/images/product-detail-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/product-detail-page.png -------------------------------------------------------------------------------- /packages/react-search-ui-views/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register"; 2 | import "@storybook/addon-links/register"; 3 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/bin/watch-js: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/tsc --watch & 2 | ./node_modules/.bin/tsc -p tsconfig-cjs.json --watch & -------------------------------------------------------------------------------- /docs/images/app-search-tutorial/completed-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/app-search-tutorial/completed-ui.png -------------------------------------------------------------------------------- /docs/images/app-search-tutorial/credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/app-search-tutorial/credentials.png -------------------------------------------------------------------------------- /docs/images/app-search-tutorial/initial-cra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/app-search-tutorial/initial-cra.png -------------------------------------------------------------------------------- /docs/images/app-search-tutorial/kibana-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/app-search-tutorial/kibana-home.png -------------------------------------------------------------------------------- /docs/images/ecommerce/category-page/sorting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/category-page/sorting.png -------------------------------------------------------------------------------- /docs/images/elasticsearch-tutorial/api-keys.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/elasticsearch-tutorial/api-keys.jpeg -------------------------------------------------------------------------------- /docs/reference/images/query-results-es-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/query-results-es-shop.png -------------------------------------------------------------------------------- /packages/search-ui-workplace-search-connector/bin/watch-js: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/tsc --watch & 2 | ./node_modules/.bin/tsc -p tsconfig-cjs.json --watch & -------------------------------------------------------------------------------- /docs/images/app-search-tutorial/create-engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/app-search-tutorial/create-engine.png -------------------------------------------------------------------------------- /docs/images/app-search-tutorial/ent-search-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/app-search-tutorial/ent-search-home.png -------------------------------------------------------------------------------- /docs/images/app-search-tutorial/paste-json-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/app-search-tutorial/paste-json-data.png -------------------------------------------------------------------------------- /docs/images/ecommerce/category-page/facet-views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/category-page/facet-views.png -------------------------------------------------------------------------------- /docs/images/elasticsearch-tutorial/search-ui.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/elasticsearch-tutorial/search-ui.jpeg -------------------------------------------------------------------------------- /docs/images/workplace-search-tutorial/authorize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/workplace-search-tutorial/authorize.png -------------------------------------------------------------------------------- /docs/images/workplace-search-tutorial/endpoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/workplace-search-tutorial/endpoints.png -------------------------------------------------------------------------------- /docs/reference/images/federated-search-products.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/federated-search-products.png -------------------------------------------------------------------------------- /docs/reference/images/query-suggestions-amazon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/query-suggestions-amazon.png -------------------------------------------------------------------------------- /docs/reference/images/query-suggestions-es-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/query-suggestions-es-shop.png -------------------------------------------------------------------------------- /packages/search-ui/src/test/setupTests.js: -------------------------------------------------------------------------------- 1 | // We use fake timers to avoid having to use async programming 2 | // in our tests. 3 | jest.useFakeTimers(); 4 | -------------------------------------------------------------------------------- /docs/images/app-search-tutorial/configure-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/app-search-tutorial/configure-schema.png -------------------------------------------------------------------------------- /docs/images/ecommerce/category-page/category-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/category-page/category-page.png -------------------------------------------------------------------------------- /docs/images/elasticsearch-tutorial/api-key-view.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/elasticsearch-tutorial/api-key-view.jpeg -------------------------------------------------------------------------------- /docs/images/elasticsearch-tutorial/copy-cloud-id.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/elasticsearch-tutorial/copy-cloud-id.jpg -------------------------------------------------------------------------------- /docs/images/elasticsearch-tutorial/cors-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/elasticsearch-tutorial/cors-settings.png -------------------------------------------------------------------------------- /docs/images/elasticsearch-tutorial/create-index.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/elasticsearch-tutorial/create-index.jpeg -------------------------------------------------------------------------------- /docs/images/elasticsearch-tutorial/edit-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/elasticsearch-tutorial/edit-settings.png -------------------------------------------------------------------------------- /docs/reference/images/cross-sell-recommendations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/cross-sell-recommendations.png -------------------------------------------------------------------------------- /docs/images/elasticsearch-tutorial/copy-es-endpoint.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/elasticsearch-tutorial/copy-es-endpoint.jpeg -------------------------------------------------------------------------------- /docs/images/elasticsearch-tutorial/update-mapping.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/elasticsearch-tutorial/update-mapping.jpeg -------------------------------------------------------------------------------- /docs/images/guides-analyzing-performance/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/guides-analyzing-performance/dashboard.png -------------------------------------------------------------------------------- /docs/images/guides-analyzing-performance/server-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/guides-analyzing-performance/server-status.png -------------------------------------------------------------------------------- /examples/sandbox/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | theme: { 4 | extend: {} 5 | }, 6 | plugins: [] 7 | }; 8 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest/presets/js-with-ts-esm", 3 | testPathIgnorePatterns: ["./lib"] 4 | }; 5 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/src/api-proxy.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export { default as ApiProxyConnector } from "./connectors/ApiProxyConnector"; 3 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest/presets/js-with-ts-esm", 3 | testPathIgnorePatterns: ["./lib"] 4 | }; 5 | -------------------------------------------------------------------------------- /docs/images/ecommerce/autocomplete/query-results-es-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/autocomplete/query-results-es-shop.png -------------------------------------------------------------------------------- /docs/images/guides-analyzing-performance/events-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/guides-analyzing-performance/events-request.png -------------------------------------------------------------------------------- /docs/images/workplace-search-tutorial/oauth-application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/workplace-search-tutorial/oauth-application.png -------------------------------------------------------------------------------- /docs/images/workplace-search-tutorial/search-ui-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/workplace-search-tutorial/search-ui-results.png -------------------------------------------------------------------------------- /docs/reference/images/query-suggestions-multiple-sources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/query-suggestions-multiple-sources.png -------------------------------------------------------------------------------- /docs/reference/images/query-suggestions-popular-queries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/reference/images/query-suggestions-popular-queries.png -------------------------------------------------------------------------------- /docs/images/ecommerce/autocomplete/query-suggestions-amazon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/autocomplete/query-suggestions-amazon.png -------------------------------------------------------------------------------- /packages/react-search-ui/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import baseConfig from "../../tsup.config.ts"; 2 | 3 | export default { 4 | ...baseConfig, 5 | external: ["react", "react-dom"] 6 | }; 7 | -------------------------------------------------------------------------------- /docs/images/ecommerce/autocomplete/federated-search-products.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/autocomplete/federated-search-products.png -------------------------------------------------------------------------------- /docs/images/ecommerce/autocomplete/query-suggestions-es-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/autocomplete/query-suggestions-es-shop.png -------------------------------------------------------------------------------- /docs/images/ecommerce/product-detail-page/product-detail-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/product-detail-page/product-detail-page.png -------------------------------------------------------------------------------- /docs/images/guides-analyzing-performance/integrations-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/guides-analyzing-performance/integrations-server.png -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_results-container.scss: -------------------------------------------------------------------------------- 1 | @include block("results-container") { 2 | padding: 0; 3 | list-style: none; 4 | } 5 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/__tests__/__snapshots__/Result.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`supports a render prop 1`] = `
`; 4 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/bin/build-js: -------------------------------------------------------------------------------- 1 | # Builds ESM JavaScript 2 | ./node_modules/.bin/tsc 3 | 4 | # Builds CJS JavaScript 5 | ./node_modules/.bin/tsc -p tsconfig-cjs.json 6 | -------------------------------------------------------------------------------- /.ci/jobs/defaults.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - meta: 3 | cluster: devops-ci 4 | 5 | - job: 6 | node: "linux && immutable" 7 | 8 | wrappers: 9 | - ansicolor 10 | - timestamps 11 | -------------------------------------------------------------------------------- /docs/docset.yml: -------------------------------------------------------------------------------- 1 | project: "Search UI docs" 2 | exclude: 3 | - README.md 4 | cross_links: 5 | - apm-agent-rum-js 6 | - docs-content 7 | - elasticsearch 8 | toc: 9 | - toc: reference 10 | -------------------------------------------------------------------------------- /docs/images/ecommerce/autocomplete/query-suggestions-multiple-sources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/autocomplete/query-suggestions-multiple-sources.png -------------------------------------------------------------------------------- /docs/images/ecommerce/autocomplete/query-suggestions-popular-queries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/autocomplete/query-suggestions-popular-queries.png -------------------------------------------------------------------------------- /docs/images/ecommerce/product-detail-page/cross-sell-recommendations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRenzo0801/search-ui/HEAD/docs/images/ecommerce/product-detail-page/cross-sell-recommendations.png -------------------------------------------------------------------------------- /packages/search-ui-analytics-plugin/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest/presets/js-with-ts-esm", 3 | testPathIgnorePatterns: ["./lib"], 4 | testEnvironment: "jsdom" 5 | }; 6 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/tsconfig-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./lib/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest/presets/js-with-ts-esm", 3 | testPathIgnorePatterns: ["./lib"], 4 | testEnvironment: "jsdom" 5 | }; 6 | -------------------------------------------------------------------------------- /packages/search-ui-workplace-search-connector/bin/build-js: -------------------------------------------------------------------------------- 1 | # Builds ESM JavaScript 2 | ./node_modules/.bin/tsc 3 | 4 | # Builds CJS JavaScript 5 | ./node_modules/.bin/tsc -p tsconfig-cjs.json 6 | -------------------------------------------------------------------------------- /packages/search-ui-workplace-search-connector/tsconfig-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./lib/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lerna-debug.log 3 | storybook-static 4 | .idea 5 | .env 6 | package-lock.json 7 | packages/**/src/version.ts 8 | packages/**/lib/* 9 | coverage 10 | html_docs 11 | dist 12 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // setup file 2 | import { configure } from "enzyme"; 3 | import Adapter from "@cfaester/enzyme-adapter-react-18"; 4 | 5 | configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/bin/build-js: -------------------------------------------------------------------------------- 1 | # clear directory 2 | # Builds ESM JavaScript 3 | ./node_modules/.bin/tsc 4 | 5 | # Builds CJS JavaScript 6 | ./node_modules/.bin/tsc -p tsconfig-cjs.json 7 | -------------------------------------------------------------------------------- /packages/search-ui-workplace-search-connector/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest/presets/js-with-ts-esm", 3 | testEnvironment: "jsdom", 4 | testPathIgnorePatterns: ["./lib"] 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/__tests__/__snapshots__/Paging.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`supports a render prop 1`] = ` 4 |
5 | 1 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import baseConfig from "../../tsup.config.ts"; 2 | 3 | 4 | export default { 5 | ...baseConfig, 6 | entry: ["src/index.ts", "src/api-proxy.ts"] 7 | }; 8 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/__tests__/__snapshots__/SearchBox.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`supports a render prop 1`] = ` 4 |
5 | test 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/__tests__/__snapshots__/Sorting.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`supports a render prop 1`] = ` 4 |
5 | field|||asc 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/postcss.config.js: -------------------------------------------------------------------------------- 1 | // Required for Vue example https://stackoverflow.com/a/49709493/4745802 2 | import autoprefixer from "autoprefixer"; 3 | 4 | module.exports = { 5 | plugins: [autoprefixer()] 6 | }; 7 | -------------------------------------------------------------------------------- /packages/search-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "exclude": ["node_modules", "lib"], 7 | "include": ["src/**/*.ts", "src/**/*.js"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_paging-info.scss: -------------------------------------------------------------------------------- 1 | @include block("paging-info") { 2 | @include themeTypography; 3 | color: $metaTextColor; 4 | font-size: $sizeM; 5 | display: inline-block; 6 | } 7 | -------------------------------------------------------------------------------- /packages/search-ui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest/presets/js-with-ts-esm", 3 | testPathIgnorePatterns: ["./lib"], 4 | setupFilesAfterEnv: ["/src/test/setupTests.js"], 5 | testEnvironment: "jsdom" 6 | }; 7 | -------------------------------------------------------------------------------- /examples/vue/src/data/README.md: -------------------------------------------------------------------------------- 1 | cards-raw was downloaded from https://hearthstonejson.com/ (https://api.hearthstonejson.com/v1/latest/enUS/cards.collectible.json) 2 | cards-cleansed are generated by cleanseCards.js script at the root of the project 3 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/__tests__/__snapshots__/ErrorBoundary.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Error Boundary supports a render prop 1`] = ` 4 |
5 | I am an error 6 |
7 | `; 8 | -------------------------------------------------------------------------------- /packages/search-ui-analytics-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "exclude": ["node_modules", "lib"], 7 | "include": ["src/**/*.ts", "src/**/*.js"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "exclude": ["node_modules", "lib"], 7 | "include": ["src/**/*.ts", "src/**/*.js"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "exclude": ["node_modules", "lib"], 7 | "include": ["src/**/*.ts", "src/**/*.js"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "exclude": ["node_modules", "lib"], 7 | "include": ["src/**/*.ts", "src/**/*.js"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/SearchContext.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SearchProviderContextInterface } from "./SearchProvider"; 3 | 4 | const SearchContext = 5 | React.createContext(null); 6 | 7 | export default SearchContext; 8 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/__snapshots__/Facets.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
7 |
8 | Children 9 |
10 |
11 | `; 12 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/__snapshots__/Results.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
    7 |
    8 | Children 9 |
    10 |
11 | `; 12 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/src/handlers/SearchkitModule.ts: -------------------------------------------------------------------------------- 1 | import * as SearchkitSDK from "@searchkit/sdk"; 2 | 3 | export default typeof SearchkitSDK.default === "object" 4 | ? (SearchkitSDK.default as unknown as typeof SearchkitSDK) 5 | : (SearchkitSDK as typeof SearchkitSDK); 6 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: "browser", 3 | testPathIgnorePatterns: ["./lib"], 4 | testEnvironment: "jsdom", 5 | testMatch: ["**/__tests__/**/*.test.ts"], 6 | transform: { 7 | "^.+\\.tsx?$": "ts-jest" 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /examples/vue/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-prettier" 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/jest.node.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: "node", 3 | testEnvironment: "node", 4 | preset: "ts-jest/presets/js-with-ts-esm", 5 | testMatch: ["**/__tests__/**/*.test.node.ts"], 6 | transform: { 7 | "^.+\\.tsx?$": "ts-jest" 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/helpers.ts: -------------------------------------------------------------------------------- 1 | // LÒpez => Lopez 2 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize 3 | export const accentFold = (str: any): string => 4 | typeof str === "string" 5 | ? str.normalize("NFD").replace(/[\u0300-\u036f]/g, "") 6 | : ""; 7 | -------------------------------------------------------------------------------- /examples/sandbox/src/components/ExampleCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "./Card"; 2 | 3 | export const ExampleCard = ({ title, description, href, icon }) => ( 4 | 11 | ); 12 | -------------------------------------------------------------------------------- /packages/react-search-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "lib": ["dom", "esnext"], 6 | "outDir": "lib" 7 | }, 8 | "exclude": ["node_modules", "lib"], 9 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "lib": ["dom", "esnext"], 6 | "outDir": "lib" 7 | }, 8 | "exclude": ["node_modules", "lib"], 9 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/sandbox/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import { BrowserRouter } from "react-router-dom"; 3 | import Router from "./Router"; 4 | import "./styles.css"; 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /examples/vue/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/view-helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as getFilterValueDisplay } from "./getFilterValueDisplay"; 2 | export { default as appendClassName } from "./appendClassName"; 3 | export { default as getUrlSanitizer } from "./getUrlSanitizer"; 4 | export { formatResult, getEscapedField, getRaw } from "./formatResult"; 5 | -------------------------------------------------------------------------------- /.github/workflows/docs-cleanup.yml: -------------------------------------------------------------------------------- 1 | name: docs-cleanup 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | docs-preview: 10 | uses: elastic/docs-builder/.github/workflows/preview-cleanup.yml@main 11 | permissions: 12 | contents: none 13 | id-token: write 14 | deployments: write 15 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as withSearch } from "./withSearch"; 2 | export { default as SearchContext } from "./SearchContext"; 3 | export { default as WithSearch } from "./WithSearchRenderProps"; 4 | export { default as SearchProvider } from "./SearchProvider"; 5 | 6 | export * from "./containers"; 7 | export * from "./hooks"; 8 | -------------------------------------------------------------------------------- /examples/sandbox/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import path from "path"; 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | "@": path.resolve(__dirname, "./src") 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /examples/sandbox/src/pages/search-bar-in-header/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "@elastic/react-search-ui-views/lib/styles/styles.css"; 4 | import Header from "./Header"; 5 | 6 | export default function App() { 7 | return ( 8 | <> 9 |
10 |

Main site content goes here

11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_sorting.scss: -------------------------------------------------------------------------------- 1 | @include block("sorting") { 2 | @include themeTypography; 3 | display: inline-block; 4 | width: 100%; 5 | 6 | @include element("label") { 7 | font-size: $sizeM; 8 | color: $facetLabel; 9 | text-transform: uppercase; 10 | letter-spacing: 1px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/sandbox/src/components/UseCaseCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "./Card"; 2 | 3 | export const UseCaseCard = ({ title, description, href, icon }) => ( 4 | 13 | ); 14 | -------------------------------------------------------------------------------- /examples/sandbox/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/react-search-ui/README.md: -------------------------------------------------------------------------------- 1 | # Search UI: React Framework 2 | 3 | Part of the [Search UI](https://github.com/elastic/search-ui) project. 4 | 5 | React library for building search experiences. 6 | 7 | Uses the [Search UI](../search-ui/README.md). 8 | 9 | See the [main README](../../README.md) for set-up instructions. 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/react-search-ui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest/presets/js-with-ts-esm", 3 | testPathIgnorePatterns: ["./lib"], 4 | testEnvironment: "jsdom", 5 | setupFilesAfterEnv: ["/src/setupTests.js"], 6 | moduleNameMapper: { 7 | "\\.(css)$": "/__mocks__/styleMock.js" 8 | }, 9 | snapshotSerializers: ["enzyme-to-json/serializer"] 10 | }; 11 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/__snapshots__/ErrorBoundary.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders an error when there is an error 1`] = ` 4 |
7 | I am an error 8 |
9 | `; 10 | 11 | exports[`renders children when there is no error 1`] = ` 12 |
13 | Child 14 |
15 | `; 16 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # Fixes commits issued from VSCode. Compiled from: https://github.com/typicode/husky/issues/912 5 | export NVM_DIR="$HOME/.nvm" 6 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 7 | a=$(nvm ls | grep 'node') 8 | b=${a#*(-> } 9 | v=${b%%[)| ]*} 10 | export PATH="$NVM_DIR/versions/node/$v/bin:$PATH" 11 | 12 | npx lint-staged 13 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest/presets/js-with-ts-esm", 3 | testPathIgnorePatterns: ["./lib"], 4 | testEnvironment: "jsdom", 5 | setupFilesAfterEnv: ["/src/setupTests.js"], 6 | moduleNameMapper: { 7 | "\\.(css)$": "/__mocks__/styleMock.js" 8 | }, 9 | snapshotSerializers: ["enzyme-to-json/serializer"] 10 | }; 11 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/src/index.ts: -------------------------------------------------------------------------------- 1 | import ElasticsearchAPIConnector from "./connectors/ElasticsearchAPIConnector"; 2 | 3 | export * from "./types"; 4 | export * from "./transporter/ApiClientTransporter"; 5 | export * from "./connectors/ApiProxyConnector"; 6 | 7 | export default ElasticsearchAPIConnector; 8 | export { default as ApiProxyConnector } from "./connectors/ApiProxyConnector"; 9 | -------------------------------------------------------------------------------- /examples/sandbox/src/components/ApiCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "./Card"; 2 | 3 | export const ApiCard = ({ title, description, href, recommended = false }) => ( 4 | 12 | ); 13 | -------------------------------------------------------------------------------- /examples/sandbox/src/pages/customizing-styles-and-html/ClearFilters.jsx: -------------------------------------------------------------------------------- 1 | import { useSearch } from "@elastic/react-search-ui"; 2 | 3 | export const ClearFilters = () => { 4 | const { filters, clearFilters } = useSearch(); 5 | 6 | return ( 7 |
8 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react"; 2 | 3 | import "../lib/styles/styles.css"; 4 | 5 | // automatically import all files ending in *.stories.js 6 | const req = require.context("../stories", true, /.stories.js$/); 7 | function loadStories() { 8 | req.keys().forEach((filename) => req(filename)); 9 | } 10 | 11 | configure(loadStories, module); 12 | -------------------------------------------------------------------------------- /examples/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/search-ui/src/__tests__/A11yNotifications.ssr.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | import { getLiveRegion, announceToScreenReader } from "../A11yNotifications"; 5 | 6 | it("does not crash or create errors in server-side rendered apps", () => { 7 | expect(getLiveRegion()).toBeNull(); 8 | expect(() => { 9 | announceToScreenReader("test"); 10 | }).not.toThrowError(); 11 | }); 12 | -------------------------------------------------------------------------------- /SECURITY.txt: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The latest minor version will receive security updates. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you notice a security vulnerability in our code, please use the instructions on this page to report it: https://www.elastic.co/community/security. Please do not create an issue in this repository. It can compromise the security of others. 10 | -------------------------------------------------------------------------------- /examples/sandbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "jsxImportSource": "react", 5 | "baseUrl": ".", 6 | "noEmit": true, 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "paths": { 10 | "@/*": ["src/*"] 11 | }, 12 | "outDir": "dist" 13 | }, 14 | "exclude": ["node_modules", "dist"], 15 | "include": ["src/**/*.ts", "src/**/*.tsx"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/reset.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Reset search experience to initial state 3 | * 4 | */ 5 | export default function reset(): void { 6 | // eslint-disable-next-line no-console 7 | if (this.debug) console.log("Search UI: Action", "reset", ...arguments); 8 | 9 | this._setState(this.startingState); 10 | if (this.trackUrlState) { 11 | this.URLManager.pushStateToURL(this.state); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/setCurrent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Set the current page 3 | * 4 | * Will trigger new search 5 | * 6 | * @param current Integer 7 | */ 8 | export default function setCurrent(current: number): void { 9 | // eslint-disable-next-line no-console 10 | if (this.debug) console.log("Search UI: Action", "setCurrent", ...arguments); 11 | 12 | this._updateSearchResults({ 13 | current 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /examples/sandbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "url"; 2 | 3 | import { defineConfig } from "vite"; 4 | import vue from "@vitejs/plugin-vue"; 5 | import vueJsx from "@vitejs/plugin-vue-jsx"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [vue(), vueJsx()], 10 | resolve: { 11 | alias: { 12 | "@": fileURLToPath(new URL("./src", import.meta.url)) 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/A11yNotifications.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Accessibility notifications 3 | * @see packages/search-ui/src/A11yNotifications.js 4 | */ 5 | 6 | const defaultMessages = { 7 | moreFilters: ({ visibleOptionsCount, showingAll }) => { 8 | let message = showingAll ? "All " : ""; 9 | message += `${visibleOptionsCount} options shown.`; 10 | return message; 11 | } 12 | }; 13 | 14 | export default defaultMessages; 15 | -------------------------------------------------------------------------------- /packages/search-ui/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as helpersSource from "./helpers"; 2 | 3 | export { default as SearchDriver, DEFAULT_STATE } from "./SearchDriver"; 4 | export type { SearchDriverOptions } from "./SearchDriver"; 5 | export const helpers = { 6 | ...helpersSource 7 | }; 8 | export * from "./constants"; 9 | export * from "./types"; 10 | export type { SearchDriverActions } from "./actions"; 11 | // export type { Event } from "./Events"; 12 | -------------------------------------------------------------------------------- /examples/vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /.github/workflows/docs-build.yml: -------------------------------------------------------------------------------- 1 | name: docs-build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request_target: ~ 8 | merge_group: ~ 9 | 10 | jobs: 11 | docs-preview: 12 | uses: elastic/docs-builder/.github/workflows/preview-build.yml@main 13 | with: 14 | path-pattern: docs/** 15 | permissions: 16 | deployments: write 17 | id-token: write 18 | contents: read 19 | pull-requests: read 20 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/__snapshots__/PagingInfo.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
7 | Showing 8 | 9 | 10 | 0 11 | - 12 | 20 13 | 14 | 15 | out of 16 | 17 | 1000 18 | 19 | 20 | for: 21 | 22 | grok 23 | 24 |
25 | `; 26 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "ESNext", 5 | "target": "ES2015", 6 | "declaration": true, 7 | "strict": false, 8 | "sourceMap": true, 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "isolatedModules": true, 15 | "skipLibCheck": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import ElasticsearchAPIConnector from "../index"; 2 | 3 | describe("elasticsearch connector interface", () => { 4 | it("should throw an exception when host or cloud not specified", () => { 5 | expect(() => { 6 | new ElasticsearchAPIConnector({ 7 | index: "test" 8 | }); 9 | }).toThrowError( 10 | "Either host or cloud configuration or custom apiClient must be provided" 11 | ); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_results-per-page.scss: -------------------------------------------------------------------------------- 1 | @include block("results-per-page") { 2 | @include themeTypography; 3 | color: $metaTextColor; 4 | font-size: 12px; 5 | display: flex; 6 | align-items: center; 7 | height: 100%; 8 | 9 | @include element("label") { 10 | margin-right: $sizeS; 11 | } 12 | 13 | .sui-select__control { 14 | align-items: center; 15 | 16 | input { 17 | position: absolute; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | const args = process.argv; 4 | const watch = args.indexOf("--watch") !== -1; 5 | 6 | export default defineConfig({ 7 | entry: ["src/index.ts"], 8 | format: ["esm", "cjs"], 9 | dts: true, 10 | sourcemap: watch, 11 | clean: !watch, 12 | splitting: false, 13 | minify: !watch, 14 | outDir: "lib", 15 | watch, 16 | silent: watch, 17 | onSuccess: "echo 'Build completed successfully! 🎉'", 18 | tsconfig: "tsconfig.json" 19 | }); 20 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/src/handlers/search/Query.ts: -------------------------------------------------------------------------------- 1 | import SearchkitModule from "../SearchkitModule"; 2 | 3 | const { CustomQuery } = SearchkitModule; 4 | 5 | export const EngineQuery = () => 6 | new CustomQuery({ 7 | queryFn: (query) => { 8 | return { 9 | bool: { 10 | must: [ 11 | { 12 | query_string: { 13 | query: query 14 | } 15 | } 16 | ] 17 | } 18 | }; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/setResultsPerPage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Set the number of results to show 3 | * 4 | * Will trigger new search 5 | * 6 | * @param resultsPerPage Integer 7 | */ 8 | export default function setResultsPerPage(resultsPerPage: number): void { 9 | if (this.debug) 10 | // eslint-disable-next-line no-console 11 | console.log("Search UI: Action", "setResultsPerPage", ...arguments); 12 | 13 | this._updateSearchResults({ 14 | current: 1, 15 | resultsPerPage 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/__tests__/A11yNotifications.test.ts: -------------------------------------------------------------------------------- 1 | import defaultMessages from "../A11yNotifications"; 2 | 3 | it("outputs moreFilters correctly", () => { 4 | expect( 5 | defaultMessages.moreFilters({ 6 | visibleOptionsCount: 15, 7 | showingAll: false 8 | }) 9 | ).toEqual("15 options shown."); 10 | 11 | expect( 12 | defaultMessages.moreFilters({ 13 | visibleOptionsCount: 28, 14 | showingAll: true 15 | }) 16 | ).toEqual("All 28 options shown."); 17 | }); 18 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | Before submitting this PR: 2 | 3 | 1. Make sure you are PR'ing from a fork, please do not push branches directly 4 | to this repository. 5 | 2. Ensure you've written sufficient tests for your work. 6 | 3. Double check that your changes will not break existing connectors (if 7 | applicable). 8 | 4. Double check that your changes do not break the `react-search-ui-views` 9 | storybook (if applicable). 10 | 11 | ## Description 12 | 13 | ## List of changes 14 | 15 | ## Associated Github Issues 16 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/WithSearchRenderProps.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import withSearch from "./withSearch"; 3 | import PropTypes from "prop-types"; 4 | 5 | function WithSearch({ mapContextToProps, children }) { 6 | const Search = withSearch(mapContextToProps)((props) => { 7 | return children(props); 8 | }); 9 | 10 | return ; 11 | } 12 | 13 | WithSearch.propTypes = { 14 | mapContextToProps: PropTypes.func, 15 | children: PropTypes.func.isRequired 16 | }; 17 | 18 | export default WithSearch; 19 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/view-helpers/getFilterValueDisplay.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Since Filter Values come in many different varieties, this helper 3 | encapsulates the logic for determining how to show the label of that 4 | filter in the UI. 5 | */ 6 | export default function getFilterValueDisplay(filterValue: any): string { 7 | if (filterValue === undefined || filterValue === null) return ""; 8 | if (Object.prototype.hasOwnProperty.call(filterValue, "name")) 9 | return filterValue.name; 10 | return String(filterValue); 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ErrorBoundary } from "./ErrorBoundary"; 2 | export { default as Facet } from "./Facet"; 3 | export { default as Paging } from "./Paging"; 4 | export { default as PagingInfo } from "./PagingInfo"; 5 | export { default as Result } from "./Result"; 6 | export { default as Results } from "./Results"; 7 | export { default as ResultsPerPage } from "./ResultsPerPage"; 8 | export { default as SearchBox } from "./SearchBox"; 9 | export { default as Sorting } from "./Sorting"; 10 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/__tests__/__snapshots__/PagingInfo.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders when it doesn't have any results or a result search term 1`] = ` 4 |
7 | Showing 8 | 9 | 10 | 1 11 | - 12 | 20 13 | 14 | 15 | out of 16 | 17 | 100 18 | 19 |
20 | `; 21 | 22 | exports[`supports a render prop 1`] = ` 23 |
24 | 1 25 | 20 26 |
27 | `; 28 | -------------------------------------------------------------------------------- /examples/vue/src/components/SearchSort.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_error-boundary.scss: -------------------------------------------------------------------------------- 1 | @include block("search-error") { 2 | @include themeTypography; 3 | color: $red; 4 | margin: auto; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | height: calc(100vh - 180px); 9 | 10 | &.no-error { 11 | @include themeTypography; 12 | color: $black; 13 | margin: auto; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | height: calc(100vh - 180px); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { estypes } from "@elastic/elasticsearch"; 2 | 3 | export type SearchRequest = estypes.SearchRequest; 4 | export type SearchResponse = estypes.SearchResponse>; 5 | export type EngineRouteFn = (host: string, engineName: string) => string; 6 | export interface Transporter { 7 | performRequest(requestBody: SearchRequest): Promise; 8 | } 9 | 10 | export type ConnectionOptions = { 11 | host: string; 12 | engineName: string; 13 | apiKey: string; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/Facets.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { appendClassName } from "./view-helpers"; 4 | 5 | type FacetsProps = { 6 | children: React.ReactNode; 7 | className?: string; 8 | } & React.HTMLAttributes; 9 | 10 | function Facets({ children, className, ...rest }: FacetsProps) { 11 | return ( 12 |
16 | {children} 17 |
18 | ); 19 | } 20 | 21 | export default Facets; 22 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // setup file 2 | import { configure } from "enzyme"; 3 | import util from "util"; 4 | import Adapter from "@cfaester/enzyme-adapter-react-18"; 5 | 6 | configure({ adapter: new Adapter() }); 7 | 8 | if (typeof global.TextEncoder === "undefined") { 9 | Object.defineProperty(global, "TextEncoder", { 10 | value: util.TextEncoder 11 | }); 12 | } 13 | 14 | if (typeof global.TextDecoder === "undefined") { 15 | Object.defineProperty(global, "TextDecoder", { 16 | value: util.TextDecoder 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "declaration": true, 5 | "outDir": "lib/esm", 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "esModuleInterop": true, 11 | "module": "es2020", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true 15 | }, 16 | "exclude": ["node_modules"], 17 | "include": ["src/**/*.ts", "src/**/*.js"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/search-ui-workplace-search-connector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "declaration": true, 5 | "outDir": "lib/esm", 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "esModuleInterop": true, 11 | "module": "es2020", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true 15 | }, 16 | "exclude": ["node_modules"], 17 | "include": ["src/**/*.ts", "src/**/*.js"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/sandbox/.codesandbox/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "setupTasks": [ 3 | { 4 | "name": "Install Dependencies", 5 | "command": "yarn install" 6 | }, 7 | { 8 | "name": "Build App", 9 | "command": "VITE_IS_SANDBOX=true yarn build" 10 | } 11 | ], 12 | "tasks": { 13 | "start": { 14 | "name": "start", 15 | "command": "VITE_IS_SANDBOX=true yarn start & node src/pages/elasticsearch-production-ready/server/index.js", 16 | "runAtStart": true, 17 | "preview": { 18 | "port": 4173 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/search-ui/src/queryString.ts: -------------------------------------------------------------------------------- 1 | import queryString from "qs"; 2 | import preserveTypesEncoder from "./preserveTypesEncoder"; 3 | 4 | export default { 5 | parse(string: string | Record): Record { 6 | return queryString.parse(string, { 7 | ignoreQueryPrefix: true, 8 | decoder: preserveTypesEncoder.decode, 9 | arrayLimit: 1000 10 | }); 11 | }, 12 | stringify(object: Record): string { 13 | return queryString.stringify(object, { 14 | encoder: preserveTypesEncoder.encode 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /.ci/pipelines/searchui.groovy: -------------------------------------------------------------------------------- 1 | @Library(['estc', 'entsearch']) _ 2 | 3 | eshPipeline( 4 | timeout: 45, 5 | project_name: 'Search UI', 6 | repository: 'searchui', 7 | stage_name: 'Search UI Unit Tests', 8 | stages: [ 9 | 10 | [ 11 | name: 'Run tests', 12 | type: 'script', 13 | label: 'Run tests', 14 | script: { 15 | sh "docker run --rm -v `pwd`:/ci -w=/ci node:16.14 npm install && npm run test-ci" 16 | }, 17 | match_on_all_branches: true, 18 | ] 19 | ], 20 | slack_channel: 'search-ui' 21 | ) 22 | -------------------------------------------------------------------------------- /examples/vue/src/components/SearchResultsPerPage.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /.ci/pipelines/search-ui.groovy: -------------------------------------------------------------------------------- 1 | @Library(['estc', 'entsearch']) _ 2 | 3 | eshPipeline( 4 | timeout: 45, 5 | project_name: 'Search UI', 6 | repository: 'search-ui', 7 | stage_name: 'Search UI Unit Tests', 8 | stages: [ 9 | 10 | [ 11 | name: 'Run tests', 12 | type: 'script', 13 | label: 'Run tests', 14 | script: { 15 | sh "docker run --rm -v `pwd`:/ci -w=/ci node:16.14 npm install && npm run test-ci" 16 | }, 17 | match_on_all_branches: true, 18 | ] 19 | ], 20 | slack_channel: 'search-ui' 21 | ) 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Questions about using this library 4 | title: "" 5 | labels: question 6 | assignees: "" 7 | --- 8 | 9 | Please read the following before posting: 10 | 11 | The Enterprise Search team at Elastic maintains this library and are happy to help. Try posting your question to the [Elastic Enterprise Search](https://discuss.elastic.co/c/enterprise-search/84) discuss forums. Be sure to mention that you're using Search UI and also let us know what backend your using; whether it's App Search, Site Search, Workplace Search, Elasticsearch, or something else entirely. 12 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/__tests__/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { accentFold } from "../helpers"; 2 | 3 | describe("accentFold", () => { 4 | it("should remove accents marks from string", () => { 5 | expect(accentFold("ǟc̈ŗÄh")).toBe("acrAh"); 6 | expect(accentFold("abc")).toBe("abc"); 7 | expect(accentFold("ABC")).toBe("ABC"); 8 | // anything other than string passed as argument should return empty string 9 | expect(accentFold(123)).toBe(""); 10 | expect(accentFold({ a: 1, b: 2 })).toBe(""); 11 | expect(accentFold([1, 2])).toBe(""); 12 | expect(accentFold(null)).toBe(""); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /.github/workflows/docs-preview.yml: -------------------------------------------------------------------------------- 1 | name: docs-preview 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | paths: 6 | - "**.asciidoc" 7 | - "**.jpg" 8 | - "**.png" 9 | - "**.gif" 10 | 11 | permissions: 12 | pull-requests: write 13 | 14 | jobs: 15 | doc-preview-pr: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: elastic/docs/.github/actions/docs-preview@master 19 | with: 20 | github-token: ${{ secrets.GITHUB_TOKEN }} 21 | repo: ${{ github.event.repository.name }} 22 | preview-path: "guide/index.html" 23 | pr: ${{ github.event.pull_request.number }} 24 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["examples/*", "packages/*"], 3 | "version": "1.23.1", 4 | "useWorkspaces": true, 5 | "npmClient": "yarn", 6 | "command": { 7 | "publish": { 8 | "npmClient": "npm", 9 | "ignoreChanges": [ 10 | "**/__tests__/**", 11 | "**/*.md", 12 | "**/src/test/**", 13 | "packages/search-ui-workplace-search-connector/**", 14 | "packages/search-ui-app-search-connector/**" 15 | ], 16 | "registry": "https://registry.npmjs.org/", 17 | "message": "Release: %s" 18 | }, 19 | "run": { 20 | "sort": true, 21 | "noPrivate": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { getHostFromCloud } from "../utils"; 2 | 3 | describe("Utils Browser Tests", () => { 4 | it("should correctly decode cloud ID in browser environment", () => { 5 | const testCloudId = 6 | "some-search-build:c29tZWNsb3VkaG9zdDo0NDMkcmFuZG9tSWQkc29tZWluZm8="; 7 | 8 | // Spy on atob 9 | const atobSpy = jest.spyOn(window, "atob"); 10 | const result = getHostFromCloud({ id: testCloudId }); 11 | 12 | expect(atobSpy).toHaveBeenCalled(); 13 | expect(result).toBe("https://randomId.somecloudhost:443"); 14 | 15 | atobSpy.mockRestore(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/sandbox/src/components/TabTrigger.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from "radix-ui"; 2 | 3 | export const TabTrigger = ({ 4 | children, 5 | value 6 | }: { 7 | children: React.ReactNode; 8 | value: string; 9 | }) => { 10 | return ( 11 | 15 | {children} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export type InputViewProps = { 4 | getAutocomplete: () => JSX.Element; 5 | getButtonProps: (additionalProps?: any) => any; 6 | getInputProps: (additionalProps?: any) => any; 7 | }; 8 | 9 | function SearchInput({ 10 | getAutocomplete, 11 | getButtonProps, 12 | getInputProps 13 | }: InputViewProps) { 14 | return ( 15 | <> 16 |
17 | 18 | {getAutocomplete()} 19 |
20 | 21 | 22 | ); 23 | } 24 | 25 | export default SearchInput; 26 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/src/__tests__/utils.test.node.ts: -------------------------------------------------------------------------------- 1 | import { getHostFromCloud } from "../utils"; 2 | 3 | describe("Utils Node.js Tests", () => { 4 | it("should correctly decode cloud ID in Node.js environment", () => { 5 | const testCloudId = 6 | "some-search-build:c29tZWNsb3VkaG9zdDo0NDMkcmFuZG9tSWQkc29tZWluZm8="; 7 | 8 | // Spy on Buffer.from 9 | const bufferSpy = jest.spyOn(Buffer, "from"); 10 | const result = getHostFromCloud({ id: testCloudId }); 11 | 12 | expect(bufferSpy).toHaveBeenCalled(); 13 | expect(result).toBe("https://randomId.somecloudhost:443"); 14 | 15 | bufferSpy.mockRestore(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Working with the docs 2 | 3 | The [Search UI docs](https://www.elastic.co/guide/en/search-ui/current/overview.html) are written in [Docs V3](https://elastic.github.io/docs-builder/) and are built using [elastic/docs](https://github.com/elastic/docs). 4 | 5 | ## Build the docs locally 6 | 7 | Before building the docs locally make sure you have: 8 | 9 | - [docs-builder](https://elastic.github.io/docs-builder/contribute/locally/#step-one) install locally 10 | 11 | Install `docs-builder` in a parent directory and run 12 | 13 | ``` 14 | ../docs-builder serve -p ./docs 15 | ``` 16 | 17 | And open: [http://localhost:3000/reference/](http://localhost:3000/reference/) 18 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/layouts/__snapshots__/LayoutSidebar.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 5 | 13 |
16 | 24 | Hello world! 25 |
26 |
27 | `; 28 | -------------------------------------------------------------------------------- /docs/reference/known-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | mapped_pages: 3 | - https://www.elastic.co/guide/en/search-ui/current/known-issues.html 4 | 5 | navigation_title: "Troubleshooting" 6 | --- 7 | 8 | # Search known issues [search-known-issues] 9 | 10 | % Use the following template to add entries to this page. 11 | 12 | % :::{dropdown} Title of known issue 13 | % **Details** 14 | % On [Month/Day/Year], a known issue was discovered that [description of known issue]. 15 | 16 | % **Workaround** 17 | % Workaround description. 18 | 19 | % **Resolved** 20 | % On [Month/Day/Year], this issue was resolved. 21 | 22 | ::: 23 | 24 | - When using the **Elasticsearch connector** Search UI does not render nested objects. 25 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_boolean-facet.scss: -------------------------------------------------------------------------------- 1 | @include block("boolean-facet") { 2 | color: $facetLinkColor; 3 | font-size: 13px; 4 | margin: 8px 0; 5 | 6 | @include element("option-label") { 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | cursor: pointer; 11 | } 12 | 13 | @include element("option-input-wrapper") { 14 | flex: 1; 15 | } 16 | 17 | @include element("checkbox") { 18 | margin-right: $sizeS; 19 | cursor: pointer; 20 | } 21 | 22 | @include element("option-count") { 23 | color: #888888; 24 | font-size: 0.85em; 25 | margin-left: $sizeL; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/view-helpers/appendClassName.test.ts: -------------------------------------------------------------------------------- 1 | import { appendClassName } from "../../view-helpers"; 2 | 3 | it("will append a className", () => { 4 | expect(appendClassName("a", "b")).toBe("a b"); 5 | }); 6 | 7 | it("will handle an empty base className", () => { 8 | expect(appendClassName("", "b")).toBe("b"); 9 | }); 10 | 11 | it("will handle an empty new className", () => { 12 | expect(appendClassName("a")).toBe("a"); 13 | }); 14 | 15 | it("will handle a missing new className", () => { 16 | expect(appendClassName()).toBe(""); 17 | }); 18 | 19 | it("will accept an array", () => { 20 | expect(appendClassName("a", ["b", null, "", "c"])).toBe("a b c"); 21 | }); 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: feature 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /examples/sandbox/src/pages/ecommerce/styles.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .ecommerce-container { 6 | .sui-layout-header.sui-layout-header { 7 | padding: 0; 8 | } 9 | 10 | .sui-layout-main-header { 11 | margin-bottom: 1rem; 12 | } 13 | 14 | .sui-layout-header { 15 | border: none; 16 | } 17 | 18 | .navigation { 19 | position: sticky; 20 | top: 0; 21 | z-index: 40; 22 | background-color: #000; 23 | transition-property: all; 24 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 25 | transition-duration: 0.15s; 26 | } 27 | 28 | .sui-search-box__suggestion-list { 29 | width: 200px; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/Facets.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Facets from "../Facets"; 3 | import { shallow } from "enzyme"; 4 | 5 | it("renders correctly", () => { 6 | const wrapper = shallow( 7 | 8 |
Children
9 |
10 | ); 11 | expect(wrapper).toMatchSnapshot(); 12 | }); 13 | 14 | it("renders with className prop applied", () => { 15 | const customClassName = "test-class"; 16 | const wrapper = shallow( 17 | 18 |
Children
19 |
20 | ); 21 | const { className } = wrapper.props(); 22 | expect(className).toEqual("sui-facet-container test-class"); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/Results.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Results from "../Results"; 3 | import { shallow } from "enzyme"; 4 | 5 | it("renders correctly", () => { 6 | const wrapper = shallow( 7 | 8 |
Children
9 |
10 | ); 11 | expect(wrapper).toMatchSnapshot(); 12 | }); 13 | 14 | it("renders with className prop applied", () => { 15 | const customClassName = "test-class"; 16 | const wrapper = shallow( 17 | 18 |
Children
19 |
20 | ); 21 | const { className } = wrapper.props(); 22 | expect(className).toEqual("sui-results-container test-class"); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/view-helpers/appendClassName.ts: -------------------------------------------------------------------------------- 1 | function getNewClassName(newClassName: string | string[]) { 2 | if (!Array.isArray(newClassName)) return newClassName; 3 | 4 | return newClassName.filter((name) => name).join(" "); 5 | } 6 | 7 | export default function appendClassName( 8 | baseClassName?: string | string[] | undefined, 9 | newClassName?: string | string[] | undefined 10 | ): string { 11 | if (!newClassName) 12 | return ( 13 | (Array.isArray(baseClassName) 14 | ? baseClassName.join(" ") 15 | : baseClassName) || "" 16 | ); 17 | if (!baseClassName) return getNewClassName(newClassName) || ""; 18 | return `${baseClassName} ${getNewClassName(newClassName)}`; 19 | } 20 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/clearFilters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove all filters 3 | * 4 | * Will trigger new search 5 | * 6 | * @param except Array[String] field name of any filters that should remain 7 | */ 8 | export default function clearFilters(except: string[] = []): void { 9 | if (this.debug) 10 | // eslint-disable-next-line no-console 11 | console.log("Search UI: Action", "clearFilters", ...arguments); 12 | 13 | const { filters } = this.state; 14 | 15 | const updatedFilters = filters.filter((filter) => { 16 | const filterField = filter.field; 17 | return except.includes(filterField); 18 | }); 19 | 20 | this._updateSearchResults({ 21 | current: 1, 22 | filters: updatedFilters 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/__tests__/SearchContext.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount } from "enzyme"; 3 | 4 | import { SearchContext } from ".."; 5 | import { SearchDriver } from "@elastic/search-ui"; 6 | 7 | it("Should be a context", () => { 8 | const searchDriver = new SearchDriver({ 9 | apiConnector: null 10 | }); 11 | 12 | searchDriver.state.searchTerm = "a search term"; 13 | 14 | const value = mount( 15 | 16 | 17 | {({ driver }) =>
{driver.state.searchTerm}
} 18 |
19 |
20 | ); 21 | 22 | expect(value.text()).toBe("a search term"); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/PagingInfo.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PagingInfo from "../PagingInfo"; 3 | import { shallow } from "enzyme"; 4 | 5 | const props = { 6 | end: 20, 7 | searchTerm: "grok", 8 | start: 0, 9 | totalResults: 1000 10 | }; 11 | 12 | it("renders correctly", () => { 13 | const wrapper = shallow(); 14 | expect(wrapper).toMatchSnapshot(); 15 | }); 16 | 17 | it("renders with className prop applied", () => { 18 | const customClassName = "test-class"; 19 | const wrapper = shallow( 20 | 21 | ); 22 | const { className } = wrapper.props(); 23 | expect(className).toEqual("sui-paging-info test-class"); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/__snapshots__/Paging.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 26 | `; 27 | -------------------------------------------------------------------------------- /examples/sandbox/src/pages/customizing-styles-and-html/custom.css: -------------------------------------------------------------------------------- 1 | /* We selectedivly override base styles to create a red theme */ 2 | .customization-example .sui-search-box__submit { 3 | background: none; 4 | background-color: red; 5 | } 6 | 7 | .customization-example .sui-layout-sidebar-toggle { 8 | color: red; 9 | border: 1px solid red; 10 | } 11 | 12 | .customization-example .sui-result__header { 13 | color: #ff0000cc; 14 | font-size: 18px; 15 | font-weight: bold; 16 | } 17 | 18 | .customization-example .sui-facet-view-more { 19 | color: red; 20 | } 21 | 22 | .customization-example .sui-result__details { 23 | color: #000000af; 24 | font-size: 14px; 25 | } 26 | 27 | .customization-example .sui-result + .sui-result { 28 | margin-top: 8px; 29 | } 30 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Facet, Filter } from "@elastic/search-ui"; 2 | 3 | export type BaseContainerProps = { 4 | children?: React.ReactNode; 5 | className?: string; 6 | }; 7 | 8 | export type BaseContainerStateProps = { 9 | error: string; 10 | filters: Filter[]; 11 | facets: Record; 12 | }; 13 | 14 | // From SO https://stackoverflow.com/a/59071783 15 | // TS Utility to rename keys in a type 16 | // Usage type x = Rename<{ renameMe: string }, { "renameMe": "newName" }> 17 | // x === { newName: string } 18 | export type Rename< 19 | T, 20 | R extends { 21 | [K in keyof R]: K extends keyof T ? PropertyKey : "Error: key not in T"; 22 | } 23 | > = { [P in keyof T as P extends keyof R ? R[P] : P]: T[P] }; 24 | -------------------------------------------------------------------------------- /examples/sandbox/src/pages/ecommerce/components/ProductCarousel/config.js: -------------------------------------------------------------------------------- 1 | import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector"; 2 | 3 | const connector = new AppSearchAPIConnector({ 4 | searchKey: "search-nyxkw1fuqex9qjhfvatbqfmw", 5 | engineName: "best-buy", 6 | endpointBase: "https://search-ui-sandbox.ent.us-central1.gcp.cloud.es.io" 7 | }); 8 | 9 | export const config = (filters) => ({ 10 | alwaysSearchOnInitialLoad: true, 11 | trackUrlState: false, 12 | initialState: { 13 | resultsPerPage: 8 14 | }, 15 | searchQuery: { 16 | filters, 17 | result_fields: { 18 | name: { 19 | raw: {} 20 | }, 21 | image: { raw: {} }, 22 | url: { raw: {} } 23 | } 24 | }, 25 | apiConnector: connector 26 | }); 27 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/_base.scss: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, 5 | *:before, 6 | *:after { 7 | box-sizing: inherit; 8 | } 9 | 10 | @import "modules/variables"; 11 | @import "modules/mixins"; 12 | @import "layouts/layout"; 13 | @import "components/error-boundary"; 14 | @import "components/facets"; 15 | @import "components/multi-checkbox-facet"; 16 | @import "components/boolean-facet"; 17 | @import "components/single-links-facet"; 18 | @import "components/paging"; 19 | @import "components/paging-info"; 20 | @import "components/result"; 21 | @import "components/results-container"; 22 | @import "components/results-per-page"; 23 | @import "components/search-box"; 24 | @import "components/sorting"; 25 | @import "components/select"; 26 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/src/responseAdapter.ts: -------------------------------------------------------------------------------- 1 | import { getFacets, getResults } from "./responseAdapters"; 2 | import type { SearchResponse } from "./types"; 3 | 4 | export default function adaptResponse( 5 | response: SearchResponse, 6 | documentType: string 7 | ) { 8 | const results = getResults(response.records, documentType); 9 | const totalPages = response.info[documentType].num_pages; 10 | const totalResults = response.info[documentType].total_result_count; 11 | const requestId = ""; 12 | const facets = getFacets(response.info[documentType]); 13 | 14 | return { 15 | rawResponse: response, 16 | results, 17 | totalPages, 18 | totalResults, 19 | requestId, 20 | ...(Object.keys(facets).length > 0 && { facets }) 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/modules/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Block 2 | @mixin block($block) { 3 | .sui-#{$block} { 4 | @content; 5 | } 6 | } 7 | 8 | // Block Element 9 | @mixin element($element) { 10 | &__#{$element} { 11 | @content; 12 | } 13 | } 14 | 15 | // Block Modifier 16 | @mixin modifier($modifier) { 17 | &--#{$modifier} { 18 | @content; 19 | } 20 | } 21 | 22 | //------------------------------------------------------ 23 | 24 | @mixin box-sizing { 25 | box-sizing: border-box; 26 | 27 | & > *, 28 | & > *::after, 29 | & > *::before { 30 | box-sizing: border-box; 31 | } 32 | } 33 | 34 | @mixin themeTypography { 35 | font-family: $fontFamily; 36 | line-height: $lineHeight; 37 | font-weight: $fontWeightNormal; 38 | } 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Which backends and packages are you using:** 27 | Backend: [App Search, Elasticsearch, Site Search, Workplace Search, custom] 28 | Packages: [react-search-ui, search-ui-app-search-connector, etc.] 29 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/Paging.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | Paging, 5 | PagingContainerProps, 6 | PagingViewProps 7 | } from "@elastic/react-search-ui-views"; 8 | import { useSearch } from "../hooks"; 9 | 10 | const PagingContainer = ({ 11 | className, 12 | view, 13 | ...rest 14 | }: PagingContainerProps) => { 15 | const { current, resultsPerPage, totalPages, setCurrent } = useSearch(); 16 | 17 | if (totalPages === 0) return null; 18 | 19 | const View: React.ComponentType = view || Paging; 20 | 21 | const viewProps = { 22 | className, 23 | current, 24 | resultsPerPage, 25 | totalPages, 26 | onChange: setCurrent, 27 | ...rest 28 | }; 29 | 30 | return ; 31 | }; 32 | export default PagingContainer; 33 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/PagingInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | PagingInfoContainerProps, 4 | PagingInfo, 5 | PagingInfoViewProps 6 | } from "@elastic/react-search-ui-views"; 7 | import { useSearch } from "../hooks"; 8 | 9 | const PagingInfoContainer = ({ 10 | className, 11 | view, 12 | ...rest 13 | }: PagingInfoContainerProps) => { 14 | const { pagingStart, pagingEnd, resultSearchTerm, totalResults } = 15 | useSearch(); 16 | const View = view || PagingInfo; 17 | const viewProps: PagingInfoViewProps = { 18 | className, 19 | searchTerm: resultSearchTerm, 20 | start: pagingStart, 21 | end: pagingEnd, 22 | totalResults: totalResults, 23 | ...rest 24 | }; 25 | 26 | return ; 27 | }; 28 | 29 | export default PagingInfoContainer; 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: true, 5 | commonjs: true, 6 | es2021: true 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:jest/recommended", 12 | "prettier" 13 | ], 14 | parserOptions: { 15 | ecmaFeatures: { 16 | jsx: true 17 | }, 18 | sourceType: "module" 19 | }, 20 | plugins: ["@typescript-eslint", "jest"], 21 | parser: "@typescript-eslint/parser", 22 | settings: { 23 | react: { 24 | version: "detect" 25 | }, 26 | jest: { 27 | version: "27" 28 | } 29 | }, 30 | rules: { 31 | "no-console": ["error", { allow: ["warn", "error"] }], 32 | "prefer-rest-params": "off", 33 | "@typescript-eslint/no-explicit-any": "off" 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /examples/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 35 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/__tests__/__snapshots__/Facet.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 34 | `; 35 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/src/buildResponseAdapterOptions.ts: -------------------------------------------------------------------------------- 1 | import type { QueryConfig } from "@elastic/search-ui"; 2 | 3 | export default function buildResponseAdapterOptions(config: QueryConfig = {}) { 4 | const additionalFacetValueFields = Object.entries(config.facets || {}).reduce( 5 | (acc, [fieldName, facetConfig]) => { 6 | if (facetConfig.unit && facetConfig.center) { 7 | return { 8 | ...(acc || {}), 9 | [fieldName]: { 10 | ...(facetConfig.unit && { unit: facetConfig.unit }), 11 | ...(facetConfig.center && { center: facetConfig.center }) 12 | } 13 | }; 14 | } 15 | 16 | return acc; 17 | }, 18 | null 19 | ); 20 | 21 | return { 22 | ...(additionalFacetValueFields && { additionalFacetValueFields }) 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | ErrorBoundary, 4 | ErrorBoundaryViewProps 5 | } from "@elastic/react-search-ui-views"; 6 | import { BaseContainerProps } from "../types"; 7 | import { useSearch } from "../hooks"; 8 | 9 | type ErrorBoundaryContainerProps = BaseContainerProps & { 10 | view?: React.ComponentType; 11 | }; 12 | const ErrorBoundaryContainer = ({ 13 | children, 14 | className, 15 | view, 16 | ...rest 17 | }: ErrorBoundaryContainerProps) => { 18 | const { error } = useSearch(); 19 | const View = view || ErrorBoundary; 20 | 21 | const viewProps = { 22 | className, 23 | children, 24 | error, 25 | ...rest 26 | }; 27 | 28 | return ; 29 | }; 30 | 31 | export default ErrorBoundaryContainer; 32 | -------------------------------------------------------------------------------- /packages/search-ui-workplace-search-connector/src/buildResponseAdapterOptions.ts: -------------------------------------------------------------------------------- 1 | import type { QueryConfig } from "@elastic/search-ui"; 2 | 3 | export default function buildResponseAdapterOptions(config: QueryConfig = {}) { 4 | const additionalFacetValueFields = Object.entries(config.facets || {}).reduce( 5 | (acc, [fieldName, facetConfig]) => { 6 | if (facetConfig.unit && facetConfig.center) { 7 | return { 8 | ...(acc || {}), 9 | [fieldName]: { 10 | ...(facetConfig.unit && { unit: facetConfig.unit }), 11 | ...(facetConfig.center && { center: facetConfig.center }) 12 | } 13 | }; 14 | } 15 | 16 | return acc; 17 | }, 18 | null 19 | ); 20 | 21 | return { 22 | ...(additionalFacetValueFields && { additionalFacetValueFields }) 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/Paging.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Paging from "../Paging"; 3 | import { shallow } from "enzyme"; 4 | import { PagingViewProps } from "../Paging"; 5 | 6 | const params = { 7 | current: 1, 8 | onChange: () => ({}), 9 | resultsPerPage: 10, 10 | totalPages: 100 11 | }; 12 | 13 | it("renders correctly", () => { 14 | const wrapper = shallow(); 15 | expect(wrapper).toMatchSnapshot(); 16 | }); 17 | 18 | it("renders with className prop applied", () => { 19 | const customClassName = "test-class"; 20 | const wrapper = shallow( 21 | 22 | ); 23 | const { className } = wrapper.dive().props() as PagingViewProps; 24 | expect(className).toEqual("rc-pagination sui-paging test-class"); 25 | }); 26 | -------------------------------------------------------------------------------- /examples/vue/src/components/SearchResults.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | 29 | 41 | -------------------------------------------------------------------------------- /examples/vue/README.md: -------------------------------------------------------------------------------- 1 | # vue 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Customize configuration 10 | 11 | See [Vite Configuration Reference](https://vitejs.dev/config/). 12 | 13 | ## Project Setup 14 | 15 | ```sh 16 | npm install 17 | ``` 18 | 19 | ### Compile and Hot-Reload for Development 20 | 21 | ```sh 22 | npm run dev 23 | ``` 24 | 25 | ### Compile and Minify for Production 26 | 27 | ```sh 28 | npm run build 29 | ``` 30 | 31 | ### Lint with [ESLint](https://eslint.org/) 32 | 33 | ```sh 34 | npm run lint 35 | ``` 36 | -------------------------------------------------------------------------------- /examples/vue/cleanseCards.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const snakecaseKeys = require("snakecase-keys"); 6 | 7 | fs.readFile( 8 | path.join(__dirname, "./src/data/cards-raw.json"), 9 | { encoding: "utf-8" }, 10 | function (err, data) { 11 | if (err) { 12 | return console.log(err); 13 | } 14 | 15 | const parsedData = JSON.parse(data); 16 | console.log(`${parsedData.length} cards found`); 17 | 18 | const newData = parsedData.map(snakecaseKeys); 19 | 20 | fs.writeFile( 21 | path.join(__dirname, "./src/data/cards-cleansed.json"), 22 | JSON.stringify(newData), 23 | function (err) { 24 | if (err) { 25 | return console.log(err); 26 | } 27 | 28 | console.log("The file was saved!"); 29 | } 30 | ); 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/ResultsPerPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | ResultsPerPage, 4 | ResultsPerPageContainerProps, 5 | ResultsPerPageViewProps 6 | } from "@elastic/react-search-ui-views"; 7 | import { useSearch } from "../hooks"; 8 | 9 | const ResultsPerPageContainer = ({ 10 | className, 11 | options = [20, 40, 60], 12 | view, 13 | ...rest 14 | }: ResultsPerPageContainerProps) => { 15 | const { resultsPerPage, setResultsPerPage } = useSearch(); 16 | const View = view || ResultsPerPage; 17 | const viewProps: ResultsPerPageViewProps = { 18 | className, 19 | onChange: (value) => { 20 | setResultsPerPage(value); 21 | }, 22 | options, 23 | value: resultsPerPage, 24 | ...rest 25 | }; 26 | 27 | return ; 28 | }; 29 | 30 | export default ResultsPerPageContainer; 31 | -------------------------------------------------------------------------------- /examples/sandbox/src/pages/ecommerce/index.jsx: -------------------------------------------------------------------------------- 1 | // create a simple react page with a list of regular a href links 2 | 3 | import * as React from "react"; 4 | import Navigation from "./components/Navigation"; 5 | import ProductCarousel from "./components/ProductCarousel"; 6 | import "./styles.scss"; 7 | 8 | export default function Ecommerce() { 9 | return ( 10 |
11 | 12 |
13 | 17 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/setSort.ts: -------------------------------------------------------------------------------- 1 | import { SortOption, SortDirection } from "../types"; 2 | /** 3 | * Set the current sort 4 | * 5 | * Will trigger new search 6 | * 7 | * @param sort SortList | string 8 | * @param sortDirection String ["asc"|"desc"] 9 | */ 10 | export default function setSort( 11 | sort: SortOption[] | string, 12 | sortDirection: SortDirection 13 | ): void { 14 | // eslint-disable-next-line no-console 15 | if (this.debug) console.log("Search UI: Action", "setSort", ...arguments); 16 | 17 | const update = { 18 | current: 1, 19 | sortList: null, 20 | sortField: null, 21 | sortDirection: null 22 | }; 23 | 24 | if (Array.isArray(sort)) { 25 | update.sortList = sort; 26 | } else { 27 | update.sortField = sort; 28 | update.sortDirection = sortDirection; 29 | } 30 | 31 | this._updateSearchResults(update); 32 | } 33 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { appendClassName } from "./view-helpers"; 4 | import type { SearchContextState } from "@elastic/search-ui"; 5 | import { BaseContainerProps } from "./types"; 6 | 7 | export type ErrorBoundaryContainerContext = Pick; 8 | 9 | export type ErrorBoundaryViewProps = BaseContainerProps & 10 | ErrorBoundaryContainerContext; 11 | 12 | function ErrorBoundary({ 13 | children, 14 | className, 15 | error, 16 | ...rest 17 | }: ErrorBoundaryViewProps & React.HTMLAttributes): JSX.Element { 18 | if (error) { 19 | return ( 20 |
21 | {error} 22 |
23 | ); 24 | } 25 | 26 | return children as JSX.Element; 27 | } 28 | 29 | export default ErrorBoundary; 30 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_facets.scss: -------------------------------------------------------------------------------- 1 | @include block("facet") { 2 | @include themeTypography; 3 | // fieldset reset 4 | margin: 0; 5 | padding: 0; 6 | border: 0; 7 | 8 | & + & { 9 | margin-top: $sizeXL; 10 | } 11 | 12 | .sui-sorting + & { 13 | margin-top: $sizeXL; 14 | } 15 | 16 | @include element("title") { 17 | text-transform: uppercase; 18 | font-size: 12px; 19 | color: #8b9bad; 20 | letter-spacing: 1px; 21 | // legend reset 22 | padding: 0; 23 | } 24 | 25 | @include element("list") { 26 | line-height: 1.5; 27 | font-size: 13px; 28 | margin: $sizeS 0; 29 | padding: 0; 30 | } 31 | 32 | @include element("count") { 33 | color: $facetCountColor; 34 | font-size: 0.85em; 35 | margin-left: 20px; 36 | display: inline-block; 37 | padding-top: 2px; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/view-helpers/getUrlSanitizer.ts: -------------------------------------------------------------------------------- 1 | const VALID_PROTOCOLS = ["http:", "https:"]; 2 | 3 | /** 4 | * 5 | * @param {URL} URLParser URL interface provided by browser https://developer.mozilla.org/en-US/docs/Web/API/URL 6 | * @param {String} currentLocation String representation of the browser's current location 7 | */ 8 | export default function getUrlSanitizer( 9 | URLParser: typeof URL, 10 | currentLocation: string 11 | ) { 12 | // This function is curried so that dependencies can be injected and don't need to be mocked in tests. 13 | return (url) => { 14 | let parsedUrl; 15 | 16 | try { 17 | // Attempts to parse a URL as relative 18 | parsedUrl = new URLParser(url, currentLocation); 19 | // eslint-disable-next-line no-empty 20 | } catch (e) {} 21 | 22 | return VALID_PROTOCOLS.includes(parsedUrl.protocol) ? url : ""; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/search-ui/src/test/sharedTests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/no-export */ 2 | 3 | import { doesStateHaveResponseData } from "./helpers"; 4 | 5 | export function itResetsCurrent(fn) { 6 | const state = fn(); 7 | it("resets current", () => { 8 | expect(state.current).toEqual(1); 9 | }); 10 | } 11 | 12 | export function itResetsFilters(fn) { 13 | const state = fn(); 14 | it("resets filters", () => { 15 | expect(state.filters).toEqual([]); 16 | }); 17 | } 18 | 19 | export function itFetchesResults(fn) { 20 | it("fetches results", () => { 21 | const state = fn(); 22 | expect(doesStateHaveResponseData(state)).toBe(true); 23 | }); 24 | } 25 | 26 | export function itUpdatesURLState(MockedURLManager, fn) { 27 | it("Updates URL state", () => { 28 | fn(); 29 | expect( 30 | MockedURLManager.mock.instances[0].pushStateToURL.mock.calls 31 | ).toHaveLength(1); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /docs/reference/guides-debugging.md: -------------------------------------------------------------------------------- 1 | --- 2 | mapped_pages: 3 | - https://www.elastic.co/guide/en/search-ui/current/guides-debugging.html 4 | --- 5 | 6 | # Debugging [guides-debugging] 7 | 8 | There is a `debug` flag available on the configuration for `SearchDriver` and `SearchProvider`. 9 | 10 | ```jsx 11 | 15 | ``` 16 | 17 | Setting this to `true` will make the `searchUI` object available globally on window. This will allow you to programmatically execute actions in the browser console which can be helpful for debugging. 18 | 19 | ```js 20 | window.searchUI.addFilter("states", "California", "all"); 21 | ``` 22 | 23 | This will also log actions and state updates as they occur to the console in the following form: 24 | 25 | ```txt 26 | Search UI: Action {Action Name} {Action Parameters} 27 | Search UI: State Update {State to update} {Full State after update} 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/vue/src/components/SearchPagingInfo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/src/handlers/transporter.ts: -------------------------------------------------------------------------------- 1 | import { SearchRequest, SearchResponse, Transporter } from "../types"; 2 | 3 | export class EngineTransporter implements Transporter { 4 | constructor( 5 | private host: string, 6 | private engineName: string, 7 | private apiKey: string 8 | ) {} 9 | 10 | async performRequest(requestBody: SearchRequest): Promise { 11 | if (!fetch) 12 | throw new Error("Fetch is not supported in this browser / environment"); 13 | 14 | const response = await fetch( 15 | `${this.host}/api/engines/${this.engineName}/_search`, 16 | { 17 | method: "POST", 18 | body: JSON.stringify(requestBody), 19 | headers: { 20 | "Content-Type": "application/json", 21 | Authorization: `ApiKey ${this.apiKey}` 22 | } 23 | } 24 | ); 25 | const json = await response.json(); 26 | return json; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/src/handlers/common.ts: -------------------------------------------------------------------------------- 1 | import type { AutocompletedResult, SearchResult } from "@elastic/search-ui"; 2 | import type { SearchkitHit } from "@searchkit/sdk"; 3 | 4 | export function fieldResponseMapper( 5 | item: SearchkitHit 6 | ): SearchResult | AutocompletedResult { 7 | const fields = item.fields; 8 | const highlights = item.highlight || {}; 9 | const combinedFieldKeys = [ 10 | ...new Set(Object.keys(fields).concat(Object.keys(highlights))) 11 | ]; 12 | return combinedFieldKeys.reduce( 13 | (acc, key) => { 14 | return { 15 | ...acc, 16 | [key]: { 17 | ...(fields[key] ? { raw: fields[key] } : {}), 18 | ...(highlights[key] ? { snippet: highlights[key] } : {}) 19 | } 20 | }; 21 | }, 22 | { 23 | id: { raw: item.id }, 24 | _meta: { 25 | id: item.rawHit._id, 26 | rawHit: item.rawHit 27 | } 28 | } 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/Result.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Result, 4 | ResultContainerProps, 5 | ResultViewProps 6 | } from "@elastic/react-search-ui-views"; 7 | import { useSearch } from "../hooks"; 8 | 9 | const ResultContainer = ({ 10 | result, 11 | shouldTrackClickThrough = true, 12 | clickThroughTags = [], 13 | view, 14 | ...rest 15 | }: ResultContainerProps) => { 16 | const { trackClickThrough } = useSearch(); 17 | 18 | const handleClickLink = (id: string) => { 19 | if (shouldTrackClickThrough) { 20 | trackClickThrough(id, clickThroughTags); 21 | } 22 | }; 23 | const View = view || Result; 24 | const id = result.id.raw; 25 | const viewProps: ResultViewProps = { 26 | result: result, 27 | key: `result-${id}`, 28 | onClickLink: () => handleClickLink(id), 29 | ...rest 30 | }; 31 | 32 | return ; 33 | }; 34 | 35 | export default ResultContainer; 36 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Autocomplete } from "./Autocomplete"; 2 | export { default as ErrorBoundary } from "./ErrorBoundary"; 3 | export { default as Facets } from "./Facets"; 4 | export { default as MultiCheckboxFacet } from "./MultiCheckboxFacet"; 5 | export { default as BooleanFacet } from "./BooleanFacet"; 6 | export { default as Paging } from "./Paging"; 7 | export { default as PagingInfo } from "./PagingInfo"; 8 | export { default as Result } from "./Result"; 9 | export { default as Results } from "./Results"; 10 | export { default as ResultsPerPage } from "./ResultsPerPage"; 11 | export { default as SearchBox } from "./SearchBox"; 12 | export { default as SingleSelectFacet } from "./SingleSelectFacet"; 13 | export { default as SingleLinksFacet } from "./SingleLinksFacet"; 14 | export { default as Sorting } from "./Sorting"; 15 | export { Layout } from "./layouts"; 16 | export * from "./types"; 17 | export * as ViewHelpers from "./view-helpers"; 18 | -------------------------------------------------------------------------------- /examples/vue/src/components/SearchHeader.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 32 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/ResultsPerPage.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ResultsPerPage from "../ResultsPerPage"; 3 | import { shallow } from "enzyme"; 4 | 5 | const requiredProps = { 6 | onChange: () => ({}), 7 | options: [20, 40] 8 | }; 9 | 10 | it("renders correctly when there is a selected value", () => { 11 | const wrapper = shallow(); 12 | expect(wrapper).toMatchSnapshot(); 13 | }); 14 | 15 | it("renders correctly when there is not a selected value", () => { 16 | const wrapper = shallow(); 17 | expect(wrapper).toMatchSnapshot(); 18 | }); 19 | 20 | it("renders with className prop applied", () => { 21 | const customClassName = "test-class"; 22 | const wrapper = shallow( 23 | 24 | ); 25 | const { className } = wrapper.props(); 26 | expect(className).toEqual("sui-results-per-page test-class"); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/ErrorBoundary.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundary from "../ErrorBoundary"; 3 | import { shallow } from "enzyme"; 4 | 5 | const params = { 6 | children:
Child
, 7 | error: "I am an error" 8 | }; 9 | 10 | it("renders an error when there is an error", () => { 11 | const wrapper = shallow(); 12 | expect(wrapper).toMatchSnapshot(); 13 | }); 14 | 15 | it("renders children when there is no error", () => { 16 | const wrapper = shallow( 17 | 23 | ); 24 | expect(wrapper).toMatchSnapshot(); 25 | }); 26 | 27 | it("renders with className prop applied", () => { 28 | const customClassName = "test-class"; 29 | const wrapper = shallow( 30 | 31 | ); 32 | const { className } = wrapper.props(); 33 | expect(className).toEqual("sui-search-error test-class"); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/Results.tsx: -------------------------------------------------------------------------------- 1 | import type { SearchResult } from "@elastic/search-ui"; 2 | import React from "react"; 3 | import { BaseContainerProps } from "./types"; 4 | import { ResultViewProps } from "./Result"; 5 | 6 | import { appendClassName } from "./view-helpers"; 7 | 8 | export type ResultsContainerProps = BaseContainerProps & { 9 | view?: React.ComponentType; 10 | resultView?: React.ComponentType; 11 | clickThroughTags?: string[]; 12 | titleField?: string; 13 | urlField?: string; 14 | thumbnailField?: string; 15 | shouldTrackClickThrough?: boolean; 16 | }; 17 | 18 | export type ResultsViewProps = BaseContainerProps; 19 | 20 | function Results({ 21 | children, 22 | className, 23 | ...rest 24 | }: ResultsViewProps & React.HTMLAttributes) { 25 | return ( 26 |
    30 | {children} 31 |
32 | ); 33 | } 34 | 35 | export default Results; 36 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/src/request.ts: -------------------------------------------------------------------------------- 1 | export default async function request( 2 | engineKey: string, 3 | method: string, 4 | path: string, 5 | params: Record 6 | ) { 7 | const headers = new Headers({ 8 | "Content-Type": "application/json" 9 | }); 10 | 11 | const response = await fetch( 12 | `https://search-api.swiftype.com/api/v1/public/${path}`, 13 | { 14 | method, 15 | headers, 16 | body: JSON.stringify({ 17 | engine_key: engineKey, 18 | ...params 19 | }), 20 | credentials: "include" 21 | } 22 | ); 23 | 24 | let json; 25 | try { 26 | json = await response.json(); 27 | } catch (error) { 28 | // Nothing to do here, certain responses won't have json 29 | } 30 | 31 | if (response.status >= 200 && response.status < 300) { 32 | return json; 33 | } else { 34 | const message = 35 | json && json.errors && Object.entries(json.errors).length > 0 36 | ? JSON.stringify(json.errors) 37 | : response.status; 38 | throw new Error(`${message}`); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/sandbox/src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | /* Base variables */ 7 | --font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 8 | 9 | /* Light theme variables */ 10 | --background: #ffffff; 11 | --foreground: #213547; 12 | --scroll-bar-color: #000; 13 | --scroll-bar-background: #fff; 14 | } 15 | 16 | /* Dark theme variables */ 17 | @media (prefers-color-scheme: dark) { 18 | :root { 19 | color-scheme: dark; 20 | --background: #242424; 21 | --foreground: #ffffffde; 22 | --scroll-bar-color: #ffffff40; 23 | --scroll-bar-background: #0000; 24 | --text-secondary: #ffffff40; 25 | } 26 | } 27 | 28 | body { 29 | font-family: var(--font-family); 30 | background-color: var(--background); 31 | color: var(--foreground); 32 | scrollbar-color: var(--scroll-bar-color) var(--scroll-bar-background); 33 | } 34 | 35 | .ExampleApp { 36 | background-color: #ffffff; 37 | color: #213547; 38 | color-scheme: auto; 39 | } 40 | 41 | .sui-layout-main-header { 42 | margin-bottom: 1rem; 43 | } 44 | -------------------------------------------------------------------------------- /examples/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search-ui-vue-example", 3 | "description": "Elastic Search UI Vue.js example", 4 | "keywords": [ 5 | "search", 6 | "interface", 7 | "ui", 8 | "example", 9 | "elastic", 10 | "elasticsearch", 11 | "vue" 12 | ], 13 | "version": "1.20.2", 14 | "private": true, 15 | "scripts": { 16 | "dev": "vite", 17 | "build": "vite build", 18 | "preview": "vite preview --port 4173", 19 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" 20 | }, 21 | "dependencies": { 22 | "@elastic/react-search-ui-views": "1.20.2", 23 | "@elastic/search-ui": "1.20.2", 24 | "@elastic/search-ui-app-search-connector": "1.20.2", 25 | "vue": "^3.2.37" 26 | }, 27 | "devDependencies": { 28 | "@rushstack/eslint-patch": "^1.1.0", 29 | "@vitejs/plugin-vue": "^2.3.3", 30 | "@vitejs/plugin-vue-jsx": "^1.3.10", 31 | "@vue/eslint-config-prettier": "^7.0.0", 32 | "eslint": "^8.5.0", 33 | "eslint-plugin-vue": "^9.26.0", 34 | "prettier": "^2.5.1", 35 | "vite": "^6.0.13" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/src/handlers/handleSearch.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | QueryConfig, 3 | RequestState, 4 | ResponseState 5 | } from "@elastic/search-ui"; 6 | import { SearchQueryBuilder } from "../queryBuilders/SearchQueryBuilder"; 7 | import { transformSearchResponse } from "../transformer/responseTransformer"; 8 | import type { IApiClientTransporter } from "../transporter/ApiClientTransporter"; 9 | import type { PostProcessRequestBodyFn } from "../types"; 10 | 11 | export const handleSearch = async ( 12 | state: RequestState, 13 | queryConfig: QueryConfig, 14 | apiClient: IApiClientTransporter, 15 | postProcessRequestBodyFn?: PostProcessRequestBodyFn 16 | ): Promise => { 17 | const queryBuilder = new SearchQueryBuilder(state, queryConfig); 18 | let requestBody = queryBuilder.build(); 19 | 20 | if (postProcessRequestBodyFn) { 21 | requestBody = postProcessRequestBodyFn(requestBody, state, queryConfig); 22 | } 23 | 24 | const response = await apiClient.performRequest(requestBody); 25 | 26 | return transformSearchResponse(response, queryBuilder); 27 | }; 28 | -------------------------------------------------------------------------------- /docs/reference/api-react-use-search.md: -------------------------------------------------------------------------------- 1 | --- 2 | mapped_pages: 3 | - https://www.elastic.co/guide/en/search-ui/current/api-react-use-search.html 4 | --- 5 | 6 | # useSearch hook [api-react-use-search] 7 | 8 | In addition, to using `withSearch` you can now use `useSearch` hook in your custom react functional component. 9 | 10 | ## Usage 11 | 12 | In order to use this hook, you should wrap your functional component in `SearchProvider`. We will see an example on how to use useSearch hook to render "loading" indicator when the application is fetching data. 13 | 14 | ``` 15 | const Component = () => { 16 | const { isLoading } = useSearch(); 17 | return ( 18 |
19 | {isLoading &&
I'm loading now
} 20 | {!isLoading && ( 21 | } 23 | bodyContent={} 24 | /> 25 | )} 26 |
27 | ); 28 | }; 29 | export const App = () => { 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | ``` 38 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import baseConfig from "../../tsup.config.ts"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | import sass from "sass"; 5 | import postcss from "postcss"; 6 | import autoprefixer from "autoprefixer"; 7 | 8 | export default { 9 | ...baseConfig, 10 | external: ["react", "react-dom"], 11 | async onSuccess() { 12 | /* eslint-disable no-console */ 13 | console.log("🚀 Compiling SCSS..."); 14 | const result = sass.renderSync({ 15 | file: "src/styles/styles.scss", 16 | outputStyle: "compressed" 17 | }); 18 | 19 | /* eslint-disable no-console */ 20 | console.log("🔄 Processing with PostCSS..."); 21 | const processed = await postcss([autoprefixer]).process(result.css, { 22 | from: undefined 23 | }); 24 | 25 | const outputPath = path.resolve("lib/styles/styles.css"); 26 | await fs.mkdir(path.dirname(outputPath), { recursive: true }); 27 | await fs.writeFile(outputPath, processed.css); 28 | 29 | /* eslint-disable no-console */ 30 | console.log("✅ Styles compiled to lib/styles/styles.css"); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /.ci/jobs/searchui.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - job: 3 | name: searchui/searchui 4 | description: "Builds and run tests against the elastic/searchui repo" 5 | project-type: multibranch 6 | node: main 7 | concurrent: true 8 | script-path: .ci/pipelines/searchui.groovy 9 | prune-dead-branches: true 10 | scm: 11 | - github: 12 | repo: searchui 13 | repo-owner: elastic 14 | branch-discovery: "no-pr" 15 | discover-pr-origin: "both" 16 | discover-pr-forks-strategy: "merge-current" 17 | discover-pr-forks-trust: "permission" 18 | discover-tags: true 19 | build-strategies: 20 | - regular-branches: true 21 | property-strategies: 22 | all-branches: 23 | - pipeline-branch-durability-override: performance-optimized 24 | credentials-id: 2a9602aa-ab9f-4e52-baf3-b71ca88469c7-UserAndToken 25 | ssh-checkout: 26 | credentials: f6c7695a-671e-4f4f-a331-acdce44ff9ba 27 | clean: 28 | after: true 29 | before: true 30 | prune: true 31 | timeout: 10 32 | -------------------------------------------------------------------------------- /.ci/jobs/search-ui.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - job: 3 | name: search-ui/search-ui 4 | description: "Builds and run tests against the elastic/search-ui repo" 5 | project-type: multibranch 6 | node: main 7 | concurrent: true 8 | script-path: .ci/pipelines/search-ui.groovy 9 | prune-dead-branches: true 10 | scm: 11 | - github: 12 | repo: search-ui 13 | repo-owner: elastic 14 | branch-discovery: "no-pr" 15 | discover-pr-origin: "both" 16 | discover-pr-forks-strategy: "merge-current" 17 | discover-pr-forks-trust: "permission" 18 | discover-tags: true 19 | build-strategies: 20 | - regular-branches: true 21 | property-strategies: 22 | all-branches: 23 | - pipeline-branch-durability-override: performance-optimized 24 | credentials-id: 2a9602aa-ab9f-4e52-baf3-b71ca88469c7-UserAndToken 25 | ssh-checkout: 26 | credentials: f6c7695a-671e-4f4f-a331-acdce44ff9ba 27 | clean: 28 | after: true 29 | before: true 30 | prune: true 31 | timeout: 10 32 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/Sorting.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Sorting from "../Sorting"; 3 | import { shallow } from "enzyme"; 4 | import { SortingViewProps } from "../Sorting"; 5 | 6 | const requiredProps: SortingViewProps = { 7 | onChange: jest.fn(), 8 | options: [ 9 | { label: "Name ASC", value: "name|||asc" }, 10 | { label: "Name DESC", value: "name|||desc" } 11 | ], 12 | value: "name|||asc" 13 | }; 14 | 15 | it("renders correctly when there is a value", () => { 16 | const wrapper = shallow(); 17 | expect(wrapper).toMatchSnapshot(); 18 | }); 19 | 20 | it("renders correctly when there is not a value", () => { 21 | const wrapper = shallow(); 22 | expect(wrapper).toMatchSnapshot(); 23 | }); 24 | 25 | it("renders with className prop applied", () => { 26 | const customClassName = "test-class"; 27 | const wrapper = shallow( 28 | 29 | ); 30 | const { className } = wrapper.props(); 31 | expect(className).toEqual("sui-sorting test-class"); 32 | }); 33 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: cimg/node:18.19.1 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v2-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v2-dependencies- 28 | - run: yarn install 29 | 30 | - save_cache: 31 | paths: 32 | - node_modules 33 | key: v2-dependencies-{{ checksum "package.json" }} 34 | 35 | - run: yarn build 36 | # run tests! 37 | - run: npm run test-ci 38 | -------------------------------------------------------------------------------- /docs/reference/guides-using-search-as-you-type.md: -------------------------------------------------------------------------------- 1 | --- 2 | mapped_pages: 3 | - https://www.elastic.co/guide/en/search-ui/current/guides-using-search-as-you-type.html 4 | --- 5 | 6 | # Using search-as-you-type [guides-using-search-as-you-type] 7 | 8 | Usually, the search query is executed when the user presses the enter key or clicks the search button. 9 | 10 | Search-as-you-type feature allows search queries to be executed on every keystroke. 11 | 12 | To implement this in Search UI, you’ll need to add a `searchAsYouType={{true}}` prop to `` component. 13 | 14 | It’s a good idea to add a debounce time — the Search UI will wait for users to finish typing before issuing the request. You can do it by adding a `debounceLength={{300}}` prop to `` component. 15 | 16 | :::{tip} 17 | See how search-as-you-type works in our [CodeSandbox demo](https://codesandbox.io/embed/github/elastic/search-ui/tree/main/examples/sandbox?autoresize=1&fontsize=12&initialpath=%2Fsearch-as-you-type&module=%2Fsrc%2Fpages%2Fsearch-as-you-type%2Findex.jsx). 18 | ::: 19 | 20 | Related documentation: 21 | 22 | - [SearchBox component](/reference/api-react-components-search-box.md) 23 | -------------------------------------------------------------------------------- /packages/search-ui/src/RequestSequencer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This deals with sequencing of our async requests. When a lot of requests are firing very close to one another 3 | and are running in parallel, what happens if they return out of order? It creates a race condition. 4 | 5 | For example, if I type the term "react" in the search box, two queries may be initiated, in parallel. 6 | 7 | 1. query for "reac" 8 | 2. query for "react" 9 | 10 | If the query for "react" actually returns **before** the query for "reac", 11 | we could end up looking at the results for "reac", despite having typed "react" in the search box. 12 | 13 | To deal with this, we keep track of a sequence. 14 | */ 15 | type RequestSequence = number; 16 | 17 | export default class RequestSequencer { 18 | requestSequence: RequestSequence = 0; 19 | lastCompleted: RequestSequence = 0; 20 | 21 | next(): RequestSequence { 22 | return ++this.requestSequence; 23 | } 24 | 25 | isOldRequest(request: RequestSequence): boolean { 26 | return request < this.lastCompleted; 27 | } 28 | 29 | completed(request: RequestSequence): void { 30 | this.lastCompleted = request; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/a11yNotify.ts: -------------------------------------------------------------------------------- 1 | import { announceToScreenReader } from "../A11yNotifications"; 2 | 3 | /** 4 | * Announces a specific message in `a11yNotificationMessages` 5 | * to Search UI's screen reader live region. 6 | * 7 | * @param {string} messageFunc - key of a message function in `a11yNotificationMessages` 8 | * @param {object} [messageArgs] - arguments to pass to the message function, if any 9 | */ 10 | export default function a11yNotify( 11 | messageFunc: string, 12 | messageArgs?: unknown 13 | ): void { 14 | if (!this.hasA11yNotifications) return; 15 | 16 | const getMessage = this.a11yNotificationMessages[messageFunc]; 17 | 18 | if (!getMessage) { 19 | const errorMessage = `Could not find corresponding message function in a11yNotificationMessages: "${messageFunc}"`; 20 | console.warn("Action", "a11yNotify", errorMessage); 21 | return; 22 | } 23 | 24 | const message = getMessage(messageArgs); 25 | announceToScreenReader(message); 26 | 27 | if (this.debug) { 28 | // eslint-disable-next-line no-console 29 | console.log("Search UI: Action", "a11yNotify", { 30 | messageFunc, 31 | messageArgs, 32 | message 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/trackAutocompleteSuggestionClickThrough.ts: -------------------------------------------------------------------------------- 1 | import Events from "../Events"; 2 | 3 | /** 4 | * Report a autocomplete suggestion click through event. A click through event is when a user 5 | * clicks on a suggestion within an autocomplete Dropdown. 6 | * 7 | * @param suggestionQuery String The suggestion query that was 8 | * clicked 9 | * @param position Number The position of the suggestion query. Zero based. 10 | * @param tag Array[String] Optional Tags which can be used to categorize 11 | * this click event 12 | */ 13 | export default function trackAutocompleteSuggestionClickThrough( 14 | suggestionQuery: string, 15 | position: number, 16 | tags: string[] = [] 17 | ): void { 18 | if (this.debug) { 19 | // eslint-disable-next-line no-console 20 | console.log( 21 | "Search UI: Action", 22 | "trackAutocompleteSuggestionClickThrough", 23 | ...arguments 24 | ); 25 | } 26 | 27 | const { searchTerm } = this.state; 28 | 29 | const events: Events = this.events; 30 | 31 | events.emit({ 32 | type: "AutocompleteSuggestionSelected", 33 | position, 34 | query: searchTerm, 35 | tags, 36 | suggestion: suggestionQuery 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /docs/reference/guides-adding-search-bar-to-header.md: -------------------------------------------------------------------------------- 1 | --- 2 | mapped_pages: 3 | - https://www.elastic.co/guide/en/search-ui/current/guides-adding-search-bar-to-header.html 4 | --- 5 | 6 | # Adding search bar to header [guides-adding-search-bar-to-header] 7 | 8 | It’s a common pattern to have a search bar in the header of your website. Submitting a query in this search bar usually redirects user to a separate search results page. 9 | 10 | To implement this with Search UI use the `` component and add `onSubmit` prop that redirects user to the search page: 11 | 12 | ```js 13 | { 15 | window.location.href = `${PATH_TO_YOUR_SEARCH_PAGE}?q=${searchTerm}`; 16 | }} 17 | /> 18 | ``` 19 | 20 | Once the redirect happens, Search UI will pick up the query from the URL and display the results. 21 | 22 | :::{tip} 23 | View this implementation in our [CodeSandbox example](https://codesandbox.io/embed/github/elastic/search-ui/tree/main/examples/sandbox?autoresize=1&fontsize=12&initialpath=%2Fsearch-bar-in-header&module=%2Fsrc%2Fpages%2Fsearch-bar-in-header%2Findex.jsx). 24 | ::: 25 | 26 | Related documentation: 27 | 28 | - [SearchBox component](/reference/api-react-components-search-box.md) 29 | -------------------------------------------------------------------------------- /packages/search-ui/src/__tests__/actions/trackAutocompleteSuggestionClickThrough.test.ts: -------------------------------------------------------------------------------- 1 | import { setupDriver, mockPlugin } from "../../test/helpers"; 2 | 3 | describe("#trackAutocompleteSuggestionClickThrough", () => { 4 | beforeEach(() => { 5 | jest.clearAllMocks(); 6 | }); 7 | 8 | function subject( 9 | { initialState }: { initialState? } = {}, 10 | documentId?, 11 | tags? 12 | ) { 13 | const { driver, mockApiConnector, updatedStateAfterAction } = setupDriver({ 14 | initialState 15 | }); 16 | driver.setSearchTerm("terms", { 17 | autocompleteSuggestions: true 18 | }); 19 | driver.state.autocompletedSuggestions = { 20 | test: [ 21 | { 22 | suggestion: "test" 23 | } 24 | ] 25 | }; 26 | driver.trackAutocompleteSuggestionClickThrough("search terms", 1, tags); 27 | return { driver, mockApiConnector, updatedStateAfterAction }; 28 | } 29 | 30 | it("Calls events plugin with correct parameters", () => { 31 | subject(); 32 | expect(mockPlugin.subscribe).toBeCalledWith({ 33 | position: 1, 34 | query: "terms", 35 | suggestion: "search terms", 36 | tags: [], 37 | type: "AutocompleteSuggestionSelected" 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/layouts/LayoutSidebar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | 4 | import LayoutSidebar from "../../layouts/LayoutSidebar"; 5 | 6 | it("renders correctly", () => { 7 | const wrapper = shallow( 8 | Hello world! 9 | ); 10 | expect(wrapper).toMatchSnapshot(); 11 | }); 12 | 13 | it("renders toggled class based on state", () => { 14 | const wrapper = shallow( 15 | Hello world! 16 | ); 17 | wrapper.setState({ isSidebarToggled: true }); 18 | 19 | expect(wrapper.find(".sui-layout-sidebar--toggled")).toHaveLength(1); 20 | }); 21 | 22 | it("updates isSidebarToggled state on button click", () => { 23 | const wrapper = shallow( 24 | Hello world! 25 | ); 26 | expect(wrapper.state("isSidebarToggled")).toEqual(false); 27 | const buttons = wrapper.find(".sui-layout-sidebar-toggle"); 28 | 29 | buttons.first().simulate("click"); 30 | expect(wrapper.state("isSidebarToggled")).toEqual(true); 31 | 32 | buttons.last().simulate("click"); 33 | expect(wrapper.state("isSidebarToggled")).toEqual(false); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/layouts/Layout.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Layout } from "../.."; 3 | import { shallow } from "enzyme"; 4 | 5 | it("renders correctly", () => { 6 | const wrapper = shallow( 7 | Header
} 9 | sideContent={ 10 |
11 |
Side Content
12 |
13 | } 14 | bodyContent={
Body Content
} 15 | bodyHeader={
Body Header
} 16 | bodyFooter={
Body Footer
} 17 | /> 18 | ); 19 | expect(wrapper).toMatchSnapshot(); 20 | }); 21 | 22 | it("will accept children instead of bodyContent", () => { 23 | const wrapper = shallow( 24 | Header} 26 | sideContent={ 27 |
28 |
Side Content
29 |
30 | } 31 | bodyHeader={
Body Header
} 32 | bodyFooter={
Body Footer
} 33 | > 34 |
Body Content
35 |
36 | ); 37 | expect(wrapper).toMatchSnapshot(); 38 | }); 39 | 40 | it("renders with className prop applied", () => { 41 | const wrapper = shallow(); 42 | 43 | expect(wrapper.hasClass("test-class")).toBeTruthy(); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/Paging.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RCPagination from "rc-pagination"; 3 | import enUsLocale from "rc-pagination/lib/locale/en_US.js"; 4 | 5 | import { appendClassName } from "./view-helpers"; 6 | import type { SearchContextState } from "@elastic/search-ui"; 7 | import { Rename, BaseContainerProps } from "./types"; 8 | 9 | export type PagingContainerContext = Pick< 10 | SearchContextState, 11 | "current" | "resultsPerPage" | "totalPages" | "setCurrent" 12 | >; 13 | 14 | export type PagingViewProps = Rename< 15 | BaseContainerProps & PagingContainerContext, 16 | { 17 | setCurrent: "onChange"; 18 | } 19 | >; 20 | 21 | export type PagingContainerProps = BaseContainerProps & { 22 | view?: React.ComponentType; 23 | }; 24 | 25 | function Paging({ 26 | className, 27 | current, 28 | resultsPerPage, 29 | onChange, 30 | totalPages, 31 | ...rest 32 | }: PagingViewProps) { 33 | return ( 34 | 43 | ); 44 | } 45 | 46 | export default Paging; 47 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/__snapshots__/BooleanFacet.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders 1`] = ` 4 |
7 | 10 | A Facet 11 | 12 |
15 |
18 | 43 |
44 |
45 |
46 | `; 47 | 48 | exports[`will not render when there are no true options 1`] = `""`; 49 | -------------------------------------------------------------------------------- /examples/sandbox/src/pages/ecommerce/components/ProductCarousel/index.jsx: -------------------------------------------------------------------------------- 1 | import { Results, SearchProvider } from "@elastic/react-search-ui"; 2 | import { config } from "./config"; 3 | 4 | const CustomResultsView = ({ children }) => { 5 | return ( 6 |
7 |
    {children}
8 |
9 | ); 10 | }; 11 | 12 | const CustomResultView = ({ result }) => { 13 | return ( 14 |
  • 18 | 19 | {result.name.raw} 24 |

    {result.name.raw}

    25 |
    26 |
  • 27 | ); 28 | }; 29 | 30 | export default function ProductCarousel(props) { 31 | return ( 32 | 33 |
    34 |

    35 | {props.title} 36 |

    37 | 38 |
    39 |
    40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /examples/sandbox/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import react from "eslint-plugin-react"; 4 | import reactHooks from "eslint-plugin-react-hooks"; 5 | import reactRefresh from "eslint-plugin-react-refresh"; 6 | 7 | export default [ 8 | { ignores: ["dist"] }, 9 | { 10 | files: ["**/*.{js,jsx}"], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: "latest", 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: "module" 18 | } 19 | }, 20 | settings: { react: { version: "18.3" } }, 21 | plugins: { 22 | react, 23 | "react-hooks": reactHooks, 24 | "react-refresh": reactRefresh 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs["jsx-runtime"].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | "react/jsx-no-target-blank": "off", 32 | "react-refresh/only-export-components": [ 33 | "warn", 34 | { allowConstantExport: true } 35 | ] 36 | }, 37 | overrides: [ 38 | { 39 | files: ["server/**/*.js"], 40 | env: { 41 | node: true 42 | } 43 | } 44 | ] 45 | } 46 | ]; 47 | -------------------------------------------------------------------------------- /packages/search-ui/src/preserveTypesEncoder.ts: -------------------------------------------------------------------------------- 1 | function isTypeNumber(value): boolean { 2 | return value !== undefined && value !== null && typeof value === "number"; 3 | } 4 | 5 | function isTypeBoolean(value): boolean { 6 | return value && typeof value === "boolean"; 7 | } 8 | 9 | function toBoolean(value): boolean { 10 | if (value === "true") return true; 11 | if (value === "false") return false; 12 | throw "Invalid type parsed as Boolean value"; 13 | } 14 | 15 | /* Encoder for qs library which preserve number types on the URL. Numbers 16 | are padded with "n_{number}_n", and booleans with "b_{boolean}_b"*/ 17 | 18 | export default { 19 | encode(value, encode): string { 20 | if (isTypeNumber(value)) { 21 | return `n_${value}_n`; 22 | } 23 | if (isTypeBoolean(value)) { 24 | return `b_${value}_b`; 25 | } 26 | return encode(value); 27 | }, 28 | decode(value, decode) { 29 | //eslint-disable-next-line 30 | if (/n_-?[\d\.]*_n/.test(value)) { 31 | const numericValueString = value.substring(2, value.length - 2); 32 | return Number(numericValueString); 33 | } 34 | if (/^b_(true|false)*_b$/.test(value)) { 35 | const booleanValueString = value.substring(2, value.length - 2); 36 | return toBoolean(booleanValueString); 37 | } 38 | return decode(value); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/Results.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Result, 4 | Results, 5 | ResultsContainerProps 6 | } from "@elastic/react-search-ui-views"; 7 | 8 | import { Result as ResultContainer } from "."; 9 | import { useSearch } from "../hooks"; 10 | 11 | function getRaw(result, value) { 12 | if (!result[value] || !result[value].raw) return; 13 | return result[value].raw; 14 | } 15 | 16 | const ResultsContainer = ({ 17 | clickThroughTags = [], 18 | resultView, 19 | shouldTrackClickThrough = true, 20 | titleField, 21 | urlField, 22 | thumbnailField, 23 | view, 24 | ...rest 25 | }: ResultsContainerProps) => { 26 | const { results } = useSearch(); 27 | 28 | const View = view || Results; 29 | const ResultView = resultView || Result; 30 | 31 | const children = results.map((result) => ( 32 | 42 | )); 43 | const viewProps = { 44 | children, 45 | ...rest 46 | }; 47 | 48 | return ; 49 | }; 50 | 51 | export default ResultsContainer; 52 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_paging.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../../../../node_modules/rc-pagination/assets/index"; 2 | @include block("paging") { 3 | font-size: 12px; 4 | 5 | .rc-pagination-prev, 6 | .rc-pagination-next, 7 | .rc-pagination-item { 8 | border: none; 9 | background: transparent; 10 | 11 | .rc-pagination-item-link { 12 | font-size: 18px; 13 | } 14 | 15 | > a, 16 | > button { 17 | border: none; 18 | background: transparent; 19 | color: $facetLinkColor; 20 | 21 | &:hover { 22 | color: $facetLinkColor; 23 | background: $pagingHoverBackground; 24 | } 25 | } 26 | } 27 | 28 | .rc-pagination-disabled { 29 | .rc-pagination-item-link { 30 | color: #ccc; 31 | opacity: 0.8; 32 | 33 | &:hover { 34 | color: #ccc; 35 | } 36 | } 37 | } 38 | 39 | .rc-pagination-item { 40 | a { 41 | color: $linkColor; 42 | text-decoration: none; 43 | } 44 | } 45 | 46 | .rc-pagination-item-active { 47 | a { 48 | color: $facetLinkColor; 49 | font-weight: $fontWeightBold; 50 | } 51 | 52 | &:hover { 53 | background: transparent; 54 | cursor: not-allowed; 55 | 56 | a { 57 | color: $facetLinkColor; 58 | cursor: not-allowed; 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/reference/advanced-usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | navigation_title: "Advanced usage" 3 | mapped_pages: 4 | - https://www.elastic.co/guide/en/search-ui/current/guides-using-with-vue-js.html 5 | --- 6 | 7 | # Using with Vue.js [guides-using-with-vue-js] 8 | 9 | While Search UI doesn’t have a component library for Vue.js, you can still use them together, just with a little extra work. 10 | 11 | To achieve that you’ll need to import and work directly with Search UI Core API. Read a short [API reference](/reference/api-reference.md) guide for high-level overview on the approach. 12 | 13 | Once you’re familiar with the concept of Search UI core, the following docs will give you deeper dive into available APIs: 14 | 15 | - [Core API](/reference/api-core-configuration.md) 16 | - [State](/reference/api-core-state.md) 17 | - [Actions](/reference/api-core-actions.md) 18 | 19 | ## Example [guides-using-with-vue-js-example] 20 | 21 | The following demo is a good starting point for your Search UI implementation. The source code is available on GitHub: [https://github.com/elastic/search-ui/tree/main/examples/vue](https://github.com/elastic/search-ui/tree/main/examples/vue) 22 | 23 | :::{tip} 24 | Check out our Vue.js integration demo in [CodeSandbox](https://codesandbox.io/embed/github/elastic/search-ui/tree/main/examples/vue). 25 | 26 | The example demonstrates how to implement Search UI in a Vue.js application. 27 | ::: 28 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/src/handlers/search/Response.ts: -------------------------------------------------------------------------------- 1 | import type { ResponseState } from "@elastic/search-ui"; 2 | import type { SearchkitResponse } from "@searchkit/sdk"; 3 | import { fieldResponseMapper } from "../common"; 4 | 5 | function SearchResponse(results: SearchkitResponse): ResponseState { 6 | const facets = (results.facets || []).reduce((acc, facet) => { 7 | return { 8 | ...acc, 9 | [facet.identifier]: [ 10 | { 11 | data: facet.entries.map((e) => ({ 12 | value: e.label, 13 | count: e.count 14 | })), 15 | type: "value" 16 | } 17 | ] 18 | }; 19 | }, {}); 20 | 21 | const pageEnd = (results.hits.page.pageNumber + 1) * results.hits.page.size; 22 | 23 | const response: ResponseState = { 24 | resultSearchTerm: results.summary.query, 25 | totalPages: results.hits.page.totalPages, 26 | pagingStart: 27 | results.summary.total && 28 | results.hits.page.pageNumber * results.hits.page.size + 1, 29 | pagingEnd: 30 | pageEnd > results.summary.total ? results.summary.total : pageEnd, 31 | wasSearched: false, 32 | totalResults: results.summary.total, 33 | facets, 34 | results: results.hits.items.map(fieldResponseMapper), 35 | requestId: null, 36 | rawResponse: null 37 | }; 38 | return response; 39 | } 40 | 41 | export default SearchResponse; 42 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elastic/search-ui-site-search-connector", 3 | "version": "1.23.1", 4 | "description": "A Search UI connector for Elastic's Site Search Service", 5 | "homepage": "https://docs.elastic.co/search-ui", 6 | "license": "Apache-2.0", 7 | "main": "lib/index.js", 8 | "module": "lib/index.mjs", 9 | "types": "lib/index.d.ts", 10 | "sideEffects": false, 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/elastic/search-ui.git", 21 | "directory": "packages/search-ui-site-search-connector" 22 | }, 23 | "scripts": { 24 | "test-ci": "jest --runInBand", 25 | "test": "jest", 26 | "build": "tsup", 27 | "watch": "yarn build --watch", 28 | "prepare": "yarn build" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/elastic/search-ui/issues" 32 | }, 33 | "devDependencies": { 34 | "typescript": "^4.9.3" 35 | }, 36 | "dependencies": { 37 | "@elastic/search-ui": "^1.23.1" 38 | }, 39 | "exports": { 40 | ".": { 41 | "types": "./lib/index.d.ts", 42 | "import": "./lib/index.mjs", 43 | "require": "./lib/index.js" 44 | }, 45 | "./package": "./package.json", 46 | "./package.json": "./package.json" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/src/queryBuilders/__tests__/BaseQueryBuilder.test.ts: -------------------------------------------------------------------------------- 1 | import { BaseQueryBuilder } from "../BaseQueryBuilder"; 2 | import type { RequestState } from "@elastic/search-ui"; 3 | 4 | describe("BaseQueryBuilder", () => { 5 | class TestQueryBuilder extends BaseQueryBuilder { 6 | getSearchTerm(): string { 7 | return "test"; 8 | } 9 | 10 | getSize(): number { 11 | return 10; 12 | } 13 | 14 | getFrom(): number { 15 | return 0; 16 | } 17 | 18 | build() { 19 | return {}; 20 | } 21 | } 22 | 23 | const baseRequestState: RequestState = { 24 | searchTerm: "test", 25 | current: 1, 26 | resultsPerPage: 10 27 | }; 28 | 29 | it("should create instance with default values", () => { 30 | const builder = new TestQueryBuilder(baseRequestState); 31 | expect(builder).toBeInstanceOf(BaseQueryBuilder); 32 | }); 33 | 34 | it("should get search term", () => { 35 | const builder = new TestQueryBuilder(baseRequestState); 36 | expect(builder.getSearchTerm()).toBe("test"); 37 | }); 38 | 39 | it("should get size", () => { 40 | const builder = new TestQueryBuilder(baseRequestState); 41 | expect(builder.getSize()).toBe(10); 42 | }); 43 | 44 | it("should get from", () => { 45 | const builder = new TestQueryBuilder(baseRequestState); 46 | expect(builder.getFrom()).toBe(0); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/PagingInfo.tsx: -------------------------------------------------------------------------------- 1 | import type { SearchContextState } from "@elastic/search-ui"; 2 | import React from "react"; 3 | import { Rename, BaseContainerProps } from "./types"; 4 | 5 | import { appendClassName } from "./view-helpers"; 6 | 7 | export type PagingInfoContainerContext = Pick< 8 | SearchContextState, 9 | "pagingStart" | "pagingEnd" | "resultSearchTerm" | "totalResults" 10 | >; 11 | 12 | export type PagingInfoViewProps = Rename< 13 | BaseContainerProps & PagingInfoContainerContext, 14 | { 15 | pagingStart: "start"; 16 | resultSearchTerm: "searchTerm"; 17 | pagingEnd: "end"; 18 | } 19 | >; 20 | 21 | export type PagingInfoContainerProps = BaseContainerProps & { 22 | view?: React.ComponentType; 23 | }; 24 | 25 | function PagingInfo({ 26 | className, 27 | end, 28 | searchTerm, 29 | start, 30 | totalResults, 31 | ...rest 32 | }: PagingInfoViewProps & React.HTMLAttributes) { 33 | return ( 34 |
    35 | Showing{" "} 36 | 37 | {start} - {end} 38 | {" "} 39 | out of {totalResults} 40 | {searchTerm && ( 41 | <> 42 | {" "} 43 | for: {searchTerm} 44 | 45 | )} 46 |
    47 | ); 48 | } 49 | 50 | export default PagingInfo; 51 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_single-links-facet.scss: -------------------------------------------------------------------------------- 1 | @include block("single-option-facet") { 2 | font-size: 13px; 3 | margin: $sizeS 0; 4 | padding: 0; 5 | list-style: none; 6 | 7 | @include element("item") { 8 | display: flex; 9 | justify-content: space-between; 10 | } 11 | 12 | @include element("link") { 13 | color: $facetLinkColor; 14 | position: relative; 15 | text-decoration: none; 16 | list-style: none; 17 | padding: 0; 18 | font-weight: bold; 19 | 20 | &:after { 21 | content: ""; 22 | opacity: 0; 23 | position: absolute; 24 | top: -1px; 25 | left: -5px; 26 | width: calc(100% + 10px); 27 | height: calc(100% + 2px); 28 | background: rgba(37, 139, 248, 0.08); 29 | pointer-events: none; 30 | } 31 | 32 | &:focus { 33 | color: $linkColor; 34 | font-weight: bold; 35 | outline: none; 36 | } 37 | 38 | &:hover { 39 | color: $linkColor; 40 | font-weight: bold; 41 | 42 | &:after { 43 | opacity: 1; 44 | } 45 | } 46 | } 47 | 48 | @include element("selected") { 49 | font-weight: 900; 50 | list-style: none; 51 | 52 | a { 53 | font-weight: 100; 54 | padding: 0 2px; 55 | } 56 | } 57 | 58 | @include element("remove") { 59 | color: #666; 60 | margin-left: 10px; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/src/queryBuilders/SuggestionsAutocompleteBuilder.ts: -------------------------------------------------------------------------------- 1 | import type { RequestState, SuggestionConfiguration } from "@elastic/search-ui"; 2 | import { BaseQueryBuilder } from "./BaseQueryBuilder"; 3 | import { SearchRequest } from "../types"; 4 | 5 | export class SuggestionsAutocompleteBuilder extends BaseQueryBuilder { 6 | constructor( 7 | state: RequestState, 8 | private readonly configuration: SuggestionConfiguration, 9 | private readonly completionSize: number = 5 10 | ) { 11 | super(state); 12 | } 13 | 14 | build() { 15 | this.setSize(0); 16 | this.setSourceFields([]); 17 | this.setSuggest(this.buildSuggestion()); 18 | 19 | return this.query; 20 | } 21 | 22 | private setSuggest(query: SearchRequest["suggest"]): void { 23 | if (query) { 24 | this.query.suggest = query; 25 | } 26 | } 27 | 28 | private buildSuggestion(): SearchRequest["suggest"] | null { 29 | if (!this.state.searchTerm) { 30 | return null; 31 | } 32 | 33 | const field = this.configuration.fields[0]; 34 | 35 | return { 36 | suggest: { 37 | prefix: this.state.searchTerm, 38 | completion: { 39 | size: this.completionSize, 40 | skip_duplicates: true, 41 | field, 42 | fuzzy: { 43 | fuzziness: 1 44 | } 45 | } 46 | } 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/__tests__/__snapshots__/Results.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`supports a render prop 1`] = ` 4 |
    5 | id", 13 | }, 14 | "title": Object { 15 | "raw": "title", 16 | "snippet": "title", 17 | }, 18 | "url": Object { 19 | "raw": "url", 20 | "snippet": "url", 21 | }, 22 | } 23 | } 24 | shouldTrackClickThrough={true} 25 | titleField="title" 26 | urlField="url" 27 | view={[Function]} 28 | /> 29 | id", 37 | }, 38 | "title": Object { 39 | "raw": "title", 40 | "snippet": "title", 41 | }, 42 | "url": Object { 43 | "raw": "url", 44 | "snippet": "url", 45 | }, 46 | } 47 | } 48 | shouldTrackClickThrough={true} 49 | titleField="title" 50 | urlField="url" 51 | view={[Function]} 52 | /> 53 |
    54 | `; 55 | -------------------------------------------------------------------------------- /examples/vue/src/searchConfig.js: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime/runtime"; 2 | import AppSearchAPIConnector from "@elastic/search-ui-app-search-connector"; 3 | 4 | const connector = new AppSearchAPIConnector({ 5 | searchKey: "search-nyxkw1fuqex9qjhfvatbqfmw", 6 | engineName: "hearthstone-cards", 7 | endpointBase: "https://search-ui-sandbox.ent.us-central1.gcp.cloud.es.io" 8 | }); 9 | 10 | const config = { 11 | debug: true, 12 | apiConnector: connector, 13 | searchQuery: { 14 | disjunctiveFacets: ["cost", "card_class", "set", "type", "race", "rarity"], 15 | facets: { 16 | race: { 17 | type: "value" 18 | }, 19 | rarity: { 20 | type: "value" 21 | }, 22 | type: { 23 | type: "value" 24 | }, 25 | set: { 26 | type: "value", 27 | size: 30 28 | }, 29 | card_class: { 30 | type: "value" 31 | }, 32 | cost: { 33 | type: "range", 34 | ranges: [ 35 | { from: 0, to: 1, name: "0" }, 36 | { from: 1, to: 2, name: "1" }, 37 | { from: 2, to: 3, name: "2" }, 38 | { from: 3, to: 4, name: "3" }, 39 | { from: 4, to: 5, name: "4" }, 40 | { from: 5, to: 6, name: "5" }, 41 | { from: 6, to: 7, name: "6" }, 42 | { from: 7, to: 8, name: "7" }, 43 | { from: 8, name: "8+" } 44 | ] 45 | } 46 | } 47 | } 48 | }; 49 | 50 | export default config; 51 | -------------------------------------------------------------------------------- /packages/search-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elastic/search-ui", 3 | "version": "1.23.1", 4 | "description": "A Headless Search UI library", 5 | "license": "Apache-2.0", 6 | "main": "lib/index.js", 7 | "module": "lib/index.mjs", 8 | "types": "lib/index.d.ts", 9 | "sideEffects": false, 10 | "homepage": "https://docs.elastic.co/search-ui", 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/elastic/search-ui.git", 21 | "directory": "packages/search-ui" 22 | }, 23 | "scripts": { 24 | "test-ci": "jest --runInBand", 25 | "test": "jest", 26 | "build": "tsup", 27 | "watch": "yarn run build --watch", 28 | "prepare": "yarn run build" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/elastic/search-ui/issues" 32 | }, 33 | "devDependencies": { 34 | "@types/history": "^4.7.11", 35 | "@types/qs": "^6.9.7", 36 | "typescript": "^4.9.3" 37 | }, 38 | "dependencies": { 39 | "date-fns": "^1.30.1", 40 | "deep-equal": "^1.0.1", 41 | "history": "^4.9.0", 42 | "qs": "^6.9.7" 43 | }, 44 | "exports": { 45 | ".": { 46 | "types": "./lib/index.d.ts", 47 | "import": "./lib/index.mjs", 48 | "require": "./lib/index.js" 49 | }, 50 | "./package": "./package.json", 51 | "./package.json": "./package.json" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/react-search-ui/src/containers/__tests__/__snapshots__/ResultsPerPage.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders the component with custom page options 1`] = ` 4 |
    7 |
    10 | Show 11 |
    12 | 55 |
    56 | `; 57 | 58 | exports[`supports a render prop 1`] = ` 59 |
    60 | 20 61 |
    62 | `; 63 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import adaptRequest from "../requestAdapter"; 2 | 3 | import type { 4 | FieldConfiguration, 5 | FacetConfiguration 6 | } from "@elastic/search-ui"; 7 | 8 | export type SearchResponse = { 9 | record_count?: number; 10 | records?: Record; 11 | info?: Record; 12 | errors?: Record; 13 | }; 14 | 15 | type Boost = "logarithmic" | "linear" | "exponential"; 16 | 17 | export type SiteSearchQueryConfig = { 18 | resultsPerPage?: number; 19 | current?: number; 20 | sortDirection?: Record; 21 | sortField?: Record; 22 | sortList?: any; 23 | result_fields?: Record; 24 | search_fields?: Record; 25 | facets?: Record; 26 | // extra params not handled by requestAdapter but available on API 27 | spelling?: "strict" | "always" | "retry"; 28 | functional_boosts?: Record>; 29 | document_types?: string[]; 30 | [key: string]: any; 31 | }; 32 | 33 | export type SearchQueryHook = ( 34 | queryOptions: ReturnType, 35 | next: (newQueryOptions: ReturnType) => any 36 | ) => any; 37 | 38 | export type SiteSearchAPIConnectorParams = { 39 | documentType: string; 40 | engineKey: string; 41 | beforeSearchCall?: SearchQueryHook; 42 | beforeAutocompleteResultsCall?: SearchQueryHook; 43 | }; 44 | -------------------------------------------------------------------------------- /docs/reference/tutorials.md: -------------------------------------------------------------------------------- 1 | --- 2 | navigation_title: "Tutorials" 3 | mapped_pages: 4 | - https://www.elastic.co/guide/en/search-ui/current/tutorials-connectors.html 5 | --- 6 | 7 | # Which connector to choose? [tutorials-connectors] 8 | 9 | The first thing you want to consider before implementing your search is the backend you’re going to use for your data. 10 | 11 | - (Recommended) [Elasticsearch Connector](/reference/api-connectors-elasticsearch.md) is the best option for working with [Elasticsearch](https://www.elastic.co/elasticsearch). It’s a flexible option that gives you full control over the request and response flow — but it can also work out of the box with minimal configuration. 12 | - [Site Search](/reference/api-connectors-site-search.md) if you are already using [Swiftype](https://swiftype.com/), these are basically a single product with two names. 13 | - Use [Custom Connector Guide](/reference/guides-building-custom-connector.md) if none of the built-in connectors fit your use case. 14 | 15 | ### ⚠️ Deprecated connectors 16 | 17 | The following connectors are no longer recommended for use in new projects: 18 | 19 | - [Elastic App Search](/reference/api-connectors-app-search.md) 20 | - [Elastic Workplace Search](/reference/api-connectors-workplace-search.md) 21 | 22 | These products are deprecated and not actively maintained. If you’re still using them, the connectors will technically work — but we suggest migrating to the [Elasticsearch Connector](/reference/api-connectors-elasticsearch.md). 23 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/src/handlers/__test__/transporter.test.ts: -------------------------------------------------------------------------------- 1 | import { EngineTransporter } from "../transporter"; 2 | import nock from "nock"; 3 | import type { SearchRequest, SearchResponse } from "../../types"; 4 | import "cross-fetch/polyfill"; 5 | 6 | describe("EngineTransporter", () => { 7 | it("is a class", () => { 8 | expect(typeof EngineTransporter).toEqual("function"); 9 | }); 10 | 11 | it("perform a request", () => { 12 | const transporter = new EngineTransporter( 13 | "http://localhost:9200", 14 | "my_engine", 15 | "apikey" 16 | ); 17 | 18 | const body = { 19 | query: { 20 | match_all: {} 21 | } 22 | }; 23 | 24 | nock("http://localhost:9200", { 25 | reqheaders: { 26 | authorization: "ApiKey apikey" 27 | } 28 | }) 29 | .post("/api/engines/my_engine/_search") 30 | .reply(200, () => ({ 31 | hits: { 32 | hits: [ 33 | { 34 | _id: "1", 35 | _source: { 36 | title: "My title" 37 | } 38 | } 39 | ] 40 | } 41 | })); 42 | 43 | return transporter 44 | .performRequest({ 45 | body 46 | } as SearchRequest) 47 | .then((response: SearchResponse) => { 48 | expect(response.hits.hits).toHaveLength(1); 49 | expect(response.hits.hits[0]._id).toEqual("1"); 50 | expect(response.hits.hits[0]._source.title).toEqual("My title"); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/__tests__/__snapshots__/SearchBox.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 22 | 23 | 24 | `; 25 | 26 | exports[`renders correctly with single autocompleteSuggestion configuration 1`] = ` 27 | 50 | 51 | 52 | `; 53 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/setFilter.ts: -------------------------------------------------------------------------------- 1 | import Events from "../Events"; 2 | import { serialiseFilter } from "../helpers"; 3 | import { FilterType, FilterValue, RequestState } from "../types"; 4 | 5 | /** 6 | * Filter results - Replaces current filter value 7 | * 8 | * Will trigger new search 9 | * 10 | * @param name String field name to filter on 11 | * @param value FilterValue to apply 12 | * @param type String (Optional) type of filter to apply 13 | * @param persistent boolean (Optional) whether to preserve the filter on new search 14 | */ 15 | export default function setFilter( 16 | name: string, 17 | value: FilterValue, 18 | type: FilterType = "all", 19 | persistent?: boolean 20 | ) { 21 | // eslint-disable-next-line no-console 22 | if (this.debug) console.log("Search UI: Action", "setFilter", ...arguments); 23 | 24 | let { filters } = this.state as RequestState; 25 | filters = filters.filter( 26 | (filter) => filter.field !== name || filter.type !== type 27 | ); 28 | const filterValue = Array.isArray(value) ? value : [value]; 29 | 30 | this._updateSearchResults({ 31 | current: 1, 32 | filters: [ 33 | ...filters, 34 | { 35 | field: name, 36 | values: filterValue, 37 | type, 38 | persistent 39 | } 40 | ] 41 | }); 42 | 43 | const events: Events = this.events; 44 | 45 | events.emit({ 46 | type: "FacetFilterSelected", 47 | field: name, 48 | value: value && serialiseFilter(filterValue), 49 | query: this.state.searchTerm 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /packages/search-ui-elasticsearch-connector/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { estypes } from "@elastic/elasticsearch"; 2 | import type { QueryConfig, RequestState } from "@elastic/search-ui"; 3 | import type { IApiClientTransporter } from "./transporter/ApiClientTransporter"; 4 | 5 | export type SearchRequest = estypes.SearchRequest; 6 | export type ResponseBody = estypes.SearchResponseBody< 7 | Record, 8 | Record 9 | >; 10 | export type SearchSourceFilter = estypes.SearchSourceFilter; 11 | export type SearchHit = estypes.SearchHit>; 12 | export type Filter = Pick; 13 | export type FilterValue = Pick; 14 | export type FilterValueRange = Pick; 15 | export type QueryRangeValue = estypes.QueryDslRangeQuery; 16 | export type Aggregation = 17 | | estypes.AggregationsTermsAggregateBase<{ key: string; doc_count: number }> 18 | | estypes.AggregationsMultiBucketAggregateBase<{ doc_count: number }>; 19 | 20 | export type PostProcessRequestBodyFn = ( 21 | requestBody: SearchRequest, 22 | requestState: RequestState, 23 | queryConfig: QueryConfig 24 | ) => SearchRequest; 25 | 26 | export interface CloudHost { 27 | id: string; 28 | } 29 | 30 | export type ConnectionOptions = { 31 | apiClient?: IApiClientTransporter; 32 | host?: string; 33 | cloud?: CloudHost; 34 | index: string; 35 | apiKey?: string; 36 | connectionOptions?: { 37 | headers?: Record; 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/search-ui-analytics-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elastic/search-ui-analytics-plugin", 3 | "version": "1.23.1", 4 | "description": "⚠️ DEPRECATED. Behavorial Analytics middleware plugin", 5 | "license": "Apache-2.0", 6 | "main": "lib/index.js", 7 | "module": "lib/index.mjs", 8 | "types": "lib/index.d.ts", 9 | "sideEffects": false, 10 | "homepage": "https://docs.elastic.co/search-ui", 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/elastic/search-ui.git", 21 | "directory": "packages/search-ui-analytics-plugin" 22 | }, 23 | "scripts": { 24 | "test-ci": "jest --runInBand", 25 | "test": "jest", 26 | "build": "tsup", 27 | "watch": "yarn build --watch", 28 | "prepare": "yarn build" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/elastic/search-ui/issues" 32 | }, 33 | "devDependencies": { 34 | "typescript": "^4.9.3" 35 | }, 36 | "publishConfig": { 37 | "access": "public", 38 | "registry": "https://registry.npmjs.org/" 39 | }, 40 | "dependencies": { 41 | "@elastic/behavioral-analytics-tracker-core": "^2.0.5", 42 | "@elastic/search-ui": "^1.23.1" 43 | }, 44 | "exports": { 45 | ".": { 46 | "types": "./lib/index.d.ts", 47 | "import": "./lib/index.mjs", 48 | "require": "./lib/index.js" 49 | }, 50 | "./package": "./package.json", 51 | "./package.json": "./package.json" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/removeFilter.ts: -------------------------------------------------------------------------------- 1 | import Events from "../Events"; 2 | import { removeSingleFilterValue, serialiseFilter } from "../helpers"; 3 | import { FilterType, FilterValue, RequestState } from "../types"; 4 | 5 | /** 6 | * Remove filter from results 7 | * 8 | * Will trigger new search 9 | * 10 | * @param name String field name for filter to remove 11 | * @param value String (Optional) field value for filter to remove 12 | * @param type String (Optional) type of filter to remove 13 | */ 14 | export default function removeFilter( 15 | name: string, 16 | value?: FilterValue, 17 | type?: FilterType 18 | ) { 19 | if (this.debug) 20 | // eslint-disable-next-line no-console 21 | console.log("Search UI: Action", "removeFilter", ...arguments); 22 | 23 | const { filters } = this.state as RequestState; 24 | 25 | let updatedFilters = filters; 26 | 27 | if (!value && type) { 28 | updatedFilters = filters.filter( 29 | (filter) => !(filter.field === name && filter.type === type) 30 | ); 31 | } else if (value) { 32 | updatedFilters = removeSingleFilterValue(filters, name, value, type); 33 | } else { 34 | updatedFilters = filters.filter((filter) => filter.field !== name); 35 | } 36 | 37 | this._updateSearchResults({ 38 | current: 1, 39 | filters: updatedFilters 40 | }); 41 | 42 | const events: Events = this.events; 43 | 44 | events.emit({ 45 | type: "FacetFilterRemoved", 46 | field: name, 47 | value: value && serialiseFilter([value]), 48 | query: this.state.searchTerm 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-search-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elastic/react-search-ui", 3 | "version": "1.23.1", 4 | "homepage": "https://docs.elastic.co/search-ui", 5 | "description": "A React library for building search experiences", 6 | "license": "Apache-2.0", 7 | "main": "lib/index.js", 8 | "module": "lib/index.mjs", 9 | "types": "lib/index.d.ts", 10 | "sideEffects": false, 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/elastic/search-ui.git", 21 | "directory": "packages/react-search-ui" 22 | }, 23 | "scripts": { 24 | "test-ci": "jest --runInBand", 25 | "test": "jest", 26 | "build": "tsup", 27 | "watch": "yarn build --watch", 28 | "prepare": "yarn build" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/elastic/search-ui/issues" 32 | }, 33 | "dependencies": { 34 | "@elastic/react-search-ui-views": "^1.23.1", 35 | "@elastic/search-ui": "^1.23.1" 36 | }, 37 | "peerDependencies": { 38 | "react": ">= 16.8.0 < 19", 39 | "react-dom": ">= 16.8.0 < 19" 40 | }, 41 | "devDependencies": { 42 | "react": "^18.3.0", 43 | "react-dom": "^18.3.0", 44 | "typescript": "^4.9.3" 45 | }, 46 | "exports": { 47 | ".": { 48 | "types": "./lib/index.d.ts", 49 | "import": "./lib/index.mjs", 50 | "require": "./lib/index.js" 51 | }, 52 | "./package": "./package.json", 53 | "./package.json": "./package.json" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/sandbox/src/pages/search-bar-in-header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { config } from "./config"; 4 | 5 | import { SearchProvider, SearchBox } from "@elastic/react-search-ui"; 6 | import "@elastic/react-search-ui-views/lib/styles/styles.css"; 7 | 8 | export default function Header() { 9 | return ( 10 |
    11 |
    18 |
    23 | 🌲 National Parks 24 |
    25 | 31 | { 33 | window.location.href = `${window.location.origin}/search-bar-in-header/search?q=${searchTerm}`; 34 | }} 35 | autocompleteMinimumCharacters={3} 36 | autocompleteResults={{ 37 | linkTarget: "_blank", 38 | sectionTitle: "Results", 39 | titleField: "title", 40 | urlField: "nps_link", 41 | shouldTrackClickThrough: true, 42 | clickThroughTags: ["test"] 43 | }} 44 | autocompleteSuggestions={true} 45 | debounceLength={0} 46 | /> 47 | 48 |
    49 |
    50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/layouts/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import LayoutSidebar from "./LayoutSidebar"; 4 | import { appendClassName } from "../view-helpers"; 5 | 6 | interface LayoutProps { 7 | className?: string; 8 | children?: React.ReactNode; 9 | header?: React.ReactNode; 10 | bodyContent?: React.ReactNode; 11 | bodyFooter?: React.ReactNode; 12 | bodyHeader?: React.ReactNode; 13 | sideContent?: React.ReactNode; 14 | } 15 | 16 | function Layout({ 17 | className, 18 | children, 19 | header, 20 | bodyContent, 21 | bodyFooter, 22 | bodyHeader, 23 | sideContent 24 | }: LayoutProps) { 25 | return ( 26 |
    27 |
    28 |
    {header}
    29 |
    30 |
    31 |
    32 | 33 | {sideContent} 34 | 35 |
    36 |
    37 |
    {bodyHeader}
    38 |
    39 |
    40 | {children || bodyContent} 41 |
    42 |
    {bodyFooter}
    43 |
    44 |
    45 |
    46 |
    47 | ); 48 | } 49 | 50 | export default Layout; 51 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/components/_multi-checkbox-facet.scss: -------------------------------------------------------------------------------- 1 | @include block("multi-checkbox-facet") { 2 | color: $facetLinkColor; 3 | font-size: 13px; 4 | margin: 8px 0; 5 | 6 | @include element("option-label") { 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | cursor: pointer; 11 | } 12 | 13 | @include element("option-input-wrapper") { 14 | flex: 1; 15 | } 16 | 17 | @include element("checkbox") { 18 | margin-right: $sizeS; 19 | cursor: pointer; 20 | } 21 | 22 | @include element("option-count") { 23 | color: #888888; 24 | font-size: 0.85em; 25 | margin-left: $sizeL; 26 | } 27 | } 28 | 29 | @include block("facet-view-more") { 30 | display: block; 31 | cursor: pointer; 32 | color: $linkColor; 33 | font-size: 13px; 34 | 35 | // Button reset 36 | font-family: inherit; 37 | line-height: inherit; 38 | text-align: left; 39 | border: unset; 40 | padding: unset; 41 | background: unset; 42 | 43 | &:hover, 44 | &:focus { 45 | background-color: $facetSelectBackground; 46 | outline: $sizeXS solid $facetSelectBackground; 47 | } 48 | } 49 | 50 | @include block("facet-search") { 51 | margin: 6px 0px 0px 0px; 52 | 53 | @include element("text-input") { 54 | width: 100%; 55 | height: 100%; 56 | padding: 6px; 57 | margin: 0; 58 | font-family: inherit; 59 | border: 1px solid #cccccc; 60 | border-radius: 4px; 61 | outline: none; 62 | 63 | &:focus { 64 | border: 1px solid $linkColor; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/src/responseAdapters.ts: -------------------------------------------------------------------------------- 1 | const addEachKeyValueToObject = (acc: any, [key, value]) => ({ 2 | ...acc, 3 | [key]: value 4 | }); 5 | 6 | export function getFacets(docInfo: any) { 7 | if (!docInfo.facets) return {}; 8 | 9 | return Object.entries(docInfo.facets) 10 | .map(([facetName, facetValue]) => { 11 | return [ 12 | facetName, 13 | [ 14 | { 15 | field: facetName, 16 | data: Object.entries(facetValue).map(([value, count]) => ({ 17 | value, 18 | count 19 | })), 20 | // Site Search does not support any other type of facet 21 | type: "value" 22 | } 23 | ] 24 | ]; 25 | }) 26 | .reduce(addEachKeyValueToObject, {}); 27 | } 28 | 29 | export function getResults(records: Record, documentType: string) { 30 | const isMetaField = (key: string) => key.startsWith("_"); 31 | const toObjectWithRaw = (value: any) => ({ raw: value }); 32 | 33 | return records[documentType].map((record: any) => { 34 | const { highlight, sort, ...rest } = record; //eslint-disable-line 35 | 36 | const result = Object.entries(rest) 37 | .filter(([fieldName]) => !isMetaField(fieldName)) 38 | .map(([fieldName, fieldValue]) => [ 39 | fieldName, 40 | toObjectWithRaw(fieldValue) 41 | ]) 42 | .reduce(addEachKeyValueToObject, {}); 43 | 44 | Object.entries(highlight).forEach(([key, value]) => { 45 | result[key].snippet = value; 46 | }); 47 | 48 | return result; 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /packages/search-ui/src/__tests__/A11yNotifications.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { 5 | getLiveRegion, 6 | announceToScreenReader, 7 | defaultMessages 8 | } from "../A11yNotifications"; 9 | 10 | it("creates a live screen reader region", () => { 11 | // Before init 12 | let region = document.getElementById("search-ui-screen-reader-notifications"); 13 | expect(region).toBeNull(); 14 | 15 | // After init 16 | region = getLiveRegion(); 17 | expect(region).not.toBeNull(); 18 | expect(region.getAttribute("role")).toEqual("status"); 19 | expect(region.getAttribute("aria-live")).toEqual("polite"); 20 | expect(region.style.overflow).toEqual("hidden"); 21 | }); 22 | 23 | it("updates the live region correctly via announceToScreenReader", () => { 24 | announceToScreenReader("Hello world!"); 25 | 26 | const region = document.getElementById( 27 | "search-ui-screen-reader-notifications" 28 | ); 29 | expect(region.textContent).toEqual("Hello world!"); 30 | }); 31 | 32 | describe("defaultMessages", () => { 33 | it("outputs searchResults correctly", () => { 34 | expect( 35 | defaultMessages.searchResults({ 36 | start: "1", 37 | end: "20", 38 | totalResults: "50", 39 | searchTerm: "foo" 40 | }) 41 | ).toEqual('Showing 1 to 20 results out of 50, searching for "foo".'); 42 | 43 | expect( 44 | defaultMessages.searchResults({ 45 | start: "0", 46 | end: "0", 47 | totalResults: "0", 48 | searchTerm: "" 49 | }) 50 | ).toEqual("Showing 0 to 0 results out of 0"); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/layouts/LayoutSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { appendClassName } from "../view-helpers"; 4 | 5 | interface LayoutSidebarProps { 6 | className: string; 7 | children: React.ReactNode; 8 | } 9 | 10 | interface LayoutSidebarState { 11 | isSidebarToggled: boolean; 12 | } 13 | 14 | class LayoutSidebar extends React.Component< 15 | LayoutSidebarProps, 16 | LayoutSidebarState 17 | > { 18 | constructor(props) { 19 | super(props); 20 | this.state = { isSidebarToggled: false }; 21 | } 22 | 23 | toggleSidebar = () => { 24 | this.setState(({ isSidebarToggled }) => ({ 25 | isSidebarToggled: !isSidebarToggled 26 | })); 27 | }; 28 | 29 | renderToggleButton = (label) => { 30 | if (!this.props.children) return null; 31 | 32 | return ( 33 | 41 | ); 42 | }; 43 | 44 | render() { 45 | const { className, children } = this.props; 46 | const { isSidebarToggled } = this.state; 47 | 48 | const classes = appendClassName( 49 | className, 50 | isSidebarToggled ? `${className}--toggled` : null 51 | ); 52 | 53 | return ( 54 | <> 55 | {this.renderToggleButton("Show Filters")} 56 |
    57 | {this.renderToggleButton("Save Filters")} 58 | {children} 59 |
    60 | 61 | ); 62 | } 63 | } 64 | 65 | export default LayoutSidebar; 66 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/trackClickThrough.ts: -------------------------------------------------------------------------------- 1 | import { SearchState } from ".."; 2 | import Events from "../Events"; 3 | 4 | /** 5 | * Report a click through event. A click through event is when a user 6 | * clicks on a result link. 7 | * 8 | * @param documentId String The document ID associated with result that was 9 | * clicked 10 | * @param tag Array[String] Optional Tags which can be used to categorize 11 | * this click event 12 | */ 13 | export default function trackClickThrough( 14 | documentId: string, 15 | tags: string[] = [] 16 | ): void { 17 | if (this.debug) 18 | // eslint-disable-next-line no-console 19 | console.log("Search UI: Action", "trackClickThrough", ...arguments); 20 | 21 | const { 22 | requestId, 23 | searchTerm, 24 | results, 25 | current, 26 | resultsPerPage, 27 | totalResults, 28 | filters 29 | }: SearchState = this.state; 30 | 31 | const resultIndexOnPage = results.findIndex( 32 | (result) => result._meta.id === documentId 33 | ); 34 | const result = results[resultIndexOnPage]; 35 | const events: Events = this.events; 36 | 37 | this.events.resultClick({ 38 | query: searchTerm, 39 | documentId, 40 | requestId, 41 | tags, 42 | result, 43 | page: current, 44 | resultsPerPage, 45 | resultIndexOnPage 46 | }); 47 | 48 | events.emit({ 49 | type: "ResultSelected", 50 | documentId, 51 | query: searchTerm, 52 | origin: "results", 53 | position: resultIndexOnPage, 54 | tags, 55 | totalResults, 56 | filters, 57 | currentPage: current, 58 | resultsPerPage: resultsPerPage 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /docs/reference/solutions-ecommerce-product-detail-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | mapped_pages: 3 | - https://www.elastic.co/guide/en/search-ui/current/solutions-ecommerce-product-detail-page.html 4 | --- 5 | 6 | # Product Detail Page [solutions-ecommerce-product-detail-page] 7 | 8 | % TO DO: Use `:class: screenshot` 9 | ![Product detail page](images/product-detail-page.png) 10 | 11 | You can put many things on a product detail page: image, description, specs. They are all describing the product itself. 12 | 13 | But a critical piece that often gets overlooked is cross-sell recommendations. 14 | 15 | % TO DO: Use `:class: screenshot` 16 | ![Cross-sell recommendations](images/cross-sell-recommendations.png) 17 | 18 | These are the lists of products located under the product description. They often come under such headings: 19 | 20 | - People who viewed this item also viewed 21 | - Often bought together 22 | - You might also like 23 | - etc. 24 | 25 | Cross-sell recommendations help users find the right product if the current one does not satisfy their criteria or find the related products faster (for example, frying pan + lid). 26 | 27 | Technically, cross-sell recommendations are usually implemented as product carousels. To build one, check out our [Product carousel guide](/reference/solutions-ecommerce-carousel.md), which has an implementation example. 28 | 29 | ## Related Articles [solutions-ecommerce-product-detail-page-related-articles] 30 | 31 | - [Product Carousels](/reference/solutions-ecommerce-carousel.md): Build a product carousel with Search UI. 32 | - [Creating Components](/reference/guides-creating-own-components.md): Build your own components for Search UI. 33 | -------------------------------------------------------------------------------- /docs/reference/api-core-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | mapped_pages: 3 | - https://www.elastic.co/guide/en/search-ui/current/api-core-index.html 4 | --- 5 | 6 | # Core API 7 | 8 | Core API is the main set of interfaces and configurations used to set up and manage search functionality in Search UI. 9 | 10 | ## Core Components 11 | 12 | Core API consists of three main parts: 13 | 14 | 1. [Configuration](/reference/api-core-configuration.md) - configuration of search queries, autocomplete, and event handlers 15 | 2. [State](/reference/api-core-state.md) - application state management, including: 16 | - Request State 17 | - Response State 18 | - Application State 19 | 3. [Actions](/reference/api-core-actions.md) - functions to update the Request State and perform API requests, including: 20 | - Search term management 21 | - Filter operations 22 | - Sorting 23 | - Pagination 24 | - Analytics tracking 25 | 26 | ## Key Features 27 | 28 | - Search query configuration with support for filters and facets 29 | - Autocomplete and suggestions configuration 30 | - Application state management 31 | - Integration with various search backends through connectors 32 | - URL state and routing configuration 33 | 34 | ## Usage Example 35 | 36 | ```jsx 37 | import { SearchProvider } from "@elastic/react-search-ui"; 38 | 39 | const config = { 40 | apiConnector: connector, 41 | searchQuery: { 42 | facets: { 43 | states: { type: "value", size: 30 } 44 | } 45 | }, 46 | autocompleteQuery: { 47 | results: { 48 | resultsPerPage: 5 49 | } 50 | } 51 | }; 52 | 53 | {/* Search UI components */}; 54 | ``` 55 | -------------------------------------------------------------------------------- /packages/search-ui-app-search-connector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elastic/search-ui-app-search-connector", 3 | "version": "1.23.1", 4 | "description": "⚠️ DEPRECATED. Please use '@elastic/search-ui-elasticsearch-connector'.", 5 | "homepage": "https://docs.elastic.co/search-ui", 6 | "license": "Apache-2.0", 7 | "main": "lib/cjs/index.js", 8 | "module": "lib/esm/index.js", 9 | "types": "lib/esm/index.d.ts", 10 | "sideEffects": false, 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/elastic/search-ui.git", 21 | "directory": "packages/search-ui-app-search-connector" 22 | }, 23 | "scripts": { 24 | "test-ci": "jest --runInBand", 25 | "test": "jest", 26 | "clean": "rimraf lib", 27 | "watch-js": "./bin/watch-js", 28 | "build-js": "./bin/build-js", 29 | "build": "npm run clean && npm run build-js", 30 | "watch": "npm run build && npm run watch-js", 31 | "prepare": "npm run build" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/elastic/search-ui/issues" 35 | }, 36 | "devDependencies": { 37 | "rimraf": "^2.6.3", 38 | "typescript": "^4.9.3" 39 | }, 40 | "dependencies": { 41 | "@elastic/app-search-javascript": "^8.1.2", 42 | "@elastic/search-ui": "^1.23.1" 43 | }, 44 | "exports": { 45 | ".": { 46 | "import": "./lib/esm/index.js", 47 | "require": "./lib/cjs/index.js", 48 | "types": "./lib/esm/index.d.ts" 49 | }, 50 | "./package": "./package.json", 51 | "./package.json": "./package.json" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/vue/src/components/SearchFacet.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 54 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/README.md: -------------------------------------------------------------------------------- 1 | # react-search-ui-views 2 | 3 | Part of the [Search UI](https://github.com/elastic/search-ui) project. 4 | 5 | This is the view layer for [react-search-ui](../react-search-ui/README.md). 6 | 7 | This library provides view implementations for all of React Search UI's 8 | components along with corresponding CSS, and a few layout components. 9 | 10 | The components provided here have no logic behind them, they are simple, 11 | stateless, view-only components. 12 | 13 | ## Usage 14 | 15 | ```jsx 16 | import React from "react"; 17 | import { 18 | Layout, 19 | PagingInfo, 20 | Result, 21 | SearchBox 22 | } from "@elastic/react-search-ui-views"; 23 | import "@elastic/react-search-ui-views/lib/styles/styles.css"; 24 | 25 | export default function App() { 26 | return ( 27 |
    28 | } 30 | bodyHeader={ 31 |
    32 | 38 |
    39 | } 40 | bodyContent={ 41 |
    42 | 46 |
    47 | } 48 | /> 49 |
    50 | ); 51 | } 52 | ``` 53 | 54 | ## Storybook 55 | 56 | A visual component reference is available locally. It is built with [Storybook](https://storybook.js.org/), and can be run locally with the following command: 57 | 58 | ``` 59 | npm run storybook 60 | ``` 61 | -------------------------------------------------------------------------------- /packages/react-search-ui-views/src/styles/themes/reference-ui/modules/_variables.scss: -------------------------------------------------------------------------------- 1 | // General Colors 2 | $black: #333333 !default; 3 | $white: #ffffff !default; 4 | $red: red; 5 | $linkColor: #3a56e4 !default; 6 | $textMatchColor: #3a56e4 !default; 7 | $facetLabel: #8b9bad !default; 8 | $facetLinkColor: #4f4f4f !default; 9 | $facetCountColor: #888888 !default; 10 | $facetSelectBackground: #f8f8f8 !default; 11 | $facetSelectBorder: #a6a6a6 !default; 12 | $facetSelectFocus: #00a9e5 !default; 13 | $metaTextColor: #4a4b4b !default; 14 | $helpTextGrayColor: #888888 !default; 15 | $pagingHoverBackground: #f8f8f8 !default; 16 | 17 | // Layout Colors 18 | $appContainerBackgroundColor: #ffffff !default; 19 | $resultsLeftBorderColor: #eeeeee !default; 20 | 21 | // Header Colors 22 | $headerBackgroundColor: #f9f9f9 !default; 23 | $headerBorderColor: #eeeeee !default; 24 | $searchboxBorderColor: #abb1b7 !default; 25 | 26 | // Result Colors 27 | $resultBackgroundColor: #fcfcfc !default; 28 | $resultBorderColor: #eeeeee !default; 29 | $fieldHoverColor: #f6f6f6 !default; 30 | $resultKeyTextColor: #777777 !default; 31 | 32 | // Font Variables 33 | $fontFamily: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, 34 | Arial, sans-serif !default; 35 | $fontWeightNormal: 400 !default; 36 | $fontWeightBold: 700 !default; 37 | $lineHeight: 1.5 !default; 38 | 39 | // Sizes 40 | $sizeXXS: 2px !default; 41 | $sizeXS: 4px !default; 42 | $sizeS: 8px !default; 43 | $sizeM: 12px !default; 44 | $sizeL: 24px !default; 45 | $sizeXL: 32px !default; 46 | $sizeXXL: 48px !default; 47 | 48 | $sidebarBreakpoint: 800px !default; 49 | $maxWidth: 1300px !default; 50 | $offset: 175px !default; 51 | 52 | $borderRadius: 4px !default; 53 | -------------------------------------------------------------------------------- /packages/search-ui-engines-connector/README.md: -------------------------------------------------------------------------------- 1 | # search-ui-engines-connector 2 | 3 | Part of the [Search UI](https://github.com/elastic/search-ui) project. 4 | 5 | This Connector is used to connect Search UI to Elasticsearch Engines. 6 | 7 | ## Usage 8 | 9 | ```shell 10 | npm install --save @elastic/search-ui-engines-connector 11 | ``` 12 | 13 | ```js 14 | import EnginesConnector from "@elastic/search-ui-engines-connector"; 15 | 16 | const connector = new EnginesConnector({ 17 | host: "https://my-engines-demo.ent.us-east4.gcp.elastic-cloud.com/", 18 | engineName: "puggles", 19 | apiKey: "QTJDMHhJVUJENHg5WETWcXlrWVQ6dXFlcF9QRWdRWHl2Rm9iSndka1Rndw==" 20 | }); 21 | ``` 22 | 23 | ## EnginesConnector 24 | 25 | **Kind**: global class 26 | 27 | ### new ElasticsearchAPIConnector(options) 28 | 29 | | Param | Type | 30 | | ------- | -------------------------------- | 31 | | options | [Options](#Options) | 32 | 33 | ## Options 34 | 35 | **Kind**: global typedef 36 | 37 | | Param | Type | Default | Description | 38 | | ---------- | ------------------- | ------- | --------------------------------------------------------------- | 39 | | host | string | | Engines Host. | 40 | | engineName | string | | The name of the engine to search | 41 | | apiKey | string | | API Key for the engine. Can be found within the Engine view UI. | 42 | 43 | ## Query Configuration Requirements 44 | 45 | - `search_fields` object is required with all fields you want to search by. 46 | -------------------------------------------------------------------------------- /packages/search-ui/src/actions/trackAutocompleteClickThrough.ts: -------------------------------------------------------------------------------- 1 | import Events from "../Events"; 2 | 3 | /** 4 | * Report a click through event. A click through event is when a user 5 | * clicks on a result within an autocomplete Dropdown. 6 | * 7 | * @param documentId String The document ID associated with result that was 8 | * clicked 9 | * @param tag Array[String] Optional Tags which can be used to categorize 10 | * this click event 11 | */ 12 | export default function trackAutocompleteClickThrough( 13 | documentId: string, 14 | tags: string[] = [] 15 | ): void { 16 | if (this.debug) { 17 | // eslint-disable-next-line no-console 18 | console.log( 19 | "Search UI: Action", 20 | "trackAutocompleteClickThrough", 21 | ...arguments 22 | ); 23 | } 24 | 25 | const { 26 | autocompletedResultsRequestId, 27 | searchTerm, 28 | autocompletedResults, 29 | current, 30 | resultsPerPage, 31 | totalResults, 32 | filters 33 | } = this.state; 34 | const resultIndex = autocompletedResults.findIndex( 35 | (result) => result._meta.id === documentId 36 | ); 37 | const result = autocompletedResults[resultIndex]; 38 | 39 | const events: Events = this.events; 40 | 41 | events.autocompleteResultClick({ 42 | query: searchTerm, 43 | documentId, 44 | requestId: autocompletedResultsRequestId, 45 | tags, 46 | result, 47 | resultIndex 48 | }); 49 | 50 | events.emit({ 51 | type: "ResultSelected", 52 | documentId, 53 | query: searchTerm, 54 | position: resultIndex, 55 | origin: "autocomplete", 56 | tags, 57 | totalResults, 58 | filters, 59 | currentPage: current, 60 | resultsPerPage: resultsPerPage 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /packages/search-ui-site-search-connector/src/__tests__/example-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "record_count": 2, 3 | "records": { 4 | "national-parks": [ 5 | { 6 | "nps_link": "https://www.nps.gov/acad/index.htm", 7 | "updated_at": "2018-11-13T21:51:11+00:00", 8 | "states": ["Maine"], 9 | "title": "Acadia", 10 | "external_id": "9000", 11 | "acres": 49057.36, 12 | "_index": "5beb4099c9f929367efa5b7d", 13 | "_type": "5beb4099c9f929367efa5b7c", 14 | "_score": 0.7048211, 15 | "_version": null, 16 | "_explanation": null, 17 | "sort": null, 18 | "highlight": { 19 | "title": "Acadia" 20 | }, 21 | "id": "5beb474f8db2315427beecc7" 22 | }, 23 | { 24 | "nps_link": "https://www.nps.gov/acad/index.htm", 25 | "title": "Acadia", 26 | "updated_at": "2018-11-13T21:52:56+00:00", 27 | "states": ["Maine"], 28 | "external_id": "0", 29 | "acres": 49057.36, 30 | "_index": "5beb4099c9f929367efa5b7d", 31 | "_type": "5beb4099c9f929367efa5b7c", 32 | "_score": 0.7048211, 33 | "_version": null, 34 | "_explanation": null, 35 | "sort": null, 36 | "highlight": { 37 | "title": "Acadia" 38 | }, 39 | "id": "5beb47b8a1f2532eb09be102" 40 | } 41 | ] 42 | }, 43 | "info": { 44 | "national-parks": { 45 | "query": "acadia", 46 | "current_page": 1, 47 | "num_pages": 1, 48 | "per_page": 20, 49 | "total_result_count": 2, 50 | "facets": { 51 | "states": { 52 | "Maine": 2 53 | } 54 | } 55 | } 56 | }, 57 | "errors": {} 58 | } 59 | --------------------------------------------------------------------------------