From bb48f9e5cfca028eb555b58b0987c7516a54d84f Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:25:46 +0100 Subject: [PATCH 01/36] introduce tags functionality to KB search --- .gitignore | 1 + docusaurus.config.js | 1 + package.json | 5 +- plugins/custom-loaders/index.js | 28 + plugins/custom-loaders/package.json | 5 + .../KBArticleSearch/KBArticleSearch.js | 59 +- src/hooks/fetchKnowledgebaseArticles.js | 27 + src/theme/BlogSidebar/Desktop/index.js | 49 +- static/kb_toc.json | 1277 +++++++++++++++++ 9 files changed, 1441 insertions(+), 11 deletions(-) create mode 100644 plugins/custom-loaders/index.js create mode 100644 plugins/custom-loaders/package.json create mode 100644 src/hooks/fetchKnowledgebaseArticles.js create mode 100644 static/kb_toc.json diff --git a/.gitignore b/.gitignore index 588d210235f..8833ba509e7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ clickhouse-docs.code-workspace yarn.lock yarn.error-log .comments +yarn-error.log # Output files used by scripts to verify links sidebar_links.txt diff --git a/docusaurus.config.js b/docusaurus.config.js index 4a563897802..fdfce20de5a 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -311,6 +311,7 @@ const config = { }), plugins: [ + 'custom-loaders', 'docusaurus-plugin-sass', function (context, options) { return { diff --git a/package.json b/package.json index a41824371a6..647a85cf9a6 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,10 @@ "@docusaurus/theme-mermaid": "3.7.0", "@mdx-js/react": "^3.1.0", "@radix-ui/react-navigation-menu": "^1.2.3", + "@yaireo/tagify": "^4.33.2", "axios": "^1.7.9", "clsx": "^2.1.0", + "custom-loaders": "file:plugins/custom-loaders", "docusaurus-plugin-sass": "^0.2.6", "esbuild": "^0.20.1", "esbuild-loader": "^4.0.3", @@ -45,7 +47,8 @@ "remark-docusaurus-tabs": "^0.2.0", "remark-link-rewrite": "^1.0.7", "remark-math": "^6.0.0", - "sass": "^1.83.1" + "sass": "^1.83.1", + "custom-loaders": "file:plugins/custom-loaders" }, "devDependencies": { "@argos-ci/cli": "^2.5.3", diff --git a/plugins/custom-loaders/index.js b/plugins/custom-loaders/index.js new file mode 100644 index 00000000000..4949c65e8e7 --- /dev/null +++ b/plugins/custom-loaders/index.js @@ -0,0 +1,28 @@ +module.exports = function (context, options) { + return { + name: 'custom-loaders', + configureWebpack(config, isServer) { + return { + module: { + rules: [ + { + test: /\.jsx$/, + // Exclude node_modules to avoid conflicts + exclude: /node_modules\/(?!(@yaireo\/tagify)\/)/, + use: { + loader: 'babel-loader', + options: { + "presets": [ + ["@babel/preset-react", { + "runtime": "automatic" + }] + ] + }, + }, + }, + ], + }, + }; + }, + }; +}; \ No newline at end of file diff --git a/plugins/custom-loaders/package.json b/plugins/custom-loaders/package.json new file mode 100644 index 00000000000..1f9b4954bfc --- /dev/null +++ b/plugins/custom-loaders/package.json @@ -0,0 +1,5 @@ +{ + "name": "custom-loaders", + "version": "0.0.0", + "private": true +} \ No newline at end of file diff --git a/src/components/KBArticleSearch/KBArticleSearch.js b/src/components/KBArticleSearch/KBArticleSearch.js index 2892b83933a..6d33ce1aaaf 100644 --- a/src/components/KBArticleSearch/KBArticleSearch.js +++ b/src/components/KBArticleSearch/KBArticleSearch.js @@ -1,17 +1,55 @@ import React from 'react'; import styles from './styles.module.css' import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import {useState, useEffect} from "react"; +import {useState, useCallback, useEffect} from "react"; +import Tags, {MixedTags} from '@yaireo/tagify/react' // React-wrapper file +import '@yaireo/tagify/dist/tagify.css' +import {tags} from "../../../static/js/docsearch"; // Tagify CSS +import {useBlogPost} from "@docusaurus/theme-common"; -const KBArticleSearch = ({kb_articles, onUpdateResults}) => { +const KBArticleSearch = ({kb_articles, onUpdateResults, allowed_tags, kb_articles_and_tags}) => { const [searchTerm, setSearchTerm] = useState(''); + const [searchTags, setSearchTags] = useState([]); + + // Settings for Tagify + const settings = { + maxTags: 3, + dropdown: { + enabled: 0, + position: "input" + }, + whitelist: allowed_tags.map((value, index) => ({ + id: index+1, + value: value + })) + }; + + const articleTagsFromTitle = (title, kb_articles_and_tags) => { + const match = kb_articles_and_tags.find(article => article.title === title) + return match ? match.tags : null; + } + const handleSearch = (event) => { setSearchTerm(event.target.value); }; + const handleTags = (event) => { + let tags_raw = [] + let tags_cleaned = [] + if (event.detail.value !== '') + { + tags_raw = JSON.parse(event.detail.value) + tags_cleaned = tags_raw.map((tag)=>tag.value) + } + setSearchTags(tags_cleaned) + } + const filteredArticles = searchTerm ? kb_articles.filter((article) => - searchTerm && article.title.match(new RegExp(searchTerm, 'i')) // Case-insensitive search + { + const tags = articleTagsFromTitle(article.title, kb_articles_and_tags) + return searchTerm && article.title.match(new RegExp(searchTerm, 'i')) + } // Case-insensitive search ) : kb_articles; useEffect(() => { @@ -19,12 +57,13 @@ const KBArticleSearch = ({kb_articles, onUpdateResults}) => { }, [filteredArticles, onUpdateResults]); // Update on filter changes return ( +
+ stroke="currentColor" fill="none" fillRule="evenodd" strokeLinecap="round" + strokeLinejoin="round"> { className={styles.KBArticleInputSearchArea} />
- ) + +
+) } export default KBArticleSearch; \ No newline at end of file diff --git a/src/hooks/fetchKnowledgebaseArticles.js b/src/hooks/fetchKnowledgebaseArticles.js new file mode 100644 index 00000000000..525b54c65cc --- /dev/null +++ b/src/hooks/fetchKnowledgebaseArticles.js @@ -0,0 +1,27 @@ +import { useState, useEffect } from 'react'; + +export const useFetchKnowledgebaseArticles = (filepath) => { + + const [articlesWithTags, setArticles] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchArticles = async () => { + try { + const response = await fetch(filepath); + const jsonData = await response.json(); + setArticles(jsonData); + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + } + fetchArticles(); + }, [filepath]) // re-fetch the articles only if the URL changes (intended to be used once) + + return {articlesWithTags, isLoading, error} +} + +export default useFetchKnowledgebaseArticles; \ No newline at end of file diff --git a/src/theme/BlogSidebar/Desktop/index.js b/src/theme/BlogSidebar/Desktop/index.js index 375aa37ea9f..7da01c678ff 100644 --- a/src/theme/BlogSidebar/Desktop/index.js +++ b/src/theme/BlogSidebar/Desktop/index.js @@ -7,15 +7,51 @@ import SearchBar from "../../SearchBar"; import KBArticleSearch from "../../../components/KBArticleSearch/KBArticleSearch"; import {DocSearchButton} from "@docsearch/react"; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import {useState} from "react"; +import {useCallback, useRef, useState} from "react"; +import {useFetchKnowledgebaseArticles} from "../../../hooks/fetchKnowledgebaseArticles"; +const allowed_tags = [ + 'Concepts', + 'Migrations', + 'Use Cases', + 'Best Practices', + 'Managing Cloud', + 'Security and Authentication', + 'Cloud Migration', + 'Core Data Concepts', + 'Managing Data', + 'Updating Data', + 'Data Modelling', + 'Deleting Data', + 'Performance and Optimizations', + 'Server Admin', + 'Deployments and Scaling', + 'Settings', + 'Tools and Utilities', + 'System Tables', + 'Functions', + 'Engines', + 'Language Clients', + 'ClickPipes', + 'Native Clients and Interfaces', + 'Data Sources', + 'Data Visualization', + 'Data Formats', + 'Data Ingestion', + 'Data Export', + 'chDB', + 'Errors and Exceptions', + 'Community', +] export default function BlogSidebarDesktop({sidebar}) { - const { siteConfig } = useDocusaurusContext(); + const { siteConfig } = useDocusaurusContext(); + const { articlesWithTags, isLoading, error} = useFetchKnowledgebaseArticles('./kb_toc.json'); // stored in /static const [filteredArticles, setFilteredArticles] = useState(sidebar.items); const updateResults = (filteredArticlesFromSearch) => { setFilteredArticles(filteredArticlesFromSearch); } + return ( From f8342497a788afce893d911d592cdcdf7398e4f9 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:46:04 +0100 Subject: [PATCH 03/36] first pass at styling --- src/css/custom.scss | 36 ++++++------- .../BlogListPage/StructuredData/index.js | 13 +++++ src/theme/BlogListPage/index.js | 52 +++++++++++++++++++ src/theme/BlogListPage/styles.module.css | 4 ++ src/theme/BlogSidebar/Desktop/index.js | 4 +- .../BlogSidebar/Desktop/styles.module.css | 6 +++ 6 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 src/theme/BlogListPage/StructuredData/index.js create mode 100644 src/theme/BlogListPage/index.js create mode 100644 src/theme/BlogListPage/styles.module.css diff --git a/src/css/custom.scss b/src/css/custom.scss index 19d25261461..66b90cf15dc 100644 --- a/src/css/custom.scss +++ b/src/css/custom.scss @@ -1142,30 +1142,30 @@ nav[aria-label='Docs sidebar']:hover { } .title_xvU1 { - font-size: 2rem; + font-size: 1.875rem; } +} - // Override tagify plugin styling +// Override tagify plugin styling - .tagify { - display: flex; - width: 98%; - margin-bottom: 1rem; - border: 1px solid var(--click-color-stroke); - border-radius: 4px; - } +.tagify { + display: flex; + width: 98%; + margin-bottom: 1rem; + border: 1px solid var(--click-color-stroke) !important; + border-radius: 4px; +} - .tagify__input { - width: 100%; - } +.tagify__input { + width: 100%; +} - span.tagify__input::before { - color: var(--docsearch-muted-color) !important; - } +span.tagify__input::before { + color: var(--docsearch-muted-color) !important; +} - span.tagify__input::after { - color: var(--docsearch-muted-color) !important; - } +span.tagify__input::after { + color: var(--docsearch-muted-color) !important; } [data-theme='dark'] .tagify { diff --git a/src/theme/BlogListPage/StructuredData/index.js b/src/theme/BlogListPage/StructuredData/index.js new file mode 100644 index 00000000000..5651b263d7b --- /dev/null +++ b/src/theme/BlogListPage/StructuredData/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import Head from '@docusaurus/Head'; +import {useBlogListPageStructuredData} from '@docusaurus/plugin-content-blog/client'; +export default function BlogListPageStructuredData(props) { + const structuredData = useBlogListPageStructuredData(props); + return ( + + + + ); +} diff --git a/src/theme/BlogListPage/index.js b/src/theme/BlogListPage/index.js new file mode 100644 index 00000000000..130cb4d9436 --- /dev/null +++ b/src/theme/BlogListPage/index.js @@ -0,0 +1,52 @@ +import React from 'react'; +import clsx from 'clsx'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import { + PageMetadata, + HtmlClassNameProvider, + ThemeClassNames, +} from '@docusaurus/theme-common'; +import BlogLayout from '@theme/BlogLayout'; +import BlogListPaginator from '@theme/BlogListPaginator'; +import SearchMetadata from '@theme/SearchMetadata'; +import BlogPostItems from '@theme/BlogPostItems'; +import BlogListPageStructuredData from '@theme/BlogListPage/StructuredData'; +import styles from './styles.module.css'; +function BlogListPageMetadata(props) { + const {metadata} = props; + const { + siteConfig: {title: siteTitle}, + } = useDocusaurusContext(); + const {blogDescription, blogTitle, permalink} = metadata; + const isBlogOnlyMode = permalink === '/'; + const title = isBlogOnlyMode ? siteTitle : blogTitle; + return ( + <> + + + + ); +} +function BlogListPageContent(props) { + const {metadata, items, sidebar} = props; + return ( + +

