From 04ba6f20995ca057da0d77dce02d81f85b5acf9e Mon Sep 17 00:00:00 2001 From: Rpeter Date: Fri, 30 Jan 2026 12:51:14 +0100 Subject: [PATCH 1/6] feat: add search bar --- gui/package-lock.json | 304 ++++++++++++++++++ gui/package.json | 2 + gui/src/assets/i18n/en.json | 5 + gui/src/assets/i18n/fr.json | 5 + gui/src/javascript/App.css | 52 +++ .../components/store/AutocompleteSearch.tsx | 135 ++++++++ .../components/store/NavigationBar.tsx | 27 +- gui/src/javascript/main.tsx | 1 + gui/src/javascript/pages/StorePage.tsx | 7 +- .../javascript/types/autocomplete-theme.d.ts | 18 ++ 10 files changed, 554 insertions(+), 2 deletions(-) create mode 100644 gui/src/javascript/components/store/AutocompleteSearch.tsx create mode 100644 gui/src/javascript/types/autocomplete-theme.d.ts diff --git a/gui/package-lock.json b/gui/package-lock.json index ed30594..d39e5c4 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -8,6 +8,8 @@ "name": "gui", "version": "0.0.0", "dependencies": { + "@algolia/autocomplete-js": "^1.19.4", + "@algolia/autocomplete-theme-classic": "^1.19.4", "@coreui/coreui": "^5.4.3", "@coreui/icons": "^3.0.1", "@coreui/icons-react": "^2.3.0", @@ -37,6 +39,263 @@ "vitest": "^2.1.9" } }, + "node_modules/@algolia/abtesting": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.13.0.tgz", + "integrity": "sha512-Zrqam12iorp3FjiKMXSTpedGYznZ3hTEOAr2oCxI8tbF8bS1kQHClyDYNq/eV0ewMNLyFkgZVWjaS+8spsOYiQ==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.4.tgz", + "integrity": "sha512-yVwXLrfwQ3dAndY12j1pfa0oyC5hTDv+/dgwvVHj57dY3zN6PbAmcHdV5DOOdGJrCMXff+fsPr8G2Ik8zWOPTw==", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.4", + "@algolia/autocomplete-shared": "1.19.4" + } + }, + "node_modules/@algolia/autocomplete-js": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-js/-/autocomplete-js-1.19.4.tgz", + "integrity": "sha512-ZkwsuTTIEuw+hbsIooMrNLvTVulUSSKqJT3ZeYYd//kA5fHaFf2/T0BDmd9qSGxZRhT5WS8AJYjFARLmj5x08g==", + "dependencies": { + "@algolia/autocomplete-core": "1.19.4", + "@algolia/autocomplete-preset-algolia": "1.19.4", + "@algolia/autocomplete-shared": "1.19.4", + "htm": "^3.1.1", + "preact": "^10.13.2" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.5.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.4.tgz", + "integrity": "sha512-K6TQhTKxx0Es1ZbjlAQjgm/QLDOtKvw23MX0xmpvO7AwkmlmaEXo2PwHdVSs3Bquv28CkO2BYKks7jVSIdcXUg==", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.4" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.19.4.tgz", + "integrity": "sha512-WhX4mYosy7yBDjkB6c/ag+WKICjvV2fqQv/+NWJlpvnk2JtMaZByi73F6svpQX945J+/PxpQe8YIRBZHuYsLAQ==", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.4" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.4.tgz", + "integrity": "sha512-V7tYDgRXP0AqL4alwZBWNm1HPWjJvEU94Nr7Qa2cuPcIAbsTAj7M/F/+Pv/iwOWXl3N7tzVzNkOWm7sX6JT1SQ==", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-theme-classic": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-theme-classic/-/autocomplete-theme-classic-1.19.4.tgz", + "integrity": "sha512-/qE8BETNFbul4WrrUyBYgaaKcgFPk0Px9FDKADnr3HlIkXquRpcFHTxXK16jdwXb33yrcXaAVSQZRfUUSSnxVA==" + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.47.0.tgz", + "integrity": "sha512-aOpsdlgS9xTEvz47+nXmw8m0NtUiQbvGWNuSEb7fA46iPL5FxOmOUZkh8PREBJpZ0/H8fclSc7BMJCVr+Dn72w==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.47.0.tgz", + "integrity": "sha512-EcF4w7IvIk1sowrO7Pdy4Ako7x/S8+nuCgdk6En+u5jsaNQM4rTT09zjBPA+WQphXkA2mLrsMwge96rf6i7Mow==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.47.0.tgz", + "integrity": "sha512-Wzg5Me2FqgRDj0lFuPWFK05UOWccSMsIBL2YqmTmaOzxVlLZ+oUqvKbsUSOE5ud8Fo1JU7JyiLmEXBtgDKzTwg==", + "peer": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.47.0.tgz", + "integrity": "sha512-Ci+cn/FDIsDxSKMRBEiyKrqybblbk8xugo6ujDN1GSTv9RIZxwxqZYuHfdLnLEwLlX7GB8pqVyqrUSlRnR+sJA==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.47.0.tgz", + "integrity": "sha512-gsLnHPZmWcX0T3IigkDL2imCNtsQ7dR5xfnwiFsb+uTHCuYQt+IwSNjsd8tok6HLGLzZrliSaXtB5mfGBtYZvQ==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.47.0.tgz", + "integrity": "sha512-PDOw0s8WSlR2fWFjPQldEpmm/gAoUgLigvC3k/jCSi/DzigdGX6RdC0Gh1RR1P8Cbk5KOWYDuL3TNzdYwkfDyA==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.47.0.tgz", + "integrity": "sha512-b5hlU69CuhnS2Rqgsz7uSW0t4VqrLMLTPbUpEl0QVz56rsSwr1Sugyogrjb493sWDA+XU1FU5m9eB8uH7MoI0g==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.47.0.tgz", + "integrity": "sha512-WvwwXp5+LqIGISK3zHRApLT1xkuEk320/EGeD7uYy+K8WwDd5OjXnhjuXRhYr1685KnkvWkq1rQ/ihCJjOfHpQ==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.47.0.tgz", + "integrity": "sha512-j2EUFKAlzM0TE4GRfkDE3IDfkVeJdcbBANWzK16Tb3RHz87WuDfQ9oeEW6XiRE1/bEkq2xf4MvZesvSeQrZRDA==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.47.0.tgz", + "integrity": "sha512-+kTSE4aQ1ARj2feXyN+DMq0CIDHJwZw1kpxIunedkmpWUg8k3TzFwWsMCzJVkF2nu1UcFbl7xsIURz3Q3XwOXA==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.47.0.tgz", + "integrity": "sha512-Ja+zPoeSA2SDowPwCNRbm5Q2mzDvVV8oqxCQ4m6SNmbKmPlCfe30zPfrt9ho3kBHnsg37pGucwOedRIOIklCHw==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.47.0.tgz", + "integrity": "sha512-N6nOvLbaR4Ge+oVm7T4W/ea1PqcSbsHR4O58FJ31XtZjFPtOyxmnhgCmGCzP9hsJI6+x0yxJjkW5BMK/XI8OvA==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.47.0.tgz", + "integrity": "sha512-z1oyLq5/UVkohVXNDEY70mJbT/sv/t6HYtCvCwNrOri6pxBJDomP9R83KOlwcat+xqBQEdJHjbrPh36f1avmZA==", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1950,6 +2209,31 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/algoliasearch": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.47.0.tgz", + "integrity": "sha512-AGtz2U7zOV4DlsuYV84tLp2tBbA7RPtLA44jbVH4TTpDcc1dIWmULjHSsunlhscbzDydnjuFlNhflR3nV4VJaQ==", + "peer": true, + "dependencies": { + "@algolia/abtesting": "1.13.0", + "@algolia/client-abtesting": "5.47.0", + "@algolia/client-analytics": "5.47.0", + "@algolia/client-common": "5.47.0", + "@algolia/client-insights": "5.47.0", + "@algolia/client-personalization": "5.47.0", + "@algolia/client-query-suggestions": "5.47.0", + "@algolia/client-search": "5.47.0", + "@algolia/ingestion": "1.47.0", + "@algolia/monitoring": "1.47.0", + "@algolia/recommend": "5.47.0", + "@algolia/requester-browser-xhr": "5.47.0", + "@algolia/requester-fetch": "5.47.0", + "@algolia/requester-node-http": "5.47.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2685,6 +2969,11 @@ "hermes-estree": "0.25.1" } }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==" + }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -3192,6 +3481,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3399,6 +3697,12 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "peer": true + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", diff --git a/gui/package.json b/gui/package.json index ce400c2..60f2bee 100644 --- a/gui/package.json +++ b/gui/package.json @@ -11,6 +11,8 @@ "test": "vitest run" }, "dependencies": { + "@algolia/autocomplete-js": "^1.19.4", + "@algolia/autocomplete-theme-classic": "^1.19.4", "@coreui/coreui": "^5.4.3", "@coreui/icons": "^3.0.1", "@coreui/icons-react": "^2.3.0", diff --git a/gui/src/assets/i18n/en.json b/gui/src/assets/i18n/en.json index 1759c78..6b9c18e 100644 --- a/gui/src/assets/i18n/en.json +++ b/gui/src/assets/i18n/en.json @@ -79,6 +79,11 @@ "file": "File", "createFolder": "Create Folder", "upload": "Upload", + "searchPlaceholder": "Search", + "searchClear": "Clear search", + "searchLoading": "Searching…", + "searchNoResults": "No results", + "searchUnavailable": "Search unavailable", "enterFolderName": "Enter folder name", "createModal": { "titleFolder": "Create Folder", diff --git a/gui/src/assets/i18n/fr.json b/gui/src/assets/i18n/fr.json index f05a9eb..4009d9d 100644 --- a/gui/src/assets/i18n/fr.json +++ b/gui/src/assets/i18n/fr.json @@ -78,6 +78,11 @@ "file": "Fichier", "createFolder": "Créer dossier", "upload": "Téléverser", + "searchPlaceholder": "Rechercher", + "searchClear": "Effacer la recherche", + "searchLoading": "Recherche…", + "searchNoResults": "Aucun résultat", + "searchUnavailable": "Recherche indisponible", "enterFolderName": "Entrez le nom du dossier", "createModal": { "titleFolder": "Créer un dossier", diff --git a/gui/src/javascript/App.css b/gui/src/javascript/App.css index c81b8c5..4331b67 100644 --- a/gui/src/javascript/App.css +++ b/gui/src/javascript/App.css @@ -22,6 +22,58 @@ padding: 0; } +/* --- Search highlights --- */ +.highlighted { + background: #fff3cd; + color: #664d03; + padding: 0 2px; + border-radius: 2px; +} + +/* --- Autocomplete search (Typesense) --- */ +.mbyte-search { + width: 260px; +} + +.mbyte-search .aa-Form { + border: 1px solid rgba(0, 0, 0, 0.15); + box-shadow: none; +} + +.mbyte-search .aa-Input { + font-size: 0.875rem; +} + +.mbyte-search .aa-Panel { + border: 1px solid rgba(0, 0, 0, 0.15); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); +} + +.mbyte-search-item__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.mbyte-search-item__title { + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mbyte-search-item__meta { + font-size: 0.75rem; + color: rgba(0, 0, 0, 0.5); +} + +.mbyte-search-item__snippet { + font-size: 0.75rem; + color: rgba(0, 0, 0, 0.6); + margin-top: 4px; +} + /* --- User block (SidebarProfile) --- */ .mbyte-userblock { display: flex; diff --git a/gui/src/javascript/components/store/AutocompleteSearch.tsx b/gui/src/javascript/components/store/AutocompleteSearch.tsx new file mode 100644 index 0000000..46292e2 --- /dev/null +++ b/gui/src/javascript/components/store/AutocompleteSearch.tsx @@ -0,0 +1,135 @@ +/// +/// Copyright (C) 2025 Jerome Blanchard +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU General Public License as published by +/// the Free Software Foundation, either version 3 of the License, or +/// (at your option) any later version. +/// +/// This program is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/// GNU General Public License for more details. +/// +/// You should have received a copy of the GNU General Public License +/// along with this program. If not, see . +/// + +import { useEffect, useRef } from 'react' +import { autocomplete } from '@algolia/autocomplete-js' +import type SearchResult from '../../api/entities/SearchResult' + +type AutocompleteSearchProps = Readonly<{ + search: (query: string) => Promise + onSelect: (id: string) => void + placeholder: string + noResultsLabel: string +}> + +type AutocompleteItem = { + type: string + identifier: string + explain: string + value: unknown + [key: string]: unknown +} + +const HIGHLIGHT_START = "" +const HIGHLIGHT_END = '' + +const escapeHtml = (value: string) => + value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') + +const toSafeHighlightHtml = (value: string) => { + if (!value) return '' + const normalized = value.replaceAll('', HIGHLIGHT_START) + let result = '' + let index = 0 + while (index < normalized.length) { + const start = normalized.indexOf(HIGHLIGHT_START, index) + if (start === -1) { + result += escapeHtml(normalized.slice(index)) + break + } + if (start > index) { + result += escapeHtml(normalized.slice(index, start)) + } + const end = normalized.indexOf(HIGHLIGHT_END, start + HIGHLIGHT_START.length) + if (end === -1) { + result += escapeHtml(normalized.slice(start)) + break + } + const text = normalized.slice(start + HIGHLIGHT_START.length, end) + result += `${HIGHLIGHT_START}${escapeHtml(text)}${HIGHLIGHT_END}` + index = end + HIGHLIGHT_END.length + } + return result +} + +export function AutocompleteSearch({ search, onSelect, placeholder, noResultsLabel }: AutocompleteSearchProps) { + const containerRef = useRef(null) + + useEffect(() => { + if (!containerRef.current) return + + const instance = autocomplete({ + container: containerRef.current, + placeholder, + openOnFocus: true, + detachedMediaQuery: '', + getSources({ query }) { + const trimmed = query.trim() + if (!trimmed) return [] + return [ + { + sourceId: 'store-search', + async getItems() { + const results = await search(trimmed).catch(() => []) + return results.map((item) => ({ + type: item.type, + identifier: item.identifier, + explain: item.explain, + value: item.value, + })) + }, + onSelect({ item, setQuery, setIsOpen }) { + setQuery('') + setIsOpen(false) + onSelect(item.identifier) + }, + templates: { + item({ item }) { + const title = escapeHtml(item.identifier ?? '') + const meta = escapeHtml(item.type ?? '') + const explain = item.explain ? toSafeHighlightHtml(item.explain) : '' + return ` +
+
+
${title}
+ ${meta ? `
${meta}
` : ''} +
+ ${explain ? `
${explain}
` : ''} +
+ ` + }, + noResults() { + return `
${escapeHtml(noResultsLabel)}
` + }, + }, + }, + ] + }, + }) + + return () => { + instance.destroy() + } + }, [onSelect, placeholder, search]) + + return
+} diff --git a/gui/src/javascript/components/store/NavigationBar.tsx b/gui/src/javascript/components/store/NavigationBar.tsx index 88a6d42..7019cd3 100644 --- a/gui/src/javascript/components/store/NavigationBar.tsx +++ b/gui/src/javascript/components/store/NavigationBar.tsx @@ -2,6 +2,8 @@ import { CButton } from '@coreui/react' import { CIcon } from '@coreui/icons-react' import { cilGrid, cilList, cilHome, cilInfo, cilFolder, cilCloudUpload } from '@coreui/icons' import { useTranslation } from 'react-i18next' +import { AutocompleteSearch } from './AutocompleteSearch' +import type SearchResult from '../../api/entities/SearchResult' type BreadcrumbItemInline = { id?: string, name: string } @@ -16,10 +18,25 @@ type NavigationBarProps = Readonly<{ onNavigate?: (folderId?: string) => void onCreateFolder?: () => void onUploadFile?: () => void + onSearch?: (query: string) => Promise + onSearchSelect?: (id: string) => void }> -export function NavigationBar({ breadcrumb, viewMode, setViewMode, detailVisible, toggleDetail, setCurrentPath, onNavigate, onCreateFolder, onUploadFile }: NavigationBarProps) { +export function NavigationBar({ + breadcrumb, + viewMode, + setViewMode, + detailVisible, + toggleDetail, + setCurrentPath, + onNavigate, + onCreateFolder, + onUploadFile, + onSearch, + onSearchSelect, +}: NavigationBarProps) { const { t } = useTranslation() + const showSearch = typeof onSearch === 'function' && typeof onSearchSelect === 'function' return (
@@ -61,6 +78,14 @@ export function NavigationBar({ breadcrumb, viewMode, setViewMode, detailVisible
+ {showSearch && ( + + )} diff --git a/gui/src/javascript/main.tsx b/gui/src/javascript/main.tsx index 7cf0c8a..bec3e2d 100644 --- a/gui/src/javascript/main.tsx +++ b/gui/src/javascript/main.tsx @@ -4,6 +4,7 @@ import { createRoot } from 'react-dom/client' import './i18n' import '@coreui/coreui/dist/css/coreui.min.css' +import '@algolia/autocomplete-theme-classic/dist/theme.css' import './index.css' import { BrowserRouter } from 'react-router-dom' diff --git a/gui/src/javascript/pages/StorePage.tsx b/gui/src/javascript/pages/StorePage.tsx index 8c5085a..551c980 100644 --- a/gui/src/javascript/pages/StorePage.tsx +++ b/gui/src/javascript/pages/StorePage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useRef } from 'react' +import { useEffect, useState, useRef, useCallback } from 'react' import { CContainer } from '@coreui/react' import { NavigationBar } from '../components/store/NavigationBar' import { BrowserArea } from '../components/store/BrowserArea' @@ -166,6 +166,9 @@ export function StorePage() { navigate(`/s/0${folderId ? `/${folderId}` : ''}`) } + const handleSearch = useCallback((query: string) => storeApi.search(query), [storeApi]) + const handleSearchSelect = useCallback((id: string) => navigate(`/s/0/${id}`), [navigate]) + const handleView = async (n?: Node | null) => { if (!n) return if (n.isFolder) { @@ -257,6 +260,8 @@ export function StorePage() { onNavigate={(folderId) => handleOpenFolder(folderId)} onCreateFolder={handleCreateFolder} onUploadFile={handleUploadFile} + onSearch={handleSearch} + onSearchSelect={handleSearchSelect} />
diff --git a/gui/src/javascript/types/autocomplete-theme.d.ts b/gui/src/javascript/types/autocomplete-theme.d.ts new file mode 100644 index 0000000..e1ad88f --- /dev/null +++ b/gui/src/javascript/types/autocomplete-theme.d.ts @@ -0,0 +1,18 @@ +/// +/// Copyright (C) 2025 Jerome Blanchard +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU General Public License as published by +/// the Free Software Foundation, either version 3 of the License, or +/// (at your option) any later version. +/// +/// This program is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/// GNU General Public License for more details. +/// +/// You should have received a copy of the GNU General Public License +/// along with this program. If not, see . +/// + +declare module '@algolia/autocomplete-theme-classic/dist/theme.css'; From 4bf274c5696f1e93729b8df23a705317900086b3 Mon Sep 17 00:00:00 2001 From: PETER Date: Fri, 13 Mar 2026 15:53:07 +0100 Subject: [PATCH 2/6] feat: change lucene to typesense --- docker-compose.yml | 19 ++ gui/image.png | Bin 0 -> 127989 bytes gui/maven/provisioning/nginx.conf | 9 + .../javascript/api/entities/Application.ts | 2 +- .../components/dashboard/DetailStoreCard.tsx | 4 + .../components/store/AutocompleteSearch.tsx | 89 +++++--- gui/src/javascript/pages/DashboardPage.tsx | 3 +- gui/src/javascript/pages/StorePage.tsx | 10 +- gui/src/javascript/utils/storeApp.ts | 30 +++ manager/mvnw | 0 manager/mvnw.cmd | 0 .../mbyte/manager/core/CoreServiceBean.java | 36 ++- .../task/store/CreateDockerStoreTask.java | 30 ++- node_modules/.package-lock.json | 6 + package-lock.json | 6 + package.json | 1 + store/mvnw | 0 store/mvnw.cmd | 0 store/pom.xml | 16 -- .../mbyte/store/files/FileServiceBean.java | 7 + .../store/index/IndexStoreBootstrapBean.java | 61 +++++ .../mbyte/store/index/IndexStoreConfig.java | 23 +- .../index/IndexStoreDocumentBuilder.java | 40 ++-- .../store/index/IndexStoreServiceBean.java | 209 +++++++++++------- .../mbyte/store/index/IndexableContent.java | 54 +++++ .../store/search/SearchServiceException.java | 1 + .../src/main/resources/application.properties | 9 +- 27 files changed, 515 insertions(+), 150 deletions(-) create mode 100644 gui/image.png create mode 100644 gui/maven/provisioning/nginx.conf create mode 100644 gui/src/javascript/utils/storeApp.ts mode change 100755 => 100644 manager/mvnw mode change 100755 => 100644 manager/mvnw.cmd create mode 100644 node_modules/.package-lock.json create mode 100644 package-lock.json create mode 100644 package.json mode change 100755 => 100644 store/mvnw mode change 100755 => 100644 store/mvnw.cmd create mode 100644 store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreBootstrapBean.java diff --git a/docker-compose.yml b/docker-compose.yml index d15aae7..1a5349b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,6 +56,20 @@ services: mbyte.fr: ipv4_address: 172.25.0.4 + typesense: + image: typesense/typesense:27.1 + hostname: typesense + container_name: mbyte.typesense + command: + - "--data-dir=/data" + - "--api-key=change-me-typesense-key" + - "--enable-cors" + volumes: + - mbyte.typesense.data:/data + networks: + mbyte.fr: + ipv4_address: 172.25.0.9 + keycloak: build: context: . @@ -104,8 +118,11 @@ services: container_name: mbyte.manager depends_on: - db + - typesense - traefik - keycloak + group_add: + - "1001" volumes: - /var/run/docker.sock:/var/run/docker.sock:rw extra_hosts: @@ -169,3 +186,5 @@ volumes: type: none o: bind device: /var/mbyte/db + mbyte.typesense.data: + name: mbyte.fr.typesense.data diff --git a/gui/image.png b/gui/image.png new file mode 100644 index 0000000000000000000000000000000000000000..dcfd8ff282aad2483b88ab1c9261fef68f2dbfc8 GIT binary patch literal 127989 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%0Ac)p#gg275%S; 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

3XHFT{;T^nlf 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 literal 0 HcmV?d00001 diff --git a/gui/maven/provisioning/nginx.conf b/gui/maven/provisioning/nginx.conf new file mode 100644 index 0000000..ba9ae4e --- /dev/null +++ b/gui/maven/provisioning/nginx.conf @@ -0,0 +1,9 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri /index.html; + } +} diff --git a/gui/src/javascript/api/entities/Application.ts b/gui/src/javascript/api/entities/Application.ts index 26c037e..6a92fe9 100644 --- a/gui/src/javascript/api/entities/Application.ts +++ b/gui/src/javascript/api/entities/Application.ts @@ -21,5 +21,5 @@ export type Application = { name?: string type?: string creationDate?: number - status?: 'CREATED' | 'STARTING' | 'AVAILABLE' | 'LOST' | 'ERROR' + status?: 'CREATED' | 'STARTED' | 'STARTING' | 'AVAILABLE' | 'LOST' | 'ERROR' | 'STOPPED' } diff --git a/gui/src/javascript/components/dashboard/DetailStoreCard.tsx b/gui/src/javascript/components/dashboard/DetailStoreCard.tsx index 02819e4..92bfb2f 100644 --- a/gui/src/javascript/components/dashboard/DetailStoreCard.tsx +++ b/gui/src/javascript/components/dashboard/DetailStoreCard.tsx @@ -58,10 +58,14 @@ export function DetailStoreCard({ app, onRefresh }: DetailStoreCardProps) { switch (status) { case 'AVAILABLE': return 'success' + case 'STARTED': + return 'success' case 'STARTING': return 'warning' case 'CREATED': return 'info' + case 'STOPPED': + return 'secondary' case 'ERROR': return 'danger' case 'LOST': diff --git a/gui/src/javascript/components/store/AutocompleteSearch.tsx b/gui/src/javascript/components/store/AutocompleteSearch.tsx index 46292e2..dbb4ad0 100644 --- a/gui/src/javascript/components/store/AutocompleteSearch.tsx +++ b/gui/src/javascript/components/store/AutocompleteSearch.tsx @@ -34,39 +34,56 @@ type AutocompleteItem = { [key: string]: unknown } -const HIGHLIGHT_START = "" -const HIGHLIGHT_END = '' +const HIGHLIGHT_SPAN_START = "" +const HIGHLIGHT_SPAN_START_DOUBLE_QUOTES = '' +const HIGHLIGHT_SPAN_END = '' +const HIGHLIGHT_MARK_START = '' +const HIGHLIGHT_MARK_END = '' -const escapeHtml = (value: string) => - value - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", ''') +type HighlightSegment = { + text: string + highlighted: boolean + className?: string +} -const toSafeHighlightHtml = (value: string) => { - if (!value) return '' - const normalized = value.replaceAll('', HIGHLIGHT_START) - let result = '' +const toHighlightSegments = (value: string): HighlightSegment[] => { + if (!value) return [] + const normalized = value + .replaceAll(HIGHLIGHT_SPAN_START_DOUBLE_QUOTES, HIGHLIGHT_SPAN_START) + const result: HighlightSegment[] = [] let index = 0 while (index < normalized.length) { - const start = normalized.indexOf(HIGHLIGHT_START, index) + const spanStart = normalized.indexOf(HIGHLIGHT_SPAN_START, index) + const markStart = normalized.indexOf(HIGHLIGHT_MARK_START, index) + let start = -1 + let startToken = '' + let endToken = '' + let className: string | undefined + if (spanStart !== -1 && (markStart === -1 || spanStart < markStart)) { + start = spanStart + startToken = HIGHLIGHT_SPAN_START + endToken = HIGHLIGHT_SPAN_END + className = 'highlighted' + } else if (markStart !== -1) { + start = markStart + startToken = HIGHLIGHT_MARK_START + endToken = HIGHLIGHT_MARK_END + } if (start === -1) { - result += escapeHtml(normalized.slice(index)) + result.push({ text: normalized.slice(index), highlighted: false }) break } if (start > index) { - result += escapeHtml(normalized.slice(index, start)) + result.push({ text: normalized.slice(index, start), highlighted: false }) } - const end = normalized.indexOf(HIGHLIGHT_END, start + HIGHLIGHT_START.length) + const end = normalized.indexOf(endToken, start + startToken.length) if (end === -1) { - result += escapeHtml(normalized.slice(start)) + result.push({ text: normalized.slice(start), highlighted: false }) break } - const text = normalized.slice(start + HIGHLIGHT_START.length, end) - result += `${HIGHLIGHT_START}${escapeHtml(text)}${HIGHLIGHT_END}` - index = end + HIGHLIGHT_END.length + const text = normalized.slice(start + startToken.length, end) + result.push({ text, highlighted: true, className }) + index = end + endToken.length } return result } @@ -101,24 +118,32 @@ export function AutocompleteSearch({ search, onSelect, placeholder, noResultsLab setQuery('') setIsOpen(false) onSelect(item.identifier) - }, - templates: { - item({ item }) { - const title = escapeHtml(item.identifier ?? '') - const meta = escapeHtml(item.type ?? '') - const explain = item.explain ? toSafeHighlightHtml(item.explain) : '' - return ` + }, + templates: { + item({ item, html }) { + const title = item.identifier ?? '' + const meta = item.type ?? '' + const explainSegments = toHighlightSegments(item.explain ?? '') + return html`

` }, - noResults() { - return `
${escapeHtml(noResultsLabel)}
` + noResults({ html }) { + return html`
${noResultsLabel}
` }, }, }, diff --git a/gui/src/javascript/pages/DashboardPage.tsx b/gui/src/javascript/pages/DashboardPage.tsx index dd749aa..ccadab9 100644 --- a/gui/src/javascript/pages/DashboardPage.tsx +++ b/gui/src/javascript/pages/DashboardPage.tsx @@ -5,6 +5,7 @@ import {useManagerStatus} from '../auth/useManagerStatus' import {useProfile} from '../auth/useProfile' import { useManagerApi } from '../api/ManagerApiProvider' import { useState } from 'react' +import { selectPreferredStoreApp } from '../utils/storeApp' type DashboardPageProps = { onNotify: (message: string) => void @@ -18,7 +19,7 @@ export function DashboardPage({ onNotify }: DashboardPageProps) { const [creationBusy, setCreationBusy] = useState(false) - const storeApp = apps.find(a => a.type === 'DOCKER_STORE') + const storeApp = selectPreferredStoreApp(apps) const handleCreateStore = async () => { if (!profile?.id) return diff --git a/gui/src/javascript/pages/StorePage.tsx b/gui/src/javascript/pages/StorePage.tsx index 551c980..0d5e614 100644 --- a/gui/src/javascript/pages/StorePage.tsx +++ b/gui/src/javascript/pages/StorePage.tsx @@ -10,6 +10,7 @@ import { useAccessToken } from '../auth/useAccessToken' import { useStoreApi } from '../api/useStoreApi' import { useManagerStatus } from '../auth/useManagerStatus' import { apiConfig } from '../api/apiConfig' +import { selectPreferredStoreApp } from '../utils/storeApp' type BreadcrumbItem = { id?: string, name: string } @@ -31,7 +32,7 @@ export function StorePage() { const { apps } = useManagerStatus() // compute per-user store base URL from the DOCKER_STORE app name when present - const userStoreApp = apps.find((a) => a?.type === 'DOCKER_STORE') + const userStoreApp = selectPreferredStoreApp(apps) const storeBaseUrl = userStoreApp?.name ? `${apiConfig.storesScheme}://${userStoreApp.name}.${apiConfig.storesDomain}/` : undefined const storeApi = useStoreApi(tokenProvider, storeBaseUrl) @@ -223,7 +224,12 @@ export function StorePage() { } const handleModalConfirm = async (data: string | File) => { - const parentId = params['*'] || (await storeApi.getRoot()).id + const routeId = params['*'] + const currentFolderId = currentPath.at(-1)?.id + const parentId = + routeId && routeId === currentFolderId + ? routeId + : currentFolderId ?? (await storeApi.getRoot()).id try { if (modalType === 'folder') { await storeApi.create(parentId, data as string) diff --git a/gui/src/javascript/utils/storeApp.ts b/gui/src/javascript/utils/storeApp.ts new file mode 100644 index 0000000..11c9b11 --- /dev/null +++ b/gui/src/javascript/utils/storeApp.ts @@ -0,0 +1,30 @@ +import type { Application } from '../api/entities/Application' + +function statusRank(status: Application['status']) { + switch (status) { + case 'AVAILABLE': + return 5 + case 'STARTED': + return 4 + case 'CREATED': + return 3 + case 'STOPPED': + return 2 + case 'LOST': + case 'ERROR': + return 1 + default: + return 0 + } +} + +export function selectPreferredStoreApp(apps: Application[]) { + const storeApps = apps.filter((app) => app.type === 'DOCKER_STORE') + return storeApps.sort((left, right) => { + const byStatus = statusRank(right.status) - statusRank(left.status) + if (byStatus !== 0) { + return byStatus + } + return (right.creationDate ?? 0) - (left.creationDate ?? 0) + })[0] +} diff --git a/manager/mvnw b/manager/mvnw old mode 100755 new mode 100644 diff --git a/manager/mvnw.cmd b/manager/mvnw.cmd old mode 100755 new mode 100644 diff --git a/manager/src/main/java/fr/jayblanc/mbyte/manager/core/CoreServiceBean.java b/manager/src/main/java/fr/jayblanc/mbyte/manager/core/CoreServiceBean.java index 4a34ca8..14a0d9f 100644 --- a/manager/src/main/java/fr/jayblanc/mbyte/manager/core/CoreServiceBean.java +++ b/manager/src/main/java/fr/jayblanc/mbyte/manager/core/CoreServiceBean.java @@ -59,13 +59,34 @@ public class CoreServiceBean implements CoreService, CoreServiceAdmin { @Transactional(Transactional.TxType.REQUIRED) public String createApp(String type, String name) throws ApplicationDescriptorNotFoundException, NotificationServiceException { LOGGER.log(Level.INFO, "Creating new application of type: {0} with name: {1}", new Object[] {type, name}); + String owner = authentication.getConnectedIdentifier(); + if ("DOCKER_STORE".equals(type)) { + List existingApps = em.createNamedQuery("Application.findByOwnerAndType", Application.class) + .setParameter("owner", owner) + .setParameter("type", type) + .getResultList(); + if (!existingApps.isEmpty()) { + Application existing = existingApps.stream() + .sorted((left, right) -> { + int statusCompare = Integer.compare(storeStatusRank(right.getStatus()), storeStatusRank(left.getStatus())); + if (statusCompare != 0) { + return statusCompare; + } + return Long.compare(right.getCreationDate(), left.getCreationDate()); + }) + .findFirst() + .orElse(existingApps.getFirst()); + LOGGER.log(Level.INFO, "Reusing existing store application for owner: {0}, app id: {1}", new Object[] {owner, existing.getId()}); + return existing.getId(); + } + } String appid = UUID.randomUUID().toString(); Application application = new Application(); application.setId(appid); application.setName(name); application.setType(type); application.setCreationDate(System.currentTimeMillis()); - application.setOwner(authentication.getConnectedIdentifier()); + application.setOwner(owner); application.setStatus(ApplicationStatus.CREATED); em.persist(application); Environment initialEnv = appRegistry.findDescriptor(type).getInitialEnv(config.instance(), appid, name, application.getOwner()); @@ -203,6 +224,19 @@ private List findAppsByOwner(String owner) { return em.createNamedQuery("Application.findByOwner", Application.class).setParameter("owner", owner).getResultList(); } + private int storeStatusRank(ApplicationStatus status) { + if (status == null) { + return 0; + } + return switch (status) { + case AVAILABLE -> 5; + case STARTED -> 4; + case CREATED -> 3; + case STOPPED -> 2; + case LOST, ERROR -> 1; + }; + } + private String locateApp(String id) { String location = topology.lookup(id); if ( location != null ) { diff --git a/manager/src/main/java/fr/jayblanc/mbyte/manager/core/runtime/task/store/CreateDockerStoreTask.java b/manager/src/main/java/fr/jayblanc/mbyte/manager/core/runtime/task/store/CreateDockerStoreTask.java index 0ac0ff5..2bfe963 100644 --- a/manager/src/main/java/fr/jayblanc/mbyte/manager/core/runtime/task/store/CreateDockerStoreTask.java +++ b/manager/src/main/java/fr/jayblanc/mbyte/manager/core/runtime/task/store/CreateDockerStoreTask.java @@ -82,6 +82,14 @@ public void execute() throws TaskException { "QUARKUS_HTTP_PORT=8080", "STORE.ROOT=/home/jboss", "STORE.AUTH.OWNER=" + storeOwner, + "STORE.INDEX.BACKEND=TYPESENSE", + "STORE.INDEX.BOOTSTRAP.REINDEX=true", + "STORE.INDEX.TYPESENSE.PROTOCOL=http", + "STORE.INDEX.TYPESENSE.HOST=typesense", + "STORE.INDEX.TYPESENSE.PORT=8108", + "STORE.INDEX.TYPESENSE.API-KEY=change-me-typesense-key", + "STORE.INDEX.TYPESENSE.COLLECTION=store_nodes", + "STORE.INDEX.TYPESENSE.STORE-ID=" + storeOwner, "STORE.TOPOLOGY.ENABLED=" + storeTopologyEnabled, "STORE.TOPOLOGY.HOST=consul", "STORE.TOPOLOGY.PORT=8500", @@ -129,6 +137,14 @@ public void execute() throws TaskException { "QUARKUS_HTTP_PORT=8080", "STORE.ROOT=/home/jboss", "STORE.AUTH.OWNER=" + storeOwner, + "STORE.INDEX.BACKEND=TYPESENSE", + "STORE.INDEX.BOOTSTRAP.REINDEX=true", + "STORE.INDEX.TYPESENSE.PROTOCOL=http", + "STORE.INDEX.TYPESENSE.HOST=typesense", + "STORE.INDEX.TYPESENSE.PORT=8108", + "STORE.INDEX.TYPESENSE.API-KEY=change-me-typesense-key", + "STORE.INDEX.TYPESENSE.COLLECTION=store_nodes", + "STORE.INDEX.TYPESENSE.STORE-ID=" + storeOwner, "STORE.TOPOLOGY.ENABLED=" + storeTopologyEnabled, "STORE.TOPOLOGY.HOST=consul", "STORE.TOPOLOGY.PORT=8500", @@ -144,11 +160,15 @@ public void execute() throws TaskException { Map labels = inspect.getConfig().getLabels(); Map expectedLabels = Map.of( "traefik.enable", "true", - "traefik.docker.network", "mbyte", - "traefik.http.routers." + storeOwner + ".rule", "Host(`" + storeFqdn + "`)", - "traefik.http.routers." + storeOwner + ".entrypoints", "http", - "traefik.http.routers." + storeOwner + ".service", storeOwner + "-http", - "traefik.http.services." + storeOwner + "-http.loadbalancer.server.port", "8080" + "traefik.docker.network", networkName, + "traefik.http.routers." + storeName + ".rule", "Host(`" + storeFqdn + "`)", + "traefik.http.routers." + storeName + ".entrypoints", "websecure", + "traefik.http.routers." + storeName + ".tls", "true", + "traefik.http.routers." + storeName + "-http.rule", "Host(`" + storeFqdn + "`)", + "traefik.http.routers." + storeName + "-http.entrypoints", "web", + "traefik.http.routers." + storeName + "-http.middlewares", "redirect-to-https", + "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme", "https", + "traefik.http.services." + storeName + ".loadbalancer.server.port", "8080" ); if (labels == null || !labels.entrySet().containsAll(expectedLabels.entrySet())) { this.fail("Existing container labels do not match expected"); diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..7137593 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "mbyte", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7137593 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "mbyte", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/store/mvnw b/store/mvnw old mode 100755 new mode 100644 diff --git a/store/mvnw.cmd b/store/mvnw.cmd old mode 100755 new mode 100644 diff --git a/store/pom.xml b/store/pom.xml index 28134f5..65500ba 100644 --- a/store/pom.xml +++ b/store/pom.xml @@ -14,7 +14,6 @@ true 2.9.1 - 9.4.1 1.5.3 @@ -104,21 +103,6 @@ tika-core ${version.tika} - - org.apache.lucene - lucene-core - ${version.lucene} - - - org.apache.lucene - lucene-queryparser - ${version.lucene} - - - org.apache.lucene - lucene-highlighter - ${version.lucene} - com.orbitz.consul consul-client diff --git a/store/src/main/java/fr/jayblanc/mbyte/store/files/FileServiceBean.java b/store/src/main/java/fr/jayblanc/mbyte/store/files/FileServiceBean.java index 28d52dd..56013ad 100644 --- a/store/src/main/java/fr/jayblanc/mbyte/store/files/FileServiceBean.java +++ b/store/src/main/java/fr/jayblanc/mbyte/store/files/FileServiceBean.java @@ -55,6 +55,7 @@ public class FileServiceBean implements FileService, IndexableContentProvider { @Inject DataStore datastore; @Inject NotificationService notification; @Inject AuthenticationService auth; + @Inject fr.jayblanc.mbyte.store.index.IndexStoreConfig indexConfig; @Inject EntityManager em; public FileServiceBean() { @@ -249,9 +250,15 @@ public IndexableContent getIndexableContent(String id) { content.setIdentifier(id); content.setType("node"); content.setScope(IndexableContent.Scope.PRIVATE); + content.setStoreId(indexConfig.typesense().storeId()); content.setContent(""); try { Node node = systemLoadNode(id); + content.setName(node.getName()); + content.setMimetype(node.getMimetype()); + content.setNodeType(node.getType().name()); + content.setParent(node.getParent()); + content.setModifiedAt(node.getModification()); if (node.isFolder()) { content.setContent(node.getName() + " " + node.getMimetype()); } else { diff --git a/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreBootstrapBean.java b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreBootstrapBean.java new file mode 100644 index 0000000..efd8092 --- /dev/null +++ b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreBootstrapBean.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 Jerome Blanchard + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.jayblanc.mbyte.store.index; + +import fr.jayblanc.mbyte.store.files.FileServiceBean; +import fr.jayblanc.mbyte.store.files.entity.Node; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.persistence.EntityManager; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Startup +@Singleton +public class IndexStoreBootstrapBean { + + private static final Logger LOGGER = Logger.getLogger(IndexStoreBootstrapBean.class.getName()); + + @Inject IndexStoreConfig config; + @Inject EntityManager em; + @Inject FileServiceBean files; + @Inject IndexStoreService index; + + @PostConstruct + public void reindexIfEnabled() { + if (!config.bootstrap().reindex()) { + LOGGER.log(Level.INFO, "Typesense bootstrap reindex disabled"); + return; + } + List nodes = em.createNamedQuery("Node.findAll", Node.class).getResultList(); + LOGGER.log(Level.INFO, "Reindexing {0} node(s) into Typesense", nodes.size()); + int indexed = 0; + for (Node node : nodes) { + try { + index.index(files.getIndexableContent(node.getId())); + indexed++; + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to reindex node " + node.getId(), e); + } + } + LOGGER.log(Level.INFO, "Typesense reindex completed, indexed {0}/{1} node(s)", new Object[]{indexed, nodes.size()}); + } +} diff --git a/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreConfig.java b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreConfig.java index 8146677..56757f9 100644 --- a/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreConfig.java +++ b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreConfig.java @@ -23,5 +23,26 @@ */ @ConfigMapping(prefix = "store.index") public interface IndexStoreConfig { - String home(); + Backend backend(); + + Bootstrap bootstrap(); + + Typesense typesense(); + + enum Backend { + TYPESENSE + } + + interface Bootstrap { + boolean reindex(); + } + + interface Typesense { + String protocol(); + String host(); + int port(); + String apiKey(); + String collection(); + String storeId(); + } } diff --git a/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreDocumentBuilder.java b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreDocumentBuilder.java index bd17737..e4b5cc8 100644 --- a/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreDocumentBuilder.java +++ b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreDocumentBuilder.java @@ -16,25 +16,37 @@ */ package fr.jayblanc.mbyte.store.index; -import org.apache.lucene.document.Document; -import org.apache.lucene.document.Field; -import org.apache.lucene.document.StringField; -import org.apache.lucene.document.TextField; +import java.util.LinkedHashMap; +import java.util.Map; public class IndexStoreDocumentBuilder { - public static final String TYPE_FIELD = "TYPE"; - public static final String IDENTIFIER_FIELD = "IDENTIFIER"; - public static final String SCOPE_FIELD = "SCOPE"; - public static final String CONTENT_FIELD = "CONTENT"; + public static final String ID_FIELD = "id"; + public static final String TYPE_FIELD = "type"; + public static final String SCOPE_FIELD = "scope"; + public static final String CONTENT_FIELD = "content"; + public static final String NAME_FIELD = "name"; + public static final String MIMETYPE_FIELD = "mimetype"; + public static final String NODE_TYPE_FIELD = "node_type"; + public static final String PARENT_FIELD = "parent"; + public static final String STORE_ID_FIELD = "store_id"; + public static final String MODIFIED_AT_FIELD = "modified_at"; - public static Document buildDocument(IndexableContent object) { - Document document = new Document(); - document.add(new Field(TYPE_FIELD, object.getType(), StringField.TYPE_STORED)); - document.add(new Field(IDENTIFIER_FIELD, object.getIdentifier(), StringField.TYPE_STORED)); - document.add(new Field(SCOPE_FIELD, object.getScope().name(), StringField.TYPE_STORED)); - document.add(new Field(CONTENT_FIELD, object.getContent(), TextField.TYPE_STORED)); + public static Map buildDocument(IndexableContent object) { + Map document = new LinkedHashMap<>(); + document.put(ID_FIELD, object.getIdentifier()); + document.put(TYPE_FIELD, object.getType()); + document.put(SCOPE_FIELD, object.getScope().name()); + document.put(CONTENT_FIELD, object.getContent()); + document.put(NAME_FIELD, object.getName()); + document.put(MIMETYPE_FIELD, object.getMimetype()); + document.put(NODE_TYPE_FIELD, object.getNodeType()); + document.put(PARENT_FIELD, object.getParent()); + document.put(STORE_ID_FIELD, object.getStoreId()); + document.put(MODIFIED_AT_FIELD, object.getModifiedAt()); return document; } + private IndexStoreDocumentBuilder() { + } } diff --git a/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreServiceBean.java b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreServiceBean.java index 84668a9..d0041fc 100644 --- a/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreServiceBean.java +++ b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreServiceBean.java @@ -16,29 +16,22 @@ */ package fr.jayblanc.mbyte.store.index; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.runtime.Startup; import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.transaction.Transactional; -import org.apache.lucene.analysis.Analyzer; -import org.apache.lucene.analysis.standard.StandardAnalyzer; -import org.apache.lucene.document.Document; -import org.apache.lucene.index.*; -import org.apache.lucene.queryparser.classic.QueryParser; -import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.Query; -import org.apache.lucene.search.TopDocs; -import org.apache.lucene.search.highlight.Highlighter; -import org.apache.lucene.search.highlight.QueryScorer; -import org.apache.lucene.search.highlight.SimpleHTMLFormatter; -import org.apache.lucene.store.Directory; -import org.apache.lucene.store.FSDirectory; import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; @@ -51,49 +44,35 @@ public class IndexStoreServiceBean implements IndexStoreService { private static final Logger LOGGER = Logger.getLogger(IndexStoreServiceBean.class.getName()); @Inject IndexStoreConfig config; + @Inject ObjectMapper mapper; - private Analyzer analyzer; - private Directory directory; - private IndexWriter writer; + private HttpClient client; + private URI baseUri; @PostConstruct public void init() { - LOGGER.log(Level.INFO, "Instantiating service"); - Path base = Paths.get(config.home()); - LOGGER.log(Level.INFO, "Initializing service with base folder: " + base); + LOGGER.log(Level.INFO, "Initializing Typesense index service"); + client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build(); + baseUri = URI.create(String.format("%s://%s:%d", config.typesense().protocol(), config.typesense().host(), config.typesense().port())); try { - analyzer = new StandardAnalyzer(); - directory = FSDirectory.open(base); - LOGGER.log(Level.FINEST, "directory implementation: " + directory.getClass()); - IndexWriterConfig config = new IndexWriterConfig(analyzer); - writer = new IndexWriter(directory, config); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "unable to configure lucene index writer", e); - } - } - - @PreDestroy - public void shutdown() { - LOGGER.log(Level.INFO, "Shutting down service"); - try { - writer.close(); - directory.close(); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "unable to close lucene index writer", e); + ensureCollection(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Unable to initialize Typesense collection", e); + throw new RuntimeException("Unable to initialize Typesense collection", e); } } @Override @Transactional(Transactional.TxType.SUPPORTS) public void index(IndexableContent object) throws IndexStoreException { - LOGGER.log(Level.INFO, "Indexing new object: " + object.getIdentifier()); + LOGGER.log(Level.INFO, "Indexing object in Typesense: {0}", object.getIdentifier()); try { - Term term = new Term("IDENTIFIER", object.getIdentifier()); - writer.deleteDocuments(term); - writer.addDocument(IndexStoreDocumentBuilder.buildDocument(object)); - writer.commit(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "unable to index object " + object, e); + String payload = mapper.writeValueAsString(IndexStoreDocumentBuilder.buildDocument(object)); + HttpRequest request = baseRequest("/collections/" + encode(config.typesense().collection()) + "/documents?action=upsert") + .POST(HttpRequest.BodyPublishers.ofString(payload)) + .build(); + sendExpectSuccess(request, "upsert document " + object.getIdentifier()); + } catch (Exception e) { throw new IndexStoreException("Can't index an object", e); } } @@ -101,13 +80,17 @@ public void index(IndexableContent object) throws IndexStoreException { @Override @Transactional(Transactional.TxType.SUPPORTS) public void remove(String identifier) throws IndexStoreException { - LOGGER.log(Level.INFO, "Removing document: " + identifier); + LOGGER.log(Level.INFO, "Removing document from Typesense: {0}", identifier); try { - Term term = new Term("IDENTIFIER", identifier); - writer.deleteDocuments(term); - writer.commit(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "unable to remove object " + identifier + " from index", e); + HttpRequest request = baseRequest("/collections/" + encode(config.typesense().collection()) + "/documents/" + encode(identifier)) + .DELETE() + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200 && response.statusCode() != 404) { + throw new IndexStoreException("Can't remove object " + identifier + " from index, status=" + response.statusCode() + " body=" + response.body()); + } + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); throw new IndexStoreException("Can't remove object " + identifier + " from index", e); } } @@ -115,37 +98,111 @@ public void remove(String identifier) throws IndexStoreException { @Override @Transactional(Transactional.TxType.SUPPORTS) public List search(String scope, String queryString) throws IndexStoreException { - LOGGER.log(Level.INFO, "Searching query: " + queryString); + LOGGER.log(Level.INFO, "Searching query in Typesense: {0}", queryString); try { - IndexReader reader = DirectoryReader.open(directory); - IndexSearcher searcher = new IndexSearcher(reader); - QueryParser parser = new QueryParser(IndexStoreDocumentBuilder.CONTENT_FIELD, analyzer); - Query query = parser.parse(queryString); - - TopDocs docs = searcher.search(query, 100); + String path = "/collections/" + encode(config.typesense().collection()) + "/documents/search" + + "?q=" + encode(queryString == null || queryString.isBlank() ? "*" : queryString) + + "&query_by=" + encode(String.join(",", IndexStoreDocumentBuilder.CONTENT_FIELD, IndexStoreDocumentBuilder.NAME_FIELD, IndexStoreDocumentBuilder.MIMETYPE_FIELD)) + + "&highlight_fields=" + encode(String.join(",", IndexStoreDocumentBuilder.CONTENT_FIELD, IndexStoreDocumentBuilder.NAME_FIELD)) + + "&filter_by=" + encode(IndexStoreDocumentBuilder.STORE_ID_FIELD + ":=" + config.typesense().storeId() + " && " + + IndexStoreDocumentBuilder.SCOPE_FIELD + ":=" + scope) + + "&per_page=100"; + HttpRequest request = baseRequest(path).GET().build(); + HttpResponse response = sendExpectSuccess(request, "search query " + queryString); + JsonNode root = mapper.readTree(response.body()); List results = new ArrayList<>(); - SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("", ""); - QueryScorer scorer = new QueryScorer(query); - Highlighter highlighter = new Highlighter(formatter, scorer); - - for (int i = 0; i < docs.scoreDocs.length; i++) { - Document doc = searcher.doc(docs.scoreDocs[i].doc); - float score = docs.scoreDocs[i].score; - String identifier = doc.get(IndexStoreDocumentBuilder.IDENTIFIER_FIELD); - String type = doc.get(IndexStoreDocumentBuilder.TYPE_FIELD); - String highlightedText = highlighter.getBestFragment(analyzer, IndexStoreDocumentBuilder.CONTENT_FIELD, doc.get(IndexStoreDocumentBuilder.CONTENT_FIELD)); + for (JsonNode hit : root.path("hits")) { + JsonNode document = hit.path("document"); IndexStoreResult result = new IndexStoreResult(); - result.setType(type); - result.setScore(score); - result.setIdentifier(identifier); - result.setExplain(highlightedText); + result.setIdentifier(document.path(IndexStoreDocumentBuilder.ID_FIELD).asText()); + result.setType(document.path(IndexStoreDocumentBuilder.TYPE_FIELD).asText()); + result.setScore((float) hit.path("text_match").asDouble(0)); + result.setExplain(extractExplain(hit, document)); results.add(result); } return results; } catch (Exception e) { - LOGGER.log(Level.WARNING, "unable search in index using " + queryString, e); - throw new IndexStoreException("Can't search in index using '" + queryString + "'\n", e); + throw new IndexStoreException("Can't search in index using '" + queryString + "'", e); + } + } + + private void ensureCollection() throws IOException, InterruptedException, IndexStoreException { + String collection = config.typesense().collection(); + HttpRequest get = baseRequest("/collections/" + encode(collection)).GET().build(); + HttpResponse existing = client.send(get, HttpResponse.BodyHandlers.ofString()); + if (existing.statusCode() == 200) { + LOGGER.log(Level.INFO, "Typesense collection already exists: {0}", collection); + return; + } + if (existing.statusCode() != 404) { + throw new IOException("Unable to inspect Typesense collection, status=" + existing.statusCode() + " body=" + existing.body()); } + String payload = """ + { + "name": "%s", + "fields": [ + { "name": "id", "type": "string" }, + { "name": "store_id", "type": "string", "facet": true }, + { "name": "type", "type": "string", "facet": true }, + { "name": "scope", "type": "string", "facet": true }, + { "name": "name", "type": "string", "optional": true }, + { "name": "mimetype", "type": "string", "facet": true, "optional": true }, + { "name": "node_type", "type": "string", "facet": true, "optional": true }, + { "name": "parent", "type": "string", "optional": true }, + { "name": "content", "type": "string" }, + { "name": "modified_at", "type": "int64", "sort": true } + ], + "default_sorting_field": "modified_at" + } + """.formatted(collection); + HttpRequest create = baseRequest("/collections") + .POST(HttpRequest.BodyPublishers.ofString(payload)) + .build(); + sendExpectSuccess(create, "create collection " + collection); + LOGGER.log(Level.INFO, "Created Typesense collection: {0}", collection); } + private String extractExplain(JsonNode hit, JsonNode document) { + for (JsonNode highlight : hit.path("highlights")) { + JsonNode snippet = highlight.path("snippet"); + if (!snippet.isMissingNode() && !snippet.asText().isBlank()) { + return snippet.asText(); + } + for (JsonNode snippets : highlight.path("snippets")) { + if (!snippets.asText().isBlank()) { + return snippets.asText(); + } + } + } + JsonNode legacyHighlight = hit.path("highlight"); + if (legacyHighlight.isObject()) { + for (String field : List.of(IndexStoreDocumentBuilder.CONTENT_FIELD, IndexStoreDocumentBuilder.NAME_FIELD)) { + JsonNode value = legacyHighlight.path(field); + if (!value.isMissingNode() && !value.asText().isBlank()) { + return value.asText(); + } + } + } + String fallback = document.path(IndexStoreDocumentBuilder.CONTENT_FIELD).asText(document.path(IndexStoreDocumentBuilder.NAME_FIELD).asText("")); + return fallback.length() > 240 ? fallback.substring(0, 240) : fallback; + } + + private HttpRequest.Builder baseRequest(String path) { + return HttpRequest.newBuilder(baseUri.resolve(path)) + .timeout(Duration.ofSeconds(5)) + .header("Content-Type", "application/json") + .header("X-TYPESENSE-API-KEY", config.typesense().apiKey()); + } + + private HttpResponse sendExpectSuccess(HttpRequest request, String action) throws IOException, InterruptedException, IndexStoreException { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() / 100 != 2) { + throw new IndexStoreException("Unable to " + action + ", status=" + response.statusCode() + " body=" + response.body()); + } + return response; + } + + private String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } } diff --git a/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexableContent.java b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexableContent.java index 96ad98b..8d2ccbe 100644 --- a/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexableContent.java +++ b/store/src/main/java/fr/jayblanc/mbyte/store/index/IndexableContent.java @@ -21,6 +21,12 @@ public class IndexableContent { private String type; private String identifier; private String content; + private String name; + private String mimetype; + private String nodeType; + private String parent; + private String storeId; + private long modifiedAt; private Scope scope; public IndexableContent() { @@ -50,6 +56,54 @@ public void setContent(String content) { this.content = content; } + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMimetype() { + return mimetype; + } + + public void setMimetype(String mimetype) { + this.mimetype = mimetype; + } + + public String getNodeType() { + return nodeType; + } + + public void setNodeType(String nodeType) { + this.nodeType = nodeType; + } + + public String getParent() { + return parent; + } + + public void setParent(String parent) { + this.parent = parent; + } + + public String getStoreId() { + return storeId; + } + + public void setStoreId(String storeId) { + this.storeId = storeId; + } + + public long getModifiedAt() { + return modifiedAt; + } + + public void setModifiedAt(long modifiedAt) { + this.modifiedAt = modifiedAt; + } + public Scope getScope() { return scope; } diff --git a/store/src/main/java/fr/jayblanc/mbyte/store/search/SearchServiceException.java b/store/src/main/java/fr/jayblanc/mbyte/store/search/SearchServiceException.java index 86d1b13..d385246 100644 --- a/store/src/main/java/fr/jayblanc/mbyte/store/search/SearchServiceException.java +++ b/store/src/main/java/fr/jayblanc/mbyte/store/search/SearchServiceException.java @@ -18,5 +18,6 @@ public class SearchServiceException extends Exception { public SearchServiceException(String message, Throwable e) { + super(message, e); } } diff --git a/store/src/main/resources/application.properties b/store/src/main/resources/application.properties index afe98fd..18c0717 100644 --- a/store/src/main/resources/application.properties +++ b/store/src/main/resources/application.properties @@ -3,7 +3,14 @@ store.root=${HOME}/.mbyte store.auth.owner=sheldon store.data.home=${store.root}/data -store.index.home=${store.root}/index +store.index.backend=typesense +store.index.bootstrap.reindex=true +store.index.typesense.protocol=http +store.index.typesense.host=typesense +store.index.typesense.port=8108 +store.index.typesense.api-key=change-me-typesense-key +store.index.typesense.collection=store_nodes +store.index.typesense.store-id=${store.auth.owner} store.topology.enabled=true store.topology.https=false store.topology.host=consul From 6bebd099a5a8555caaa501798460d07df04cd2d9 Mon Sep 17 00:00:00 2001 From: Rpeter Date: Thu, 19 Mar 2026 14:51:06 +0100 Subject: [PATCH 3/6] feat(search-engine): implement typesense ai --- docker-compose.yml | 23 ++ gui/src/assets/i18n/en.json | 7 +- gui/src/assets/i18n/fr.json | 7 +- gui/src/javascript/App.css | 125 ++++++++ gui/src/javascript/api/ManagerApiProvider.tsx | 1 + gui/src/javascript/api/managerApi.ts | 10 + gui/src/javascript/api/storeApi.ts | 70 +++++ .../components/dashboard/DetailStoreCard.tsx | 38 ++- .../components/store/StoreAiChat.tsx | 153 ++++++++++ gui/src/javascript/pages/DashboardPage.tsx | 4 + gui/src/javascript/pages/StorePage.tsx | 12 + .../manager/api/resources/AppsResource.java | 14 + .../mbyte/manager/core/CoreServiceBean.java | 77 +++++ .../task/store/CreateDockerStoreTask.java | 27 +- .../api/dto/SearchConversationRequest.java | 39 +++ .../store/api/resources/SearchResource.java | 37 +++ .../mbyte/store/files/FileServiceBean.java | 36 ++- .../store/index/IndexStoreBootstrapBean.java | 5 + .../mbyte/store/index/IndexStoreConfig.java | 22 ++ .../index/IndexStoreConversationResult.java | 60 ++++ .../mbyte/store/index/IndexStoreService.java | 7 + .../store/index/IndexStoreServiceBean.java | 284 +++++++++++++++++- .../search/SearchConversationResult.java | 71 +++++ .../mbyte/store/search/SearchService.java | 5 + .../mbyte/store/search/SearchServiceBean.java | 29 ++ .../src/main/resources/application.properties | 11 + 26 files changed, 1152 insertions(+), 22 deletions(-) create mode 100644 gui/src/javascript/components/store/StoreAiChat.tsx create mode 100644 store/src/main/java/fr/jayblanc/mbyte/store/api/dto/SearchConversationRequest.java create mode 100644 store/src/main/java/fr/jayblanc/mbyte/store/index/IndexStoreConversationResult.java create mode 100644 store/src/main/java/fr/jayblanc/mbyte/store/search/SearchConversationResult.java diff --git a/docker-compose.yml b/docker-compose.yml index 1a5349b..86f7b6c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,6 +70,26 @@ services: mbyte.fr: ipv4_address: 172.25.0.9 + vllm: + image: vllm/vllm-openai:latest + hostname: vllm + container_name: mbyte.vllm + command: + - "--model=Qwen/Qwen2.5-1.5B-Instruct" + - "--served-model-name=Qwen2.5-3B-Instruct" + - "--gpu-memory-utilization=0.75" + - "--max-model-len=4096" + - "--host=0.0.0.0" + - "--port=8000" + environment: + - HUGGING_FACE_HUB_TOKEN={$HUGGING_FACE_HUB_TOKEN} + volumes: + - mbyte.vllm.hf:/root/.cache/huggingface + gpus: all + networks: + mbyte.fr: + ipv4_address: 172.25.0.10 + keycloak: build: context: . @@ -119,6 +139,7 @@ services: depends_on: - db - typesense + - vllm - traefik - keycloak group_add: @@ -188,3 +209,5 @@ volumes: device: /var/mbyte/db mbyte.typesense.data: name: mbyte.fr.typesense.data + mbyte.vllm.hf: + name: mbyte.fr.vllm.hf diff --git a/gui/src/assets/i18n/en.json b/gui/src/assets/i18n/en.json index 6b9c18e..5f037a3 100644 --- a/gui/src/assets/i18n/en.json +++ b/gui/src/assets/i18n/en.json @@ -45,10 +45,13 @@ "running": "Running…", "loadingCommands": "Loading commands…", "noCommands": "No available commands", - "availableCommands": "Available commands" + "availableCommands": "Available commands", + "delete": "Delete store", + "deleting": "Deleting…" }, "storeCreated": "Store created", - "storeCreationFailed": "Failed to create store" + "storeCreationFailed": "Failed to create store", + "storeDeleted": "Store deleted" }, "commands": { "START": { diff --git a/gui/src/assets/i18n/fr.json b/gui/src/assets/i18n/fr.json index 4009d9d..706c1e3 100644 --- a/gui/src/assets/i18n/fr.json +++ b/gui/src/assets/i18n/fr.json @@ -45,10 +45,13 @@ "running": "En cours…", "loadingCommands": "Chargement des commandes…", "noCommands": "Aucune commande disponible", - "availableCommands": "Commandes disponibles" + "availableCommands": "Commandes disponibles", + "delete": "Supprimer la boutique", + "deleting": "Suppression…" }, "storeCreated": "Boutique créée", - "storeCreationFailed": "Échec de la création de la boutique" + "storeCreationFailed": "Échec de la création de la boutique", + "storeDeleted": "Boutique supprimée" }, "commands": { "START": { diff --git a/gui/src/javascript/App.css b/gui/src/javascript/App.css index 4331b67..62be3a1 100644 --- a/gui/src/javascript/App.css +++ b/gui/src/javascript/App.css @@ -255,3 +255,128 @@ --cui-btn-bg: rgba(0, 0, 0, 0.06); --cui-btn-border-color: rgba(0, 0, 0, 0.45); } + +.mbyte-ai-chat { + position: fixed; + right: 24px; + bottom: 24px; + z-index: 1200; + width: 60px; + height: 60px; + transition: width 220ms ease, height 220ms ease; +} + +.mbyte-ai-chat--open { + width: min(380px, calc(100vw - 24px)); + height: min(560px, calc(100vh - 32px)); +} + +.mbyte-ai-chat__fab { + width: 60px; + height: 60px; + border: 0; + border-radius: 999px; + background: #23b7e5; + color: #fff; + font-size: 1.5rem; + box-shadow: 0 10px 24px rgba(35, 183, 229, 0.35); +} + +.mbyte-ai-chat__panel { + width: 100%; + height: 100%; + display: grid; + grid-template-rows: auto 1fr auto; + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 18px; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.16); + overflow: hidden; +} + +.mbyte-ai-chat__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); +} + +.mbyte-ai-chat__title { + font-weight: 600; + font-size: 0.95rem; +} + +.mbyte-ai-chat__messages { + padding: 12px; + overflow: auto; + display: flex; + flex-direction: column; + gap: 10px; + background: linear-gradient(180deg, #fbfcff 0%, #f4f7fb 100%); +} + +.mbyte-ai-chat__empty { + color: rgba(0, 0, 0, 0.55); + font-size: 0.875rem; +} + +.mbyte-ai-chat__message { + padding: 10px 12px; + border-radius: 12px; + line-height: 1.4; + white-space: pre-wrap; +} + +.mbyte-ai-chat__message--user { + align-self: flex-end; + background: #23b7e5; + color: #fff; + max-width: 85%; +} + +.mbyte-ai-chat__message--assistant { + align-self: flex-start; + background: #fff; + border: 1px solid rgba(0, 0, 0, 0.08); + max-width: 95%; +} + +.mbyte-ai-chat__typing, +.mbyte-ai-chat__error { + font-size: 0.8rem; + color: rgba(0, 0, 0, 0.6); +} + +.mbyte-ai-chat__error { + color: #b42318; +} + +.mbyte-ai-chat__input { + display: grid; + grid-template-columns: 1fr auto; + gap: 8px; + padding: 10px; + border-top: 1px solid rgba(0, 0, 0, 0.08); +} + +.mbyte-ai-chat__input textarea { + resize: none; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 10px; + padding: 8px 10px; + font-size: 0.9rem; + line-height: 1.35; +} + +@media (max-width: 640px) { + .mbyte-ai-chat { + right: 12px; + bottom: 12px; + } + .mbyte-ai-chat--open { + width: calc(100vw - 24px); + height: min(70vh, 560px); + } +} diff --git a/gui/src/javascript/api/ManagerApiProvider.tsx b/gui/src/javascript/api/ManagerApiProvider.tsx index 5459174..8b1d4f1 100644 --- a/gui/src/javascript/api/ManagerApiProvider.tsx +++ b/gui/src/javascript/api/ManagerApiProvider.tsx @@ -14,6 +14,7 @@ export type ManagerApi = { listApps(owner?: string): Promise getApp(appId: string): Promise createApp(type: string, name: string): Promise + deleteApp(appId: string): Promise getAppProcs(appId: string, active: boolean): Promise listAppCommands(appId: string): Promise runAppCommand(appId: string, commandName: string): Promise diff --git a/gui/src/javascript/api/managerApi.ts b/gui/src/javascript/api/managerApi.ts index 77a404b..83b9a03 100644 --- a/gui/src/javascript/api/managerApi.ts +++ b/gui/src/javascript/api/managerApi.ts @@ -78,6 +78,16 @@ export function createManagerApi(tokenProvider: TokenProvider) { return (await readJsonOrThrow(res)) as Application }, + /** Deletes an application (DELETE /api/apps/{id}). Idempotent on backend. */ + async deleteApp(appId: string): Promise { + const base = requireBaseUrl() + const res = await fetchWithAuth(tokenProvider, `/api/apps/${encodeURIComponent(appId)}`, { method: 'DELETE' }, base) + if (!res.ok && res.status !== 404) { + const text = await res.text() + throw new Error(`Delete failed (${res.status}): ${text}`) + } + }, + /** Creates an application (POST /api/apps). Returns created id. */ async createApp(type: string, name: string): Promise { const base = requireBaseUrl() diff --git a/gui/src/javascript/api/storeApi.ts b/gui/src/javascript/api/storeApi.ts index 1062e43..4f5d5cd 100644 --- a/gui/src/javascript/api/storeApi.ts +++ b/gui/src/javascript/api/storeApi.ts @@ -230,6 +230,76 @@ export function createStoreApi(tokenProvider: TokenProvider, options: CreateStor const arr = (await readJsonOrThrow(res)) as any[] return (arr || []).map((d: any) => SearchResult.fromDto(d)) }, + + async streamConversation( + query: string, + conversationId: string | null, + onChunk: (chunk: string) => void, + onConversationId?: (id: string) => void, + ): Promise { + if (!baseUrl) throw new Error('Store base URL is not configured') + const res = await fetchWithAuth( + tokenProvider, + '/api/search/conversation/stream', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, conversationId }), + }, + baseUrl, + ) + if (!res.ok || !res.body) { + const text = await res.text() + throw new Error(`Conversation stream failed (${res.status}): ${text}`) + } + + const reader = res.body.getReader() + const decoder = new TextDecoder('utf-8') + let buffer = '' + const consumePayload = (payloadRaw: string) => { + if (!payloadRaw || payloadRaw === '[DONE]') return + try { + const payload = JSON.parse(payloadRaw) + const conversation = payload?.conversation + const message = conversation?.message ?? conversation?.answer + if (typeof message === 'string' && message.length > 0) { + onChunk(message) + } + if (typeof conversation?.conversation_id === 'string' && conversation.conversation_id.length > 0) { + onConversationId?.(conversation.conversation_id) + } + } catch { + // Ignore malformed chunks. + } + } + const consumeEvent = (eventText: string) => { + const text = eventText.trim() + if (!text) return + const dataLines = text + .split(/\r?\n/) + .filter((line) => line.startsWith('data:')) + .map((line) => line.slice(5).trim()) + if (dataLines.length) { + consumePayload(dataLines.join('\n')) + return + } + // Typesense may return plain JSON instead of SSE-framed events. + consumePayload(text) + } + + while (true) { + const { value, done } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + const events = buffer.split(/\r?\n\r?\n/) + buffer = events.pop() ?? '' + for (const event of events) { + consumeEvent(event) + } + } + // Parse remaining buffer for non-SSE single-payload responses. + consumeEvent(buffer) + }, } } diff --git a/gui/src/javascript/components/dashboard/DetailStoreCard.tsx b/gui/src/javascript/components/dashboard/DetailStoreCard.tsx index 92bfb2f..43a2209 100644 --- a/gui/src/javascript/components/dashboard/DetailStoreCard.tsx +++ b/gui/src/javascript/components/dashboard/DetailStoreCard.tsx @@ -12,9 +12,10 @@ import { useManagerApi } from '../../api/ManagerApiProvider' export type DetailStoreCardProps = Readonly<{ app: Application onRefresh: () => void + onDeleted?: () => void }> -export function DetailStoreCard({ app, onRefresh }: DetailStoreCardProps) { +export function DetailStoreCard({ app, onRefresh, onDeleted }: DetailStoreCardProps) { const { t } = useTranslation() const commandProcessing = useAppCommandProcessing(app.id) const managerApi = useManagerApi() @@ -22,6 +23,8 @@ export function DetailStoreCard({ app, onRefresh }: DetailStoreCardProps) { const [commands, setCommands] = useState(null) const [commandsError, setCommandsError] = useState(null) const [commandsLoading, setCommandsLoading] = useState(false) + const [deleteBusy, setDeleteBusy] = useState(false) + const [deleteError, setDeleteError] = useState(null) useEffect(() => { let cancelled = false @@ -81,6 +84,20 @@ export function DetailStoreCard({ app, onRefresh }: DetailStoreCardProps) { void commandProcessing.runCommand(app.id, commandName) } + const handleDeleteStore = async () => { + try { + setDeleteBusy(true) + setDeleteError(null) + await managerApi.deleteApp(app.id) + onDeleted?.() + } catch (err) { + console.error('Failed to delete store:', err) + setDeleteError(err instanceof Error ? err.message : String(err)) + } finally { + setDeleteBusy(false) + } + } + return ( @@ -92,6 +109,7 @@ export function DetailStoreCard({ app, onRefresh }: DetailStoreCardProps) { color="light" size="sm" onClick={onRefresh} + disabled={deleteBusy} title={t('dashboard.storeDetail.refresh')} > @@ -135,6 +153,12 @@ export function DetailStoreCard({ app, onRefresh }: DetailStoreCardProps) {
)} + {deleteError && ( +
+ {deleteError} +
+ )} +
{commandsLoading && (
@@ -152,11 +176,11 @@ export function DetailStoreCard({ app, onRefresh }: DetailStoreCardProps) { )} {commands?.length ? ( -
+
{commands.map((cmd) => { const appStatus = app.status ?? '' const allowedForStatus = !cmd.appStatus || cmd.appStatus.includes(appStatus) - const disabled = commandProcessing.phase !== 'idle' || !allowedForStatus + const disabled = deleteBusy || commandProcessing.phase !== 'idle' || !allowedForStatus const btnStyle = allowedForStatus ? undefined : { opacity: 0.5 } return ( cmd.description ? ( @@ -190,6 +214,14 @@ export function DetailStoreCard({ app, onRefresh }: DetailStoreCardProps) { ) ) })} + + {deleteBusy ? t('dashboard.storeDetail.deleting') : t('dashboard.storeDetail.delete')} +
) : null}
diff --git a/gui/src/javascript/components/store/StoreAiChat.tsx b/gui/src/javascript/components/store/StoreAiChat.tsx new file mode 100644 index 0000000..9808e8f --- /dev/null +++ b/gui/src/javascript/components/store/StoreAiChat.tsx @@ -0,0 +1,153 @@ +import { useEffect, useRef, useState } from 'react' +import type { FormEvent, KeyboardEvent } from 'react' +import { CButton } from '@coreui/react' + +type Message = { + id: string + role: 'user' | 'assistant' + content: string +} + +type StoreAiChatProps = Readonly<{ + streamConversation: ( + query: string, + conversationId: string | null, + onChunk: (chunk: string) => void, + onConversationId?: (id: string) => void, + ) => Promise +}> + +const newId = () => `${Date.now()}-${Math.random().toString(16).slice(2)}` + +export function StoreAiChat({ streamConversation }: StoreAiChatProps) { + const [open, setOpen] = useState(false) + const [prompt, setPrompt] = useState('') + const [conversationId, setConversationId] = useState(null) + const [isStreaming, setIsStreaming] = useState(false) + const [messages, setMessages] = useState([]) + const [error, setError] = useState(null) + const bottomRef = useRef(null) + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages, open, isStreaming]) + + const appendAssistantChunk = (assistantId: string, chunk: string) => { + setMessages((prev) => + prev.map((msg) => + msg.id === assistantId + ? { ...msg, content: `${msg.content}${chunk}` } + : msg, + ), + ) + } + + const handleSubmit = async (event?: FormEvent) => { + event?.preventDefault() + const query = prompt.trim() + if (!query || isStreaming) return + + const userMessage: Message = { id: newId(), role: 'user', content: query } + const assistantMessage: Message = { id: newId(), role: 'assistant', content: '' } + setPrompt('') + setError(null) + setMessages((prev) => [...prev, userMessage, assistantMessage]) + setIsStreaming(true) + + try { + await streamConversation( + query, + conversationId, + (chunk) => appendAssistantChunk(assistantMessage.id, chunk), + (id) => setConversationId(id), + ) + setMessages((prev) => + prev.map((msg) => + msg.id === assistantMessage.id && !msg.content.trim() + ? { ...msg, content: 'No answer generated.' } + : msg, + ), + ) + } catch (e) { + setError((e as Error).message || 'Streaming failed') + setMessages((prev) => + prev.map((msg) => + msg.id === assistantMessage.id && !msg.content.trim() + ? { ...msg, content: 'I could not generate an answer.' } + : msg, + ), + ) + } finally { + setIsStreaming(false) + } + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault() + void handleSubmit() + } + } + + const handleReset = () => { + setMessages([]) + setConversationId(null) + setError(null) + } + + return ( +
+ {!open ? ( + + ) : ( +
+
+
🤖 AI Search
+
+ Reset + setOpen(false)} disabled={isStreaming}>✕ +
+
+ +
+ {!messages.length && ( +
Ask questions about your uploaded files.
+ )} + {messages.map((message) => ( +
+ {message.content} +
+ ))} + {isStreaming &&
Thinking…
} + {error &&
{error}
} +
+
+ +
+