diff --git a/web/config/moduleFederation.config.js b/web/config/moduleFederation.config.js index 83d2c77f0..3679e49a0 100644 --- a/web/config/moduleFederation.config.js +++ b/web/config/moduleFederation.config.js @@ -51,7 +51,7 @@ module.exports = { './Settings': './src/pages/RepositorySettings/RepositorySettings.tsx', './Webhooks': './src/pages/Webhooks/Webhooks.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', './NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx' }, diff --git a/web/src/AppProps.ts b/web/src/AppProps.ts index 83363c090..71a37d301 100644 --- a/web/src/AppProps.ts +++ b/web/src/AppProps.ts @@ -66,6 +66,7 @@ export interface AppProps { useExecutionDataHook: Unknown useLogsContent: Unknown useLogsStreaming: Unknown + useFeatureFlags: Unknown }> currentUser: Required diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx index 785900d8a..05f147a03 100644 --- a/web/src/RouteDestinations.tsx +++ b/web/src/RouteDestinations.tsx @@ -47,7 +47,7 @@ import ExecutionList from 'pages/ExecutionList/ExecutionList' import Execution from 'pages/Execution/Execution' import Secret from 'pages/Secret/Secret' 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 { useAppContext } from 'AppContext' 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 })]} exact> - + diff --git a/web/src/bootstrap.tsx b/web/src/bootstrap.tsx index b66577d32..33ed2079f 100644 --- a/web/src/bootstrap.tsx +++ b/web/src/bootstrap.tsx @@ -19,6 +19,7 @@ import ReactDOM from 'react-dom' import { noop } from 'lodash-es' import { routes } from 'RouteDefinitions' import { defaultCurrentUser } from 'AppContext' +import { useFeatureFlags } from 'hooks/useFeatureFlag' import App from './App' import './bootstrap.scss' @@ -36,7 +37,8 @@ ReactDOM.render( usePermissionTranslate: noop, useExecutionDataHook: noop, useLogsContent: noop, - useLogsStreaming: noop + useLogsStreaming: noop, + useFeatureFlags: useFeatureFlags }} currentUser={defaultCurrentUser} currentUserProfileURL="" diff --git a/web/src/components/CodeSearch/CodeSearch.module.scss b/web/src/components/CodeSearch/CodeSearch.module.scss new file mode 100644 index 000000000..2e8878eb4 --- /dev/null +++ b/web/src/components/CodeSearch/CodeSearch.module.scss @@ -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; + } + } +} diff --git a/web/src/components/CodeSearch/CodeSearch.module.scss.d.ts b/web/src/components/CodeSearch/CodeSearch.module.scss.d.ts new file mode 100644 index 000000000..ee72090b3 --- /dev/null +++ b/web/src/components/CodeSearch/CodeSearch.module.scss.d.ts @@ -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 diff --git a/web/src/components/CodeSearch/CodeSearch.tsx b/web/src/components/CodeSearch/CodeSearch.tsx new file mode 100644 index 000000000..ee48efff0 --- /dev/null +++ b/web/src/components/CodeSearch/CodeSearch.tsx @@ -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(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 ( + { + setShowSearchModal(true) + }}> + + {isSemanticSearch && } + {showSearchModal && ( + + { + setShowSearchModal(false) + }}> + + + + + + { + 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 + } + } + } + }} + /> + + + + + + {isSemanticSearch ? getString('searchHeader') : getString('searchExamples')} + + + {isSemanticSearch + ? semanticSearchSampleQueries.map((sampleQuery, index) => ( + { + setSearch(sampleQuery) + }}> + {sampleQuery} + + + )) + : keywordSearchSampleQueries.map((sampleQuery: { keyword: string; description: string }) => { + const { keyword, description } = sampleQuery + + return ( + setSearch(keyword)}> + + + {keyword} + + {description} + + ) + })} + + + + + + )} + + ) +} + +export default CodeSearch diff --git a/web/src/components/CodeSearch/SemanticSearch.tsx b/web/src/components/CodeSearch/SemanticSearch.tsx index 06deb7688..198064c03 100644 --- a/web/src/components/CodeSearch/SemanticSearch.tsx +++ b/web/src/components/CodeSearch/SemanticSearch.tsx @@ -45,12 +45,12 @@ const SemanticSearch = ({ repoMetadata }: Pick) => (q: string) => { history.push({ pathname: routes.toCODESemanticSearch({ - repoPath: repoMetadata.path as string + repoPath: repoMetadata?.path as string }), search: `q=${q}` }) }, - [history, repoMetadata.path, routes] + [history, repoMetadata?.path, routes] ) const onSearch = useCallback(() => { if (search?.trim()) { diff --git a/web/src/components/CodeSearchBar/CodeSearchBar.module.scss b/web/src/components/CodeSearchBar/CodeSearchBar.module.scss new file mode 100644 index 000000000..f5c302e63 --- /dev/null +++ b/web/src/components/CodeSearchBar/CodeSearchBar.module.scss @@ -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; + } +} diff --git a/web/src/components/CodeSearchBar/CodeSearchBar.module.scss.d.ts b/web/src/components/CodeSearchBar/CodeSearchBar.module.scss.d.ts new file mode 100644 index 000000000..0949cd81e --- /dev/null +++ b/web/src/components/CodeSearchBar/CodeSearchBar.module.scss.d.ts @@ -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 diff --git a/web/src/components/CodeSearchBar/CodeSearchBar.tsx b/web/src/components/CodeSearchBar/CodeSearchBar.tsx new file mode 100644 index 000000000..f396a2d7c --- /dev/null +++ b/web/src/components/CodeSearchBar/CodeSearchBar.tsx @@ -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) => void + setSearchMode: Dispatch> + searchMode: SEARCH_MODE +} + +const KEYWORD_REGEX = /((?:(?:-{0,1})(?:repo|lang|file|case|count)):\S*|(?: or|and ))/gi + +const CodeSearchBar: FC = ({ 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 ( +
+
+ {!isSemanticMode && + value.split(KEYWORD_REGEX).map(text => { + const isMatch = text.match(KEYWORD_REGEX) + + if (text.match(/ or|and /gi)) { + return ( + + {text} + + ) + } + + const separatorIdx = text.indexOf(':') + const [keyWord, keyVal] = [text.slice(0, separatorIdx), text.slice(separatorIdx + 1)] + + if (isMatch) { + if (keyWord.match(/ or|and /gi)) { + return ( + + {keyWord} + + ) + } + + return ( + <> + {keyWord}:{keyVal} + + ) + } else { + return text + } + })} +
+ + {isSemanticSearchEnabled && ( + <> + {isSemanticMode ? ( + + ) : ( + + {getString('enableAISearch')} + + )} + + )} + + {isSemanticSearchEnabled && ( + { + searchMode === SEARCH_MODE.KEYWORD + ? setSearchMode(SEARCH_MODE.SEMANTIC) + : setSearchMode(SEARCH_MODE.KEYWORD) + }} + className={cx(css.toggleBtn)} + checked={SEARCH_MODE.SEMANTIC === searchMode}> + )} + {isSemanticSearchEnabled && ( + + )} +
+ ) +} + +export default CodeSearchBar diff --git a/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss b/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss index d8e2eb8db..2a8bd6237 100644 --- a/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss +++ b/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss @@ -51,4 +51,17 @@ .andOr { color: var(--red-700); } + + .toggleSearch { + position: absolute; + right: 14px; + top: 6px; + z-index: 1; + } + + .toggleTitle { + position: absolute; + right: 53px; + top: 9px; + } } diff --git a/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts b/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts index 34103d2b3..86a626247 100644 --- a/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts +++ b/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts @@ -20,3 +20,5 @@ export declare const andOr: string export declare const highltedText: string export declare const searchCtn: string export declare const textCtn: string +export declare const toggleSearch: string +export declare const toggleTitle: string diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index fd19319ce..d18f15d85 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -3,6 +3,7 @@ * Use the command `yarn strings` to regenerate this file. */ export interface StringsMap { + AIDA: string Enable: string accessControl: string accountEmail: string @@ -251,6 +252,7 @@ export interface StringsMap { emptyRepoHeader: string emptyRepoInclude: string emptySpaceText: string + enableAISearch: string enableSSLVerification: string enableWebhookContent: string enableWebhookTitle: string @@ -401,6 +403,11 @@ export interface StringsMap { inactiveBranches: string isRequired: string key: string + 'keywordSearch.sampleQueries.searchForClass': string + 'keywordSearch.sampleQueries.searchForFilesWithCMD': string + 'keywordSearch.sampleQueries.searchForInitialCommit': string + 'keywordSearch.sampleQueries.searchForPattern': string + keywordSearchPlaceholder: string killed: string language: string leaveAComment: string @@ -753,6 +760,7 @@ export interface StringsMap { selectStatuses: string selectToViewMore: string selectUsers: string + 'semanticSearch.sampleQueries': string setAsAdmin: string setting: string settings: string diff --git a/web/src/hooks/useFeatureFlag.ts b/web/src/hooks/useFeatureFlag.ts new file mode 100644 index 000000000..eb6eb85b4 --- /dev/null +++ b/web/src/hooks/useFeatureFlag.ts @@ -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>() { + return {} as T +} diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index d35f80dbb..350f5b962 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -725,11 +725,23 @@ userUpdateSuccess: 'User updated successfully' viewFile: View File searchResult: 'Search Result {count}' 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 codeSearchModal: Begin search by describing what you are looking for searchHeader: 'Here are some example to get you start:' 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". +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.' run: Run plugins: diff --git a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx index f8cc00c93..6fd2f6357 100644 --- a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx +++ b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx @@ -27,9 +27,10 @@ import { CloneButtonTooltip } from 'components/CloneButtonTooltip/CloneButtonToo import { CodeIcon, GitInfoProps, isDir, isGitRev, isRefATag } from 'utils/GitUtils' import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect' import { useCreateBranchModal } from 'components/CreateBranchModal/CreateBranchModal' -import KeywordSearch from 'components/CodeSearch/KeywordSearch' +// import KeywordSearch from 'components/CodeSearch/KeywordSearch' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { permissionProps } from 'utils/Utils' +import CodeSearch from 'components/CodeSearch/CodeSearch' import css from './ContentHeader.module.scss' export function ContentHeader({ @@ -156,7 +157,7 @@ export function ContentHeader({ )} -
{!standalone ? : null}
+
{!standalone ? : null}
) } diff --git a/web/src/pages/Search/KeywordSearch.tsx b/web/src/pages/Search/CodeSearchPage.tsx similarity index 58% rename from web/src/pages/Search/KeywordSearch.tsx rename to web/src/pages/Search/CodeSearchPage.tsx index 7476cd6ca..dfa37f3b0 100644 --- a/web/src/pages/Search/KeywordSearch.tsx +++ b/web/src/pages/Search/CodeSearchPage.tsx @@ -34,7 +34,7 @@ import { useMutate } from 'restful-react' import { debounce, escapeRegExp, flatten, sortBy, uniq } from 'lodash-es' import Keywords from 'react-keywords' import cx from 'classnames' - +import { useHistory } from 'react-router-dom' import { useAppContext } from 'AppContext' import { useStrings } from 'framework/strings' import { useQueryParams } from 'hooks/useQueryParams' @@ -47,34 +47,55 @@ import { Editor } from 'components/Editor/Editor' import { NoResultCard } from 'components/NoResultCard/NoResultCard' import KeywordSearchbar from 'components/KeywordSearchbar/KeywordSearchbar' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' - +import { SEARCH_MODE } from 'components/CodeSearch/CodeSearch' +import CodeSearchBar from 'components/CodeSearchBar/CodeSearchBar' import KeywordSearchFilters from './KeywordSearchFilters' -import type { FileMatch, KeywordSearchResponse } from './KeywordSearch.types' - +import type { FileMatch, KeywordSearchResponse } from './CodeSearchPage.types' 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 { getString } = useStrings() const space = useGetSpaceParam() - const { repoName } = useGetRepositoryMetadata() + const { repoName, repoMetadata } = useGetRepositoryMetadata() const { updateQueryParams } = useUpdateQueryParams() const { showError } = useToaster() - 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 [searchMode, setSearchMode] = useState(mode) const [selectedRepositories, setSelectedRepositories] = useState([]) - const [selectedLanguages, setSelectedLanguages] = useState([]) - - const [searchResults, setSearchResults] = useState() + const [selectedLanguages, setSelectedLanguages] = useState<(SelectOption & { extension?: string })[]>([]) + const [keywordSearchResults, setKeyowordSearchResults] = useState() + //semantic + // const [loadingSearch, setLoadingSearch] = useState(false) + const [semanticSearchResult, setSemanticSearchResult] = useState([]) + const [uniqueFiles, setUniqueFiles] = useState(0) + const history = useHistory() + // const { mutate, loading: isSearching } = useMutate({ path: `/api/v1/search`, verb: 'POST' }) - + const { + mutate: sendSemanticSearch, + loading: loadingSearch, + cancel: cancelPreviousSearch + } = useMutate({ + verb: 'POST', + path: `/api/v1/repos/${repoMetadata?.path}/+/semantic/search` + }) const debouncedSearch = useCallback( debounce(async (text: string) => { try { @@ -105,65 +126,141 @@ const Search = () => { max_result_count: maxResultCount }) - setSearchResults(res) + setKeyowordSearchResults(res) } else { - setSearchResults(undefined) + setKeyowordSearchResults(undefined) } } catch (error) { showError(getErrorMessage(error)) } }, 300), - [selectedLanguages, selectedRepositories, repoPath] + [selectedLanguages, selectedRepositories, repoPath, mode] ) - useEffect(() => { - if (searchTerm) { - debouncedSearch(searchTerm) - } - }, [selectedLanguages, selectedRepositories]) + const performSemanticSearch = useCallback(() => { + // setLoadingSearch(true) + // history.replace({ pathname: location.pathname, search: `q=${searchTerm}` }) + 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 ( - { - setSearchTerm(text) - }} - onSearch={text => { - setSearchResults(undefined) - updateQueryParams({ q: text }) - debouncedSearch(text) - }} - /> + {repoName ? ( + { + setSearchTerm(text) + }} + onSearch={text => { + cancelPreviousSearch() + setKeyowordSearchResults(undefined) + setSemanticSearchResult([]) + updateQueryParams({ q: text, mode: searchMode }) + if (searchMode === SEARCH_MODE.SEMANTIC) { + performSemanticSearch() + } else { + debouncedSearch(text) + } + }} + /> + ) : ( + { + setSearchTerm(text) + }} + onSearch={text => { + setKeyowordSearchResults(undefined) + updateQueryParams({ q: text }) + debouncedSearch(text) + }} + /> + )} - - - {searchResults?.file_matches.length ? ( + + {keywordSearchResults && ( + + )} + + {keywordSearchResults?.file_matches.length ? ( <> - {searchResults?.stats.total_files} {getString('files')} + {keywordSearchResults?.stats.total_files} {getString('files')} {getString('results')} - {searchResults?.file_matches?.map(fileMatch => { + {keywordSearchResults?.file_matches?.map(fileMatch => { if (fileMatch.matches.length) { return } })} ) : null} - !isSearching && !searchResults?.file_matches?.length} forSearch={true} /> + {/* semantic search results -> */} + {semanticSearchResult?.length ? ( + <> + + {!loadingSearch && ( + + {uniqueFiles} {getString('files')} + + )} + + {getString('results')} + + + {loadingSearch ? ( + + ) : ( + + {semanticSearchResult.map((result, index) => ( + + ))} + + )} + + ) : null} + + !isSearching && + !keywordSearchResults?.file_matches?.length && + !loadingSearch && + !semanticSearchResult?.length + } + forSearch={true} + /> ) @@ -171,6 +268,7 @@ const Search = () => { export default Search +// KEYOWORD SEARCH CODE interface CodeBlock { lineNumberOffset: number codeBlock: string @@ -232,7 +330,6 @@ export const SearchResult = ({ fileMatch, searchTerm }: { fileMatch: FileMatch; const flattenedMatches = flatten(codeBlocks.map(codeBlock => codeBlock.fragmentMatches)) const allFileMatches = isCaseSensitive ? flattenedMatches : uniq(flattenedMatches.map(match => match.toLowerCase())) - return ( @@ -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 ( + + + + + + {repoName} + + + + + {result.file_path} + + + {getString('AIDA')} + +
+ 10} + showMoreLines={showMoreMatchs} + setShowMoreMatches={setShowMoreMatches} + showLines={showLines} + /> +
+
+ ) +} + +const SemanticCodeBlock = ({ + showMoreMatchesFooter, + result, + showMoreLines, + setShowMoreMatches, + showLines +}: { + showMoreMatchesFooter: boolean + result: SemanticSearchResultType + showMoreLines: boolean + setShowMoreMatches: React.Dispatch> + showLines: string[] +}) => { + const { getString } = useStrings() + const totalLines = result.end_line - result.start_line + 1 + if (totalLines === 0) { + return null + } + return ( + <> + String(n - 1 + result.start_line) + }) + ]} + className={css.editorCtn} + /> + {showMoreMatchesFooter ? ( + setShowMoreMatches(prevVal => !prevVal)} + flex={{ alignItems: 'center', justifyContent: 'flex-start' }} + {...ButtonRoleProps}> + {!showMoreLines ? : } + + {!showMoreLines ? `Show ${totalLines - 10} more lines` : getString('showLessMatches')} + + + ) : null} + + ) +} diff --git a/web/src/pages/Search/KeywordSearch.types.ts b/web/src/pages/Search/CodeSearchPage.types.ts similarity index 100% rename from web/src/pages/Search/KeywordSearch.types.ts rename to web/src/pages/Search/CodeSearchPage.types.ts diff --git a/web/src/pages/Search/Search.module.scss b/web/src/pages/Search/Search.module.scss index 7901c2d96..f3525bf61 100644 --- a/web/src/pages/Search/Search.module.scss +++ b/web/src/pages/Search/Search.module.scss @@ -23,6 +23,10 @@ min-height: var(--page-height); background-color: var(--primary-bg) !important; + .loadingSpinner { + width: 77% !important; + } + .pageHeader { flex-direction: column; height: var(--header-height); @@ -282,6 +286,19 @@ 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 { background-color: var(--grey-50) !important; border: 1px solid var(--grey-100) !important; diff --git a/web/src/pages/Search/Search.module.scss.d.ts b/web/src/pages/Search/Search.module.scss.d.ts index c8174bc5e..469689b7b 100644 --- a/web/src/pages/Search/Search.module.scss.d.ts +++ b/web/src/pages/Search/Search.module.scss.d.ts @@ -27,6 +27,7 @@ export declare const highlight: string export declare const highlightLineNumber: string export declare const isCollapsed: string export declare const layout: string +export declare const loadingSpinner: string export declare const main: string export declare const multiSelect: string export declare const noResult: string @@ -41,6 +42,7 @@ export declare const resultTitle: string export declare const searchBox: string export declare const searchResult: string export declare const selected: string +export declare const semanticStamp: string export declare const showMoreCtn: string export declare const split: string export declare const texts: string