Recently Added

+ + +
+ ); +} +export default function BlogListPage(props) { + return ( + + + + + + ); +} diff --git a/src/theme/BlogListPage/styles.module.css b/src/theme/BlogListPage/styles.module.css new file mode 100644 index 00000000000..b8cf2b5568f --- /dev/null +++ b/src/theme/BlogListPage/styles.module.css @@ -0,0 +1,4 @@ +.mostRecent { + margin-top: 2rem; + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/src/theme/BlogSidebar/Desktop/index.js b/src/theme/BlogSidebar/Desktop/index.js index 149668a02df..e015ad1d7b1 100644 --- a/src/theme/BlogSidebar/Desktop/index.js +++ b/src/theme/BlogSidebar/Desktop/index.js @@ -78,7 +78,7 @@ export default function BlogSidebarDesktop({sidebar}) { description: 'The ARIA label for recent posts in the blog sidebar', })}> diff --git a/src/theme/BlogSidebar/Desktop/styles.module.css b/src/theme/BlogSidebar/Desktop/styles.module.css index 212bdc1a828..ef751411fc1 100644 --- a/src/theme/BlogSidebar/Desktop/styles.module.css +++ b/src/theme/BlogSidebar/Desktop/styles.module.css @@ -23,6 +23,12 @@ display: block; } +.sidebarItemNoResult{ + color: var(--ifm-font-color-base); + display: block; + margin-top: 1rem; +} + .sidebarItemLink:hover { text-decoration: none; } From 370fec065fd080b9dede04015139e8b721364336 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:00:20 +0100 Subject: [PATCH 04/36] change to mixed-mode search --- .../KBArticleSearch/KBArticleSearch.js | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/components/KBArticleSearch/KBArticleSearch.js b/src/components/KBArticleSearch/KBArticleSearch.js index 83916079c70..94a294f237e 100644 --- a/src/components/KBArticleSearch/KBArticleSearch.js +++ b/src/components/KBArticleSearch/KBArticleSearch.js @@ -1,20 +1,22 @@ import React from 'react'; import styles from './styles.module.css' -import {useState, useEffect} from "react"; -import Tags from '@yaireo/tagify/react' // React-wrapper file +import {useState, useEffect, useCallback} from "react"; +import {MixedTags} from '@yaireo/tagify/react' // React-wrapper file import '@yaireo/tagify/dist/tagify.css' const KBArticleSearch = ({kb_articles, onUpdateResults, allowed_tags, kb_articles_and_tags}) => { - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, setSearchTerm] = useState(null); const [searchTags, setSearchTags] = useState([]); // Settings for Tagify const settings = { + mode: 'mix', + pattern: /@|#/, maxTags: 3, dropdown: { enabled: 0, - position: "input", + position: "text", maxItems: allowed_tags.length, }, whitelist: allowed_tags.sort().map((value, index) => ({ @@ -64,6 +66,26 @@ const KBArticleSearch = ({kb_articles, onUpdateResults, allowed_tags, kb_article setSearchTags(tags_cleaned) } + const handleMixed = useCallback( (e) => { + console.log(e.detail.value) + const regex_tags = /\[\[\{.*?"value":"(.*?)".*?\}\]\]/g; + const tag_matches = e.detail.value.matchAll(regex_tags); + const tags = Array.from(tag_matches, match => match[1]); + setSearchTags(tags) + + const regex_text = /\[\[.*?\]\]\s?/g; + const text = e.detail.value.replace(regex_text, '') + if (text.length === 0 || text === "@ " || text === "# ") { // Tagify adds a space after the tag is added, which we don't want + console.log("set searchTerm to null") + setSearchTerm(null) + } + else + setSearchTerm(text) + + console.log(searchTags) + console.log(searchTerm) + }, []) + // Helper function to sort by tags const sortByTags = (matching_article_titles, kb_articles) => { @@ -75,20 +97,22 @@ const KBArticleSearch = ({kb_articles, onUpdateResults, allowed_tags, kb_article { const regex = new RegExp(searchTerm, 'i'); // return all articles if there is no search term, or we aren't filtering by tag - if (searchTags.length === 0 && searchTerm.length === 0) { + if (searchTags.length === 0 && searchTerm === null) { console.log("Returning KB articles") return kb_articles; // sort only by tag if we filter by tags but there is no search term - } else if (searchTags.length >= 1 && searchTerm.length === 0) { + } else if (searchTags.length >= 1 && searchTerm === null) { console.log("Sorting only by tags") const matching_article_titles = articlesTitlesFromTags(searchTags, kb_articles_and_tags); return sortByTags(matching_article_titles, kb_articles); // sort only by searchTerm if there are no tags set - } else if (searchTags.length === 0 && searchTerm.length >= 1) { + } else if (searchTags.length === 0 && searchTerm) { console.log("Sorting only by search term") return kb_articles.filter((article)=>article.title.match(regex)) // sort by tags first, then by search term - } else if (searchTags.length >= 1 && searchTerm.length >= 1) { + } else if (searchTags.length >= 1 && searchTerm) { + console.log("tags", searchTags) + console.log("searchTerm",searchTerm===null) console.log("Sorting by both tags and search term") const matching_article_titles = articlesTitlesFromTags(searchTags, kb_articles_and_tags); const sorted_by_tag = sortByTags(matching_article_titles, kb_articles); @@ -111,17 +135,15 @@ const KBArticleSearch = ({kb_articles, onUpdateResults, allowed_tags, kb_article - ) From e62d4c09a410e9457af4ec9edab0fb4711ce655a Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Tue, 28 Jan 2025 23:15:47 +0100 Subject: [PATCH 05/36] Search by title, description --- docusaurus.config.js | 1 - package.json | 8 +- plugins/custom-loaders/index.js | 28 - plugins/custom-loaders/package.json | 5 - scripts/autogenerate-table-of-contents.sh | 22 + .../table-of-contents-generator/toc_gen.py | 2 +- .../KBArticleSearch/KBArticleSearch.js | 163 +-- src/theme/BlogSidebar/Desktop/index.js | 40 +- static/knowledgebase_toc.json | 1264 +++++++++++++++++ 9 files changed, 1342 insertions(+), 191 deletions(-) delete mode 100644 plugins/custom-loaders/index.js delete mode 100644 plugins/custom-loaders/package.json create mode 100644 scripts/autogenerate-table-of-contents.sh create mode 100644 static/knowledgebase_toc.json diff --git a/docusaurus.config.js b/docusaurus.config.js index 2c1280bd936..ff66eab5b12 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -311,7 +311,6 @@ const config = { }), plugins: [ - 'custom-loaders', 'docusaurus-plugin-sass', function (context, options) { return { diff --git a/package.json b/package.json index efe91fd3477..2f0d58a0812 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "build-api-doc": "node clickhouseapi.js", "build-swagger": "npx @redocly/cli build-docs https://api.clickhouse.cloud/v1 --output build/en/cloud/manage/api/swagger.html", "auto-generate-settings": "bash ./scripts/settings/autogenerate-settings.sh", - "new-build": "yarn copy-clickhouse-repo-docs && yarn auto-generate-settings && yarn build-api-doc && yarn build && yarn build-swagger", + "auto-generate-table-of-contents": "bash ./scripts/autogenerate-table-of-contents.sh", + "new-build": "yarn copy-clickhouse-repo-docs && yarn auto-generate-settings && yarn auto-generate-table-of-contents && yarn build-api-doc && yarn build && yarn build-swagger", "start": "docusaurus start", "swizzle": "docusaurus swizzle", "write-heading-ids": "docusaurus write-heading-ids", @@ -35,10 +36,10 @@ "@yaireo/tagify": "^4.33.2", "axios": "^1.7.9", "clsx": "^2.1.0", - "custom-loaders": "file:plugins/custom-loaders", "docusaurus-plugin-sass": "^0.2.6", "esbuild": "^0.20.1", "esbuild-loader": "^4.0.3", + "flexsearch": "^0.7.43", "gray-matter": "^4.0.3", "hast-util-is-element": "1.1.0", "http-proxy-middleware": "2.0.7", @@ -51,8 +52,7 @@ "remark-docusaurus-tabs": "^0.2.0", "remark-link-rewrite": "^1.0.7", "remark-math": "^6.0.0", - "sass": "^1.83.1", - "custom-loaders": "file:plugins/custom-loaders" + "sass": "^1.83.1" }, "devDependencies": { "@argos-ci/cli": "^2.5.3", diff --git a/plugins/custom-loaders/index.js b/plugins/custom-loaders/index.js deleted file mode 100644 index 4949c65e8e7..00000000000 --- a/plugins/custom-loaders/index.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = function (context, options) { - return { - name: 'custom-loaders', - configureWebpack(config, isServer) { - return { - module: { - rules: [ - { - test: /\.jsx$/, - // Exclude node_modules to avoid conflicts - exclude: /node_modules\/(?!(@yaireo\/tagify)\/)/, - use: { - loader: 'babel-loader', - options: { - "presets": [ - ["@babel/preset-react", { - "runtime": "automatic" - }] - ] - }, - }, - }, - ], - }, - }; - }, - }; -}; \ No newline at end of file diff --git a/plugins/custom-loaders/package.json b/plugins/custom-loaders/package.json deleted file mode 100644 index 1f9b4954bfc..00000000000 --- a/plugins/custom-loaders/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "custom-loaders", - "version": "0.0.0", - "private": true -} \ No newline at end of file diff --git a/scripts/autogenerate-table-of-contents.sh b/scripts/autogenerate-table-of-contents.sh new file mode 100644 index 00000000000..c09773aed70 --- /dev/null +++ b/scripts/autogenerate-table-of-contents.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# This script is used for automatically generating any .json table of contents files +# used for various things on the docs site, such as: + # - landing pages + # - indexing of the knowledgebase + +# check if virtual environment exists +if [ ! -d "venv" ]; then + echo "Creating virtual environment..." + python3 -m venv venv +fi + +source venv/bin/activate +pip install -r scripts/table-of-contents-generator/requirements.txt + +# Add runs of the script below for any table of contents files that need to be generated +# You can run toc_gen.py --help for descriptions of the parameters +python3 scripts/table-of-contents-generator/toc_gen.py --dir="knowledgebase" --single-toc --out="static" --ignore images + +deactivate +rm -r venv \ No newline at end of file diff --git a/scripts/table-of-contents-generator/toc_gen.py b/scripts/table-of-contents-generator/toc_gen.py index 878e5fc026f..b0aebd88970 100755 --- a/scripts/table-of-contents-generator/toc_gen.py +++ b/scripts/table-of-contents-generator/toc_gen.py @@ -90,7 +90,7 @@ def write_to_file(json_items, directory, output=None): except OSError as e: if e.errno == 21: print(f"Directory already exists: {e}") - else: + elif e.errno != 17: print(f"An error occurred creating directory: {e}") def write_file(json_items, args, directory): print(args) diff --git a/src/components/KBArticleSearch/KBArticleSearch.js b/src/components/KBArticleSearch/KBArticleSearch.js index 94a294f237e..3b30b80bf8f 100644 --- a/src/components/KBArticleSearch/KBArticleSearch.js +++ b/src/components/KBArticleSearch/KBArticleSearch.js @@ -3,126 +3,66 @@ import styles from './styles.module.css' import {useState, useEffect, useCallback} from "react"; import {MixedTags} from '@yaireo/tagify/react' // React-wrapper file import '@yaireo/tagify/dist/tagify.css' - -const KBArticleSearch = ({kb_articles, onUpdateResults, allowed_tags, kb_articles_and_tags}) => { - - const [searchTerm, setSearchTerm] = useState(null); - const [searchTags, setSearchTags] = useState([]); - - // Settings for Tagify - const settings = { - mode: 'mix', - pattern: /@|#/, - maxTags: 3, - dropdown: { - enabled: 0, - position: "text", - maxItems: allowed_tags.length, - }, - whitelist: allowed_tags.sort().map((value, index) => ({ - id: index+1, - value: value - })) - }; - - // Helper function to return an array of article tags given the article title - const articleTagsFromTitle = (title, kb_articles_and_tags) => { - const match = kb_articles_and_tags.find(article => article.title === title) - return match ? match.tags : null; - } - - // Helper function to return matches between a list of objects with tags property and an array of tags - const filterObjectsByTags = (articles, tags) => { - return articles.filter(article => { - if (!article.tags) { - return false; +import FlexSearch from 'flexsearch' +import kb_articles_and_tags from '@site/static/knowledgebase_toc.json'; +const KBArticleSearch = ({kb_articles, onUpdateResults}) => { + + const [searchTerm, setSearchTerm] = useState(""); + const [searchResults, setSearchResults] = useState([]); + const [matchedArticles, setMatchedArticles] = useState(kb_articles_and_tags); + + // Add id property to kb_articles_and_tags to be used for indexing + kb_articles_and_tags.forEach((object, index)=> { + object.id = index; + }) + + // These don't have a permalink so we need to add it first + const titleToPermalinkMap = new Map(kb_articles.map(item => [item.title, item.permalink])); + kb_articles_and_tags.forEach((article)=>{ + const permalink = titleToPermalinkMap.get(article.title); + if (permalink) + article.permalink = permalink + }) + + const index = new FlexSearch.Document({ + document: { + id: "id", + tag: "tags", + index: ["title", "description"] } - return tags.some(tag => article.tags.includes(tag)); - }); - - } - // Helper function to return a list of articles given some tags - const articlesTitlesFromTags = (searchTags, kb_articles_and_tags) => { - const matches = filterObjectsByTags(kb_articles_and_tags, searchTags) - if (matches.length > 1) - return matches.map((match)=>match.title); - return [] - } + } + ); + kb_articles_and_tags.forEach((article)=>{ + index.add(article.id, article); + }) // handler function called on onKeyUp events in the text search bar const handleSearch = (event) => { + console.log(event.target.value) setSearchTerm(event.target.value); + const results = index.search(event.target.value, {index: ["title", "description"]}); + setSearchResults(results); }; - // handler function called on onChange events in the tags bar - const handleTags = (event) => { - let tags_raw = [] - let tags_cleaned = [] - if (event.detail.tagify.getCleanValue() !== undefined) - { - tags_raw = event.detail.tagify.getCleanValue() - tags_cleaned = tags_raw.map((tag)=>tag.value) + const convert_indexes_to_articles = () => { + console.log("SearchTerm:", searchTerm) + if (searchTerm.length === 0 || /\s+/.test(searchTerm)) + setMatchedArticles(kb_articles_and_tags) // return all if search term is empty or consists of spaces + else { + const indices = searchResults.flatMap(search_field_results => search_field_results.result); + const unique_indices = [...new Set(indices)]; + setMatchedArticles(kb_articles_and_tags.filter((article)=>{ + return unique_indices.includes(article.id) + })); } - setSearchTags(tags_cleaned) + console.log(searchResults) } - const handleMixed = useCallback( (e) => { - console.log(e.detail.value) - const regex_tags = /\[\[\{.*?"value":"(.*?)".*?\}\]\]/g; - const tag_matches = e.detail.value.matchAll(regex_tags); - const tags = Array.from(tag_matches, match => match[1]); - setSearchTags(tags) - - const regex_text = /\[\[.*?\]\]\s?/g; - const text = e.detail.value.replace(regex_text, '') - if (text.length === 0 || text === "@ " || text === "# ") { // Tagify adds a space after the tag is added, which we don't want - console.log("set searchTerm to null") - setSearchTerm(null) - } - else - setSearchTerm(text) - - console.log(searchTags) - console.log(searchTerm) - }, []) - - // Helper function to sort by tags - const sortByTags = (matching_article_titles, kb_articles) => - { - return kb_articles.filter((article)=> matching_article_titles.includes(article.title)); - } - - // filter articles based on the provided search term and tags - const filteredArticles = () => - { - const regex = new RegExp(searchTerm, 'i'); - // return all articles if there is no search term, or we aren't filtering by tag - if (searchTags.length === 0 && searchTerm === null) { - console.log("Returning KB articles") - return kb_articles; - // sort only by tag if we filter by tags but there is no search term - } else if (searchTags.length >= 1 && searchTerm === null) { - console.log("Sorting only by tags") - const matching_article_titles = articlesTitlesFromTags(searchTags, kb_articles_and_tags); - return sortByTags(matching_article_titles, kb_articles); - // sort only by searchTerm if there are no tags set - } else if (searchTags.length === 0 && searchTerm) { - console.log("Sorting only by search term") - return kb_articles.filter((article)=>article.title.match(regex)) - // sort by tags first, then by search term - } else if (searchTags.length >= 1 && searchTerm) { - console.log("tags", searchTags) - console.log("searchTerm",searchTerm===null) - console.log("Sorting by both tags and search term") - const matching_article_titles = articlesTitlesFromTags(searchTags, kb_articles_and_tags); - const sorted_by_tag = sortByTags(matching_article_titles, kb_articles); - return sorted_by_tag.filter((article)=>article.title.match(regex)) - } - }; + useEffect(convert_indexes_to_articles, [searchResults]); useEffect(() => { - onUpdateResults(filteredArticles); // Call callback with filtered articles - }, [searchTerm, searchTags]); // Update on filter changes + onUpdateResults(matchedArticles); // Call callback with filtered articles + }, [matchedArticles]); // Update on filter changes return (
@@ -135,16 +75,11 @@ const KBArticleSearch = ({kb_articles, onUpdateResults, allowed_tags, kb_article -
) } diff --git a/src/theme/BlogSidebar/Desktop/index.js b/src/theme/BlogSidebar/Desktop/index.js index e015ad1d7b1..76829375bc9 100644 --- a/src/theme/BlogSidebar/Desktop/index.js +++ b/src/theme/BlogSidebar/Desktop/index.js @@ -8,47 +8,13 @@ import KBArticleSearch from "../../../components/KBArticleSearch/KBArticleSearch import {DocSearchButton} from "@docsearch/react"; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import {useCallback, useRef, useState} from "react"; -import kb_articles_and_tags from '@site/static/kb_toc.json'; -const allowed_tags = [ - 'Concepts', - 'Migrations', - 'Use Cases', - 'Best Practices', - 'Managing Cloud', - 'Security and Authentication', - 'Cloud Migration', - 'Core Data Concepts', - 'Managing Data', - 'Updating Data', - 'Data Modelling', - 'Deleting Data', - 'Performance and Optimizations', - 'Server Admin', - 'Deployments and Scaling', - 'Settings', - 'Tools and Utilities', - 'System Tables', - 'Functions', - 'Engines', - 'Language Clients', - 'ClickPipes', - 'Native Clients and Interfaces', - 'Data Sources', - 'Data Visualization', - 'Data Formats', - 'Data Ingestion', - 'Data Export', - 'chDB', - 'Errors and Exceptions', - 'Community', -] export default function BlogSidebarDesktop({sidebar}) { const { siteConfig } = useDocusaurusContext(); const [filteredArticles, setFilteredArticles] = useState(sidebar.items); - const updateResults = (filteredArticlesFromSearch) => { - setFilteredArticles(filteredArticlesFromSearch); + const updateResults = (matchingArticlesFromSearch) => { + setFilteredArticles(matchingArticlesFromSearch); } return ( @@ -63,11 +29,9 @@ export default function BlogSidebarDesktop({sidebar}) {
From 8911dbe210f37d0c6193c0ad373e6e695adaca73 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Sat, 1 Feb 2025 16:48:07 +0100 Subject: [PATCH 13/36] improve styling, persist search on refresh --- .../KBArticleSearch/KBArticleSearch.js | 32 ++++++++++++------- src/css/custom.scss | 2 +- src/theme/BlogSidebar/Desktop/index.js | 25 +++++++++------ .../BlogSidebar/Desktop/styles.module.css | 5 ++- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/components/KBArticleSearch/KBArticleSearch.js b/src/components/KBArticleSearch/KBArticleSearch.js index c1906def1a0..b0f4b568e09 100644 --- a/src/components/KBArticleSearch/KBArticleSearch.js +++ b/src/components/KBArticleSearch/KBArticleSearch.js @@ -5,13 +5,18 @@ import FlexSearch from 'flexsearch' const KBArticleSearch = ({kb_articles, kb_articles_and_tags, onUpdateResults}) => { const indexRef = useRef(null); - const [searchTerm, setSearchTerm] = useState(""); + const [searchTerm, setSearchTerm] = useState(''); const [searchResults, setSearchResults] = useState([]); const [matchedArticles, setMatchedArticles] = useState(kb_articles_and_tags); useEffect(() => { - if (!indexRef.current) { + const storedTerm = localStorage.getItem('last_search_term'); + if(storedTerm) { + setSearchTerm(storedTerm) + } + + if (!indexRef.current) { // Add id property to kb_articles_and_tags to be used for indexing kb_articles_and_tags.forEach((object, index)=> { object.id = index; @@ -46,22 +51,26 @@ const KBArticleSearch = ({kb_articles, kb_articles_and_tags, onUpdateResults}) = }, []); - // handler function called on onKeyUp events in the text search bar const handleSearch = (event) => { setSearchTerm(event.target.value); - const results = indexRef.current.search(event.target.value); - setSearchResults(results); + setSearchResults(indexRef.current.search(event.target.value)); }; + useEffect(() => { + localStorage.setItem('last_search_term', searchTerm); + setSearchResults(indexRef.current.search(searchTerm)); + }, [searchTerm]); + const convert_indexes_to_articles = () => { - if (searchTerm.length === 0 || /\s+/.test(searchTerm)) + if (searchTerm.length === 0 || /\s+/.test(searchTerm)){ setMatchedArticles(kb_articles_and_tags) // return all if search term is empty or consists of spaces - else { + } else { const indices = searchResults.flatMap(search_field_results => search_field_results.result); const unique_indices = [...new Set(indices)]; - setMatchedArticles(kb_articles_and_tags.filter((article)=>{ + const results = kb_articles_and_tags.filter((article)=>{ return unique_indices.includes(article.id) - })); + }) + setMatchedArticles(results); } } @@ -81,9 +90,10 @@ const KBArticleSearch = ({kb_articles, kb_articles_and_tags, onUpdateResults}) = strokeLinejoin="round"> diff --git a/src/css/custom.scss b/src/css/custom.scss index 9f3f628eafc..69e386bf286 100644 --- a/src/css/custom.scss +++ b/src/css/custom.scss @@ -1237,7 +1237,7 @@ nav[aria-label='Docs sidebar']:hover { form.DocSearch-Button { justify-content: normal; - margin-bottom: 8px; + margin-bottom: 24px; width: 98%; } diff --git a/src/theme/BlogSidebar/Desktop/index.js b/src/theme/BlogSidebar/Desktop/index.js index dd9037b298e..00678b3b7d5 100644 --- a/src/theme/BlogSidebar/Desktop/index.js +++ b/src/theme/BlogSidebar/Desktop/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import clsx from 'clsx'; import Link from '@docusaurus/Link'; import {translate} from '@docusaurus/Translate'; @@ -13,23 +13,30 @@ import kb_articles_and_tags from '@site/static/knowledgebase_toc.json'; export default function BlogSidebarDesktop({sidebar}) { const { siteConfig } = useDocusaurusContext(); - const [filteredArticles, setFilteredArticles] = useState(kb_articles_and_tags); + + const storedResults = localStorage.getItem("last_search_results"); + const initialArticles = storedResults ? JSON.parse(storedResults) : kb_articles_and_tags + const [filteredArticles, setFilteredArticles] = useState(initialArticles); + const updateResults = (matchingArticlesFromSearch) => { setFilteredArticles(matchingArticlesFromSearch); } + useEffect(() => { + // Check for a stored search term. If it exists, use it to filter. + const storedTerm = localStorage.getItem('last_search_results'); + if (storedTerm) { + setFilteredArticles(JSON.parse(storedTerm)) + } + }, []); // This effect runs only once on mount + return (