mirror of
https://github.com/harness/drone.git
synced 2025-05-05 06:21:50 +08:00
Update : [CODE-1410 ] Semantic Search Integration (#1046)
This commit is contained in:
parent
478db74046
commit
3ddcd2d3cc
@ -51,7 +51,7 @@ module.exports = {
|
|||||||
'./Settings': './src/pages/RepositorySettings/RepositorySettings.tsx',
|
'./Settings': './src/pages/RepositorySettings/RepositorySettings.tsx',
|
||||||
'./Webhooks': './src/pages/Webhooks/Webhooks.tsx',
|
'./Webhooks': './src/pages/Webhooks/Webhooks.tsx',
|
||||||
'./WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx',
|
'./WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx',
|
||||||
'./Search': './src/pages/Search/KeywordSearch.tsx',
|
'./Search': './src/pages/Search/CodeSearchPage.tsx',
|
||||||
'./WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
|
'./WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
|
||||||
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx'
|
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx'
|
||||||
},
|
},
|
||||||
|
@ -66,6 +66,7 @@ export interface AppProps {
|
|||||||
useExecutionDataHook: Unknown
|
useExecutionDataHook: Unknown
|
||||||
useLogsContent: Unknown
|
useLogsContent: Unknown
|
||||||
useLogsStreaming: Unknown
|
useLogsStreaming: Unknown
|
||||||
|
useFeatureFlags: Unknown
|
||||||
}>
|
}>
|
||||||
|
|
||||||
currentUser: Required<TypesUser>
|
currentUser: Required<TypesUser>
|
||||||
|
@ -47,7 +47,7 @@ import ExecutionList from 'pages/ExecutionList/ExecutionList'
|
|||||||
import Execution from 'pages/Execution/Execution'
|
import Execution from 'pages/Execution/Execution'
|
||||||
import Secret from 'pages/Secret/Secret'
|
import Secret from 'pages/Secret/Secret'
|
||||||
import Search from 'pages/Search/Search'
|
import Search from 'pages/Search/Search'
|
||||||
import KeywordSearch from 'pages/Search/KeywordSearch'
|
import CodeSearchPage from 'pages/Search/CodeSearchPage'
|
||||||
import AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline'
|
import AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import PipelineSettings from 'components/PipelineSettings/PipelineSettings'
|
import PipelineSettings from 'components/PipelineSettings/PipelineSettings'
|
||||||
@ -297,7 +297,7 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
|||||||
path={[routes.toCODEProjectSearch({ space: pathProps.space }), routes.toCODERepositorySearch({ repoPath })]}
|
path={[routes.toCODEProjectSearch({ space: pathProps.space }), routes.toCODERepositorySearch({ repoPath })]}
|
||||||
exact>
|
exact>
|
||||||
<LayoutWithSideNav title={getString('search')}>
|
<LayoutWithSideNav title={getString('search')}>
|
||||||
<KeywordSearch />
|
<CodeSearchPage />
|
||||||
</LayoutWithSideNav>
|
</LayoutWithSideNav>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import ReactDOM from 'react-dom'
|
|||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
import { routes } from 'RouteDefinitions'
|
import { routes } from 'RouteDefinitions'
|
||||||
import { defaultCurrentUser } from 'AppContext'
|
import { defaultCurrentUser } from 'AppContext'
|
||||||
|
import { useFeatureFlags } from 'hooks/useFeatureFlag'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import './bootstrap.scss'
|
import './bootstrap.scss'
|
||||||
|
|
||||||
@ -36,7 +37,8 @@ ReactDOM.render(
|
|||||||
usePermissionTranslate: noop,
|
usePermissionTranslate: noop,
|
||||||
useExecutionDataHook: noop,
|
useExecutionDataHook: noop,
|
||||||
useLogsContent: noop,
|
useLogsContent: noop,
|
||||||
useLogsStreaming: noop
|
useLogsStreaming: noop,
|
||||||
|
useFeatureFlags: useFeatureFlags
|
||||||
}}
|
}}
|
||||||
currentUser={defaultCurrentUser}
|
currentUser={defaultCurrentUser}
|
||||||
currentUserProfileURL=""
|
currentUserProfileURL=""
|
||||||
|
133
web/src/components/CodeSearch/CodeSearch.module.scss
Normal file
133
web/src/components/CodeSearch/CodeSearch.module.scss
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.searchModal {
|
||||||
|
--modal-width: min(1254px, 80vw);
|
||||||
|
|
||||||
|
width: var(--modal-width);
|
||||||
|
padding: var(--spacing-medium) var(--spacing-xlarge) var(--spacing-xlarge);
|
||||||
|
|
||||||
|
> span:first-of-type {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.searchContainer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchIcon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 11px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
right: 58px;
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
font-size: 10px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
letter-spacing: 0.237px;
|
||||||
|
color: var(--grey-300) !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sampleKeywordSearchQuery {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--grey-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sampleSemanticSearchQuery {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 44px;
|
||||||
|
background-color: var(--grey-50);
|
||||||
|
color: var(--grey-500) !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-left: 32px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
line-height: 19px !important;
|
||||||
|
letter-spacing: 0.23749999701976776px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background-image: url('data:image/svg+xml,%3Csvg%20fill%3D%22none%22%20viewBox%3D%220%200%2017%2017%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22m9.91699%208.50004h3.54171c.1878%200%20.368.07463.5008.20747.1329.13283.2075.313.2075.50086v2.47913c0%20.1879-.0746.3681-.2075.5009-.1328.1328-.313.2075-.5008.2075h-2.8334c-.1878%200-.368-.0747-.5008-.2075-.13288-.1328-.20751-.313-.20751-.5009zm0%200c0-1.77083.70831-2.83333%202.83331-3.89583m-9.91664%203.89583h3.54167c.18786%200%20.36802.07463.50086.20747.13284.13283.20747.313.20747.50086v2.47913c0%20.1879-.07463.3681-.20747.5009s-.313.2075-.50086.2075h-2.83334c-.18786%200-.36803-.0747-.50087-.2075-.13283-.1328-.20746-.313-.20746-.5009zm0%200c0-1.77083.70833-2.83333%202.83333-3.89583%22%20stroke%3D%22%23dad0f6%22%20stroke-linecap%3D%22round%22%2F%3E%3Cg%20fill%3D%22%23dad0f6%22%3E%3Cpath%20d%3D%22m10%209h4v3h-4z%22%2F%3E%3Cpath%20d%3D%22m3%209h4v3h-4z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 16px;
|
||||||
|
background-position: left 12px top 7px;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--grey-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected svg {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
right: 15px;
|
||||||
|
color: var(--grey-300);
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
input,
|
||||||
|
input:focus {
|
||||||
|
border: 1px solid var(--grey-200) !important;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 350px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
background-color: rgb(16 22 26 / 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portal {
|
||||||
|
:global {
|
||||||
|
.bp3-dialog-container.bp3-overlay-content {
|
||||||
|
align-items: flex-start !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
web/src/components/CodeSearch/CodeSearch.module.scss.d.ts
vendored
Normal file
29
web/src/components/CodeSearch/CodeSearch.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
// This is an auto-generated file
|
||||||
|
export declare const backdrop: string
|
||||||
|
export declare const layout: string
|
||||||
|
export declare const portal: string
|
||||||
|
export declare const sampleKeywordSearchQuery: string
|
||||||
|
export declare const sampleSemanticSearchQuery: string
|
||||||
|
export declare const searchBox: string
|
||||||
|
export declare const searchContainer: string
|
||||||
|
export declare const searchIcon: string
|
||||||
|
export declare const searchModal: string
|
||||||
|
export declare const sectionHeader: string
|
||||||
|
export declare const selected: string
|
235
web/src/components/CodeSearch/CodeSearch.tsx
Normal file
235
web/src/components/CodeSearch/CodeSearch.tsx
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import { Container, Dialog, Layout, Text, Utils } from '@harnessio/uicore'
|
||||||
|
import { Color } from '@harnessio/design-system'
|
||||||
|
import { Filter, LongArrowDownLeft } from 'iconoir-react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
|
import cx from 'classnames'
|
||||||
|
|
||||||
|
import { useAppContext } from 'AppContext'
|
||||||
|
import { useStrings } from 'framework/strings'
|
||||||
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||||
|
import { ButtonRoleProps } from 'utils/Utils'
|
||||||
|
import type { GitInfoProps } from 'utils/GitUtils'
|
||||||
|
|
||||||
|
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
|
||||||
|
import CodeSearchBar from 'components/CodeSearchBar/CodeSearchBar'
|
||||||
|
import svg from './search-background.svg'
|
||||||
|
import css from './CodeSearch.module.scss'
|
||||||
|
|
||||||
|
interface CodeSearchProps {
|
||||||
|
repoMetadata?: GitInfoProps['repoMetadata']
|
||||||
|
}
|
||||||
|
export enum SEARCH_MODE {
|
||||||
|
KEYWORD = 'keyword',
|
||||||
|
SEMANTIC = 'semantic'
|
||||||
|
}
|
||||||
|
const CodeSearch = ({ repoMetadata }: CodeSearchProps) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { routes } = useAppContext()
|
||||||
|
const space = useGetSpaceParam()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [showSearchModal, setShowSearchModal] = useState(false)
|
||||||
|
const [searchSampleQueryIndex, setSearchSampleQueryIndex] = useState<number>(0)
|
||||||
|
const [searchMode, setSearchMode] = useState(SEARCH_MODE.KEYWORD)
|
||||||
|
|
||||||
|
const performSearch = useCallback(
|
||||||
|
(q: string, mode: SEARCH_MODE) => {
|
||||||
|
if (repoMetadata?.path) {
|
||||||
|
history.push({
|
||||||
|
pathname: routes.toCODERepositorySearch({
|
||||||
|
repoPath: repoMetadata.path as string
|
||||||
|
}),
|
||||||
|
search: `q=${q}&mode=${mode}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
history.push({
|
||||||
|
pathname: routes.toCODEProjectSearch({
|
||||||
|
space
|
||||||
|
}),
|
||||||
|
search: `q=${q}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[history, repoMetadata?.path, routes, searchMode]
|
||||||
|
)
|
||||||
|
const onSearch = useCallback(() => {
|
||||||
|
if (search?.trim()) {
|
||||||
|
performSearch(search, searchMode)
|
||||||
|
} else if (
|
||||||
|
searchMode === SEARCH_MODE.SEMANTIC &&
|
||||||
|
searchSampleQueryIndex > 0 &&
|
||||||
|
searchSampleQueryIndex <= semanticSearchSampleQueries.length
|
||||||
|
) {
|
||||||
|
performSearch(semanticSearchSampleQueries[searchSampleQueryIndex - 1], searchMode)
|
||||||
|
} else if (
|
||||||
|
searchMode === SEARCH_MODE.KEYWORD &&
|
||||||
|
searchSampleQueryIndex > 0 &&
|
||||||
|
searchSampleQueryIndex <= keywordSearchSampleQueries.length
|
||||||
|
) {
|
||||||
|
performSearch(keywordSearchSampleQueries[searchSampleQueryIndex - 1].description, searchMode)
|
||||||
|
}
|
||||||
|
}, [performSearch, search, searchSampleQueryIndex, searchMode])
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'ctrl+k',
|
||||||
|
() => {
|
||||||
|
if (!showSearchModal) {
|
||||||
|
setShowSearchModal(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[showSearchModal]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isSemanticSearch = searchMode === SEARCH_MODE.SEMANTIC
|
||||||
|
const keywordSearchSampleQueries = [
|
||||||
|
{ keyword: `class`, description: getString('keywordSearch.sampleQueries.searchForClass') },
|
||||||
|
{ keyword: `class file:^cmd`, description: getString('keywordSearch.sampleQueries.searchForFilesWithCMD') },
|
||||||
|
{
|
||||||
|
keyword: `class or printf`,
|
||||||
|
description: getString('keywordSearch.sampleQueries.searchForPattern')
|
||||||
|
},
|
||||||
|
{ keyword: 'initial commit', description: getString('keywordSearch.sampleQueries.searchForInitialCommit') }
|
||||||
|
]
|
||||||
|
const semanticSearchSampleQueries = getString('semanticSearch.sampleQueries').split(',')
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
className={css.searchBox}
|
||||||
|
{...ButtonRoleProps}
|
||||||
|
onClick={() => {
|
||||||
|
setShowSearchModal(true)
|
||||||
|
}}>
|
||||||
|
<SearchInputWithSpinner readOnly placeholder={getString('codeSearch') + ` (ctrl-k)`} query={''} setQuery={noop} />
|
||||||
|
{isSemanticSearch && <img src={svg} width={95} height={22} />}
|
||||||
|
{showSearchModal && (
|
||||||
|
<Container onClick={Utils.stopEvent}>
|
||||||
|
<Dialog
|
||||||
|
className={css.searchModal}
|
||||||
|
backdropClassName={css.backdrop}
|
||||||
|
data-search-mode={searchMode}
|
||||||
|
portalClassName={css.portal}
|
||||||
|
isOpen={true}
|
||||||
|
enforceFocus={false}
|
||||||
|
onClose={() => {
|
||||||
|
setShowSearchModal(false)
|
||||||
|
}}>
|
||||||
|
<Container>
|
||||||
|
<Layout.Vertical spacing="large">
|
||||||
|
<Container>
|
||||||
|
<Layout.Horizontal className={css.layout}>
|
||||||
|
<Container className={css.searchContainer}>
|
||||||
|
<CodeSearchBar
|
||||||
|
searchMode={searchMode}
|
||||||
|
setSearchMode={setSearchMode}
|
||||||
|
value={search}
|
||||||
|
onChange={setSearch}
|
||||||
|
onSearch={onSearch}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (isSemanticSearch) {
|
||||||
|
if (!search?.trim()) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
setSearchSampleQueryIndex(index => {
|
||||||
|
return index + 1 > semanticSearchSampleQueries.length ? 1 : index + 1
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
setSearchSampleQueryIndex(index => {
|
||||||
|
return index - 1 > 0 ? index - 1 : semanticSearchSampleQueries.length
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!search?.trim()) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
setSearchSampleQueryIndex(index => {
|
||||||
|
return index + 1 > keywordSearchSampleQueries.length ? 1 : index + 1
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
setSearchSampleQueryIndex(index => {
|
||||||
|
return index - 1 > 0 ? index - 1 : keywordSearchSampleQueries.length
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Text className={css.sectionHeader}>
|
||||||
|
{isSemanticSearch ? getString('searchHeader') : getString('searchExamples')}
|
||||||
|
</Text>
|
||||||
|
<Container>
|
||||||
|
{isSemanticSearch
|
||||||
|
? semanticSearchSampleQueries.map((sampleQuery, index) => (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
className={cx(css.sampleSemanticSearchQuery, {
|
||||||
|
[css.selected]: index === searchSampleQueryIndex - 1
|
||||||
|
})}
|
||||||
|
{...ButtonRoleProps}
|
||||||
|
onClick={() => {
|
||||||
|
setSearch(sampleQuery)
|
||||||
|
}}>
|
||||||
|
{sampleQuery}
|
||||||
|
<LongArrowDownLeft color="" />
|
||||||
|
</Text>
|
||||||
|
))
|
||||||
|
: keywordSearchSampleQueries.map((sampleQuery: { keyword: string; description: string }) => {
|
||||||
|
const { keyword, description } = sampleQuery
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Horizontal
|
||||||
|
key={keyword}
|
||||||
|
className={cx(css.sampleKeywordSearchQuery, {
|
||||||
|
[css.selected]:
|
||||||
|
keywordSearchSampleQueries.indexOf(sampleQuery) === searchSampleQueryIndex - 1
|
||||||
|
})}
|
||||||
|
padding={'small'}
|
||||||
|
spacing={'xsmall'}
|
||||||
|
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
|
||||||
|
onClick={() => setSearch(keyword)}>
|
||||||
|
<Filter />
|
||||||
|
<Text color={Color.PRIMARY_7} font={{ weight: 'semi-bold' }}>
|
||||||
|
{keyword}
|
||||||
|
</Text>
|
||||||
|
<Text color={Color.GREY_600}>{description}</Text>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Container>
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Container>
|
||||||
|
</Dialog>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeSearch
|
@ -45,12 +45,12 @@ const SemanticSearch = ({ repoMetadata }: Pick<GitInfoProps, 'repoMetadata'>) =>
|
|||||||
(q: string) => {
|
(q: string) => {
|
||||||
history.push({
|
history.push({
|
||||||
pathname: routes.toCODESemanticSearch({
|
pathname: routes.toCODESemanticSearch({
|
||||||
repoPath: repoMetadata.path as string
|
repoPath: repoMetadata?.path as string
|
||||||
}),
|
}),
|
||||||
search: `q=${q}`
|
search: `q=${q}`
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[history, repoMetadata.path, routes]
|
[history, repoMetadata?.path, routes]
|
||||||
)
|
)
|
||||||
const onSearch = useCallback(() => {
|
const onSearch = useCallback(() => {
|
||||||
if (search?.trim()) {
|
if (search?.trim()) {
|
||||||
|
113
web/src/components/CodeSearchBar/CodeSearchBar.module.scss
Normal file
113
web/src/components/CodeSearchBar/CodeSearchBar.module.scss
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.searchCtn {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&[data-search-mode='keyword'] {
|
||||||
|
input {
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: transparent !important;
|
||||||
|
caret-color: var(--black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-search-mode='semantic'] {
|
||||||
|
input {
|
||||||
|
caret-color: var(--black);
|
||||||
|
border-color: var(--ai-purple-600) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:nth-child(2),
|
||||||
|
> div:nth-child(2) > div,
|
||||||
|
> div:nth-child(2) > div > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:nth-child(2) > div > div {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textCtn {
|
||||||
|
position: absolute;
|
||||||
|
left: 31px;
|
||||||
|
top: 8px;
|
||||||
|
color: black;
|
||||||
|
font-size: 13px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highltedText {
|
||||||
|
background-color: var(--primary-1);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--primary-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.andOr {
|
||||||
|
color: var(--red-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleBtn {
|
||||||
|
position: absolute;
|
||||||
|
right: 31px;
|
||||||
|
top: 7px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleHiddenBtn {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
right: 41px !important;
|
||||||
|
top: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
right: 49px;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 7.5px 14px;
|
||||||
|
color: transparent;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.bp3-control.bp3-switch input:checked ~ .bp3-control-indicator {
|
||||||
|
background-color: var(--ai-purple-600);
|
||||||
|
}
|
||||||
|
.bp3-control.bp3-switch input:checked ~ .bp3-control-indicator {
|
||||||
|
background-color: var(--ai-purple-800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggleTitle {
|
||||||
|
position: absolute;
|
||||||
|
right: 81px;
|
||||||
|
top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleLogo {
|
||||||
|
position: absolute;
|
||||||
|
right: 80px !important;
|
||||||
|
top: 3.7px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchIcon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 11px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
27
web/src/components/CodeSearchBar/CodeSearchBar.module.scss.d.ts
vendored
Normal file
27
web/src/components/CodeSearchBar/CodeSearchBar.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
// This is an auto-generated file
|
||||||
|
export declare const andOr: string
|
||||||
|
export declare const highltedText: string
|
||||||
|
export declare const searchCtn: string
|
||||||
|
export declare const searchIcon: string
|
||||||
|
export declare const textCtn: string
|
||||||
|
export declare const toggleBtn: string
|
||||||
|
export declare const toggleHiddenBtn: string
|
||||||
|
export declare const toggleLogo: string
|
||||||
|
export declare const toggleTitle: string
|
131
web/src/components/CodeSearchBar/CodeSearchBar.tsx
Normal file
131
web/src/components/CodeSearchBar/CodeSearchBar.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Dispatch, FC, SetStateAction } from 'react'
|
||||||
|
|
||||||
|
import cx from 'classnames'
|
||||||
|
import { Switch } from '@blueprintjs/core'
|
||||||
|
import { Text } from '@harnessio/uicore'
|
||||||
|
import { Color, FontVariation } from '@harnessio/design-system'
|
||||||
|
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
|
||||||
|
import { useAppContext } from 'AppContext'
|
||||||
|
import { useStrings } from 'framework/strings'
|
||||||
|
import { SEARCH_MODE } from 'components/CodeSearch/CodeSearch'
|
||||||
|
import svg from '../CodeSearch/search-background.svg'
|
||||||
|
import css from './CodeSearchBar.module.scss'
|
||||||
|
|
||||||
|
interface CodeSearchBarProps {
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
onSearch?: (searchTerm: string) => void
|
||||||
|
onKeyDown?: (e: React.KeyboardEvent<HTMLElement>) => void
|
||||||
|
setSearchMode: Dispatch<SetStateAction<SEARCH_MODE>>
|
||||||
|
searchMode: SEARCH_MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYWORD_REGEX = /((?:(?:-{0,1})(?:repo|lang|file|case|count)):\S*|(?: or|and ))/gi
|
||||||
|
|
||||||
|
const CodeSearchBar: FC<CodeSearchBarProps> = ({ value, onChange, onSearch, onKeyDown, searchMode, setSearchMode }) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { hooks } = useAppContext()
|
||||||
|
const { SEMANTIC_SEARCH_ENABLED: isSemanticSearchEnabled } = hooks?.useFeatureFlags()
|
||||||
|
const isSemanticMode = isSemanticSearchEnabled && searchMode === SEARCH_MODE.SEMANTIC
|
||||||
|
return (
|
||||||
|
<div className={css.searchCtn} data-search-mode={searchMode}>
|
||||||
|
<div className={css.textCtn}>
|
||||||
|
{!isSemanticMode &&
|
||||||
|
value.split(KEYWORD_REGEX).map(text => {
|
||||||
|
const isMatch = text.match(KEYWORD_REGEX)
|
||||||
|
|
||||||
|
if (text.match(/ or|and /gi)) {
|
||||||
|
return (
|
||||||
|
<span key={text} className={css.andOr}>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const separatorIdx = text.indexOf(':')
|
||||||
|
const [keyWord, keyVal] = [text.slice(0, separatorIdx), text.slice(separatorIdx + 1)]
|
||||||
|
|
||||||
|
if (isMatch) {
|
||||||
|
if (keyWord.match(/ or|and /gi)) {
|
||||||
|
return (
|
||||||
|
<span key={keyWord} className={css.andOr}>
|
||||||
|
{keyWord}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{keyWord}:<span className={css.highltedText}>{keyVal}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<SearchInputWithSpinner
|
||||||
|
width={'100%' as any}
|
||||||
|
query={value}
|
||||||
|
spinnerPosition="right"
|
||||||
|
setQuery={onChange}
|
||||||
|
onSearch={onSearch}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
placeholder={isSemanticMode ? getString('codeSearchModal') : getString('keywordSearchPlaceholder')}
|
||||||
|
/>
|
||||||
|
{isSemanticSearchEnabled && (
|
||||||
|
<>
|
||||||
|
{isSemanticMode ? (
|
||||||
|
<img className={cx(css.toggleLogo)} src={svg} width={122} height={25} />
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
className={cx(css.toggleTitle)}
|
||||||
|
font={{ variation: FontVariation.SMALL_SEMI }}
|
||||||
|
color={Color.GREY_300}
|
||||||
|
style={{ whiteSpace: 'nowrap' }}>
|
||||||
|
{getString('enableAISearch')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSemanticSearchEnabled && (
|
||||||
|
<Switch
|
||||||
|
onChange={() => {
|
||||||
|
searchMode === SEARCH_MODE.KEYWORD
|
||||||
|
? setSearchMode(SEARCH_MODE.SEMANTIC)
|
||||||
|
: setSearchMode(SEARCH_MODE.KEYWORD)
|
||||||
|
}}
|
||||||
|
className={cx(css.toggleBtn)}
|
||||||
|
checked={SEARCH_MODE.SEMANTIC === searchMode}></Switch>
|
||||||
|
)}
|
||||||
|
{isSemanticSearchEnabled && (
|
||||||
|
<button
|
||||||
|
className={cx(css.toggleHiddenBtn, css.toggleBtn)}
|
||||||
|
onClick={() =>
|
||||||
|
searchMode === SEARCH_MODE.KEYWORD
|
||||||
|
? setSearchMode(SEARCH_MODE.SEMANTIC)
|
||||||
|
: setSearchMode(SEARCH_MODE.KEYWORD)
|
||||||
|
}></button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeSearchBar
|
@ -51,4 +51,17 @@
|
|||||||
.andOr {
|
.andOr {
|
||||||
color: var(--red-700);
|
color: var(--red-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggleSearch {
|
||||||
|
position: absolute;
|
||||||
|
right: 14px;
|
||||||
|
top: 6px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleTitle {
|
||||||
|
position: absolute;
|
||||||
|
right: 53px;
|
||||||
|
top: 9px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,3 +20,5 @@ export declare const andOr: string
|
|||||||
export declare const highltedText: string
|
export declare const highltedText: string
|
||||||
export declare const searchCtn: string
|
export declare const searchCtn: string
|
||||||
export declare const textCtn: string
|
export declare const textCtn: string
|
||||||
|
export declare const toggleSearch: string
|
||||||
|
export declare const toggleTitle: string
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
* Use the command `yarn strings` to regenerate this file.
|
* Use the command `yarn strings` to regenerate this file.
|
||||||
*/
|
*/
|
||||||
export interface StringsMap {
|
export interface StringsMap {
|
||||||
|
AIDA: string
|
||||||
Enable: string
|
Enable: string
|
||||||
accessControl: string
|
accessControl: string
|
||||||
accountEmail: string
|
accountEmail: string
|
||||||
@ -251,6 +252,7 @@ export interface StringsMap {
|
|||||||
emptyRepoHeader: string
|
emptyRepoHeader: string
|
||||||
emptyRepoInclude: string
|
emptyRepoInclude: string
|
||||||
emptySpaceText: string
|
emptySpaceText: string
|
||||||
|
enableAISearch: string
|
||||||
enableSSLVerification: string
|
enableSSLVerification: string
|
||||||
enableWebhookContent: string
|
enableWebhookContent: string
|
||||||
enableWebhookTitle: string
|
enableWebhookTitle: string
|
||||||
@ -401,6 +403,11 @@ export interface StringsMap {
|
|||||||
inactiveBranches: string
|
inactiveBranches: string
|
||||||
isRequired: string
|
isRequired: string
|
||||||
key: string
|
key: string
|
||||||
|
'keywordSearch.sampleQueries.searchForClass': string
|
||||||
|
'keywordSearch.sampleQueries.searchForFilesWithCMD': string
|
||||||
|
'keywordSearch.sampleQueries.searchForInitialCommit': string
|
||||||
|
'keywordSearch.sampleQueries.searchForPattern': string
|
||||||
|
keywordSearchPlaceholder: string
|
||||||
killed: string
|
killed: string
|
||||||
language: string
|
language: string
|
||||||
leaveAComment: string
|
leaveAComment: string
|
||||||
@ -753,6 +760,7 @@ export interface StringsMap {
|
|||||||
selectStatuses: string
|
selectStatuses: string
|
||||||
selectToViewMore: string
|
selectToViewMore: string
|
||||||
selectUsers: string
|
selectUsers: string
|
||||||
|
'semanticSearch.sampleQueries': string
|
||||||
setAsAdmin: string
|
setAsAdmin: string
|
||||||
setting: string
|
setting: string
|
||||||
settings: string
|
settings: string
|
||||||
|
19
web/src/hooks/useFeatureFlag.ts
Normal file
19
web/src/hooks/useFeatureFlag.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Harness, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function useFeatureFlags<T = Record<string, boolean>>() {
|
||||||
|
return {} as T
|
||||||
|
}
|
@ -725,11 +725,23 @@ userUpdateSuccess: 'User updated successfully'
|
|||||||
viewFile: View File
|
viewFile: View File
|
||||||
searchResult: 'Search Result {count}'
|
searchResult: 'Search Result {count}'
|
||||||
aiSearch: AIDA SEARCH
|
aiSearch: AIDA SEARCH
|
||||||
|
enableAISearch: Enable AI Search
|
||||||
|
AIDA: AIDA
|
||||||
|
keywordSearch:
|
||||||
|
sampleQueries:
|
||||||
|
searchForClass: Search for class
|
||||||
|
searchForFilesWithCMD: Search for class in files starting with cmd
|
||||||
|
searchForPattern: Include only results from file paths matching the given search pattern
|
||||||
|
searchForInitialCommit: Search for exact phrase initial commit
|
||||||
|
|
||||||
|
keywordSearchPlaceholder: Search for code or files...
|
||||||
codeSearch: Code Search
|
codeSearch: Code Search
|
||||||
codeSearchModal: Begin search by describing what you are looking for
|
codeSearchModal: Begin search by describing what you are looking for
|
||||||
searchHeader: 'Here are some example to get you start:'
|
searchHeader: 'Here are some example to get you start:'
|
||||||
startSearching: Begin search by describing what you are looking for.
|
startSearching: Begin search by describing what you are looking for.
|
||||||
poweredByAI: Unlock the power of AI with Semantic Code search. Try phrases like "Locate the code for authentication".
|
poweredByAI: Unlock the power of AI with Semantic Code search. Try phrases like "Locate the code for authentication".
|
||||||
|
semanticSearch:
|
||||||
|
sampleQueries: Where is the code that handles authentication?,Where is the application entry point?,Where do we configure the logger?
|
||||||
failedToFetchFileContent: 'ERROR: Failed to fetch file content.'
|
failedToFetchFileContent: 'ERROR: Failed to fetch file content.'
|
||||||
run: Run
|
run: Run
|
||||||
plugins:
|
plugins:
|
||||||
|
@ -27,9 +27,10 @@ import { CloneButtonTooltip } from 'components/CloneButtonTooltip/CloneButtonToo
|
|||||||
import { CodeIcon, GitInfoProps, isDir, isGitRev, isRefATag } from 'utils/GitUtils'
|
import { CodeIcon, GitInfoProps, isDir, isGitRev, isRefATag } from 'utils/GitUtils'
|
||||||
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
||||||
import { useCreateBranchModal } from 'components/CreateBranchModal/CreateBranchModal'
|
import { useCreateBranchModal } from 'components/CreateBranchModal/CreateBranchModal'
|
||||||
import KeywordSearch from 'components/CodeSearch/KeywordSearch'
|
// import KeywordSearch from 'components/CodeSearch/KeywordSearch'
|
||||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||||
import { permissionProps } from 'utils/Utils'
|
import { permissionProps } from 'utils/Utils'
|
||||||
|
import CodeSearch from 'components/CodeSearch/CodeSearch'
|
||||||
import css from './ContentHeader.module.scss'
|
import css from './ContentHeader.module.scss'
|
||||||
|
|
||||||
export function ContentHeader({
|
export function ContentHeader({
|
||||||
@ -156,7 +157,7 @@ export function ContentHeader({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
<div className={css.searchBoxCtn}>{!standalone ? <KeywordSearch repoMetadata={repoMetadata} /> : null}</div>
|
<div className={css.searchBoxCtn}>{!standalone ? <CodeSearch repoMetadata={repoMetadata} /> : null}</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ import { useMutate } from 'restful-react'
|
|||||||
import { debounce, escapeRegExp, flatten, sortBy, uniq } from 'lodash-es'
|
import { debounce, escapeRegExp, flatten, sortBy, uniq } from 'lodash-es'
|
||||||
import Keywords from 'react-keywords'
|
import Keywords from 'react-keywords'
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { useQueryParams } from 'hooks/useQueryParams'
|
import { useQueryParams } from 'hooks/useQueryParams'
|
||||||
@ -47,34 +47,55 @@ import { Editor } from 'components/Editor/Editor'
|
|||||||
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
||||||
import KeywordSearchbar from 'components/KeywordSearchbar/KeywordSearchbar'
|
import KeywordSearchbar from 'components/KeywordSearchbar/KeywordSearchbar'
|
||||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||||
|
import { SEARCH_MODE } from 'components/CodeSearch/CodeSearch'
|
||||||
|
import CodeSearchBar from 'components/CodeSearchBar/CodeSearchBar'
|
||||||
import KeywordSearchFilters from './KeywordSearchFilters'
|
import KeywordSearchFilters from './KeywordSearchFilters'
|
||||||
import type { FileMatch, KeywordSearchResponse } from './KeywordSearch.types'
|
import type { FileMatch, KeywordSearchResponse } from './CodeSearchPage.types'
|
||||||
|
|
||||||
import css from './Search.module.scss'
|
import css from './Search.module.scss'
|
||||||
|
|
||||||
|
type SemanticSearchResultType = {
|
||||||
|
commit: string
|
||||||
|
file_path: string
|
||||||
|
start_line: number
|
||||||
|
end_line: number
|
||||||
|
file_name: string
|
||||||
|
lines: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// COMMON
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const space = useGetSpaceParam()
|
const space = useGetSpaceParam()
|
||||||
const { repoName } = useGetRepositoryMetadata()
|
const { repoName, repoMetadata } = useGetRepositoryMetadata()
|
||||||
const { updateQueryParams } = useUpdateQueryParams()
|
const { updateQueryParams } = useUpdateQueryParams()
|
||||||
const { showError } = useToaster()
|
const { showError } = useToaster()
|
||||||
|
|
||||||
const repoPath = repoName ? `${space}/${repoName}` : undefined
|
const repoPath = repoName ? `${space}/${repoName}` : undefined
|
||||||
|
|
||||||
const { q } = useQueryParams<{ q: string }>()
|
const { q, mode } = useQueryParams<{ q: string; mode: SEARCH_MODE }>()
|
||||||
const [searchTerm, setSearchTerm] = useState(q || '')
|
const [searchTerm, setSearchTerm] = useState(q || '')
|
||||||
|
const [searchMode, setSearchMode] = useState(mode)
|
||||||
const [selectedRepositories, setSelectedRepositories] = useState<SelectOption[]>([])
|
const [selectedRepositories, setSelectedRepositories] = useState<SelectOption[]>([])
|
||||||
const [selectedLanguages, setSelectedLanguages] = useState<SelectOption[]>([])
|
const [selectedLanguages, setSelectedLanguages] = useState<(SelectOption & { extension?: string })[]>([])
|
||||||
|
const [keywordSearchResults, setKeyowordSearchResults] = useState<KeywordSearchResponse>()
|
||||||
const [searchResults, setSearchResults] = useState<KeywordSearchResponse>()
|
|
||||||
|
|
||||||
|
//semantic
|
||||||
|
// const [loadingSearch, setLoadingSearch] = useState(false)
|
||||||
|
const [semanticSearchResult, setSemanticSearchResult] = useState<SemanticSearchResultType[]>([])
|
||||||
|
const [uniqueFiles, setUniqueFiles] = useState(0)
|
||||||
|
const history = useHistory()
|
||||||
|
//
|
||||||
const { mutate, loading: isSearching } = useMutate<KeywordSearchResponse>({
|
const { mutate, loading: isSearching } = useMutate<KeywordSearchResponse>({
|
||||||
path: `/api/v1/search`,
|
path: `/api/v1/search`,
|
||||||
verb: 'POST'
|
verb: 'POST'
|
||||||
})
|
})
|
||||||
|
const {
|
||||||
|
mutate: sendSemanticSearch,
|
||||||
|
loading: loadingSearch,
|
||||||
|
cancel: cancelPreviousSearch
|
||||||
|
} = useMutate<SemanticSearchResultType[]>({
|
||||||
|
verb: 'POST',
|
||||||
|
path: `/api/v1/repos/${repoMetadata?.path}/+/semantic/search`
|
||||||
|
})
|
||||||
const debouncedSearch = useCallback(
|
const debouncedSearch = useCallback(
|
||||||
debounce(async (text: string) => {
|
debounce(async (text: string) => {
|
||||||
try {
|
try {
|
||||||
@ -105,65 +126,141 @@ const Search = () => {
|
|||||||
max_result_count: maxResultCount
|
max_result_count: maxResultCount
|
||||||
})
|
})
|
||||||
|
|
||||||
setSearchResults(res)
|
setKeyowordSearchResults(res)
|
||||||
} else {
|
} else {
|
||||||
setSearchResults(undefined)
|
setKeyowordSearchResults(undefined)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(getErrorMessage(error))
|
showError(getErrorMessage(error))
|
||||||
}
|
}
|
||||||
}, 300),
|
}, 300),
|
||||||
[selectedLanguages, selectedRepositories, repoPath]
|
[selectedLanguages, selectedRepositories, repoPath, mode]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const performSemanticSearch = useCallback(() => {
|
||||||
if (searchTerm) {
|
// setLoadingSearch(true)
|
||||||
debouncedSearch(searchTerm)
|
// history.replace({ pathname: location.pathname, search: `q=${searchTerm}` })
|
||||||
}
|
|
||||||
}, [selectedLanguages, selectedRepositories])
|
|
||||||
|
|
||||||
|
sendSemanticSearch({ query: searchTerm })
|
||||||
|
.then(response => {
|
||||||
|
setSemanticSearchResult(response)
|
||||||
|
const countUniqueFiles = () => new Set(response.map(item => item.file_path)).size
|
||||||
|
setUniqueFiles(countUniqueFiles)
|
||||||
|
})
|
||||||
|
.catch(exception => {
|
||||||
|
showError(getErrorMessage(exception), 0)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// setLoadingSearch(false)
|
||||||
|
})
|
||||||
|
}, [searchTerm, history, location, repoPath, sendSemanticSearch, showError, mode])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (q && mode !== SEARCH_MODE.SEMANTIC) {
|
||||||
|
debouncedSearch(searchTerm)
|
||||||
|
} else if (searchTerm && repoMetadata?.path && mode === SEARCH_MODE.SEMANTIC) {
|
||||||
|
performSemanticSearch()
|
||||||
|
}
|
||||||
|
}, [selectedLanguages, selectedRepositories, repoMetadata?.path])
|
||||||
return (
|
return (
|
||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
<Container padding="medium" border={{ bottom: true }} flex className={css.header}>
|
<Container padding="medium" border={{ bottom: true }} flex className={css.header}>
|
||||||
<KeywordSearchbar
|
{repoName ? (
|
||||||
value={searchTerm}
|
<CodeSearchBar
|
||||||
onChange={text => {
|
searchMode={searchMode}
|
||||||
setSearchTerm(text)
|
setSearchMode={setSearchMode}
|
||||||
}}
|
value={searchTerm}
|
||||||
onSearch={text => {
|
onChange={text => {
|
||||||
setSearchResults(undefined)
|
setSearchTerm(text)
|
||||||
updateQueryParams({ q: text })
|
}}
|
||||||
debouncedSearch(text)
|
onSearch={text => {
|
||||||
}}
|
cancelPreviousSearch()
|
||||||
/>
|
setKeyowordSearchResults(undefined)
|
||||||
|
setSemanticSearchResult([])
|
||||||
|
updateQueryParams({ q: text, mode: searchMode })
|
||||||
|
if (searchMode === SEARCH_MODE.SEMANTIC) {
|
||||||
|
performSemanticSearch()
|
||||||
|
} else {
|
||||||
|
debouncedSearch(text)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<KeywordSearchbar
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={text => {
|
||||||
|
setSearchTerm(text)
|
||||||
|
}}
|
||||||
|
onSearch={text => {
|
||||||
|
setKeyowordSearchResults(undefined)
|
||||||
|
updateQueryParams({ q: text })
|
||||||
|
debouncedSearch(text)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
<Container padding="xlarge">
|
<Container padding="xlarge">
|
||||||
<LoadingSpinner visible={isSearching} />
|
<LoadingSpinner className={css.loadingSpinner} visible={isSearching || loadingSearch} />
|
||||||
<KeywordSearchFilters
|
{keywordSearchResults && (
|
||||||
isRepoLevelSearch={Boolean(repoName)}
|
<KeywordSearchFilters
|
||||||
selectedLanguages={selectedLanguages}
|
isRepoLevelSearch={Boolean(repoName)}
|
||||||
selectedRepositories={selectedRepositories}
|
selectedLanguages={selectedLanguages}
|
||||||
setLanguages={setSelectedLanguages}
|
selectedRepositories={selectedRepositories}
|
||||||
setRepositories={setSelectedRepositories}
|
setLanguages={setSelectedLanguages}
|
||||||
/>
|
setRepositories={setSelectedRepositories}
|
||||||
{searchResults?.file_matches.length ? (
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{keywordSearchResults?.file_matches.length ? (
|
||||||
<>
|
<>
|
||||||
<Layout.Horizontal spacing="xsmall" margin={{ bottom: 'large' }}>
|
<Layout.Horizontal spacing="xsmall" margin={{ bottom: 'large' }}>
|
||||||
<Text font={{ variation: FontVariation.SMALL_SEMI }}>
|
<Text font={{ variation: FontVariation.SMALL_SEMI }}>
|
||||||
{searchResults?.stats.total_files} {getString('files')}
|
{keywordSearchResults?.stats.total_files} {getString('files')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text font={{ variation: FontVariation.SMALL_SEMI }} color={Color.GREY_400}>
|
<Text font={{ variation: FontVariation.SMALL_SEMI }} color={Color.GREY_400}>
|
||||||
{getString('results')}
|
{getString('results')}
|
||||||
</Text>
|
</Text>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
{searchResults?.file_matches?.map(fileMatch => {
|
{keywordSearchResults?.file_matches?.map(fileMatch => {
|
||||||
if (fileMatch.matches.length) {
|
if (fileMatch.matches.length) {
|
||||||
return <SearchResult key={fileMatch.file_name} fileMatch={fileMatch} searchTerm={searchTerm} />
|
return <SearchResult key={fileMatch.file_name} fileMatch={fileMatch} searchTerm={searchTerm} />
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<NoResultCard showWhen={() => !isSearching && !searchResults?.file_matches?.length} forSearch={true} />
|
{/* semantic search results -> */}
|
||||||
|
{semanticSearchResult?.length ? (
|
||||||
|
<>
|
||||||
|
<Layout.Horizontal spacing="xsmall" margin={{ bottom: 'large' }}>
|
||||||
|
{!loadingSearch && (
|
||||||
|
<Text font={{ variation: FontVariation.SMALL_SEMI }}>
|
||||||
|
{uniqueFiles} {getString('files')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text font={{ variation: FontVariation.SMALL_SEMI }} color={Color.GREY_400}>
|
||||||
|
{getString('results')}
|
||||||
|
</Text>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
{loadingSearch ? (
|
||||||
|
<Text></Text>
|
||||||
|
) : (
|
||||||
|
<Text>
|
||||||
|
{semanticSearchResult.map((result, index) => (
|
||||||
|
<SemanticSearchResult key={index} result={result} index={index} />
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<NoResultCard
|
||||||
|
showWhen={() =>
|
||||||
|
!isSearching &&
|
||||||
|
!keywordSearchResults?.file_matches?.length &&
|
||||||
|
!loadingSearch &&
|
||||||
|
!semanticSearchResult?.length
|
||||||
|
}
|
||||||
|
forSearch={true}
|
||||||
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@ -171,6 +268,7 @@ const Search = () => {
|
|||||||
|
|
||||||
export default Search
|
export default Search
|
||||||
|
|
||||||
|
// KEYOWORD SEARCH CODE
|
||||||
interface CodeBlock {
|
interface CodeBlock {
|
||||||
lineNumberOffset: number
|
lineNumberOffset: number
|
||||||
codeBlock: string
|
codeBlock: string
|
||||||
@ -232,7 +330,6 @@ export const SearchResult = ({ fileMatch, searchTerm }: { fileMatch: FileMatch;
|
|||||||
|
|
||||||
const flattenedMatches = flatten(codeBlocks.map(codeBlock => codeBlock.fragmentMatches))
|
const flattenedMatches = flatten(codeBlocks.map(codeBlock => codeBlock.fragmentMatches))
|
||||||
const allFileMatches = isCaseSensitive ? flattenedMatches : uniq(flattenedMatches.map(match => match.toLowerCase()))
|
const allFileMatches = isCaseSensitive ? flattenedMatches : uniq(flattenedMatches.map(match => match.toLowerCase()))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.searchResult}>
|
<Container className={css.searchResult}>
|
||||||
<Layout.Horizontal spacing="small" className={css.resultHeader}>
|
<Layout.Horizontal spacing="small" className={css.resultHeader}>
|
||||||
@ -394,3 +491,110 @@ const CodeBlock = ({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SEMANTIC SEARCH CODE
|
||||||
|
|
||||||
|
interface SemanticCodeBlock {
|
||||||
|
lineNumberOffset: number
|
||||||
|
codeBlock: string
|
||||||
|
result: SemanticSearchResultType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SemanticSearchResult = ({ result, index }: { result: SemanticSearchResultType; index: number }) => {
|
||||||
|
const { routes } = useAppContext()
|
||||||
|
const { gitRef, repoName, repoMetadata } = useGetRepositoryMetadata()
|
||||||
|
const [isCollapsed, setIsCollapsed] = useToggle(false)
|
||||||
|
const [showMoreMatchs, setShowMoreMatches] = useState(false)
|
||||||
|
const totalLines = result.end_line - result.start_line + 1
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const showLines = totalLines > 10 ? (showMoreMatchs ? result.lines : result.lines.slice(0, 10)) : result.lines
|
||||||
|
return (
|
||||||
|
<Container className={css.searchResult}>
|
||||||
|
<Layout.Horizontal spacing="small" className={cx(css.resultHeader)}>
|
||||||
|
<Icon name={isCollapsed ? 'chevron-up' : 'chevron-down'} {...ButtonRoleProps} onClick={setIsCollapsed} />
|
||||||
|
<Link to={routes.toCODERepository({ repoPath: repoMetadata?.path as string })}>
|
||||||
|
<Text
|
||||||
|
icon="code-repo"
|
||||||
|
font={{ variation: FontVariation.SMALL_SEMI }}
|
||||||
|
color={Color.GREY_500}
|
||||||
|
border={{ right: true }}
|
||||||
|
padding={{ right: 'small' }}>
|
||||||
|
{repoName}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to={routes.toCODERepository({
|
||||||
|
repoPath: repoMetadata?.path as string,
|
||||||
|
gitRef: gitRef,
|
||||||
|
resourcePath: result.file_path
|
||||||
|
})}>
|
||||||
|
<Text font={{ variation: FontVariation.SMALL_BOLD }} color={Color.PRIMARY_7}>
|
||||||
|
{result.file_path}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
<span className={css.semanticStamp}>{getString('AIDA')}</span>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
<div className={cx({ [css.isCollapsed]: isCollapsed })}>
|
||||||
|
<SemanticCodeBlock
|
||||||
|
key={`${result.file_name}_${showLines}_${index}`}
|
||||||
|
result={result}
|
||||||
|
showMoreMatchesFooter={totalLines > 10}
|
||||||
|
showMoreLines={showMoreMatchs}
|
||||||
|
setShowMoreMatches={setShowMoreMatches}
|
||||||
|
showLines={showLines}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SemanticCodeBlock = ({
|
||||||
|
showMoreMatchesFooter,
|
||||||
|
result,
|
||||||
|
showMoreLines,
|
||||||
|
setShowMoreMatches,
|
||||||
|
showLines
|
||||||
|
}: {
|
||||||
|
showMoreMatchesFooter: boolean
|
||||||
|
result: SemanticSearchResultType
|
||||||
|
showMoreLines: boolean
|
||||||
|
setShowMoreMatches: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
showLines: string[]
|
||||||
|
}) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const totalLines = result.end_line - result.start_line + 1
|
||||||
|
if (totalLines === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Editor
|
||||||
|
inGitBlame
|
||||||
|
content={showLines.join('\n')}
|
||||||
|
standalone={true}
|
||||||
|
readonly={true}
|
||||||
|
repoMetadata={undefined}
|
||||||
|
filename={result.file_name}
|
||||||
|
extensions={[
|
||||||
|
lineNumbers({
|
||||||
|
formatNumber: n => String(n - 1 + result.start_line)
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
className={css.editorCtn}
|
||||||
|
/>
|
||||||
|
{showMoreMatchesFooter ? (
|
||||||
|
<Layout.Horizontal
|
||||||
|
spacing="small"
|
||||||
|
className={css.showMoreCtn}
|
||||||
|
onClick={() => setShowMoreMatches(prevVal => !prevVal)}
|
||||||
|
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
|
||||||
|
{...ButtonRoleProps}>
|
||||||
|
{!showMoreLines ? <Plus /> : <Minus />}
|
||||||
|
<Text font={{ variation: FontVariation.TINY_SEMI }} color={Color.GREY_400}>
|
||||||
|
{!showMoreLines ? `Show ${totalLines - 10} more lines` : getString('showLessMatches')}
|
||||||
|
</Text>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -23,6 +23,10 @@
|
|||||||
min-height: var(--page-height);
|
min-height: var(--page-height);
|
||||||
background-color: var(--primary-bg) !important;
|
background-color: var(--primary-bg) !important;
|
||||||
|
|
||||||
|
.loadingSpinner {
|
||||||
|
width: 77% !important;
|
||||||
|
}
|
||||||
|
|
||||||
.pageHeader {
|
.pageHeader {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
@ -282,6 +286,19 @@
|
|||||||
padding: var(--spacing-small);
|
padding: var(--spacing-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.semanticStamp {
|
||||||
|
background-color: var(--ai-purple-200);
|
||||||
|
color: var(--ai-purple-600);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
margin-left: auto !important;
|
||||||
|
padding: 3px 7px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: Inter;
|
||||||
|
letter-spacing: 0.34px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.showMoreCtn {
|
.showMoreCtn {
|
||||||
background-color: var(--grey-50) !important;
|
background-color: var(--grey-50) !important;
|
||||||
border: 1px solid var(--grey-100) !important;
|
border: 1px solid var(--grey-100) !important;
|
||||||
|
2
web/src/pages/Search/Search.module.scss.d.ts
vendored
2
web/src/pages/Search/Search.module.scss.d.ts
vendored
@ -27,6 +27,7 @@ export declare const highlight: string
|
|||||||
export declare const highlightLineNumber: string
|
export declare const highlightLineNumber: string
|
||||||
export declare const isCollapsed: string
|
export declare const isCollapsed: string
|
||||||
export declare const layout: string
|
export declare const layout: string
|
||||||
|
export declare const loadingSpinner: string
|
||||||
export declare const main: string
|
export declare const main: string
|
||||||
export declare const multiSelect: string
|
export declare const multiSelect: string
|
||||||
export declare const noResult: string
|
export declare const noResult: string
|
||||||
@ -41,6 +42,7 @@ export declare const resultTitle: string
|
|||||||
export declare const searchBox: string
|
export declare const searchBox: string
|
||||||
export declare const searchResult: string
|
export declare const searchResult: string
|
||||||
export declare const selected: string
|
export declare const selected: string
|
||||||
|
export declare const semanticStamp: string
|
||||||
export declare const showMoreCtn: string
|
export declare const showMoreCtn: string
|
||||||
export declare const split: string
|
export declare const split: string
|
||||||
export declare const texts: string
|
export declare const texts: string
|
||||||
|
Loading…
Reference in New Issue
Block a user