From 4f79980e0eca30c484c973040ef24005626a7d80 Mon Sep 17 00:00:00 2001 From: Stefan Hengl Date: Mon, 1 Jul 2024 10:45:46 +0200 Subject: [PATCH] notebooks: store default pattern type per notebook (#63472) This PR refactors notebooks to support the upcoming Keyword Search GA. The main goal is to make it easier to switch to a new default pattern type without breaking existing notebooks. **Before** - pattern type and version were hardcoded in several places **After** - Each notebook has a read-only pattern type as determined by the new column `notebooks.pattern_type` (defaults to "standard"). **Notes** - Notebooks call the Stream API via various helper functions. `patternType` and `version` are both required parameters, which is redundant, because version acts as a default pattern type already. I left a TODO in the code for that. I don't want to change this as part of this PR because the change would get very big and affect too much code outside of Notebooks. - We support rendering notebooks with `.snb.md` extension. Unlike notebooks stored in our db, we cannot migrate those files. **Q&A** Q: How does this help for Keyword Search GA? A: Once we default to keyword search, we can change the default of `notebooks.pattern_type` from "standard" to "keyword". Existing notebooks will still work with "standard". New Notebooks will use "keyword". Q: How can customers migrate existing notebooks to a new version? A: Use the existing "Copy to My Notebooks" function of Notebooks. The copied notebook will have the current default pattern type. Test plan: - existing tests pass - manual testing - I created a couple of notebooks with all the different block types and verified via the network tab that all requests to the Stream API have the proper pattern type. I played around with different values in `notebooks.pattern_type` to make sure that the request parameters change. --- client/shared/src/search/stream.ts | 12 ++++++--- client/shared/src/search/suggestions/index.ts | 10 ++++++- client/web/src/integration/nav.test.ts | 3 ++- client/web/src/integration/notebook.test.ts | 2 ++ client/web/src/notebooks/backend.ts | 1 + .../blocks/file/NotebookFileBlock.story.tsx | 4 +++ .../file/NotebookFileBlockInputs.story.tsx | 3 +++ .../blocks/file/NotebookFileBlockInputs.tsx | 7 +++-- .../blocks/markdown/NotebookMarkdownBlock.tsx | 2 +- .../blocks/query/NotebookQueryBlock.story.tsx | 4 +++ .../blocks/query/NotebookQueryBlock.tsx | 8 +++--- .../blocks/suggestions/suggestions.ts | 6 +++-- .../symbol/NotebookSymbolBlockInput.tsx | 7 +++-- client/web/src/notebooks/index.ts | 2 ++ .../listPage/NotebooksListPage.story.tsx | 3 +++ .../notebook/NotebookComponent.story.tsx | 3 +++ .../notebooks/notebook/NotebookComponent.tsx | 11 ++++++-- client/web/src/notebooks/notebook/index.ts | 27 ++++++++++++------- .../notebookPage/EmbeddedNotebookPage.tsx | 1 + .../notebookPage/NotebookContent.tsx | 4 +++ .../notebooks/notebookPage/NotebookPage.tsx | 1 + client/web/src/repo/blob/BlobPage.tsx | 3 ++- cmd/frontend/graphqlbackend/notebooks.go | 1 + cmd/frontend/graphqlbackend/notebooks.graphql | 4 +++ .../internal/notebooks/resolvers/resolvers.go | 4 +++ .../shared/data/stitched-migration-graph.json | 15 ++++++++++- internal/database/schema.json | 23 ++++++++++++++++ internal/database/schema.md | 9 +++++++ internal/notebooks/store.go | 2 ++ internal/notebooks/types.go | 1 + .../down.sql | 2 ++ .../metadata.yaml | 2 ++ .../up.sql | 9 +++++++ migrations/frontend/squashed.sql | 9 +++++++ 34 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 migrations/frontend/1719214941_notebooks_add_field_pattern_type/down.sql create mode 100644 migrations/frontend/1719214941_notebooks_add_field_pattern_type/metadata.yaml create mode 100644 migrations/frontend/1719214941_notebooks_add_field_pattern_type/up.sql diff --git a/client/shared/src/search/stream.ts b/client/shared/src/search/stream.ts index e35ba3b7ba7..509a7ed0938 100644 --- a/client/shared/src/search/stream.ts +++ b/client/shared/src/search/stream.ts @@ -1,12 +1,12 @@ import { fetchEventSource } from '@microsoft/fetch-event-source' import { - Observable, fromEvent, - Subscription, + type Notification, + Observable, type OperatorFunction, pipe, type Subscriber, - type Notification, + Subscription, } from 'rxjs' import { defaultIfEmpty, map, materialize, scan, switchMap } from 'rxjs/operators' @@ -501,6 +501,12 @@ export const messageHandlers: MessageHandlers = { export interface StreamSearchOptions { version: string + /** + * TODO(stefan): "patternType" should be an optional parameter. Both Stream API and the GQL API don't require it. + * In the UI, we sometimes prefer to remove the "patternType:" filter from the query for better readability. + * "patternType" should be used to set the patternType of a query for those cases. Use "version" to + * define the default patternType instead. + */ patternType: SearchPatternType caseSensitive: boolean trace: string | undefined diff --git a/client/shared/src/search/suggestions/index.ts b/client/shared/src/search/suggestions/index.ts index c3b0e43b3c3..ae6fc051b1c 100644 --- a/client/shared/src/search/suggestions/index.ts +++ b/client/shared/src/search/suggestions/index.ts @@ -45,9 +45,17 @@ function firstMatchStreamingSearch( } export function fetchStreamSuggestions(query: string, sourcegraphURL?: string): Observable { + return fetchStreamSuggestionsPatternType(query, SearchPatternType.standard, sourcegraphURL) +} + +export function fetchStreamSuggestionsPatternType( + query: string, + patternType: SearchPatternType, + sourcegraphURL?: string +): Observable { return firstMatchStreamingSearch(of(query), { version: LATEST_VERSION, - patternType: SearchPatternType.standard, + patternType, caseSensitive: false, trace: undefined, sourcegraphURL, diff --git a/client/web/src/integration/nav.test.ts b/client/web/src/integration/nav.test.ts index 1ed6648ab30..762150bacf4 100644 --- a/client/web/src/integration/nav.test.ts +++ b/client/web/src/integration/nav.test.ts @@ -3,7 +3,7 @@ import expect from 'expect' import { after, afterEach, before, beforeEach, describe, test } from 'mocha' import { encodeURIPathComponent } from '@sourcegraph/common' -import type { SharedGraphQlOperations } from '@sourcegraph/shared/src/graphql-operations' +import { SearchPatternType, type SharedGraphQlOperations } from '@sourcegraph/shared/src/graphql-operations' import { mixedSearchStreamEvents } from '@sourcegraph/shared/src/search/integration/streaming-search-mocks' import { createDriverForTest, type Driver } from '@sourcegraph/shared/src/testing/driver' import { afterEachSaveScreenshotIfFailed } from '@sourcegraph/shared/src/testing/screenshotReporter' @@ -66,6 +66,7 @@ const notebookFixture = (id: string, title: string, blocks: NotebookFields['bloc creator: { __typename: 'User', username: 'user1' }, updater: { __typename: 'User', username: 'user1' }, blocks, + patternType: SearchPatternType.standard, }) describe('GlobalNavbar', () => { diff --git a/client/web/src/integration/notebook.test.ts b/client/web/src/integration/notebook.test.ts index acade60d31d..b6dbb650222 100644 --- a/client/web/src/integration/notebook.test.ts +++ b/client/web/src/integration/notebook.test.ts @@ -6,6 +6,7 @@ import expect from 'expect' import { afterEach, beforeEach, describe, it } from 'mocha' import type { SharedGraphQlOperations } from '@sourcegraph/shared/src/graphql-operations' +import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' import { highlightFileResult, mixedSearchStreamEvents, @@ -88,6 +89,7 @@ const notebookFixture = (id: string, title: string, blocks: NotebookFields['bloc creator: { __typename: 'User', username: 'user1' }, updater: { __typename: 'User', username: 'user1' }, blocks, + patternType: SearchPatternType.standard, }) const GQLBlockInputToResponse = (block: CreateNotebookBlockInput): NotebookFields['blocks'][number] => { diff --git a/client/web/src/notebooks/backend.ts b/client/web/src/notebooks/backend.ts index a555138b1f9..086b81c09a0 100644 --- a/client/web/src/notebooks/backend.ts +++ b/client/web/src/notebooks/backend.ts @@ -49,6 +49,7 @@ const notebooksFragment = gql` stars { totalCount } + patternType blocks { ... on MarkdownBlock { __typename diff --git a/client/web/src/notebooks/blocks/file/NotebookFileBlock.story.tsx b/client/web/src/notebooks/blocks/file/NotebookFileBlock.story.tsx index 11b66fb43fd..f1fc150ceb3 100644 --- a/client/web/src/notebooks/blocks/file/NotebookFileBlock.story.tsx +++ b/client/web/src/notebooks/blocks/file/NotebookFileBlock.story.tsx @@ -2,6 +2,7 @@ import type { Decorator, Meta, StoryFn } from '@storybook/react' import { noop } from 'lodash' import { of } from 'rxjs' +import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' import { HIGHLIGHTED_FILE_LINES_LONG } from '@sourcegraph/shared/src/testing/searchTestHelpers' import type { FileBlockInput } from '../..' @@ -49,6 +50,7 @@ export const Default: StoryFn = () => ( isReadOnly={false} showMenu={false} isSourcegraphDotCom={false} + patternType={SearchPatternType.standard} /> )} @@ -67,6 +69,7 @@ export const EditMode: StoryFn = () => ( isReadOnly={false} showMenu={false} isSourcegraphDotCom={false} + patternType={SearchPatternType.standard} /> )} @@ -87,6 +90,7 @@ export const ErrorFetchingFile: StoryFn = () => ( isReadOnly={false} showMenu={false} isSourcegraphDotCom={false} + patternType={SearchPatternType.standard} /> )} diff --git a/client/web/src/notebooks/blocks/file/NotebookFileBlockInputs.story.tsx b/client/web/src/notebooks/blocks/file/NotebookFileBlockInputs.story.tsx index c94d70e0831..7e89f98b84b 100644 --- a/client/web/src/notebooks/blocks/file/NotebookFileBlockInputs.story.tsx +++ b/client/web/src/notebooks/blocks/file/NotebookFileBlockInputs.story.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryFn, Decorator } from '@storybook/react' import { noop } from 'lodash' +import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' + import { WebStory } from '../../../components/WebStory' import { NotebookFileBlockInputs } from './NotebookFileBlockInputs' @@ -30,6 +32,7 @@ const defaultProps = { editor: undefined, onEditorCreated: noop, isSourcegraphDotCom: false, + patternType: SearchPatternType.standard, } export const Default: StoryFn = () => ( diff --git a/client/web/src/notebooks/blocks/file/NotebookFileBlockInputs.tsx b/client/web/src/notebooks/blocks/file/NotebookFileBlockInputs.tsx index ca0333803fa..91a8128cb9b 100644 --- a/client/web/src/notebooks/blocks/file/NotebookFileBlockInputs.tsx +++ b/client/web/src/notebooks/blocks/file/NotebookFileBlockInputs.tsx @@ -12,6 +12,7 @@ import { Icon, Button, Input, InputStatus } from '@sourcegraph/wildcard' import type { BlockProps, FileBlockInput } from '../..' import type { HighlightLineRange } from '../../../graphql-operations' +import { SearchPatternType } from '../../../graphql-operations' import { parseLineRange, serializeLineRange } from '../../serialize' import { SearchTypeSuggestionsInput } from '../suggestions/SearchTypeSuggestionsInput' import { fetchSuggestions } from '../suggestions/suggestions' @@ -21,6 +22,7 @@ import styles from './NotebookFileBlockInputs.module.scss' interface NotebookFileBlockInputsProps extends Pick { id: string queryInput: string + patternType: SearchPatternType lineRange: HighlightLineRange | null onEditorCreated: (editor: EditorView) => void setQueryInput: (value: string) => void @@ -44,7 +46,7 @@ const editorAttributes = [ export const NotebookFileBlockInputs: React.FunctionComponent< React.PropsWithChildren -> = ({ id, lineRange, onFileSelected, onLineRangeChange, isSourcegraphDotCom, ...inputProps }) => { +> = ({ id, lineRange, onFileSelected, onLineRangeChange, isSourcegraphDotCom, patternType, ...inputProps }) => { const [lineRangeInput, setLineRangeInput] = useState(serializeLineRange(lineRange)) const debouncedOnLineRangeChange = useMemo(() => debounce(onLineRangeChange, 300), [onLineRangeChange]) @@ -65,10 +67,11 @@ export const NotebookFileBlockInputs: React.FunctionComponent< (query: string) => fetchSuggestions( getFileSuggestionsQuery(query), + patternType, (suggestion): suggestion is PathMatch => suggestion.type === 'path', file => file ), - [] + [patternType] ) const countSuggestions = useCallback((suggestions: PathMatch[]) => suggestions.length, []) diff --git a/client/web/src/notebooks/blocks/markdown/NotebookMarkdownBlock.tsx b/client/web/src/notebooks/blocks/markdown/NotebookMarkdownBlock.tsx index f530fe443ef..d3e5dfbda53 100644 --- a/client/web/src/notebooks/blocks/markdown/NotebookMarkdownBlock.tsx +++ b/client/web/src/notebooks/blocks/markdown/NotebookMarkdownBlock.tsx @@ -80,7 +80,7 @@ const staticExtensions: Extension[] = [ editorHeight({ maxHeight: '60rem' }), ] -interface NotebookMarkdownBlockProps extends BlockProps { +interface NotebookMarkdownBlockProps extends Omit, 'patternType'> { isEmbedded?: boolean } diff --git a/client/web/src/notebooks/blocks/query/NotebookQueryBlock.story.tsx b/client/web/src/notebooks/blocks/query/NotebookQueryBlock.story.tsx index c63db23ec54..fa72643665f 100644 --- a/client/web/src/notebooks/blocks/query/NotebookQueryBlock.story.tsx +++ b/client/web/src/notebooks/blocks/query/NotebookQueryBlock.story.tsx @@ -2,6 +2,7 @@ import type { Decorator, StoryFn, Meta } from '@storybook/react' import { noop } from 'lodash' import { of } from 'rxjs' +import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' import type { AggregateStreamingSearchResults } from '@sourcegraph/shared/src/search/stream' import { EMPTY_SETTINGS_CASCADE } from '@sourcegraph/shared/src/settings/settings' import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry' @@ -68,6 +69,7 @@ export const Default: StoryFn = () => ( fetchHighlightedFileLineRanges={() => of([HIGHLIGHTED_FILE_LINES_LONG])} settingsCascade={EMPTY_SETTINGS_CASCADE} platformContext={NOOP_PLATFORM_CONTEXT} + patternType={SearchPatternType.standard} /> )} @@ -94,6 +96,7 @@ export const Selected: StoryFn = () => ( settingsCascade={EMPTY_SETTINGS_CASCADE} authenticatedUser={null} platformContext={NOOP_PLATFORM_CONTEXT} + patternType={SearchPatternType.standard} /> )} @@ -120,6 +123,7 @@ export const ReadOnlySelected: StoryFn = () => ( settingsCascade={EMPTY_SETTINGS_CASCADE} authenticatedUser={null} platformContext={NOOP_PLATFORM_CONTEXT} + patternType={SearchPatternType.standard} /> )} diff --git a/client/web/src/notebooks/blocks/query/NotebookQueryBlock.tsx b/client/web/src/notebooks/blocks/query/NotebookQueryBlock.tsx index d940630c315..abec1e4c5ff 100644 --- a/client/web/src/notebooks/blocks/query/NotebookQueryBlock.tsx +++ b/client/web/src/notebooks/blocks/query/NotebookQueryBlock.tsx @@ -40,6 +40,7 @@ interface NotebookQueryBlockProps isSourcegraphDotCom: boolean fetchHighlightedFileLineRanges: (parameters: FetchFileParameters, force?: boolean) => Observable authenticatedUser: AuthenticatedUser | null + patternType: SearchPatternType } // Defines the max height for the CodeMirror editor @@ -68,6 +69,7 @@ export const NotebookQueryBlock: React.FunctionComponent { const [editor, setEditor] = useState(null) @@ -107,10 +109,10 @@ export const NotebookQueryBlock: React.FunctionComponent, - url: `/search?${buildSearchURLQuery(input.query, SearchPatternType.standard, false)}`, + url: `/search?${buildSearchURLQuery(input.query, patternType, false)}`, }, ], - [input] + [input, patternType] ) const commonMenuActions = linkMenuActions.concat(useCommonBlockMenuActions({ id, ...props })) @@ -161,7 +163,7 @@ export const NotebookQueryBlock: React.FunctionComponent( query: string, + patternType: SearchPatternType, filterSuggestionFunc: (match: SearchMatch) => match is T, mapSuggestionFunc: (match: T) => O ): Observable { - return fetchStreamSuggestions(query).pipe( + return fetchStreamSuggestionsPatternType(query, patternType).pipe( map(suggestions => suggestions.filter(filterSuggestionFunc).map(mapSuggestionFunc)) ) } diff --git a/client/web/src/notebooks/blocks/symbol/NotebookSymbolBlockInput.tsx b/client/web/src/notebooks/blocks/symbol/NotebookSymbolBlockInput.tsx index 088483de85a..75632eb9ef9 100644 --- a/client/web/src/notebooks/blocks/symbol/NotebookSymbolBlockInput.tsx +++ b/client/web/src/notebooks/blocks/symbol/NotebookSymbolBlockInput.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useMemo } from 'react' import { EditorView } from '@codemirror/view' import { createDefaultSuggestions, RepoFileLink } from '@sourcegraph/branded' +import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' import { getFileMatchUrl, getRepositoryUrl, type SymbolMatch } from '@sourcegraph/shared/src/search/stream' import { fetchStreamSuggestions } from '@sourcegraph/shared/src/search/suggestions' import { useExperimentalFeatures } from '@sourcegraph/shared/src/settings/settings' @@ -18,6 +19,7 @@ import styles from './NotebookSymbolBlockInput.module.scss' interface NotebookSymbolBlockInputProps extends Pick { id: string queryInput: string + patternType: SearchPatternType onEditorCreated: (editor: EditorView) => void setQueryInput: (value: string) => void onSymbolSelected: (symbol: SymbolBlockInput) => void @@ -39,15 +41,16 @@ const editorAttributes = [ export const NotebookSymbolBlockInput: React.FunctionComponent< React.PropsWithChildren -> = ({ onSymbolSelected, isSourcegraphDotCom, ...inputProps }) => { +> = ({ onSymbolSelected, isSourcegraphDotCom, patternType, ...inputProps }) => { const fetchSymbolSuggestions = useCallback( (query: string) => fetchSuggestions( getSymbolSuggestionsQuery(query), + patternType, (suggestion): suggestion is SymbolMatch => suggestion.type === 'symbol', symbol => symbol ), - [] + [patternType] ) const countSuggestions = useCallback( diff --git a/client/web/src/notebooks/index.ts b/client/web/src/notebooks/index.ts index 21f444917e2..14a9f3f617a 100644 --- a/client/web/src/notebooks/index.ts +++ b/client/web/src/notebooks/index.ts @@ -6,6 +6,7 @@ import type { AggregateStreamingSearchResults } from '@sourcegraph/shared/src/se import type { UIRangeSpec } from '@sourcegraph/shared/src/util/url' import type { HighlightLineRange, SymbolKind } from '../graphql-operations' +import { SearchPatternType } from '../graphql-operations' // When adding a new block type, make sure to track its usage in internal/usagestats/notebooks.go. export type BlockType = 'md' | 'query' | 'file' | 'compute' | 'symbol' @@ -108,6 +109,7 @@ export interface BlockProps { id: T['id'] input: T['input'] output: T['output'] + patternType: SearchPatternType onRunBlock(id: string): void onDeleteBlock(id: string): void onBlockInputChange(id: string, blockInput: BlockInput): void diff --git a/client/web/src/notebooks/listPage/NotebooksListPage.story.tsx b/client/web/src/notebooks/listPage/NotebooksListPage.story.tsx index 026354fa1bc..936d8cbe059 100644 --- a/client/web/src/notebooks/listPage/NotebooksListPage.story.tsx +++ b/client/web/src/notebooks/listPage/NotebooksListPage.story.tsx @@ -7,6 +7,7 @@ import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/teleme import { WebStory } from '../../components/WebStory' import type { ListNotebooksResult } from '../../graphql-operations' +import { SearchPatternType } from '../../graphql-operations' import { NotebooksListPage } from './NotebooksListPage' @@ -45,6 +46,7 @@ const fetchNotebooks = (): Observable => { __typename: 'MarkdownBlock', id: '1', markdownInput: '# Title' }, { __typename: 'QueryBlock', id: '2', queryInput: 'query' }, ], + patternType: SearchPatternType.standard, }, { __typename: 'Notebook', @@ -60,6 +62,7 @@ const fetchNotebooks = (): Observable => updater: { __typename: 'User', username: 'user2' }, namespace: { __typename: 'User', namespaceName: 'user2', id: '2' }, blocks: [{ __typename: 'MarkdownBlock', id: '1', markdownInput: '# Title' }], + patternType: SearchPatternType.standard, }, ], pageInfo: { hasNextPage: false, endCursor: null }, diff --git a/client/web/src/notebooks/notebook/NotebookComponent.story.tsx b/client/web/src/notebooks/notebook/NotebookComponent.story.tsx index 90da9ac376e..cb17c1e93d7 100644 --- a/client/web/src/notebooks/notebook/NotebookComponent.story.tsx +++ b/client/web/src/notebooks/notebook/NotebookComponent.story.tsx @@ -1,6 +1,7 @@ import type { Decorator, Meta, StoryFn } from '@storybook/react' import { NEVER, of } from 'rxjs' +import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' import { EMPTY_SETTINGS_CASCADE } from '@sourcegraph/shared/src/settings/settings' import { noOpTelemetryRecorder } from '@sourcegraph/shared/src/telemetry' import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -58,6 +59,7 @@ export const Default: StoryFn = () => ( platformContext={NOOP_PLATFORM_CONTEXT} exportedFileName="notebook.snb.md" onCopyNotebook={() => NEVER} + patternType={SearchPatternType.standard} /> )} @@ -83,6 +85,7 @@ export const DefaultReadOnly: StoryFn = () => ( platformContext={NOOP_PLATFORM_CONTEXT} exportedFileName="notebook.snb.md" onCopyNotebook={() => NEVER} + patternType={SearchPatternType.standard} /> )} diff --git a/client/web/src/notebooks/notebook/NotebookComponent.tsx b/client/web/src/notebooks/notebook/NotebookComponent.tsx index 08b0b444be2..06447948a25 100644 --- a/client/web/src/notebooks/notebook/NotebookComponent.tsx +++ b/client/web/src/notebooks/notebook/NotebookComponent.tsx @@ -17,6 +17,7 @@ import { Button, useEventObservable, Icon } from '@sourcegraph/wildcard' import { V2BlockTypes, type Block, type BlockDirection, type BlockInit, type BlockInput, type BlockType } from '..' import type { AuthenticatedUser } from '../../auth' import type { NotebookFields } from '../../graphql-operations' +import { SearchPatternType } from '../../graphql-operations' import type { OwnConfigProps } from '../../own/OwnConfigProps' import { PageRoutes } from '../../routes.constants' import type { SearchStreamingProps } from '../../search' @@ -47,6 +48,7 @@ export interface NotebookComponentProps outlineContainerElement?: HTMLElement | null onSerializeBlocks: (blocks: Block[]) => void onCopyNotebook: (props: Omit) => Observable + patternType: SearchPatternType } const LOADING = 'LOADING' as const @@ -94,13 +96,14 @@ export const NotebookComponent: React.FunctionComponent { const notebook = useMemo( () => - new Notebook(initialBlocks, { + new Notebook(initialBlocks, patternType, { fetchHighlightedFileLineRanges, }), - [initialBlocks, fetchHighlightedFileLineRanges] + [initialBlocks, fetchHighlightedFileLineRanges, patternType] ) const notebookElement = useRef(null) @@ -421,6 +424,7 @@ export const NotebookComponent: React.FunctionComponent ) } @@ -438,6 +442,7 @@ export const NotebookComponent: React.FunctionComponent ) } @@ -450,6 +455,7 @@ export const NotebookComponent: React.FunctionComponent ) } @@ -476,6 +482,7 @@ export const NotebookComponent: React.FunctionComponent, + patternType: SearchPatternType, revision: string ): Observable<{ range: UIRangeSpec['range']; revision: string } | Error> { const { repositoryName, filePath, symbolName, symbolContainerName, symbolKind } = input @@ -53,6 +53,7 @@ function findSymbolAtRevision( `repo:${escapeRegExp(repositoryName)} file:${escapeRegExp( filePath )} rev:${revision} ${symbolName} type:symbol count:50`, + patternType, (suggestion): suggestion is SymbolMatch => suggestion.type === 'symbol', symbol => symbol ).pipe( @@ -98,11 +99,17 @@ export class NotebookHeadingMarkdownRenderer extends Renderer { export class Notebook { private blocks: Map private blockOrder: string[] + private patternType: SearchPatternType - constructor(initializerBlocks: BlockInit[], private dependencies: BlockDependencies) { + constructor( + initializerBlocks: BlockInit[], + patternType: SearchPatternType, + private dependencies: BlockDependencies + ) { const blocks = initializerBlocks.map(block => ({ ...block, output: null })) this.blocks = new Map(blocks.map(block => [block.id, block])) + this.patternType = patternType this.blockOrder = blocks.map(block => block.id) // Pre-run certain blocks, for a better user experience. @@ -162,7 +169,7 @@ export class Notebook { ...block, output: aggregateStreamingSearch(of(query), { version: LATEST_VERSION, - patternType: SearchPatternType.standard, + patternType: this.patternType, caseSensitive: false, trace: undefined, chunkMatches: true, @@ -192,13 +199,13 @@ export class Notebook { } case 'symbol': { // Start by searching for the symbol at the latest HEAD (main) revision. - const output = findSymbolAtRevision(block.input, 'HEAD').pipe( + const output = findSymbolAtRevision(block.input, this.patternType, 'HEAD').pipe( switchMap(symbolSearchResult => { if (!isErrorLike(symbolSearchResult)) { return of({ ...symbolSearchResult, symbolFoundAtLatestRevision: true }) } // If not found, look at the revision stored in the block input (should always be found). - return findSymbolAtRevision(block.input, block.input.revision).pipe( + return findSymbolAtRevision(block.input, this.patternType, block.input.revision).pipe( map(symbolSearchResult => !isErrorLike(symbolSearchResult) ? { ...symbolSearchResult, symbolFoundAtLatestRevision: false } diff --git a/client/web/src/notebooks/notebookPage/EmbeddedNotebookPage.tsx b/client/web/src/notebooks/notebookPage/EmbeddedNotebookPage.tsx index b0057faaff8..0d543ba2178 100644 --- a/client/web/src/notebooks/notebookPage/EmbeddedNotebookPage.tsx +++ b/client/web/src/notebooks/notebookPage/EmbeddedNotebookPage.tsx @@ -85,6 +85,7 @@ export const EmbeddedNotebookPage: FC = ({ platformCo // Copying is not supported in embedded notebooks onCopyNotebook={() => NEVER} isEmbedded={true} + patternType={notebookOrError.patternType} /> )} diff --git a/client/web/src/notebooks/notebookPage/NotebookContent.tsx b/client/web/src/notebooks/notebookPage/NotebookContent.tsx index 4a78c09fb83..bf84a105a20 100644 --- a/client/web/src/notebooks/notebookPage/NotebookContent.tsx +++ b/client/web/src/notebooks/notebookPage/NotebookContent.tsx @@ -11,6 +11,7 @@ import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetry import type { Block, BlockInit } from '..' import type { NotebookFields } from '../../graphql-operations' +import { SearchPatternType } from '../../graphql-operations' import type { OwnConfigProps } from '../../own/OwnConfigProps' import type { SearchStreamingProps } from '../../search' import type { CopyNotebookProps } from '../notebook' @@ -31,6 +32,7 @@ export interface NotebookContentProps outlineContainerElement?: HTMLElement | null onUpdateBlocks: (blocks: Block[]) => void onCopyNotebook: (props: Omit) => Observable + patternType: SearchPatternType } export const NotebookContent: React.FunctionComponent> = React.memo( @@ -52,6 +54,7 @@ export const NotebookContent: React.FunctionComponent { const initializerBlocks: BlockInit[] = useMemo( () => @@ -101,6 +104,7 @@ export const NotebookContent: React.FunctionComponent ) } diff --git a/client/web/src/notebooks/notebookPage/NotebookPage.tsx b/client/web/src/notebooks/notebookPage/NotebookPage.tsx index 44714921965..7317175b9ee 100644 --- a/client/web/src/notebooks/notebookPage/NotebookPage.tsx +++ b/client/web/src/notebooks/notebookPage/NotebookPage.tsx @@ -299,6 +299,7 @@ export const NotebookPage: React.FunctionComponent )} diff --git a/client/web/src/repo/blob/BlobPage.tsx b/client/web/src/repo/blob/BlobPage.tsx index 4c0e9c099bb..d7702175ab3 100644 --- a/client/web/src/repo/blob/BlobPage.tsx +++ b/client/web/src/repo/blob/BlobPage.tsx @@ -22,7 +22,7 @@ import { useCurrentSpan, } from '@sourcegraph/observability-client' import type { FetchFileParameters } from '@sourcegraph/shared/src/backend/file' -import { HighlightResponseFormat } from '@sourcegraph/shared/src/graphql-operations' +import { HighlightResponseFormat, SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' import type { SearchContextProps } from '@sourcegraph/shared/src/search' import { useExperimentalFeatures, type SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings' @@ -582,6 +582,7 @@ export const BlobPage: React.FunctionComponent = ({ className, co onCopyNotebook={onCopyNotebook} exportedFileName={basename(blobInfoOrError.filePath)} className={styles.border} + patternType={SearchPatternType.standard} /> )} diff --git a/cmd/frontend/graphqlbackend/notebooks.go b/cmd/frontend/graphqlbackend/notebooks.go index d66b954881f..2cfb1d09351 100644 --- a/cmd/frontend/graphqlbackend/notebooks.go +++ b/cmd/frontend/graphqlbackend/notebooks.go @@ -60,6 +60,7 @@ type NotebookResolver interface { ViewerCanManage(ctx context.Context) (bool, error) ViewerHasStarred(ctx context.Context) (bool, error) Stars(ctx context.Context, args ListNotebookStarsArgs) (NotebookStarConnectionResolver, error) + PatternType(ctx context.Context) string } type NotebookBlockResolver interface { diff --git a/cmd/frontend/graphqlbackend/notebooks.graphql b/cmd/frontend/graphqlbackend/notebooks.graphql index d770c6546ce..27d7ee1b231 100644 --- a/cmd/frontend/graphqlbackend/notebooks.graphql +++ b/cmd/frontend/graphqlbackend/notebooks.graphql @@ -298,6 +298,10 @@ type Notebook implements Node { """ after: String ): NotebookStarConnection! + """ + The default pattern type that is used to interpret queries that do not contain a patternType: filter. + """ + patternType: SearchPatternType! } """ diff --git a/cmd/frontend/internal/notebooks/resolvers/resolvers.go b/cmd/frontend/internal/notebooks/resolvers/resolvers.go index 3931619703d..c5c78e173df 100644 --- a/cmd/frontend/internal/notebooks/resolvers/resolvers.go +++ b/cmd/frontend/internal/notebooks/resolvers/resolvers.go @@ -513,6 +513,10 @@ func (r *notebookBlockResolver) ToSymbolBlock() (graphqlbackend.SymbolBlockResol return nil, false } +func (r *notebookResolver) PatternType(_ context.Context) string { + return r.notebook.PatternType +} + type markdownBlockResolver struct { // block.type == NotebookMarkdownBlockType block notebooks.NotebookBlock diff --git a/internal/database/migration/shared/data/stitched-migration-graph.json b/internal/database/migration/shared/data/stitched-migration-graph.json index d0f8a057a66..3cf51c21f34 100644 --- a/internal/database/migration/shared/data/stitched-migration-graph.json +++ b/internal/database/migration/shared/data/stitched-migration-graph.json @@ -11234,6 +11234,19 @@ ], "IsCreateIndexConcurrently": false, "IndexMetadata": null + }, + { + "ID": 1719214941, + "Name": "notebooks_add_field_pattern_type", + "UpQuery": "DO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'pattern_type') THEN\n CREATE TYPE pattern_type AS ENUM ('keyword', 'literal', 'regexp', 'standard', 'structural');\n END IF;\nEND\n$$;\n\nALTER TABLE notebooks ADD COLUMN IF NOT EXISTS pattern_type pattern_type NOT NULL DEFAULT 'standard';", + "DownQuery": "ALTER TABLE notebooks DROP COLUMN IF EXISTS pattern_type;\nDROP TYPE IF EXISTS pattern_type;", + "Privileged": false, + "NonIdempotent": false, + "Parents": [ + 1717699555 + ], + "IsCreateIndexConcurrently": false, + "IndexMetadata": null } ], "BoundsByRev": { @@ -11494,7 +11507,7 @@ "v5.4.0": { "RootID": 1648051770, "LeafIDs": [ - 1717699555 + 1719214941 ], "PreCreation": false } diff --git a/internal/database/schema.json b/internal/database/schema.json index ae2c8bc90a6..58ae9dc3ac8 100644 --- a/internal/database/schema.json +++ b/internal/database/schema.json @@ -45,6 +45,16 @@ "rollout" ] }, + { + "Name": "pattern_type", + "Labels": [ + "keyword", + "literal", + "regexp", + "standard", + "structural" + ] + }, { "Name": "persistmode", "Labels": [ @@ -19077,6 +19087,19 @@ "GenerationExpression": "", "Comment": "" }, + { + "Name": "pattern_type", + "Index": 12, + "TypeName": "pattern_type", + "IsNullable": false, + "Default": "'standard'::pattern_type", + "CharacterMaximumLength": 0, + "IsIdentity": false, + "IdentityGeneration": "", + "IsGenerated": "NEVER", + "GenerationExpression": "", + "Comment": "" + }, { "Name": "public", "Index": 4, diff --git a/internal/database/schema.md b/internal/database/schema.md index 43c0aea5edd..0b6a0958762 100644 --- a/internal/database/schema.md +++ b/internal/database/schema.md @@ -2798,6 +2798,7 @@ Foreign-key constraints: namespace_user_id | integer | | | namespace_org_id | integer | | | updater_user_id | integer | | | + pattern_type | pattern_type | | not null | 'standard'::pattern_type Indexes: "notebooks_pkey" PRIMARY KEY, btree (id) "notebooks_blocks_tsvector_idx" gin (blocks_tsvector) @@ -5096,6 +5097,14 @@ Foreign-key constraints: - bool - rollout +# Type pattern_type + +- keyword +- literal +- regexp +- standard +- structural + # Type persistmode - record diff --git a/internal/notebooks/store.go b/internal/notebooks/store.go index dece19c62cd..8ab92b8c3e8 100644 --- a/internal/notebooks/store.go +++ b/internal/notebooks/store.go @@ -117,6 +117,7 @@ var notebookColumns = []*sqlf.Query{ sqlf.Sprintf("notebooks.namespace_org_id"), sqlf.Sprintf("notebooks.created_at"), sqlf.Sprintf("notebooks.updated_at"), + sqlf.Sprintf("pattern_type"), } func notebooksPermissionsCondition(ctx context.Context) *sqlf.Query { @@ -196,6 +197,7 @@ func scanNotebook(scanner dbutil.Scanner) (*Notebook, error) { &dbutil.NullInt32{N: &n.NamespaceOrgID}, &n.CreatedAt, &n.UpdatedAt, + &n.PatternType, ) if err != nil { return nil, err diff --git a/internal/notebooks/types.go b/internal/notebooks/types.go index 54eb56bc9a4..ede1203d80b 100644 --- a/internal/notebooks/types.go +++ b/internal/notebooks/types.go @@ -68,6 +68,7 @@ type Notebook struct { NamespaceOrgID int32 // if non-zero, the owner is this organization. NamespaceUserID/NamespaceOrgID are mutually exclusive. CreatedAt time.Time UpdatedAt time.Time + PatternType string } type NotebookStar struct { diff --git a/migrations/frontend/1719214941_notebooks_add_field_pattern_type/down.sql b/migrations/frontend/1719214941_notebooks_add_field_pattern_type/down.sql new file mode 100644 index 00000000000..81f0fc5b570 --- /dev/null +++ b/migrations/frontend/1719214941_notebooks_add_field_pattern_type/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE notebooks DROP COLUMN IF EXISTS pattern_type; +DROP TYPE IF EXISTS pattern_type; diff --git a/migrations/frontend/1719214941_notebooks_add_field_pattern_type/metadata.yaml b/migrations/frontend/1719214941_notebooks_add_field_pattern_type/metadata.yaml new file mode 100644 index 00000000000..6db2c1073ca --- /dev/null +++ b/migrations/frontend/1719214941_notebooks_add_field_pattern_type/metadata.yaml @@ -0,0 +1,2 @@ +name: notebooks_add_field_pattern_type +parents: [1717699555] diff --git a/migrations/frontend/1719214941_notebooks_add_field_pattern_type/up.sql b/migrations/frontend/1719214941_notebooks_add_field_pattern_type/up.sql new file mode 100644 index 00000000000..b8fc55f4f7b --- /dev/null +++ b/migrations/frontend/1719214941_notebooks_add_field_pattern_type/up.sql @@ -0,0 +1,9 @@ +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'pattern_type') THEN + CREATE TYPE pattern_type AS ENUM ('keyword', 'literal', 'regexp', 'standard', 'structural'); + END IF; +END +$$; + +ALTER TABLE notebooks ADD COLUMN IF NOT EXISTS pattern_type pattern_type NOT NULL DEFAULT 'standard'; diff --git a/migrations/frontend/squashed.sql b/migrations/frontend/squashed.sql index eb5683ad065..5c6e305fe0c 100644 --- a/migrations/frontend/squashed.sql +++ b/migrations/frontend/squashed.sql @@ -76,6 +76,14 @@ CREATE TYPE lsif_uploads_transition_columns AS ( COMMENT ON TYPE lsif_uploads_transition_columns IS 'A type containing the columns that make-up the set of tracked transition columns. Primarily used to create a nulled record due to `OLD` being unset in INSERT queries, and creating a nulled record with a subquery is not allowed.'; +CREATE TYPE pattern_type AS ENUM ( + 'keyword', + 'literal', + 'regexp', + 'standard', + 'structural' +); + CREATE TYPE persistmode AS ENUM ( 'record', 'snapshot' @@ -3553,6 +3561,7 @@ CREATE TABLE notebooks ( namespace_user_id integer, namespace_org_id integer, updater_user_id integer, + pattern_type pattern_type DEFAULT 'standard'::pattern_type NOT NULL, CONSTRAINT blocks_is_array CHECK ((jsonb_typeof(blocks) = 'array'::text)), CONSTRAINT notebooks_has_max_1_namespace CHECK ((((namespace_user_id IS NULL) AND (namespace_org_id IS NULL)) OR ((namespace_user_id IS NULL) <> (namespace_org_id IS NULL)))) );