mirror of
https://github.com/harness/drone.git
synced 2025-05-12 23:20:10 +08:00
Implement Semantic Search (#440)
This commit is contained in:
parent
c41d944d44
commit
ce6d798fb2
@ -35,6 +35,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',
|
||||
'./WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
|
||||
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx'
|
||||
},
|
||||
|
@ -73,7 +73,7 @@ export interface CODERoutes {
|
||||
toCODEWebhookNew: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||
toCODEWebhookDetails: (args: Required<Pick<CODEProps, 'repoPath' | 'webhookId'>>) => string
|
||||
toCODESettings: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||
|
||||
toCODESearch: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||
toCODEExecutions: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
||||
toCODEExecution: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline' | 'execution'>>) => string
|
||||
toCODESecret: (args: Required<Pick<CODEProps, 'space' | 'secret'>>) => string
|
||||
@ -126,6 +126,7 @@ export const routes: CODERoutes = {
|
||||
toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`,
|
||||
toCODETags: ({ repoPath }) => `/${repoPath}/tags`,
|
||||
toCODESettings: ({ repoPath }) => `/${repoPath}/settings`,
|
||||
toCODESearch: ({ repoPath }) => `/${repoPath}/search`,
|
||||
toCODEWebhooks: ({ repoPath }) => `/${repoPath}/webhooks`,
|
||||
toCODEWebhookNew: ({ repoPath }) => `/${repoPath}/webhooks/new`,
|
||||
toCODEWebhookDetails: ({ repoPath, webhookId }) => `/${repoPath}/webhook/${webhookId}`,
|
||||
|
@ -30,6 +30,7 @@ import { useStrings } from 'framework/strings'
|
||||
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 AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline'
|
||||
import { useAppContext } from 'AppContext'
|
||||
|
||||
@ -249,6 +250,12 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
||||
</LayoutWithSideNav>
|
||||
</Route>
|
||||
|
||||
<Route path={routes.toCODESearch({ repoPath })} exact>
|
||||
<LayoutWithSideNav title={getString('pageTitle.search')}>
|
||||
<Search />
|
||||
</LayoutWithSideNav>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={routes.toCODEFileEdit({
|
||||
repoPath,
|
||||
|
@ -11,7 +11,7 @@ import { EditorView, keymap, placeholder as placeholderExtension } from '@codemi
|
||||
import { Compartment, EditorState, Extension } from '@codemirror/state'
|
||||
import { color } from '@uiw/codemirror-extensions-color'
|
||||
import { hyperLink } from '@uiw/codemirror-extensions-hyper-link'
|
||||
import { githubLight as theme } from '@uiw/codemirror-themes-all'
|
||||
import { githubLight, githubDark } from '@uiw/codemirror-themes-all'
|
||||
import css from './Editor.module.scss'
|
||||
|
||||
export interface EditorProps {
|
||||
@ -28,6 +28,7 @@ export interface EditorProps {
|
||||
setDirty?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
onChange?: (doc: Text, viewUpdate: ViewUpdate, isDirty: boolean) => void
|
||||
onViewUpdate?: (viewUpdate: ViewUpdate) => void
|
||||
darkTheme?: boolean
|
||||
}
|
||||
|
||||
export const Editor = React.memo(function CodeMirrorReactEditor({
|
||||
@ -43,8 +44,10 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
|
||||
viewRef,
|
||||
setDirty,
|
||||
onChange,
|
||||
onViewUpdate
|
||||
onViewUpdate,
|
||||
darkTheme
|
||||
}: EditorProps) {
|
||||
const contentRef = useRef(content)
|
||||
const view = useRef<EditorView>()
|
||||
const ref = useRef<HTMLDivElement>()
|
||||
const languageConfig = useMemo(() => new Compartment(), [])
|
||||
@ -70,7 +73,7 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
|
||||
|
||||
color,
|
||||
hyperLink,
|
||||
theme,
|
||||
darkTheme ? githubDark : githubLight,
|
||||
|
||||
EditorView.lineWrapping,
|
||||
|
||||
@ -136,5 +139,14 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
|
||||
}
|
||||
}, [filename, forMarkdown, view, languageConfig, markdownLanguageSupport])
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current !== content) {
|
||||
contentRef.current = content
|
||||
viewRef?.current?.dispatch({
|
||||
changes: { from: 0, to: viewRef?.current?.state.doc.length, insert: content }
|
||||
})
|
||||
}
|
||||
}, [content, viewRef])
|
||||
|
||||
return <Container ref={ref} className={cx(css.editor, className)} style={style} />
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import { Container, Layout, Text, PageHeader } from '@harnessio/uicore'
|
||||
import { Container, Layout, Text, PageHeader, PageHeaderProps } from '@harnessio/uicore'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
@ -19,33 +19,35 @@ interface RepositoryPageHeaderProps extends Optional<Pick<GitInfoProps, 'repoMet
|
||||
title: string | JSX.Element
|
||||
dataTooltipId: string
|
||||
extraBreadcrumbLinks?: BreadcrumbLink[]
|
||||
className?: string
|
||||
content?: PageHeaderProps['content']
|
||||
}
|
||||
|
||||
export function RepositoryPageHeader({
|
||||
repoMetadata,
|
||||
title,
|
||||
dataTooltipId,
|
||||
extraBreadcrumbLinks = []
|
||||
extraBreadcrumbLinks = [],
|
||||
className,
|
||||
content
|
||||
}: RepositoryPageHeaderProps) {
|
||||
const { gitRef } = useParams<CODEProps>()
|
||||
const { getString } = useStrings()
|
||||
const space = useGetSpaceParam()
|
||||
const { routes } = useAppContext()
|
||||
|
||||
if (!repoMetadata) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<PageHeader
|
||||
className={className}
|
||||
content={content}
|
||||
title=""
|
||||
breadcrumbs={
|
||||
<Container className={css.header}>
|
||||
<Layout.Horizontal spacing="small" className={css.breadcrumb}>
|
||||
<Link to={routes.toCODERepositories({ space })}>{getString('repositories')}</Link>
|
||||
<Icon name="main-chevron-right" size={8} color={Color.GREY_500} />
|
||||
<Link to={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef })}>
|
||||
{repoMetadata.uid}
|
||||
<Link to={routes.toCODERepository({ repoPath: (repoMetadata?.path as string) || '', gitRef })}>
|
||||
{repoMetadata?.uid || ''}
|
||||
</Link>
|
||||
{extraBreadcrumbLinks.map(link => (
|
||||
<Fragment key={link.url}>
|
||||
|
@ -16,6 +16,7 @@ interface SearchInputWithSpinnerProps {
|
||||
icon?: IconName
|
||||
spinnerIcon?: IconName
|
||||
spinnerPosition?: 'left' | 'right'
|
||||
onSearch?: (searchTerm: string) => void
|
||||
}
|
||||
|
||||
export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
@ -26,7 +27,8 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
placeholder,
|
||||
icon = 'search',
|
||||
spinnerIcon = 'steps-spinner',
|
||||
spinnerPosition = 'left'
|
||||
spinnerPosition = 'left',
|
||||
onSearch
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const spinner = <Icon name={spinnerIcon as IconName} color={Color.PRIMARY_7} />
|
||||
@ -37,6 +39,7 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
<Layout.Horizontal className={css.layout}>
|
||||
<Render when={loading && !spinnerOnRight}>{spinner}</Render>
|
||||
<TextInput
|
||||
type="search"
|
||||
value={query}
|
||||
wrapperClassName={cx(css.wrapper, { [css.spinnerOnRight]: spinnerOnRight })}
|
||||
className={css.input}
|
||||
@ -45,7 +48,14 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||
style={{ width }}
|
||||
autoFocus
|
||||
onFocus={event => event.target.select()}
|
||||
onInput={event => setQuery(event.currentTarget.value || '')}
|
||||
onInput={event => {
|
||||
setQuery(event.currentTarget.value || '')
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onSearch?.((e as unknown as React.FormEvent<HTMLInputElement>).currentTarget.value || '')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Render when={loading && spinnerOnRight}>{spinner}</Render>
|
||||
</Layout.Horizontal>
|
||||
|
@ -48,7 +48,7 @@
|
||||
width: min(calc(100vw - var(--nav-menu-width)), 840px) !important;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 5px;
|
||||
left: 241px;
|
||||
top: -5px;
|
||||
|
||||
> div {
|
||||
@ -79,6 +79,13 @@
|
||||
background-color: rgba(26, 26, 26, 0.4);
|
||||
}
|
||||
|
||||
:global {
|
||||
.bp3-popover-arrow {
|
||||
left: -11px !important;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
48
web/src/components/Split/Split.module.scss
Normal file
48
web/src/components/Split/Split.module.scss
Normal file
@ -0,0 +1,48 @@
|
||||
.main {
|
||||
:global {
|
||||
.Resizer {
|
||||
background-color: var(--grey-300);
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 11px;
|
||||
margin: 0 -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
3
web/src/components/Split/Split.module.scss.d.ts
vendored
Normal file
3
web/src/components/Split/Split.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const main: string
|
8
web/src/components/Split/Split.tsx
Normal file
8
web/src/components/Split/Split.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import cx from 'classnames'
|
||||
import SplitPane, { type SplitPaneProps } from 'react-split-pane'
|
||||
import css from './Split.module.scss'
|
||||
|
||||
export const Split: React.FC<SplitPaneProps> = ({ className, ...props }) => (
|
||||
<SplitPane className={cx(css.main, className)} {...props} />
|
||||
)
|
@ -17,6 +17,7 @@ export interface StringsMap {
|
||||
addNewFile: string
|
||||
addReadMe: string
|
||||
admin: string
|
||||
aiSearch: string
|
||||
all: string
|
||||
allBranches: string
|
||||
allComments: string
|
||||
@ -65,6 +66,7 @@ export interface StringsMap {
|
||||
cloneText: string
|
||||
close: string
|
||||
closed: string
|
||||
codeSearch: string
|
||||
comment: string
|
||||
commentDeleted: string
|
||||
commit: string
|
||||
@ -307,6 +309,7 @@ export interface StringsMap {
|
||||
'pageTitle.repositories': string
|
||||
'pageTitle.repository': string
|
||||
'pageTitle.repositorySettings': string
|
||||
'pageTitle.search': string
|
||||
'pageTitle.secrets': string
|
||||
'pageTitle.signin': string
|
||||
'pageTitle.spaceSettings': string
|
||||
@ -336,6 +339,7 @@ export interface StringsMap {
|
||||
'pipelines.run': string
|
||||
'pipelines.saveAndRun': string
|
||||
'pipelines.time': string
|
||||
poweredByAI: string
|
||||
'pipelines.updated': string
|
||||
'pipelines.yamlPath': string
|
||||
'plugins.addAPlugin': string
|
||||
@ -482,6 +486,7 @@ export interface StringsMap {
|
||||
scrollToTop: string
|
||||
search: string
|
||||
searchBranches: string
|
||||
searchResult: string
|
||||
secret: string
|
||||
'secrets.createSecret': string
|
||||
'secrets.createSuccess': string
|
||||
@ -527,6 +532,7 @@ export interface StringsMap {
|
||||
'spaceSetting.setting': string
|
||||
spaces: string
|
||||
sslVerificationLabel: string
|
||||
startSearching: string
|
||||
status: string
|
||||
submitReview: string
|
||||
success: string
|
||||
@ -586,6 +592,7 @@ export interface StringsMap {
|
||||
viewAllTags: string
|
||||
viewCommitDetails: string
|
||||
viewFile: string
|
||||
viewFileHistory: string
|
||||
viewFiles: string
|
||||
viewRaw: string
|
||||
viewRepo: string
|
||||
|
@ -96,6 +96,7 @@ pageTitle:
|
||||
pipelines: Pipelines
|
||||
secrets: Secrets
|
||||
executions: Executions
|
||||
search: Search powered by AI
|
||||
repos:
|
||||
name: Repo Name
|
||||
data: Repo Data
|
||||
@ -482,7 +483,7 @@ newTag: New Tag
|
||||
overview: Overview
|
||||
fileTooLarge: File is too large to open. {download}
|
||||
clickHereToDownload: Click here to download.
|
||||
viewFile: View the file at this point in the history
|
||||
viewFileHistory: View the file at this point in the history
|
||||
viewRepo: View the repository at this point in the history
|
||||
hideCommitHistory: Renamed from {file} - Hide History
|
||||
showCommitHistory: Renamed from {file} - Show History
|
||||
@ -669,6 +670,12 @@ secrets:
|
||||
failedToDeleteSecret: Failed to delete Secret. Please try again.
|
||||
deleteSecret: Delete Secrets
|
||||
userUpdateSuccess: 'User updated successfully'
|
||||
viewFile: View File
|
||||
searchResult: 'Search Result {count}'
|
||||
aiSearch: AI Search
|
||||
codeSearch: Code Search
|
||||
startSearching: Begin search by describing what you are looking for.
|
||||
poweredByAI: Unlock the power of AI with Semantic Code search. Try phrases like "Locate the code for authentication".
|
||||
run: Run
|
||||
plugins:
|
||||
title: Plugins
|
||||
|
@ -11,6 +11,7 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
.settings {
|
||||
margin: 0 var(--spacing-medium);
|
||||
|
@ -126,15 +126,25 @@ export const DefaultMenu: React.FC = () => {
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NavMenuItem
|
||||
data-code-repo-section="settings"
|
||||
data-code-repo-section="pipelines"
|
||||
isSubLink
|
||||
label={getString('settings')}
|
||||
to={routes.toCODESettings({
|
||||
label={getString('pageTitle.pipelines')}
|
||||
to={routes.toCODEPipelines({
|
||||
repoPath
|
||||
})}
|
||||
/>
|
||||
|
||||
{!standalone && (
|
||||
<NavMenuItem
|
||||
data-code-repo-section="search"
|
||||
isSubLink
|
||||
label={getString('search')}
|
||||
to={routes.toCODESearch({
|
||||
repoPath
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
</Render>
|
||||
|
@ -1,53 +1,6 @@
|
||||
.main {
|
||||
min-height: var(--page-height);
|
||||
background-color: var(--primary-bg) !important;
|
||||
|
||||
:global {
|
||||
.Resizer {
|
||||
background-color: var(--grey-300);
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 11px;
|
||||
margin: 0 -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
|
@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'
|
||||
import cx from 'classnames'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useGet } from 'restful-react'
|
||||
import SplitPane from 'react-split-pane'
|
||||
import { routes, type CODEProps } from 'RouteDefinitions'
|
||||
import type { TypesExecution } from 'services/code'
|
||||
import ExecutionStageList from 'components/ExecutionStageList/ExecutionStageList'
|
||||
@ -12,6 +11,7 @@ import { getErrorMessage, voidFn } from 'utils/Utils'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { Split } from 'components/Split/Split'
|
||||
import { ExecutionPageHeader } from 'components/ExecutionPageHeader/ExecutionPageHeader'
|
||||
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
||||
import { ExecutionState } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
@ -102,7 +102,7 @@ const Execution = () => {
|
||||
}}>
|
||||
<LoadingSpinner visible={loading || isInitialLoad} withBorder={!!execution && isInitialLoad} />
|
||||
{execution && (
|
||||
<SplitPane split="vertical" size={300} minSize={200} maxSize={400}>
|
||||
<Split split="vertical" size={300} minSize={200} maxSize={400}>
|
||||
<ExecutionStageList
|
||||
stages={execution?.stages || []}
|
||||
setSelectedStage={setSelectedStage}
|
||||
@ -111,7 +111,7 @@ const Execution = () => {
|
||||
{selectedStage && (
|
||||
<Console stage={execution?.stages?.[selectedStage - 1]} repoPath={repoMetadata?.path as string} />
|
||||
)}
|
||||
</SplitPane>
|
||||
</Split>
|
||||
)}
|
||||
</PageBody>
|
||||
</Container>
|
||||
|
@ -139,53 +139,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.Resizer {
|
||||
background-color: var(--grey-300);
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 11px;
|
||||
margin: 0 -5px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Falsy, Match, Render, Truthy } from 'react-jsx-match'
|
||||
import { /*CheckCircle,*/ NavArrowRight } from 'iconoir-react'
|
||||
import SplitPane from 'react-split-pane'
|
||||
import { get } from 'lodash-es'
|
||||
import cx from 'classnames'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
@ -24,6 +23,7 @@ import type { GitInfoProps } from 'utils/GitUtils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useQueryParams } from 'hooks/useQueryParams'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { Split } from 'components/Split/Split'
|
||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
|
||||
import type { TypesCheck } from 'services/code'
|
||||
@ -56,7 +56,7 @@ export const Checks: React.FC<ChecksProps> = props => {
|
||||
<Container className={css.main}>
|
||||
<Match expr={props.prChecksDecisionResult?.overallStatus}>
|
||||
<Truthy>
|
||||
<SplitPane
|
||||
<Split
|
||||
split="vertical"
|
||||
size="calc(100% - 400px)"
|
||||
minSize={800}
|
||||
@ -118,7 +118,7 @@ export const Checks: React.FC<ChecksProps> = props => {
|
||||
</Falsy>
|
||||
</Match>
|
||||
</Container>
|
||||
</SplitPane>
|
||||
</Split>
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
<Container flex={{ align: 'center-center' }} height="90%">
|
||||
|
@ -1,5 +1,6 @@
|
||||
.main {
|
||||
padding: var(--spacing-large) var(--spacing-xlarge) 0 var(--spacing-xlarge) !important;
|
||||
position: relative;
|
||||
|
||||
div[class*='TextInput'] {
|
||||
margin-bottom: 0 !important;
|
||||
@ -54,7 +55,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbItem {
|
||||
white-space: nowrap !important;
|
||||
.searchBox {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: -50px;
|
||||
z-index: 2;
|
||||
padding-bottom: 0 !important;
|
||||
margin: 0;
|
||||
|
||||
input,
|
||||
input:focus {
|
||||
border: 1px solid var(--ai-purple-600) !important;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 350px !important;
|
||||
}
|
||||
|
||||
svg path {
|
||||
fill: var(--ai-purple-600) !important;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const breadcrumbItem: string
|
||||
export declare const btnColorFix: string
|
||||
export declare const main: string
|
||||
export declare const refRoot: string
|
||||
export declare const rootSlash: string
|
||||
export declare const searchBox: string
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { Container, Layout, Button, FlexExpander, ButtonVariation, Text } from '@harnessio/uicore'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { Color } from '@harnessio/design-system'
|
||||
@ -12,6 +12,8 @@ import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
||||
import { useCreateBranchModal } from 'components/CreateBranchModal/CreateBranchModal'
|
||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||
import { permissionProps } from 'utils/Utils'
|
||||
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
|
||||
import svg from './search-background.svg'
|
||||
import css from './ContentHeader.module.scss'
|
||||
|
||||
export function ContentHeader({
|
||||
@ -21,11 +23,9 @@ export function ContentHeader({
|
||||
resourceContent
|
||||
}: Pick<GitInfoProps, 'repoMetadata' | 'gitRef' | 'resourcePath' | 'resourceContent'>) {
|
||||
const { getString } = useStrings()
|
||||
const { routes } = useAppContext()
|
||||
const { routes, standalone, hooks } = useAppContext()
|
||||
const history = useHistory()
|
||||
const _isDir = isDir(resourceContent)
|
||||
const { standalone } = useAppContext()
|
||||
const { hooks } = useAppContext()
|
||||
const space = useGetSpaceParam()
|
||||
|
||||
const permPushResult = hooks?.usePermissionTranslate?.(
|
||||
@ -62,6 +62,7 @@ export function ContentHeader({
|
||||
return { href, text: _path }
|
||||
})
|
||||
}, [resourcePath, gitRef, repoMetadata.path, routes])
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
@ -139,6 +140,25 @@ export function ContentHeader({
|
||||
</>
|
||||
)}
|
||||
</Layout.Horizontal>
|
||||
{!standalone && (
|
||||
<Container className={css.searchBox}>
|
||||
<SearchInputWithSpinner
|
||||
placeholder={getString('codeSearch')}
|
||||
spinnerPosition="right"
|
||||
query={search}
|
||||
setQuery={setSearch}
|
||||
onSearch={() => {
|
||||
history.push({
|
||||
pathname: routes.toCODESearch({
|
||||
repoPath: repoMetadata.path as string
|
||||
}),
|
||||
search: `q=${search}`
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{!search && <img src={svg} width={95} height={22} />}
|
||||
</Container>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.1 KiB |
@ -248,9 +248,8 @@ export function FileContent({
|
||||
<Match expr={isViewable}>
|
||||
<Falsy>
|
||||
<Center>
|
||||
rawURL: {rawURL}
|
||||
<Link
|
||||
to={rawURL} // TODO: Link component generates wrong copy link
|
||||
to={rawURL}
|
||||
onClick={e => {
|
||||
Utils.stopEvent(e)
|
||||
downloadFile({ repoMetadata, resourcePath, gitRef, filename })
|
||||
|
229
web/src/pages/Search/Search.module.scss
Normal file
229
web/src/pages/Search/Search.module.scss
Normal file
@ -0,0 +1,229 @@
|
||||
@import 'src/utils/utils';
|
||||
|
||||
.main {
|
||||
--header-height: 128px;
|
||||
--border-color: var(--grey-100);
|
||||
|
||||
min-height: var(--page-height);
|
||||
background-color: var(--primary-bg) !important;
|
||||
|
||||
.pageHeader {
|
||||
flex-direction: column;
|
||||
height: var(--header-height);
|
||||
align-items: normal !important;
|
||||
justify-content: flex-start !important;
|
||||
padding-top: 0 !important;
|
||||
|
||||
[class*='breadcrumbs'] > [class*='module_header'] {
|
||||
padding-top: 13px !important;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
> div {
|
||||
flex-grow: 1;
|
||||
|
||||
div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: calc(100% - 8px) !important;
|
||||
border: 1px solid var(--ai-purple-600) !important;
|
||||
}
|
||||
|
||||
svg path {
|
||||
fill: var(--ai-purple-600) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& + div {
|
||||
--page-header-height: var(--header-height) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.split {
|
||||
> div:first-of-type {
|
||||
background-color: #fbfcfd;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
> div:last-of-type {
|
||||
background-color: var(--white);
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.searchResult {
|
||||
padding: var(--spacing-medium) var(--spacing-large) var(--spacing-large) var(--spacing-xlarge);
|
||||
|
||||
.resultTitle {
|
||||
text-transform: uppercase;
|
||||
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
.result {
|
||||
padding: var(--spacing-medium);
|
||||
border: 1px solid rgba(243, 243, 250, 1);
|
||||
border-radius: 5px;
|
||||
background-color: var(--white);
|
||||
|
||||
&.selected {
|
||||
border-color: rgba(217, 218, 229, 1);
|
||||
background-color: rgba(246, 241, 255, 1);
|
||||
box-shadow: 0px 0.5px 2px 0px rgba(96, 97, 112, 0.16), 0px 0px 1px 0px rgba(40, 41, 61, 0.08);
|
||||
}
|
||||
|
||||
&:hover:not(.selected) {
|
||||
border-color: rgba(217, 218, 229, 1);
|
||||
background-color: rgba(246, 241, 255, 0.5);
|
||||
}
|
||||
|
||||
.layout {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.texts {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.filename {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(79, 81, 98, 1);
|
||||
}
|
||||
|
||||
.path {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: rgba(146, 147, 171, 1);
|
||||
}
|
||||
|
||||
.aiLabel {
|
||||
background: var(--ai-purple-100);
|
||||
color: var(--ai-purple-600);
|
||||
text-transform: uppercase;
|
||||
font-size: 8px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
padding: 3px 6px;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.Resizer.vertical {
|
||||
width: 13px;
|
||||
background-color: var(--border-color);
|
||||
opacity: 1;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: var(--primary-6);
|
||||
border-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
&.noResult {
|
||||
> * {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.filePath {
|
||||
height: 45px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing-medium);
|
||||
|
||||
> div:first-of-type {
|
||||
flex-grow: 1;
|
||||
width: calc(100% - 150px);
|
||||
}
|
||||
|
||||
button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pathText {
|
||||
align-self: center;
|
||||
color: var(--grey-500);
|
||||
}
|
||||
|
||||
:global {
|
||||
.bp3-breadcrumb,
|
||||
.bp3-breadcrumb-current,
|
||||
.bp3-breadcrumbs-collapsed {
|
||||
white-space: nowrap !important;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--grey-500);
|
||||
}
|
||||
|
||||
.bp3-breadcrumbs > li::after {
|
||||
background: none;
|
||||
content: '/';
|
||||
color: var(--grey-500);
|
||||
background: none;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bp3-breadcrumbs-collapsed {
|
||||
background: var(--grey-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fileContent {
|
||||
flex-grow: 1;
|
||||
height: calc(100% - 45px);
|
||||
overflow: auto;
|
||||
|
||||
:global {
|
||||
.cm-editor {
|
||||
border: none;
|
||||
|
||||
.cm-scroller {
|
||||
padding: 0;
|
||||
|
||||
.cm-line {
|
||||
&,
|
||||
* {
|
||||
@include mono-font;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
border-right: none;
|
||||
|
||||
.cm-gutterElement {
|
||||
padding-left: 30px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.highlightLineNumber {
|
||||
background-color: var(--ai-purple-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
web/src/pages/Search/Search.module.scss.d.ts
vendored
Normal file
21
web/src/pages/Search/Search.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const aiLabel: string
|
||||
export declare const fileContent: string
|
||||
export declare const filename: string
|
||||
export declare const filePath: string
|
||||
export declare const highlightLineNumber: string
|
||||
export declare const layout: string
|
||||
export declare const main: string
|
||||
export declare const noResult: string
|
||||
export declare const pageHeader: string
|
||||
export declare const path: string
|
||||
export declare const pathText: string
|
||||
export declare const preview: string
|
||||
export declare const result: string
|
||||
export declare const resultTitle: string
|
||||
export declare const searchBox: string
|
||||
export declare const searchResult: string
|
||||
export declare const selected: string
|
||||
export declare const split: string
|
||||
export declare const texts: string
|
332
web/src/pages/Search/Search.tsx
Normal file
332
web/src/pages/Search/Search.tsx
Normal file
@ -0,0 +1,332 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
ButtonSize,
|
||||
ButtonVariation,
|
||||
Container,
|
||||
Layout,
|
||||
PageBody,
|
||||
stringSubstitute,
|
||||
Text,
|
||||
useToaster
|
||||
} from '@harnessio/uicore'
|
||||
import cx from 'classnames'
|
||||
import { lineNumbers, ViewUpdate } from '@codemirror/view'
|
||||
import { Breadcrumbs, IBreadcrumbProps } from '@blueprintjs/core'
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { Match, Truthy, Falsy } from 'react-jsx-match'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { useMutate } from 'restful-react'
|
||||
import { Editor } from 'components/Editor/Editor'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||
import { Split } from 'components/Split/Split'
|
||||
import { CodeIcon, decodeGitContent } from 'utils/GitUtils'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
import { useQueryParams } from 'hooks/useQueryParams'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
|
||||
import { voidFn, getErrorMessage, ButtonRoleProps } from 'utils/Utils'
|
||||
import type { RepoFileContent } from 'services/code'
|
||||
import { useShowRequestError } from 'hooks/useShowRequestError'
|
||||
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
|
||||
import { useGetResourceContent } from 'hooks/useGetResourceContent'
|
||||
import { addClassToLinesExtension } from 'utils/codemirror/addClassToLinesExtension'
|
||||
import css from './Search.module.scss'
|
||||
|
||||
export default function Search() {
|
||||
const { showError } = useToaster()
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
const highlightedLines = useRef<number[]>([])
|
||||
const [highlightlLineNumbersExtension, updateHighlightlLineNumbers] = useMemo(
|
||||
() => addClassToLinesExtension([], css.highlightLineNumber),
|
||||
[]
|
||||
)
|
||||
const extensions = useMemo(() => {
|
||||
return [
|
||||
lineNumbers({
|
||||
formatNumber: (lineNo: number) => lineNo.toString()
|
||||
}),
|
||||
highlightlLineNumbersExtension
|
||||
]
|
||||
}, [highlightlLineNumbersExtension])
|
||||
const viewRef = useRef<EditorView>()
|
||||
const { getString } = useStrings()
|
||||
const { routes } = useAppContext()
|
||||
const { q } = useQueryParams<{ q: string }>()
|
||||
const [searchTerm, setSearchTerm] = useState(q || '')
|
||||
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
|
||||
const [resourcePath, setResourcePath] = useState('')
|
||||
const [filename, setFileName] = useState('')
|
||||
const gitRef = useMemo(() => repoMetadata?.default_branch || '', [repoMetadata])
|
||||
const breadcrumbs = useMemo(() => {
|
||||
return repoMetadata?.path
|
||||
? resourcePath.split('/').map((_path, index, paths) => {
|
||||
const pathAtIndex = paths.slice(0, index + 1).join('/')
|
||||
const href = routes.toCODERepository({
|
||||
repoPath: repoMetadata.path as string,
|
||||
gitRef,
|
||||
resourcePath: pathAtIndex
|
||||
})
|
||||
|
||||
return { href, text: _path }
|
||||
})
|
||||
: []
|
||||
}, [resourcePath, repoMetadata, gitRef, routes])
|
||||
const onSelectResult = useCallback(
|
||||
(fileName: string, filePath: string, _content: string, _highlightedLines: number[]) => {
|
||||
updateHighlightlLineNumbers(_highlightedLines, viewRef.current)
|
||||
highlightedLines.current = _highlightedLines
|
||||
setFileName(fileName)
|
||||
setResourcePath(filePath)
|
||||
},
|
||||
[updateHighlightlLineNumbers]
|
||||
)
|
||||
const {
|
||||
data: resourceContent,
|
||||
error: resourceError = null,
|
||||
loading: resourceLoading
|
||||
} = useGetResourceContent({ repoMetadata, gitRef, resourcePath, includeCommit: false, lazy: !resourcePath })
|
||||
const fileContent = useMemo(
|
||||
() =>
|
||||
resourceContent?.path === resourcePath
|
||||
? decodeGitContent((resourceContent?.content as RepoFileContent)?.data)
|
||||
: '',
|
||||
[resourceContent?.content, resourceContent?.path, resourcePath]
|
||||
)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const onViewUpdate = useCallback(({ view, docChanged }: ViewUpdate) => {
|
||||
const firstLine = (highlightedLines.current || [])[0]
|
||||
|
||||
if (docChanged && firstLine > 0 && view.state.doc.lines >= firstLine) {
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(view.state.doc.line(firstLine).from, { y: 'start', yMargin: 18 * 2 })
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
const [loadingSearch, setLoadingSearch] = useState(false)
|
||||
const { mutate: sendSearch } = useMutate<SearchResultType[]>({
|
||||
verb: 'POST',
|
||||
path: `/api/v1/repos/${repoMetadata?.path}/+/semantic/search`
|
||||
})
|
||||
const [searchResult, setSearchResult] = useState<SearchResultType[]>([])
|
||||
const performSearch = useCallback(() => {
|
||||
setLoadingSearch(true)
|
||||
history.replace({ pathname: location.pathname, search: `q=${searchTerm}` })
|
||||
|
||||
sendSearch({ query: searchTerm })
|
||||
.then(response => {
|
||||
setSearchResult(response)
|
||||
})
|
||||
.catch(exception => {
|
||||
showError(getErrorMessage(exception), 0)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingSearch(false)
|
||||
})
|
||||
}, [searchTerm, history, location, sendSearch, showError])
|
||||
|
||||
useEffect(() => {
|
||||
if (q && repoMetadata?.path) {
|
||||
performSearch()
|
||||
}
|
||||
}, [repoMetadata?.path]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useShowRequestError(resourceError)
|
||||
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
<RepositoryPageHeader
|
||||
repoMetadata={repoMetadata}
|
||||
title={getString('search')}
|
||||
dataTooltipId="semanticSearch"
|
||||
content={
|
||||
<Container className={css.searchBox}>
|
||||
<SearchInputWithSpinner
|
||||
spinnerPosition="right"
|
||||
query={searchTerm}
|
||||
setQuery={setSearchTerm}
|
||||
onSearch={performSearch}
|
||||
/>
|
||||
<Button
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
text={getString('search')}
|
||||
disabled={!searchTerm.trim()}
|
||||
onClick={performSearch}
|
||||
loading={loadingSearch}
|
||||
/>
|
||||
</Container>
|
||||
}
|
||||
className={css.pageHeader}
|
||||
/>
|
||||
<PageBody error={getErrorMessage(error)} retryOnError={voidFn(refetch)}>
|
||||
<LoadingSpinner visible={loading || resourceLoading || loadingSearch} withBorder />
|
||||
|
||||
<Match expr={q}>
|
||||
<Truthy>
|
||||
<Split split="vertical" className={css.split} size={450} minSize={300} maxSize={700} primary="first">
|
||||
<SearchResults onSelect={onSelectResult} data={searchResult} />
|
||||
|
||||
<Layout.Vertical className={cx(css.preview, { [css.noResult]: !searchResult?.length })}>
|
||||
<Container className={css.filePath}>
|
||||
<Container>
|
||||
<Layout.Horizontal spacing="small">
|
||||
<Link
|
||||
className={css.pathText}
|
||||
to={routes.toCODERepository({
|
||||
repoPath: (repoMetadata?.path as string) || '',
|
||||
gitRef
|
||||
})}>
|
||||
<Icon name={CodeIcon.Folder} />
|
||||
</Link>
|
||||
<Text inline className={css.pathText}>
|
||||
/
|
||||
</Text>
|
||||
<Breadcrumbs
|
||||
items={breadcrumbs}
|
||||
breadcrumbRenderer={({ text, href }: IBreadcrumbProps) => {
|
||||
return (
|
||||
<Link to={href as string}>
|
||||
<Text inline className={css.pathText}>
|
||||
{text}
|
||||
</Text>
|
||||
</Link>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
<Button
|
||||
variation={ButtonVariation.SECONDARY}
|
||||
text={getString('viewFile')}
|
||||
size={ButtonSize.SMALL}
|
||||
rightIcon="chevron-right"
|
||||
onClick={() => {
|
||||
history.push(
|
||||
routes.toCODERepository({
|
||||
repoPath: (repoMetadata?.path as string) || '',
|
||||
gitRef,
|
||||
resourcePath
|
||||
})
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
<Container className={css.fileContent}>
|
||||
<Editor
|
||||
viewRef={viewRef}
|
||||
filename={filename}
|
||||
content={fileContent}
|
||||
readonly={true}
|
||||
onViewUpdate={onViewUpdate}
|
||||
extensions={extensions}
|
||||
maxHeight="auto"
|
||||
/>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
</Split>
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
<NoResultCard
|
||||
showWhen={() => true}
|
||||
forSearch={true}
|
||||
title={getString('startSearching')}
|
||||
emptySearchMessage={getString('poweredByAI')}
|
||||
/>
|
||||
</Falsy>
|
||||
</Match>
|
||||
</PageBody>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
interface SearchResultsProps {
|
||||
data: SearchResultType[]
|
||||
onSelect: (fileName: string, filePath: string, content: string, highlightedLines: number[]) => void
|
||||
}
|
||||
|
||||
const SearchResults: React.FC<SearchResultsProps> = ({ data, onSelect }) => {
|
||||
const { getString } = useStrings()
|
||||
const [selected, setSelected] = useState(data?.[0]?.file_path || '')
|
||||
const count = useMemo(() => data?.length || 0, [data])
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.length) {
|
||||
const item = data[0]
|
||||
onSelect(
|
||||
item.file_name,
|
||||
item.file_path,
|
||||
(item.content || []).join('\n').replace(/^\n/g, '').trim(),
|
||||
range(item.start_line, item.end_line)
|
||||
)
|
||||
}
|
||||
}, [data, onSelect])
|
||||
|
||||
return (
|
||||
<Container className={css.searchResult}>
|
||||
<Layout.Vertical spacing="medium">
|
||||
<Text className={css.resultTitle}>
|
||||
{stringSubstitute(getString('searchResult'), { count: count - 4 ? `(${count})` : ' ' })}
|
||||
</Text>
|
||||
{data.map(item => (
|
||||
<Container
|
||||
key={item.file_path}
|
||||
className={cx(css.result, { [css.selected]: item.file_path === selected })}
|
||||
{...ButtonRoleProps}
|
||||
onClick={() => {
|
||||
setSelected(item.file_path)
|
||||
onSelect(
|
||||
item.file_name,
|
||||
item.file_path,
|
||||
(item.content || []).join('\n').replace(/^\n/g, '').trim(),
|
||||
range(item.start_line, item.end_line)
|
||||
)
|
||||
}}>
|
||||
<Layout.Vertical spacing="small">
|
||||
<Container>
|
||||
<Layout.Horizontal className={css.layout}>
|
||||
<Container className={css.texts}>
|
||||
<Layout.Vertical spacing="xsmall">
|
||||
<Text className={css.filename} lineClamp={1}>
|
||||
{item.file_name}
|
||||
</Text>
|
||||
<Text className={css.path} lineClamp={1}>
|
||||
{item.file_path}
|
||||
</Text>
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
<Text inline className={css.aiLabel}>
|
||||
{getString('aiSearch')}
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
<Editor
|
||||
filename={item.file_name}
|
||||
content={(item.content || []).join('\n').replace(/^\n/g, '').trim()}
|
||||
readonly={true}
|
||||
maxHeight="200px"
|
||||
darkTheme
|
||||
/>
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
))}
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
type SearchResultType = {
|
||||
commit: string
|
||||
file_path: string
|
||||
start_line: number
|
||||
end_line: number
|
||||
file_name: string
|
||||
content: string[]
|
||||
}
|
||||
|
||||
const range = (start: number, stop: number, step = 1) =>
|
||||
Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step)
|
66
web/src/utils/codemirror/addClassToLinesExtension.tsx
Normal file
66
web/src/utils/codemirror/addClassToLinesExtension.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { ViewPlugin, ViewUpdate, type EditorView, Decoration } from '@codemirror/view'
|
||||
import { RangeSetBuilder, Compartment, Extension } from '@codemirror/state'
|
||||
|
||||
export const addClassToLinesExtension: AddClassToLinesExtension = (lines = [], className) => {
|
||||
const highlightLineDecoration = Decoration.line({
|
||||
attributes: { class: className }
|
||||
})
|
||||
|
||||
function updateDecorations(view: EditorView) {
|
||||
const builder = new RangeSetBuilder<Decoration>()
|
||||
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
for (let pos = from; pos <= to; ) {
|
||||
const line = view.state.doc.lineAt(pos)
|
||||
|
||||
if (lines.includes(line.number)) {
|
||||
builder.add(line.from, line.from, highlightLineDecoration)
|
||||
}
|
||||
pos = line.to + 1
|
||||
}
|
||||
}
|
||||
|
||||
return builder.finish()
|
||||
}
|
||||
|
||||
class Plugin {
|
||||
decorations = Decoration.none
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = updateDecorations(view)
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = updateDecorations(update.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = new Compartment()
|
||||
|
||||
const update: AddClassToLinesReturnType[1] = (_lines, view) => {
|
||||
lines = _lines
|
||||
|
||||
view?.dispatch({
|
||||
effects: config.reconfigure(
|
||||
ViewPlugin.fromClass(Plugin, {
|
||||
decorations: v => v.decorations
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return [
|
||||
config.of(
|
||||
ViewPlugin.fromClass(Plugin, {
|
||||
decorations: v => v.decorations
|
||||
})
|
||||
),
|
||||
update
|
||||
]
|
||||
}
|
||||
|
||||
type AddClassToLinesReturnType = [Extension, (lines: number[], view?: EditorView) => void]
|
||||
|
||||
type AddClassToLinesExtension = (lines: number[], className: string) => AddClassToLinesReturnType
|
Loading…
Reference in New Issue
Block a user