From f6c228dd75ca6ea880241598d19b5733a94250b0 Mon Sep 17 00:00:00 2001 From: "rajarshee.chatterjee@harness.io" Date: Tue, 12 Dec 2023 17:36:59 +0000 Subject: [PATCH] feat: [CODE-1143] Keyword Search UI (#884) --- web/config/moduleFederation.config.js | 2 +- web/src/RouteDefinitions.ts | 8 +- web/src/RouteDestinations.tsx | 11 +- .../CodeSearch/KeywordSearch.module.scss | 80 +++++ .../CodeSearch/KeywordSearch.module.scss.d.ts | 27 ++ .../components/CodeSearch/KeywordSearch.tsx | 178 ++++++++++ .../CodeSearch/SemanticSearch.module.scss | 142 ++++++++ .../SemanticSearch.module.scss.d.ts | 28 ++ .../components/CodeSearch/SemanticSearch.tsx | 172 +++++++++ .../CodeSearch}/search-background.svg | 0 .../KeywordSearchbar.module.scss | 54 +++ .../KeywordSearchbar.module.scss.d.ts | 22 ++ .../KeywordSearchbar/KeywordSearchbar.tsx | 80 +++++ web/src/framework/strings/stringTypes.ts | 10 + web/src/i18n/strings.en.yaml | 10 + web/src/layouts/menu/DefaultMenu.tsx | 19 +- .../RepositoriesListing.tsx | 5 +- .../ContentHeader/ContentHeader.module.scss | 162 ++------- .../ContentHeader.module.scss.d.ts | 11 +- .../ContentHeader/ContentHeader.tsx | 157 +-------- .../FileContent/FileContent.module.scss | 6 + .../FileContent/FileContent.tsx | 32 ++ web/src/pages/Search/KeywordSearch.tsx | 332 ++++++++++++++++++ web/src/pages/Search/KeywordSearch.types.ts | 30 ++ web/src/pages/Search/KeywordSearchFilters.tsx | 107 ++++++ web/src/pages/Search/Search.module.scss | 90 +++++ web/src/pages/Search/Search.module.scss.d.ts | 8 + 27 files changed, 1474 insertions(+), 309 deletions(-) create mode 100644 web/src/components/CodeSearch/KeywordSearch.module.scss create mode 100644 web/src/components/CodeSearch/KeywordSearch.module.scss.d.ts create mode 100644 web/src/components/CodeSearch/KeywordSearch.tsx create mode 100644 web/src/components/CodeSearch/SemanticSearch.module.scss create mode 100644 web/src/components/CodeSearch/SemanticSearch.module.scss.d.ts create mode 100644 web/src/components/CodeSearch/SemanticSearch.tsx rename web/src/{pages/Repository/RepositoryContent/ContentHeader => components/CodeSearch}/search-background.svg (100%) create mode 100644 web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss create mode 100644 web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts create mode 100644 web/src/components/KeywordSearchbar/KeywordSearchbar.tsx create mode 100644 web/src/pages/Search/KeywordSearch.tsx create mode 100644 web/src/pages/Search/KeywordSearch.types.ts create mode 100644 web/src/pages/Search/KeywordSearchFilters.tsx diff --git a/web/config/moduleFederation.config.js b/web/config/moduleFederation.config.js index d79080951..83d2c77f0 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/Search.tsx', + './Search': './src/pages/Search/KeywordSearch.tsx', './WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx', './NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx' }, diff --git a/web/src/RouteDefinitions.ts b/web/src/RouteDefinitions.ts index 51fc5a144..4c47d386c 100644 --- a/web/src/RouteDefinitions.ts +++ b/web/src/RouteDefinitions.ts @@ -98,7 +98,9 @@ export interface CODERoutes { toCODESettings: ( args: RequiredField, 'repoPath'> ) => string - toCODESearch: (args: Required>) => string + toCODEProjectSearch: (args: Required>) => string + toCODERepositorySearch: (args: Required>) => string + toCODESemanticSearch: (args: Required>) => string toCODEExecutions: (args: Required>) => string toCODEExecution: (args: Required>) => string toCODESecret: (args: Required>) => string @@ -155,7 +157,9 @@ export const routes: CODERoutes = { `/${repoPath}/settings${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${ settingSectionMode ? '/' + settingSectionMode : '' }`, - toCODESearch: ({ repoPath }) => `/${repoPath}/search`, + toCODEProjectSearch: ({ space }) => `/${space}/search`, + toCODERepositorySearch: ({ repoPath }) => `/${repoPath}/search`, + toCODESemanticSearch: ({ repoPath }) => `/${repoPath}/search/semantic`, toCODEWebhooks: ({ repoPath }) => `/${repoPath}/webhooks`, toCODEWebhookNew: ({ repoPath }) => `/${repoPath}/webhooks/new`, toCODEWebhookDetails: ({ repoPath, webhookId }) => `/${repoPath}/webhook/${webhookId}`, diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx index a6417b54b..079add232 100644 --- a/web/src/RouteDestinations.tsx +++ b/web/src/RouteDestinations.tsx @@ -47,6 +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 AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline' import { useAppContext } from 'AppContext' import PipelineSettings from 'components/PipelineSettings/PipelineSettings' @@ -292,7 +293,15 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations - + + + + + + + diff --git a/web/src/components/CodeSearch/KeywordSearch.module.scss b/web/src/components/CodeSearch/KeywordSearch.module.scss new file mode 100644 index 000000000..dc0d230d3 --- /dev/null +++ b/web/src/components/CodeSearch/KeywordSearch.module.scss @@ -0,0 +1,80 @@ +/* + * 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%; + } + } + + .sectionHeader { + font-size: 10px !important; + font-weight: 600 !important; + letter-spacing: 0.237px; + color: var(--grey-300) !important; + text-transform: uppercase; + } + + .sampleQuery { + cursor: pointer; + border-radius: 4px; + + &:hover, + &.selected { + background-color: var(--grey-100); + } + } +} + +.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/KeywordSearch.module.scss.d.ts b/web/src/components/CodeSearch/KeywordSearch.module.scss.d.ts new file mode 100644 index 000000000..7d12c4f63 --- /dev/null +++ b/web/src/components/CodeSearch/KeywordSearch.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 backdrop: string +export declare const layout: string +export declare const portal: string +export declare const sampleQuery: string +export declare const searchBox: string +export declare const searchContainer: string +export declare const searchModal: string +export declare const sectionHeader: string +export declare const selected: string diff --git a/web/src/components/CodeSearch/KeywordSearch.tsx b/web/src/components/CodeSearch/KeywordSearch.tsx new file mode 100644 index 000000000..cbc93740c --- /dev/null +++ b/web/src/components/CodeSearch/KeywordSearch.tsx @@ -0,0 +1,178 @@ +/* + * 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 } 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 KeywordSearchbar from 'components/KeywordSearchbar/KeywordSearchbar' + +import css from './KeywordSearch.module.scss' + +interface KeywordSearchProps { + repoMetadata?: GitInfoProps['repoMetadata'] +} + +const KeywordSearch = ({ repoMetadata }: KeywordSearchProps) => { + 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 performSearch = useCallback( + (q: string) => { + if (repoMetadata?.path) { + history.push({ + pathname: routes.toCODERepositorySearch({ + repoPath: repoMetadata.path as string + }), + search: `q=${q}` + }) + } else { + history.push({ + pathname: routes.toCODEProjectSearch({ + space + }), + search: `q=${q}` + }) + } + }, + [history, repoMetadata?.path, routes] + ) + const onSearch = useCallback(() => { + if (search?.trim()) { + performSearch(search) + } + }, [performSearch, search]) + + useHotkeys( + 'ctrl+k', + () => { + if (!showSearchModal) { + setShowSearchModal(true) + } + }, + [showSearchModal] + ) + + return ( + { + setShowSearchModal(true) + }}> + + {showSearchModal && ( + + { + setShowSearchModal(false) + }}> + + + + + + { + if (!search?.trim()) { + switch (e.key) { + case 'ArrowDown': + setSearchSampleQueryIndex(index => { + return index + 1 > searchSampleQueries.length ? 1 : index + 1 + }) + break + case 'ArrowUp': + setSearchSampleQueryIndex(index => { + return index - 1 > 0 ? index - 1 : searchSampleQueries.length + }) + break + } + } + }} + /> + + + + {getString('searchExamples')} + + {searchSampleQueries.map(sampleQuery => { + const { keyword, description } = sampleQuery + + return ( + setSearch(keyword)}> + + + {keyword} + + {description} + + ) + })} + + + + + + )} + + ) +} + +export default KeywordSearch + +const searchSampleQueries = [ + { keyword: `class`, description: 'Search for class' }, + { keyword: `class file:^cmd`, description: 'Search for class in files starting with cmd' }, + { + keyword: `class or printf`, + description: 'Include only results from file paths matching the given search pattern' + }, + { keyword: 'initial commit', description: 'Search for exact phrase initial commit' } +] diff --git a/web/src/components/CodeSearch/SemanticSearch.module.scss b/web/src/components/CodeSearch/SemanticSearch.module.scss new file mode 100644 index 000000000..22ede8758 --- /dev/null +++ b/web/src/components/CodeSearch/SemanticSearch.module.scss @@ -0,0 +1,142 @@ +/* + * 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. + */ + +.searchBox { + input, + input:focus { + border: 1px solid var(--ai-purple-600) !important; + } + + svg path { + fill: var(--ai-purple-600) !important; + } + + img { + position: absolute; + top: 5px; + right: 6px; + } +} + +.searchModal { + --modal-width: min(970px, 80vw); + --input-width: calc(var(--modal-width) - 154px); + + width: var(--modal-width); + padding: var(--spacing-medium) var(--spacing-xxlarge) var(--spacing-xlarge); + + > span:first-of-type { + display: none; + } + + .layout { + width: 100%; + + .searchContainer { + position: relative; + + span[icon] { + display: none; + } + + .searchIcon { + position: absolute; + left: 12px; + top: 11px; + z-index: 1; + } + + img { + position: absolute; + right: 14px; + top: 6px; + } + + input { + padding-left: 35px !important; + font-size: 14px; + font-weight: 500; + line-height: 19px; + letter-spacing: 0.23749999701976776px; + border-color: var(--ai-purple-600); + color: var(--grey-500); + width: var(--input-width) !important; + } + } + + button { + --button-height: 40px !important; + } + } + + .sectionHeader { + font-size: 13px !important; + font-weight: 500 !important; + letter-spacing: 0.23749999701976776px; + color: var(--grey-500) !important; + text-transform: uppercase; + } + + .sampleQuery { + height: 44px; + background-color: var(--grey-50); + color: var(--grey-500) !important; + border-radius: 4px; + padding-left: 32px; + 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; + } + } +} + +.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/SemanticSearch.module.scss.d.ts b/web/src/components/CodeSearch/SemanticSearch.module.scss.d.ts new file mode 100644 index 000000000..e87875261 --- /dev/null +++ b/web/src/components/CodeSearch/SemanticSearch.module.scss.d.ts @@ -0,0 +1,28 @@ +/* + * 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 sampleQuery: 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/SemanticSearch.tsx b/web/src/components/CodeSearch/SemanticSearch.tsx new file mode 100644 index 000000000..06deb7688 --- /dev/null +++ b/web/src/components/CodeSearch/SemanticSearch.tsx @@ -0,0 +1,172 @@ +/* + * 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 { Button, ButtonSize, ButtonVariation, Container, Dialog, Layout, Text, Utils } from '@harnessio/uicore' +import { LongArrowDownLeft, Search } from 'iconoir-react' +import { useHotkeys } from 'react-hotkeys-hook' +import { useHistory } from 'react-router-dom' +import { noop } from 'lodash-es' +import cx from 'classnames' + +import { useAppContext } from 'AppContext' +import { useStrings } from 'framework/strings' +import { ButtonRoleProps } from 'utils/Utils' +import type { GitInfoProps } from 'utils/GitUtils' + +import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner' + +import svg from './search-background.svg' +import css from './SemanticSearch.module.scss' + +const SemanticSearch = ({ repoMetadata }: Pick) => { + const { getString } = useStrings() + const { routes } = useAppContext() + const history = useHistory() + + const [search, setSearch] = useState('') + const [searchSampleQueryIndex, setSearchSampleQueryIndex] = useState(0) + const [showSearchModal, setShowSearchModal] = useState(false) + + const performSearch = useCallback( + (q: string) => { + history.push({ + pathname: routes.toCODESemanticSearch({ + repoPath: repoMetadata.path as string + }), + search: `q=${q}` + }) + }, + [history, repoMetadata.path, routes] + ) + const onSearch = useCallback(() => { + if (search?.trim()) { + performSearch(search) + } else if (searchSampleQueryIndex > 0 && searchSampleQueryIndex <= searchSampleQueries.length) { + performSearch(searchSampleQueries[searchSampleQueryIndex - 1]) + } + }, [performSearch, search, searchSampleQueryIndex]) + + useHotkeys( + 'ctrl+k', + () => { + if (!showSearchModal) { + setShowSearchModal(true) + } + }, + [showSearchModal] + ) + + return ( + { + setShowSearchModal(true) + }}> + + {} + {showSearchModal && ( + + { + setShowSearchModal(false) + }}> + + + + + + + { + if (q?.trim()) { + setSearchSampleQueryIndex(0) + } + setSearch(q) + }} + height={40} + onSearch={onSearch} + onKeyDown={e => { + if (!search?.trim()) { + switch (e.key) { + case 'ArrowDown': + setSearchSampleQueryIndex(index => { + return index + 1 > searchSampleQueries.length ? 1 : index + 1 + }) + break + case 'ArrowUp': + setSearchSampleQueryIndex(index => { + return index - 1 > 0 ? index - 1 : searchSampleQueries.length + }) + break + } + } + }} + /> + {!search && } + + + + )} + + ) +} + +export default SemanticSearch + +// These sample queries are in English only - No need to do i18n for them +const searchSampleQueries = [ + `Where is the code that handles authentication?`, + `Where is the application entry point?`, + `Where do we configure the logger?` +] diff --git a/web/src/pages/Repository/RepositoryContent/ContentHeader/search-background.svg b/web/src/components/CodeSearch/search-background.svg similarity index 100% rename from web/src/pages/Repository/RepositoryContent/ContentHeader/search-background.svg rename to web/src/components/CodeSearch/search-background.svg diff --git a/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss b/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss new file mode 100644 index 000000000..d8e2eb8db --- /dev/null +++ b/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss @@ -0,0 +1,54 @@ +/* + * 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%; + + input { + background-color: transparent !important; + color: transparent !important; + caret-color: var(--black); + } + + > 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; + } + + .highltedText { + background-color: var(--primary-1); + border-radius: 4px; + color: var(--primary-7); + } + + .andOr { + color: var(--red-700); + } +} diff --git a/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts b/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts new file mode 100644 index 000000000..34103d2b3 --- /dev/null +++ b/web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts @@ -0,0 +1,22 @@ +/* + * 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 textCtn: string diff --git a/web/src/components/KeywordSearchbar/KeywordSearchbar.tsx b/web/src/components/KeywordSearchbar/KeywordSearchbar.tsx new file mode 100644 index 000000000..856e98bbf --- /dev/null +++ b/web/src/components/KeywordSearchbar/KeywordSearchbar.tsx @@ -0,0 +1,80 @@ +/* + * 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, { FC } from 'react' + +import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner' + +import css from './KeywordSearchbar.module.scss' + +interface KeywordSearchbarProps { + value: string + onChange: (value: string) => void + onSearch?: (searchTerm: string) => void + onKeyDown?: (e: React.KeyboardEvent) => void +} + +const KEYWORD_REGEX = /((?:(?:-{0,1})(?:repo|lang|file|case|count)):\S*|(?: or|and ))/gi + +const KeywordSearchbar: FC = ({ value, onChange, onSearch, onKeyDown }) => { + return ( +
+
+ {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 + } + })} +
+ +
+ ) +} + +export default KeywordSearchbar diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index 0981ac4d6..3dbc224f4 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -132,6 +132,7 @@ export interface StringsMap { checkRuns: string checkSuites: string checks: string + clear: string clickHereToDownload: string clone: string cloneHTTPS: string @@ -389,6 +390,7 @@ export interface StringsMap { isRequired: string key: string killed: string + language: string leaveAComment: string license: string lineBreaks: string @@ -407,6 +409,7 @@ export interface StringsMap { missingPermsContent: string myComments: string nDays: string + nMoreMatches: string name: string nameYourBranch: string nameYourFile: string @@ -690,6 +693,7 @@ export interface StringsMap { resolve: string resolved: string resolvedComments: string + results: string reviewerNotFound: string reviewers: string role: string @@ -701,6 +705,7 @@ export interface StringsMap { scrollToTop: string search: string searchBranches: string + searchExamples: string searchHeader: string searchResult: string secret: string @@ -721,8 +726,11 @@ export interface StringsMap { 'secrets.secretUpdated': string 'secrets.showValue': string 'secrets.updateSecret': string + seeNMoreMatches: string selectBranchPlaceHolder: string + selectLanguagePlaceholder: string selectRange: string + selectRepositoryPlaceholder: string selectSpace: string selectSpaceText: string selectStatuses: string @@ -734,7 +742,9 @@ export interface StringsMap { showCommitHistory: string showEverything: string showLess: string + showLessMatches: string showMore: string + showNMoreMatches: string signIn: string signUp: string skipped: string diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index e14429b8c..624f6cf17 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -943,3 +943,13 @@ pipelineConfig: label: Pipeline Configuration yamlUpdated: It looks like the YAML got modified. Refresh changes? discard: Discard +language: Language +selectLanguagePlaceholder: '- Select language here -' +selectRepositoryPlaceholder: '- Select repository here -' +results: Results +showNMoreMatches: Show {{ n }} more matches +showLessMatches: Show less +clear: Clear +searchExamples: Search Examples +nMoreMatches: This file contains {{ n }} more matches not shown. +seeNMoreMatches: See all {{ n }} matches in the full file \ No newline at end of file diff --git a/web/src/layouts/menu/DefaultMenu.tsx b/web/src/layouts/menu/DefaultMenu.tsx index ac928519d..1fd7aa527 100644 --- a/web/src/layouts/menu/DefaultMenu.tsx +++ b/web/src/layouts/menu/DefaultMenu.tsx @@ -50,6 +50,8 @@ export const DefaultMenu: React.FC = () => { return !isGitRev(ref) ? ref : '' }, [commitRef, gitRef]) + const isSemanticSearchEnabled = false + return ( @@ -159,15 +161,26 @@ export const DefaultMenu: React.FC = () => { data-code-repo-section="search" isSubLink label={getString('search')} - to={routes.toCODESearch({ - repoPath - })} + to={ + isSemanticSearchEnabled + ? routes.toCODESemanticSearch({ repoPath }) + : `${routes.toCODERepositorySearch({ repoPath })}?q=repo:${repoPath}` + } /> )} + + + + {standalone && ( () - const { routes } = useAppContext() + const { routes, standalone } = useAppContext() const { updateQueryParams } = useUpdateQueryParams() const pageBrowser = useQueryParams() const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1 @@ -197,7 +198,7 @@ export default function RepositoriesListing() { return ( - + } /> span:first-of-type { - display: none; - } - - .layout { - width: 100%; - - .searchContainer { - position: relative; - - span[icon] { - display: none; - } - - .searchIcon { - position: absolute; - left: 12px; - top: 11px; - z-index: 1; - } - - img { - position: absolute; - right: 14px; - top: 6px; - } - - input { - padding-left: 35px !important; - font-size: 14px; - font-weight: 500; - line-height: 19px; - letter-spacing: 0.23749999701976776px; - border-color: var(--ai-purple-600); - color: var(--grey-500); - width: var(--input-width) !important; - } - } - - button { - --button-height: 40px !important; - } - } - - .sectionHeader { - font-size: 13px !important; - font-weight: 500 !important; - letter-spacing: 0.23749999701976776px; - color: var(--grey-500) !important; - text-transform: uppercase; - } - - .sampleQuery { - height: 44px; - background-color: var(--grey-50); - color: var(--grey-500) !important; - border-radius: 4px; - padding-left: 32px; - 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; - } - } -} - -.backdrop { - background-color: rgb(16 22 26 / 25%); -} - -.portal { - :global { - .bp3-dialog-container.bp3-overlay-content { - align-items: flex-start !important; - } + top: 5px; + right: 6px; } } diff --git a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss.d.ts b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss.d.ts index b0aba5d20..83aab27c0 100644 --- a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss.d.ts +++ b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss.d.ts @@ -16,19 +16,10 @@ /* eslint-disable */ // This is an auto-generated file -export declare const backdrop: string export declare const btnColorFix: string -export declare const layout: string export declare const main: string export declare const mainBorder: string export declare const mainContainer: string -export declare const portal: string export declare const refRoot: string export declare const rootSlash: string -export declare const sampleQuery: 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 +export declare const searchBoxCtn: string diff --git a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx index 49a75d303..7ac4a722e 100644 --- a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx +++ b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx @@ -14,23 +14,10 @@ * limitations under the License. */ -import React, { useCallback, useMemo, useState } from 'react' -import { noop } from 'lodash-es' -import { - Container, - Layout, - Button, - ButtonSize, - FlexExpander, - ButtonVariation, - Text, - Utils, - Dialog -} from '@harnessio/uicore' +import React, { useMemo } from 'react' +import { Container, Layout, Button, FlexExpander, ButtonVariation, Text } from '@harnessio/uicore' import cx from 'classnames' import { Icon } from '@harnessio/icons' -import { useHotkeys } from 'react-hotkeys-hook' -import { LongArrowDownLeft, Search } from 'iconoir-react' import { Color } from '@harnessio/design-system' import { Breadcrumbs, IBreadcrumbProps } from '@blueprintjs/core' import { Link, useHistory } from 'react-router-dom' @@ -40,10 +27,9 @@ 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 { useGetSpaceParam } from 'hooks/useGetSpaceParam' -import { ButtonRoleProps, permissionProps } from 'utils/Utils' -import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner' -import svg from './search-background.svg' +import { permissionProps } from 'utils/Utils' import css from './ContentHeader.module.scss' export function ContentHeader({ @@ -57,37 +43,6 @@ export function ContentHeader({ const history = useHistory() const _isDir = isDir(resourceContent) const space = useGetSpaceParam() - const [showSearchModal, setShowSearchModal] = useState(false) - const [searchSampleQueryIndex, setSearchSampleQueryIndex] = useState(0) - const [search, setSearch] = useState('') - const performSearch = useCallback( - (q: string) => { - history.push({ - pathname: routes.toCODESearch({ - repoPath: repoMetadata.path as string - }), - search: `q=${q}` - }) - }, - [history, repoMetadata.path, routes] - ) - const onSearch = useCallback(() => { - if (search?.trim()) { - performSearch(search) - } else if (searchSampleQueryIndex > 0 && searchSampleQueryIndex <= searchSampleQueries.length) { - performSearch(searchSampleQueries[searchSampleQueryIndex - 1]) - } - }, [performSearch, search, searchSampleQueryIndex]) - - useHotkeys( - 'ctrl+k', - () => { - if (!showSearchModal) { - setShowSearchModal(true) - } - }, - [showSearchModal] - ) const permPushResult = hooks?.usePermissionTranslate?.( { @@ -200,109 +155,7 @@ export function ContentHeader({ )} - {!standalone && false && ( - { - setShowSearchModal(true) - }}> - - {} - - {showSearchModal && ( - - { - setShowSearchModal(false) - }}> - - - - - - - { - if (q?.trim()) { - setSearchSampleQueryIndex(0) - } - setSearch(q) - }} - height={40} - onSearch={onSearch} - onKeyDown={e => { - if (!search?.trim()) { - switch (e.key) { - case 'ArrowDown': - setSearchSampleQueryIndex(index => { - return index + 1 > searchSampleQueries.length ? 1 : index + 1 - }) - break - case 'ArrowUp': - setSearchSampleQueryIndex(index => { - return index - 1 > 0 ? index - 1 : searchSampleQueries.length - }) - break - } - } - }} - /> - {!search && } - - - - )} - - )} +
{!standalone ? : null}
) } - -// These sample queries are in English only - No need to do i18n for them -const searchSampleQueries = [ - `Where is the code that handles authentication?`, - `Where is the application entry point?`, - `Where do we configure the logger?` -] diff --git a/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.module.scss b/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.module.scss index 881e73101..038b8beb5 100644 --- a/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.module.scss +++ b/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.module.scss @@ -22,6 +22,12 @@ display: flex; flex-direction: column; + :global { + .highlight { + background-color: var(--yellow-300); + } + } + .heading { box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16); align-items: center; diff --git a/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.tsx b/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.tsx index b0f3da386..fa34ceb41 100644 --- a/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.tsx +++ b/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.tsx @@ -32,6 +32,8 @@ import { Color } from '@harnessio/design-system' import { Document, Page, pdfjs } from 'react-pdf' import { Render, Match, Truthy, Falsy, Case, Else } from 'react-jsx-match' import { Link, useHistory } from 'react-router-dom' +import type { EditorDidMount } from 'react-monaco-editor' +import type { editor } from 'monaco-editor' import { SourceCodeViewer } from 'components/SourceCodeViewer/SourceCodeViewer' import type { OpenapiContentInfo, RepoFileContent, TypesCommit } from 'services/code' import { @@ -54,6 +56,7 @@ import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButto import { PlainButton } from 'components/PlainButton/PlainButton' import { CommitsView } from 'components/CommitsView/CommitsView' import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import { useQueryParams } from 'hooks/useQueryParams' import { FileCategory, RepoContentExtended, useFileContentViewerDecision } from 'utils/FileUtils' import { useDownloadRawFile } from 'hooks/useDownloadRawFile' import { usePageIndex } from 'hooks/usePageIndex' @@ -162,6 +165,34 @@ export function FileContent({ [editButtonDisabled, isFileTooLarge, category] ) + const { keyword } = useQueryParams<{ keyword?: string }>() + + const onEditorMount: EditorDidMount = editor => { + if (!keyword) { + return + } + + const editorModel = editor.getModel() as editor.ITextModel + const keywordMatches: editor.FindMatch[] = editorModel.findMatches(keyword, false, false, false, null, false) + + if (keywordMatches.length > 0) { + keywordMatches.forEach((match: editor.FindMatch): void => { + editorModel.deltaDecorations( + [], + [ + { + range: match.range, + options: { + isWholeLine: false, + inlineClassName: 'highlight' + } + } + ] + ) + }) + } + } + return ( diff --git a/web/src/pages/Search/KeywordSearch.tsx b/web/src/pages/Search/KeywordSearch.tsx new file mode 100644 index 000000000..e82880c16 --- /dev/null +++ b/web/src/pages/Search/KeywordSearch.tsx @@ -0,0 +1,332 @@ +/* + * 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, useEffect, useMemo, useState } from 'react' +import { Container, Layout, SelectOption, Text, useToaster, useToggle } from '@harnessio/uicore' +import { Color, FontVariation } from '@harnessio/design-system' +import { Icon } from '@harnessio/icons' +import { Minus, Plus } from 'iconoir-react' +import { + Decoration, + DecorationSet, + EditorView, + MatchDecorator, + ViewPlugin, + ViewUpdate, + lineNumbers +} from '@codemirror/view' +import type { Extension } from '@codemirror/state' +import { Link } from 'react-router-dom' +import { useMutate } from 'restful-react' +import { debounce, escapeRegExp } from 'lodash-es' +import Keywords from 'react-keywords' +import cx from 'classnames' + +import { useAppContext } from 'AppContext' +import { useStrings } from 'framework/strings' +import { useQueryParams } from 'hooks/useQueryParams' +import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' +import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams' +import { ButtonRoleProps, getErrorMessage } from 'utils/Utils' + +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 KeywordSearchFilters from './KeywordSearchFilters' +import type { FileMatch, KeywordSearchResponse } from './KeywordSearch.types' + +import css from './Search.module.scss' + +const Search = () => { + const { getString } = useStrings() + const space = useGetSpaceParam() + const { repoName } = useGetRepositoryMetadata() + const { updateQueryParams } = useUpdateQueryParams() + const { showError } = useToaster() + + const repoPath = repoName ? `${space}/${repoName}` : undefined + + const { q } = useQueryParams<{ q: string }>() + const [searchTerm, setSearchTerm] = useState(q || '') + + const [selectedRepositories, setSelectedRepositories] = useState([]) + const [selectedLanguage, setSelectedLanguage] = useState() + + const [searchResults, setSearchResults] = useState() + + const { mutate, loading: isSearching } = useMutate({ + path: `/api/v1/search`, + verb: 'POST' + }) + + const debouncedSearch = useCallback( + debounce(async (text: string) => { + try { + if (text.length > 2) { + const maxResultCount = Number(text.match(/count:(\d*)/)?.[1]) || 50 + + const repoPaths = selectedRepositories.map(option => String(option.value)) + + if (selectedLanguage) { + text += ` lang:${String(selectedLanguage)}` + } + + const query = text.replace(/(?:repo|count):(?:[^\s]+|$)/g, '').trim() + + const res = await mutate({ + repo_paths: repoPath ? [repoPath] : repoPaths, + space_paths: !repoPath && !repoPaths.length ? [space] : [], + query, + max_result_count: maxResultCount + }) + + setSearchResults(res) + } else { + setSearchResults(undefined) + } + } catch (error) { + showError(getErrorMessage(error)) + } + }, 300), + [selectedLanguage, selectedRepositories, repoPath] + ) + + useEffect(() => { + if (searchTerm) { + debouncedSearch(searchTerm) + } + }, [selectedLanguage, selectedRepositories]) + + return ( + + + { + setSearchResults(undefined) + setSearchTerm(text) + updateQueryParams({ q: text }) + debouncedSearch(text) + }} + /> + + + + + {searchResults?.file_matches.length ? ( + <> + + + {searchResults?.stats.total_files} {getString('files')} + + + {getString('results')} + + + {searchResults?.file_matches?.map(fileMatch => { + return + })} + + ) : null} + !isSearching && !searchResults?.file_matches?.length} forSearch={true} /> + + + ) +} + +export default Search + +interface CodeBlock { + lineNumberOffset: number + codeBlock: string +} + +export const SearchResult = ({ fileMatch, searchTerm }: { fileMatch: FileMatch; searchTerm: string }) => { + const { routes } = useAppContext() + const { getString } = useStrings() + + const [isCollapsed, setIsCollapsed] = useToggle(false) + const [showMoreMatchs, setShowMoreMatches] = useState(false) + + const matchDecoratorPlugin: Extension = useMemo(() => { + const placeholderMatcher = new MatchDecorator({ + regexp: new RegExp(`${escapeRegExp(fileMatch.matches[0].fragments[0].match)}`, 'gi'), + decoration: Decoration.mark({ class: css.highlight }) + }) + + return ViewPlugin.fromClass( + class { + placeholders: DecorationSet + constructor(view: EditorView) { + this.placeholders = placeholderMatcher.createDeco(view) + } + update(update: ViewUpdate) { + this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders) + } + }, + { + decorations: instance => instance.placeholders, + provide: plugin => + EditorView.atomicRanges.of(view => { + return view.plugin(plugin)?.placeholders || Decoration.none + }) + } + ) + }, []) + + const codeBlocks: CodeBlock[] = useMemo(() => { + const codeBlocksArr: Array<{ lineNumberOffset: number; codeBlock: string }> = [] + + fileMatch.matches.forEach(keywordMatch => { + const lines: string[] = [] + + if (keywordMatch.before.trim()) { + lines.push(keywordMatch.before) + } + + const line: string[] = [] + + keywordMatch.fragments.forEach(fragmentMatch => { + line.push(fragmentMatch.pre + fragmentMatch.match + fragmentMatch.post) + }) + + lines.push(line.join('')) + + if (keywordMatch.after.trim()) { + lines.push(keywordMatch.after) + } + + const codeBlock = lines.join('\n') + + const lineNumberOffset = keywordMatch.before.trim() + ? keywordMatch.line_num - Math.floor(codeBlock.split('\n').length / 2) + : keywordMatch.line_num + + codeBlocksArr.push({ + lineNumberOffset, + codeBlock + }) + }) + + return codeBlocksArr + }, [fileMatch]) + + const collapsedCodeBlocks = showMoreMatchs ? codeBlocks.slice(0, 25) : codeBlocks.slice(0, 2) + const repoName = fileMatch.repo_path.split('/').pop() + + return ( + + + + + + {repoName} + + + + + + {fileMatch.file_name} + + + + +
+ {collapsedCodeBlocks.map((codeBlock, index) => { + const showMoreMatchesFooter = codeBlocks.length > 2 && index === collapsedCodeBlocks.length - 1 + + /** File Match */ + if (codeBlock.lineNumberOffset === 0) { + return null + } + + return ( + <> + String(n - 1 + codeBlock.lineNumberOffset) + }) + ]} + className={css.editorCtn} + /> + {showMoreMatchesFooter ? ( + setShowMoreMatches(prevVal => !prevVal)} + flex={{ alignItems: 'center', justifyContent: 'flex-start' }} + {...ButtonRoleProps}> + {!showMoreMatchs ? : } + + {!showMoreMatchs + ? getString('showNMoreMatches', { n: codeBlocks.length - 2 }) + : getString('showLessMatches')} + + {codeBlocks.length > 25 && showMoreMatchs ? ( + + {getString('nMoreMatches', { n: codeBlocks.length - 25 })}{' '} + + {getString('seeNMoreMatches', { n: codeBlocks.length })} + + + ) : null} + + ) : null} + + ) + })} +
+
+ ) +} diff --git a/web/src/pages/Search/KeywordSearch.types.ts b/web/src/pages/Search/KeywordSearch.types.ts new file mode 100644 index 000000000..226cbdc83 --- /dev/null +++ b/web/src/pages/Search/KeywordSearch.types.ts @@ -0,0 +1,30 @@ +export interface KeywordSearchResponse { + file_matches: FileMatch[] + stats: Stats +} + +export interface FileMatch { + file_name: string + repo_path: string + repo_branch: string + language: string + matches: Match[] +} + +export interface Match { + line_num: number + fragments: Fragment[] + before: string + after: string +} + +export interface Fragment { + pre: string + match: string + post: string +} + +export interface Stats { + total_files: number + total_matches: number +} diff --git a/web/src/pages/Search/KeywordSearchFilters.tsx b/web/src/pages/Search/KeywordSearchFilters.tsx new file mode 100644 index 000000000..9b32f8fe7 --- /dev/null +++ b/web/src/pages/Search/KeywordSearchFilters.tsx @@ -0,0 +1,107 @@ +import React, { Dispatch, SetStateAction } from 'react' +import { + Container, + Text, + type SelectOption, + MultiSelectDropDown, + Button, + ButtonVariation, + DropDown +} from '@harnessio/uicore' +import { Color, FontVariation } from '@harnessio/design-system' +import { useGet } from 'restful-react' + +import { useStrings } from 'framework/strings' +import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import type { TypesRepository } from 'services/code' + +import css from './Search.module.scss' + +const languageOptions = [ + { label: 'JavaScript', value: 'javascript' }, + { label: 'Python', value: 'python' }, + { label: 'Java', value: 'java' }, + { label: 'C#', value: 'csharp' }, + { label: 'PHP', value: 'php' }, + { label: 'TypeScript', value: 'typescript' }, + { label: 'C++', value: 'cpp' }, + { label: 'C', value: 'c' }, + { label: 'Ruby', value: 'ruby' }, + { label: 'Go', value: 'go' }, + { label: 'Swift', value: 'swift' }, + { label: 'Kotlin', value: 'kotlin' }, + { label: 'Rust', value: 'rust' }, + { label: 'Scala', value: 'scala' } +] + +interface KeywordSearchFiltersProps { + isRepoLevelSearch?: boolean + selectedRepositories: SelectOption[] + setRepositories: Dispatch> + selectedLanguage?: string + setLanguage: Dispatch> +} + +const KeywordSearchFilters: React.FC = ({ + isRepoLevelSearch, + selectedLanguage, + selectedRepositories, + setLanguage, + setRepositories +}) => { + const { getString } = useStrings() + const space = useGetSpaceParam() + + const { data } = useGet({ + path: `/api/v1/spaces/${space}/+/repos`, + debounce: 500, + lazy: isRepoLevelSearch + }) + + const repositoryOptions = + data?.map(repository => ({ + label: String(repository.uid), + value: String(repository.path) + })) || [] + + return ( +
+ {isRepoLevelSearch ? null : ( + + + {getString('pageTitle.repository')} + + + + )} + + + {getString('language')} + + setLanguage(String(option.value))} + items={languageOptions} + /> + +
+ ) +} + +export default KeywordSearchFilters diff --git a/web/src/pages/Search/Search.module.scss b/web/src/pages/Search/Search.module.scss index e4fa4779b..7901c2d96 100644 --- a/web/src/pages/Search/Search.module.scss +++ b/web/src/pages/Search/Search.module.scss @@ -249,3 +249,93 @@ display: none !important; } } + +.header { + background-color: var(--white) !important; +} + +.filtersCtn { + align-items: flex-end; + display: grid; + grid-template-columns: max-content max-content min-content; + column-gap: var(--spacing-medium); + margin-bottom: var(--spacing-large); + + :global { + button { + --padding-left: 0 !important; + --font-weight: 500 !important; + } + } + + .multiSelect { + min-width: 203px; + } +} + +.searchResult { + background-color: var(--grey-100) !important; + border-radius: 4px; + margin-bottom: var(--spacing-medium) !important; + + .resultHeader { + padding: var(--spacing-small); + } + + .showMoreCtn { + background-color: var(--grey-50) !important; + border: 1px solid var(--grey-100) !important; + border-top: 0 !important; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + padding: var(--spacing-small) var(--spacing-4) !important; + } + + .isCollapsed { + display: none; + } + + .editorCtn { + :global { + .cm-editor { + border: 1px solid var(--grey-100) !important; + min-height: unset; + } + + .cm-scroller { + padding: 0 !important; + + .cm-line, + .cm-gutterElement { + &, + * { + @include mono-font; + } + } + } + + .cm-gutters { + border-right: 0; + } + + .cm-gutter.cm-lineNumbers { + padding-left: var(--spacing-small); + background-color: var(--grey-50); + } + + .cm-content.cm-lineWrapping { + padding: var(--spacing-small); + } + } + } + + .editorCtn ~ .editorCtn { + > div { + border-top: 0 !important; + } + } +} + +.highlight { + background-color: var(--yellow-300); +} diff --git a/web/src/pages/Search/Search.module.scss.d.ts b/web/src/pages/Search/Search.module.scss.d.ts index 3be27d498..c8174bc5e 100644 --- a/web/src/pages/Search/Search.module.scss.d.ts +++ b/web/src/pages/Search/Search.module.scss.d.ts @@ -17,12 +17,18 @@ /* eslint-disable */ // This is an auto-generated file export declare const aiLabel: string +export declare const editorCtn: string export declare const fileContent: string export declare const filename: string export declare const filePath: string +export declare const filtersCtn: string +export declare const header: string +export declare const highlight: string export declare const highlightLineNumber: string +export declare const isCollapsed: string export declare const layout: string export declare const main: string +export declare const multiSelect: string export declare const noResult: string export declare const noResultContainer: string export declare const pageHeader: string @@ -30,9 +36,11 @@ export declare const path: string export declare const pathText: string export declare const preview: string export declare const result: string +export declare const resultHeader: string export declare const resultTitle: string export declare const searchBox: string export declare const searchResult: string export declare const selected: string +export declare const showMoreCtn: string export declare const split: string export declare const texts: string