mirror of
https://github.com/harness/drone.git
synced 2025-05-21 19:39:59 +08:00
feat: [CODE-1143] Keyword Search UI (#884)
This commit is contained in:
parent
b9abcdee89
commit
f6c228dd75
@ -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/Search.tsx',
|
'./Search': './src/pages/Search/KeywordSearch.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'
|
||||||
},
|
},
|
||||||
|
@ -98,7 +98,9 @@ export interface CODERoutes {
|
|||||||
toCODESettings: (
|
toCODESettings: (
|
||||||
args: RequiredField<Pick<CODEProps, 'repoPath' | 'settingSection' | 'ruleId' | 'settingSectionMode'>, 'repoPath'>
|
args: RequiredField<Pick<CODEProps, 'repoPath' | 'settingSection' | 'ruleId' | 'settingSectionMode'>, 'repoPath'>
|
||||||
) => string
|
) => string
|
||||||
toCODESearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
toCODEProjectSearch: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||||
|
toCODERepositorySearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
|
toCODESemanticSearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
toCODEExecutions: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
toCODEExecutions: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
||||||
toCODEExecution: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline' | 'execution'>>) => string
|
toCODEExecution: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline' | 'execution'>>) => string
|
||||||
toCODESecret: (args: Required<Pick<CODEProps, 'space' | 'secret'>>) => string
|
toCODESecret: (args: Required<Pick<CODEProps, 'space' | 'secret'>>) => string
|
||||||
@ -155,7 +157,9 @@ export const routes: CODERoutes = {
|
|||||||
`/${repoPath}/settings${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${
|
`/${repoPath}/settings${settingSection ? '/' + settingSection : ''}${ruleId ? '/' + ruleId : ''}${
|
||||||
settingSectionMode ? '/' + settingSectionMode : ''
|
settingSectionMode ? '/' + settingSectionMode : ''
|
||||||
}`,
|
}`,
|
||||||
toCODESearch: ({ repoPath }) => `/${repoPath}/search`,
|
toCODEProjectSearch: ({ space }) => `/${space}/search`,
|
||||||
|
toCODERepositorySearch: ({ repoPath }) => `/${repoPath}/search`,
|
||||||
|
toCODESemanticSearch: ({ repoPath }) => `/${repoPath}/search/semantic`,
|
||||||
toCODEWebhooks: ({ repoPath }) => `/${repoPath}/webhooks`,
|
toCODEWebhooks: ({ repoPath }) => `/${repoPath}/webhooks`,
|
||||||
toCODEWebhookNew: ({ repoPath }) => `/${repoPath}/webhooks/new`,
|
toCODEWebhookNew: ({ repoPath }) => `/${repoPath}/webhooks/new`,
|
||||||
toCODEWebhookDetails: ({ repoPath, webhookId }) => `/${repoPath}/webhook/${webhookId}`,
|
toCODEWebhookDetails: ({ repoPath, webhookId }) => `/${repoPath}/webhook/${webhookId}`,
|
||||||
|
@ -47,6 +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 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'
|
||||||
@ -292,7 +293,15 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
|||||||
</LayoutWithSideNav>
|
</LayoutWithSideNav>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path={routes.toCODESearch({ repoPath })} exact>
|
<Route
|
||||||
|
path={[routes.toCODEProjectSearch({ space: pathProps.space }), routes.toCODERepositorySearch({ repoPath })]}
|
||||||
|
exact>
|
||||||
|
<LayoutWithSideNav title={'Search'}>
|
||||||
|
<KeywordSearch />
|
||||||
|
</LayoutWithSideNav>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path={routes.toCODESemanticSearch({ repoPath })} exact>
|
||||||
<LayoutWithSideNav title={getString('pageTitle.search')}>
|
<LayoutWithSideNav title={getString('pageTitle.search')}>
|
||||||
<Search />
|
<Search />
|
||||||
</LayoutWithSideNav>
|
</LayoutWithSideNav>
|
||||||
|
80
web/src/components/CodeSearch/KeywordSearch.module.scss
Normal file
80
web/src/components/CodeSearch/KeywordSearch.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
web/src/components/CodeSearch/KeywordSearch.module.scss.d.ts
vendored
Normal file
27
web/src/components/CodeSearch/KeywordSearch.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 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
|
178
web/src/components/CodeSearch/KeywordSearch.tsx
Normal file
178
web/src/components/CodeSearch/KeywordSearch.tsx
Normal file
@ -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<number>(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 (
|
||||||
|
<Container
|
||||||
|
className={css.searchBox}
|
||||||
|
{...ButtonRoleProps}
|
||||||
|
onClick={() => {
|
||||||
|
setShowSearchModal(true)
|
||||||
|
}}>
|
||||||
|
<SearchInputWithSpinner readOnly placeholder={getString('codeSearch') + ` (ctrl-k)`} query={''} setQuery={noop} />
|
||||||
|
{showSearchModal && (
|
||||||
|
<Container onClick={Utils.stopEvent}>
|
||||||
|
<Dialog
|
||||||
|
className={css.searchModal}
|
||||||
|
backdropClassName={css.backdrop}
|
||||||
|
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}>
|
||||||
|
<KeywordSearchbar
|
||||||
|
value={search}
|
||||||
|
onChange={setSearch}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
<Text className={css.sectionHeader}>{getString('searchExamples')}</Text>
|
||||||
|
<Container>
|
||||||
|
{searchSampleQueries.map(sampleQuery => {
|
||||||
|
const { keyword, description } = sampleQuery
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Horizontal
|
||||||
|
key={keyword}
|
||||||
|
className={cx(css.sampleQuery, {
|
||||||
|
[css.selected]: searchSampleQueries.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 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' }
|
||||||
|
]
|
142
web/src/components/CodeSearch/SemanticSearch.module.scss
Normal file
142
web/src/components/CodeSearch/SemanticSearch.module.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
web/src/components/CodeSearch/SemanticSearch.module.scss.d.ts
vendored
Normal file
28
web/src/components/CodeSearch/SemanticSearch.module.scss.d.ts
vendored
Normal file
@ -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
|
172
web/src/components/CodeSearch/SemanticSearch.tsx
Normal file
172
web/src/components/CodeSearch/SemanticSearch.tsx
Normal file
@ -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<GitInfoProps, 'repoMetadata'>) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const { routes } = useAppContext()
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [searchSampleQueryIndex, setSearchSampleQueryIndex] = useState<number>(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 (
|
||||||
|
<Container
|
||||||
|
className={css.searchBox}
|
||||||
|
{...ButtonRoleProps}
|
||||||
|
onClick={() => {
|
||||||
|
setShowSearchModal(true)
|
||||||
|
}}>
|
||||||
|
<SearchInputWithSpinner readOnly placeholder={getString('codeSearch') + ` (ctrl-k)`} query={''} setQuery={noop} />
|
||||||
|
{<img src={svg} width={95} height={22} />}
|
||||||
|
{showSearchModal && (
|
||||||
|
<Container onClick={Utils.stopEvent}>
|
||||||
|
<Dialog
|
||||||
|
className={css.searchModal}
|
||||||
|
backdropClassName={css.backdrop}
|
||||||
|
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}>
|
||||||
|
<Search className={css.searchIcon} width={18} height={18} color="var(--ai-purple-600)" />
|
||||||
|
<SearchInputWithSpinner
|
||||||
|
placeholder={getString('codeSearchModal')}
|
||||||
|
spinnerPosition="right"
|
||||||
|
query={search}
|
||||||
|
type="text"
|
||||||
|
setQuery={q => {
|
||||||
|
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 && <img src={svg} width={132} height={28} />}
|
||||||
|
</Container>
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.AI}
|
||||||
|
text={getString('search')}
|
||||||
|
size={ButtonSize.MEDIUM}
|
||||||
|
onClick={onSearch}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Container>
|
||||||
|
<Text className={css.sectionHeader}>{getString('searchHeader')}</Text>
|
||||||
|
<Container>
|
||||||
|
<Layout.Vertical spacing="medium">
|
||||||
|
{searchSampleQueries.map((sampleQuery, index) => (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
className={cx(css.sampleQuery, { [css.selected]: index === searchSampleQueryIndex - 1 })}
|
||||||
|
{...ButtonRoleProps}
|
||||||
|
onClick={() => {
|
||||||
|
performSearch(sampleQuery)
|
||||||
|
}}>
|
||||||
|
{sampleQuery}
|
||||||
|
<LongArrowDownLeft color="" />
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Container>
|
||||||
|
</Layout.Vertical>
|
||||||
|
</Container>
|
||||||
|
</Dialog>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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?`
|
||||||
|
]
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
@ -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);
|
||||||
|
}
|
||||||
|
}
|
22
web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts
vendored
Normal file
22
web/src/components/KeywordSearchbar/KeywordSearchbar.module.scss.d.ts
vendored
Normal file
@ -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
|
80
web/src/components/KeywordSearchbar/KeywordSearchbar.tsx
Normal file
80
web/src/components/KeywordSearchbar/KeywordSearchbar.tsx
Normal file
@ -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<HTMLElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYWORD_REGEX = /((?:(?:-{0,1})(?:repo|lang|file|case|count)):\S*|(?: or|and ))/gi
|
||||||
|
|
||||||
|
const KeywordSearchbar: FC<KeywordSearchbarProps> = ({ value, onChange, onSearch, onKeyDown }) => {
|
||||||
|
return (
|
||||||
|
<div className={css.searchCtn}>
|
||||||
|
<div className={css.textCtn}>
|
||||||
|
{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}
|
||||||
|
setQuery={onChange}
|
||||||
|
onSearch={onSearch}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeywordSearchbar
|
@ -132,6 +132,7 @@ export interface StringsMap {
|
|||||||
checkRuns: string
|
checkRuns: string
|
||||||
checkSuites: string
|
checkSuites: string
|
||||||
checks: string
|
checks: string
|
||||||
|
clear: string
|
||||||
clickHereToDownload: string
|
clickHereToDownload: string
|
||||||
clone: string
|
clone: string
|
||||||
cloneHTTPS: string
|
cloneHTTPS: string
|
||||||
@ -389,6 +390,7 @@ export interface StringsMap {
|
|||||||
isRequired: string
|
isRequired: string
|
||||||
key: string
|
key: string
|
||||||
killed: string
|
killed: string
|
||||||
|
language: string
|
||||||
leaveAComment: string
|
leaveAComment: string
|
||||||
license: string
|
license: string
|
||||||
lineBreaks: string
|
lineBreaks: string
|
||||||
@ -407,6 +409,7 @@ export interface StringsMap {
|
|||||||
missingPermsContent: string
|
missingPermsContent: string
|
||||||
myComments: string
|
myComments: string
|
||||||
nDays: string
|
nDays: string
|
||||||
|
nMoreMatches: string
|
||||||
name: string
|
name: string
|
||||||
nameYourBranch: string
|
nameYourBranch: string
|
||||||
nameYourFile: string
|
nameYourFile: string
|
||||||
@ -690,6 +693,7 @@ export interface StringsMap {
|
|||||||
resolve: string
|
resolve: string
|
||||||
resolved: string
|
resolved: string
|
||||||
resolvedComments: string
|
resolvedComments: string
|
||||||
|
results: string
|
||||||
reviewerNotFound: string
|
reviewerNotFound: string
|
||||||
reviewers: string
|
reviewers: string
|
||||||
role: string
|
role: string
|
||||||
@ -701,6 +705,7 @@ export interface StringsMap {
|
|||||||
scrollToTop: string
|
scrollToTop: string
|
||||||
search: string
|
search: string
|
||||||
searchBranches: string
|
searchBranches: string
|
||||||
|
searchExamples: string
|
||||||
searchHeader: string
|
searchHeader: string
|
||||||
searchResult: string
|
searchResult: string
|
||||||
secret: string
|
secret: string
|
||||||
@ -721,8 +726,11 @@ export interface StringsMap {
|
|||||||
'secrets.secretUpdated': string
|
'secrets.secretUpdated': string
|
||||||
'secrets.showValue': string
|
'secrets.showValue': string
|
||||||
'secrets.updateSecret': string
|
'secrets.updateSecret': string
|
||||||
|
seeNMoreMatches: string
|
||||||
selectBranchPlaceHolder: string
|
selectBranchPlaceHolder: string
|
||||||
|
selectLanguagePlaceholder: string
|
||||||
selectRange: string
|
selectRange: string
|
||||||
|
selectRepositoryPlaceholder: string
|
||||||
selectSpace: string
|
selectSpace: string
|
||||||
selectSpaceText: string
|
selectSpaceText: string
|
||||||
selectStatuses: string
|
selectStatuses: string
|
||||||
@ -734,7 +742,9 @@ export interface StringsMap {
|
|||||||
showCommitHistory: string
|
showCommitHistory: string
|
||||||
showEverything: string
|
showEverything: string
|
||||||
showLess: string
|
showLess: string
|
||||||
|
showLessMatches: string
|
||||||
showMore: string
|
showMore: string
|
||||||
|
showNMoreMatches: string
|
||||||
signIn: string
|
signIn: string
|
||||||
signUp: string
|
signUp: string
|
||||||
skipped: string
|
skipped: string
|
||||||
|
@ -943,3 +943,13 @@ pipelineConfig:
|
|||||||
label: Pipeline Configuration
|
label: Pipeline Configuration
|
||||||
yamlUpdated: It looks like the YAML got modified. Refresh changes?
|
yamlUpdated: It looks like the YAML got modified. Refresh changes?
|
||||||
discard: Discard
|
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
|
@ -50,6 +50,8 @@ export const DefaultMenu: React.FC = () => {
|
|||||||
return !isGitRev(ref) ? ref : ''
|
return !isGitRev(ref) ? ref : ''
|
||||||
}, [commitRef, gitRef])
|
}, [commitRef, gitRef])
|
||||||
|
|
||||||
|
const isSemanticSearchEnabled = false
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
<Layout.Vertical spacing="small">
|
<Layout.Vertical spacing="small">
|
||||||
@ -159,15 +161,26 @@ export const DefaultMenu: React.FC = () => {
|
|||||||
data-code-repo-section="search"
|
data-code-repo-section="search"
|
||||||
isSubLink
|
isSubLink
|
||||||
label={getString('search')}
|
label={getString('search')}
|
||||||
to={routes.toCODESearch({
|
to={
|
||||||
repoPath
|
isSemanticSearchEnabled
|
||||||
})}
|
? routes.toCODESemanticSearch({ repoPath })
|
||||||
|
: `${routes.toCODERepositorySearch({ repoPath })}?q=repo:${repoPath}`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
</Container>
|
</Container>
|
||||||
</Render>
|
</Render>
|
||||||
|
|
||||||
|
<Render when={selectedSpace}>
|
||||||
|
<NavMenuItem
|
||||||
|
icon="thinner-search"
|
||||||
|
data-code-repo-section="search"
|
||||||
|
label={getString('search')}
|
||||||
|
to={routes.toCODEProjectSearch({ space: selectedSpace?.path as string })}
|
||||||
|
/>
|
||||||
|
</Render>
|
||||||
|
|
||||||
{standalone && (
|
{standalone && (
|
||||||
<Render when={selectedSpace}>
|
<Render when={selectedSpace}>
|
||||||
<NavMenuItem
|
<NavMenuItem
|
||||||
|
@ -47,6 +47,7 @@ import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
|||||||
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
||||||
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
|
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
|
||||||
import { RepoPublicLabel } from 'components/RepoPublicLabel/RepoPublicLabel'
|
import { RepoPublicLabel } from 'components/RepoPublicLabel/RepoPublicLabel'
|
||||||
|
import KeywordSearch from 'components/CodeSearch/KeywordSearch'
|
||||||
import noRepoImage from './no-repo.svg'
|
import noRepoImage from './no-repo.svg'
|
||||||
import FeatureMap from './FeatureMap/FeatureMap'
|
import FeatureMap from './FeatureMap/FeatureMap'
|
||||||
import css from './RepositoriesListing.module.scss'
|
import css from './RepositoriesListing.module.scss'
|
||||||
@ -62,7 +63,7 @@ export default function RepositoriesListing() {
|
|||||||
const [nameTextWidth, setNameTextWidth] = useState(600)
|
const [nameTextWidth, setNameTextWidth] = useState(600)
|
||||||
const space = useGetSpaceParam()
|
const space = useGetSpaceParam()
|
||||||
const [searchTerm, setSearchTerm] = useState<string | undefined>()
|
const [searchTerm, setSearchTerm] = useState<string | undefined>()
|
||||||
const { routes } = useAppContext()
|
const { routes, standalone } = useAppContext()
|
||||||
const { updateQueryParams } = useUpdateQueryParams()
|
const { updateQueryParams } = useUpdateQueryParams()
|
||||||
const pageBrowser = useQueryParams<PageBrowserProps>()
|
const pageBrowser = useQueryParams<PageBrowserProps>()
|
||||||
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
|
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
|
||||||
@ -197,7 +198,7 @@ export default function RepositoriesListing() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
<PageHeader title={getString('repositories')} />
|
<PageHeader title={getString('repositories')} toolbar={standalone ? null : <KeywordSearch />} />
|
||||||
<PageBody
|
<PageBody
|
||||||
className={cx({ [css.withError]: !!error })}
|
className={cx({ [css.withError]: !!error })}
|
||||||
error={error ? getErrorMessage(error) : null}
|
error={error ? getErrorMessage(error) : null}
|
||||||
|
@ -70,146 +70,32 @@
|
|||||||
background: var(--grey-100);
|
background: var(--grey-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.searchBox {
|
.searchBoxCtn {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: -50px;
|
||||||
|
z-index: 2;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
input,
|
||||||
|
input:focus {
|
||||||
|
border: 1px solid var(--grey-200) !important;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 350px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
top: 5px;
|
||||||
top: -50px;
|
right: 6px;
|
||||||
z-index: 2;
|
|
||||||
padding-bottom: 0 !important;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
input,
|
|
||||||
input:focus {
|
|
||||||
border: 1px solid var(--ai-purple-600) !important;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 350px !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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,19 +16,10 @@
|
|||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// This is an auto-generated file
|
// This is an auto-generated file
|
||||||
export declare const backdrop: string
|
|
||||||
export declare const btnColorFix: string
|
export declare const btnColorFix: string
|
||||||
export declare const layout: string
|
|
||||||
export declare const main: string
|
export declare const main: string
|
||||||
export declare const mainBorder: string
|
export declare const mainBorder: string
|
||||||
export declare const mainContainer: string
|
export declare const mainContainer: string
|
||||||
export declare const portal: string
|
|
||||||
export declare const refRoot: string
|
export declare const refRoot: string
|
||||||
export declare const rootSlash: string
|
export declare const rootSlash: string
|
||||||
export declare const sampleQuery: string
|
export declare const searchBoxCtn: 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
|
|
||||||
|
@ -14,23 +14,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { noop } from 'lodash-es'
|
import { Container, Layout, Button, FlexExpander, ButtonVariation, Text } from '@harnessio/uicore'
|
||||||
import {
|
|
||||||
Container,
|
|
||||||
Layout,
|
|
||||||
Button,
|
|
||||||
ButtonSize,
|
|
||||||
FlexExpander,
|
|
||||||
ButtonVariation,
|
|
||||||
Text,
|
|
||||||
Utils,
|
|
||||||
Dialog
|
|
||||||
} from '@harnessio/uicore'
|
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import { Icon } from '@harnessio/icons'
|
import { Icon } from '@harnessio/icons'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
|
||||||
import { LongArrowDownLeft, Search } from 'iconoir-react'
|
|
||||||
import { Color } from '@harnessio/design-system'
|
import { Color } from '@harnessio/design-system'
|
||||||
import { Breadcrumbs, IBreadcrumbProps } from '@blueprintjs/core'
|
import { Breadcrumbs, IBreadcrumbProps } from '@blueprintjs/core'
|
||||||
import { Link, useHistory } from 'react-router-dom'
|
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 { 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 { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||||
import { ButtonRoleProps, permissionProps } from 'utils/Utils'
|
import { permissionProps } from 'utils/Utils'
|
||||||
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
|
|
||||||
import svg from './search-background.svg'
|
|
||||||
import css from './ContentHeader.module.scss'
|
import css from './ContentHeader.module.scss'
|
||||||
|
|
||||||
export function ContentHeader({
|
export function ContentHeader({
|
||||||
@ -57,37 +43,6 @@ export function ContentHeader({
|
|||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const _isDir = isDir(resourceContent)
|
const _isDir = isDir(resourceContent)
|
||||||
const space = useGetSpaceParam()
|
const space = useGetSpaceParam()
|
||||||
const [showSearchModal, setShowSearchModal] = useState(false)
|
|
||||||
const [searchSampleQueryIndex, setSearchSampleQueryIndex] = useState<number>(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?.(
|
const permPushResult = hooks?.usePermissionTranslate?.(
|
||||||
{
|
{
|
||||||
@ -200,109 +155,7 @@ export function ContentHeader({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
{!standalone && false && (
|
<div className={css.searchBoxCtn}>{!standalone ? <KeywordSearch repoMetadata={repoMetadata} /> : null}</div>
|
||||||
<Container
|
|
||||||
className={css.searchBox}
|
|
||||||
{...ButtonRoleProps}
|
|
||||||
onClick={() => {
|
|
||||||
setShowSearchModal(true)
|
|
||||||
}}>
|
|
||||||
<SearchInputWithSpinner
|
|
||||||
readOnly
|
|
||||||
placeholder={getString('codeSearch') + ` (ctrl-k)`}
|
|
||||||
query={''}
|
|
||||||
setQuery={noop}
|
|
||||||
/>
|
|
||||||
{<img src={svg} width={95} height={22} />}
|
|
||||||
|
|
||||||
{showSearchModal && (
|
|
||||||
<Container onClick={Utils.stopEvent}>
|
|
||||||
<Dialog
|
|
||||||
className={css.searchModal}
|
|
||||||
backdropClassName={css.backdrop}
|
|
||||||
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}>
|
|
||||||
<Search className={css.searchIcon} width={18} height={18} color="var(--ai-purple-600)" />
|
|
||||||
<SearchInputWithSpinner
|
|
||||||
placeholder={getString('codeSearchModal')}
|
|
||||||
spinnerPosition="right"
|
|
||||||
query={search}
|
|
||||||
type="text"
|
|
||||||
setQuery={q => {
|
|
||||||
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 && <img src={svg} width={132} height={28} />}
|
|
||||||
</Container>
|
|
||||||
<Button
|
|
||||||
variation={ButtonVariation.AI}
|
|
||||||
text={getString('search')}
|
|
||||||
size={ButtonSize.MEDIUM}
|
|
||||||
onClick={onSearch}
|
|
||||||
/>
|
|
||||||
</Layout.Horizontal>
|
|
||||||
</Container>
|
|
||||||
<Text className={css.sectionHeader}>{getString('searchHeader')}</Text>
|
|
||||||
<Container>
|
|
||||||
<Layout.Vertical spacing="medium">
|
|
||||||
{searchSampleQueries.map((sampleQuery, index) => (
|
|
||||||
<Text
|
|
||||||
key={index}
|
|
||||||
className={cx(css.sampleQuery, { [css.selected]: index === searchSampleQueryIndex - 1 })}
|
|
||||||
{...ButtonRoleProps}
|
|
||||||
onClick={() => {
|
|
||||||
performSearch(sampleQuery)
|
|
||||||
}}>
|
|
||||||
{sampleQuery}
|
|
||||||
<LongArrowDownLeft color="" />
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</Layout.Vertical>
|
|
||||||
</Container>
|
|
||||||
</Layout.Vertical>
|
|
||||||
</Container>
|
|
||||||
</Dialog>
|
|
||||||
</Container>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
)}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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?`
|
|
||||||
]
|
|
||||||
|
@ -22,6 +22,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.highlight {
|
||||||
|
background-color: var(--yellow-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
|
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -32,6 +32,8 @@ import { Color } from '@harnessio/design-system'
|
|||||||
import { Document, Page, pdfjs } from 'react-pdf'
|
import { Document, Page, pdfjs } from 'react-pdf'
|
||||||
import { Render, Match, Truthy, Falsy, Case, Else } from 'react-jsx-match'
|
import { Render, Match, Truthy, Falsy, Case, Else } from 'react-jsx-match'
|
||||||
import { Link, useHistory } from 'react-router-dom'
|
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 { SourceCodeViewer } from 'components/SourceCodeViewer/SourceCodeViewer'
|
||||||
import type { OpenapiContentInfo, RepoFileContent, TypesCommit } from 'services/code'
|
import type { OpenapiContentInfo, RepoFileContent, TypesCommit } from 'services/code'
|
||||||
import {
|
import {
|
||||||
@ -54,6 +56,7 @@ import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButto
|
|||||||
import { PlainButton } from 'components/PlainButton/PlainButton'
|
import { PlainButton } from 'components/PlainButton/PlainButton'
|
||||||
import { CommitsView } from 'components/CommitsView/CommitsView'
|
import { CommitsView } from 'components/CommitsView/CommitsView'
|
||||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||||
|
import { useQueryParams } from 'hooks/useQueryParams'
|
||||||
import { FileCategory, RepoContentExtended, useFileContentViewerDecision } from 'utils/FileUtils'
|
import { FileCategory, RepoContentExtended, useFileContentViewerDecision } from 'utils/FileUtils'
|
||||||
import { useDownloadRawFile } from 'hooks/useDownloadRawFile'
|
import { useDownloadRawFile } from 'hooks/useDownloadRawFile'
|
||||||
import { usePageIndex } from 'hooks/usePageIndex'
|
import { usePageIndex } from 'hooks/usePageIndex'
|
||||||
@ -162,6 +165,34 @@ export function FileContent({
|
|||||||
[editButtonDisabled, isFileTooLarge, category]
|
[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 (
|
return (
|
||||||
<Container className={css.tabsContainer} ref={ref}>
|
<Container className={css.tabsContainer} ref={ref}>
|
||||||
<Tabs
|
<Tabs
|
||||||
@ -409,6 +440,7 @@ export function FileContent({
|
|||||||
</Case>
|
</Case>
|
||||||
<Case val={FileCategory.TEXT}>
|
<Case val={FileCategory.TEXT}>
|
||||||
<SourceCodeViewer
|
<SourceCodeViewer
|
||||||
|
editorDidMount={onEditorMount}
|
||||||
language={filenameToLanguage(filename)}
|
language={filenameToLanguage(filename)}
|
||||||
source={decodeGitContent(base64Data)}
|
source={decodeGitContent(base64Data)}
|
||||||
/>
|
/>
|
||||||
|
332
web/src/pages/Search/KeywordSearch.tsx
Normal file
332
web/src/pages/Search/KeywordSearch.tsx
Normal file
@ -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<SelectOption[]>([])
|
||||||
|
const [selectedLanguage, setSelectedLanguage] = useState<string>()
|
||||||
|
|
||||||
|
const [searchResults, setSearchResults] = useState<KeywordSearchResponse>()
|
||||||
|
|
||||||
|
const { mutate, loading: isSearching } = useMutate<KeywordSearchResponse>({
|
||||||
|
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 (
|
||||||
|
<Container className={css.main}>
|
||||||
|
<Container padding="medium" border={{ bottom: true }} flex className={css.header}>
|
||||||
|
<KeywordSearchbar
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={text => {
|
||||||
|
setSearchResults(undefined)
|
||||||
|
setSearchTerm(text)
|
||||||
|
updateQueryParams({ q: text })
|
||||||
|
debouncedSearch(text)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
<Container padding="xlarge">
|
||||||
|
<LoadingSpinner visible={isSearching} />
|
||||||
|
<KeywordSearchFilters
|
||||||
|
isRepoLevelSearch={Boolean(repoName)}
|
||||||
|
selectedLanguage={selectedLanguage}
|
||||||
|
selectedRepositories={selectedRepositories}
|
||||||
|
setLanguage={setSelectedLanguage}
|
||||||
|
setRepositories={setSelectedRepositories}
|
||||||
|
/>
|
||||||
|
{searchResults?.file_matches.length ? (
|
||||||
|
<>
|
||||||
|
<Layout.Horizontal spacing="xsmall" margin={{ bottom: 'large' }}>
|
||||||
|
<Text font={{ variation: FontVariation.SMALL_SEMI }}>
|
||||||
|
{searchResults?.stats.total_files} {getString('files')}
|
||||||
|
</Text>
|
||||||
|
<Text font={{ variation: FontVariation.SMALL_SEMI }} color={Color.GREY_400}>
|
||||||
|
{getString('results')}
|
||||||
|
</Text>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
{searchResults?.file_matches?.map(fileMatch => {
|
||||||
|
return <SearchResult key={fileMatch.file_name} fileMatch={fileMatch} searchTerm={searchTerm} />
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<NoResultCard showWhen={() => !isSearching && !searchResults?.file_matches?.length} forSearch={true} />
|
||||||
|
</Container>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Container className={css.searchResult}>
|
||||||
|
<Layout.Horizontal spacing="small" className={css.resultHeader}>
|
||||||
|
<Icon name={isCollapsed ? 'chevron-up' : 'chevron-down'} {...ButtonRoleProps} onClick={setIsCollapsed} />
|
||||||
|
<Link to={routes.toCODERepository({ repoPath: fileMatch.repo_path })}>
|
||||||
|
<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: fileMatch.repo_path,
|
||||||
|
gitRef: fileMatch.repo_branch,
|
||||||
|
resourcePath: fileMatch.file_name
|
||||||
|
})}>
|
||||||
|
<Text font={{ variation: FontVariation.SMALL_BOLD }} color={Color.PRIMARY_7}>
|
||||||
|
<Keywords value={searchTerm} backgroundColor="var(--yellow-300">
|
||||||
|
{fileMatch.file_name}
|
||||||
|
</Keywords>
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
<div className={cx({ [css.isCollapsed]: isCollapsed })}>
|
||||||
|
{collapsedCodeBlocks.map((codeBlock, index) => {
|
||||||
|
const showMoreMatchesFooter = codeBlocks.length > 2 && index === collapsedCodeBlocks.length - 1
|
||||||
|
|
||||||
|
/** File Match */
|
||||||
|
if (codeBlock.lineNumberOffset === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Editor
|
||||||
|
inGitBlame
|
||||||
|
content={codeBlock.codeBlock}
|
||||||
|
standalone={true}
|
||||||
|
readonly={true}
|
||||||
|
repoMetadata={undefined}
|
||||||
|
filename={fileMatch.file_name}
|
||||||
|
extensions={[
|
||||||
|
matchDecoratorPlugin,
|
||||||
|
lineNumbers({
|
||||||
|
formatNumber: n => String(n - 1 + codeBlock.lineNumberOffset)
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
className={css.editorCtn}
|
||||||
|
/>
|
||||||
|
{showMoreMatchesFooter ? (
|
||||||
|
<Layout.Horizontal
|
||||||
|
spacing="small"
|
||||||
|
className={css.showMoreCtn}
|
||||||
|
onClick={() => setShowMoreMatches(prevVal => !prevVal)}
|
||||||
|
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
|
||||||
|
{...ButtonRoleProps}>
|
||||||
|
{!showMoreMatchs ? <Plus /> : <Minus />}
|
||||||
|
<Text font={{ variation: FontVariation.TINY_SEMI }} color={Color.GREY_400}>
|
||||||
|
{!showMoreMatchs
|
||||||
|
? getString('showNMoreMatches', { n: codeBlocks.length - 2 })
|
||||||
|
: getString('showLessMatches')}
|
||||||
|
</Text>
|
||||||
|
{codeBlocks.length > 25 && showMoreMatchs ? (
|
||||||
|
<Text
|
||||||
|
font={{ variation: FontVariation.TINY }}
|
||||||
|
border={{ left: true }}
|
||||||
|
padding={{ left: 'small' }}
|
||||||
|
color={Color.GREY_400}>
|
||||||
|
{getString('nMoreMatches', { n: codeBlocks.length - 25 })}{' '}
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
to={`${routes.toCODERepository({
|
||||||
|
repoPath: fileMatch.repo_path,
|
||||||
|
gitRef: fileMatch.repo_branch,
|
||||||
|
resourcePath: fileMatch.file_name
|
||||||
|
})}?keyword=${searchTerm}`}>
|
||||||
|
{getString('seeNMoreMatches', { n: codeBlocks.length })}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Layout.Horizontal>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
30
web/src/pages/Search/KeywordSearch.types.ts
Normal file
30
web/src/pages/Search/KeywordSearch.types.ts
Normal file
@ -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
|
||||||
|
}
|
107
web/src/pages/Search/KeywordSearchFilters.tsx
Normal file
107
web/src/pages/Search/KeywordSearchFilters.tsx
Normal file
@ -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<SetStateAction<SelectOption[]>>
|
||||||
|
selectedLanguage?: string
|
||||||
|
setLanguage: Dispatch<SetStateAction<string | undefined>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeywordSearchFilters: React.FC<KeywordSearchFiltersProps> = ({
|
||||||
|
isRepoLevelSearch,
|
||||||
|
selectedLanguage,
|
||||||
|
selectedRepositories,
|
||||||
|
setLanguage,
|
||||||
|
setRepositories
|
||||||
|
}) => {
|
||||||
|
const { getString } = useStrings()
|
||||||
|
const space = useGetSpaceParam()
|
||||||
|
|
||||||
|
const { data } = useGet<TypesRepository[]>({
|
||||||
|
path: `/api/v1/spaces/${space}/+/repos`,
|
||||||
|
debounce: 500,
|
||||||
|
lazy: isRepoLevelSearch
|
||||||
|
})
|
||||||
|
|
||||||
|
const repositoryOptions =
|
||||||
|
data?.map(repository => ({
|
||||||
|
label: String(repository.uid),
|
||||||
|
value: String(repository.path)
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={css.filtersCtn}>
|
||||||
|
{isRepoLevelSearch ? null : (
|
||||||
|
<Container>
|
||||||
|
<Text font={{ variation: FontVariation.SMALL_SEMI }} color={Color.GREY_600} margin={{ bottom: 'xsmall' }}>
|
||||||
|
{getString('pageTitle.repository')}
|
||||||
|
</Text>
|
||||||
|
<MultiSelectDropDown
|
||||||
|
className={css.multiSelect}
|
||||||
|
value={selectedRepositories}
|
||||||
|
placeholder={selectedRepositories.length ? '' : getString('selectRepositoryPlaceholder')}
|
||||||
|
onChange={setRepositories}
|
||||||
|
items={repositoryOptions}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
<Container>
|
||||||
|
<Text font={{ variation: FontVariation.SMALL_SEMI }} color={Color.GREY_600} margin={{ bottom: 'xsmall' }}>
|
||||||
|
{getString('language')}
|
||||||
|
</Text>
|
||||||
|
<DropDown
|
||||||
|
className={css.multiSelect}
|
||||||
|
value={selectedLanguage}
|
||||||
|
placeholder={getString('selectLanguagePlaceholder')}
|
||||||
|
onChange={option => setLanguage(String(option.value))}
|
||||||
|
items={languageOptions}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
<Button
|
||||||
|
variation={ButtonVariation.LINK}
|
||||||
|
text={getString('clear')}
|
||||||
|
onClick={() => {
|
||||||
|
setRepositories([])
|
||||||
|
setLanguage(undefined)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeywordSearchFilters
|
@ -249,3 +249,93 @@
|
|||||||
display: none !important;
|
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);
|
||||||
|
}
|
||||||
|
8
web/src/pages/Search/Search.module.scss.d.ts
vendored
8
web/src/pages/Search/Search.module.scss.d.ts
vendored
@ -17,12 +17,18 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// This is an auto-generated file
|
// This is an auto-generated file
|
||||||
export declare const aiLabel: string
|
export declare const aiLabel: string
|
||||||
|
export declare const editorCtn: string
|
||||||
export declare const fileContent: string
|
export declare const fileContent: string
|
||||||
export declare const filename: string
|
export declare const filename: string
|
||||||
export declare const filePath: 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 highlightLineNumber: string
|
||||||
|
export declare const isCollapsed: string
|
||||||
export declare const layout: string
|
export declare const layout: string
|
||||||
export declare const main: string
|
export declare const main: string
|
||||||
|
export declare const multiSelect: string
|
||||||
export declare const noResult: string
|
export declare const noResult: string
|
||||||
export declare const noResultContainer: string
|
export declare const noResultContainer: string
|
||||||
export declare const pageHeader: string
|
export declare const pageHeader: string
|
||||||
@ -30,9 +36,11 @@ export declare const path: string
|
|||||||
export declare const pathText: string
|
export declare const pathText: string
|
||||||
export declare const preview: string
|
export declare const preview: string
|
||||||
export declare const result: string
|
export declare const result: string
|
||||||
|
export declare const resultHeader: string
|
||||||
export declare const resultTitle: string
|
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 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