diff --git a/web/config/moduleFederation.config.js b/web/config/moduleFederation.config.js index 5148930c1..919873364 100644 --- a/web/config/moduleFederation.config.js +++ b/web/config/moduleFederation.config.js @@ -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' }, diff --git a/web/src/RouteDefinitions.ts b/web/src/RouteDefinitions.ts index 1d40d7384..1a7000138 100644 --- a/web/src/RouteDefinitions.ts +++ b/web/src/RouteDefinitions.ts @@ -73,7 +73,7 @@ export interface CODERoutes { toCODEWebhookNew: (args: Required>) => string toCODEWebhookDetails: (args: Required>) => string toCODESettings: (args: Required>) => string - + toCODESearch: (args: Required>) => string toCODEExecutions: (args: Required>) => string toCODEExecution: (args: Required>) => string toCODESecret: (args: Required>) => 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}`, diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx index 3425889aa..60ec8dd21 100644 --- a/web/src/RouteDestinations.tsx +++ b/web/src/RouteDestinations.tsx @@ -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 + + + + + + > 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() const ref = useRef() 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 }) diff --git a/web/src/components/RepositoryPageHeader/RepositoryPageHeader.tsx b/web/src/components/RepositoryPageHeader/RepositoryPageHeader.tsx index 3455bc491..044d0561b 100644 --- a/web/src/components/RepositoryPageHeader/RepositoryPageHeader.tsx +++ b/web/src/components/RepositoryPageHeader/RepositoryPageHeader.tsx @@ -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() const { getString } = useStrings() const space = useGetSpaceParam() const { routes } = useAppContext() - if (!repoMetadata) { - return null - } - return ( {getString('repositories')} - - {repoMetadata.uid} + + {repoMetadata?.uid || ''} {extraBreadcrumbLinks.map(link => ( diff --git a/web/src/components/SearchInputWithSpinner/SearchInputWithSpinner.tsx b/web/src/components/SearchInputWithSpinner/SearchInputWithSpinner.tsx index 8c7864557..fe38d3d30 100644 --- a/web/src/components/SearchInputWithSpinner/SearchInputWithSpinner.tsx +++ b/web/src/components/SearchInputWithSpinner/SearchInputWithSpinner.tsx @@ -16,6 +16,7 @@ interface SearchInputWithSpinnerProps { icon?: IconName spinnerIcon?: IconName spinnerPosition?: 'left' | 'right' + onSearch?: (searchTerm: string) => void } export const SearchInputWithSpinner: React.FC = ({ @@ -26,7 +27,8 @@ export const SearchInputWithSpinner: React.FC = ({ placeholder, icon = 'search', spinnerIcon = 'steps-spinner', - spinnerPosition = 'left' + spinnerPosition = 'left', + onSearch }) => { const { getString } = useStrings() const spinner = @@ -37,6 +39,7 @@ export const SearchInputWithSpinner: React.FC = ({ {spinner} = ({ style={{ width }} autoFocus onFocus={event => event.target.select()} - onInput={event => setQuery(event.currentTarget.value || '')} + onInput={event => { + setQuery(event.currentTarget.value || '') + }} + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onSearch?.((e as unknown as React.FormEvent).currentTarget.value || '') + } + }} /> {spinner} diff --git a/web/src/components/SpaceSelector/SpaceSelector.module.scss b/web/src/components/SpaceSelector/SpaceSelector.module.scss index f2c4d8bee..e12cf551b 100644 --- a/web/src/components/SpaceSelector/SpaceSelector.module.scss +++ b/web/src/components/SpaceSelector/SpaceSelector.module.scss @@ -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%; diff --git a/web/src/components/Split/Split.module.scss b/web/src/components/Split/Split.module.scss new file mode 100644 index 000000000..d8caa6cc6 --- /dev/null +++ b/web/src/components/Split/Split.module.scss @@ -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; + } + } +} diff --git a/web/src/components/Split/Split.module.scss.d.ts b/web/src/components/Split/Split.module.scss.d.ts new file mode 100644 index 000000000..b8f356255 --- /dev/null +++ b/web/src/components/Split/Split.module.scss.d.ts @@ -0,0 +1,3 @@ +/* eslint-disable */ +// This is an auto-generated file +export declare const main: string diff --git a/web/src/components/Split/Split.tsx b/web/src/components/Split/Split.tsx new file mode 100644 index 000000000..9cda425f1 --- /dev/null +++ b/web/src/components/Split/Split.tsx @@ -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 = ({ className, ...props }) => ( + +) diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index 529fa1704..e465d5136 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -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 diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index eeb4fa3e7..28d463d49 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -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 diff --git a/web/src/layouts/layout.module.scss b/web/src/layouts/layout.module.scss index 697222d4b..ebef47d25 100644 --- a/web/src/layouts/layout.module.scss +++ b/web/src/layouts/layout.module.scss @@ -11,6 +11,7 @@ height: 100%; display: flex; flex-direction: column; + overflow: auto; .settings { margin: 0 var(--spacing-medium); diff --git a/web/src/layouts/menu/DefaultMenu.tsx b/web/src/layouts/menu/DefaultMenu.tsx index 92ad95177..4ecca3f79 100644 --- a/web/src/layouts/menu/DefaultMenu.tsx +++ b/web/src/layouts/menu/DefaultMenu.tsx @@ -126,15 +126,25 @@ export const DefaultMenu: React.FC = () => { })} /> )} - + + {!standalone && ( + + )} diff --git a/web/src/pages/Execution/Execution.module.scss b/web/src/pages/Execution/Execution.module.scss index 524ad90cf..3929081dc 100644 --- a/web/src/pages/Execution/Execution.module.scss +++ b/web/src/pages/Execution/Execution.module.scss @@ -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 { diff --git a/web/src/pages/Execution/Execution.tsx b/web/src/pages/Execution/Execution.tsx index 8b98ee10d..68efba320 100644 --- a/web/src/pages/Execution/Execution.tsx +++ b/web/src/pages/Execution/Execution.tsx @@ -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 = () => { }}> {execution && ( - + { {selectedStage && ( )} - + )} diff --git a/web/src/pages/PullRequest/Checks/Checks.module.scss b/web/src/pages/PullRequest/Checks/Checks.module.scss index fc977ee48..c61618802 100644 --- a/web/src/pages/PullRequest/Checks/Checks.module.scss +++ b/web/src/pages/PullRequest/Checks/Checks.module.scss @@ -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 { diff --git a/web/src/pages/PullRequest/Checks/Checks.tsx b/web/src/pages/PullRequest/Checks/Checks.tsx index ae74a03a5..be58524af 100644 --- a/web/src/pages/PullRequest/Checks/Checks.tsx +++ b/web/src/pages/PullRequest/Checks/Checks.tsx @@ -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 = props => { - = props => { - + diff --git a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss index 9ebf384cb..5ec8b15de 100644 --- a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss +++ b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss @@ -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; + } } } diff --git a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss.d.ts b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss.d.ts index 3de56dd26..c6a69acea 100644 --- a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss.d.ts +++ b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.module.scss.d.ts @@ -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 diff --git a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx index 4c530ef07..cf97aa0cd 100644 --- a/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx +++ b/web/src/pages/Repository/RepositoryContent/ContentHeader/ContentHeader.tsx @@ -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) { 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 ( @@ -139,6 +140,25 @@ export function ContentHeader({ )} + {!standalone && ( + + { + history.push({ + pathname: routes.toCODESearch({ + repoPath: repoMetadata.path as string + }), + search: `q=${search}` + }) + }} + /> + {!search && } + + )} ) } diff --git a/web/src/pages/Repository/RepositoryContent/ContentHeader/search-background.svg b/web/src/pages/Repository/RepositoryContent/ContentHeader/search-background.svg new file mode 100644 index 000000000..ae7d01305 --- /dev/null +++ b/web/src/pages/Repository/RepositoryContent/ContentHeader/search-background.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.tsx b/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.tsx index 7eb3c4cdd..2f840d337 100644 --- a/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.tsx +++ b/web/src/pages/Repository/RepositoryContent/FileContent/FileContent.tsx @@ -248,9 +248,8 @@ export function FileContent({
- rawURL: {rawURL} { Utils.stopEvent(e) downloadFile({ repoMetadata, resourcePath, gitRef, filename }) diff --git a/web/src/pages/Search/Search.module.scss b/web/src/pages/Search/Search.module.scss new file mode 100644 index 000000000..c5fe438ff --- /dev/null +++ b/web/src/pages/Search/Search.module.scss @@ -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); + } + } + } +} diff --git a/web/src/pages/Search/Search.module.scss.d.ts b/web/src/pages/Search/Search.module.scss.d.ts new file mode 100644 index 000000000..8ae28e339 --- /dev/null +++ b/web/src/pages/Search/Search.module.scss.d.ts @@ -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 diff --git a/web/src/pages/Search/Search.tsx b/web/src/pages/Search/Search.tsx new file mode 100644 index 000000000..c2c4cde52 --- /dev/null +++ b/web/src/pages/Search/Search.tsx @@ -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([]) + const [highlightlLineNumbersExtension, updateHighlightlLineNumbers] = useMemo( + () => addClassToLinesExtension([], css.highlightLineNumber), + [] + ) + const extensions = useMemo(() => { + return [ + lineNumbers({ + formatNumber: (lineNo: number) => lineNo.toString() + }), + highlightlLineNumbersExtension + ] + }, [highlightlLineNumbersExtension]) + const viewRef = useRef() + 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({ + verb: 'POST', + path: `/api/v1/repos/${repoMetadata?.path}/+/semantic/search` + }) + const [searchResult, setSearchResult] = useState([]) + 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 ( + + + +