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.
This commit is contained in:
Stefan Hengl 2024-07-01 10:45:46 +02:00 committed by GitHub
parent 40ea9132ab
commit 4f79980e0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 176 additions and 29 deletions

View File

@ -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

View File

@ -45,9 +45,17 @@ function firstMatchStreamingSearch(
}
export function fetchStreamSuggestions(query: string, sourcegraphURL?: string): Observable<SearchMatch[]> {
return fetchStreamSuggestionsPatternType(query, SearchPatternType.standard, sourcegraphURL)
}
export function fetchStreamSuggestionsPatternType(
query: string,
patternType: SearchPatternType,
sourcegraphURL?: string
): Observable<SearchMatch[]> {
return firstMatchStreamingSearch(of(query), {
version: LATEST_VERSION,
patternType: SearchPatternType.standard,
patternType,
caseSensitive: false,
trace: undefined,
sourcegraphURL,

View File

@ -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', () => {

View File

@ -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] => {

View File

@ -49,6 +49,7 @@ const notebooksFragment = gql`
stars {
totalCount
}
patternType
blocks {
... on MarkdownBlock {
__typename

View File

@ -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}
/>
)}
</WebStory>
@ -67,6 +69,7 @@ export const EditMode: StoryFn = () => (
isReadOnly={false}
showMenu={false}
isSourcegraphDotCom={false}
patternType={SearchPatternType.standard}
/>
)}
</WebStory>
@ -87,6 +90,7 @@ export const ErrorFetchingFile: StoryFn = () => (
isReadOnly={false}
showMenu={false}
isSourcegraphDotCom={false}
patternType={SearchPatternType.standard}
/>
)}
</WebStory>

View File

@ -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 = () => (

View File

@ -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<BlockProps, 'onRunBlock'> {
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<NotebookFileBlockInputsProps>
> = ({ 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, [])

View File

@ -80,7 +80,7 @@ const staticExtensions: Extension[] = [
editorHeight({ maxHeight: '60rem' }),
]
interface NotebookMarkdownBlockProps extends BlockProps<MarkdownBlock> {
interface NotebookMarkdownBlockProps extends Omit<BlockProps<MarkdownBlock>, 'patternType'> {
isEmbedded?: boolean
}

View File

@ -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}
/>
)}
</WebStory>
@ -94,6 +96,7 @@ export const Selected: StoryFn = () => (
settingsCascade={EMPTY_SETTINGS_CASCADE}
authenticatedUser={null}
platformContext={NOOP_PLATFORM_CONTEXT}
patternType={SearchPatternType.standard}
/>
)}
</WebStory>
@ -120,6 +123,7 @@ export const ReadOnlySelected: StoryFn = () => (
settingsCascade={EMPTY_SETTINGS_CASCADE}
authenticatedUser={null}
platformContext={NOOP_PLATFORM_CONTEXT}
patternType={SearchPatternType.standard}
/>
)}
</WebStory>

View File

@ -40,6 +40,7 @@ interface NotebookQueryBlockProps
isSourcegraphDotCom: boolean
fetchHighlightedFileLineRanges: (parameters: FetchFileParameters, force?: boolean) => Observable<string[][]>
authenticatedUser: AuthenticatedUser | null
patternType: SearchPatternType
}
// Defines the max height for the CodeMirror editor
@ -68,6 +69,7 @@ export const NotebookQueryBlock: React.FunctionComponent<React.PropsWithChildren
isSourcegraphDotCom,
searchContextsEnabled,
ownEnabled,
patternType,
...props
}) => {
const [editor, setEditor] = useState<EditorView | null>(null)
@ -107,10 +109,10 @@ export const NotebookQueryBlock: React.FunctionComponent<React.PropsWithChildren
type: 'link',
label: 'Open in new tab',
icon: <Icon aria-hidden={true} svgPath={mdiOpenInNew} />,
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<React.PropsWithChildren
<CodeMirrorQueryInput
ref={setEditor}
value={input.query}
patternType={SearchPatternType.standard}
patternType={patternType}
interpretComments={true}
onChange={onInputChange}
multiLine={true}

View File

@ -1,15 +1,17 @@
import type { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
import type { PathMatch, RepositoryMatch, SearchMatch, SymbolMatch } from '@sourcegraph/shared/src/search/stream'
import { fetchStreamSuggestions } from '@sourcegraph/shared/src/search/suggestions'
import { fetchStreamSuggestionsPatternType } from '@sourcegraph/shared/src/search/suggestions'
export function fetchSuggestions<T extends RepositoryMatch | PathMatch | SymbolMatch, O>(
query: string,
patternType: SearchPatternType,
filterSuggestionFunc: (match: SearchMatch) => match is T,
mapSuggestionFunc: (match: T) => O
): Observable<O[]> {
return fetchStreamSuggestions(query).pipe(
return fetchStreamSuggestionsPatternType(query, patternType).pipe(
map(suggestions => suggestions.filter(filterSuggestionFunc).map(mapSuggestionFunc))
)
}

View File

@ -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<BlockProps, 'onRunBlock'> {
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<NotebookSymbolBlockInputProps>
> = ({ 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(

View File

@ -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<T extends Block = Block> {
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

View File

@ -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<ListNotebooksResult['notebooks']> =>
{ __typename: 'MarkdownBlock', id: '1', markdownInput: '# Title' },
{ __typename: 'QueryBlock', id: '2', queryInput: 'query' },
],
patternType: SearchPatternType.standard,
},
{
__typename: 'Notebook',
@ -60,6 +62,7 @@ const fetchNotebooks = (): Observable<ListNotebooksResult['notebooks']> =>
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 },

View File

@ -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}
/>
)}
</WebStory>
@ -83,6 +85,7 @@ export const DefaultReadOnly: StoryFn = () => (
platformContext={NOOP_PLATFORM_CONTEXT}
exportedFileName="notebook.snb.md"
onCopyNotebook={() => NEVER}
patternType={SearchPatternType.standard}
/>
)}
</WebStory>

View File

@ -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<CopyNotebookProps, 'title'>) => Observable<NotebookFields>
patternType: SearchPatternType
}
const LOADING = 'LOADING' as const
@ -94,13 +96,14 @@ export const NotebookComponent: React.FunctionComponent<React.PropsWithChildren<
ownEnabled,
settingsCascade,
outlineContainerElement,
patternType,
}) => {
const notebook = useMemo(
() =>
new Notebook(initialBlocks, {
new Notebook(initialBlocks, patternType, {
fetchHighlightedFileLineRanges,
}),
[initialBlocks, fetchHighlightedFileLineRanges]
[initialBlocks, fetchHighlightedFileLineRanges, patternType]
)
const notebookElement = useRef<HTMLDivElement | null>(null)
@ -421,6 +424,7 @@ export const NotebookComponent: React.FunctionComponent<React.PropsWithChildren<
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
isSourcegraphDotCom={isSourcegraphDotCom}
patternType={patternType}
/>
)
}
@ -438,6 +442,7 @@ export const NotebookComponent: React.FunctionComponent<React.PropsWithChildren<
telemetryRecorder={telemetryRecorder}
platformContext={platformContext}
authenticatedUser={authenticatedUser}
patternType={patternType}
/>
)
}
@ -450,6 +455,7 @@ export const NotebookComponent: React.FunctionComponent<React.PropsWithChildren<
telemetryService={telemetryService}
telemetryRecorder={telemetryRecorder}
platformContext={platformContext}
patternType={patternType}
/>
)
}
@ -476,6 +482,7 @@ export const NotebookComponent: React.FunctionComponent<React.PropsWithChildren<
settingsCascade,
platformContext,
authenticatedUser,
patternType,
]
)

View File

@ -1,22 +1,21 @@
import { escapeRegExp } from 'lodash'
// We're using marked import here to access the `marked` package type definitions.
// eslint-disable-next-line no-restricted-imports
import { type marked, Renderer } from 'marked'
import { type Observable, forkJoin, of } from 'rxjs'
import { startWith, catchError, map, switchMap } from 'rxjs/operators'
import { forkJoin, type Observable, of } from 'rxjs'
import { catchError, map, startWith, switchMap } from 'rxjs/operators'
import * as uuid from 'uuid'
import { renderMarkdown, asError, isErrorLike } from '@sourcegraph/common'
import { asError, isErrorLike, renderMarkdown } from '@sourcegraph/common'
import {
aggregateStreamingSearch,
emptyAggregateResults,
LATEST_VERSION,
type SymbolMatch,
LATEST_VERSION,
} from '@sourcegraph/shared/src/search/stream'
import type { UIRangeSpec } from '@sourcegraph/shared/src/util/url'
import type { Block, BlockInit, BlockDependencies, BlockInput, BlockDirection, SymbolBlockInput } from '..'
import type { Block, BlockDependencies, BlockDirection, BlockInit, BlockInput, SymbolBlockInput } from '..'
import { type NotebookFields, SearchPatternType } from '../../graphql-operations'
import { parseBrowserRepoURL } from '../../util/url'
import { createNotebook } from '../backend'
@ -46,6 +45,7 @@ export function copyNotebook({ title, blocks, namespace }: CopyNotebookProps): O
function findSymbolAtRevision(
input: Omit<SymbolBlockInput, 'revision'>,
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<string, Block>
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 }

View File

@ -85,6 +85,7 @@ export const EmbeddedNotebookPage: FC<EmbeddedNotebookPageProps> = ({ platformCo
// Copying is not supported in embedded notebooks
onCopyNotebook={() => NEVER}
isEmbedded={true}
patternType={notebookOrError.patternType}
/>
)}
</div>

View File

@ -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<CopyNotebookProps, 'title'>) => Observable<NotebookFields>
patternType: SearchPatternType
}
export const NotebookContent: React.FunctionComponent<React.PropsWithChildren<NotebookContentProps>> = React.memo(
@ -52,6 +54,7 @@ export const NotebookContent: React.FunctionComponent<React.PropsWithChildren<No
platformContext,
outlineContainerElement,
isEmbedded,
patternType,
}) => {
const initializerBlocks: BlockInit[] = useMemo(
() =>
@ -101,6 +104,7 @@ export const NotebookContent: React.FunctionComponent<React.PropsWithChildren<No
onCopyNotebook={onCopyNotebook}
outlineContainerElement={outlineContainerElement}
isEmbedded={isEmbedded}
patternType={patternType}
/>
)
}

View File

@ -299,6 +299,7 @@ export const NotebookPage: React.FunctionComponent<React.PropsWithChildren<Noteb
settingsCascade={settingsCascade}
platformContext={platformContext}
outlineContainerElement={outlineContainerElement.current}
patternType={notebookOrError.patternType}
/>
</>
)}

View File

@ -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<BlobPageProps> = ({ className, co
onCopyNotebook={onCopyNotebook}
exportedFileName={basename(blobInfoOrError.filePath)}
className={styles.border}
patternType={SearchPatternType.standard}
/>
</React.Suspense>
)}

View File

@ -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 {

View File

@ -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!
}
"""

View File

@ -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

View File

@ -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
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -0,0 +1,2 @@
ALTER TABLE notebooks DROP COLUMN IF EXISTS pattern_type;
DROP TYPE IF EXISTS pattern_type;

View File

@ -0,0 +1,2 @@
name: notebooks_add_field_pattern_type
parents: [1717699555]

View File

@ -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';

View File

@ -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))))
);