mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 11:01:44 +00:00
feat(svelte): Migrate dotcom community search pages to Svelte (#64388)
What: This PR does the bare minimum to migrate the current community search pages to Svelte. A better strategy for managing them is needed in the medium/long term. How: The community pages live at the root (e.g. `/kubernetes`) which complicates things, but I'll get to that later. The page is implemented as a single parameterized route. A parameter matcher is used to validate the community name. Because these pages should only be accessible on dotcom the matcher also validates whether or not we are on dotcom (if not, the path will be matched against a different route). The page config is stored in a separate module so that it's no included in every page and so that it can be used in the integration test. The loader and page implementation themselves are straightforward. I made a couple of changes in other modules to make implementation easier: - Extracted the parameter type of the `marked` function so that it can be used as prop type. - Added an `inline` option to `marked` that allows formatting markdown as 'inline', i.e. without `p` wrapper. - Added a `wrap` prop to `SyntaxHighlightedQuery.svelte` to configure line wrapping of syntax highlighted search queries (instead of having to overwrite styles with `:global`). - Extended the route code generator to be able to handle single parameter segments and the `communitySearchContext` matcher. Because the community routes should only be available on dotcom I added a new tag to the code generator that allows it include routes only for dotcom. Once we change how all this works and have community search pages live under a different path we can simplify this again. Result: | React | Svelte | |--------|--------| |  |  | ## Test plan - New integration tests. - Verified that `/kubernetes` shows a 'repo not found error' when running against S2. - Verified that `/kubernetes` shows the community page when running against dotcom. - Verified that `window.context.svelteKit.enabledRoutes` contains the community page route in enterprise mode but not in dotcom mode.
This commit is contained in:
parent
f1060eccac
commit
8296e9804f
@ -41,6 +41,42 @@ export const highlightCodeSafe = (code: string, language?: string): string => {
|
||||
}
|
||||
}
|
||||
|
||||
export interface RenderMarkdownOptions {
|
||||
/**
|
||||
* Whether to render markdown inline, without paragraph tags
|
||||
*/
|
||||
inline?: boolean
|
||||
/**
|
||||
* Whether to render line breaks as HTML `<br>`s
|
||||
*/
|
||||
breaks?: boolean
|
||||
/**
|
||||
* Whether to disable autolinks. Explicit links using `[text](url)` are still allowed.
|
||||
*/
|
||||
disableAutolinks?: boolean
|
||||
/**
|
||||
* A custom renderer to use
|
||||
*/
|
||||
renderer?: marked.Renderer
|
||||
/**
|
||||
* A prefix to add to all header IDs
|
||||
*/
|
||||
headerPrefix?: string
|
||||
/**
|
||||
* Strip off any HTML and return a plain text string, useful for previews
|
||||
*/
|
||||
plainText?: boolean
|
||||
/**
|
||||
* DOMPurify configuration to use
|
||||
*/
|
||||
dompurifyConfig?: DOMPurifyConfig & { RETURN_DOM_FRAGMENT?: false; RETURN_DOM?: false }
|
||||
/**
|
||||
* Add target="_blank" and rel="noopener" to all <a> links that have a href value.
|
||||
* This affects all markdown-formatted links and all inline HTML links.
|
||||
*/
|
||||
addTargetBlankToAllLinks?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the given markdown to HTML, highlighting code and sanitizing dangerous HTML.
|
||||
* Can throw an exception on parse errors.
|
||||
@ -55,18 +91,7 @@ export const highlightCodeSafe = (code: string, language?: string): string => {
|
||||
* @param options.addTargetBlankToAllLinks Add target="_blank" and rel="noopener" to all <a> links
|
||||
* that have a href value. This affects all markdown-formatted links and all inline HTML links.
|
||||
*/
|
||||
export const renderMarkdown = (
|
||||
markdown: string,
|
||||
options: {
|
||||
breaks?: boolean
|
||||
disableAutolinks?: boolean
|
||||
renderer?: marked.Renderer
|
||||
headerPrefix?: string
|
||||
plainText?: boolean
|
||||
dompurifyConfig?: DOMPurifyConfig & { RETURN_DOM_FRAGMENT?: false; RETURN_DOM?: false }
|
||||
addTargetBlankToAllLinks?: boolean
|
||||
} = {}
|
||||
): string => {
|
||||
export const renderMarkdown = (markdown: string, options: RenderMarkdownOptions = {}): string => {
|
||||
const tokenizer = new marked.Tokenizer()
|
||||
if (options.disableAutolinks) {
|
||||
// Why the odd double-casting below?
|
||||
@ -76,7 +101,7 @@ export const renderMarkdown = (
|
||||
tokenizer.url = () => undefined as unknown as marked.Tokens.Link
|
||||
}
|
||||
|
||||
const rendered = marked(markdown, {
|
||||
const rendered = (options.inline ? marked.parseInline : marked)(markdown, {
|
||||
gfm: true,
|
||||
breaks: options.breaks,
|
||||
highlight: (code, language) => highlightCodeSafe(code, language),
|
||||
|
||||
@ -199,7 +199,7 @@ playwright_test_bin.playwright_test(
|
||||
],
|
||||
data = glob(
|
||||
[
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.ts",
|
||||
"src/testing/*.ts",
|
||||
],
|
||||
) + [
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
window.context,
|
||||
// Dev specific overwrites
|
||||
{
|
||||
sentryDNS: undefined,
|
||||
sentryDSN: undefined,
|
||||
},
|
||||
// Playwright specific overwrites
|
||||
window.playwrightContext
|
||||
|
||||
@ -6,7 +6,7 @@ export { createAggregateError, asError } from '@sourcegraph/common/src/errors/er
|
||||
export { memoizeObservable, resetAllMemoizationCaches } from '@sourcegraph/common/src/util/rxjs/memoizeObservable'
|
||||
export { encodeURIPathComponent } from '@sourcegraph/common/src/util/url'
|
||||
export { pluralize, numberWithCommas } from '@sourcegraph/common/src/util/strings'
|
||||
export { renderMarkdown } from '@sourcegraph/common/src/util/markdown/markdown'
|
||||
export { renderMarkdown, type RenderMarkdownOptions } from '@sourcegraph/common/src/util/markdown/markdown'
|
||||
export { highlightNodeMultiline, highlightNode } from '@sourcegraph/common/src/util/highlightNode'
|
||||
export { logger } from '@sourcegraph/common/src/util/logger'
|
||||
export { isSafari } from '@sourcegraph/common/src/util/browserDetection'
|
||||
|
||||
@ -61,6 +61,11 @@ export const svelteKitRoutes: SvelteKitRoute[] = [
|
||||
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/tags/?$'),
|
||||
isRepoRoot: false,
|
||||
},
|
||||
{
|
||||
id: '/[community=communitySearchContext]',
|
||||
pattern: new RegExp('^/(backstage|chakraui|cncf|julia|kubernetes|o3de|stackstorm|stanford|temporal)/?$'),
|
||||
isRepoRoot: false,
|
||||
},
|
||||
{
|
||||
id: '/search',
|
||||
pattern: new RegExp('^/search/?$'),
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { decorateQuery } from '$lib/branded'
|
||||
import type { SearchPatternType } from '@sourcegraph/web/src/graphql-operations'
|
||||
|
||||
import { decorateQuery } from '$lib/branded'
|
||||
|
||||
import EmphasizedLabel from './EmphasizedLabel.svelte'
|
||||
|
||||
export let query: string
|
||||
export let patternType: SearchPatternType | undefined = undefined
|
||||
export let matches: Set<number> | null = null
|
||||
/**
|
||||
* If true the query will be wrapped between tokens as necessary
|
||||
*/
|
||||
export let wrap: boolean = false
|
||||
|
||||
$: decorations = decorateQuery(query, patternType)
|
||||
</script>
|
||||
|
||||
<code class="search-query-link">
|
||||
<code class="search-query-link" class:wrap>
|
||||
{#if decorations}
|
||||
{#each decorations as { key, className, value, token } (key)}
|
||||
<span class={className}>
|
||||
@ -27,5 +33,10 @@
|
||||
<style lang="scss">
|
||||
code {
|
||||
font-size: inherit;
|
||||
|
||||
&.wrap {
|
||||
white-space: initial;
|
||||
line-height: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
13
client/web-sveltekit/src/lib/search/communityPages.ts
Normal file
13
client/web-sveltekit/src/lib/search/communityPages.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const communities = [
|
||||
'backstage',
|
||||
'chakraui',
|
||||
'cncf',
|
||||
'julia',
|
||||
'kubernetes',
|
||||
'o3de',
|
||||
'stackstorm',
|
||||
'stanford',
|
||||
'temporal',
|
||||
] as const
|
||||
|
||||
export type Community = typeof communities[number]
|
||||
@ -1,13 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { renderMarkdown } from '$lib/common'
|
||||
import { renderMarkdown, type RenderMarkdownOptions } from '$lib/common'
|
||||
|
||||
import styles from './Markdown.module.scss'
|
||||
|
||||
export let content: string
|
||||
export let options: RenderMarkdownOptions | undefined = undefined
|
||||
export let inline = false
|
||||
|
||||
$: finalOptions = options ? { ...options, inline } : { inline }
|
||||
</script>
|
||||
|
||||
{#if content}
|
||||
<div class={styles.markdown}>
|
||||
{@html renderMarkdown(content)}
|
||||
</div>
|
||||
<svelte:element this={inline ? 'span' : 'div'} class={styles.markdown}>
|
||||
{@html renderMarkdown(content, finalOptions)}
|
||||
</svelte:element >
|
||||
{/if}
|
||||
|
||||
10
client/web-sveltekit/src/params/communitySearchContext.ts
Normal file
10
client/web-sveltekit/src/params/communitySearchContext.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { ParamMatcher } from '@sveltejs/kit'
|
||||
|
||||
import { browser } from '$app/environment'
|
||||
import { communities } from '$lib/search/communityPages'
|
||||
|
||||
const names: readonly string[] = communities
|
||||
|
||||
export const match: ParamMatcher = param => {
|
||||
return browser && window.context.sourcegraphDotComMode && names.includes(param.toLowerCase())
|
||||
}
|
||||
@ -0,0 +1,274 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { Community } from '$lib/search/communityPages'
|
||||
|
||||
// Unique number identifier for telemetry
|
||||
const specTypes: Record<string, number> = {
|
||||
backstage: 0,
|
||||
chakraui: 1,
|
||||
cncf: 2,
|
||||
temporal: 3,
|
||||
o3de: 4,
|
||||
stackstorm: 5,
|
||||
kubernetes: 6,
|
||||
stanford: 7,
|
||||
julia: 8,
|
||||
} satisfies Record<Community, number>
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
// @sg EnableRollout Dotcom
|
||||
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import { buildSearchURLQuery } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
|
||||
import CodeHostIcon from '$lib/search/CodeHostIcon.svelte'
|
||||
import SearchInput from '$lib/search/input/SearchInput.svelte'
|
||||
import { queryStateStore } from '$lib/search/state'
|
||||
import SyntaxHighlightedQuery from '$lib/search/SyntaxHighlightedQuery.svelte'
|
||||
import { displayRepoName } from '$lib/shared'
|
||||
import { settings } from '$lib/stores'
|
||||
import { TELEMETRY_RECORDER } from '$lib/telemetry'
|
||||
import { Alert, Button, Markdown } from '$lib/wildcard'
|
||||
|
||||
import type { PageData } from './$types'
|
||||
|
||||
export let data: PageData
|
||||
|
||||
$: ({ title, description, homepageIcon, spec, lowProfile, examples } = data)
|
||||
|
||||
$: context = `context:${spec}`
|
||||
$: queryState = queryStateStore({ query: context + ' ' }, $settings)
|
||||
|
||||
onMount(() => {
|
||||
TELEMETRY_RECORDER.recordEvent('communitySearchContext', 'view', {
|
||||
metadata: { spec: specTypes[data.community] },
|
||||
})
|
||||
})
|
||||
|
||||
function handleSearchExampleClick() {
|
||||
TELEMETRY_RECORDER.recordEvent('communitySearchContext.suggestion', 'click')
|
||||
}
|
||||
function handleRepoLinkClick() {
|
||||
TELEMETRY_RECORDER.recordEvent('communitySearchContext.repoLink', 'click')
|
||||
}
|
||||
function handleExternalRepoLinkClick() {
|
||||
TELEMETRY_RECORDER.recordEvent('communitySearchContext.repoLink.external', 'click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title} - Sourcegraph</title>
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<hgroup>
|
||||
<h2><img src={homepageIcon} alt="" aria-hidden /><span>{title}</span></h2>
|
||||
<!-- We provide our own p element so that we can target it with a CSS selector without :global -->
|
||||
<p>
|
||||
<Markdown content={description} inline />
|
||||
</p>
|
||||
</hgroup>
|
||||
|
||||
<SearchInput {queryState} autoFocus />
|
||||
|
||||
{#if !lowProfile}
|
||||
<div class="main">
|
||||
{#if examples.length > 0}
|
||||
<div class="column examples">
|
||||
<h3>Search examples</h3>
|
||||
<ul data-testid="page.community.examples">
|
||||
{#each examples as example}
|
||||
{@const query = `${context} ${example.query}`}
|
||||
<li>
|
||||
<h4><Markdown content={example.title} inline /></h4>
|
||||
<SyntaxHighlightedQuery wrap {query} patternType={example.patternType} />
|
||||
<Button variant="secondary">
|
||||
<a
|
||||
slot="custom"
|
||||
let:buttonClass
|
||||
class={buttonClass}
|
||||
href="/search?{buildSearchURLQuery(query, example.patternType, false)}"
|
||||
on:click={handleSearchExampleClick}>Search</a
|
||||
>
|
||||
</Button>
|
||||
{#if example.description}
|
||||
<div class="description">
|
||||
<Markdown content={example.description} />
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
<aside class="column repositories">
|
||||
<h3>Repositories</h3>
|
||||
<div class="content">
|
||||
<p>
|
||||
Using the context <SyntaxHighlightedQuery query={context} /> in a query will search these repositories:
|
||||
</p>
|
||||
|
||||
{#await data.repositories}
|
||||
<LoadingSpinner center />
|
||||
{:then repositories}
|
||||
<ul class="repositories" data-testid="page.community.repositories">
|
||||
{#each repositories as { repository: repo } (repo.name)}
|
||||
<li>
|
||||
{#if repo.externalURLs.length > 0}
|
||||
<a
|
||||
href={repo.externalURLs[0].url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
on:click={handleExternalRepoLinkClick}
|
||||
>
|
||||
<CodeHostIcon repository={repo.name} />
|
||||
</a>
|
||||
{/if}
|
||||
<a href="/{repo.name}" on:click={handleRepoLinkClick}>
|
||||
{displayRepoName(repo.name)}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:catch error}
|
||||
<Alert variant="danger">{error.message}</Alert>
|
||||
{/await}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
width: 100%;
|
||||
max-width: var(--viewport-lg);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
img {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
> * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
hgroup {
|
||||
margin-top: 3rem;
|
||||
|
||||
@media (--sm-breakpoint-down) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
> * {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
|
||||
--card-border-radius: var(--border-radius);
|
||||
|
||||
@media (--sm-breakpoint-down) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@media (--xs-breakpoint-down) {
|
||||
--card-border-radius: 0;
|
||||
padding: 0;
|
||||
|
||||
h3 {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.examples {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
ul {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-row-gap: 1rem;
|
||||
|
||||
li {
|
||||
display: grid;
|
||||
grid-column: span 2;
|
||||
grid-template-columns: subgrid;
|
||||
grid-template-rows: auto auto auto;
|
||||
align-items: center;
|
||||
row-gap: 0.5rem;
|
||||
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 1rem;
|
||||
border-radius: var(--card-border-radius);
|
||||
background-color: var(--color-bg-1);
|
||||
|
||||
> h4 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.description {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.repositories {
|
||||
flex: 1;
|
||||
// Forces column to grow so that examples and repositories are
|
||||
// the same width
|
||||
min-width: 0;
|
||||
|
||||
.content {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 1rem;
|
||||
border-radius: var(--card-border-radius);
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
|
||||
ul {
|
||||
columns: 2;
|
||||
|
||||
@media (--md-breakpoint-down) {
|
||||
columns: 1;
|
||||
}
|
||||
|
||||
li {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,27 @@
|
||||
import { error } from '@sveltejs/kit'
|
||||
|
||||
import { getGraphQLClient, mapOrThrow } from '$lib/graphql'
|
||||
|
||||
import type { PageLoad } from './$types'
|
||||
import { communityPageConfigs } from './config'
|
||||
import { CommunitySearchPage_SearchContext } from './page.gql'
|
||||
|
||||
export const load: PageLoad = ({ params }) => {
|
||||
const community = params.community.toLowerCase()
|
||||
const config = communityPageConfigs[community]
|
||||
if (!config) {
|
||||
// This should never happen because the router won't even load this page if the
|
||||
// parameter is not a known community name.
|
||||
error(404, `Community search context not found: ${params.community}`)
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
community,
|
||||
repositories: getGraphQLClient()
|
||||
.query(CommunitySearchPage_SearchContext, {
|
||||
spec: config.spec,
|
||||
})
|
||||
.then(mapOrThrow(({ data }) => data?.searchContextBySpec?.repositories ?? [])),
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
import { SearchPatternType } from '$lib/graphql-types'
|
||||
import type { Community } from '$lib/search/communityPages'
|
||||
|
||||
// This needs to be exported so that TS type inference can work in SvelteKit generated files.
|
||||
export interface ExampleQuery {
|
||||
title: string
|
||||
description?: string
|
||||
query: string
|
||||
patternType: SearchPatternType
|
||||
}
|
||||
|
||||
// This needs to be exported so that TS type inference can work in SvelteKit generated files.
|
||||
export interface CommunitySearchContextMetadata {
|
||||
/**
|
||||
* The title of the community search context. This is displayed on the search homepage, and is typically prose. E.g. Refactor python 2 to 3.
|
||||
*/
|
||||
title: string
|
||||
/**
|
||||
* The name of the community search context, must match the community search context name as configured in settings. E.g. python2-to-3.
|
||||
*/
|
||||
spec: string
|
||||
/**
|
||||
* A list of example queries using the community search context. Don't include the `context:name` portion of the query. It will be automatically added.
|
||||
*/
|
||||
examples: ExampleQuery[]
|
||||
/**
|
||||
* A description of the community search context to be displayed on the page.
|
||||
*/
|
||||
description: string
|
||||
/**
|
||||
* Base64 data uri to an icon.
|
||||
*/
|
||||
homepageIcon: string
|
||||
/**
|
||||
* A description when displayed on the search homepage.
|
||||
*/
|
||||
homepageDescription: string
|
||||
/**
|
||||
* Whether to display this in a minimal community search context page, without examples/repositories/descriptions below the search bar.
|
||||
*/
|
||||
lowProfile?: boolean
|
||||
}
|
||||
|
||||
export const communityPageConfigs: Record<string, CommunitySearchContextMetadata> = {
|
||||
backstage: {
|
||||
title: 'Backstage',
|
||||
spec: 'backstage',
|
||||
description: 'Explore over 25 different Backstage related repositories. Search with examples below.',
|
||||
examples: [
|
||||
{
|
||||
title: 'Browse diffs for recent code changes',
|
||||
query: 'type:diff after:"1 week ago"',
|
||||
patternType: SearchPatternType.standard,
|
||||
},
|
||||
],
|
||||
homepageDescription: 'Search within the Backstage community.',
|
||||
homepageIcon: 'https://raw.githubusercontent.com/sourcegraph-community/backstage-context/main/backstage.svg',
|
||||
},
|
||||
julia: {
|
||||
title: 'Julia',
|
||||
spec: 'julia',
|
||||
description: 'Explore over 20 different Julia repositories. Search with examples below.',
|
||||
examples: [
|
||||
{
|
||||
title: "List all TODO's in Julia code",
|
||||
query: 'lang:Julia TODO case:yes',
|
||||
patternType: SearchPatternType.standard,
|
||||
},
|
||||
{
|
||||
title: 'Browse diffs for recent code changes',
|
||||
query: 'type:diff after:"1 week ago"',
|
||||
patternType: SearchPatternType.standard,
|
||||
},
|
||||
],
|
||||
homepageDescription: 'Search within the Julia community.',
|
||||
homepageIcon: 'https://raw.githubusercontent.com/JuliaLang/julia/master/doc/src/assets/logo.svg',
|
||||
},
|
||||
kubernetes: {
|
||||
title: 'Kubernetes',
|
||||
spec: 'kubernetes',
|
||||
description: 'Explore Kubernetes repositories on GitHub. Search with examples below.',
|
||||
examples: [
|
||||
{
|
||||
title: 'Use a ReplicationController configuration to ensure specified number of pod replicas are running at any one time',
|
||||
query: 'file:pod.yaml content:"kind: ReplicationController"',
|
||||
patternType: SearchPatternType.standard,
|
||||
},
|
||||
{
|
||||
title: 'Look for outdated `apiVersions` of admission webhooks',
|
||||
description: `This apiVersion has been deprecated in favor of "admissionregistration.k8s.io/v1".
|
||||
You can read more about this at https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/`,
|
||||
query: 'content:"apiVersion: admissionregistration.k8s.io/v1beta1"',
|
||||
patternType: SearchPatternType.standard,
|
||||
},
|
||||
{
|
||||
title: 'Find Prometheus usage in YAML files',
|
||||
query: 'lang:yaml prom/prometheus',
|
||||
patternType: SearchPatternType.standard,
|
||||
},
|
||||
{
|
||||
title: 'Search for examples of the sidecar pattern in Go',
|
||||
query: 'lang:go sidecar',
|
||||
patternType: SearchPatternType.standard,
|
||||
},
|
||||
{
|
||||
title: 'Browse diffs for recent code changes',
|
||||
query: 'type:diff after:"1 week ago"',
|
||||
patternType: SearchPatternType.standard,
|
||||
},
|
||||
],
|
||||
homepageDescription: 'Search within the Kubernetes community.',
|
||||
homepageIcon: 'https://code.benco.io/icon-collection/logos/kubernetes.svg',
|
||||
},
|
||||
stackstorm: {
|
||||
title: 'StackStorm',
|
||||
spec: 'stackstorm',
|
||||
description: '',
|
||||
examples: [
|
||||
{
|
||||
title: 'Passive sensor examples',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'from st2reactor.sensor.base import Sensor',
|
||||
},
|
||||
{
|
||||
title: 'Polling sensor examples',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'from st2reactor.sensor.base import PollingSensor',
|
||||
},
|
||||
{
|
||||
title: 'Trigger examples in rules',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'repo:Exchange trigger: file:.yaml$',
|
||||
},
|
||||
{
|
||||
title: 'Actions that use the Orquesta runner',
|
||||
patternType: SearchPatternType.regexp,
|
||||
query: 'repo:Exchange runner_type:\\s*"orquesta"',
|
||||
},
|
||||
],
|
||||
homepageDescription: 'Search within the StackStorm and StackStorm Exchange community.',
|
||||
homepageIcon: 'https://avatars.githubusercontent.com/u/4969009?s=200&v=4',
|
||||
},
|
||||
stanford: {
|
||||
title: 'Stanford University',
|
||||
spec: 'stanford',
|
||||
description: 'Explore open-source code from Stanford students, faculty, research groups, and clubs.',
|
||||
examples: [
|
||||
{
|
||||
title: 'Find all mentions of "machine learning" in Stanford projects.',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'machine learning',
|
||||
},
|
||||
{
|
||||
title: 'Explore the code of specific research groups like Hazy Research, a group that investigates machine learning models and automated training set creation.',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'repo:/HazyResearch/',
|
||||
},
|
||||
{
|
||||
title: 'Explore the code of a specific user or organization such as Stanford University School of Medicine.',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'repo:/susom/',
|
||||
},
|
||||
{
|
||||
title: 'Search for repositories related to introductory programming concepts.',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'repo:recursion',
|
||||
},
|
||||
{
|
||||
title: 'Explore the README files of thousands of projects.',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'file:README.txt',
|
||||
},
|
||||
{
|
||||
title: 'Find old-style string formatted print statements.',
|
||||
patternType: SearchPatternType.structural,
|
||||
query: 'lang:python print(:[args] % :[v])',
|
||||
},
|
||||
],
|
||||
homepageDescription: 'Explore Stanford open-source code.',
|
||||
homepageIcon:
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Icons8_flat_graduation_cap.svg/120px-Icons8_flat_graduation_cap.svg.png',
|
||||
},
|
||||
temporal: {
|
||||
title: 'Temporal',
|
||||
spec: 'temporalio',
|
||||
description: '',
|
||||
examples: [
|
||||
{
|
||||
title: 'All test functions',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'type:symbol Test',
|
||||
},
|
||||
{
|
||||
title: 'Search for a specifc function or class',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'type:symbol SimpleSslContextBuilder',
|
||||
},
|
||||
],
|
||||
homepageDescription: 'Search within the Temporal organization.',
|
||||
homepageIcon: 'https://avatars.githubusercontent.com/u/56493103?s=200&v=4',
|
||||
},
|
||||
chakraui: {
|
||||
title: 'CHAKRA UI',
|
||||
spec: 'chakraui',
|
||||
description: '',
|
||||
examples: [
|
||||
{
|
||||
title: 'Search for Chakra UI packages',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'file:package.json',
|
||||
},
|
||||
{
|
||||
title: 'Browse diffs for recent code changes',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'type:diff after:"1 week ago"',
|
||||
},
|
||||
],
|
||||
homepageDescription: 'Search within the Chakra UI organization.',
|
||||
homepageIcon: 'https://raw.githubusercontent.com/chakra-ui/chakra-ui/main/logo/logomark-colored.svg',
|
||||
},
|
||||
cncf: {
|
||||
title: 'Cloud Native Computing Foundation (CNCF)',
|
||||
spec: 'cncf',
|
||||
description: 'Search the [CNCF projects](https://landscape.cncf.io/project=hosted)',
|
||||
examples: [],
|
||||
homepageDescription: 'Search CNCF projects.',
|
||||
homepageIcon: 'https://raw.githubusercontent.com/cncf/artwork/master/other/cncf/icon/color/cncf-icon-color.png',
|
||||
lowProfile: true,
|
||||
},
|
||||
o3de: {
|
||||
title: 'O3DE',
|
||||
spec: 'o3de',
|
||||
description: '',
|
||||
examples: [
|
||||
{
|
||||
title: 'Search for O3DE gems',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'file:gem.json',
|
||||
},
|
||||
{
|
||||
title: 'Browse diffs for recent code changes',
|
||||
patternType: SearchPatternType.standard,
|
||||
query: 'type:diff after:"1 week ago"',
|
||||
},
|
||||
],
|
||||
homepageDescription: 'Search within the O3DE organization.',
|
||||
homepageIcon:
|
||||
'https://raw.githubusercontent.com/o3de/artwork/19b89e72e15824f20204a8977a007f53d5fcd5b8/o3de/03_O3DE%20Application%20Icon/SVG/O3DE-Circle-Icon.svg',
|
||||
},
|
||||
} satisfies Record<Community, CommunitySearchContextMetadata>
|
||||
@ -0,0 +1,12 @@
|
||||
query CommunitySearchPage_SearchContext($spec: String!) {
|
||||
searchContextBySpec(spec: $spec) {
|
||||
repositories {
|
||||
repository {
|
||||
name
|
||||
externalURLs {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import { test, expect } from '$testing/integration'
|
||||
|
||||
import { communityPageConfigs } from './config'
|
||||
|
||||
test.describe('community', async () => {
|
||||
test.beforeEach(async ({ sg }) => {
|
||||
await sg.dotcomMode()
|
||||
sg.mockOperations({
|
||||
CommunitySearchPage_SearchContext() {
|
||||
return {
|
||||
searchContextBySpec: {
|
||||
repositories: [
|
||||
{ repository: { name: 'repo1' } },
|
||||
{ repository: { name: 'repo2' } },
|
||||
{ repository: { name: 'repo3' } },
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
for (const [name, config] of Object.entries(communityPageConfigs)) {
|
||||
test(name, async ({ page }) => {
|
||||
await page.goto(`/${name}`)
|
||||
|
||||
await test.step('correct heading is shown', async () => {
|
||||
await expect(page.getByRole('heading', { name: config.title, level: 2 })).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('search input is prefilled with search context', async () => {
|
||||
await expect(page.getByRole('textbox')).toHaveText(new RegExp(`^context:${config.spec} `))
|
||||
})
|
||||
|
||||
// We are not testing description because it may contain markdown which
|
||||
// is more difficult to test for
|
||||
|
||||
if (config.lowProfile) {
|
||||
await test.step('low profile mode', async () => {
|
||||
await expect(page.getByTestId('page.community.examples')).not.toBeVisible()
|
||||
await expect(page.getByTestId('page.community.repositories')).not.toBeVisible()
|
||||
})
|
||||
} else {
|
||||
if (config.examples.length > 0) {
|
||||
await test.step('search examples are listed', async () => {
|
||||
await expect(page.getByTestId('page.community.examples').getByRole('listitem')).toHaveCount(
|
||||
config.examples.length
|
||||
)
|
||||
})
|
||||
} else {
|
||||
await expect(page.getByRole('heading', { name: 'Search examples' })).not.toBeVisible()
|
||||
}
|
||||
|
||||
await test.step('repositories are listed', async () => {
|
||||
// Repositories are rendered
|
||||
await expect(page.getByTestId('page.community.repositories').getByRole('listitem')).toHaveCount(3)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('show repository resolution error', async ({ page, sg }) => {
|
||||
await page.goto('/backstage')
|
||||
|
||||
sg.mockOperations({
|
||||
CommunitySearchPage_SearchContext() {
|
||||
throw new Error('Test error')
|
||||
},
|
||||
})
|
||||
|
||||
await expect(page.getByText('Test error')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@ -107,7 +107,7 @@ export class Sourcegraph {
|
||||
})
|
||||
|
||||
// Intercept any asset calls and replace them with static files
|
||||
await this.page.route(/.assets|_app/, route => {
|
||||
await this.page.route(/\.assets|_app/, route => {
|
||||
const assetPath = new URL(route.request().url()).pathname.replace('/.assets/', '')
|
||||
const asset = joinDistinct(ASSETS_DIR, assetPath)
|
||||
const contentType = mime.contentType(path.basename(asset)) || undefined
|
||||
|
||||
@ -61,6 +61,11 @@ export const svelteKitRoutes: SvelteKitRoute[] = [
|
||||
pattern: new RegExp('^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/tags/?$'),
|
||||
isRepoRoot: false,
|
||||
},
|
||||
{
|
||||
id: '/[community=communitySearchContext]',
|
||||
pattern: new RegExp('^/(backstage|chakraui|cncf|julia|kubernetes|o3de|stackstorm|stanford|temporal)/?$'),
|
||||
isRepoRoot: false,
|
||||
},
|
||||
{
|
||||
id: '/search',
|
||||
pattern: new RegExp('^/search/?$'),
|
||||
|
||||
@ -11,6 +11,7 @@ go_library(
|
||||
visibility = ["//cmd/frontend:__subpackages__"],
|
||||
deps = [
|
||||
"//cmd/frontend/internal/app/ui/sveltekit/tags",
|
||||
"//internal/dotcom",
|
||||
"//internal/env",
|
||||
"//internal/featureflag",
|
||||
"//lib/errors",
|
||||
|
||||
@ -183,6 +183,7 @@ var (
|
||||
tagPattern = regexp.MustCompile(`^\s*//\s+@sg\s+`)
|
||||
groupPattern = regexp.MustCompile(`^\([^)]+\)$`)
|
||||
restParamPattern = regexp.MustCompile(`^\[\.\.\.(\w+)(?:=(\w+))?\]$`)
|
||||
paramPattern = regexp.MustCompile(`^\[(\w+)(?:=(\w+))?\]$`)
|
||||
optionalParamPattern = regexp.MustCompile(`^\[\[(\w+)(?:=(\w+))?\]\]$`)
|
||||
)
|
||||
|
||||
@ -226,7 +227,8 @@ func getRouteInfo(path string) (*routeInfo, error) {
|
||||
// Map SvelteKit specific parameter matchers to regular expressions. This is a "best effort" approach
|
||||
// because parameter matchers in SvelteKit are functions that can perform arbitrary logic.
|
||||
var paramMatchers = map[string]string{
|
||||
"reporev": "/" + routevar.RepoPatternNonCapturing + `(?:@` + routevar.RevPatternNonCapturing + `)?`,
|
||||
"reporev": "/" + routevar.RepoPatternNonCapturing + `(?:@` + routevar.RevPatternNonCapturing + `)?`,
|
||||
"communitySearchContext": "/(backstage|chakraui|cncf|julia|kubernetes|o3de|stackstorm|stanford|temporal)",
|
||||
}
|
||||
|
||||
// This code follows the regex generation logic from
|
||||
@ -257,6 +259,19 @@ func patternForRouteId(routeId string) (string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// [param]
|
||||
if paramPattern.MatchString(segment) {
|
||||
matches := paramPattern.FindStringSubmatch(segment)
|
||||
if len(matches) == 3 {
|
||||
if matcher, ok := paramMatchers[matches[2]]; ok {
|
||||
b.WriteString(matcher)
|
||||
continue
|
||||
}
|
||||
}
|
||||
b.WriteString(`(?:/[^/]+)`)
|
||||
continue
|
||||
}
|
||||
|
||||
// [[optional]]
|
||||
if optionalParamPattern.MatchString(segment) {
|
||||
b.WriteString(`(?:/[^/]+)?`)
|
||||
|
||||
@ -68,6 +68,11 @@ var svelteKitRoutes = []svelteKitRoute{
|
||||
Pattern: regexp.MustCompile("^/(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,})))(?:@(?:(?:(?:[^@/-]|(?:[^/@]{2,}))/)*(?:[^@/-]|(?:[^/@]{2,}))))?/-/tags/?$"),
|
||||
Tag: tags.EnableOptIn | tags.EnableRollout,
|
||||
},
|
||||
{
|
||||
Id: "/[community=communitySearchContext]",
|
||||
Pattern: regexp.MustCompile("^/(backstage|chakraui|cncf|julia|kubernetes|o3de|stackstorm|stanford|temporal)/?$"),
|
||||
Tag: tags.EnableOptIn | tags.EnableRollout | tags.Dotcom,
|
||||
},
|
||||
{
|
||||
Id: "/search",
|
||||
Pattern: regexp.MustCompile("^/search/?$"),
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/internal/app/ui/sveltekit/tags"
|
||||
"github.com/sourcegraph/sourcegraph/internal/dotcom"
|
||||
"github.com/sourcegraph/sourcegraph/internal/env"
|
||||
"github.com/sourcegraph/sourcegraph/internal/featureflag"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
@ -55,6 +56,7 @@ func (r *svelteKitRoute) matches(url *url.URL) bool {
|
||||
// RegisterSvelteKit registers a middleware that determines which routes are enabled for SvelteKit.
|
||||
// It also extends the request context with information that is sent to the client apps via JSContext.
|
||||
func RegisterSvelteKit(r *mux.Router, repoRootRoute *mux.Route) {
|
||||
isDotComMode := dotcom.SourcegraphDotComMode()
|
||||
var knownRoutes []string
|
||||
|
||||
r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
||||
@ -84,16 +86,21 @@ func RegisterSvelteKit(r *mux.Router, repoRootRoute *mux.Route) {
|
||||
enabledRoutes := make([]int, 0, len(svelteKitRoutes))
|
||||
enabled := false
|
||||
|
||||
availabilityMask := tags.EnableAlways
|
||||
inclusionMask := tags.EnableAlways
|
||||
if ff.GetBoolOr("web-next", false) {
|
||||
availabilityMask |= tags.EnableOptIn
|
||||
inclusionMask |= tags.EnableOptIn
|
||||
}
|
||||
if ff.GetBoolOr("web-next-rollout", false) {
|
||||
availabilityMask |= tags.EnableRollout
|
||||
inclusionMask |= tags.EnableRollout
|
||||
}
|
||||
|
||||
var exclusionMask tags.Tag
|
||||
if !isDotComMode {
|
||||
exclusionMask |= tags.Dotcom
|
||||
}
|
||||
|
||||
for i, skr := range svelteKitRoutes {
|
||||
if skr.Tag&availabilityMask != 0 {
|
||||
if skr.Tag&inclusionMask != 0 && skr.Tag&exclusionMask == 0 {
|
||||
enabledRoutes = append(enabledRoutes, i)
|
||||
|
||||
if !enabled {
|
||||
|
||||
@ -10,18 +10,20 @@ const (
|
||||
EnableRollout
|
||||
// EnableOptin renders the SvelteKit app for this route when the "web-next" feature flag is enabled.
|
||||
EnableOptIn
|
||||
// Dotcom marks a route as being only available on Sourcegraph.com
|
||||
Dotcom
|
||||
)
|
||||
|
||||
// IsTagValid returns true if the tag is a valid tag for a route
|
||||
// this is used by the code generator to validate the tags
|
||||
func IsTagValid(tag string) bool {
|
||||
switch tag {
|
||||
case "RepoRoot", "EnableAlways", "EnableRollout", "EnableOptIn":
|
||||
case "RepoRoot", "EnableAlways", "EnableRollout", "EnableOptIn", "Dotcom":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AvailableTags() []string {
|
||||
return []string{"RepoRoot", "EnableAlways", "EnableRollout", "EnableOptIn"}
|
||||
return []string{"RepoRoot", "EnableAlways", "EnableRollout", "EnableOptIn", "Dotcom"}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user