Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: search by natural language #613

Merged
merged 1 commit into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
785 changes: 390 additions & 395 deletions ui/src/components/sqlSearch/index.tsx

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion ui/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 3 additions & 1 deletion ui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 3 additions & 1 deletion ui/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 3 additions & 1 deletion ui/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,7 @@
"LoginSuccess": "登录成功",
"Login": "登录",
"LogoutSuccess": "登出成功",
"InputToken": "请输入 token"
"InputToken": "请输入 token",
"SearchByNaturalLanguage": "自然语言搜索",
"CannotBeEmpty": "不能为空"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
203 changes: 184 additions & 19 deletions ui/src/pages/result/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{options.map((option, index) => (
<div
key={index}
style={{ padding: '5px', borderBottom: '1px solid #ccc' }}
>
<Option value={option.value}>
<span>{option.value}</span> -{' '}
<span style={{ color: '#999' }}>{option.value || '默认标签'}</span>
</Option>
</div>
))}
</div>
)
}

const Result = () => {
const { t } = useTranslation()
const location = useLocation()
const navigate = useNavigate()
const [pageData, setPageData] = useState<any>()
const urlSearchParams = queryString.parse(location.search)
const [searchType, setSearchType] = useState<string>('sql')
const urlSearchParams: any = queryString.parse(location.search)
const [searchType, setSearchType] = useState<string>(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) {
Expand All @@ -48,23 +121,43 @@ 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 })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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,
},
Expand All @@ -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,
Expand Down Expand Up @@ -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 (
<div
Expand Down Expand Up @@ -255,6 +362,37 @@ const Result = () => {
)
}

const handleDelete = val => {
deleteHistoryByItem('naturalHistory', val)
const list = getHistoryList('naturalHistory') || []
setNaturalOptions(list)
}

const renderOption = val => {
return (
<Space
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span>{val}</span>
<CloseOutlined
onClick={event => {
event?.stopPropagation()
handleDelete(val)
}}
/>
</Space>
)
}

const tmpOptions = naturalOptions?.map(val => ({
value: val,
label: renderOption(val),
}))

return (
<div className={styles.container}>
<div className={styles.searchTab}>
Expand All @@ -264,12 +402,39 @@ const Result = () => {
onChange={handleTabChange}
/>
</div>
<SqlSearch
sqlEditorValue={
(searchParams?.query || urlSearchParams?.query) as string
}
handleSearch={handleSearch}
/>
{searchType === 'sql' && (
<SqlSearch
sqlEditorValue={(sqlValue || urlSearchParams?.query) as string}
handleSqlSearch={handleSqlSearch}
/>
)}
{searchType === 'natural' && (
<div className={styles.search_codemirror_container}>
<AutoComplete
style={{ width: '100%' }}
size="large"
options={tmpOptions}
value={naturalValue}
onChange={handleNaturalAutoCompleteChange}
filterOption={(inputValue, option) => {
if (option?.value) {
return (
(option?.value as string)
?.toUpperCase()
.indexOf(inputValue.toUpperCase()) !== -1
)
}
}}
>
<Search
size="large"
placeholder={`${t('SearchByNaturalLanguage')}...`}
enterButton
onSearch={handleNaturalSearch}
/>
</AutoComplete>
</div>
)}
<div className={styles.content}>
{loading
? renderLoading()
Expand Down
Loading
Loading