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',
|
'./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',
|
||||||
'./WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
|
'./WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
|
||||||
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx'
|
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx'
|
||||||
},
|
},
|
||||||
|
@ -73,7 +73,7 @@ export interface CODERoutes {
|
|||||||
toCODEWebhookNew: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
toCODEWebhookNew: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
toCODEWebhookDetails: (args: Required<Pick<CODEProps, 'repoPath' | 'webhookId'>>) => string
|
toCODEWebhookDetails: (args: Required<Pick<CODEProps, 'repoPath' | 'webhookId'>>) => string
|
||||||
toCODESettings: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
toCODESettings: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||||
|
toCODESearch: (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
|
||||||
@ -126,6 +126,7 @@ export const routes: CODERoutes = {
|
|||||||
toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`,
|
toCODEBranches: ({ repoPath }) => `/${repoPath}/branches`,
|
||||||
toCODETags: ({ repoPath }) => `/${repoPath}/tags`,
|
toCODETags: ({ repoPath }) => `/${repoPath}/tags`,
|
||||||
toCODESettings: ({ repoPath }) => `/${repoPath}/settings`,
|
toCODESettings: ({ repoPath }) => `/${repoPath}/settings`,
|
||||||
|
toCODESearch: ({ repoPath }) => `/${repoPath}/search`,
|
||||||
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}`,
|
||||||
|
@ -30,6 +30,7 @@ import { useStrings } from 'framework/strings'
|
|||||||
import ExecutionList from 'pages/ExecutionList/ExecutionList'
|
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 AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline'
|
import AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
|
|
||||||
@ -249,6 +250,12 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
|||||||
</LayoutWithSideNav>
|
</LayoutWithSideNav>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path={routes.toCODESearch({ repoPath })} exact>
|
||||||
|
<LayoutWithSideNav title={getString('pageTitle.search')}>
|
||||||
|
<Search />
|
||||||
|
</LayoutWithSideNav>
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={routes.toCODEFileEdit({
|
path={routes.toCODEFileEdit({
|
||||||
repoPath,
|
repoPath,
|
||||||
|
@ -11,7 +11,7 @@ import { EditorView, keymap, placeholder as placeholderExtension } from '@codemi
|
|||||||
import { Compartment, EditorState, Extension } from '@codemirror/state'
|
import { Compartment, EditorState, Extension } from '@codemirror/state'
|
||||||
import { color } from '@uiw/codemirror-extensions-color'
|
import { color } from '@uiw/codemirror-extensions-color'
|
||||||
import { hyperLink } from '@uiw/codemirror-extensions-hyper-link'
|
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'
|
import css from './Editor.module.scss'
|
||||||
|
|
||||||
export interface EditorProps {
|
export interface EditorProps {
|
||||||
@ -28,6 +28,7 @@ export interface EditorProps {
|
|||||||
setDirty?: React.Dispatch<React.SetStateAction<boolean>>
|
setDirty?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
onChange?: (doc: Text, viewUpdate: ViewUpdate, isDirty: boolean) => void
|
onChange?: (doc: Text, viewUpdate: ViewUpdate, isDirty: boolean) => void
|
||||||
onViewUpdate?: (viewUpdate: ViewUpdate) => void
|
onViewUpdate?: (viewUpdate: ViewUpdate) => void
|
||||||
|
darkTheme?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = React.memo(function CodeMirrorReactEditor({
|
export const Editor = React.memo(function CodeMirrorReactEditor({
|
||||||
@ -43,8 +44,10 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
|
|||||||
viewRef,
|
viewRef,
|
||||||
setDirty,
|
setDirty,
|
||||||
onChange,
|
onChange,
|
||||||
onViewUpdate
|
onViewUpdate,
|
||||||
|
darkTheme
|
||||||
}: EditorProps) {
|
}: EditorProps) {
|
||||||
|
const contentRef = useRef(content)
|
||||||
const view = useRef<EditorView>()
|
const view = useRef<EditorView>()
|
||||||
const ref = useRef<HTMLDivElement>()
|
const ref = useRef<HTMLDivElement>()
|
||||||
const languageConfig = useMemo(() => new Compartment(), [])
|
const languageConfig = useMemo(() => new Compartment(), [])
|
||||||
@ -70,7 +73,7 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
|
|||||||
|
|
||||||
color,
|
color,
|
||||||
hyperLink,
|
hyperLink,
|
||||||
theme,
|
darkTheme ? githubDark : githubLight,
|
||||||
|
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
|
|
||||||
@ -136,5 +139,14 @@ export const Editor = React.memo(function CodeMirrorReactEditor({
|
|||||||
}
|
}
|
||||||
}, [filename, forMarkdown, view, languageConfig, markdownLanguageSupport])
|
}, [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} />
|
return <Container ref={ref} className={cx(css.editor, className)} style={style} />
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Fragment } from 'react'
|
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 { Icon } from '@harnessio/icons'
|
||||||
import { Color, FontVariation } from '@harnessio/design-system'
|
import { Color, FontVariation } from '@harnessio/design-system'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
@ -19,33 +19,35 @@ interface RepositoryPageHeaderProps extends Optional<Pick<GitInfoProps, 'repoMet
|
|||||||
title: string | JSX.Element
|
title: string | JSX.Element
|
||||||
dataTooltipId: string
|
dataTooltipId: string
|
||||||
extraBreadcrumbLinks?: BreadcrumbLink[]
|
extraBreadcrumbLinks?: BreadcrumbLink[]
|
||||||
|
className?: string
|
||||||
|
content?: PageHeaderProps['content']
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepositoryPageHeader({
|
export function RepositoryPageHeader({
|
||||||
repoMetadata,
|
repoMetadata,
|
||||||
title,
|
title,
|
||||||
dataTooltipId,
|
dataTooltipId,
|
||||||
extraBreadcrumbLinks = []
|
extraBreadcrumbLinks = [],
|
||||||
|
className,
|
||||||
|
content
|
||||||
}: RepositoryPageHeaderProps) {
|
}: RepositoryPageHeaderProps) {
|
||||||
const { gitRef } = useParams<CODEProps>()
|
const { gitRef } = useParams<CODEProps>()
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const space = useGetSpaceParam()
|
const space = useGetSpaceParam()
|
||||||
const { routes } = useAppContext()
|
const { routes } = useAppContext()
|
||||||
|
|
||||||
if (!repoMetadata) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader
|
<PageHeader
|
||||||
|
className={className}
|
||||||
|
content={content}
|
||||||
title=""
|
title=""
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Container className={css.header}>
|
<Container className={css.header}>
|
||||||
<Layout.Horizontal spacing="small" className={css.breadcrumb}>
|
<Layout.Horizontal spacing="small" className={css.breadcrumb}>
|
||||||
<Link to={routes.toCODERepositories({ space })}>{getString('repositories')}</Link>
|
<Link to={routes.toCODERepositories({ space })}>{getString('repositories')}</Link>
|
||||||
<Icon name="main-chevron-right" size={8} color={Color.GREY_500} />
|
<Icon name="main-chevron-right" size={8} color={Color.GREY_500} />
|
||||||
<Link to={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef })}>
|
<Link to={routes.toCODERepository({ repoPath: (repoMetadata?.path as string) || '', gitRef })}>
|
||||||
{repoMetadata.uid}
|
{repoMetadata?.uid || ''}
|
||||||
</Link>
|
</Link>
|
||||||
{extraBreadcrumbLinks.map(link => (
|
{extraBreadcrumbLinks.map(link => (
|
||||||
<Fragment key={link.url}>
|
<Fragment key={link.url}>
|
||||||
|
@ -16,6 +16,7 @@ interface SearchInputWithSpinnerProps {
|
|||||||
icon?: IconName
|
icon?: IconName
|
||||||
spinnerIcon?: IconName
|
spinnerIcon?: IconName
|
||||||
spinnerPosition?: 'left' | 'right'
|
spinnerPosition?: 'left' | 'right'
|
||||||
|
onSearch?: (searchTerm: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
||||||
@ -26,7 +27,8 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
|||||||
placeholder,
|
placeholder,
|
||||||
icon = 'search',
|
icon = 'search',
|
||||||
spinnerIcon = 'steps-spinner',
|
spinnerIcon = 'steps-spinner',
|
||||||
spinnerPosition = 'left'
|
spinnerPosition = 'left',
|
||||||
|
onSearch
|
||||||
}) => {
|
}) => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const spinner = <Icon name={spinnerIcon as IconName} color={Color.PRIMARY_7} />
|
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}>
|
<Layout.Horizontal className={css.layout}>
|
||||||
<Render when={loading && !spinnerOnRight}>{spinner}</Render>
|
<Render when={loading && !spinnerOnRight}>{spinner}</Render>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
type="search"
|
||||||
value={query}
|
value={query}
|
||||||
wrapperClassName={cx(css.wrapper, { [css.spinnerOnRight]: spinnerOnRight })}
|
wrapperClassName={cx(css.wrapper, { [css.spinnerOnRight]: spinnerOnRight })}
|
||||||
className={css.input}
|
className={css.input}
|
||||||
@ -45,7 +48,14 @@ export const SearchInputWithSpinner: React.FC<SearchInputWithSpinnerProps> = ({
|
|||||||
style={{ width }}
|
style={{ width }}
|
||||||
autoFocus
|
autoFocus
|
||||||
onFocus={event => event.target.select()}
|
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>
|
<Render when={loading && spinnerOnRight}>{spinner}</Render>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
width: min(calc(100vw - var(--nav-menu-width)), 840px) !important;
|
width: min(calc(100vw - var(--nav-menu-width)), 840px) !important;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 5px;
|
left: 241px;
|
||||||
top: -5px;
|
top: -5px;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
@ -79,6 +79,13 @@
|
|||||||
background-color: rgba(26, 26, 26, 0.4);
|
background-color: rgba(26, 26, 26, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.bp3-popover-arrow {
|
||||||
|
left: -11px !important;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 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
|
addNewFile: string
|
||||||
addReadMe: string
|
addReadMe: string
|
||||||
admin: string
|
admin: string
|
||||||
|
aiSearch: string
|
||||||
all: string
|
all: string
|
||||||
allBranches: string
|
allBranches: string
|
||||||
allComments: string
|
allComments: string
|
||||||
@ -65,6 +66,7 @@ export interface StringsMap {
|
|||||||
cloneText: string
|
cloneText: string
|
||||||
close: string
|
close: string
|
||||||
closed: string
|
closed: string
|
||||||
|
codeSearch: string
|
||||||
comment: string
|
comment: string
|
||||||
commentDeleted: string
|
commentDeleted: string
|
||||||
commit: string
|
commit: string
|
||||||
@ -307,6 +309,7 @@ export interface StringsMap {
|
|||||||
'pageTitle.repositories': string
|
'pageTitle.repositories': string
|
||||||
'pageTitle.repository': string
|
'pageTitle.repository': string
|
||||||
'pageTitle.repositorySettings': string
|
'pageTitle.repositorySettings': string
|
||||||
|
'pageTitle.search': string
|
||||||
'pageTitle.secrets': string
|
'pageTitle.secrets': string
|
||||||
'pageTitle.signin': string
|
'pageTitle.signin': string
|
||||||
'pageTitle.spaceSettings': string
|
'pageTitle.spaceSettings': string
|
||||||
@ -336,6 +339,7 @@ export interface StringsMap {
|
|||||||
'pipelines.run': string
|
'pipelines.run': string
|
||||||
'pipelines.saveAndRun': string
|
'pipelines.saveAndRun': string
|
||||||
'pipelines.time': string
|
'pipelines.time': string
|
||||||
|
poweredByAI: string
|
||||||
'pipelines.updated': string
|
'pipelines.updated': string
|
||||||
'pipelines.yamlPath': string
|
'pipelines.yamlPath': string
|
||||||
'plugins.addAPlugin': string
|
'plugins.addAPlugin': string
|
||||||
@ -482,6 +486,7 @@ export interface StringsMap {
|
|||||||
scrollToTop: string
|
scrollToTop: string
|
||||||
search: string
|
search: string
|
||||||
searchBranches: string
|
searchBranches: string
|
||||||
|
searchResult: string
|
||||||
secret: string
|
secret: string
|
||||||
'secrets.createSecret': string
|
'secrets.createSecret': string
|
||||||
'secrets.createSuccess': string
|
'secrets.createSuccess': string
|
||||||
@ -527,6 +532,7 @@ export interface StringsMap {
|
|||||||
'spaceSetting.setting': string
|
'spaceSetting.setting': string
|
||||||
spaces: string
|
spaces: string
|
||||||
sslVerificationLabel: string
|
sslVerificationLabel: string
|
||||||
|
startSearching: string
|
||||||
status: string
|
status: string
|
||||||
submitReview: string
|
submitReview: string
|
||||||
success: string
|
success: string
|
||||||
@ -586,6 +592,7 @@ export interface StringsMap {
|
|||||||
viewAllTags: string
|
viewAllTags: string
|
||||||
viewCommitDetails: string
|
viewCommitDetails: string
|
||||||
viewFile: string
|
viewFile: string
|
||||||
|
viewFileHistory: string
|
||||||
viewFiles: string
|
viewFiles: string
|
||||||
viewRaw: string
|
viewRaw: string
|
||||||
viewRepo: string
|
viewRepo: string
|
||||||
|
@ -96,6 +96,7 @@ pageTitle:
|
|||||||
pipelines: Pipelines
|
pipelines: Pipelines
|
||||||
secrets: Secrets
|
secrets: Secrets
|
||||||
executions: Executions
|
executions: Executions
|
||||||
|
search: Search powered by AI
|
||||||
repos:
|
repos:
|
||||||
name: Repo Name
|
name: Repo Name
|
||||||
data: Repo Data
|
data: Repo Data
|
||||||
@ -482,7 +483,7 @@ newTag: New Tag
|
|||||||
overview: Overview
|
overview: Overview
|
||||||
fileTooLarge: File is too large to open. {download}
|
fileTooLarge: File is too large to open. {download}
|
||||||
clickHereToDownload: Click here to 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
|
viewRepo: View the repository at this point in the history
|
||||||
hideCommitHistory: Renamed from {file} - Hide History
|
hideCommitHistory: Renamed from {file} - Hide History
|
||||||
showCommitHistory: Renamed from {file} - Show History
|
showCommitHistory: Renamed from {file} - Show History
|
||||||
@ -669,6 +670,12 @@ secrets:
|
|||||||
failedToDeleteSecret: Failed to delete Secret. Please try again.
|
failedToDeleteSecret: Failed to delete Secret. Please try again.
|
||||||
deleteSecret: Delete Secrets
|
deleteSecret: Delete Secrets
|
||||||
userUpdateSuccess: 'User updated successfully'
|
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
|
run: Run
|
||||||
plugins:
|
plugins:
|
||||||
title: Plugins
|
title: Plugins
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
.settings {
|
.settings {
|
||||||
margin: 0 var(--spacing-medium);
|
margin: 0 var(--spacing-medium);
|
||||||
|
@ -126,15 +126,25 @@ export const DefaultMenu: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<NavMenuItem
|
<NavMenuItem
|
||||||
data-code-repo-section="settings"
|
data-code-repo-section="pipelines"
|
||||||
isSubLink
|
isSubLink
|
||||||
label={getString('settings')}
|
label={getString('pageTitle.pipelines')}
|
||||||
to={routes.toCODESettings({
|
to={routes.toCODEPipelines({
|
||||||
repoPath
|
repoPath
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!standalone && (
|
||||||
|
<NavMenuItem
|
||||||
|
data-code-repo-section="search"
|
||||||
|
isSubLink
|
||||||
|
label={getString('search')}
|
||||||
|
to={routes.toCODESearch({
|
||||||
|
repoPath
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
</Container>
|
</Container>
|
||||||
</Render>
|
</Render>
|
||||||
|
@ -1,53 +1,6 @@
|
|||||||
.main {
|
.main {
|
||||||
min-height: var(--page-height);
|
min-height: var(--page-height);
|
||||||
background-color: var(--primary-bg) !important;
|
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 {
|
.container {
|
||||||
|
@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { useGet } from 'restful-react'
|
import { useGet } from 'restful-react'
|
||||||
import SplitPane from 'react-split-pane'
|
|
||||||
import { routes, type CODEProps } from 'RouteDefinitions'
|
import { routes, type CODEProps } from 'RouteDefinitions'
|
||||||
import type { TypesExecution } from 'services/code'
|
import type { TypesExecution } from 'services/code'
|
||||||
import ExecutionStageList from 'components/ExecutionStageList/ExecutionStageList'
|
import ExecutionStageList from 'components/ExecutionStageList/ExecutionStageList'
|
||||||
@ -12,6 +11,7 @@ import { getErrorMessage, voidFn } from 'utils/Utils'
|
|||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||||
|
import { Split } from 'components/Split/Split'
|
||||||
import { ExecutionPageHeader } from 'components/ExecutionPageHeader/ExecutionPageHeader'
|
import { ExecutionPageHeader } from 'components/ExecutionPageHeader/ExecutionPageHeader'
|
||||||
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
||||||
import { ExecutionState } from 'components/ExecutionStatus/ExecutionStatus'
|
import { ExecutionState } from 'components/ExecutionStatus/ExecutionStatus'
|
||||||
@ -102,7 +102,7 @@ const Execution = () => {
|
|||||||
}}>
|
}}>
|
||||||
<LoadingSpinner visible={loading || isInitialLoad} withBorder={!!execution && isInitialLoad} />
|
<LoadingSpinner visible={loading || isInitialLoad} withBorder={!!execution && isInitialLoad} />
|
||||||
{execution && (
|
{execution && (
|
||||||
<SplitPane split="vertical" size={300} minSize={200} maxSize={400}>
|
<Split split="vertical" size={300} minSize={200} maxSize={400}>
|
||||||
<ExecutionStageList
|
<ExecutionStageList
|
||||||
stages={execution?.stages || []}
|
stages={execution?.stages || []}
|
||||||
setSelectedStage={setSelectedStage}
|
setSelectedStage={setSelectedStage}
|
||||||
@ -111,7 +111,7 @@ const Execution = () => {
|
|||||||
{selectedStage && (
|
{selectedStage && (
|
||||||
<Console stage={execution?.stages?.[selectedStage - 1]} repoPath={repoMetadata?.path as string} />
|
<Console stage={execution?.stages?.[selectedStage - 1]} repoPath={repoMetadata?.path as string} />
|
||||||
)}
|
)}
|
||||||
</SplitPane>
|
</Split>
|
||||||
)}
|
)}
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</Container>
|
</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 {
|
.status {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Falsy, Match, Render, Truthy } from 'react-jsx-match'
|
import { Falsy, Match, Render, Truthy } from 'react-jsx-match'
|
||||||
import { /*CheckCircle,*/ NavArrowRight } from 'iconoir-react'
|
import { /*CheckCircle,*/ NavArrowRight } from 'iconoir-react'
|
||||||
import SplitPane from 'react-split-pane'
|
|
||||||
import { get } from 'lodash-es'
|
import { get } from 'lodash-es'
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
@ -24,6 +23,7 @@ import type { GitInfoProps } from 'utils/GitUtils'
|
|||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import { useQueryParams } from 'hooks/useQueryParams'
|
import { useQueryParams } from 'hooks/useQueryParams'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
|
import { Split } from 'components/Split/Split'
|
||||||
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
import { MarkdownViewer } from 'components/MarkdownViewer/MarkdownViewer'
|
||||||
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
|
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
|
||||||
import type { TypesCheck } from 'services/code'
|
import type { TypesCheck } from 'services/code'
|
||||||
@ -56,7 +56,7 @@ export const Checks: React.FC<ChecksProps> = props => {
|
|||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
<Match expr={props.prChecksDecisionResult?.overallStatus}>
|
<Match expr={props.prChecksDecisionResult?.overallStatus}>
|
||||||
<Truthy>
|
<Truthy>
|
||||||
<SplitPane
|
<Split
|
||||||
split="vertical"
|
split="vertical"
|
||||||
size="calc(100% - 400px)"
|
size="calc(100% - 400px)"
|
||||||
minSize={800}
|
minSize={800}
|
||||||
@ -118,7 +118,7 @@ export const Checks: React.FC<ChecksProps> = props => {
|
|||||||
</Falsy>
|
</Falsy>
|
||||||
</Match>
|
</Match>
|
||||||
</Container>
|
</Container>
|
||||||
</SplitPane>
|
</Split>
|
||||||
</Truthy>
|
</Truthy>
|
||||||
<Falsy>
|
<Falsy>
|
||||||
<Container flex={{ align: 'center-center' }} height="90%">
|
<Container flex={{ align: 'center-center' }} height="90%">
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
.main {
|
.main {
|
||||||
padding: var(--spacing-large) var(--spacing-xlarge) 0 var(--spacing-xlarge) !important;
|
padding: var(--spacing-large) var(--spacing-xlarge) 0 var(--spacing-xlarge) !important;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
div[class*='TextInput'] {
|
div[class*='TextInput'] {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
@ -54,7 +55,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbItem {
|
.searchBox {
|
||||||
white-space: nowrap !important;
|
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 */
|
/* eslint-disable */
|
||||||
// This is an auto-generated file
|
// This is an auto-generated file
|
||||||
export declare const breadcrumbItem: string
|
|
||||||
export declare const btnColorFix: string
|
export declare const btnColorFix: string
|
||||||
export declare const main: string
|
export declare const main: string
|
||||||
export declare const refRoot: string
|
export declare const refRoot: string
|
||||||
export declare const rootSlash: 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 { Container, Layout, Button, FlexExpander, ButtonVariation, Text } from '@harnessio/uicore'
|
||||||
import { Icon } from '@harnessio/icons'
|
import { Icon } from '@harnessio/icons'
|
||||||
import { Color } from '@harnessio/design-system'
|
import { Color } from '@harnessio/design-system'
|
||||||
@ -12,6 +12,8 @@ import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
|||||||
import { useCreateBranchModal } from 'components/CreateBranchModal/CreateBranchModal'
|
import { useCreateBranchModal } from 'components/CreateBranchModal/CreateBranchModal'
|
||||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||||
import { 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({
|
||||||
@ -21,11 +23,9 @@ export function ContentHeader({
|
|||||||
resourceContent
|
resourceContent
|
||||||
}: Pick<GitInfoProps, 'repoMetadata' | 'gitRef' | 'resourcePath' | 'resourceContent'>) {
|
}: Pick<GitInfoProps, 'repoMetadata' | 'gitRef' | 'resourcePath' | 'resourceContent'>) {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { routes } = useAppContext()
|
const { routes, standalone, hooks } = useAppContext()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const _isDir = isDir(resourceContent)
|
const _isDir = isDir(resourceContent)
|
||||||
const { standalone } = useAppContext()
|
|
||||||
const { hooks } = useAppContext()
|
|
||||||
const space = useGetSpaceParam()
|
const space = useGetSpaceParam()
|
||||||
|
|
||||||
const permPushResult = hooks?.usePermissionTranslate?.(
|
const permPushResult = hooks?.usePermissionTranslate?.(
|
||||||
@ -62,6 +62,7 @@ export function ContentHeader({
|
|||||||
return { href, text: _path }
|
return { href, text: _path }
|
||||||
})
|
})
|
||||||
}, [resourcePath, gitRef, repoMetadata.path, routes])
|
}, [resourcePath, gitRef, repoMetadata.path, routes])
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={css.main}>
|
<Container className={css.main}>
|
||||||
@ -139,6 +140,25 @@ export function ContentHeader({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Layout.Horizontal>
|
</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>
|
</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}>
|
<Match expr={isViewable}>
|
||||||
<Falsy>
|
<Falsy>
|
||||||
<Center>
|
<Center>
|
||||||
rawURL: {rawURL}
|
|
||||||
<Link
|
<Link
|
||||||
to={rawURL} // TODO: Link component generates wrong copy link
|
to={rawURL}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
Utils.stopEvent(e)
|
Utils.stopEvent(e)
|
||||||
downloadFile({ repoMetadata, resourcePath, gitRef, filename })
|
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