diff --git a/ui/src/components/sqlSearch/index.tsx b/ui/src/components/sqlSearch/index.tsx index 66d9b166..b7df71a8 100644 --- a/ui/src/components/sqlSearch/index.tsx +++ b/ui/src/components/sqlSearch/index.tsx @@ -35,6 +35,11 @@ import { searchSqlPrefix, } from '@/utils/constants' import { useAxios } from '@/utils/request' +import { + cacheHistory, + deleteHistoryByItem, + getHistoryList, +} from '@/utils/tools' import styles from './styles.module.less' @@ -90,436 +95,426 @@ const focusHandlerExtension = EditorView.domEventHandlers({ type SqlSearchIProps = { sqlEditorValue: string - handleSearch: (val: string) => void -} - -function getHistoryList() { - return localStorage?.getItem('sqlEditorHistory') - ? JSON.parse(localStorage?.getItem('sqlEditorHistory')) - : [] + handleSqlSearch: (val: string) => void } -function deleteHistoryByItem(val: string) { - const lastHistory: any = localStorage.getItem('sqlEditorHistory') - const tmp = lastHistory ? JSON.parse(lastHistory) : [] - if (tmp?.length > 0 && tmp?.includes(val)) { - const newList = tmp?.filter(item => item !== val) - localStorage.setItem('sqlEditorHistory', JSON.stringify(newList)) - } -} +const SqlSearch = memo( + ({ sqlEditorValue, handleSqlSearch }: SqlSearchIProps) => { + const editorRef = useRef(null) + const { t, i18n } = useTranslation() + const clusterListRef = useRef(null) + const [clusterList, setClusterList] = useState([]) + const [historyCompletions, setHistoryCompletions] = useState< + { value: string }[] + >([]) + const historyCompletionsRef = useRef( + getHistoryList('sqlEditorHistory'), + ) + + function cacheSqlHistory(val: string) { + const result = cacheHistory('sqlEditorHistory', val) + historyCompletionsRef.current = result + setHistoryCompletions(historyCompletionsRef.current) + } -const SqlSearch = memo(({ sqlEditorValue, handleSearch }: SqlSearchIProps) => { - const editorRef = useRef(null) - const { t, i18n } = useTranslation() - const clusterListRef = useRef(null) - const [clusterList, setClusterList] = useState([]) - const [historyCompletions, setHistoryCompletions] = useState< - { value: string }[] - >([]) - const historyCompletionsRef = useRef(getHistoryList()) - - function cacheHistory(val: string) { - const lastHistory: any = localStorage.getItem('sqlEditorHistory') - const tmp = lastHistory ? JSON.parse(lastHistory) : [] - const newList = [val, ...tmp?.filter(item => item !== val)] - localStorage.setItem('sqlEditorHistory', JSON.stringify(newList)) - historyCompletionsRef.current = getHistoryList() - setHistoryCompletions(historyCompletionsRef.current) - } - - const { response } = useAxios({ - url: '/rest-api/v1/clusters', - option: { - params: { - orderBy: 'name', - ascending: true, + const { response } = useAxios({ + url: '/rest-api/v1/clusters', + option: { + params: { + orderBy: 'name', + ascending: true, + }, }, - }, - manual: false, - method: 'GET', - }) - - useEffect(() => { - if (response?.success) { - clusterListRef.current = response?.data?.items - setClusterList(response?.data?.items) - } - }, [response]) - - useEffect(() => { - getHistoryList() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - function getCustomCompletions(regMatch, cusCompletions, pos) { - const filterTerm = regMatch[2] - const customCompletions = cusCompletions - .filter(completion => - completion.toLowerCase().includes(filterTerm.toLowerCase()), - ) - .map(completion => ({ - label: completion, - type: 'custom', - apply: completion, - boost: 0, - })) - - const from = pos - filterTerm?.length - if (customCompletions?.length > 0) { - return { from, options: customCompletions } - } - return null - } - - useEffect(() => { - if (editorRef.current) { - const contentEditableElement = editorRef.current.querySelector( - '.cm-content', - ) as any - if (contentEditableElement) { - contentEditableElement.style.outline = 'none' + manual: false, + method: 'GET', + }) + + useEffect(() => { + if (response?.success) { + clusterListRef.current = response?.data?.items + setClusterList(response?.data?.items) } - const customCompletionKeymap: KeyBinding[] = [ - { key: 'Tab', run: acceptCompletion }, - ] - const overrideKeymap = keymap.of( - customCompletionKeymap.concat( - completionKeymap.filter(b => b.key !== 'Enter'), - ), - ) - const mySQLHighlightStyle = HighlightStyle.define([ - { tag: tags.keyword, color: 'blue' }, - ]) - - const customCompletion = context => { - const { state, pos } = context - const beforeCursor = state.doc.sliceString(0, pos) - if (state.doc?.length === 0) { - const historyOptions: any[] = historyCompletionsRef?.current?.map( - record => ({ - label: record, - type: 'history', - apply: record, - }), - ) - return { - from: context.pos, - options: historyOptions, - filter: false, - } - } - - const whereMatch = /\b(where|or|and) (\S*)$/.exec(beforeCursor) - if (whereMatch) { - return getCustomCompletions(whereMatch, whereKeywords, pos) - } - - const kindMatch = /(kind\s*=\s*)(\S*)$/i.exec(beforeCursor) - if (kindMatch) { - return getCustomCompletions(kindMatch, kindCompletions, pos) - } + }, [response]) + + useEffect(() => { + getHistoryList('sqlEditorHistory') + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + function getCustomCompletions(regMatch, cusCompletions, pos) { + const filterTerm = regMatch[2] + const customCompletions = cusCompletions + .filter(completion => + completion.toLowerCase().includes(filterTerm.toLowerCase()), + ) + .map(completion => ({ + label: completion, + type: 'custom', + apply: completion, + boost: 0, + })) + + const from = pos - filterTerm?.length + if (customCompletions?.length > 0) { + return { from, options: customCompletions } + } + return null + } - if (whereKeywords?.some(item => beforeCursor?.endsWith(`${item} `))) { - const customCompletions = operatorKeywords.map(completion => ({ - label: completion, - type: 'custom', - validFor: () => false, - })) - return { from: pos, options: customCompletions } + useEffect(() => { + if (editorRef.current) { + const contentEditableElement = editorRef.current.querySelector( + '.cm-content', + ) as any + if (contentEditableElement) { + contentEditableElement.style.outline = 'none' } + const customCompletionKeymap: KeyBinding[] = [ + { key: 'Tab', run: acceptCompletion }, + ] + const overrideKeymap = keymap.of( + customCompletionKeymap.concat( + completionKeymap.filter(b => b.key !== 'Enter'), + ), + ) + const mySQLHighlightStyle = HighlightStyle.define([ + { tag: tags.keyword, color: 'blue' }, + ]) + + const customCompletion = context => { + const { state, pos } = context + const beforeCursor = state.doc.sliceString(0, pos) + if (state.doc?.length === 0) { + const historyOptions: any[] = historyCompletionsRef?.current?.map( + record => ({ + label: record, + type: 'history', + apply: record, + }), + ) + return { + from: context.pos, + options: historyOptions, + filter: false, + } + } - const clusterMatch = /(cluster\s*=\s*)(\S*)$/i.exec(beforeCursor) - if (clusterMatch) { - const clusterNameList = clusterListRef.current?.map( - item => `'${item?.metadata?.name}'`, - ) - return getCustomCompletions(clusterMatch, clusterNameList, pos) - } + const whereMatch = /\b(where|or|and) (\S*)$/.exec(beforeCursor) + if (whereMatch) { + return getCustomCompletions(whereMatch, whereKeywords, pos) + } - const word = context?.matchBefore(/\w*/) + const kindMatch = /(kind\s*=\s*)(\S*)$/i.exec(beforeCursor) + if (kindMatch) { + return getCustomCompletions(kindMatch, kindCompletions, pos) + } - if (!word || (word?.from === word?.to && !context?.explicit)) { - return null - } + if (whereKeywords?.some(item => beforeCursor?.endsWith(`${item} `))) { + const customCompletions = operatorKeywords.map(completion => ({ + label: completion, + type: 'custom', + validFor: () => false, + })) + return { from: pos, options: customCompletions } + } - const options = defaultKeywords - .filter(kw => kw.toLowerCase().includes(word.text?.toLowerCase())) - .map(stmt => ({ label: stmt, type: 'custom' })) - if (options?.length === 0) { - return null - } + const clusterMatch = /(cluster\s*=\s*)(\S*)$/i.exec(beforeCursor) + if (clusterMatch) { + const clusterNameList = clusterListRef.current?.map( + item => `'${item?.metadata?.name}'`, + ) + return getCustomCompletions(clusterMatch, clusterNameList, pos) + } - return completeFromList(options)(context) - } + const word = context?.matchBefore(/\w*/) - const completionPlugin = ViewPlugin.fromClass( - class { - constructor(view) { - this.addDeleteButtons(view) + if (!word || (word?.from === word?.to && !context?.explicit)) { + return null } - update(update) { - this.addDeleteButtons(update.view) + const options = defaultKeywords + .filter(kw => kw.toLowerCase().includes(word.text?.toLowerCase())) + .map(stmt => ({ label: stmt, type: 'custom' })) + if (options?.length === 0) { + return null } - addDeleteButtons(view) { - const compState: any = completionStatus(view.state) - if (compState === 'active') { - const completions: any = currentCompletions(view.state) - setTimeout(() => { - if (completions?.[0]?.type === 'history') { - view.dom - .querySelectorAll( - '.cm-tooltip.cm-tooltip-autocomplete > ul > li', - ) - .forEach((item, index) => { - if ( - item.querySelector( - '.cm-tooltip-autocomplete_item_label', - ) - ) { - return - } - if (item.querySelector('.delete-btn')) { - return - } - const labelSpan = document.createElement('span') - labelSpan.className = 'cm-tooltip-autocomplete_item_label' - labelSpan.innerText = completions?.[index]?.label - item.style.display = 'flex' - item.style.justifyContent = 'space-between' - item.style.alignItems = 'center' - labelSpan.style.flex = '1' - labelSpan.style.overflow = 'hidden' - labelSpan.style.textOverflow = 'ellipsis' - labelSpan.style.whiteSpace = 'nowrap' - labelSpan.style.padding = '0 10px' - labelSpan.style.fontSize = '14px' - const btn = document.createElement('span') - btn.innerText = '✕' - btn.className = 'delete-btn' - btn.style.border = 'none' - btn.style.fontSize = '20px' - btn.style.display = 'flex' - btn.style.justifyContent = 'center' - btn.style.alignItems = 'center' - btn.style.height = '100%' - btn.style.padding = '0 15px' - item.innerText = '' - item.appendChild(labelSpan) - item.appendChild(btn) - btn.addEventListener('mousedown', event => { - event.preventDefault() - event.stopPropagation() - const completionOption = completions?.[index] - historyCompletionsRef.current = - historyCompletionsRef?.current?.filter( - item => item !== completionOption?.label, + return completeFromList(options)(context) + } + + const completionPlugin = ViewPlugin.fromClass( + class { + constructor(view) { + this.addDeleteButtons(view) + } + + update(update) { + this.addDeleteButtons(update.view) + } + + addDeleteButtons(view) { + const compState: any = completionStatus(view.state) + if (compState === 'active') { + const completions: any = currentCompletions(view.state) + setTimeout(() => { + if (completions?.[0]?.type === 'history') { + view.dom + .querySelectorAll( + '.cm-tooltip.cm-tooltip-autocomplete > ul > li', + ) + .forEach((item, index) => { + if ( + item.querySelector( + '.cm-tooltip-autocomplete_item_label', ) - if (view) { - startCompletion(view) - deleteHistoryByItem(completionOption?.label) + ) { + return } + if (item.querySelector('.delete-btn')) { + return + } + const labelSpan = document.createElement('span') + labelSpan.className = + 'cm-tooltip-autocomplete_item_label' + labelSpan.innerText = completions?.[index]?.label + item.style.display = 'flex' + item.style.justifyContent = 'space-between' + item.style.alignItems = 'center' + labelSpan.style.flex = '1' + labelSpan.style.overflow = 'hidden' + labelSpan.style.textOverflow = 'ellipsis' + labelSpan.style.whiteSpace = 'nowrap' + labelSpan.style.padding = '0 10px' + labelSpan.style.fontSize = '14px' + const btn = document.createElement('span') + btn.innerText = '✕' + btn.className = 'delete-btn' + btn.style.border = 'none' + btn.style.fontSize = '20px' + btn.style.display = 'flex' + btn.style.justifyContent = 'center' + btn.style.alignItems = 'center' + btn.style.height = '100%' + btn.style.padding = '0 15px' + item.innerText = '' + item.appendChild(labelSpan) + item.appendChild(btn) + btn.addEventListener('mousedown', event => { + event.preventDefault() + event.stopPropagation() + const completionOption = completions?.[index] + historyCompletionsRef.current = + historyCompletionsRef?.current?.filter( + item => item !== completionOption?.label, + ) + if (view) { + startCompletion(view) + deleteHistoryByItem( + 'sqlEditorHistory', + completionOption?.label, + ) + } + }) }) - }) - } - }, 0) + } + }, 0) + } } - } - }, - ) - - const startState = EditorState.create({ - doc: '', - extensions: [ - completionPlugin, - placeholder(`${t('SearchUsingSQL')} ......`), - placeholderStyle, - new LanguageSupport(sql() as any), - highlightSpecialChars(), - syntaxHighlighting(mySQLHighlightStyle), - autocompletion({ - override: [customCompletion], - }), - focusHandlerExtension, - autocompletion(), - overrideKeymap, - EditorView.domEventHandlers({ - keydown: (event, view) => { - if (event.key === 'Enter') { - const completions = currentCompletions(view.state) - if (!completions || completions?.length === 0) { - event.preventDefault() - handleClick() - return true + }, + ) + + const startState = EditorState.create({ + doc: '', + extensions: [ + completionPlugin, + placeholder(`${t('SearchUsingSQL')} ......`), + placeholderStyle, + new LanguageSupport(sql() as any), + highlightSpecialChars(), + syntaxHighlighting(mySQLHighlightStyle), + autocompletion({ + override: [customCompletion], + }), + focusHandlerExtension, + autocompletion(), + overrideKeymap, + EditorView.domEventHandlers({ + keydown: (event, view) => { + if (event.key === 'Enter') { + const completions = currentCompletions(view.state) + if (!completions || completions?.length === 0) { + event.preventDefault() + handleClick() + return true + } + } + return false + }, + }), + EditorState.allowMultipleSelections.of(false), + EditorView.updateListener.of(update => { + if (update.docChanged) { + if (update.state.doc.lines > 1) { + update.view.dispatch({ + changes: { + from: update.startState.doc?.length, + to: update.state.doc?.length, + }, + }) } } - return false - }, - }), - EditorState.allowMultipleSelections.of(false), - EditorView.updateListener.of(update => { - if (update.docChanged) { - if (update.state.doc.lines > 1) { - update.view.dispatch({ - changes: { - from: update.startState.doc?.length, - to: update.state.doc?.length, - }, - }) - } - } - }), - ], - }) + }), + ], + }) - const view = new EditorView({ - state: startState, - parent: editorRef.current, - }) + const view = new EditorView({ + state: startState, + parent: editorRef.current, + }) - editorRef.current.view = view + editorRef.current.view = view - return () => { - if (editorRef.current?.view) { - // eslint-disable-next-line react-hooks/exhaustive-deps - editorRef.current?.view?.destroy() + return () => { + if (editorRef.current?.view) { + // eslint-disable-next-line react-hooks/exhaustive-deps + editorRef.current?.view?.destroy() + } } } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editorRef.current, historyCompletions, i18n?.language]) - - useEffect(() => { - if (sqlEditorValue && clusterList && editorRef.current?.view) { - editorRef.current?.view.dispatch({ - changes: { - from: 0, - to: editorRef.current?.view.state.doc?.length, - insert: sqlEditorValue, - }, - }) - } - }, [clusterList, editorRef.current?.view, sqlEditorValue]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editorRef.current, historyCompletions, i18n?.language]) + + useEffect(() => { + if (sqlEditorValue && clusterList && editorRef.current?.view) { + editorRef.current?.view.dispatch({ + changes: { + from: 0, + to: editorRef.current?.view.state.doc?.length, + insert: sqlEditorValue, + }, + }) + } + }, [clusterList, editorRef.current?.view, sqlEditorValue]) - const getContent = () => { - if (editorRef.current?.view) { - const content = editorRef.current.view.state.doc.toString() - return content + const getContent = () => { + if (editorRef.current?.view) { + const content = editorRef.current.view.state.doc.toString() + return content + } + return '' } - return '' - } - - function handleClick() { - const inputValue = getContent() - if (!inputValue) { - message.warning(t('PleaseEnterValidSQLStatement')) - return + + function handleClick() { + const inputValue = getContent() + if (!inputValue) { + message.warning(t('PleaseEnterValidSQLStatement')) + return + } + cacheSqlHistory(inputValue) + handleSqlSearch(inputValue) } - cacheHistory(inputValue) - handleSearch(inputValue) - } - - return ( -
-
-
{searchSqlPrefix}
-
-
ul { - box-sizing: border-box; - height: auto; - max-height: 40vh; - overflow-y: auto !important; - } - .cm-tooltip.cm-tooltip-autocomplete > ul > li { - background-color: #f5f5f5 !important; - margin: 5px 0 !important; - padding: 10px 0 !important; - border-radius: 6px !important; - width: auto !important; - box-sizing: border-box; - } - .cm-tooltip.cm-tooltip-autocomplete - > ul - > li[aria-selected='true'], - .cm-tooltip.cm-tooltip-autocomplete > ul > li:hover { - background-color: #97a9f5 !important; - color: white !important; - } - `} - /> -
- -
-
-
- + return ( +
+
+
{searchSqlPrefix}
+
+
ul { + box-sizing: border-box; + height: auto; + max-height: 40vh; + overflow-y: auto !important; + } + .cm-tooltip.cm-tooltip-autocomplete > ul > li { + background-color: #f5f5f5 !important; + margin: 5px 0 !important; + padding: 10px 0 !important; + border-radius: 6px !important; + width: auto !important; + box-sizing: border-box; + } + + .cm-tooltip.cm-tooltip-autocomplete + > ul + > li[aria-selected='true'], + .cm-tooltip.cm-tooltip-autocomplete > ul > li:hover { + background-color: #97a9f5 !important; + color: white !important; + } + `} + /> +
+ +
+
+
+ +
-
- ) -}) + ) + }, +) export default SqlSearch diff --git a/ui/src/locales/de.json b/ui/src/locales/de.json index df6d8c68..e1c874a8 100644 --- a/ui/src/locales/de.json +++ b/ui/src/locales/de.json @@ -119,5 +119,7 @@ "LoginSuccess": "Erfolgreich eingeloggt", "Login": "Einloggen", "LogoutSuccess": "Erfolgreich abgemeldet", - "InputToken": "Geben Sie bitte den Token ein" + "InputToken": "Geben Sie bitte den Token ein", + "SearchByNaturalLanguage": "Suche mit natürlicher Sprache", + "CannotBeEmpty": "Darf nicht leer sein" } diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index b96d3ffa..1b153882 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -119,5 +119,7 @@ "LoginSuccess": "Login successful", "Login": "Login", "LogoutSuccess": "Successfully logged out", - "InputToken": "Please enter the token" + "InputToken": "Please enter the token", + "SearchByNaturalLanguage": "Search By Natural Language", + "CannotBeEmpty": "Cannot be empty" } diff --git a/ui/src/locales/pt.json b/ui/src/locales/pt.json index 112f8f10..f295d23a 100644 --- a/ui/src/locales/pt.json +++ b/ui/src/locales/pt.json @@ -119,5 +119,7 @@ "LoginSuccess": "Login bem-sucedido", "Login": "Login", "LogoutSuccess": "Sessão encerrada com sucesso", - "InputToken": "Por favor, insira o token" + "InputToken": "Por favor, insira o token", + "SearchByNaturalLanguage": "Procure por linguagem natural", + "CannotBeEmpty": "Não pode estar vazio" } diff --git a/ui/src/locales/zh.json b/ui/src/locales/zh.json index 662434f7..b2a73e50 100644 --- a/ui/src/locales/zh.json +++ b/ui/src/locales/zh.json @@ -119,5 +119,7 @@ "LoginSuccess": "登录成功", "Login": "登录", "LogoutSuccess": "登出成功", - "InputToken": "请输入 token" + "InputToken": "请输入 token", + "SearchByNaturalLanguage": "自然语言搜索", + "CannotBeEmpty": "不能为空" } diff --git a/ui/src/pages/insightDetail/components/tagVariableSizeList/index.tsx b/ui/src/pages/insightDetail/components/tagVariableSizeList/index.tsx index cbf3b5b1..02014977 100644 --- a/ui/src/pages/insightDetail/components/tagVariableSizeList/index.tsx +++ b/ui/src/pages/insightDetail/components/tagVariableSizeList/index.tsx @@ -68,6 +68,7 @@ const TagVariableSizeList = ({ allTags, containerWidth }: IProps) => { return rows } + // eslint-disable-next-line react-hooks/exhaustive-deps const transformedData = useMemo(() => convertDataToRows(allTags), [allTags]) const itemSize = 30 // lineHeight const itemCount = transformedData?.length // row count diff --git a/ui/src/pages/result/index.tsx b/ui/src/pages/result/index.tsx index 358c8a4d..90f38f53 100644 --- a/ui/src/pages/result/index.tsx +++ b/ui/src/pages/result/index.tsx @@ -1,36 +1,109 @@ import React, { useState, useEffect } from 'react' -import { Pagination, Empty, Divider, Tooltip } from 'antd' +import { + Pagination, + Empty, + Divider, + Tooltip, + Input, + message, + AutoComplete, + Space, +} from 'antd' import { useLocation, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { ClockCircleOutlined } from '@ant-design/icons' +import { ClockCircleOutlined, CloseOutlined } from '@ant-design/icons' import queryString from 'query-string' import classNames from 'classnames' import SqlSearch from '@/components/sqlSearch' import KarporTabs from '@/components/tabs/index' -import { utcDateToLocalDate } from '@/utils/tools' +import { + cacheHistory, + deleteHistoryByItem, + getHistoryList, + utcDateToLocalDate, +} from '@/utils/tools' import Loading from '@/components/loading' import { ICON_MAP } from '@/utils/images' import { searchSqlPrefix, tabsList } from '@/utils/constants' import { useAxios } from '@/utils/request' +// import useDebounce from '@/hooks/useDebounce' import styles from './styles.module.less' +const { Search } = Input +const Option = AutoComplete.Option + +export const CustomDropdown = props => { + const { options } = props + + return ( +
+ {options.map((option, index) => ( +
+ +
+ ))} +
+ ) +} + const Result = () => { const { t } = useTranslation() const location = useLocation() const navigate = useNavigate() const [pageData, setPageData] = useState() - const urlSearchParams = queryString.parse(location.search) - const [searchType, setSearchType] = useState('sql') + const urlSearchParams: any = queryString.parse(location.search) + const [searchType, setSearchType] = useState(urlSearchParams?.pattern) const [searchParams, setSearchParams] = useState({ pageSize: 20, page: 1, query: urlSearchParams?.query || '', total: 0, }) + const [naturalValue, setNaturalValue] = useState('') + const [sqlValue, setSqlValue] = useState('') + const [naturalOptions, setNaturalOptions] = useState( + getHistoryList('naturalHistory') || [], + ) + + function cacheNaturalHistory(key, val) { + const result = cacheHistory(key, val) + setNaturalOptions(result) + } + + useEffect(() => { + if (searchType === 'natural') { + setNaturalValue(urlSearchParams?.query) + handleNaturalSearch(urlSearchParams?.query) + } + if (searchType === 'sql') { + setSqlValue(urlSearchParams?.query) + handleSqlSearch(urlSearchParams?.query) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (urlSearchParams?.pattern) { + setSearchType(urlSearchParams?.pattern) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [urlSearchParams?.pattern, urlSearchParams?.query]) function handleTabChange(value: string) { setSearchType(value) + const urlString = queryString.stringify({ + pattern: value, + query: + value === 'natural' ? naturalValue : value === 'sql' ? sqlValue : '', + }) + navigate(`${location?.pathname}?${urlString}`, { replace: true }) } function handleChangePage(page: number, pageSize: number) { @@ -48,11 +121,23 @@ const Result = () => { useEffect(() => { if (response?.success) { - setPageData(response?.data?.items || {}) const objParams = { ...urlSearchParams, + pattern: 'sql', query: response?.successParams?.query || searchParams?.query, } + if (searchType === 'natural') { + let sqlVal + if (response?.data?.sqlQuery?.includes('WHERE')) { + sqlVal = `where ${response?.data?.sqlQuery?.split(' WHERE ')?.[1]}` + } + if (response?.data?.sqlQuery?.includes('where')) { + sqlVal = `where ${response?.data?.sqlQuery?.split(' where ')?.[1]}` + } + setSearchType('sql') + setSqlValue(sqlVal) + } + setPageData(response?.data?.items || {}) const urlString = queryString.stringify(objParams) navigate(`${location?.pathname}?${urlString}`, { replace: true }) } @@ -60,11 +145,19 @@ const Result = () => { }, [response]) function getPageData(params) { + const pattern = + searchType === 'natural' ? 'nl' : searchType === 'sql' ? 'sql' : '' + const query = + searchType === 'natural' + ? params?.query + : searchType === 'sql' + ? `${searchSqlPrefix} ${params?.query}` + : '' refetch({ option: { params: { - query: `${searchSqlPrefix} ${params?.query || searchParams?.query}`, - ...(searchType === 'sql' ? { pattern: 'sql' } : {}), + pattern, + query, page: params?.page || searchParams?.page, pageSize: params?.pageSize || searchParams?.pageSize, }, @@ -78,11 +171,8 @@ const Result = () => { }) } - useEffect(() => { - getPageData(searchParams) - }, []) // eslint-disable-line react-hooks/exhaustive-deps - - function handleSearch(inputValue) { + function handleSqlSearch(inputValue) { + setSqlValue(inputValue) setSearchParams({ ...searchParams, query: inputValue, @@ -128,6 +218,23 @@ const Result = () => { navigate(`/insightDetail/${nav}?${urlParams}`) } + function handleNaturalAutoCompleteChange(val) { + setNaturalValue(val) + } + + function handleNaturalSearch(value) { + if (!value && !naturalValue) { + message.warning(t('CannotBeEmpty')) + return + } + cacheNaturalHistory('naturalHistory', value) + getPageData({ + pageSize: searchParams?.pageSize, + page: 1, + query: value, + }) + } + function renderEmpty() { return (
{ ) } + const handleDelete = val => { + deleteHistoryByItem('naturalHistory', val) + const list = getHistoryList('naturalHistory') || [] + setNaturalOptions(list) + } + + const renderOption = val => { + return ( + + {val} + { + event?.stopPropagation() + handleDelete(val) + }} + /> + + ) + } + + const tmpOptions = naturalOptions?.map(val => ({ + value: val, + label: renderOption(val), + })) + return (
@@ -264,12 +402,39 @@ const Result = () => { onChange={handleTabChange} />
- + {searchType === 'sql' && ( + + )} + {searchType === 'natural' && ( +
+ { + if (option?.value) { + return ( + (option?.value as string) + ?.toUpperCase() + .indexOf(inputValue.toUpperCase()) !== -1 + ) + } + }} + > + + +
+ )}
{loading ? renderLoading() diff --git a/ui/src/pages/search/index.tsx b/ui/src/pages/search/index.tsx index ace766dd..dbe96df2 100644 --- a/ui/src/pages/search/index.tsx +++ b/ui/src/pages/search/index.tsx @@ -15,14 +15,21 @@ */ import React, { useCallback, useState } from 'react' -import { Tag } from 'antd' -import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons' +import { AutoComplete, Input, message, Space, Tag } from 'antd' +import { + DoubleLeftOutlined, + DoubleRightOutlined, + CloseOutlined, +} from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import KarporTabs from '@/components/tabs/index' import logoFull from '@/assets/img/logo-full.svg' import SqlSearch from '@/components/sqlSearch' import { defaultSqlExamples, tabsList } from '@/utils/constants' +import { deleteHistoryByItem, getHistoryList } from '@/utils/tools' + +const { Search } = Input import styles from './styles.module.less' @@ -32,6 +39,9 @@ const SearchPage = () => { const [searchType, setSearchType] = useState('sql') const [sqlEditorValue, setSqlEditorValue] = useState('') const [showAll, setShowAll] = useState(false) + const [naturalOptions, setNaturalOptions] = useState( + getHistoryList('naturalHistory') || [], + ) const toggleTags = () => { setShowAll(!showAll) @@ -45,7 +55,7 @@ const SearchPage = () => { setSqlEditorValue(str) } - const handleSearch = useCallback( + const handleSqlSearch = useCallback( inputValue => { navigate(`/search/result?query=${inputValue}&pattern=sql`) }, @@ -75,6 +85,45 @@ const SearchPage = () => { return renderSqlExamples(sqlExamples) } + function handleNaturalSearch(value) { + if (!value) { + message.warning(t('CannotBeEmpty')) + return + } + navigate(`/search/result?query=${value}&pattern=natural`) + } + + const handleDelete = val => { + deleteHistoryByItem('naturalHistory', val) + const list = getHistoryList('naturalHistory') || [] + setNaturalOptions(list) + } + + const renderOption = val => { + return ( + + {val} + { + event?.stopPropagation() + handleDelete(val) + }} + /> + + ) + } + + const tmpOptions = naturalOptions?.map(val => ({ + value: val, + label: renderOption(val), + })) + return (
@@ -88,12 +137,42 @@ const SearchPage = () => { onChange={handleTabChange} />
-
- -
+ + {searchType === 'sql' && ( +
+ +
+ )} + + {searchType === 'natural' && ( +
+ { + if (option?.value) { + return ( + (option?.value as string) + ?.toUpperCase() + .indexOf(inputValue.toUpperCase()) !== -1 + ) + } + }} + > + + +
+ )} +
{searchType === 'keyword' ? (
@@ -108,7 +187,7 @@ const SearchPage = () => {
- ) : ( + ) : searchType === 'natural' ? null : (
{renderSqlExamples(null)} {!showAll && ( diff --git a/ui/src/utils/constants.ts b/ui/src/utils/constants.ts index 11d42923..931a345b 100644 --- a/ui/src/utils/constants.ts +++ b/ui/src/utils/constants.ts @@ -81,6 +81,10 @@ export const defaultKeywords = [ export const tabsList = [ { label: 'KeywordSearch', value: 'keyword', disabled: true }, { label: 'SQLSearch', value: 'sql' }, + { + label: 'SearchByNaturalLanguage', + value: 'natural', + }, ] export const insightTabsList = [ diff --git a/ui/src/utils/tools.ts b/ui/src/utils/tools.ts index 75e91664..d7ef0494 100644 --- a/ui/src/utils/tools.ts +++ b/ui/src/utils/tools.ts @@ -199,3 +199,26 @@ export function getTextSizeByCanvas( canvas.remove() return width + 2 } + +export function getHistoryList(key) { + return localStorage?.getItem(key) + ? JSON.parse(localStorage?.getItem(key)) + : [] +} + +export function deleteHistoryByItem(key, val: string) { + const lastHistory: any = localStorage.getItem(key) + const tmp = lastHistory ? JSON.parse(lastHistory) : [] + if (tmp?.length > 0 && tmp?.includes(val)) { + const newList = tmp?.filter(item => item !== val) + localStorage.setItem(key, JSON.stringify(newList)) + } +} + +export function cacheHistory(key, val: string) { + const lastHistory: any = localStorage.getItem(key) + const tmp = lastHistory ? JSON.parse(lastHistory) : [] + const newList = [val, ...tmp?.filter(item => item !== val)] + localStorage.setItem(key, JSON.stringify(newList)) + return getHistoryList(key) +}