mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
remove enableLegacyExtensions and all now-unreachable code (#47517)
This is part of the extension API deprecation (https://docs.google.com/document/d/10vtoe-kpNvVZ8Etrx34bSCoTaCCHxX8o3ncCmuErPZo/edit). There are a lot more refactors to do after this, but this removes the admin-facing `enableLegacyExtensions` setting and some of the root-level instantiation and usage of the extensions controller API. ## Test plan Check that hovers still work on dotcom.
This commit is contained in:
parent
7e395819c4
commit
e99c4145f7
@ -100,6 +100,7 @@ All notable changes to Sourcegraph are documented in this file.
|
||||
|
||||
- The Code insights "run over all repositories" mode has been replaced with search-powered repositories filed syntax. [#45687](https://github.com/sourcegraph/sourcegraph/pull/45687)
|
||||
- The settings `search.repositoryGroups`, `codeInsightsGqlApi`, `codeInsightsAllRepos`, `experimentalFeatures.copyQueryButton`,, `experimentalFeatures.showRepogroupHomepage`, `experimentalFeatures.showOnboardingTour`, `experimentalFeatures.showSearchContextManagement` and `codeIntelligence.autoIndexRepositoryGroups` have been removed as they were deprecated and unsued. [#47481](https://github.com/sourcegraph/sourcegraph/pull/47481)
|
||||
- The site config `enableLegacyExtensions` setting was removed. It is no longer possible to enable legacy Sourcegraph extension API functionality in this version.
|
||||
|
||||
## 4.4.2
|
||||
|
||||
|
||||
@ -102,11 +102,6 @@ describe('GitHub', () => {
|
||||
siteAdmin: false,
|
||||
},
|
||||
}),
|
||||
EnableLegacyExtensions: () => ({
|
||||
site: {
|
||||
enableLegacyExtensions: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
// Ensure that the same assets are requested in all environments.
|
||||
@ -155,10 +150,7 @@ describe('GitHub', () => {
|
||||
// it('shows hover tooltips when hovering a token and respects "Enable single click to go to definition" setting', async () => {
|
||||
// mockUrls(['https://github.com/*path/find-definition'])
|
||||
|
||||
// const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking({
|
||||
// pollyServer: testContext.server,
|
||||
// sourcegraphBaseUrl: driver.sourcegraphBaseUrl,
|
||||
// })
|
||||
// const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking()
|
||||
|
||||
// const userSettings: Settings = {
|
||||
// extensions: extensionSettings,
|
||||
@ -316,16 +308,14 @@ describe('GitHub', () => {
|
||||
// })
|
||||
|
||||
describe('Pull request pages', () => {
|
||||
describe('Files Changed view', () => {
|
||||
// TODO(sqs): skipped because these have not been reimplemented after the extension API deprecation
|
||||
describe.skip('Files Changed view', () => {
|
||||
// For each pull request test, set up a mock extension that verifies that the correct
|
||||
// file and revision info reach extensions.
|
||||
beforeEach(() => {
|
||||
mockUrls(['https://github.com/*path/find-definition'])
|
||||
|
||||
const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking({
|
||||
pollyServer: testContext.server,
|
||||
sourcegraphBaseUrl: driver.sourcegraphBaseUrl,
|
||||
})
|
||||
const { mockExtension, extensionSettings } = setupExtensionMocking()
|
||||
|
||||
const userSettings: Settings = {
|
||||
extensions: extensionSettings,
|
||||
@ -350,7 +340,6 @@ describe('GitHub', () => {
|
||||
merged: { contents: JSON.stringify(userSettings), messages: [] },
|
||||
},
|
||||
}),
|
||||
Extensions,
|
||||
ResolveRev: ({ revision }) => ({
|
||||
repository: {
|
||||
mirrorInfo: { cloned: true },
|
||||
@ -639,7 +628,8 @@ describe('GitHub', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Commit view', () => {
|
||||
// TODO(sqs): skipped because these have not been reimplemented after the extension API deprecation
|
||||
describe.skip('Commit view', () => {
|
||||
beforeEach(() => {
|
||||
mockUrls([
|
||||
'https://github.com/*path/find-definition',
|
||||
@ -647,10 +637,7 @@ describe('GitHub', () => {
|
||||
'https://github.com/commits/badges',
|
||||
])
|
||||
|
||||
const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking({
|
||||
pollyServer: testContext.server,
|
||||
sourcegraphBaseUrl: driver.sourcegraphBaseUrl,
|
||||
})
|
||||
const { mockExtension, extensionSettings } = setupExtensionMocking()
|
||||
|
||||
const userSettings: Settings = {
|
||||
extensions: extensionSettings,
|
||||
@ -691,7 +678,6 @@ describe('GitHub', () => {
|
||||
},
|
||||
},
|
||||
}),
|
||||
Extensions,
|
||||
})
|
||||
|
||||
// Serve a mock extension with a simple hover provider
|
||||
|
||||
@ -91,11 +91,6 @@ describe('GitLab', () => {
|
||||
hasCodeIntelligence: true,
|
||||
},
|
||||
}),
|
||||
EnableLegacyExtensions: () => ({
|
||||
site: {
|
||||
enableLegacyExtensions: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
// Ensure that the same assets are requested in all environments.
|
||||
@ -146,11 +141,9 @@ describe('GitLab', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('shows hover tooltips when hovering a token', async () => {
|
||||
const { mockExtension, Extensions, extensionSettings } = setupExtensionMocking({
|
||||
pollyServer: testContext.server,
|
||||
sourcegraphBaseUrl: driver.sourcegraphBaseUrl,
|
||||
})
|
||||
// TODO(sqs): skipped because these have not been reimplemented after the extension API deprecation
|
||||
it.skip('shows hover tooltips when hovering a token', async () => {
|
||||
const { mockExtension, extensionSettings } = setupExtensionMocking()
|
||||
|
||||
const userSettings: Settings = {
|
||||
extensions: extensionSettings,
|
||||
@ -175,7 +168,6 @@ describe('GitLab', () => {
|
||||
merged: { contents: JSON.stringify(userSettings), messages: [] },
|
||||
},
|
||||
}),
|
||||
Extensions,
|
||||
})
|
||||
|
||||
// Serve a mock extension with a simple hover provider
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { combineLatest, ReplaySubject, of } from 'rxjs'
|
||||
import { map, switchMap } from 'rxjs/operators'
|
||||
import { combineLatest, ReplaySubject } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
import { asError } from '@sourcegraph/common'
|
||||
import { isHTTPAuthError } from '@sourcegraph/http-client'
|
||||
@ -8,13 +8,11 @@ import { mutateSettings, updateSettings } from '@sourcegraph/shared/src/settings
|
||||
import { EMPTY_SETTINGS_CASCADE, gqlToCascade, SettingsSubject } from '@sourcegraph/shared/src/settings/settings'
|
||||
import { toPrettyBlobURL } from '@sourcegraph/shared/src/util/url'
|
||||
|
||||
import { background } from '../../browser-extension/web-extension-api/runtime'
|
||||
import { createGraphQLHelpers } from '../backend/requestGraphQl'
|
||||
import { CodeHost } from '../code-hosts/shared/codeHost'
|
||||
import { isInPage } from '../context'
|
||||
|
||||
import { createExtensionHost } from './extensionHost'
|
||||
import { getInlineExtensions, shouldUseInlineExtensions } from './inlineExtensionsService'
|
||||
import { getInlineExtensions } from './inlineExtensionsService'
|
||||
import { editClientSettings, fetchViewerSettings, mergeCascades, storageSettingsCascade } from './settings'
|
||||
|
||||
export interface SourcegraphIntegrationURLs {
|
||||
@ -57,8 +55,6 @@ export function createPlatformContext(
|
||||
}>(1)
|
||||
const { requestGraphQL, getBrowserGraphQLClient } = createGraphQLHelpers(sourcegraphURL, isExtension)
|
||||
|
||||
const shouldUseInlineExtensionsObservable = shouldUseInlineExtensions(requestGraphQL)
|
||||
|
||||
const context: BrowserPlatformContext = {
|
||||
/**
|
||||
* The active settings cascade.
|
||||
@ -120,31 +116,6 @@ export function createPlatformContext(
|
||||
requestGraphQL,
|
||||
getGraphQLClient: getBrowserGraphQLClient,
|
||||
createExtensionHost: () => createExtensionHost({ assetsURL }),
|
||||
getScriptURLForExtension: () => {
|
||||
if (isInPage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return bundleURLs =>
|
||||
shouldUseInlineExtensionsObservable.toPromise().then(shouldUseInlineExtensions => {
|
||||
if (shouldUseInlineExtensions) {
|
||||
// inline extensions have fixed scriptURLs
|
||||
return bundleURLs
|
||||
}
|
||||
|
||||
// We need to import the extension's JavaScript file (in importScripts in the Web Worker) from a blob:
|
||||
// URI, not its original http:/https: URL, because Chrome extensions are not allowed to be published
|
||||
// with a CSP that allowlists https://* in script-src (see
|
||||
// https://developer.chrome.com/extensions/contentSecurityPolicy#relaxing-remote-script). (Firefox
|
||||
// add-ons have an even stricter restriction.)
|
||||
return Promise.allSettled(bundleURLs.map(bundleURL => background.createBlobURL(bundleURL))).then(
|
||||
results =>
|
||||
results.map(result =>
|
||||
result.status === 'rejected' ? asError(result.reason) : result.value
|
||||
)
|
||||
)
|
||||
})
|
||||
},
|
||||
urlToFile: ({ rawRepoName, ...target }, context) => {
|
||||
// We don't always resolve the rawRepoName, e.g. if there are multiple definitions.
|
||||
// Construct URL to file on code host, if possible.
|
||||
@ -156,10 +127,7 @@ export function createPlatformContext(
|
||||
},
|
||||
sourcegraphURL,
|
||||
clientApplication: 'other',
|
||||
getStaticExtensions: () =>
|
||||
shouldUseInlineExtensionsObservable.pipe(
|
||||
switchMap(shouldUseInline => (shouldUseInline ? getInlineExtensions() : of(undefined)))
|
||||
),
|
||||
getStaticExtensions: () => getInlineExtensions(),
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
@ -1,60 +1,10 @@
|
||||
import { Observable, from } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
import { checkOk, isErrorGraphQLResult, gql } from '@sourcegraph/http-client'
|
||||
import { checkOk } from '@sourcegraph/http-client'
|
||||
import { ExecutableExtension } from '@sourcegraph/shared/src/api/extension/activation'
|
||||
import { ExtensionManifest } from '@sourcegraph/shared/src/extensions/extensionManifest'
|
||||
import { PlatformContext } from '@sourcegraph/shared/src/platform/context'
|
||||
|
||||
import extensions from '../../../code-intel-extensions.json'
|
||||
import { EnableLegacyExtensionsResult } from '../../graphql-operations'
|
||||
|
||||
const DEFAULT_ENABLE_LEGACY_EXTENSIONS = false
|
||||
|
||||
/**
|
||||
* Determine which extensions should be loaded:
|
||||
* - inline (bundled with the browser extension)
|
||||
* - or from the extensions registry (if `enableLegacyExtensions` experimental feature value is set to `true`).
|
||||
*/
|
||||
export const shouldUseInlineExtensions = (requestGraphQL: PlatformContext['requestGraphQL']): Observable<boolean> =>
|
||||
requestGraphQL<EnableLegacyExtensionsResult>({
|
||||
request: gql`
|
||||
query EnableLegacyExtensions {
|
||||
site {
|
||||
enableLegacyExtensions
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {},
|
||||
mightContainPrivateInfo: false,
|
||||
}).pipe(
|
||||
map(result => {
|
||||
if (isErrorGraphQLResult(result)) {
|
||||
// EnableLegacyExtensions query resolver may not be implemented on older versions.
|
||||
// Return `true` by default.
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const enableLegacyExtensions = result.data.site.enableLegacyExtensions
|
||||
return typeof enableLegacyExtensions === 'undefined'
|
||||
? DEFAULT_ENABLE_LEGACY_EXTENSIONS
|
||||
: enableLegacyExtensions
|
||||
} catch {
|
||||
return DEFAULT_ENABLE_LEGACY_EXTENSIONS
|
||||
}
|
||||
}),
|
||||
map(enableLegacyExtensions => {
|
||||
// TODO: The Phabricator native extension is currently the only runtime that runs the
|
||||
// browser extension code but does not use bundled extensions yet. We will fix this
|
||||
// when we update the browser extensions to use the new code intel APIs (#42104).
|
||||
if (window.SOURCEGRAPH_PHABRICATOR_EXTENSION === true) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !enableLegacyExtensions
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Get the manifest URL and script URL for a Sourcegraph extension which is inline (bundled with the browser add-on).
|
||||
|
||||
@ -44,13 +44,7 @@ export async function createExtensionHostClientConnection(
|
||||
initData: Omit<InitData, 'initialSettings'>,
|
||||
platformContext: Pick<
|
||||
PlatformContext,
|
||||
| 'settings'
|
||||
| 'updateSettings'
|
||||
| 'getGraphQLClient'
|
||||
| 'requestGraphQL'
|
||||
| 'telemetryService'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'clientApplication'
|
||||
'settings' | 'updateSettings' | 'getGraphQLClient' | 'requestGraphQL' | 'telemetryService' | 'clientApplication'
|
||||
>
|
||||
): Promise<{
|
||||
subscription: Unsubscribable
|
||||
|
||||
@ -1,90 +0,0 @@
|
||||
import { isEqual, once } from 'lodash'
|
||||
import { combineLatest, from, Observable, throwError } from 'rxjs'
|
||||
import { catchError, distinctUntilChanged, map, publishReplay, refCount, shareReplay, switchMap } from 'rxjs/operators'
|
||||
|
||||
import { asError } from '@sourcegraph/common'
|
||||
|
||||
import { ConfiguredExtension, extensionIDsFromSettings, isExtensionEnabled } from '../../extensions/extension'
|
||||
import { areExtensionsSame } from '../../extensions/extensions'
|
||||
import { queryConfiguredRegistryExtensions } from '../../extensions/helpers'
|
||||
import { PlatformContext } from '../../platform/context'
|
||||
import { isSettingsValid } from '../../settings/settings'
|
||||
|
||||
/**
|
||||
* @returns An observable that emits the list of extensions configured in the viewer's final settings upon
|
||||
* subscription and each time it changes.
|
||||
*/
|
||||
function viewerConfiguredExtensions({
|
||||
settings,
|
||||
getGraphQLClient,
|
||||
}: Pick<PlatformContext, 'settings' | 'getGraphQLClient'>): Observable<ConfiguredExtension[]> {
|
||||
return from(settings).pipe(
|
||||
map(settings => extensionIDsFromSettings(settings)),
|
||||
distinctUntilChanged((a, b) => isEqual(a, b)),
|
||||
switchMap(extensionIDs => queryConfiguredRegistryExtensions({ getGraphQLClient }, extensionIDs)),
|
||||
catchError(error => throwError(asError(error))),
|
||||
// TODO: Restore reference counter after refactoring contributions service
|
||||
// to not unsubscribe from existing entries when new entries are registered,
|
||||
// in order to ensure that the source is unsubscribed from.
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* List of extensions migrated to the core workflow.
|
||||
*/
|
||||
export const MIGRATED_TO_CORE_WORKFLOW_EXTENSION_IDS = new Set([
|
||||
'sourcegraph/git-extras',
|
||||
'sourcegraph/search-export',
|
||||
'sourcegraph/open-in-editor',
|
||||
'sourcegraph/open-in-vscode',
|
||||
'dymka/open-in-webstorm',
|
||||
'sourcegraph/open-in-atom',
|
||||
])
|
||||
|
||||
/**
|
||||
* Returns an Observable of extensions enabled for the user.
|
||||
* Wrapped with the `once` function from lodash.
|
||||
*/
|
||||
export const getEnabledExtensions = once(
|
||||
(
|
||||
context: Pick<
|
||||
PlatformContext,
|
||||
'settings' | 'getGraphQLClient' | 'getScriptURLForExtension' | 'clientApplication'
|
||||
>
|
||||
): Observable<ConfiguredExtension[]> =>
|
||||
combineLatest([viewerConfiguredExtensions(context), context.settings]).pipe(
|
||||
map(([configuredExtensions, settings]) => {
|
||||
const enableGoImportsSearchQueryTransform =
|
||||
isSettingsValid(settings) &&
|
||||
settings.final.experimentalFeatures?.enableGoImportsSearchQueryTransform
|
||||
|
||||
return configuredExtensions.filter(extension => {
|
||||
const extensionsAsCoreFeatureMigratedExtension = MIGRATED_TO_CORE_WORKFLOW_EXTENSION_IDS.has(
|
||||
extension.id
|
||||
)
|
||||
// Ignore extensions migrated to the core workflow if the experimental feature is enabled
|
||||
if (context.clientApplication === 'sourcegraph' && extensionsAsCoreFeatureMigratedExtension) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Go import search query transform is enabled by default but can be disabled by the setting
|
||||
const enableGoImportsSearchQueryTransformMigratedExtension =
|
||||
(enableGoImportsSearchQueryTransform === undefined || enableGoImportsSearchQueryTransform) &&
|
||||
extension.id === 'go-imports-search'
|
||||
// Ignore loading the go-imports-search extension when the migrated go imports search is enabled
|
||||
if (
|
||||
context.clientApplication === 'sourcegraph' &&
|
||||
enableGoImportsSearchQueryTransformMigratedExtension
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isExtensionEnabled(settings.final, extension.id)
|
||||
})
|
||||
}),
|
||||
distinctUntilChanged((a, b) => areExtensionsSame(a, b)),
|
||||
publishReplay(1),
|
||||
refCount()
|
||||
)
|
||||
)
|
||||
@ -22,18 +22,12 @@ describe('MainThreadAPI', () => {
|
||||
|
||||
const platformContext: Pick<
|
||||
PlatformContext,
|
||||
| 'updateSettings'
|
||||
| 'settings'
|
||||
| 'getGraphQLClient'
|
||||
| 'requestGraphQL'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'clientApplication'
|
||||
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
|
||||
> = {
|
||||
settings: EMPTY,
|
||||
getGraphQLClient,
|
||||
updateSettings: () => Promise.resolve(),
|
||||
requestGraphQL,
|
||||
getScriptURLForExtension: () => undefined,
|
||||
clientApplication: 'other',
|
||||
}
|
||||
|
||||
@ -61,18 +55,12 @@ describe('MainThreadAPI', () => {
|
||||
|
||||
const platformContext: Pick<
|
||||
PlatformContext,
|
||||
| 'updateSettings'
|
||||
| 'settings'
|
||||
| 'getGraphQLClient'
|
||||
| 'requestGraphQL'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'clientApplication'
|
||||
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
|
||||
> = {
|
||||
settings: EMPTY,
|
||||
getGraphQLClient,
|
||||
updateSettings: () => Promise.resolve(),
|
||||
requestGraphQL,
|
||||
getScriptURLForExtension: () => undefined,
|
||||
clientApplication: 'other',
|
||||
}
|
||||
|
||||
@ -93,12 +81,7 @@ describe('MainThreadAPI', () => {
|
||||
}
|
||||
const platformContext: Pick<
|
||||
PlatformContext,
|
||||
| 'updateSettings'
|
||||
| 'settings'
|
||||
| 'requestGraphQL'
|
||||
| 'getGraphQLClient'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'clientApplication'
|
||||
'updateSettings' | 'settings' | 'requestGraphQL' | 'getGraphQLClient' | 'clientApplication'
|
||||
> = {
|
||||
settings: of({
|
||||
subjects: [
|
||||
@ -131,7 +114,6 @@ describe('MainThreadAPI', () => {
|
||||
updateSettings,
|
||||
getGraphQLClient,
|
||||
requestGraphQL: () => EMPTY,
|
||||
getScriptURLForExtension: () => undefined,
|
||||
clientApplication: 'other',
|
||||
}
|
||||
|
||||
@ -161,18 +143,12 @@ describe('MainThreadAPI', () => {
|
||||
|
||||
const platformContext: Pick<
|
||||
PlatformContext,
|
||||
| 'updateSettings'
|
||||
| 'settings'
|
||||
| 'getGraphQLClient'
|
||||
| 'requestGraphQL'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'clientApplication'
|
||||
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
|
||||
> = {
|
||||
getGraphQLClient,
|
||||
settings: of(...values),
|
||||
updateSettings: () => Promise.resolve(),
|
||||
requestGraphQL: () => EMPTY,
|
||||
getScriptURLForExtension: () => undefined,
|
||||
clientApplication: 'other',
|
||||
}
|
||||
|
||||
@ -193,18 +169,12 @@ describe('MainThreadAPI', () => {
|
||||
const values = new Subject<SettingsCascade<{ a: string }>>()
|
||||
const platformContext: Pick<
|
||||
PlatformContext,
|
||||
| 'updateSettings'
|
||||
| 'settings'
|
||||
| 'getGraphQLClient'
|
||||
| 'requestGraphQL'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'clientApplication'
|
||||
'updateSettings' | 'settings' | 'getGraphQLClient' | 'requestGraphQL' | 'clientApplication'
|
||||
> = {
|
||||
settings: values.asObservable(),
|
||||
updateSettings: () => Promise.resolve(),
|
||||
getGraphQLClient,
|
||||
requestGraphQL: () => EMPTY,
|
||||
getScriptURLForExtension: () => undefined,
|
||||
clientApplication: 'other',
|
||||
}
|
||||
const passedToExtensionHost: SettingsCascade<object>[] = []
|
||||
|
||||
@ -13,7 +13,6 @@ import { proxySubscribable } from '../extension/api/common'
|
||||
import { NotificationType, PlainNotification } from '../extension/extensionHostApi'
|
||||
|
||||
import { ProxySubscription } from './api/common'
|
||||
import { getEnabledExtensions } from './enabledExtensions'
|
||||
import { updateSettings } from './services/settings'
|
||||
|
||||
/** A registered command in the command registry. */
|
||||
@ -65,7 +64,6 @@ export const initMainThreadAPI = (
|
||||
| 'requestGraphQL'
|
||||
| 'showMessage'
|
||||
| 'showInputBox'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'getStaticExtensions'
|
||||
| 'telemetryService'
|
||||
| 'clientApplication'
|
||||
@ -149,14 +147,6 @@ export const initMainThreadAPI = (
|
||||
showInputBox: options =>
|
||||
platformContext.showInputBox ? platformContext.showInputBox(options) : defaultShowInputBox(options),
|
||||
|
||||
getScriptURLForExtension: () => {
|
||||
const getScriptURL = platformContext.getScriptURLForExtension()
|
||||
if (!getScriptURL) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return proxy(getScriptURL)
|
||||
},
|
||||
getEnabledExtensions: () => {
|
||||
if (platformContext.getStaticExtensions) {
|
||||
return proxySubscribable(
|
||||
@ -164,15 +154,13 @@ export const initMainThreadAPI = (
|
||||
.getStaticExtensions()
|
||||
.pipe(
|
||||
switchMap(staticExtensions =>
|
||||
staticExtensions
|
||||
? of(staticExtensions).pipe(publishReplay(1), refCount())
|
||||
: getEnabledExtensions(platformContext)
|
||||
staticExtensions ? of(staticExtensions).pipe(publishReplay(1), refCount()) : of([])
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return proxySubscribable(getEnabledExtensions(platformContext))
|
||||
return proxySubscribable(of([]))
|
||||
},
|
||||
logEvent: (eventName, eventProperties) => platformContext.telemetryService?.log(eventName, eventProperties),
|
||||
logExtensionMessage: (...data) => logger.log(...data),
|
||||
|
||||
@ -1,29 +1,18 @@
|
||||
// For search-related extension API features, such as query transformers
|
||||
|
||||
import { Remote } from 'comlink'
|
||||
import { from, Observable, of, TimeoutError } from 'rxjs'
|
||||
import { catchError, filter, first, switchMap, timeout } from 'rxjs/operators'
|
||||
import { Observable, of } from 'rxjs'
|
||||
|
||||
import { logger } from '@sourcegraph/common'
|
||||
|
||||
import { FlatExtensionHostAPI } from '../contract'
|
||||
import { SharedEventLogger } from '../sharedEventLogger'
|
||||
|
||||
import { wrapRemoteObservable } from './api/common'
|
||||
|
||||
const TRANSFORM_QUERY_TIMEOUT = 3000
|
||||
|
||||
/**
|
||||
* Executes search query transformers contributed by Sourcegraph extensions.
|
||||
*/
|
||||
export function transformSearchQuery({
|
||||
query,
|
||||
extensionHostAPIPromise,
|
||||
enableGoImportsSearchQueryTransform,
|
||||
eventLogger,
|
||||
}: {
|
||||
query: string
|
||||
extensionHostAPIPromise: null | Promise<Remote<FlatExtensionHostAPI>>
|
||||
enableGoImportsSearchQueryTransform: undefined | boolean
|
||||
eventLogger: SharedEventLogger
|
||||
}): Observable<string> {
|
||||
@ -33,37 +22,7 @@ export function transformSearchQuery({
|
||||
query = goImportsTransform(query, eventLogger)
|
||||
}
|
||||
|
||||
if (extensionHostAPIPromise === null) {
|
||||
return of(query)
|
||||
}
|
||||
|
||||
return from(extensionHostAPIPromise).pipe(
|
||||
switchMap(extensionHostAPI =>
|
||||
// Since we won't re-compute on subsequent extension activation, ensure that
|
||||
// at least the initial set of extensions, which should include always-activated
|
||||
// query-transforming extensions, have been loaded to ensure that the initial
|
||||
// search query is transformed
|
||||
wrapRemoteObservable(extensionHostAPI.haveInitialExtensionsLoaded()).pipe(
|
||||
filter(haveLoaded => haveLoaded),
|
||||
first(), // Ensure that it only emits once
|
||||
switchMap(() =>
|
||||
wrapRemoteObservable(extensionHostAPI.transformSearchQuery(query)).pipe(
|
||||
first() // Ensure that it only emits once
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
// Timeout: if this is hanging due to any sort of extension bug, it may not result in a thrown error,
|
||||
// but will degrade search UX.
|
||||
// Wait up to 5 seconds and log to console for users to debug slow query transformer extensions
|
||||
timeout(TRANSFORM_QUERY_TIMEOUT),
|
||||
catchError(error => {
|
||||
if (error instanceof TimeoutError) {
|
||||
logger.error(`Extension query transformers took more than ${TRANSFORM_QUERY_TIMEOUT}ms`)
|
||||
}
|
||||
return of(query)
|
||||
})
|
||||
)
|
||||
return of(query)
|
||||
}
|
||||
|
||||
function goImportsTransform(query: string, eventLogger: SharedEventLogger): string {
|
||||
|
||||
@ -4,7 +4,7 @@ import { DocumentHighlight } from 'sourcegraph'
|
||||
|
||||
import { Contributions, Evaluated, Raw, TextDocumentPositionParameters, HoverMerged } from '@sourcegraph/client-api'
|
||||
import { MaybeLoadingResult } from '@sourcegraph/codeintellify'
|
||||
import { DeepReplace, ErrorLike } from '@sourcegraph/common'
|
||||
import { DeepReplace } from '@sourcegraph/common'
|
||||
import * as clientType from '@sourcegraph/extension-api-types'
|
||||
import { GraphQLResult } from '@sourcegraph/http-client'
|
||||
|
||||
@ -196,10 +196,6 @@ export interface MainThreadAPI {
|
||||
showMessage: (message: string) => Promise<void>
|
||||
showInputBox: (options?: InputBoxOptions) => Promise<string | undefined>
|
||||
|
||||
getScriptURLForExtension: () =>
|
||||
| undefined
|
||||
| (((bundleURLs: string[]) => Promise<(string | ErrorLike)[]>) & ProxyMarked)
|
||||
|
||||
getEnabledExtensions: () => ProxySubscribable<(ConfiguredExtension | ExecutableExtension)[]>
|
||||
|
||||
/**
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Remote } from 'comlink'
|
||||
import { BehaviorSubject, combineLatest, from, Observable, Subscription } from 'rxjs'
|
||||
import { BehaviorSubject, combineLatest, from, Observable, of, Subscription } from 'rxjs'
|
||||
import { catchError, concatMap, distinctUntilChanged, first, map, switchMap, tap } from 'rxjs/operators'
|
||||
import sourcegraph from 'sourcegraph'
|
||||
|
||||
import { Contributions } from '@sourcegraph/client-api'
|
||||
import { asError, ErrorLike, isErrorLike, hashCode, memoizeObservable, logger } from '@sourcegraph/common'
|
||||
import { asError, isErrorLike, hashCode, logger } from '@sourcegraph/common'
|
||||
|
||||
import { ConfiguredExtension, getScriptURLFromExtensionManifest, splitExtensionID } from '../../extensions/extension'
|
||||
import { areExtensionsSame, getEnabledExtensionsForSubject } from '../../extensions/extensions'
|
||||
@ -62,7 +62,7 @@ const DEPRECATED_EXTENSION_IDS = new Set(['sourcegraph/code-stats-insights', 'so
|
||||
|
||||
export function activateExtensions(
|
||||
state: Pick<ExtensionHostState, 'activeExtensions' | 'contributions' | 'haveInitialExtensionsLoaded' | 'settings'>,
|
||||
mainAPI: Remote<Pick<MainThreadAPI, 'getScriptURLForExtension' | 'logEvent'>>,
|
||||
mainAPI: Remote<Pick<MainThreadAPI, 'logEvent'>>,
|
||||
createExtensionAPI: (extensionID: string) => typeof sourcegraph,
|
||||
mainThreadAPIInitializations: Observable<boolean>,
|
||||
/**
|
||||
@ -76,31 +76,12 @@ export function activateExtensions(
|
||||
* */
|
||||
deactivate = deactivateExtension
|
||||
): Subscription {
|
||||
const getScriptURLs = memoizeObservable(
|
||||
() =>
|
||||
mainThreadAPIInitializations.pipe(
|
||||
first(initialized => initialized),
|
||||
switchMap(() =>
|
||||
from(mainAPI.getScriptURLForExtension()).pipe(
|
||||
map(getScriptURL => {
|
||||
function getBundleURLs(urls: string[]): Promise<(string | ErrorLike)[]> {
|
||||
return getScriptURL ? getScriptURL(urls) : Promise.resolve(urls)
|
||||
}
|
||||
|
||||
return getBundleURLs
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
() => 'getScriptURL'
|
||||
)
|
||||
|
||||
const previouslyActivatedExtensions = new Set<string>()
|
||||
const extensionContributions = new Map<string, Contributions>()
|
||||
const contributionsToAdd = new Map<string, Contributions>()
|
||||
const extensionsSubscription = combineLatest([state.activeExtensions, getScriptURLs(null)])
|
||||
const extensionsSubscription = combineLatest([state.activeExtensions])
|
||||
.pipe(
|
||||
concatMap(([activeExtensions, getScriptURLs]) => {
|
||||
concatMap(([activeExtensions]) => {
|
||||
const toDeactivate = new Set<string>()
|
||||
const toActivate = new Map<string, ConfiguredExtension | ExecutableExtension>()
|
||||
const activeExtensionIDs = new Set<string>()
|
||||
@ -126,32 +107,24 @@ export function activateExtensions(
|
||||
}
|
||||
}
|
||||
|
||||
return from(
|
||||
getScriptURLs(
|
||||
[...toActivate.values()].map(extension => {
|
||||
if ('scriptURL' in extension) {
|
||||
// This is already an executable extension (inline extension)
|
||||
return extension.scriptURL
|
||||
}
|
||||
const scriptURLs = [...toActivate.values()].map(extension => {
|
||||
if ('scriptURL' in extension) {
|
||||
// This is already an executable extension (inline extension)
|
||||
return extension.scriptURL
|
||||
}
|
||||
|
||||
return getScriptURLFromExtensionManifest(extension)
|
||||
})
|
||||
).then(scriptURLs => {
|
||||
// TODO: (not urgent) add scriptURL cache
|
||||
return getScriptURLFromExtensionManifest(extension)
|
||||
})
|
||||
|
||||
const executableExtensionsToActivate: ExecutableExtension[] = [...toActivate.values()]
|
||||
.map((extension, index) => ({
|
||||
id: extension.id,
|
||||
manifest: extension.manifest,
|
||||
scriptURL: scriptURLs[index],
|
||||
}))
|
||||
.filter(
|
||||
(extension): extension is ExecutableExtension => typeof extension.scriptURL === 'string'
|
||||
)
|
||||
const executableExtensionsToActivate: ExecutableExtension[] = [...toActivate.values()]
|
||||
.map((extension, index) => ({
|
||||
id: extension.id,
|
||||
manifest: extension.manifest,
|
||||
scriptURL: scriptURLs[index],
|
||||
}))
|
||||
.filter((extension): extension is ExecutableExtension => typeof extension.scriptURL === 'string')
|
||||
|
||||
return { toActivate: executableExtensionsToActivate, toDeactivate }
|
||||
})
|
||||
).pipe(
|
||||
return of({ toActivate: executableExtensionsToActivate, toDeactivate }).pipe(
|
||||
tap(({ toActivate }) => {
|
||||
for (const extension of toActivate) {
|
||||
if (
|
||||
|
||||
@ -16,8 +16,7 @@ describe('Extension activation', () => {
|
||||
it('logs events for activated extensions', async () => {
|
||||
const logEvent = sinon.spy()
|
||||
|
||||
const mockMain = pretendRemote<Pick<MainThreadAPI, 'getScriptURLForExtension' | 'logEvent'>>({
|
||||
getScriptURLForExtension: () => undefined,
|
||||
const mockMain = pretendRemote<Pick<MainThreadAPI, 'logEvent'>>({
|
||||
logEvent,
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { proxy } from 'comlink'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
|
||||
import { SettingsCascade } from '../../../settings/settings'
|
||||
@ -24,7 +23,6 @@ describe('ExtensionHost: Configuration', () => {
|
||||
sourcegraphURL: 'https://example.com/',
|
||||
},
|
||||
pretendRemote<ClientAPI>({
|
||||
getScriptURLForExtension: proxy(() => undefined),
|
||||
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
|
||||
})
|
||||
)
|
||||
@ -44,7 +42,6 @@ describe('ExtensionHost: Configuration', () => {
|
||||
sourcegraphURL: 'https://example.com/',
|
||||
},
|
||||
pretendRemote<ClientAPI>({
|
||||
getScriptURLForExtension: proxy(() => undefined),
|
||||
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
|
||||
})
|
||||
)
|
||||
@ -62,7 +59,6 @@ describe('ExtensionHost: Configuration', () => {
|
||||
sourcegraphURL: 'https://example.com/',
|
||||
},
|
||||
pretendRemote<ClientAPI>({
|
||||
getScriptURLForExtension: proxy(() => undefined),
|
||||
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
|
||||
})
|
||||
)
|
||||
@ -82,7 +78,6 @@ describe('ExtensionHost: Configuration', () => {
|
||||
sourcegraphURL: 'https://example.com/',
|
||||
},
|
||||
pretendRemote<ClientAPI>({
|
||||
getScriptURLForExtension: proxy(() => undefined),
|
||||
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
|
||||
})
|
||||
)
|
||||
@ -105,7 +100,6 @@ describe('ExtensionHost: Configuration', () => {
|
||||
sourcegraphURL: 'https://example.com/',
|
||||
},
|
||||
pretendRemote<ClientAPI>({
|
||||
getScriptURLForExtension: proxy(() => undefined),
|
||||
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
|
||||
applySettingsEdit: edit =>
|
||||
Promise.resolve().then(() => {
|
||||
|
||||
@ -15,7 +15,6 @@ describe('getDocumentHighlights from ExtensionHost API, it aims to have more e2e
|
||||
// integration(ish) tests for scenarios not covered by providers tests
|
||||
const noopMain = pretendRemote<ClientAPI>({
|
||||
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
|
||||
getScriptURLForExtension: () => undefined,
|
||||
})
|
||||
const initialSettings: SettingsCascade<object> = {
|
||||
subjects: [],
|
||||
|
||||
@ -16,7 +16,6 @@ describe('getHover from ExtensionHost API, it aims to have more e2e feel', () =>
|
||||
// integration(ish) tests for scenarios not covered by providers tests
|
||||
const noopMain = pretendRemote<ClientAPI>({
|
||||
getEnabledExtensions: () => pretendProxySubscribable(of([])),
|
||||
getScriptURLForExtension: () => undefined,
|
||||
})
|
||||
const initialSettings: SettingsCascade<object> = {
|
||||
subjects: [],
|
||||
|
||||
@ -11,7 +11,6 @@ import { initializeExtensionHostTest } from './test-helpers'
|
||||
|
||||
const noopMain = pretendRemote<ClientAPI>({
|
||||
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
|
||||
getScriptURLForExtension: () => undefined,
|
||||
logExtensionMessage: (...data) => logger.log(...data),
|
||||
})
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import { initializeExtensionHostTest } from './test-helpers'
|
||||
|
||||
const noopMain = pretendRemote<ClientAPI>({
|
||||
getEnabledExtensions: () => proxySubscribable(new BehaviorSubject([])),
|
||||
getScriptURLForExtension: () => undefined,
|
||||
})
|
||||
const initialSettings: SettingsCascade<object> = { subjects: [], final: {} }
|
||||
|
||||
|
||||
@ -5,13 +5,6 @@ import { TypedTypePolicies } from '../graphql-operations'
|
||||
// Defines how the Apollo cache interacts with our GraphQL schema.
|
||||
// See https://www.apollographql.com/docs/react/caching/cache-configuration/#typepolicy-fields
|
||||
const typePolicies: TypedTypePolicies = {
|
||||
ExtensionRegistry: {
|
||||
// Replace existing `ExtensionRegistry` with the incoming value.
|
||||
// Required because of the missing `id` on the `ExtensionRegistry` field.
|
||||
merge(existing, incoming) {
|
||||
return incoming
|
||||
},
|
||||
},
|
||||
Person: {
|
||||
// Replace existing `Person` with the incoming value.
|
||||
// Required because of the missing `id` on the `Person` field.
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
import { from, Subscription } from 'rxjs'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
|
||||
import type { InitData } from '../api/extension/extensionHost'
|
||||
import { syncPromiseSubscription } from '../api/util'
|
||||
import type { PlatformContext } from '../platform/context'
|
||||
|
||||
import type { Controller } from './controller'
|
||||
|
||||
/**
|
||||
* Creates the controller, which handles all communication between the client application and extensions.
|
||||
*
|
||||
* There should only be a single controller for the entire client application. The controller's model represents
|
||||
* all of the client application state that the client needs to know.
|
||||
*
|
||||
* The implementation (`createExtensionHostClientConnection`) is lazy loaded to avoid adding bytes when
|
||||
* the extension system is disabled
|
||||
*/
|
||||
export function createController(
|
||||
context: Pick<
|
||||
PlatformContext,
|
||||
| 'updateSettings'
|
||||
| 'settings'
|
||||
| 'getGraphQLClient'
|
||||
| 'requestGraphQL'
|
||||
| 'showMessage'
|
||||
| 'showInputBox'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'getStaticExtensions'
|
||||
| 'telemetryService'
|
||||
| 'clientApplication'
|
||||
| 'sourcegraphURL'
|
||||
| 'createExtensionHost'
|
||||
>
|
||||
): Controller {
|
||||
const subscriptions = new Subscription()
|
||||
const initData: Omit<InitData, 'initialSettings'> = {
|
||||
sourcegraphURL: context.sourcegraphURL,
|
||||
clientApplication: context.clientApplication,
|
||||
}
|
||||
const extensionHostClientPromise = import('../api/client/connection').then(module =>
|
||||
module.createExtensionHostClientConnection(context.createExtensionHost(), initData, context)
|
||||
)
|
||||
|
||||
subscriptions.add(() => extensionHostClientPromise.then(({ subscription }) => subscription.unsubscribe()))
|
||||
|
||||
// TODO: Debug helpers, logging
|
||||
|
||||
return {
|
||||
executeCommand: (parameters, suppressNotificationOnError) =>
|
||||
extensionHostClientPromise.then(({ exposedToClient }) =>
|
||||
exposedToClient.executeCommand(parameters, suppressNotificationOnError)
|
||||
),
|
||||
commandErrors: from(extensionHostClientPromise).pipe(
|
||||
switchMap(({ exposedToClient }) => exposedToClient.commandErrors)
|
||||
),
|
||||
registerCommand: entryToRegister =>
|
||||
syncPromiseSubscription(
|
||||
extensionHostClientPromise.then(({ exposedToClient }) =>
|
||||
exposedToClient.registerCommand(entryToRegister)
|
||||
)
|
||||
),
|
||||
extHostAPI: extensionHostClientPromise.then(({ api }) => api),
|
||||
unsubscribe: () => subscriptions.unsubscribe(),
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,6 @@ export function createController(
|
||||
| 'requestGraphQL'
|
||||
| 'showMessage'
|
||||
| 'showInputBox'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'getStaticExtensions'
|
||||
| 'telemetryService'
|
||||
| 'clientApplication'
|
||||
|
||||
@ -8,7 +8,7 @@ import { ExtensionManifest } from './extensionManifest'
|
||||
* The default fields in the {@link ConfiguredExtension} manifest (i.e., the default value of the
|
||||
* `K` type parameter).
|
||||
*/
|
||||
export const CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS = ['contributes', 'activationEvents', 'url'] as const
|
||||
const CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS = ['contributes', 'activationEvents', 'url'] as const
|
||||
export type ConfiguredExtensionManifestDefaultFields = typeof CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS[number]
|
||||
|
||||
/**
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { createGraphQLClientGetter } from '../testing/apollo/createGraphQLClientGetter'
|
||||
|
||||
import { ConfiguredExtension, ConfiguredExtensionManifestDefaultFields } from './extension'
|
||||
import { ExtensionManifest } from './extensionManifest'
|
||||
import { queryConfiguredRegistryExtensions } from './helpers'
|
||||
|
||||
const TEST_MANIFEST: Pick<ExtensionManifest, ConfiguredExtensionManifestDefaultFields | 'publisher'> = {
|
||||
publisher: 'a',
|
||||
url: 'https://example.com',
|
||||
activationEvents: [],
|
||||
}
|
||||
|
||||
describe('queryConfiguredRegistryExtensions', () => {
|
||||
it('gets extensions from GraphQL servers supporting extensions(extensionIDs)', done => {
|
||||
const extensionsMock = {
|
||||
data: {
|
||||
extensionRegistry: {
|
||||
extensions: {
|
||||
nodes: [{ extensionID: 'a/b', manifest: { jsonFields: TEST_MANIFEST } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const getGraphQLClient = createGraphQLClientGetter({ watchQueryMocks: [extensionsMock] })
|
||||
|
||||
queryConfiguredRegistryExtensions({ getGraphQLClient }, ['a/b']).subscribe(data => {
|
||||
expect(data).toEqual([{ id: 'a/b', manifest: TEST_MANIFEST }] as ConfiguredExtension[])
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,87 +0,0 @@
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
import { createAggregateError } from '@sourcegraph/common'
|
||||
import { fromObservableQueryPromise, getDocumentNode, gql } from '@sourcegraph/http-client'
|
||||
|
||||
import { ExtensionsResult, ExtensionsVariables } from '../graphql-operations'
|
||||
import { PlatformContext } from '../platform/context'
|
||||
|
||||
import {
|
||||
ConfiguredExtension,
|
||||
ConfiguredExtensionManifestDefaultFields,
|
||||
CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS,
|
||||
} from './extension'
|
||||
import { ExtensionManifest } from './extensionManifest'
|
||||
|
||||
const ExtensionsQuery = gql`
|
||||
query Extensions($first: Int!, $extensionIDs: [String!]!, $extensionManifestFields: [String!]!) {
|
||||
extensionRegistry {
|
||||
extensions(first: $first, extensionIDs: $extensionIDs) {
|
||||
nodes {
|
||||
id
|
||||
extensionID
|
||||
manifest {
|
||||
jsonFields(fields: $extensionManifestFields)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Query the GraphQL API for registry metadata about the extensions given in {@link extensionIDs}.
|
||||
*
|
||||
* @returns An observable that emits once with the results.
|
||||
*/
|
||||
export function queryConfiguredRegistryExtensions(
|
||||
// TODO(tj): can copy this over to extension host, just replace platformContext.requestGraphQL
|
||||
// with mainThreadAPI.requestGraphQL
|
||||
{ getGraphQLClient }: Pick<PlatformContext, 'getGraphQLClient'>,
|
||||
extensionIDs: string[]
|
||||
): Observable<ConfiguredExtension[]> {
|
||||
if (extensionIDs.length === 0) {
|
||||
return of([])
|
||||
}
|
||||
|
||||
const queryObservablePromise = getGraphQLClient().then(client =>
|
||||
client.watchQuery<ExtensionsResult, ExtensionsVariables>({
|
||||
query: getDocumentNode(ExtensionsQuery),
|
||||
variables: {
|
||||
first: extensionIDs.length,
|
||||
extensionIDs,
|
||||
// Spread operator is required to avoid Typescript type error
|
||||
// because of `readonly` type of `CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS`.
|
||||
extensionManifestFields: [...CONFIGURED_EXTENSION_DEFAULT_MANIFEST_FIELDS],
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
return fromObservableQueryPromise(queryObservablePromise).pipe(
|
||||
map(({ data, errors }) => {
|
||||
if (!data?.extensionRegistry?.extensions?.nodes) {
|
||||
throw createAggregateError(errors)
|
||||
}
|
||||
|
||||
const { nodes } = data.extensionRegistry.extensions
|
||||
|
||||
return nodes
|
||||
.filter(({ extensionID }) => extensionIDs.includes(extensionID))
|
||||
.map(({ extensionID, manifest }) => {
|
||||
const getManifest = (value: typeof manifest): ConfiguredExtension['manifest'] => {
|
||||
if (!value) {
|
||||
return value
|
||||
}
|
||||
|
||||
return value.jsonFields as Pick<ExtensionManifest, ConfiguredExtensionManifestDefaultFields>
|
||||
}
|
||||
|
||||
return {
|
||||
id: extensionID,
|
||||
manifest: getManifest(manifest),
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
@ -488,47 +488,45 @@ export function registerHoverContributions({
|
||||
subscriptions.add(syncRemoteSubscription(referencesContributionPromise))
|
||||
|
||||
let implementationsContributionPromise: Promise<unknown> = Promise.resolve()
|
||||
if (window.context?.enableLegacyExtensions === false) {
|
||||
const promise = extensionHostAPI.registerContributions({
|
||||
actions: [
|
||||
...languageSpecs.map(spec => ({
|
||||
actionItem: { label: 'Find implementations' },
|
||||
command: 'open',
|
||||
commandArguments: [
|
||||
"${get(context, 'implementations_" +
|
||||
spec.languageID +
|
||||
"') && get(context, 'panel.url') && sub(get(context, 'panel.url'), 'panelID', 'implementations_" +
|
||||
spec.languageID +
|
||||
"') || 'noop'}",
|
||||
],
|
||||
id: 'findImplementations_' + spec.languageID,
|
||||
title: 'Find implementations',
|
||||
})),
|
||||
],
|
||||
menus: {
|
||||
hover: languageSpecs.map(spec => ({
|
||||
action: 'findImplementations_' + spec.languageID,
|
||||
when:
|
||||
"resource.language == '" +
|
||||
const promise = extensionHostAPI.registerContributions({
|
||||
actions: [
|
||||
...languageSpecs.map(spec => ({
|
||||
actionItem: { label: 'Find implementations' },
|
||||
command: 'open',
|
||||
commandArguments: [
|
||||
"${get(context, 'implementations_" +
|
||||
spec.languageID +
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
"' && get(context, `implementations_${resource.language}`) && (goToDefinition.showLoading || goToDefinition.url || goToDefinition.error)",
|
||||
})),
|
||||
},
|
||||
})
|
||||
implementationsContributionPromise = promise
|
||||
subscriptions.add(syncRemoteSubscription(promise))
|
||||
for (const spec of languageSpecs) {
|
||||
if (spec.textDocumentImplemenationSupport) {
|
||||
extensionHostAPI
|
||||
.updateContext({
|
||||
[`implementations_${spec.languageID}`]: true,
|
||||
})
|
||||
.then(
|
||||
() => {},
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
"') && get(context, 'panel.url') && sub(get(context, 'panel.url'), 'panelID', 'implementations_" +
|
||||
spec.languageID +
|
||||
"') || 'noop'}",
|
||||
],
|
||||
id: 'findImplementations_' + spec.languageID,
|
||||
title: 'Find implementations',
|
||||
})),
|
||||
],
|
||||
menus: {
|
||||
hover: languageSpecs.map(spec => ({
|
||||
action: 'findImplementations_' + spec.languageID,
|
||||
when:
|
||||
"resource.language == '" +
|
||||
spec.languageID +
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
"' && get(context, `implementations_${resource.language}`) && (goToDefinition.showLoading || goToDefinition.url || goToDefinition.error)",
|
||||
})),
|
||||
},
|
||||
})
|
||||
implementationsContributionPromise = promise
|
||||
subscriptions.add(syncRemoteSubscription(promise))
|
||||
for (const spec of languageSpecs) {
|
||||
if (spec.textDocumentImplemenationSupport) {
|
||||
extensionHostAPI
|
||||
.updateContext({
|
||||
[`implementations_${spec.languageID}`]: true,
|
||||
})
|
||||
.then(
|
||||
() => {},
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { from, Subject, Subscription } from 'rxjs'
|
||||
import { catchError, distinctUntilChanged, map, scan, switchMap } from 'rxjs/operators'
|
||||
|
||||
import { renderMarkdown } from '@sourcegraph/common'
|
||||
import { Alert, AlertProps } from '@sourcegraph/wildcard'
|
||||
import { Alert } from '@sourcegraph/wildcard'
|
||||
|
||||
import type { NotificationType, Progress } from '../codeintel/legacy-extensions/api'
|
||||
|
||||
@ -17,21 +17,11 @@ export interface UnbrandedNotificationItemStyleProps {
|
||||
notificationItemClassNames: Record<NotificationType, string>
|
||||
}
|
||||
|
||||
export interface BrandedNotificationItemStyleProps {
|
||||
notificationItemVariants: Record<NotificationType, AlertProps['variant']>
|
||||
}
|
||||
|
||||
/**
|
||||
* Note, we do not export this type because it is not intended to be used directly.
|
||||
* Consumers should use either `UnbrandedNotificationItemStyleProps` or `BrandedNotificationItemStyleProps` when configuring this component.
|
||||
*/
|
||||
type NotificationItemStyleProps = UnbrandedNotificationItemStyleProps | BrandedNotificationItemStyleProps
|
||||
|
||||
export interface NotificationItemProps {
|
||||
notification: Notification
|
||||
onDismiss: (notification: Notification) => void
|
||||
className?: string
|
||||
notificationItemStyleProps: NotificationItemStyleProps
|
||||
notificationItemStyleProps: UnbrandedNotificationItemStyleProps
|
||||
}
|
||||
|
||||
interface NotificationItemState {
|
||||
@ -93,18 +83,12 @@ export class NotificationItem extends React.PureComponent<NotificationItemProps,
|
||||
const baseAlertClassName = classNames(styles.sourcegraphNotificationItem, this.props.className)
|
||||
|
||||
const { notificationItemStyleProps } = this.props
|
||||
const alertProps =
|
||||
'notificationItemVariants' in notificationItemStyleProps
|
||||
? {
|
||||
variant: notificationItemStyleProps.notificationItemVariants[this.props.notification.type],
|
||||
className: baseAlertClassName,
|
||||
}
|
||||
: {
|
||||
className: classNames(
|
||||
baseAlertClassName,
|
||||
notificationItemStyleProps.notificationItemClassNames[this.props.notification.type]
|
||||
),
|
||||
}
|
||||
const alertProps = {
|
||||
className: classNames(
|
||||
baseAlertClassName,
|
||||
notificationItemStyleProps.notificationItemClassNames[this.props.notification.type]
|
||||
),
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert {...alertProps}>
|
||||
|
||||
@ -3,7 +3,7 @@ import { isObject } from 'lodash'
|
||||
import { Observable, Subscribable, Subscription } from 'rxjs'
|
||||
|
||||
import { DiffPart } from '@sourcegraph/codeintellify'
|
||||
import { ErrorLike, hasProperty } from '@sourcegraph/common'
|
||||
import { hasProperty } from '@sourcegraph/common'
|
||||
import { GraphQLClient, GraphQLResult } from '@sourcegraph/http-client'
|
||||
|
||||
import { SettingsEdit } from '../api/client/services/settings'
|
||||
@ -130,22 +130,6 @@ export interface PlatformContext {
|
||||
*/
|
||||
createExtensionHost: () => Promise<ClosableEndpointPair>
|
||||
|
||||
/**
|
||||
* Returns the script URL suitable for passing to importScripts for an extension's bundle.
|
||||
*
|
||||
* This is necessary because some platforms (such as Chrome extensions) use a script-src CSP
|
||||
* that would prevent loading bundles from arbitrary URLs, which requires us to pass blob: URIs
|
||||
* to importScripts.
|
||||
*
|
||||
* @param bundleURL The URL to the JavaScript bundle file specified in the extension manifest.
|
||||
* @returns A script URL suitable for passing to importScripts, typically either the original
|
||||
* https:// URL for the extension's bundle or a blob: URI for it.
|
||||
*
|
||||
* TODO(tj): If this doesn't return a getScriptURLForExtension function, the original bundleURL will be used.
|
||||
* Also, make getScriptURL batched to minimize round trips between extension host and client application
|
||||
*/
|
||||
getScriptURLForExtension: () => undefined | ((bundleURL: string[]) => Promise<(string | ErrorLike)[]>)
|
||||
|
||||
/**
|
||||
* Constructs the URL (possibly relative or absolute) to the file with the specified options.
|
||||
*
|
||||
|
||||
@ -1,20 +1,4 @@
|
||||
import { PollyServer } from '@pollyjs/core'
|
||||
|
||||
import type { ExtensionContext } from '../../codeintel/legacy-extensions/api'
|
||||
import { ExtensionManifest } from '../../extensions/extensionManifest'
|
||||
import { ExtensionsResult, SharedGraphQlOperations } from '../../graphql-operations'
|
||||
import { Settings } from '../../settings/settings'
|
||||
|
||||
interface ExtensionMockingInit {
|
||||
/**
|
||||
* The polly server object, used to intercept extension bundle requests.
|
||||
*/
|
||||
pollyServer: PollyServer
|
||||
/**
|
||||
* The base Sourcegraph URL for the test instance, used to construst bundle URL.
|
||||
*/
|
||||
sourcegraphBaseUrl: string
|
||||
}
|
||||
|
||||
interface ExtensionMockingUtils {
|
||||
/**
|
||||
@ -23,75 +7,17 @@ interface ExtensionMockingUtils {
|
||||
* and exports an `activate` function, just like any other Sourcegraph extension.
|
||||
*/
|
||||
mockExtension: ({ id, bundle }: { id: string; bundle: () => void }) => void
|
||||
/**
|
||||
* Use this as the `Extension` override for `TestContext#overrideGraphQL`.
|
||||
*/
|
||||
Extensions: SharedGraphQlOperations['Extensions']
|
||||
/**
|
||||
* Merge/replace your mock settings `extensions` property with this object.
|
||||
*/
|
||||
extensionSettings: Settings['extensions']
|
||||
}
|
||||
|
||||
interface ExtensionsResultMock {
|
||||
extensionRegistry: ExtensionsResult['extensionRegistry'] & {
|
||||
__typename: 'ExtensionRegistry'
|
||||
}
|
||||
extensionSettings: {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up Sourcegraph extension mocking for an integration test.
|
||||
*/
|
||||
export function setupExtensionMocking({
|
||||
pollyServer,
|
||||
sourcegraphBaseUrl,
|
||||
}: ExtensionMockingInit): ExtensionMockingUtils {
|
||||
let internalID = 0
|
||||
|
||||
const extensionSettings: Settings['extensions'] = {}
|
||||
const extensionsResult: ExtensionsResultMock = {
|
||||
extensionRegistry: {
|
||||
__typename: 'ExtensionRegistry',
|
||||
extensions: {
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
mockExtension: ({ id, bundle }) => {
|
||||
internalID++
|
||||
|
||||
/** The URL at which the manifest says the extension bundle is served. We should intercept requests to this URL. */
|
||||
const bundleURL = new URL(
|
||||
`/-/static/extension/00${internalID}-${id.replace(/\//g, '-')}.js?hash--${id.replace(/\//g, '-')}`,
|
||||
sourcegraphBaseUrl
|
||||
).href
|
||||
|
||||
const extensionManifest: ExtensionManifest = {
|
||||
url: bundleURL,
|
||||
activationEvents: ['*'],
|
||||
}
|
||||
|
||||
// Mutate mock data objects
|
||||
extensionSettings[id] = true
|
||||
extensionsResult.extensionRegistry.extensions.nodes.push({
|
||||
id,
|
||||
extensionID: id,
|
||||
manifest: {
|
||||
jsonFields: extensionManifest,
|
||||
},
|
||||
})
|
||||
|
||||
pollyServer.get(bundleURL).intercept((request, response) => {
|
||||
// Create an immediately-invoked function expression for the extensionBundle function
|
||||
const extensionBundleString = `(${bundle.toString()})()`
|
||||
response.type('application/javascript; charset=utf-8').send(extensionBundleString)
|
||||
})
|
||||
},
|
||||
Extensions: () => extensionsResult,
|
||||
extensionSettings,
|
||||
}
|
||||
export function setupExtensionMocking(): ExtensionMockingUtils {
|
||||
throw new Error('not yet reimplemented after the extension API deprecation')
|
||||
}
|
||||
|
||||
// Commonly mocked extensions.
|
||||
|
||||
@ -44,7 +44,6 @@ interface Mocks
|
||||
| 'updateSettings'
|
||||
| 'getGraphQLClient'
|
||||
| 'requestGraphQL'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'clientApplication'
|
||||
| 'showMessage'
|
||||
| 'showInputBox'
|
||||
@ -55,7 +54,6 @@ const NOOP_MOCKS: Mocks = {
|
||||
updateSettings: () => Promise.reject(new Error('Mocks#updateSettings not implemented')),
|
||||
getGraphQLClient: () => Promise.reject(new Error('Mocks#getGraphQLClient not implemented')),
|
||||
requestGraphQL: () => throwError(new Error('Mocks#queryGraphQL not implemented')),
|
||||
getScriptURLForExtension: () => undefined,
|
||||
clientApplication: 'sourcegraph',
|
||||
}
|
||||
|
||||
|
||||
@ -26,7 +26,6 @@ export interface VSCodePlatformContext
|
||||
| 'getGraphQLClient'
|
||||
| 'showMessage'
|
||||
| 'showInputBox'
|
||||
| 'getScriptURLForExtension'
|
||||
| 'getStaticExtensions'
|
||||
| 'telemetryService'
|
||||
| 'clientApplication'
|
||||
@ -60,7 +59,6 @@ export function createPlatformContext(extensionCoreAPI: Comlink.Remote<Extension
|
||||
updateSettings: () => Promise.resolve(),
|
||||
telemetryService: new EventLogger(extensionCoreAPI),
|
||||
clientApplication: 'other', // TODO add 'vscode-extension' to `clientApplication`,
|
||||
getScriptURLForExtension: () => undefined,
|
||||
// TODO showInputBox
|
||||
// TODO showMessage
|
||||
getStaticExtensions: () => getInlineExtensions(),
|
||||
|
||||
@ -2,14 +2,11 @@ import { downloadAndUnzipVSCode } from '@vscode/test-electron'
|
||||
|
||||
import { mixedSearchStreamEvents, highlightFileResult } from '@sourcegraph/shared/src/search/integration'
|
||||
import { Settings } from '@sourcegraph/shared/src/settings/settings'
|
||||
import { setupExtensionMocking } from '@sourcegraph/shared/src/testing/integration/mockExtension'
|
||||
|
||||
import { createVSCodeIntegrationTestContext, VSCodeIntegrationTestContext } from './context'
|
||||
import { getVSCodeWebviewFrames } from './getWebview'
|
||||
import { launchVsCode, VSCodeTestDriver } from './launch'
|
||||
|
||||
const sourcegraphBaseUrl = 'https://sourcegraph.com'
|
||||
|
||||
describe('VS Code extension', () => {
|
||||
let vsCodeDriver: VSCodeTestDriver
|
||||
before(async () => {
|
||||
@ -39,18 +36,11 @@ describe('VS Code extension', () => {
|
||||
// fixing before we add more test cases to the suite.
|
||||
|
||||
it('works', async () => {
|
||||
const { Extensions } = setupExtensionMocking({
|
||||
pollyServer: testContext.server,
|
||||
sourcegraphBaseUrl,
|
||||
})
|
||||
|
||||
const userSettings: Settings = {
|
||||
extensions: {},
|
||||
}
|
||||
|
||||
testContext.overrideGraphQL({
|
||||
Extensions,
|
||||
|
||||
...highlightFileResult,
|
||||
ViewerSettings: () => ({
|
||||
viewerSettings: {
|
||||
|
||||
@ -67,7 +67,6 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st
|
||||
openTelemetry: {
|
||||
endpoint: ENVIRONMENT_CONFIG.CLIENT_OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||
},
|
||||
enableLegacyExtensions: false,
|
||||
// Site-config overrides default JS context
|
||||
...siteConfig,
|
||||
}
|
||||
|
||||
@ -85,9 +85,6 @@ const jsContextChanges = `
|
||||
// Only username/password auth-provider provider is supported with the standalone server.
|
||||
authProviders: window.context.authProviders.filter(provider => provider.isBuiltin),
|
||||
|
||||
// For some reason, the standalone server crashes with legacy extensions enabled.
|
||||
enableLegacyExtensions: false,
|
||||
|
||||
// Sync externalURL with the development environment config.
|
||||
externalURL: '${HTTPS_WEB_SERVER_URL}',
|
||||
|
||||
|
||||
@ -197,7 +197,6 @@ export const Layout: React.FC<LegacyLayoutProps> = props => {
|
||||
isRepositoryRelatedPage={isRepositoryRelatedPage}
|
||||
showKeyboardShortcutsHelp={showKeyboardShortcutsHelp}
|
||||
showFeedbackModal={showFeedbackModal}
|
||||
enableLegacyExtensions={window.context.enableLegacyExtensions}
|
||||
/>
|
||||
)}
|
||||
{needsSiteInit && !isSiteInit && <Navigate replace={true} to="/site-admin/init" />}
|
||||
|
||||
@ -189,7 +189,6 @@ export const LegacyLayout: FC<LegacyLayoutProps> = props => {
|
||||
isRepositoryRelatedPage={isRepositoryRelatedPage}
|
||||
showKeyboardShortcutsHelp={showKeyboardShortcutsHelp}
|
||||
showFeedbackModal={showFeedbackModal}
|
||||
enableLegacyExtensions={window.context.enableLegacyExtensions}
|
||||
/>
|
||||
)}
|
||||
{needsSiteInit && !isSiteInit && <Navigate replace={true} to="/site-admin/init" />}
|
||||
|
||||
@ -10,14 +10,10 @@ import { combineLatest, from, Subscription, fromEvent, Observable } from 'rxjs'
|
||||
import { logger } from '@sourcegraph/common'
|
||||
import { GraphQLClient, HTTPStatusError } from '@sourcegraph/http-client'
|
||||
import { SharedSpanName, TraceSpanProvider } from '@sourcegraph/observability-client'
|
||||
import { NotificationType } from '@sourcegraph/shared/src/api/extension/extensionHostApi'
|
||||
import { FetchFileParameters, fetchHighlightedFileLineRanges } from '@sourcegraph/shared/src/backend/file'
|
||||
import { setCodeIntelSearchContext } from '@sourcegraph/shared/src/codeintel/searchContext'
|
||||
import { Controller as ExtensionsController } from '@sourcegraph/shared/src/extensions/controller'
|
||||
import { createController as createExtensionsController } from '@sourcegraph/shared/src/extensions/createLazyLoadedController'
|
||||
import { createNoopController } from '@sourcegraph/shared/src/extensions/createNoopLoadedController'
|
||||
import { BrandedNotificationItemStyleProps } from '@sourcegraph/shared/src/notifications/NotificationItem'
|
||||
import { Notifications } from '@sourcegraph/shared/src/notifications/Notifications'
|
||||
import { PlatformContext } from '@sourcegraph/shared/src/platform/context'
|
||||
import { ShortcutProvider } from '@sourcegraph/shared/src/react-shortcuts'
|
||||
import {
|
||||
@ -91,16 +87,6 @@ interface LegacySourcegraphWebAppState extends SettingsCascadeProps {
|
||||
globbing: boolean
|
||||
}
|
||||
|
||||
const notificationStyles: BrandedNotificationItemStyleProps = {
|
||||
notificationItemVariants: {
|
||||
[NotificationType.Log]: 'secondary',
|
||||
[NotificationType.Success]: 'success',
|
||||
[NotificationType.Info]: 'info',
|
||||
[NotificationType.Warning]: 'warning',
|
||||
[NotificationType.Error]: 'danger',
|
||||
},
|
||||
}
|
||||
|
||||
const WILDCARD_THEME: WildcardTheme = {
|
||||
isBranded: true,
|
||||
}
|
||||
@ -113,9 +99,7 @@ setLinkComponent(RouterLink)
|
||||
export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, LegacySourcegraphWebAppState> {
|
||||
private readonly subscriptions = new Subscription()
|
||||
private readonly platformContext: PlatformContext = createPlatformContext()
|
||||
private readonly extensionsController: ExtensionsController | null = window.context.enableLegacyExtensions
|
||||
? createExtensionsController(this.platformContext)
|
||||
: createNoopController(this.platformContext)
|
||||
private readonly extensionsController: ExtensionsController | null = createNoopController(this.platformContext)
|
||||
|
||||
constructor(props: StaticAppConfig) {
|
||||
super(props)
|
||||
@ -304,13 +288,6 @@ export class LegacySourcegraphWebApp extends React.Component<StaticAppConfig, Le
|
||||
]}
|
||||
>
|
||||
<RouterProvider router={router} />
|
||||
{this.extensionsController !== null && window.context.enableLegacyExtensions ? (
|
||||
<Notifications
|
||||
key={2}
|
||||
extensionsController={this.extensionsController}
|
||||
notificationItemStyleProps={notificationStyles}
|
||||
/>
|
||||
) : null}
|
||||
<UserSessionStores />
|
||||
</ComponentsComposer>
|
||||
)
|
||||
|
||||
@ -25,7 +25,6 @@ describe('persistenceMapper', () => {
|
||||
const persistedString = await persistenceMapper(
|
||||
createStringifiedCache({
|
||||
viewerSettings: { empty: null, data: true },
|
||||
extensionRegistry: { data: true },
|
||||
shouldNotBePersisted: {},
|
||||
})
|
||||
)
|
||||
|
||||
@ -5,11 +5,7 @@ import { QueryFieldPolicy } from '@sourcegraph/shared/src/graphql-operations'
|
||||
* After the implementation of the `persistLink` which will support `@persist` directive
|
||||
* hardcoded query names will be deprecated.
|
||||
*/
|
||||
export const QUERIES_TO_PERSIST: (keyof QueryFieldPolicy)[] = [
|
||||
'viewerSettings',
|
||||
'extensionRegistry',
|
||||
'temporarySettings',
|
||||
]
|
||||
export const QUERIES_TO_PERSIST: (keyof QueryFieldPolicy)[] = ['viewerSettings', 'temporarySettings']
|
||||
export const ROOT_QUERY_KEY = 'ROOT_QUERY'
|
||||
|
||||
export interface CacheReference {
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
.button {
|
||||
color: var(--icon-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
// Required to overwrite .btn:hover styles with greater specificity.
|
||||
color: var(--body-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.action-item {
|
||||
--link-color: var(--body-color);
|
||||
--link-hover-color: var(--body-color);
|
||||
|
||||
display: block;
|
||||
text-align: left;
|
||||
|
||||
/* Overwrite `ButtonLink` style */
|
||||
text-decoration: none !important;
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import {
|
||||
CommandListPopoverButton,
|
||||
CommandListPopoverButtonProps,
|
||||
} from '@sourcegraph/shared/src/commandPalette/CommandList'
|
||||
import { useKeyboardShortcut } from '@sourcegraph/shared/src/keyboardShortcuts/useKeyboardShortcut'
|
||||
import { Button } from '@sourcegraph/wildcard'
|
||||
|
||||
import styles from './WebCommandListPopoverButton.module.scss'
|
||||
|
||||
export const WebCommandListPopoverButton: React.FunctionComponent<
|
||||
React.PropsWithChildren<CommandListPopoverButtonProps>
|
||||
> = props => {
|
||||
const showCommandPaletteShortcut = useKeyboardShortcut('commandPalette')
|
||||
|
||||
return (
|
||||
<CommandListPopoverButton
|
||||
{...props}
|
||||
as={Button}
|
||||
variant="link"
|
||||
buttonClassName={classNames('m-0 p-0', styles.button)}
|
||||
formClassName="form p-2 bg-1 border-bottom"
|
||||
inputClassName="form-control px-2 py-1"
|
||||
listClassName="list-group list-group-flush list-unstyled pt-1"
|
||||
actionItemClassName={classNames('list-group-item list-group-item-action p-2 border-0', styles.actionItem)}
|
||||
selectedActionItemClassName="active border-primary"
|
||||
noResultsClassName="list-group-item text-muted"
|
||||
keyboardShortcutForShow={showCommandPaletteShortcut}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
WebCommandListPopoverButton.displayName = 'WebCommandListPopoverButton'
|
||||
@ -1 +0,0 @@
|
||||
export * from './WebCommandListPopoverButton'
|
||||
@ -1,3 +1,2 @@
|
||||
// Components from shared with web-styling class names applied
|
||||
export { WebHoverOverlay } from './WebHoverOverlay'
|
||||
export { WebCommandListPopoverButton } from './WebCommandListPopoverButton'
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { isEqual } from 'lodash'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
import { observeResize } from '@sourcegraph/common'
|
||||
|
||||
interface CarouselOptions {
|
||||
amountToScroll?: number
|
||||
direction: CarouselDirection
|
||||
}
|
||||
|
||||
type CarouselDirection = 'leftToRight' | 'topToBottom'
|
||||
|
||||
interface CarouselState {
|
||||
canScrollNegative: boolean
|
||||
canScrollPositive: boolean
|
||||
onNegativeClicked: () => void
|
||||
onPositiveClicked: () => void
|
||||
carouselReference: React.RefCallback<HTMLElement>
|
||||
}
|
||||
|
||||
const defaultCarouselState = { canScrollNegative: false, canScrollPositive: false }
|
||||
|
||||
const carouselScrollHandlers: Record<
|
||||
CarouselDirection,
|
||||
(carousel: HTMLElement) => Pick<CarouselState, 'canScrollNegative' | 'canScrollPositive'>
|
||||
> = {
|
||||
leftToRight: carousel => ({
|
||||
canScrollNegative: carousel.scrollLeft > 0,
|
||||
canScrollPositive: carousel.scrollLeft + carousel.clientWidth < carousel.scrollWidth,
|
||||
}),
|
||||
topToBottom: carousel => ({
|
||||
canScrollNegative: carousel.scrollTop > 0,
|
||||
canScrollPositive: carousel.scrollTop + carousel.clientHeight < carousel.scrollHeight,
|
||||
}),
|
||||
}
|
||||
|
||||
const carouselClickHandlers: Record<
|
||||
CarouselDirection,
|
||||
(options: { carousel: HTMLElement; amountToScroll: number; sign: 'positive' | 'negative' }) => void
|
||||
> = {
|
||||
leftToRight: ({ carousel, amountToScroll, sign }) => {
|
||||
const width = carousel.clientWidth
|
||||
carousel.scrollBy({
|
||||
top: 0,
|
||||
left: sign === 'positive' ? width * amountToScroll : -(width * amountToScroll),
|
||||
behavior: 'smooth',
|
||||
})
|
||||
},
|
||||
topToBottom: ({ carousel, amountToScroll, sign }) => {
|
||||
const height = carousel.clientHeight
|
||||
carousel.scrollBy({
|
||||
top: sign === 'positive' ? height * amountToScroll : -(height * amountToScroll),
|
||||
left: 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export function useCarousel({ amountToScroll = 0.9, direction }: CarouselOptions): CarouselState {
|
||||
const [carousel, setCarousel] = useState<HTMLElement | null>()
|
||||
const nextCarousel = useCallback((carousel: HTMLElement) => {
|
||||
setCarousel(carousel)
|
||||
}, [])
|
||||
|
||||
const [scrollability, setScrollability] = useState(defaultCarouselState)
|
||||
|
||||
const scrollabilityReference = useRef(scrollability)
|
||||
scrollabilityReference.current = scrollability
|
||||
|
||||
// Listen for UIEvents that can affect scrollability (e.g. scroll, resize)
|
||||
useEffect(() => {
|
||||
function onScroll(): void {
|
||||
if (carousel) {
|
||||
const newScrollability = carouselScrollHandlers[direction](carousel)
|
||||
if (!isEqual(scrollabilityReference.current, newScrollability)) {
|
||||
setScrollability(newScrollability)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
carousel?.addEventListener('scroll', onScroll)
|
||||
|
||||
let subscription: Subscription | undefined
|
||||
|
||||
if (carousel) {
|
||||
subscription = observeResize(carousel).subscribe(() => {
|
||||
const newScrollability = carouselScrollHandlers[direction](carousel)
|
||||
|
||||
if (!isEqual(scrollabilityReference.current, newScrollability)) {
|
||||
setScrollability(newScrollability)
|
||||
}
|
||||
})
|
||||
|
||||
// Check initial scroll state
|
||||
const newScrollability = carouselScrollHandlers[direction](carousel)
|
||||
if (!isEqual(scrollabilityReference.current, newScrollability)) {
|
||||
setScrollability(newScrollability)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
carousel?.removeEventListener('scroll', onScroll)
|
||||
subscription?.unsubscribe()
|
||||
}
|
||||
}, [carousel, direction])
|
||||
|
||||
// Handle negative and positive click events
|
||||
const onNegativeClicked = useCallback(() => {
|
||||
if (carousel) {
|
||||
carouselClickHandlers[direction]({ sign: 'negative', amountToScroll, carousel })
|
||||
}
|
||||
}, [direction, amountToScroll, carousel])
|
||||
|
||||
const onPositiveClicked = useCallback(() => {
|
||||
if (carousel) {
|
||||
carouselClickHandlers[direction]({ sign: 'positive', amountToScroll, carousel })
|
||||
}
|
||||
}, [direction, amountToScroll, carousel])
|
||||
|
||||
return {
|
||||
canScrollNegative: scrollability.canScrollNegative,
|
||||
canScrollPositive: scrollability.canScrollPositive,
|
||||
onNegativeClicked,
|
||||
onPositiveClicked,
|
||||
carouselReference: nextCarousel,
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,7 @@ export interface CodeInsightsRouterProps extends TelemetryProps {
|
||||
export const CodeInsightsRouter: FC<CodeInsightsRouterProps> = props => {
|
||||
const { authenticatedUser, telemetryService } = props
|
||||
|
||||
if (!window.context.codeInsightsEnabled) {
|
||||
if (!window.context?.codeInsightsEnabled) {
|
||||
return (
|
||||
<CodeInsightsDotComGetStartedLazy
|
||||
telemetryService={telemetryService}
|
||||
|
||||
@ -1,151 +0,0 @@
|
||||
$default-icon-colors: --oc-grape-7, --oc-violet-7, --oc-cyan-9, --oc-indigo-7, --oc-pink-8;
|
||||
|
||||
:root {
|
||||
--action-item-width: 2.5rem;
|
||||
--action-item-container-width: 2.5625rem; /* 2.5rem + 1px */
|
||||
}
|
||||
|
||||
.bar {
|
||||
flex: 0 0 auto;
|
||||
width: var(--action-item-width);
|
||||
background-color: var(--body-bg);
|
||||
list-style: none;
|
||||
|
||||
&--collapsed {
|
||||
width: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-container {
|
||||
width: var(--action-item-width);
|
||||
background-color: var(--color-bg-2);
|
||||
background-color: var(--body-bg);
|
||||
|
||||
&--open {
|
||||
margin-bottom: -0.0625rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Used to visually separate action items bar sections. */
|
||||
.divider-horizontal {
|
||||
height: 0.0625rem;
|
||||
width: 1.25rem;
|
||||
background-color: var(--border-color);
|
||||
left: 0.625rem;
|
||||
}
|
||||
|
||||
/* Used to visually separate action items bar toggle from repo header actions. */
|
||||
.divider-vertical {
|
||||
height: 1.25rem;
|
||||
width: 0.0625rem;
|
||||
top: 0.75rem;
|
||||
align-self: center;
|
||||
|
||||
border-radius: 2px;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.list {
|
||||
overflow-y: auto;
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item {
|
||||
user-select: none;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-left: 0.25rem;
|
||||
border-radius: 0.1875rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
|
||||
&--toggle {
|
||||
height: auto;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&--pressed {
|
||||
color: var(--body-color);
|
||||
background-color: var(--color-bg-3);
|
||||
|
||||
/* Override existing hover styles */
|
||||
&:hover {
|
||||
background-color: var(--color-bg-3);
|
||||
}
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
cursor: default;
|
||||
filter: saturate(0%);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Default icon generated for extensions with no iconURL */
|
||||
&--no-icon {
|
||||
&::after {
|
||||
color: var(--white);
|
||||
|
||||
/* Center letter */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
font-size: (10 / 16) + rem;
|
||||
content: attr(data-content);
|
||||
border-radius: (2 / 16) + rem;
|
||||
}
|
||||
|
||||
&-inactive {
|
||||
&::after {
|
||||
background-color: var(--color-bg-3) !important;
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 1rem !important;
|
||||
width: 1rem !important;
|
||||
|
||||
/* Default icon background color */
|
||||
@for $i from 1 through length($default-icon-colors) {
|
||||
&-#{$i} {
|
||||
&::after {
|
||||
background-color: var(nth($default-icon-colors, $i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* e.g. "close extensions panel", "add extensions" */
|
||||
.aux-icon {
|
||||
color: var(--icon-color);
|
||||
}
|
||||
|
||||
.scroll {
|
||||
width: var(--action-item-width);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { DecoratorFn, Meta } from '@storybook/react'
|
||||
import { EMPTY, noop, of } from 'rxjs'
|
||||
|
||||
import { ContributableMenu, Contributions, Evaluated } from '@sourcegraph/client-api'
|
||||
import { FlatExtensionHostAPI } from '@sourcegraph/shared/src/api/contract'
|
||||
import { pretendProxySubscribable, pretendRemote } from '@sourcegraph/shared/src/api/util'
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { extensionsController, NOOP_PLATFORM_CONTEXT } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
|
||||
import { AppRouterContainer } from '../../components/AppRouterContainer'
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
import { SourcegraphContext } from '../../jscontext'
|
||||
|
||||
import { ActionItemsBar, useWebActionItems } from './ActionItemsBar'
|
||||
|
||||
const mockIconURL =
|
||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE2LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHdpZHRoPSI5N3B4IiBoZWlnaHQ9Ijk3cHgiIHZpZXdCb3g9IjAgMCA5NyA5NyIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTcgOTciIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxwYXRoIGZpbGw9IiNGMDUxMzMiIGQ9Ik05Mi43MSw0NC40MDhMNTIuNTkxLDQuMjkxYy0yLjMxLTIuMzExLTYuMDU3LTIuMzExLTguMzY5LDBsLTguMzMsOC4zMzJMNDYuNDU5LDIzLjE5CgkJYzIuNDU2LTAuODMsNS4yNzItMC4yNzMsNy4yMjksMS42ODVjMS45NjksMS45NywyLjUyMSw0LjgxLDEuNjcsNy4yNzVsMTAuMTg2LDEwLjE4NWMyLjQ2NS0wLjg1LDUuMzA3LTAuMyw3LjI3NSwxLjY3MQoJCWMyLjc1LDIuNzUsMi43NSw3LjIwNiwwLDkuOTU4Yy0yLjc1MiwyLjc1MS03LjIwOCwyLjc1MS05Ljk2MSwwYy0yLjA2OC0yLjA3LTIuNTgtNS4xMS0xLjUzMS03LjY1OGwtOS41LTkuNDk5djI0Ljk5NwoJCWMwLjY3LDAuMzMyLDEuMzAzLDAuNzc0LDEuODYxLDEuMzMyYzIuNzUsMi43NSwyLjc1LDcuMjA2LDAsOS45NTljLTIuNzUsMi43NDktNy4yMDksMi43NDktOS45NTcsMGMtMi43NS0yLjc1NC0yLjc1LTcuMjEsMC05Ljk1OQoJCWMwLjY4LTAuNjc5LDEuNDY3LTEuMTkzLDIuMzA3LTEuNTM3VjM2LjM2OWMtMC44NC0wLjM0NC0xLjYyNS0wLjg1My0yLjMwNy0xLjUzN2MtMi4wODMtMi4wODItMi41ODQtNS4xNC0xLjUxNi03LjY5OAoJCUwzMS43OTgsMTYuNzE1TDQuMjg4LDQ0LjIyMmMtMi4zMTEsMi4zMTMtMi4zMTEsNi4wNiwwLDguMzcxbDQwLjEyMSw0MC4xMThjMi4zMSwyLjMxMSw2LjA1NiwyLjMxMSw4LjM2OSwwTDkyLjcxLDUyLjc3OQoJCUM5NS4wMjEsNTAuNDY4LDk1LjAyMSw0Ni43MTksOTIuNzEsNDQuNDA4eiIvPgo8L2c+Cjwvc3ZnPgo='
|
||||
|
||||
if (!window.context) {
|
||||
window.context = { enableLegacyExtensions: false } as SourcegraphContext & Mocha.SuiteFunction
|
||||
}
|
||||
|
||||
// eslint-disable-next-line id-length
|
||||
const mockActionItems = [...(new Array(10) as (number | undefined)[])].map((_, index) => `${index}`)
|
||||
const mockContributions: Evaluated<Contributions> = {
|
||||
actions: mockActionItems.map((id, index) => ({
|
||||
id,
|
||||
actionItem: {
|
||||
iconURL: mockIconURL,
|
||||
label: 'Some label',
|
||||
pressed: index < 2,
|
||||
description: 'Some description',
|
||||
},
|
||||
command: 'open',
|
||||
active: true,
|
||||
category: 'Some category',
|
||||
title: 'Some title',
|
||||
})),
|
||||
menus: {
|
||||
[ContributableMenu.EditorTitle]: mockActionItems.map(id => ({
|
||||
action: id,
|
||||
alt: id,
|
||||
when: true,
|
||||
})),
|
||||
},
|
||||
}
|
||||
|
||||
const mockExtensionsController = {
|
||||
...extensionsController,
|
||||
extHostAPI: Promise.resolve(
|
||||
pretendRemote<FlatExtensionHostAPI>({
|
||||
getContributions: () => pretendProxySubscribable(of(mockContributions)),
|
||||
registerContributions: () => pretendProxySubscribable(EMPTY).subscribe(noop as any),
|
||||
haveInitialExtensionsLoaded: () => pretendProxySubscribable(of(true)),
|
||||
})
|
||||
),
|
||||
}
|
||||
|
||||
const decorator: DecoratorFn = story => (
|
||||
<WebStory
|
||||
initialEntries={[
|
||||
'/github.com/sourcegraph/sourcegraph/-/blob/client/browser/src/browser-extension/ThemeWrapper.tsx',
|
||||
]}
|
||||
>
|
||||
{() => (
|
||||
<AppRouterContainer>
|
||||
<div className="container mt-3">{story()}</div>
|
||||
</AppRouterContainer>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
const config: Meta = {
|
||||
title: 'web/extensions/ActionItemsBar',
|
||||
decorators: [decorator],
|
||||
component: ActionItemsBar,
|
||||
parameters: {
|
||||
chromatic: {
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
export const Default: React.FunctionComponent<React.PropsWithChildren<unknown>> = () => {
|
||||
const { useActionItemsBar } = useWebActionItems()
|
||||
|
||||
return (
|
||||
<ActionItemsBar
|
||||
repo={undefined}
|
||||
useActionItemsBar={useActionItemsBar}
|
||||
extensionsController={mockExtensionsController}
|
||||
platformContext={NOOP_PLATFORM_CONTEXT as any}
|
||||
telemetryService={NOOP_TELEMETRY_SERVICE}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -1,412 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { mdiChevronDoubleDown, mdiChevronDoubleUp, mdiMenuDown, mdiMenuUp, mdiPuzzleOutline } from '@mdi/js'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
import classNames from 'classnames'
|
||||
import { head, last } from 'lodash'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { BehaviorSubject, from, of } from 'rxjs'
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { focusable, FocusableElement } from 'tabbable'
|
||||
import { Key } from 'ts-key-enum'
|
||||
|
||||
import { ContributableMenu } from '@sourcegraph/client-api'
|
||||
import { LocalStorageSubject } from '@sourcegraph/common'
|
||||
import { ActionItem } from '@sourcegraph/shared/src/actions/ActionItem'
|
||||
import { ActionsContainer } from '@sourcegraph/shared/src/actions/ActionsContainer'
|
||||
import { haveInitialExtensionsLoaded } from '@sourcegraph/shared/src/api/features'
|
||||
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
import { isSettingsValid } from '@sourcegraph/shared/src/settings/settings'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { Button, ButtonLink, Icon, LoadingSpinner, Tooltip, useObservable } from '@sourcegraph/wildcard'
|
||||
|
||||
import { ErrorBoundary } from '../../components/ErrorBoundary'
|
||||
import { useCarousel } from '../../components/useCarousel'
|
||||
import { RepositoryFields } from '../../graphql-operations'
|
||||
import { OpenInEditorActionItem } from '../../open-in-editor/OpenInEditorActionItem'
|
||||
import { GoToCodeHostAction } from '../../repo/actions/GoToCodeHostAction'
|
||||
import { ToggleBlameAction } from '../../repo/actions/ToggleBlameAction'
|
||||
import { fetchFileExternalLinks } from '../../repo/backend'
|
||||
import { parseBrowserRepoURL } from '../../util/url'
|
||||
|
||||
import styles from './ActionItemsBar.module.scss'
|
||||
|
||||
const scrollButtonClassName = styles.scroll
|
||||
|
||||
function getIconClassName(index: number): string | undefined {
|
||||
return (styles as Record<string, string>)[`icon${index % 5}`]
|
||||
}
|
||||
|
||||
function arrowable(element: HTMLElement): FocusableElement[] {
|
||||
return focusable(element).filter(
|
||||
elm => !elm.classList.contains('disabled') && !elm.classList.contains(scrollButtonClassName)
|
||||
)
|
||||
}
|
||||
|
||||
export function useWebActionItems(): Pick<ActionItemsBarProps, 'useActionItemsBar'> &
|
||||
Pick<ActionItemsToggleProps, 'useActionItemsToggle'> {
|
||||
const toggles = useMemo(() => new LocalStorageSubject('action-items-bar-expanded', true), [])
|
||||
|
||||
const [toggleReference, setToggleReference] = useState<HTMLElement | null>(null)
|
||||
const nextToggleReference = useCallback((toggle: HTMLElement) => {
|
||||
setToggleReference(toggle)
|
||||
}, [])
|
||||
|
||||
const [barReference, setBarReference] = useState<HTMLElement | null>(null)
|
||||
const nextBarReference = useCallback((bar: HTMLElement) => {
|
||||
setBarReference(bar)
|
||||
}, [])
|
||||
|
||||
// Set up keyboard navigation for distant toggle and bar. Remove previous event
|
||||
// listeners whenever references change.
|
||||
useEffect(() => {
|
||||
function onKeyDownToggle(event: KeyboardEvent): void {
|
||||
if (event.key === Key.ArrowDown && barReference) {
|
||||
const firstBarArrowable = head(arrowable(barReference))
|
||||
if (firstBarArrowable) {
|
||||
firstBarArrowable.focus()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === Key.ArrowUp && barReference) {
|
||||
const lastBarArrowable = last(arrowable(barReference))
|
||||
if (lastBarArrowable) {
|
||||
lastBarArrowable.focus()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDownBar(event: KeyboardEvent): void {
|
||||
if (event.target instanceof HTMLElement && toggleReference && barReference) {
|
||||
const arrowableChildren = arrowable(barReference)
|
||||
const indexOfTarget = arrowableChildren.indexOf(event.target)
|
||||
|
||||
if (event.key === Key.ArrowDown) {
|
||||
// If this is the last arrowable element, go back to the toggle
|
||||
if (indexOfTarget === arrowableChildren.length - 1) {
|
||||
toggleReference.focus()
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const itemToFocus = arrowableChildren[indexOfTarget + 1]
|
||||
if (itemToFocus instanceof HTMLElement) {
|
||||
itemToFocus.focus()
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === Key.ArrowUp) {
|
||||
// If this is the first arrowable element, go back to the toggle
|
||||
if (indexOfTarget === 0) {
|
||||
toggleReference.focus()
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
const itemToFocus = arrowableChildren[indexOfTarget - 1]
|
||||
if (itemToFocus instanceof HTMLElement) {
|
||||
itemToFocus.focus()
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleReference?.addEventListener('keydown', onKeyDownToggle)
|
||||
barReference?.addEventListener('keydown', onKeyDownBar)
|
||||
|
||||
return () => {
|
||||
toggleReference?.removeEventListener('keydown', onKeyDownToggle)
|
||||
toggleReference?.removeEventListener('keydown', onKeyDownBar)
|
||||
}
|
||||
}, [toggleReference, barReference])
|
||||
|
||||
const barsReferenceCounts = useMemo(() => new BehaviorSubject(0), [])
|
||||
|
||||
const useActionItemsBar = useCallback(() => {
|
||||
// `useActionItemsBar` will be used as a hook
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const isOpen = useObservable(toggles)
|
||||
|
||||
// Let the toggle know it's on the page
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
// Use reference counter so that effect order doesn't matter
|
||||
barsReferenceCounts.next(barsReferenceCounts.value + 1)
|
||||
|
||||
return () => barsReferenceCounts.next(barsReferenceCounts.value - 1)
|
||||
}, [])
|
||||
|
||||
return { isOpen, barReference: nextBarReference }
|
||||
}, [toggles, nextBarReference, barsReferenceCounts])
|
||||
|
||||
const useActionItemsToggle = useCallback(() => {
|
||||
// `useActionItemsToggle` will be used as a hook
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const isOpen = useObservable(toggles)
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const toggle = useCallback(() => toggles.next(!isOpen), [isOpen])
|
||||
|
||||
// Only show the action items toggle when the <ActionItemsBar> component is on the page
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const barInPage = !!useObservable(
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useMemo(
|
||||
() =>
|
||||
barsReferenceCounts.pipe(
|
||||
map(count => count > 0),
|
||||
distinctUntilChanged()
|
||||
),
|
||||
[]
|
||||
)
|
||||
)
|
||||
|
||||
return { isOpen, toggle, barInPage, toggleReference: nextToggleReference }
|
||||
}, [toggles, nextToggleReference, barsReferenceCounts])
|
||||
|
||||
return {
|
||||
useActionItemsBar,
|
||||
useActionItemsToggle,
|
||||
}
|
||||
}
|
||||
|
||||
export interface ActionItemsBarProps extends ExtensionsControllerProps, TelemetryProps, PlatformContextProps {
|
||||
repo?: RepositoryFields
|
||||
useActionItemsBar: () => { isOpen: boolean | undefined; barReference: React.RefCallback<HTMLElement> }
|
||||
source?: 'compare' | 'commit' | 'blob'
|
||||
}
|
||||
|
||||
const actionItemClassName = classNames(
|
||||
'd-flex justify-content-center align-items-center text-decoration-none',
|
||||
styles.action
|
||||
)
|
||||
|
||||
/**
|
||||
* Renders extensions (both migrated to the core workflow and legacy) actions items in the sidebar.
|
||||
*/
|
||||
export const ActionItemsBar = React.memo<ActionItemsBarProps>(function ActionItemsBar(props) {
|
||||
const { extensionsController, source } = props
|
||||
|
||||
const location = useLocation()
|
||||
const { isOpen, barReference } = props.useActionItemsBar()
|
||||
const { repoName, rawRevision, filePath, commitRange, position, range } = parseBrowserRepoURL(
|
||||
location.pathname + location.search + location.hash
|
||||
)
|
||||
|
||||
const { carouselReference, canScrollNegative, canScrollPositive, onNegativeClicked, onPositiveClicked } =
|
||||
useCarousel({ direction: 'topToBottom' })
|
||||
|
||||
const haveExtensionsLoaded = useObservable(
|
||||
useMemo(
|
||||
() =>
|
||||
extensionsController !== null ? haveInitialExtensionsLoaded(extensionsController.extHostAPI) : of(true),
|
||||
[extensionsController]
|
||||
)
|
||||
)
|
||||
|
||||
const settingsOrError = useObservable(
|
||||
useMemo(() => from(props.platformContext.settings), [props.platformContext.settings])
|
||||
)
|
||||
const settings =
|
||||
settingsOrError !== undefined && isSettingsValid(settingsOrError) ? settingsOrError.final : undefined
|
||||
const perforceCodeHostUrlToSwarmUrlMap =
|
||||
(settings?.['perforce.codeHostToSwarmMap'] as { [codeHost: string]: string } | undefined) || {}
|
||||
|
||||
if (!isOpen) {
|
||||
return <div className={classNames(styles.bar, styles.barCollapsed)} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('p-0 mr-2 position-relative d-flex flex-column', styles.bar)} ref={barReference}>
|
||||
{/* To be clear to users that this isn't an error reported by extensions about e.g. the code they're viewing. */}
|
||||
<ErrorBoundary location={location} render={error => <span>Component error: {error.message}</span>}>
|
||||
<ActionItemsDivider />
|
||||
{canScrollNegative && (
|
||||
<Button
|
||||
className={classNames('p-0 border-0', styles.scroll, styles.listItem)}
|
||||
onClick={onNegativeClicked}
|
||||
tabIndex={-1}
|
||||
variant="link"
|
||||
aria-label="Scroll up"
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={mdiMenuUp} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{source !== 'compare' && source !== 'commit' && (
|
||||
<GoToCodeHostAction
|
||||
repo={props.repo}
|
||||
repoName={repoName}
|
||||
// We need a revision to generate code host URLs, if revision isn't available, we use the default branch or HEAD.
|
||||
revision={rawRevision || props.repo?.defaultBranch?.displayName || 'HEAD'}
|
||||
filePath={filePath}
|
||||
commitRange={commitRange}
|
||||
range={range}
|
||||
position={position}
|
||||
perforceCodeHostUrlToSwarmUrlMap={perforceCodeHostUrlToSwarmUrlMap}
|
||||
fetchFileExternalLinks={fetchFileExternalLinks}
|
||||
actionType="nav"
|
||||
source="actionItemsBar"
|
||||
/>
|
||||
)}
|
||||
|
||||
{source === 'blob' && (
|
||||
<>
|
||||
<ToggleBlameAction actionType="nav" source="actionItemsBar" />
|
||||
{window.context.isAuthenticatedUser && (
|
||||
<OpenInEditorActionItem
|
||||
platformContext={props.platformContext}
|
||||
externalServiceType={props.repo?.externalRepository?.serviceType}
|
||||
actionType="nav"
|
||||
source="actionItemsBar"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{extensionsController !== null ? (
|
||||
<ActionsContainer
|
||||
menu={ContributableMenu.EditorTitle}
|
||||
returnInactiveMenuItems={true}
|
||||
extensionsController={extensionsController}
|
||||
empty={null}
|
||||
location={location}
|
||||
platformContext={props.platformContext}
|
||||
telemetryService={props.telemetryService}
|
||||
>
|
||||
{items => (
|
||||
<ul className={classNames('list-unstyled m-0', styles.list)} ref={carouselReference}>
|
||||
{items.map((item, index) => {
|
||||
const hasIconURL = !!item.action.actionItem?.iconURL
|
||||
const className = classNames(
|
||||
actionItemClassName,
|
||||
!hasIconURL &&
|
||||
classNames(styles.actionNoIcon, getIconClassName(index), 'text-sm')
|
||||
)
|
||||
const inactiveClassName = classNames(
|
||||
styles.actionInactive,
|
||||
!hasIconURL && styles.actionNoIconInactive
|
||||
)
|
||||
const listItemClassName = classNames(
|
||||
styles.listItem,
|
||||
index !== items.length - 1 && 'mb-1'
|
||||
)
|
||||
|
||||
const dataContent = !hasIconURL ? item.action.category?.slice(0, 1) : undefined
|
||||
|
||||
return (
|
||||
<li key={item.action.id} className={listItemClassName}>
|
||||
<ActionItem
|
||||
{...props}
|
||||
{...item}
|
||||
location={location}
|
||||
extensionsController={extensionsController}
|
||||
className={className}
|
||||
dataContent={dataContent}
|
||||
variant="actionItem"
|
||||
iconClassName={styles.icon}
|
||||
pressedClassName={styles.actionPressed}
|
||||
inactiveClassName={inactiveClassName}
|
||||
hideLabel={true}
|
||||
tabIndex={-1}
|
||||
hideExternalLinkIcon={true}
|
||||
disabledDuringExecution={true}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</ActionsContainer>
|
||||
) : null}
|
||||
{canScrollPositive && (
|
||||
<Button
|
||||
className={classNames('p-0 border-0', styles.scroll, styles.listItem)}
|
||||
onClick={onPositiveClicked}
|
||||
tabIndex={-1}
|
||||
variant="link"
|
||||
aria-label="Scroll down"
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={mdiMenuDown} />
|
||||
</Button>
|
||||
)}
|
||||
{haveExtensionsLoaded && <ActionItemsDivider />}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export interface ActionItemsToggleProps extends ExtensionsControllerProps<'extHostAPI'> {
|
||||
useActionItemsToggle: () => {
|
||||
isOpen: boolean | undefined
|
||||
toggle: () => void
|
||||
toggleReference: React.RefCallback<HTMLElement>
|
||||
barInPage: boolean
|
||||
}
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const ActionItemsToggle: React.FunctionComponent<React.PropsWithChildren<ActionItemsToggleProps>> = ({
|
||||
useActionItemsToggle,
|
||||
extensionsController,
|
||||
className,
|
||||
}) => {
|
||||
const panelName = extensionsController !== null && window.context.enableLegacyExtensions ? 'extensions' : 'actions'
|
||||
|
||||
const { isOpen, toggle, toggleReference, barInPage } = useActionItemsToggle()
|
||||
|
||||
const haveExtensionsLoaded = useObservable(
|
||||
useMemo(
|
||||
() =>
|
||||
extensionsController !== null ? haveInitialExtensionsLoaded(extensionsController.extHostAPI) : of(true),
|
||||
[extensionsController]
|
||||
)
|
||||
)
|
||||
|
||||
return barInPage ? (
|
||||
<>
|
||||
<li className={styles.dividerVertical} />
|
||||
<li className={classNames('nav-item mr-2', className)}>
|
||||
<div className={classNames(styles.toggleContainer, isOpen && styles.toggleContainerOpen)}>
|
||||
<Tooltip content={`${isOpen ? 'Close' : 'Open'} ${panelName} panel`}>
|
||||
<ButtonLink
|
||||
aria-label={
|
||||
isOpen
|
||||
? `Close ${panelName} panel. Press the down arrow key to enter the ${panelName} panel.`
|
||||
: `Open ${panelName} panel`
|
||||
}
|
||||
className={classNames(actionItemClassName, styles.auxIcon, styles.actionToggle)}
|
||||
onSelect={toggle}
|
||||
ref={toggleReference}
|
||||
>
|
||||
{!haveExtensionsLoaded ? (
|
||||
<LoadingSpinner />
|
||||
) : isOpen ? (
|
||||
<Icon aria-hidden={true} svgPath={mdiChevronDoubleUp} />
|
||||
) : (
|
||||
<Icon
|
||||
aria-hidden={true}
|
||||
svgPath={
|
||||
window.context.enableLegacyExtensions ? mdiPuzzleOutline : mdiChevronDoubleDown
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{haveExtensionsLoaded && <VisuallyHidden>Down arrow to enter</VisuallyHidden>}
|
||||
</ButtonLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
const ActionItemsDivider: React.FunctionComponent<React.PropsWithChildren<{ className?: string }>> = ({
|
||||
className,
|
||||
}) => <div className={classNames('position-relative rounded-sm d-flex', styles.dividerHorizontal, className)} />
|
||||
@ -1,10 +1,6 @@
|
||||
import assert from 'assert'
|
||||
|
||||
import type { ExtensionContext } from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
|
||||
import { SharedGraphQlOperations } from '@sourcegraph/shared/src/graphql-operations'
|
||||
import { ExtensionManifest } from '@sourcegraph/shared/src/schema/extensionSchema'
|
||||
import { Settings } from '@sourcegraph/shared/src/schema/settings.schema'
|
||||
import { accessibilityAudit } from '@sourcegraph/shared/src/testing/accessibility'
|
||||
import { Driver, createDriverForTest } from '@sourcegraph/shared/src/testing/driver'
|
||||
import { afterEachSaveScreenshotIfFailed } from '@sourcegraph/shared/src/testing/screenshotReporter'
|
||||
|
||||
@ -18,7 +14,6 @@ import {
|
||||
createBlobContentResult,
|
||||
} from './graphQlResponseHelpers'
|
||||
import { commonWebGraphQlResults, createViewerSettingsGraphQLOverride } from './graphQlResults'
|
||||
import { percySnapshotWithVariants } from './utils'
|
||||
|
||||
describe('Blob viewer', () => {
|
||||
let driver: Driver
|
||||
@ -161,366 +156,4 @@ describe('Blob viewer', () => {
|
||||
await driver.assertWindowLocation(`/${repositoryName}/-/blob/${fileName}?L1-3`)
|
||||
})
|
||||
})
|
||||
|
||||
// Describes the ways the blob viewer can be extended through Sourcegraph extensions.
|
||||
describe('extensibility', () => {
|
||||
beforeEach(() => {
|
||||
const userSettings: Settings = {
|
||||
extensions: {
|
||||
'test/test': true,
|
||||
},
|
||||
}
|
||||
const extensionManifest: ExtensionManifest = {
|
||||
url: new URL('/-/static/extension/0001-test-test.js?hash--test-test', driver.sourcegraphBaseUrl).href,
|
||||
activationEvents: ['*'],
|
||||
}
|
||||
testContext.overrideGraphQL({
|
||||
...commonBlobGraphQlResults,
|
||||
ViewerSettings: () => ({
|
||||
viewerSettings: {
|
||||
__typename: 'SettingsCascade',
|
||||
final: JSON.stringify(userSettings),
|
||||
subjects: [
|
||||
{
|
||||
__typename: 'User',
|
||||
displayName: 'Test User',
|
||||
id: 'TestUserSettingsID',
|
||||
latestSettings: {
|
||||
id: 123,
|
||||
contents: JSON.stringify(userSettings),
|
||||
},
|
||||
username: 'test',
|
||||
viewerCanAdminister: true,
|
||||
settingsURL: '/users/test/settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
Blob: () => ({
|
||||
repository: {
|
||||
commit: {
|
||||
blob: null,
|
||||
file: {
|
||||
__typename: 'VirtualFile',
|
||||
content: '// Log to console\nconsole.log("Hello world")',
|
||||
totalLines: 2,
|
||||
richHTML: '',
|
||||
highlight: {
|
||||
aborted: false,
|
||||
html:
|
||||
// Note: whitespace in this string is significant.
|
||||
'<table><tbody><tr>' +
|
||||
'<td class="line" data-line="1"/>' +
|
||||
'<td class="code"><span class="hl-source hl-js hl-react"><span class="hl-comment hl-line hl-double-slash hl-js">' +
|
||||
'<span class="hl-punctuation hl-definition hl-comment hl-js">//</span> ' +
|
||||
'Log to console\n</span></span></td></tr>' +
|
||||
'<tr><td class="line" data-line="2"/>' +
|
||||
'<td class="code"><span class="hl-source hl-js hl-react"><span class="hl-meta hl-function-call hl-method hl-js">' +
|
||||
'<span class="hl-support hl-type hl-object hl-console hl-js test-console-token">console</span>' +
|
||||
'<span class="hl-punctuation hl-accessor hl-js">.</span>' +
|
||||
'<span class="hl-support hl-function hl-console hl-js test-log-token">log</span>' +
|
||||
'<span class="hl-meta hl-group hl-js"><span class="hl-punctuation hl-section hl-group hl-begin hl-js">(</span>' +
|
||||
'<span class="hl-meta hl-string hl-js"><span class="hl-string hl-quoted hl-double hl-js">' +
|
||||
'<span class="hl-punctuation hl-definition hl-string hl-begin hl-js">"</span>' +
|
||||
'Hello world' +
|
||||
'<span class="hl-punctuation hl-definition hl-string hl-end hl-js">"</span></span></span>' +
|
||||
'<span class="hl-punctuation hl-section hl-group hl-end hl-js">)</span></span>\n</span></span></td></tr></tbody></table>',
|
||||
lsif: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
Extensions: () => ({
|
||||
extensionRegistry: {
|
||||
__typename: 'ExtensionRegistry',
|
||||
extensions: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'test',
|
||||
extensionID: 'test/test',
|
||||
manifest: {
|
||||
jsonFields: extensionManifest,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
// Serve a mock extension bundle with a simple hover provider
|
||||
testContext.server
|
||||
.get(new URL(extensionManifest.url, driver.sourcegraphBaseUrl).href)
|
||||
.intercept((request, response) => {
|
||||
function extensionBundle(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
||||
const sourcegraph = require('sourcegraph') as typeof import('sourcegraph')
|
||||
|
||||
function activate(context: ExtensionContext): void {
|
||||
context.subscriptions.add(
|
||||
sourcegraph.languages.registerHoverProvider([{ language: 'typescript' }], {
|
||||
provideHover: () => ({
|
||||
contents: {
|
||||
kind: sourcegraph.MarkupKind.Markdown,
|
||||
value: 'Test hover content',
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
exports.activate = activate
|
||||
}
|
||||
// Create an immediately-invoked function expression for the extensionBundle function
|
||||
const extensionBundleString = `(${extensionBundle.toString()})()`
|
||||
response.type('application/javascript; charset=utf-8').send(extensionBundleString)
|
||||
})
|
||||
})
|
||||
it('truncates long file paths properly', async () => {
|
||||
await driver.page.goto(
|
||||
`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/this_is_a_long_file_path/apps/rest-showcase/src/main/java/org/demo/rest/example/OrdersController.java`
|
||||
)
|
||||
await driver.page.waitForSelector('[data-testid="repo-blob"]')
|
||||
await driver.page.waitForSelector('.test-breadcrumb')
|
||||
// Uncomment this snapshot once https://github.com/sourcegraph/sourcegraph/issues/15126 is resolved
|
||||
// await percySnapshot(driver.page, this.test!.fullTitle())
|
||||
})
|
||||
|
||||
it.skip('shows a hover overlay from a hover provider when a token is hovered', async () => {
|
||||
await driver.page.goto(`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/${fileName}`)
|
||||
await driver.page.waitForSelector('[data-testid="repo-blob"]')
|
||||
// TODO
|
||||
})
|
||||
|
||||
it.skip('gets displayed when navigating to a URL with a token position', async () => {
|
||||
await driver.page.goto(`${driver.sourcegraphBaseUrl}/github.com/sourcegraph/test/-/test.ts#2:9`)
|
||||
// TODO
|
||||
})
|
||||
|
||||
// Disabled because it's flaky. See: https://github.com/sourcegraph/sourcegraph/issues/31806
|
||||
it.skip('properly displays reference panel for URIs with spaces', async () => {
|
||||
const repositoryName = 'github.com/sourcegraph/test%20repo'
|
||||
const files = ['test.ts', 'test spaces.ts']
|
||||
const commitID = '1234'
|
||||
const userSettings: Settings = {
|
||||
extensions: {
|
||||
'test/references': true,
|
||||
},
|
||||
}
|
||||
const extensionManifest: ExtensionManifest = {
|
||||
url: new URL(
|
||||
'/-/static/extension/0001-test-references.js?hash--test-references',
|
||||
driver.sourcegraphBaseUrl
|
||||
).href,
|
||||
activationEvents: ['*'],
|
||||
}
|
||||
testContext.overrideGraphQL({
|
||||
...commonBlobGraphQlResults,
|
||||
ViewerSettings: () => ({
|
||||
viewerSettings: {
|
||||
__typename: 'SettingsCascade',
|
||||
final: JSON.stringify(userSettings),
|
||||
subjects: [
|
||||
{
|
||||
__typename: 'User',
|
||||
displayName: 'Test User',
|
||||
id: 'TestUserSettingsID',
|
||||
latestSettings: {
|
||||
id: 123,
|
||||
contents: JSON.stringify(userSettings),
|
||||
},
|
||||
username: 'test',
|
||||
viewerCanAdminister: true,
|
||||
settingsURL: '/users/test/settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
Extensions: () => ({
|
||||
extensionRegistry: {
|
||||
__typename: 'ExtensionRegistry',
|
||||
extensions: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'test',
|
||||
extensionID: 'test/references',
|
||||
manifest: {
|
||||
jsonFields: extensionManifest,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
Blob: ({ filePath }) => ({
|
||||
repository: {
|
||||
commit: {
|
||||
blob: null,
|
||||
file: {
|
||||
__typename: 'VirtualFile',
|
||||
content: `// file path: ${filePath}\nconsole.log("Hello world")`,
|
||||
totalLines: 2,
|
||||
richHTML: '',
|
||||
highlight: {
|
||||
aborted: false,
|
||||
html:
|
||||
'<table><tbody><tr><td class="line" data-line="1"/><td class="code">' +
|
||||
'<span class="hl-source hl-js hl-react"><span class="hl-comment hl-line hl-double-slash hl-js">' +
|
||||
'<span class="hl-punctuation hl-definition hl-comment hl-js">//</span>' +
|
||||
` file path: ${filePath}\n` +
|
||||
'</span></span></td></tr>' +
|
||||
'<tr><td class="line" data-line="2"/>' +
|
||||
'<td class="code"><span class="hl-source hl-js hl-react">' +
|
||||
'<span class="hl-meta hl-function-call hl-method hl-js">' +
|
||||
'<span class="hl-support hl-type hl-object hl-console hl-js test-console-token">console</span>' +
|
||||
'<span class="hl-punctuation hl-accessor hl-js">.</span>' +
|
||||
'<span class="hl-support hl-function hl-console hl-js test-log-token">log</span>' +
|
||||
'<span class="hl-meta hl-group hl-js"><span class="hl-punctuation hl-section hl-group hl-begin hl-js">(</span>' +
|
||||
'<span class="hl-meta hl-string hl-js"><span class="hl-string hl-quoted hl-double hl-js">' +
|
||||
'<span class="hl-punctuation hl-definition hl-string hl-begin hl-js">"</span>' +
|
||||
'Hello world' +
|
||||
'<span class="hl-punctuation hl-definition hl-string hl-end hl-js">"</span></span></span>' +
|
||||
'<span class="hl-punctuation hl-section hl-group hl-end hl-js">)</span></span>' +
|
||||
'\n</span></span></td></tr></tbody></table>',
|
||||
lsif: '',
|
||||
lineRanges: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
TreeEntries: () => createTreeEntriesResult(repositorySourcegraphUrl, files),
|
||||
ResolveRepoRev: () => createResolveRepoRevisionResult(repositorySourcegraphUrl, commitID),
|
||||
HighlightedFile: ({ filePath }) => ({
|
||||
repository: {
|
||||
commit: {
|
||||
file: {
|
||||
isDirectory: false,
|
||||
richHTML: '',
|
||||
highlight: {
|
||||
aborted: false,
|
||||
html:
|
||||
'<table><tbody><tr><td class="line" data-line="1"/><td class="code">' +
|
||||
'<span class="hl-source hl-js hl-react"><span class="hl-comment hl-line hl-double-slash hl-js">' +
|
||||
'<span class="hl-punctuation hl-definition hl-comment hl-js">//</span>' +
|
||||
` file path: ${filePath}\n` +
|
||||
'</span></span></td></tr>' +
|
||||
'<tr><td class="line" data-line="2"/>' +
|
||||
'<td class="code"><span class="hl-source hl-js hl-react">' +
|
||||
'<span class="hl-meta hl-function-call hl-method hl-js">' +
|
||||
'<span class="hl-support hl-type hl-object hl-console hl-js test-console-token">console</span>' +
|
||||
'<span class="hl-punctuation hl-accessor hl-js">.</span>' +
|
||||
'<span class="hl-support hl-function hl-console hl-js test-log-token">log</span>' +
|
||||
'<span class="hl-meta hl-group hl-js"><span class="hl-punctuation hl-section hl-group hl-begin hl-js">(</span>' +
|
||||
'<span class="hl-meta hl-string hl-js"><span class="hl-string hl-quoted hl-double hl-js">' +
|
||||
'<span class="hl-punctuation hl-definition hl-string hl-begin hl-js">"</span>' +
|
||||
'Hello world' +
|
||||
'<span class="hl-punctuation hl-definition hl-string hl-end hl-js">"</span></span></span>' +
|
||||
'<span class="hl-punctuation hl-section hl-group hl-end hl-js">)</span></span>' +
|
||||
'\n</span></span></td></tr></tbody></table>',
|
||||
lineRanges: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
FetchCommits: () => ({
|
||||
node: { __typename: 'GitCommit' },
|
||||
}),
|
||||
// Required for definition provider,
|
||||
ResolveRawRepoName: () => ({
|
||||
repository: {
|
||||
mirrorInfo: {
|
||||
cloned: true,
|
||||
},
|
||||
uri: repositoryName,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
// Serve a mock extension bundle with a simple reference provider
|
||||
testContext.server
|
||||
.get(new URL(extensionManifest.url, driver.sourcegraphBaseUrl).href)
|
||||
.intercept((request, response) => {
|
||||
function extensionBundle(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
||||
const sourcegraph = require('sourcegraph') as typeof import('sourcegraph')
|
||||
|
||||
function activate(context: ExtensionContext): void {
|
||||
context.subscriptions.add(
|
||||
sourcegraph.languages.registerReferenceProvider(['*'], {
|
||||
provideReferences: () => [
|
||||
new sourcegraph.Location(
|
||||
new URL('git://github.com/sourcegraph/test%20repo?1234#test%20spaces.ts'),
|
||||
new sourcegraph.Range(
|
||||
new sourcegraph.Position(0, 0),
|
||||
new sourcegraph.Position(1, 0)
|
||||
)
|
||||
),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
// We aren't testing definition providers in this test; we include a definition provider
|
||||
// because the "Find references" action isn't displayed unless a definition is found
|
||||
context.subscriptions.add(
|
||||
sourcegraph.languages.registerDefinitionProvider(['*'], {
|
||||
provideDefinition: () =>
|
||||
new sourcegraph.Location(
|
||||
new URL('git://github.com/sourcegraph/test%20repo?1234#test%20spaces.ts'),
|
||||
new sourcegraph.Range(
|
||||
new sourcegraph.Position(0, 0),
|
||||
new sourcegraph.Position(1, 0)
|
||||
)
|
||||
),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
exports.activate = activate
|
||||
}
|
||||
// Create an immediately-invoked function expression for the extensionBundle function
|
||||
const extensionBundleString = `(${extensionBundle.toString()})()`
|
||||
response.type('application/javascript; charset=utf-8').send(extensionBundleString)
|
||||
})
|
||||
|
||||
// TEMPORARY: Mock `Date.now` to prevent temporary Firefox from rendering.
|
||||
await driver.page.evaluateOnNewDocument(() => {
|
||||
// Number of ms between Unix epoch and July 1, 2020 (outside of Firefox campaign range)
|
||||
const mockMs = new Date('July 1, 2020 00:00:00 UTC').getTime()
|
||||
Date.now = () => mockMs
|
||||
})
|
||||
|
||||
await driver.page.goto(`${driver.sourcegraphBaseUrl}/${repositoryName}/-/blob/test.ts`)
|
||||
|
||||
// Click on "log" in "console.log()" in line 2
|
||||
await driver.page.waitForSelector('.test-log-token', { visible: true })
|
||||
await driver.page.click('.test-log-token')
|
||||
|
||||
// Click 'Find references'
|
||||
await driver.page.waitForSelector('.test-tooltip-find-references', { visible: true })
|
||||
await driver.page.click('.test-tooltip-find-references')
|
||||
|
||||
await driver.page.waitForSelector('.test-file-match-children-item', { visible: true })
|
||||
|
||||
await percySnapshotWithVariants(driver.page, 'Blob Reference Panel', { waitForCodeHighlighting: true })
|
||||
await accessibilityAudit(driver.page)
|
||||
// Click on the first reference
|
||||
await driver.page.click('.test-file-match-children-item')
|
||||
|
||||
// Assert that the first line of code has text content which contains: 'file path: test spaces.ts'
|
||||
try {
|
||||
await driver.page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.querySelector('[data-testid="repo-blob"] [data-line="1"]')
|
||||
?.nextElementSibling?.textContent?.includes('file path: test spaces.ts'),
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
} catch {
|
||||
throw new Error('Expected to navigate to file after clicking on link in references panel')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,11 +2,8 @@ import assert from 'assert'
|
||||
|
||||
import { ElementHandle, MouseButton } from 'puppeteer'
|
||||
|
||||
import type { ExtensionContext } from '@sourcegraph/shared/src/codeintel/legacy-extensions/api'
|
||||
import { JsonDocument, SyntaxKind } from '@sourcegraph/shared/src/codeintel/scip'
|
||||
import { SharedGraphQlOperations } from '@sourcegraph/shared/src/graphql-operations'
|
||||
import { ExtensionManifest } from '@sourcegraph/shared/src/schema/extensionSchema'
|
||||
import { Settings } from '@sourcegraph/shared/src/schema/settings.schema'
|
||||
import { Driver, createDriverForTest, percySnapshot } from '@sourcegraph/shared/src/testing/driver'
|
||||
import { afterEachSaveScreenshotIfFailed } from '@sourcegraph/shared/src/testing/screenshotReporter'
|
||||
|
||||
@ -284,136 +281,6 @@ describe('CodeMirror blob view', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Describes the ways the blob viewer can be extended through Sourcegraph extensions.
|
||||
describe('extensibility', () => {
|
||||
beforeEach(() => {
|
||||
testContext.overrideJsContext({ enableLegacyExtensions: true })
|
||||
})
|
||||
|
||||
describe('hovercards', () => {
|
||||
beforeEach(() => {
|
||||
const {
|
||||
graphqlResults: extensionGraphQlResult,
|
||||
intercept,
|
||||
userSettings,
|
||||
} = createExtensionData([
|
||||
{
|
||||
id: 'test',
|
||||
extensionID: 'test/test',
|
||||
extensionManifest: {
|
||||
url: new URL(
|
||||
'/-/static/extension/0001-test-test.js?hash--test-test',
|
||||
driver.sourcegraphBaseUrl
|
||||
).href,
|
||||
activationEvents: ['*'],
|
||||
},
|
||||
bundle: function extensionBundle(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
||||
const sourcegraph = require('sourcegraph') as typeof import('sourcegraph')
|
||||
|
||||
function activate(context: ExtensionContext): void {
|
||||
context.subscriptions.add(
|
||||
sourcegraph.languages.registerHoverProvider([{ language: 'typescript' }], {
|
||||
provideHover: () => ({
|
||||
contents: {
|
||||
kind: sourcegraph.MarkupKind.Markdown,
|
||||
value: 'Test hover content',
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
exports.activate = activate
|
||||
},
|
||||
},
|
||||
])
|
||||
testContext.overrideGraphQL({
|
||||
...commonBlobGraphQlResults,
|
||||
...createViewerSettingsGraphQLOverride({
|
||||
user: {
|
||||
...userSettings,
|
||||
experimentalFeatures: {
|
||||
enableCodeMirrorFileView: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
...extensionGraphQlResult,
|
||||
})
|
||||
|
||||
// Serve a mock extension bundle with a simple hover provider
|
||||
intercept(testContext, driver)
|
||||
})
|
||||
|
||||
it('shows a hover overlay from a hover provider when a token is hovered', async () => {
|
||||
await driver.page.goto(`${driver.sourcegraphBaseUrl}${filePaths['test.ts']}`)
|
||||
await waitForView()
|
||||
await driver.page.hover(wordSelector)
|
||||
await driver.page.waitForSelector('.cm-code-intel-hovercard')
|
||||
assert.strictEqual(
|
||||
await driver.page.evaluate(
|
||||
(): string =>
|
||||
document.querySelector('[data-testid="hover-overlay-contents"]')?.textContent?.trim() ?? ''
|
||||
),
|
||||
'Test hover content',
|
||||
'hovercard is visible with correct content'
|
||||
)
|
||||
|
||||
await driver.page.hover(lineAt(5))
|
||||
try {
|
||||
await driver.page.waitForSelector('.cm-code-intel-hovercard', { hidden: true })
|
||||
} catch {
|
||||
throw new Error('Timeout waiting for hovercard to disappear')
|
||||
}
|
||||
})
|
||||
|
||||
it('pins a hovercard and unpins hovercards', async () => {
|
||||
await driver.page.goto(`${driver.sourcegraphBaseUrl}${filePaths['test.ts']}`)
|
||||
await waitForView()
|
||||
await driver.page.hover(wordSelector)
|
||||
await driver.page.waitForSelector('.cm-code-intel-hovercard [data-testid="hover-copy-link"]')
|
||||
|
||||
await driver.page.click('.cm-code-intel-hovercard [data-testid="hover-copy-link"]')
|
||||
|
||||
// URL gets updated
|
||||
await driver.assertWindowLocation(`${filePaths['test.ts']}?L1:1&popover=pinned`)
|
||||
|
||||
// Close button is visible
|
||||
await driver.page.waitForSelector('.cm-code-intel-hovercard [aria-label="Close"]')
|
||||
|
||||
// Hovercard stay open when moving the mouse away
|
||||
await driver.page.hover(lineAt(5))
|
||||
await driver.page.waitForSelector('.cm-code-intel-hovercard')
|
||||
|
||||
// Closes hovercard when clicking on another line
|
||||
await driver.page.click(lineAt(5))
|
||||
try {
|
||||
await driver.page.waitForSelector('.cm-code-intel-hovercard', { hidden: true })
|
||||
} catch {
|
||||
throw new Error('Timeout waiting for hovercard to close after selecting another line')
|
||||
}
|
||||
|
||||
// Opens pinned hovecard when navigating back
|
||||
await driver.page.goBack()
|
||||
await driver.page.waitForSelector('.cm-code-intel-hovercard')
|
||||
|
||||
// Closes hover card when clicking the close button
|
||||
await driver.page.click('.cm-code-intel-hovercard [aria-label="Close"]')
|
||||
try {
|
||||
await driver.page.waitForSelector('.cm-code-intel-hovercard', { hidden: true })
|
||||
} catch {
|
||||
throw new Error('Timeout waiting for hovercard to close after clicking close button')
|
||||
}
|
||||
})
|
||||
|
||||
it('opens a pinned hovercard on page load', async () => {
|
||||
await driver.page.goto(`${driver.sourcegraphBaseUrl}${filePaths['test.ts']}?L1:1&popover=pinned`)
|
||||
await waitForView()
|
||||
await driver.page.waitForSelector('.cm-code-intel-hovercard')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('in-document search', () => {
|
||||
const { graphqlResults: blobGraphqlResults, filePaths } = createBlobPageData({
|
||||
repoName,
|
||||
@ -578,54 +445,3 @@ function createBlobPageData<T extends BlobInfo>({
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface MockExtension {
|
||||
id: string
|
||||
extensionID: string
|
||||
extensionManifest: ExtensionManifest
|
||||
/**
|
||||
* A function whose body is a Sourcegraph extension.
|
||||
*
|
||||
* Bundle must import 'sourcegraph' (e.g. `const sourcegraph = require('sourcegraph')`)
|
||||
* */
|
||||
bundle: () => void
|
||||
}
|
||||
|
||||
function createExtensionData(extensions: MockExtension[]): {
|
||||
intercept: (testContext: WebIntegrationTestContext, driver: Driver) => void
|
||||
graphqlResults: Pick<SharedGraphQlOperations, 'Extensions'>
|
||||
userSettings: Required<Pick<Settings, 'extensions'>>
|
||||
} {
|
||||
return {
|
||||
intercept(testContext: WebIntegrationTestContext, driver: Driver) {
|
||||
for (const extension of extensions) {
|
||||
testContext.server
|
||||
.get(new URL(extension.extensionManifest.url, driver.sourcegraphBaseUrl).href)
|
||||
.intercept((_request, response) => {
|
||||
// Create an immediately-invoked function expression for the extensionBundle function
|
||||
const extensionBundleString = `(${extension.bundle.toString()})()`
|
||||
response.type('application/javascript; charset=utf-8').send(extensionBundleString)
|
||||
})
|
||||
}
|
||||
},
|
||||
graphqlResults: {
|
||||
Extensions: () => ({
|
||||
extensionRegistry: {
|
||||
__typename: 'ExtensionRegistry',
|
||||
extensions: {
|
||||
nodes: extensions.map(extension => ({
|
||||
...extension,
|
||||
manifest: { jsonFields: extension.extensionManifest },
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
userSettings: {
|
||||
extensions: extensions.reduce((extensionsSettings: Record<string, boolean>, mockExtension) => {
|
||||
extensionsSettings[mockExtension.extensionID] = true
|
||||
return extensionsSettings
|
||||
}, {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,5 +49,4 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st
|
||||
xhrHeaders: {},
|
||||
authProviders: [builtinAuthProvider],
|
||||
authMinPasswordLength: 12,
|
||||
enableLegacyExtensions: false,
|
||||
})
|
||||
|
||||
@ -201,9 +201,6 @@ export interface SourcegraphContext extends Pick<Required<SiteConfiguration>, 'e
|
||||
/** Whether the product research sign-up page is enabled on the site. */
|
||||
productResearchPageEnabled: boolean
|
||||
|
||||
/** Whether the use of extensions are enabled. (Doesn't affect code intel and git extras.) */
|
||||
enableLegacyExtensions?: boolean
|
||||
|
||||
/** Contains information about the product license. */
|
||||
licenseInfo?: {
|
||||
currentPlan: 'old-starter-0' | 'old-enterprise-0' | 'team-0' | 'enterprise-0' | 'business-0' | 'enterprise-1'
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
import { extensionsController } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
import { Grid, H3 } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../auth'
|
||||
@ -21,7 +20,6 @@ const defaultProps: GlobalNavbarProps = {
|
||||
final: null,
|
||||
subjects: null,
|
||||
},
|
||||
extensionsController,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
globbing: false,
|
||||
platformContext: {} as any,
|
||||
@ -49,7 +47,6 @@ const allNavItemsProps: Partial<GlobalNavbarProps> = {
|
||||
batchChangesExecutionEnabled: true,
|
||||
batchChangesWebhookLogsEnabled: true,
|
||||
codeInsightsEnabled: true,
|
||||
enableLegacyExtensions: true,
|
||||
}
|
||||
|
||||
const allAuthenticatedNavItemsProps: Partial<GlobalNavbarProps> = {
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
import { extensionsController, NOOP_SETTINGS_CASCADE } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
import { NOOP_SETTINGS_CASCADE } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
|
||||
|
||||
import { GlobalNavbar } from './GlobalNavbar'
|
||||
@ -15,7 +15,6 @@ jest.mock('../components/branding/BrandLogo', () => ({ BrandLogo: 'BrandLogo' })
|
||||
|
||||
const PROPS: React.ComponentProps<typeof GlobalNavbar> = {
|
||||
authenticatedUser: null,
|
||||
extensionsController,
|
||||
isSourcegraphDotCom: false,
|
||||
isSourcegraphApp: false,
|
||||
platformContext: {} as any,
|
||||
@ -41,16 +40,6 @@ const PROPS: React.ComponentProps<typeof GlobalNavbar> = {
|
||||
}
|
||||
|
||||
describe('GlobalNavbar', () => {
|
||||
const origContext = window.context
|
||||
beforeEach(() => {
|
||||
window.context = {
|
||||
enableLegacyExtensions: false,
|
||||
} as any
|
||||
})
|
||||
afterEach(() => {
|
||||
window.context = origContext
|
||||
})
|
||||
|
||||
test('default', () => {
|
||||
const { asFragment } = renderWithBrandedContext(
|
||||
<MockedTestProvider>
|
||||
|
||||
@ -6,9 +6,7 @@ import BookOutlineIcon from 'mdi-react/BookOutlineIcon'
|
||||
import MagnifyIcon from 'mdi-react/MagnifyIcon'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { ContributableMenu } from '@sourcegraph/client-api'
|
||||
import { isErrorLike, isMacPlatform } from '@sourcegraph/common'
|
||||
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
|
||||
import { shortcutDisplayName } from '@sourcegraph/shared/src/keyboardShortcuts'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
import { Settings } from '@sourcegraph/shared/src/schema/settings.schema'
|
||||
@ -27,7 +25,6 @@ import { CodeMonitoringProps } from '../codeMonitoring'
|
||||
import { CodyIcon } from '../cody/CodyIcon'
|
||||
import { BrandLogo } from '../components/branding/BrandLogo'
|
||||
import { getFuzzyFinderFeatureFlags } from '../components/fuzzyFinder/FuzzyFinderFeatureFlag'
|
||||
import { WebCommandListPopoverButton } from '../components/shared'
|
||||
import { useFeatureFlag } from '../featureFlags/useFeatureFlag'
|
||||
import { useRoutesMatch } from '../hooks'
|
||||
import { CodeInsightsProps } from '../insights/types'
|
||||
@ -50,7 +47,6 @@ import styles from './GlobalNavbar.module.scss'
|
||||
export interface GlobalNavbarProps
|
||||
extends SettingsCascadeProps<Settings>,
|
||||
PlatformContextProps,
|
||||
ExtensionsControllerProps,
|
||||
TelemetryProps,
|
||||
SearchContextInputProps,
|
||||
CodeInsightsProps,
|
||||
@ -67,7 +63,6 @@ export interface GlobalNavbarProps
|
||||
globbing: boolean
|
||||
isSearchAutoFocusRequired?: boolean
|
||||
isRepositoryRelatedPage?: boolean
|
||||
enableLegacyExtensions?: boolean
|
||||
branding?: typeof window.context.branding
|
||||
showKeyboardShortcutsHelp: () => void
|
||||
showFeedbackModal: () => void
|
||||
@ -132,8 +127,6 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
|
||||
searchContextsEnabled,
|
||||
codeMonitoringEnabled,
|
||||
notebooksEnabled,
|
||||
extensionsController,
|
||||
enableLegacyExtensions,
|
||||
showFeedbackModal,
|
||||
...props
|
||||
}) => {
|
||||
@ -293,16 +286,6 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
|
||||
</NavAction>
|
||||
)}
|
||||
{fuzzyFinderNavbar && FuzzyFinderNavItem(props.setFuzzyFinderIsVisible)}
|
||||
{props.authenticatedUser && extensionsController !== null && enableLegacyExtensions && (
|
||||
<NavAction>
|
||||
<WebCommandListPopoverButton
|
||||
{...props}
|
||||
extensionsController={extensionsController}
|
||||
location={location}
|
||||
menu={ContributableMenu.CommandPalette}
|
||||
/>
|
||||
</NavAction>
|
||||
)}
|
||||
{props.authenticatedUser?.siteAdmin && (
|
||||
<NavAction>
|
||||
<StatusMessagesNavItem />
|
||||
@ -325,7 +308,7 @@ export const GlobalNavbar: React.FunctionComponent<React.PropsWithChildren<Globa
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
{!isSourcegraphDotCom && window.context.allowSignup && (
|
||||
{!isSourcegraphDotCom && window.context?.allowSignup && (
|
||||
<ButtonLink to="/sign-up" variant="primary" size="sm">
|
||||
Sign up
|
||||
</ButtonLink>
|
||||
|
||||
@ -71,10 +71,10 @@ export function createPlatformContext(): PlatformContext {
|
||||
},
|
||||
getGraphQLClient: getWebGraphQLClient,
|
||||
requestGraphQL: ({ request, variables }) => requestGraphQL(request, variables),
|
||||
createExtensionHost: async () =>
|
||||
(await import('@sourcegraph/shared/src/api/extension/worker')).createExtensionHost(),
|
||||
createExtensionHost: () => {
|
||||
throw new Error('extensions are no longer supported in the web app')
|
||||
},
|
||||
urlToFile: toPrettyWebBlobURL,
|
||||
getScriptURLForExtension: () => undefined,
|
||||
sourcegraphURL: window.context.externalURL,
|
||||
clientApplication: 'sourcegraph',
|
||||
telemetryService: eventLogger,
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { FC, Suspense, useEffect, useMemo, useState } from 'react'
|
||||
import { mdiSourceRepository } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
import { escapeRegExp } from 'lodash'
|
||||
import { matchPath, Location, useLocation, Route, Routes } from 'react-router-dom'
|
||||
import { Location, useLocation, Route, Routes } from 'react-router-dom'
|
||||
import { NEVER, of } from 'rxjs'
|
||||
import { catchError, switchMap } from 'rxjs/operators'
|
||||
|
||||
@ -33,7 +33,6 @@ import { CodeIntelligenceProps } from '../codeintel'
|
||||
import { BreadcrumbSetters, BreadcrumbsProps } from '../components/Breadcrumbs'
|
||||
import { ErrorBoundary } from '../components/ErrorBoundary'
|
||||
import { HeroPage } from '../components/HeroPage'
|
||||
import { ActionItemsBarProps, useWebActionItems } from '../extensions/components/ActionItemsBar'
|
||||
import { ExternalLinkFields, RepositoryFields } from '../graphql-operations'
|
||||
import { CodeInsightsProps } from '../insights/types'
|
||||
import { NotebookProps } from '../notebooks'
|
||||
@ -45,7 +44,6 @@ import { parseBrowserRepoURL } from '../util/url'
|
||||
import { GoToCodeHostAction } from './actions/GoToCodeHostAction'
|
||||
import { fetchFileExternalLinks, ResolvedRevision, resolveRepoRevision } from './backend'
|
||||
import { RepoContainerError } from './RepoContainerError'
|
||||
import { compareSpecPath } from './repoContainerRoutes'
|
||||
import { RepoHeader, RepoHeaderActionButton, RepoHeaderContributionsLifecycleProps } from './RepoHeader'
|
||||
import { RepoHeaderContributionPortal } from './RepoHeaderContributionPortal'
|
||||
import {
|
||||
@ -53,7 +51,7 @@ import {
|
||||
RepoRevisionContainerContext,
|
||||
RepoRevisionContainerRoute,
|
||||
} from './RepoRevisionContainer'
|
||||
import { commitsPath, repoSplat } from './repoRevisionContainerRoutes'
|
||||
import { repoSplat } from './repoRevisionContainerRoutes'
|
||||
import { RepoSettingsAreaRoute } from './settings/RepoSettingsArea'
|
||||
import { RepoSettingsSideBarGroup } from './settings/RepoSettingsSidebar'
|
||||
import { repoSettingsAreaPath } from './settings/routes'
|
||||
@ -74,7 +72,6 @@ export interface RepoContainerContext
|
||||
TelemetryProps,
|
||||
Pick<SearchContextProps, 'selectedSearchContextSpec' | 'searchContextsEnabled'>,
|
||||
BreadcrumbSetters,
|
||||
ActionItemsBarProps,
|
||||
SearchStreamingProps,
|
||||
Pick<StreamingSearchResultsListProps, 'fetchHighlightedFileLineRanges'>,
|
||||
CodeIntelligenceProps,
|
||||
@ -273,23 +270,6 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
|
||||
})
|
||||
}, [revision, filePath, repoName, onNavbarQueryChange, globbing])
|
||||
|
||||
const { useActionItemsBar, useActionItemsToggle } = useWebActionItems()
|
||||
|
||||
// render go to the code host action on all the repo container routes and on all compare spec routes
|
||||
const isGoToCodeHostActionVisible = useMemo(() => {
|
||||
if (!window.context.enableLegacyExtensions) {
|
||||
return true
|
||||
}
|
||||
const paths = [
|
||||
...repoContainerRoutes.map(route => route.path),
|
||||
compareSpecPath,
|
||||
repoSettingsAreaPath,
|
||||
commitsPath,
|
||||
]
|
||||
|
||||
return paths.some(path => matchPath(path, location.pathname))
|
||||
}, [repoContainerRoutes, location.pathname])
|
||||
|
||||
const isError = isErrorLike(repoOrError) || isErrorLike(resolvedRevisionOrError)
|
||||
|
||||
// if revision for given repo does not resolve then we still proceed to render settings routes
|
||||
@ -327,7 +307,6 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
|
||||
repoName,
|
||||
revision: revision || '',
|
||||
resolvedRevision,
|
||||
useActionItemsBar,
|
||||
}
|
||||
|
||||
const perforceCodeHostUrlToSwarmUrlMap =
|
||||
@ -347,7 +326,6 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
|
||||
<div className={classNames('w-100 d-flex flex-column', styles.repoContainer)}>
|
||||
<RepoHeader
|
||||
actionButtons={props.repoHeaderActionButtons}
|
||||
useActionItemsToggle={useActionItemsToggle}
|
||||
breadcrumbs={props.breadcrumbs}
|
||||
repoName={repoName}
|
||||
revision={revision}
|
||||
@ -355,36 +333,33 @@ export const RepoContainer: FC<RepoContainerProps> = props => {
|
||||
settingsCascade={props.settingsCascade}
|
||||
authenticatedUser={authenticatedUser}
|
||||
platformContext={props.platformContext}
|
||||
extensionsController={extensionsController}
|
||||
telemetryService={props.telemetryService}
|
||||
/>
|
||||
{isGoToCodeHostActionVisible && (
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={2}
|
||||
id="go-to-code-host"
|
||||
{...repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{({ actionType }) => (
|
||||
<GoToCodeHostAction
|
||||
repo={repo}
|
||||
repoName={repoName}
|
||||
// We need a revision to generate code host URLs, if revision isn't available, we use the default branch or HEAD.
|
||||
revision={rawRevision || repo?.defaultBranch?.displayName || 'HEAD'}
|
||||
filePath={filePath}
|
||||
commitRange={commitRange}
|
||||
range={range}
|
||||
position={position}
|
||||
perforceCodeHostUrlToSwarmUrlMap={perforceCodeHostUrlToSwarmUrlMap}
|
||||
fetchFileExternalLinks={fetchFileExternalLinks}
|
||||
actionType={actionType}
|
||||
source="repoHeader"
|
||||
key="go-to-code-host"
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
)}
|
||||
</RepoHeaderContributionPortal>
|
||||
)}
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={2}
|
||||
id="go-to-code-host"
|
||||
{...repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{({ actionType }) => (
|
||||
<GoToCodeHostAction
|
||||
repo={repo}
|
||||
repoName={repoName}
|
||||
// We need a revision to generate code host URLs, if revision isn't available, we use the default branch or HEAD.
|
||||
revision={rawRevision || repo?.defaultBranch?.displayName || 'HEAD'}
|
||||
filePath={filePath}
|
||||
commitRange={commitRange}
|
||||
range={range}
|
||||
position={position}
|
||||
perforceCodeHostUrlToSwarmUrlMap={perforceCodeHostUrlToSwarmUrlMap}
|
||||
fetchFileExternalLinks={fetchFileExternalLinks}
|
||||
actionType={actionType}
|
||||
source="repoHeader"
|
||||
key="go-to-code-host"
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
)}
|
||||
</RepoHeaderContributionPortal>
|
||||
|
||||
{isCodeIntelRepositoryBadgeVisible && (
|
||||
<RepoHeaderContributionPortal
|
||||
|
||||
@ -8,7 +8,6 @@ import { Button, H1, H2, Icon, Link } from '@sourcegraph/wildcard'
|
||||
import { BrandedStory } from '@sourcegraph/wildcard/src/stories'
|
||||
|
||||
import { AuthenticatedUser } from '../auth'
|
||||
import { SourcegraphContext } from '../jscontext'
|
||||
|
||||
import { GoToPermalinkAction } from './actions/GoToPermalinkAction'
|
||||
import { FilePathBreadcrumbs } from './FilePathBreadcrumbs'
|
||||
@ -25,10 +24,6 @@ const mockUser = {
|
||||
siteAdmin: true,
|
||||
} as AuthenticatedUser
|
||||
|
||||
if (!window.context) {
|
||||
window.context = { enableLegacyExtensions: false } as SourcegraphContext & Mocha.SuiteFunction
|
||||
}
|
||||
|
||||
const decorator: DecoratorFn = story => (
|
||||
<BrandedStory initialEntries={['/github.com/sourcegraph/sourcegraph/-/tree/']} styles={webStyles}>
|
||||
{() => <div className="container mt-3">{story()}</div>}
|
||||
@ -70,12 +65,6 @@ export const Default: Story = () => (
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
const useActionItemsToggle = () => ({
|
||||
isOpen: false,
|
||||
toggle: () => null,
|
||||
toggleReference: () => null,
|
||||
barInPage: false,
|
||||
})
|
||||
|
||||
const onLifecyclePropsChange = (lifecycleProps: RepoHeaderContributionsLifecycleProps) => {
|
||||
lifecycleProps.repoHeaderContributionsLifecycleProps?.onRepoHeaderContributionAdd({
|
||||
@ -152,7 +141,6 @@ const createBreadcrumbs = (path: string) => [
|
||||
|
||||
const createProps = (path: string, forceWrap: boolean = false): React.ComponentProps<typeof RepoHeader> => ({
|
||||
actionButtons: [],
|
||||
useActionItemsToggle,
|
||||
breadcrumbs: createBreadcrumbs(path),
|
||||
repoName: 'sourcegraph/sourcegraph',
|
||||
revision: 'main',
|
||||
@ -160,7 +148,6 @@ const createProps = (path: string, forceWrap: boolean = false): React.ComponentP
|
||||
settingsCascade: EMPTY_SETTINGS_CASCADE,
|
||||
authenticatedUser: mockUser,
|
||||
platformContext: {} as any,
|
||||
extensionsController: null,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
forceWrap,
|
||||
})
|
||||
|
||||
@ -7,13 +7,11 @@ import { useLocation } from 'react-router-dom'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
import { SettingsCascadeOrError } from '@sourcegraph/shared/src/settings/settings'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
|
||||
import { Menu, MenuList, Position, Icon } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../auth'
|
||||
import { Breadcrumbs, BreadcrumbsProps } from '../components/Breadcrumbs'
|
||||
import { ErrorBoundary } from '../components/ErrorBoundary'
|
||||
import type { ActionItemsToggleProps } from '../extensions/components/ActionItemsBar'
|
||||
import { ActionButtonDescriptor } from '../util/contributions'
|
||||
import { useBreakpoint } from '../util/dom'
|
||||
|
||||
@ -21,8 +19,6 @@ import { RepoHeaderActionDropdownToggle } from './components/RepoHeaderActions'
|
||||
|
||||
import styles from './RepoHeader.module.scss'
|
||||
|
||||
const ActionItemsToggle = lazyComponent(() => import('../extensions/components/ActionItemsBar'), 'ActionItemsToggle')
|
||||
|
||||
/**
|
||||
* Stores the list of RepoHeaderContributions, manages addition/deletion, and ensures they are sorted.
|
||||
*
|
||||
@ -121,7 +117,7 @@ export interface RepoHeaderContext {
|
||||
|
||||
export interface RepoHeaderActionButton extends ActionButtonDescriptor<RepoHeaderContext> {}
|
||||
|
||||
interface Props extends PlatformContextProps, TelemetryProps, BreadcrumbsProps, ActionItemsToggleProps {
|
||||
interface Props extends PlatformContextProps, TelemetryProps, BreadcrumbsProps {
|
||||
/**
|
||||
* An array of render functions for action buttons that can be configured *in addition* to action buttons
|
||||
* contributed through {@link RepoHeaderContributionsLifecycleProps} and through extensions.
|
||||
@ -251,14 +247,6 @@ export const RepoHeader: React.FunctionComponent<React.PropsWithChildren<Props>>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{window.context.enableLegacyExtensions ? (
|
||||
<ul className="navbar-nav">
|
||||
<ActionItemsToggle
|
||||
useActionItemsToggle={props.useActionItemsToggle}
|
||||
extensionsController={props.extensionsController}
|
||||
/>
|
||||
</ul>
|
||||
) : null}
|
||||
</ErrorBoundary>
|
||||
</nav>
|
||||
)
|
||||
|
||||
@ -16,7 +16,6 @@ import { AuthenticatedUser } from '../auth'
|
||||
import { BatchChangesProps } from '../batches'
|
||||
import { CodeIntelligenceProps } from '../codeintel'
|
||||
import { BreadcrumbSetters } from '../components/Breadcrumbs'
|
||||
import { ActionItemsBarProps } from '../extensions/components/ActionItemsBar'
|
||||
import { RepositoryFields } from '../graphql-operations'
|
||||
import { CodeInsightsProps } from '../insights/types'
|
||||
import { NotebookProps } from '../notebooks'
|
||||
@ -49,7 +48,6 @@ export interface RepoRevisionContainerContext
|
||||
Pick<SearchContextProps, 'selectedSearchContextSpec' | 'searchContextsEnabled'>,
|
||||
RevisionSpec,
|
||||
BreadcrumbSetters,
|
||||
ActionItemsBarProps,
|
||||
SearchStreamingProps,
|
||||
Pick<StreamingSearchResultsListProps, 'fetchHighlightedFileLineRanges'>,
|
||||
BatchChangesProps,
|
||||
@ -81,7 +79,6 @@ interface RepoRevisionContainerProps
|
||||
Pick<SearchContextProps, 'selectedSearchContextSpec' | 'searchContextsEnabled'>,
|
||||
RevisionSpec,
|
||||
BreadcrumbSetters,
|
||||
ActionItemsBarProps,
|
||||
SearchStreamingProps,
|
||||
Pick<StreamingSearchResultsListProps, 'fetchHighlightedFileLineRanges'>,
|
||||
CodeIntelligenceProps,
|
||||
|
||||
@ -116,7 +116,6 @@ export const RepositoryFileTreePage: FC<RepositoryFileTreePageProps> = props =>
|
||||
globbing={globbing}
|
||||
repo={repo}
|
||||
repoName={repoName}
|
||||
useActionItemsBar={context.useActionItemsBar}
|
||||
isSourcegraphDotCom={context.isSourcegraphDotCom}
|
||||
className={styles.pageContent}
|
||||
/>
|
||||
|
||||
@ -363,38 +363,33 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, ..
|
||||
const alwaysRender = (
|
||||
<>
|
||||
<PageTitle title={getPageTitle()} />
|
||||
{!window.context.enableLegacyExtensions ? (
|
||||
<>
|
||||
{window.context.isAuthenticatedUser && (
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={112}
|
||||
id="open-in-editor-action"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{({ actionType }) => (
|
||||
<OpenInEditorActionItem
|
||||
platformContext={props.platformContext}
|
||||
externalServiceType={props.repoServiceType}
|
||||
actionType={actionType}
|
||||
source="repoHeader"
|
||||
/>
|
||||
)}
|
||||
</RepoHeaderContributionPortal>
|
||||
{window.context.isAuthenticatedUser && (
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={112}
|
||||
id="open-in-editor-action"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{({ actionType }) => (
|
||||
<OpenInEditorActionItem
|
||||
platformContext={props.platformContext}
|
||||
externalServiceType={props.repoServiceType}
|
||||
actionType={actionType}
|
||||
source="repoHeader"
|
||||
/>
|
||||
)}
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={111}
|
||||
id="toggle-blame-action"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{({ actionType }) => (
|
||||
<ToggleBlameAction actionType={actionType} source="repoHeader" renderMode={renderMode} />
|
||||
)}
|
||||
</RepoHeaderContributionPortal>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
</RepoHeaderContributionPortal>
|
||||
)}
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={111}
|
||||
id="toggle-blame-action"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{({ actionType }) => (
|
||||
<ToggleBlameAction actionType={actionType} source="repoHeader" renderMode={renderMode} />
|
||||
)}
|
||||
</RepoHeaderContributionPortal>
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={20}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
|
||||
|
||||
import { ActionItemsBarProps } from '../extensions/components/ActionItemsBar'
|
||||
|
||||
import { RepoRevisionWrapper } from './components/RepoRevision'
|
||||
import { RepoContainerRoute } from './RepoContainer'
|
||||
|
||||
@ -17,10 +15,6 @@ const RepositoryReleasesArea = lazyComponent(
|
||||
)
|
||||
const RepositoryCompareArea = lazyComponent(() => import('./compare/RepositoryCompareArea'), 'RepositoryCompareArea')
|
||||
const RepositoryStatsArea = lazyComponent(() => import('./stats/RepositoryStatsArea'), 'RepositoryStatsArea')
|
||||
const ActionItemsBar = lazyComponent<ActionItemsBarProps, 'ActionItemsBar'>(
|
||||
() => import('../extensions/components/ActionItemsBar'),
|
||||
'ActionItemsBar'
|
||||
)
|
||||
|
||||
export const compareSpecPath = '/-/compare/*'
|
||||
|
||||
@ -30,15 +24,6 @@ export const repoContainerRoutes: readonly RepoContainerRoute[] = [
|
||||
render: context => (
|
||||
<RepoRevisionWrapper>
|
||||
<RepositoryCommitPage {...context} />
|
||||
{window.context.enableLegacyExtensions && (
|
||||
<ActionItemsBar
|
||||
extensionsController={context.extensionsController}
|
||||
platformContext={context.platformContext}
|
||||
useActionItemsBar={context.useActionItemsBar}
|
||||
telemetryService={context.telemetryService}
|
||||
source="commit"
|
||||
/>
|
||||
)}
|
||||
</RepoRevisionWrapper>
|
||||
),
|
||||
},
|
||||
@ -55,15 +40,6 @@ export const repoContainerRoutes: readonly RepoContainerRoute[] = [
|
||||
render: context => (
|
||||
<RepoRevisionWrapper>
|
||||
<RepositoryCompareArea {...context} />
|
||||
{window.context.enableLegacyExtensions && (
|
||||
<ActionItemsBar
|
||||
extensionsController={context.extensionsController}
|
||||
platformContext={context.platformContext}
|
||||
useActionItemsBar={context.useActionItemsBar}
|
||||
telemetryService={context.telemetryService}
|
||||
source="compare"
|
||||
/>
|
||||
)}
|
||||
</RepoRevisionWrapper>
|
||||
),
|
||||
},
|
||||
|
||||
@ -2,19 +2,12 @@ import { TraceSpanProvider } from '@sourcegraph/observability-client'
|
||||
import { lazyComponent } from '@sourcegraph/shared/src/util/lazyComponent'
|
||||
import { LoadingSpinner } from '@sourcegraph/wildcard'
|
||||
|
||||
import { ActionItemsBarProps } from '../extensions/components/ActionItemsBar'
|
||||
|
||||
import { RepoRevisionContainerRoute } from './RepoRevisionContainer'
|
||||
|
||||
const RepositoryCommitsPage = lazyComponent(() => import('./commits/RepositoryCommitsPage'), 'RepositoryCommitsPage')
|
||||
|
||||
const RepositoryFileTreePage = lazyComponent(() => import('./RepositoryFileTreePage'), 'RepositoryFileTreePage')
|
||||
|
||||
const ActionItemsBar = lazyComponent<ActionItemsBarProps, 'ActionItemsBar'>(
|
||||
() => import('../extensions/components/ActionItemsBar'),
|
||||
'ActionItemsBar'
|
||||
)
|
||||
|
||||
// Work around the issue that react router can not match nested splats when the URL contains spaces
|
||||
// by expanding the repo matcher to an optional path of up to 10 segments.
|
||||
//
|
||||
@ -42,16 +35,6 @@ export const repoRevisionContainerRoutes: readonly RepoRevisionContainerRoute[]
|
||||
render: props => (
|
||||
<TraceSpanProvider name="RepositoryFileTreePage" attributes={{ objectType }}>
|
||||
<RepositoryFileTreePage {...props} objectType={objectType} />
|
||||
{window.context.enableLegacyExtensions && (
|
||||
<ActionItemsBar
|
||||
repo={props.repo}
|
||||
useActionItemsBar={props.useActionItemsBar}
|
||||
extensionsController={props.extensionsController}
|
||||
platformContext={props.platformContext}
|
||||
telemetryService={props.telemetryService}
|
||||
source={objectType === 'blob' ? 'blob' : undefined}
|
||||
/>
|
||||
)}
|
||||
</TraceSpanProvider>
|
||||
),
|
||||
})),
|
||||
|
||||
@ -35,7 +35,6 @@ import { RepoBatchChangesButton } from '../../batches/RepoBatchChangesButton'
|
||||
import { CodeIntelligenceProps } from '../../codeintel'
|
||||
import { BreadcrumbSetters } from '../../components/Breadcrumbs'
|
||||
import { PageTitle } from '../../components/PageTitle'
|
||||
import { ActionItemsBarProps } from '../../extensions/components/ActionItemsBar'
|
||||
import { RepositoryFields } from '../../graphql-operations'
|
||||
import { basename } from '../../util/path'
|
||||
import { FilePathBreadcrumbs } from '../FilePathBreadcrumbs'
|
||||
@ -60,7 +59,6 @@ interface Props
|
||||
commitID: string
|
||||
revision: string
|
||||
globbing: boolean
|
||||
useActionItemsBar: ActionItemsBarProps['useActionItemsBar']
|
||||
isSourcegraphDotCom: boolean
|
||||
className?: string
|
||||
}
|
||||
@ -85,7 +83,6 @@ export const TreePage: FC<Props> = ({
|
||||
useBreadcrumb,
|
||||
codeIntelligenceEnabled,
|
||||
batchChangesEnabled,
|
||||
useActionItemsBar,
|
||||
isSourcegraphDotCom,
|
||||
className,
|
||||
...props
|
||||
|
||||
@ -43,9 +43,7 @@ interface SearchConsolePageProps
|
||||
export const SearchConsolePage: React.FunctionComponent<React.PropsWithChildren<SearchConsolePageProps>> = props => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { globbing, streamSearch, extensionsController, isSourcegraphDotCom } = props
|
||||
const extensionHostAPI =
|
||||
extensionsController !== null && window.context.enableLegacyExtensions ? extensionsController.extHostAPI : null
|
||||
const { globbing, streamSearch, isSourcegraphDotCom } = props
|
||||
const enableGoImportsSearchQueryTransform = useExperimentalFeatures(
|
||||
features => features.enableGoImportsSearchQueryTransform
|
||||
)
|
||||
@ -72,11 +70,10 @@ export const SearchConsolePage: React.FunctionComponent<React.PropsWithChildren<
|
||||
|
||||
return transformSearchQuery({
|
||||
query,
|
||||
extensionHostAPIPromise: extensionHostAPI,
|
||||
enableGoImportsSearchQueryTransform,
|
||||
eventLogger,
|
||||
})
|
||||
}, [location.search, extensionHostAPI, enableGoImportsSearchQueryTransform])
|
||||
}, [location.search, enableGoImportsSearchQueryTransform])
|
||||
|
||||
const autocompletion = useMemo(
|
||||
() =>
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
import { extensionsController } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
import { MockedFeatureFlagsProvider } from '../../featureFlags/MockedFeatureFlagsProvider'
|
||||
@ -18,7 +17,6 @@ const defaultProps: SearchPageProps = {
|
||||
final: null,
|
||||
subjects: null,
|
||||
},
|
||||
extensionsController,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
authenticatedUser: null,
|
||||
globbing: false,
|
||||
|
||||
@ -4,7 +4,6 @@ import { mdiArrowRight } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { QueryExamples } from '@sourcegraph/branded/src/search-ui/components/QueryExamples'
|
||||
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
import { Settings } from '@sourcegraph/shared/src/schema/settings.schema'
|
||||
import { QueryState, SearchContextInputProps } from '@sourcegraph/shared/src/search'
|
||||
@ -30,7 +29,6 @@ import styles from './SearchPage.module.scss'
|
||||
export interface SearchPageProps
|
||||
extends SettingsCascadeProps<Settings>,
|
||||
TelemetryProps,
|
||||
ExtensionsControllerProps<'extHostAPI' | 'executeCommand'>,
|
||||
PlatformContextProps<'settings' | 'sourcegraphURL' | 'updateSettings' | 'requestGraphQL'>,
|
||||
SearchContextInputProps,
|
||||
CodeInsightsProps {
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import React, { createContext, Dispatch, SetStateAction, useContext, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { Remote } from 'comlink'
|
||||
import { isEqual } from 'lodash'
|
||||
import { useNavigationType, useLocation } from 'react-router-dom'
|
||||
import { merge, of } from 'rxjs'
|
||||
import { last, share, throttleTime } from 'rxjs/operators'
|
||||
|
||||
import { transformSearchQuery } from '@sourcegraph/shared/src/api/client/search'
|
||||
import { FlatExtensionHostAPI } from '@sourcegraph/shared/src/api/contract'
|
||||
import { AggregateStreamingSearchResults, StreamSearchOptions } from '@sourcegraph/shared/src/search/stream'
|
||||
import { TelemetryService } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { useObservable } from '@sourcegraph/wildcard'
|
||||
@ -39,7 +37,6 @@ export function useCachedSearchResults(
|
||||
streamSearch: SearchStreamingProps['streamSearch'],
|
||||
query: string,
|
||||
options: StreamSearchOptions,
|
||||
extensionHostAPI: Promise<Remote<FlatExtensionHostAPI>> | null,
|
||||
telemetryService: TelemetryService
|
||||
): AggregateStreamingSearchResults | undefined {
|
||||
const [cachedResults, setCachedResults] = useContext(SearchResultsCacheContext)
|
||||
@ -55,11 +52,10 @@ export function useCachedSearchResults(
|
||||
() =>
|
||||
transformSearchQuery({
|
||||
query,
|
||||
extensionHostAPIPromise: extensionHostAPI,
|
||||
enableGoImportsSearchQueryTransform,
|
||||
eventLogger,
|
||||
}),
|
||||
[query, extensionHostAPI, enableGoImportsSearchQueryTransform]
|
||||
[query, enableGoImportsSearchQueryTransform]
|
||||
)
|
||||
|
||||
const results = useObservable(
|
||||
|
||||
@ -3,7 +3,6 @@ import { NEVER } from 'rxjs'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
|
||||
import { extensionsController } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
|
||||
|
||||
import { SearchPatternType } from '../../graphql-operations'
|
||||
@ -11,7 +10,6 @@ import { SearchPatternType } from '../../graphql-operations'
|
||||
import { SearchResultsInfoBar, SearchResultsInfoBarProps } from './SearchResultsInfoBar'
|
||||
|
||||
const COMMON_PROPS: Omit<SearchResultsInfoBarProps, 'enableCodeMonitoring'> = {
|
||||
extensionsController,
|
||||
platformContext: { settings: NEVER, sourcegraphURL: 'https://sourcegraph.com' },
|
||||
authenticatedUser: {
|
||||
id: 'userID',
|
||||
@ -40,12 +38,6 @@ const renderSearchResultsInfoBar = (
|
||||
)
|
||||
|
||||
describe('SearchResultsInfoBar', () => {
|
||||
beforeAll(() => {
|
||||
window.context = {
|
||||
enableLegacyExtensions: false,
|
||||
} as any
|
||||
})
|
||||
|
||||
test('code monitoring feature flag disabled', () => {
|
||||
expect(
|
||||
renderSearchResultsInfoBar({ query: 'foo type:diff', enableCodeMonitoring: false }).asFragment()
|
||||
|
||||
@ -2,12 +2,7 @@ import React, { useMemo, useState } from 'react'
|
||||
|
||||
import { mdiChevronDoubleUp, mdiChevronDoubleDown } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { ContributableMenu } from '@sourcegraph/client-api'
|
||||
import { ActionItem } from '@sourcegraph/shared/src/actions/ActionItem'
|
||||
import { ActionsContainer } from '@sourcegraph/shared/src/actions/ActionsContainer'
|
||||
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
import { SearchPatternTypeProps, CaseSensitivityProps } from '@sourcegraph/shared/src/search'
|
||||
import { FilterKind, findFilter } from '@sourcegraph/shared/src/search/query/query'
|
||||
@ -29,8 +24,7 @@ import { SearchActionsMenu } from './SearchActionsMenu'
|
||||
import styles from './SearchResultsInfoBar.module.scss'
|
||||
|
||||
export interface SearchResultsInfoBarProps
|
||||
extends ExtensionsControllerProps<'executeCommand' | 'extHostAPI'>,
|
||||
TelemetryProps,
|
||||
extends TelemetryProps,
|
||||
PlatformContextProps<'settings' | 'sourcegraphURL'>,
|
||||
SearchPatternTypeProps,
|
||||
Pick<CaseSensitivityProps, 'caseSensitive'> {
|
||||
@ -74,7 +68,6 @@ export interface SearchResultsInfoBarProps
|
||||
export const SearchResultsInfoBar: React.FunctionComponent<
|
||||
React.PropsWithChildren<SearchResultsInfoBarProps>
|
||||
> = props => {
|
||||
const location = useLocation()
|
||||
const globalTypeFilter = useMemo(
|
||||
() => (props.query ? findFilter(props.query, 'type', FilterKind.Global)?.value?.value : undefined),
|
||||
[props.query]
|
||||
@ -106,7 +99,7 @@ export const SearchResultsInfoBar: React.FunctionComponent<
|
||||
)
|
||||
),
|
||||
getSearchContextCreateAction(props.query, props.authenticatedUser),
|
||||
getInsightsCreateAction(props.query, props.patternType, window.context.codeInsightsEnabled),
|
||||
getInsightsCreateAction(props.query, props.patternType, window.context?.codeInsightsEnabled),
|
||||
].filter((button): button is CreateAction => button !== null),
|
||||
[
|
||||
props.authenticatedUser,
|
||||
@ -125,15 +118,6 @@ export const SearchResultsInfoBar: React.FunctionComponent<
|
||||
[props.enableCodeMonitoring, props.patternType, props.query]
|
||||
)
|
||||
|
||||
const extraContext = useMemo(
|
||||
() => ({
|
||||
searchQuery: props.query || null,
|
||||
patternType: props.patternType,
|
||||
caseSensitive: props.caseSensitive,
|
||||
}),
|
||||
[props.query, props.patternType, props.caseSensitive]
|
||||
)
|
||||
|
||||
// Show/hide mobile filters menu
|
||||
const [showMobileFilters, setShowMobileFilters] = useState(false)
|
||||
const onShowMobileFiltersClicked = (): void => {
|
||||
@ -142,8 +126,6 @@ export const SearchResultsInfoBar: React.FunctionComponent<
|
||||
props.onShowMobileFiltersChanged?.(newShowFilters)
|
||||
}
|
||||
|
||||
const { extensionsController } = props
|
||||
|
||||
return (
|
||||
<aside
|
||||
role="region"
|
||||
@ -157,39 +139,6 @@ export const SearchResultsInfoBar: React.FunctionComponent<
|
||||
<div className={styles.expander} />
|
||||
|
||||
<ul className="nav align-items-center">
|
||||
{extensionsController !== null && window.context.enableLegacyExtensions ? (
|
||||
<ActionsContainer
|
||||
{...props}
|
||||
location={location}
|
||||
extensionsController={extensionsController}
|
||||
extraContext={extraContext}
|
||||
menu={ContributableMenu.SearchResultsToolbar}
|
||||
>
|
||||
{actionItems => (
|
||||
<>
|
||||
{actionItems.map(actionItem => (
|
||||
<ActionItem
|
||||
{...props}
|
||||
{...actionItem}
|
||||
location={location}
|
||||
extensionsController={extensionsController}
|
||||
key={actionItem.action.id}
|
||||
showLoadingSpinnerDuringExecution={false}
|
||||
className="mr-2 text-decoration-none"
|
||||
actionItemStyleProps={{
|
||||
actionItemVariant: 'secondary',
|
||||
actionItemSize: 'sm',
|
||||
actionItemOutline: true,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ActionsContainer>
|
||||
) : null}
|
||||
|
||||
<li className={styles.divider} aria-hidden="true" />
|
||||
|
||||
<SearchActionsMenu
|
||||
query={props.query}
|
||||
patternType={props.patternType}
|
||||
|
||||
@ -6,7 +6,6 @@ import { SearchQueryStateStoreProvider } from '@sourcegraph/shared/src/search'
|
||||
import { AggregateStreamingSearchResults } from '@sourcegraph/shared/src/search/stream'
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import {
|
||||
extensionsController,
|
||||
HIGHLIGHTED_FILE_LINES_LONG_REQUEST,
|
||||
MULTIPLE_SEARCH_RESULT,
|
||||
REPO_MATCH_RESULTS_WITH_METADATA,
|
||||
@ -33,7 +32,6 @@ const streamingSearchResult: AggregateStreamingSearchResults = {
|
||||
}
|
||||
|
||||
const defaultProps: StreamingSearchResultsProps = {
|
||||
extensionsController,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
|
||||
authenticatedUser: {
|
||||
|
||||
@ -13,7 +13,6 @@ import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/teleme
|
||||
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
|
||||
import {
|
||||
COLLAPSABLE_SEARCH_RESULT,
|
||||
extensionsController,
|
||||
HIGHLIGHTED_FILE_LINES_REQUEST,
|
||||
MULTIPLE_SEARCH_RESULT,
|
||||
REPO_MATCH_RESULT,
|
||||
@ -33,7 +32,6 @@ describe('StreamingSearchResults', () => {
|
||||
const streamingSearchResult = MULTIPLE_SEARCH_RESULT
|
||||
|
||||
const defaultProps: StreamingSearchResultsProps = {
|
||||
extensionsController,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
|
||||
authenticatedUser: null,
|
||||
@ -80,9 +78,6 @@ describe('StreamingSearchResults', () => {
|
||||
searchCaseSensitivity: false,
|
||||
searchQueryFromURL: 'r:golang/oauth2 test f:travis',
|
||||
})
|
||||
window.context = {
|
||||
enableLegacyExtensions: false,
|
||||
} as any
|
||||
})
|
||||
|
||||
it('should call streaming search API with the right parameters from URL', async () => {
|
||||
|
||||
@ -8,7 +8,6 @@ import { limitHit, StreamingProgress, StreamingSearchResultsList } from '@source
|
||||
import { asError } from '@sourcegraph/common'
|
||||
import { FetchFileParameters } from '@sourcegraph/shared/src/backend/file'
|
||||
import { FilePrefetcher } from '@sourcegraph/shared/src/components/PrefetchableFile'
|
||||
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
|
||||
import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
import { QueryUpdate, SearchContextProps } from '@sourcegraph/shared/src/search'
|
||||
@ -47,7 +46,6 @@ export interface StreamingSearchResultsProps
|
||||
extends SearchStreamingProps,
|
||||
Pick<SearchContextProps, 'selectedSearchContextSpec' | 'searchContextsEnabled'>,
|
||||
SettingsCascadeProps,
|
||||
ExtensionsControllerProps<'executeCommand' | 'extHostAPI'>,
|
||||
PlatformContextProps<'settings' | 'requestGraphQL' | 'sourcegraphURL'>,
|
||||
TelemetryProps,
|
||||
CodeInsightsProps,
|
||||
@ -64,7 +62,6 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
|
||||
authenticatedUser,
|
||||
telemetryService,
|
||||
isSourcegraphDotCom,
|
||||
extensionsController,
|
||||
searchAggregationEnabled,
|
||||
codeMonitoringEnabled,
|
||||
} = props
|
||||
@ -95,8 +92,6 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
|
||||
const [showMobileSidebar, setShowMobileSidebar] = useState(false)
|
||||
|
||||
// Derived state
|
||||
const extensionHostAPI =
|
||||
extensionsController !== null && window.context.enableLegacyExtensions ? extensionsController.extHostAPI : null
|
||||
const trace = useMemo(() => new URLSearchParams(location.search).get('trace') ?? undefined, [location.search])
|
||||
const featureOverrides = useDeepMemo(
|
||||
// Nested use memo here is used for avoiding extra object calculation step on each render
|
||||
@ -117,7 +112,7 @@ export const StreamingSearchResults: FC<StreamingSearchResultsProps> = props =>
|
||||
[caseSensitive, patternType, searchMode, trace, featureOverrides]
|
||||
)
|
||||
|
||||
const results = useCachedSearchResults(streamSearch, submittedURLQuery, options, extensionHostAPI, telemetryService)
|
||||
const results = useCachedSearchResults(streamSearch, submittedURLQuery, options, telemetryService)
|
||||
|
||||
// Log view event on first load
|
||||
useEffect(
|
||||
|
||||
@ -18,10 +18,6 @@ exports[`SearchResultsInfoBar code monitoring feature flag disabled 1`] = `
|
||||
<ul
|
||||
class="nav align-items-center"
|
||||
>
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class="divider"
|
||||
/>
|
||||
<li
|
||||
class="mr-2 navItem"
|
||||
>
|
||||
@ -96,10 +92,6 @@ exports[`SearchResultsInfoBar code monitoring feature flag enabled, can create m
|
||||
<ul
|
||||
class="nav align-items-center"
|
||||
>
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class="divider"
|
||||
/>
|
||||
<li
|
||||
class="mr-2 navItem"
|
||||
>
|
||||
@ -174,10 +166,6 @@ exports[`SearchResultsInfoBar code monitoring feature flag enabled, can create m
|
||||
<ul
|
||||
class="nav align-items-center"
|
||||
>
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class="divider"
|
||||
/>
|
||||
<li
|
||||
class="mr-2 navItem"
|
||||
>
|
||||
@ -252,10 +240,6 @@ exports[`SearchResultsInfoBar code monitoring feature flag enabled, cannot creat
|
||||
<ul
|
||||
class="nav align-items-center"
|
||||
>
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class="divider"
|
||||
/>
|
||||
<li
|
||||
class="mr-2 navItem"
|
||||
>
|
||||
@ -330,10 +314,6 @@ exports[`SearchResultsInfoBar unauthenticated user 1`] = `
|
||||
<ul
|
||||
class="nav align-items-center"
|
||||
>
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class="divider"
|
||||
/>
|
||||
<li
|
||||
class="mr-2 navItem"
|
||||
>
|
||||
|
||||
@ -265,14 +265,6 @@ export class MonacoSettingsEditor extends React.PureComponent<Props, State> {
|
||||
|
||||
function setDiagnosticsOptions(editor: typeof monaco, jsonSchema: JSONSchema | undefined): void {
|
||||
const schema = { ...settingsSchema, properties: { ...settingsSchema.properties } }
|
||||
if (!window.context.enableLegacyExtensions) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- we need to remove this key conditionally, but not from the schema
|
||||
// @ts-ignore
|
||||
delete schema.properties.extensions
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- we need to remove this key conditionally, but not from the schema
|
||||
// @ts-ignore
|
||||
delete schema.properties['extensions.activeLoggers']
|
||||
}
|
||||
editor.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
allowComments: true,
|
||||
|
||||
@ -2,26 +2,24 @@ import * as React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import AlertCircleIcon from 'mdi-react/AlertCircleIcon'
|
||||
import { combineLatest, Observable, of, Subject, Subscription } from 'rxjs'
|
||||
import { combineLatest, Observable, Subject, Subscription } from 'rxjs'
|
||||
import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'
|
||||
|
||||
import { asError, createAggregateError, ErrorLike, isErrorLike, logger } from '@sourcegraph/common'
|
||||
import { gql } from '@sourcegraph/http-client'
|
||||
import { extensionIDsFromSettings } from '@sourcegraph/shared/src/extensions/extension'
|
||||
import { queryConfiguredRegistryExtensions } from '@sourcegraph/shared/src/extensions/helpers'
|
||||
import { Scalars } from '@sourcegraph/shared/src/graphql-operations'
|
||||
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
|
||||
import { gqlToCascade, SettingsCascadeProps, SettingsSubject } from '@sourcegraph/shared/src/settings/settings'
|
||||
import { SettingsCascadeProps, SettingsSubject } from '@sourcegraph/shared/src/settings/settings'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { LoadingSpinner, PageHeader, ErrorMessage } from '@sourcegraph/wildcard'
|
||||
|
||||
import settingsSchemaJSON from '../../../../schema/settings.schema.json'
|
||||
import { AuthenticatedUser } from '../auth'
|
||||
import { queryGraphQL } from '../backend/graphql'
|
||||
import { HeroPage } from '../components/HeroPage'
|
||||
import { SettingsCascadeResult } from '../graphql-operations'
|
||||
import { eventLogger } from '../tracking/eventLogger'
|
||||
|
||||
import { mergeSettingsSchemas } from './configuration'
|
||||
import { SettingsPage } from './SettingsPage'
|
||||
|
||||
/** Props shared by SettingsArea and its sub-pages. */
|
||||
@ -92,11 +90,7 @@ export class SettingsArea extends React.Component<Props, State> {
|
||||
.pipe(
|
||||
switchMap(([{ id }]) =>
|
||||
fetchSettingsCascade(id).pipe(
|
||||
switchMap(cascade =>
|
||||
this.getMergedSettingsJSONSchema(cascade).pipe(
|
||||
map(settingsJSONSchema => ({ subjects: cascade.subjects, settingsJSONSchema }))
|
||||
)
|
||||
),
|
||||
map(cascade => ({ subjects: cascade.subjects, settingsJSONSchema: settingsSchemaJSON })),
|
||||
catchError(error => [asError(error)]),
|
||||
map(dataOrError => ({ dataOrError }))
|
||||
)
|
||||
@ -177,25 +171,6 @@ export class SettingsArea extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
private onUpdate = (): void => this.refreshRequests.next()
|
||||
|
||||
private getMergedSettingsJSONSchema(cascade: SettingsSubjects): Observable<{ $id: string }> {
|
||||
return queryConfiguredRegistryExtensions(
|
||||
this.props.platformContext,
|
||||
extensionIDsFromSettings(gqlToCascade(cascade))
|
||||
)
|
||||
.pipe(
|
||||
catchError(error => {
|
||||
logger.warn('Unable to get extension settings JSON Schemas for settings editor.', { error })
|
||||
return of([])
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
map(extensions => ({
|
||||
$id: 'mergedSettings.schema.json#',
|
||||
...mergeSettingsSchemas(extensions),
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function fetchSettingsCascade(subject: Scalars['ID']): Observable<SettingsSubjects> {
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
import settingsSchemaJSON from '../../../../schema/settings.schema.json'
|
||||
|
||||
import { mergeSettingsSchemas } from './configuration'
|
||||
|
||||
describe('mergeSettingsSchemas', () => {
|
||||
test('handles empty', () =>
|
||||
expect(mergeSettingsSchemas([])).toEqual({
|
||||
allOf: [{ $ref: settingsSchemaJSON.$id }],
|
||||
}))
|
||||
|
||||
test('overwrites additionalProperties and required', () =>
|
||||
expect(
|
||||
mergeSettingsSchemas([
|
||||
{
|
||||
manifest: {
|
||||
contributes: {
|
||||
configuration: { additionalProperties: false, properties: { a: { type: 'string' } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
manifest: {
|
||||
contributes: {
|
||||
configuration: { required: ['b'], properties: { b: { type: 'string' } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
allOf: [
|
||||
{ $ref: settingsSchemaJSON.$id },
|
||||
{ additionalProperties: true, required: [], properties: { a: { type: 'string' } } },
|
||||
{ additionalProperties: true, required: [], properties: { b: { type: 'string' } } },
|
||||
],
|
||||
}))
|
||||
|
||||
test('handles error and null configuration', () =>
|
||||
expect(
|
||||
mergeSettingsSchemas([
|
||||
{
|
||||
manifest: {
|
||||
contributes: {
|
||||
configuration: { additionalProperties: false, properties: { a: { type: 'string' } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
manifest: new Error('x'),
|
||||
},
|
||||
{
|
||||
manifest: null,
|
||||
},
|
||||
{
|
||||
manifest: {},
|
||||
},
|
||||
{
|
||||
manifest: { contributes: {} },
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
allOf: [
|
||||
{ $ref: settingsSchemaJSON.$id },
|
||||
{ additionalProperties: true, required: [], properties: { a: { type: 'string' } } },
|
||||
],
|
||||
}))
|
||||
})
|
||||
@ -1,54 +0,0 @@
|
||||
import { isErrorLike } from '@sourcegraph/common'
|
||||
import { ConfiguredExtension } from '@sourcegraph/shared/src/extensions/extension'
|
||||
|
||||
import settingsSchemaJSON from '../../../../schema/settings.schema.json'
|
||||
|
||||
/**
|
||||
* Merges settings schemas from base settings and all configured extensions.
|
||||
*
|
||||
* @param configuredExtensions
|
||||
* @returns A JSON Schema that describes an instance of settings for a particular subject.
|
||||
*/
|
||||
export function mergeSettingsSchemas(configuredExtensions: Pick<ConfiguredExtension<'contributes'>, 'manifest'>[]): {
|
||||
allOf: object
|
||||
} {
|
||||
return {
|
||||
allOf: [
|
||||
{ $ref: settingsSchemaJSON.$id },
|
||||
...(configuredExtensions || [])
|
||||
.map(configuredExtension => {
|
||||
if (
|
||||
configuredExtension.manifest &&
|
||||
!isErrorLike(configuredExtension.manifest) &&
|
||||
configuredExtension.manifest.contributes?.configuration
|
||||
) {
|
||||
// Adjust the schema to describe a valid instance of settings for a subject (instead of the
|
||||
// final, merged settings).
|
||||
//
|
||||
// This is necessary to avoid erroneous validation errors. For example, suppose an extension's
|
||||
// configuration schema declares that the property "x" is required. For the configuration to be
|
||||
// valid, "x" may be set in global, organization, or user settings. It is valid for user
|
||||
// settings to NOT contain "x" (if global or organization settings contains "x").
|
||||
//
|
||||
// The JSON Schema returned by mergeSettingsSchema is used for a single subject's settings
|
||||
// (e.g., for user settings in the above example). Therefore, we must allow additionalProperties
|
||||
// and set required to [] to avoid erroneous validation errors.
|
||||
return {
|
||||
...configuredExtension.manifest.contributes.configuration,
|
||||
|
||||
// Force allow additionalProperties to prevent any single extension's configuration schema
|
||||
// from invalidating all other extensions' configuration properties.
|
||||
additionalProperties: true,
|
||||
|
||||
// Force no required properties because this instance is only the settings for a single
|
||||
// subject. It is possible that a required property is specified at a different subject in
|
||||
// the cascade, in which case we don't want to report this instance as invalid.
|
||||
required: [],
|
||||
}
|
||||
}
|
||||
return true // JSON Schema that matches everything
|
||||
})
|
||||
.filter(schema => schema !== true), // omit trivial JSON Schemas
|
||||
],
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ package graphqlbackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
"github.com/graph-gophers/graphql-go/relay"
|
||||
@ -11,63 +10,6 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
)
|
||||
|
||||
var builtinExtensions = map[string]bool{
|
||||
"sourcegraph/apex": true,
|
||||
"sourcegraph/clojure": true,
|
||||
"sourcegraph/cobol": true,
|
||||
"sourcegraph/cpp": true,
|
||||
"sourcegraph/csharp": true,
|
||||
"sourcegraph/cuda": true,
|
||||
"sourcegraph/dart": true,
|
||||
"sourcegraph/elixir": true,
|
||||
"sourcegraph/erlang": true,
|
||||
"sourcegraph/git-extras": true,
|
||||
"sourcegraph/go": true,
|
||||
"sourcegraph/graphql": true,
|
||||
"sourcegraph/groovy": true,
|
||||
"sourcegraph/haskell": true,
|
||||
"sourcegraph/java": true,
|
||||
"sourcegraph/jsonnet": true,
|
||||
"sourcegraph/kotlin": true,
|
||||
"sourcegraph/lisp": true,
|
||||
"sourcegraph/lua": true,
|
||||
"sourcegraph/ocaml": true,
|
||||
"sourcegraph/pascal": true,
|
||||
"sourcegraph/perl": true,
|
||||
"sourcegraph/php": true,
|
||||
"sourcegraph/powershell": true,
|
||||
"sourcegraph/protobuf": true,
|
||||
"sourcegraph/python": true,
|
||||
"sourcegraph/r": true,
|
||||
"sourcegraph/ruby": true,
|
||||
"sourcegraph/rust": true,
|
||||
"sourcegraph/scala": true,
|
||||
"sourcegraph/shell": true,
|
||||
"sourcegraph/starlark": true,
|
||||
"sourcegraph/swift": true,
|
||||
"sourcegraph/tcl": true,
|
||||
"sourcegraph/thrift": true,
|
||||
"sourcegraph/typescript": true,
|
||||
"sourcegraph/verilog": true,
|
||||
"sourcegraph/vhdl": true,
|
||||
}
|
||||
|
||||
func defaultSettings() map[string]any {
|
||||
extensionIDs := []string{}
|
||||
for id := range builtinExtensions {
|
||||
extensionIDs = append(extensionIDs, id)
|
||||
}
|
||||
extensions := map[string]bool{}
|
||||
for _, id := range extensionIDs {
|
||||
extensions[id] = true
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"experimentalFeatures": map[string]any{},
|
||||
"extensions": extensions,
|
||||
}
|
||||
}
|
||||
|
||||
const singletonDefaultSettingsGQLID = "DefaultSettings"
|
||||
|
||||
type defaultSettingsResolver struct {
|
||||
@ -82,11 +24,7 @@ func marshalDefaultSettingsGQLID(defaultSettingsID string) graphql.ID {
|
||||
func (r *defaultSettingsResolver) ID() graphql.ID { return marshalDefaultSettingsGQLID(r.gqlID) }
|
||||
|
||||
func (r *defaultSettingsResolver) LatestSettings(ctx context.Context) (*settingsResolver, error) {
|
||||
contents, err := json.Marshal(defaultSettings())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settings := &api.Settings{Subject: api.SettingsSubject{Default: true}, Contents: string(contents)}
|
||||
settings := &api.Settings{Subject: api.SettingsSubject{Default: true}, Contents: "{}"}
|
||||
return &settingsResolver{r.db, &settingsSubject{defaultSettings: r}, settings, nil}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
package graphqlbackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
var ErrExtensionsDisabled = errors.New("extensions are disabled in site configuration (contact the site admin to enable extensions)")
|
||||
|
||||
func (r *schemaResolver) ExtensionRegistry(ctx context.Context) (ExtensionRegistryResolver, error) {
|
||||
reg := ExtensionRegistry(r.db)
|
||||
if conf.Extensions() == nil {
|
||||
return nil, ErrExtensionsDisabled
|
||||
}
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// ExtensionRegistry is the implementation of the GraphQL type ExtensionRegistry.
|
||||
var ExtensionRegistry func(db database.DB) ExtensionRegistryResolver
|
||||
|
||||
// ExtensionRegistryResolver is the interface for the GraphQL type ExtensionRegistry.
|
||||
type ExtensionRegistryResolver interface {
|
||||
Extensions(context.Context, *RegistryExtensionConnectionArgs) (RegistryExtensionConnection, error)
|
||||
}
|
||||
|
||||
type RegistryExtensionConnectionArgs struct {
|
||||
graphqlutil.ConnectionArgs
|
||||
ExtensionIDs *[]string
|
||||
}
|
||||
|
||||
// NodeToRegistryExtension is called to convert GraphQL node values to values of type
|
||||
// RegistryExtension. It is assigned at init time.
|
||||
var NodeToRegistryExtension func(any) (RegistryExtension, bool)
|
||||
|
||||
// RegistryExtensionByID is called to look up values of GraphQL type RegistryExtension. It is
|
||||
// assigned at init time.
|
||||
var RegistryExtensionByID func(context.Context, database.DB, graphql.ID) (RegistryExtension, error)
|
||||
|
||||
// RegistryExtension is the interface for the GraphQL type RegistryExtension.
|
||||
type RegistryExtension interface {
|
||||
ID() graphql.ID
|
||||
ExtensionID() string
|
||||
Manifest(ctx context.Context) (ExtensionManifest, error)
|
||||
}
|
||||
|
||||
// ExtensionManifest is the interface for the GraphQL type ExtensionManifest.
|
||||
type ExtensionManifest interface {
|
||||
Raw() string
|
||||
JSONFields(*struct{ Fields []string }) JSONValue
|
||||
}
|
||||
|
||||
// RegistryExtensionConnection is the interface for the GraphQL type RegistryExtensionConnection.
|
||||
type RegistryExtensionConnection interface {
|
||||
Nodes(context.Context) ([]RegistryExtension, error)
|
||||
}
|
||||
@ -636,9 +636,6 @@ func newSchemaResolver(db database.DB, gitserverClient gitserver.Client) *schema
|
||||
"GitCommit": func(ctx context.Context, id graphql.ID) (Node, error) {
|
||||
return r.gitCommitByID(ctx, id)
|
||||
},
|
||||
"RegistryExtension": func(ctx context.Context, id graphql.ID) (Node, error) {
|
||||
return RegistryExtensionByID(ctx, db, id)
|
||||
},
|
||||
"SavedSearch": func(ctx context.Context, id graphql.ID) (Node, error) {
|
||||
return r.savedSearchByID(ctx, id)
|
||||
},
|
||||
|
||||
@ -196,13 +196,6 @@ func (r *NodeResolver) ToGitCommit() (*GitCommitResolver, bool) {
|
||||
return n, ok
|
||||
}
|
||||
|
||||
func (r *NodeResolver) ToRegistryExtension() (RegistryExtension, bool) {
|
||||
if NodeToRegistryExtension == nil {
|
||||
return nil, false
|
||||
}
|
||||
return NodeToRegistryExtension(r.Node)
|
||||
}
|
||||
|
||||
func (r *NodeResolver) ToSavedSearch() (*savedSearchResolver, bool) {
|
||||
n, ok := r.Node.(*savedSearchResolver)
|
||||
return n, ok
|
||||
|
||||
@ -245,7 +245,6 @@ type Mutation {
|
||||
|
||||
- All user data (access tokens, email addresses, external account info, survey responses, etc)
|
||||
- Organization membership information (which organizations the user is a part of, any invitations created by or targeting the user).
|
||||
- Sourcegraph extensions published by the user.
|
||||
- User, Organization, or Global settings authored by the user.
|
||||
"""
|
||||
deleteUser(user: ID!, hard: Boolean): EmptyResponse
|
||||
@ -1599,10 +1598,6 @@ type Query {
|
||||
first: Int
|
||||
): SurveyResponseConnection!
|
||||
"""
|
||||
The extension registry.
|
||||
"""
|
||||
extensionRegistry: ExtensionRegistry!
|
||||
"""
|
||||
FOR INTERNAL USE ONLY: Lists all status messages
|
||||
"""
|
||||
statusMessages: [StatusMessage!]!
|
||||
@ -6910,11 +6905,6 @@ type Site implements SettingsSubject {
|
||||
the GLOBAL_SETTINGS_FILE environment variable, site settings edits cannot be made through the API.
|
||||
"""
|
||||
allowSiteSettingsEdits: Boolean!
|
||||
"""
|
||||
Whether to enable the extension registry and the use of extensions.
|
||||
Reflects the site configuration `enableLegacyExtensions` experimental feature value.
|
||||
"""
|
||||
enableLegacyExtensions: Boolean!
|
||||
|
||||
"""
|
||||
FOR INTERNAL USE ONLY: Returns information about instance upgrade readiness.
|
||||
@ -8022,75 +8012,6 @@ type ProductLicenseInfo {
|
||||
expiresAt: DateTime!
|
||||
}
|
||||
|
||||
"""
|
||||
An extension registry.
|
||||
"""
|
||||
type ExtensionRegistry {
|
||||
"""
|
||||
A list of extensions published in the extension registry.
|
||||
"""
|
||||
extensions(
|
||||
"""
|
||||
Returns the first n extensions from the list.
|
||||
"""
|
||||
first: Int
|
||||
"""
|
||||
Returns only extensions with the given IDs.
|
||||
"""
|
||||
extensionIDs: [String!]
|
||||
): RegistryExtensionConnection!
|
||||
}
|
||||
|
||||
"""
|
||||
An extension's listing in the extension registry.
|
||||
"""
|
||||
type RegistryExtension implements Node {
|
||||
"""
|
||||
The unique, opaque, permanent ID of the extension. Do not display this ID to the user; display
|
||||
RegistryExtension.extensionID instead (it is friendlier and still unique, but it can be renamed).
|
||||
"""
|
||||
id: ID!
|
||||
"""
|
||||
The qualified, unique name that refers to this extension, consisting of the registry name (if non-default),
|
||||
publisher's name, and the extension's name, all joined by "/" (for example, "acme-corp/my-extension-name").
|
||||
"""
|
||||
extensionID: String!
|
||||
"""
|
||||
The extension manifest, or null if none is set.
|
||||
"""
|
||||
manifest: ExtensionManifest
|
||||
}
|
||||
|
||||
"""
|
||||
A description of the extension, how to run or access it, and when to activate it.
|
||||
"""
|
||||
type ExtensionManifest {
|
||||
"""
|
||||
The raw JSON (or JSONC) contents of the manifest. This value may be large (because many
|
||||
manifests contain README and icon data), and it is JSONC (not strict JSON), which means
|
||||
it must be parsed with a JSON parser that supports trailing commas and comments. Consider
|
||||
using jsonFields instead.
|
||||
"""
|
||||
raw: String!
|
||||
"""
|
||||
The manifest as JSON (not JSONC, even if the raw manifest is JSONC) with only the
|
||||
specified fields. This is useful for callers that only need certain fields and want
|
||||
to avoid fetching a large amount of data (because many manifests contain README
|
||||
and icon data).
|
||||
"""
|
||||
jsonFields(fields: [String!]!): JSONValue!
|
||||
}
|
||||
|
||||
"""
|
||||
A list of registry extensions.
|
||||
"""
|
||||
type RegistryExtensionConnection {
|
||||
"""
|
||||
A list of registry extensions.
|
||||
"""
|
||||
nodes: [RegistryExtension!]!
|
||||
}
|
||||
|
||||
"""
|
||||
Aggregate local code intelligence for all ranges that fall between a window of lines in a document.
|
||||
"""
|
||||
|
||||
@ -230,10 +230,6 @@ func canUpdateSiteConfiguration() bool {
|
||||
return os.Getenv("SITE_CONFIG_FILE") == "" || siteConfigAllowEdits
|
||||
}
|
||||
|
||||
func (r *siteResolver) EnableLegacyExtensions() bool {
|
||||
return conf.ExperimentalFeatures().EnableLegacyExtensions
|
||||
}
|
||||
|
||||
func (r *siteResolver) UpgradeReadiness(ctx context.Context) (*upgradeReadinessResolver, error) {
|
||||
// 🚨 SECURITY: Only site admins may view upgrade readiness information.
|
||||
if err := auth.CheckCurrentUserIsSiteAdmin(ctx, r.db); err != nil {
|
||||
|
||||
@ -164,8 +164,6 @@ type JSContext struct {
|
||||
|
||||
ExperimentalFeatures schema.ExperimentalFeatures `json:"experimentalFeatures"`
|
||||
|
||||
EnableLegacyExtensions bool `json:"enableLegacyExtensions"`
|
||||
|
||||
LicenseInfo *hooks.LicenseInfo `json:"licenseInfo"`
|
||||
|
||||
OutboundRequestLogLimit int `json:"outboundRequestLogLimit"`
|
||||
@ -309,8 +307,6 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext {
|
||||
|
||||
ExperimentalFeatures: conf.ExperimentalFeatures(),
|
||||
|
||||
EnableLegacyExtensions: conf.ExperimentalFeatures().EnableLegacyExtensions,
|
||||
|
||||
LicenseInfo: licenseInfo,
|
||||
|
||||
OutboundRequestLogLimit: conf.Get().OutboundRequestLogLimit,
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
)
|
||||
|
||||
func ShouldRedirectLegacyExtensionEndpoints() bool {
|
||||
if conf.ExperimentalFeatures().EnableLegacyExtensions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -5,9 +5,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
func TestLegacyExtensionsRedirects(t *testing.T) {
|
||||
@ -38,39 +36,3 @@ func TestLegacyExtensionsRedirects(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyExtensionsRedirectsWithExtensionsEnabled(t *testing.T) {
|
||||
enableLegacyExtensions()
|
||||
defer conf.Mock(nil)
|
||||
|
||||
InitRouter(database.NewMockDB())
|
||||
router := Router()
|
||||
|
||||
tests := []string{
|
||||
"/extensions",
|
||||
"/extensions/sourcegraph/codecov",
|
||||
"/extensions/sourcegraph/codecov/-/manifest",
|
||||
"/-/static/extension/13594-sourcegraph-codecov.js",
|
||||
}
|
||||
for i := range tests {
|
||||
rw := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", tests[i], nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
router.ServeHTTP(rw, req)
|
||||
|
||||
if got := rw.Header().Get("location"); got != "" {
|
||||
t.Errorf("%s: expected router to not redirect to root page but got %s", tests[i], got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enableLegacyExtensions() {
|
||||
conf.Mock(&conf.Unified{SiteConfiguration: schema.SiteConfiguration{
|
||||
ExperimentalFeatures: &schema.ExperimentalFeatures{
|
||||
EnableLegacyExtensions: true,
|
||||
},
|
||||
}})
|
||||
}
|
||||
|
||||
@ -270,12 +270,10 @@ func initRouter(db database.DB, router *mux.Router) {
|
||||
router.Get(routeSurvey).Handler(brandedNoIndex("Survey"))
|
||||
router.Get(routeSurveyScore).Handler(brandedNoIndex("Survey"))
|
||||
router.Get(routeRegistry).Handler(brandedNoIndex("Registry"))
|
||||
if ShouldRedirectLegacyExtensionEndpoints() {
|
||||
if envvar.SourcegraphDotComMode() {
|
||||
router.Get(routeExtensions).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusMovedPermanently)
|
||||
})
|
||||
} else {
|
||||
router.Get(routeExtensions).Handler(brandedIndex("Extensions"))
|
||||
}
|
||||
router.Get(routeHelp).HandlerFunc(serveHelp)
|
||||
router.Get(routeSnippets).Handler(brandedNoIndex("Snippets"))
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/client"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
)
|
||||
|
||||
func (r *extensionRegistryResolver) Extensions(ctx context.Context, args *graphqlbackend.RegistryExtensionConnectionArgs) (graphqlbackend.RegistryExtensionConnection, error) {
|
||||
return ®istryExtensionConnectionResolver{
|
||||
args: *args,
|
||||
db: r.db,
|
||||
listRemoteRegistryExtensions: listRemoteRegistryExtensions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// registryExtensionConnectionResolver resolves a list of registry extensions.
|
||||
type registryExtensionConnectionResolver struct {
|
||||
args graphqlbackend.RegistryExtensionConnectionArgs
|
||||
|
||||
db database.DB
|
||||
listRemoteRegistryExtensions func(_ context.Context, query string) ([]*registry.Extension, error)
|
||||
|
||||
// cache results because they are used by multiple fields
|
||||
once sync.Once
|
||||
registryExtensions []graphqlbackend.RegistryExtension
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *registryExtensionConnectionResolver) compute(ctx context.Context) ([]graphqlbackend.RegistryExtension, error) {
|
||||
r.once.Do(func() {
|
||||
args2 := r.args
|
||||
if args2.First != nil {
|
||||
tmp := *args2.First
|
||||
tmp++ // so we can detect if there is a next page
|
||||
args2.First = &tmp
|
||||
}
|
||||
|
||||
// Query remote registry extensions, if filters would match any.
|
||||
var remote []*registry.Extension
|
||||
xs, err := r.listRemoteRegistryExtensions(ctx, "")
|
||||
if err != nil {
|
||||
// Continue execution even if r.err != nil so that partial (local) results are returned
|
||||
// even when the remote registry is inaccessible.
|
||||
r.err = err
|
||||
}
|
||||
|
||||
if r.args.ExtensionIDs == nil {
|
||||
remote = append(remote, xs...)
|
||||
} else {
|
||||
// The ExtensionIDs arg ("only include extensions specified by these IDs") is not
|
||||
// applied at query time for remote extensions, so apply it here.
|
||||
include := map[string]struct{}{}
|
||||
for _, id := range *r.args.ExtensionIDs {
|
||||
include[id] = struct{}{}
|
||||
}
|
||||
for _, x := range xs {
|
||||
if _, ok := include[x.ExtensionID]; ok {
|
||||
remote = append(remote, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.registryExtensions = make([]graphqlbackend.RegistryExtension, len(remote))
|
||||
for i, x := range remote {
|
||||
r.registryExtensions[i] = ®istryExtensionRemoteResolver{v: x}
|
||||
}
|
||||
|
||||
sort.SliceStable(r.registryExtensions, func(i, j int) bool {
|
||||
return r.registryExtensions[i].ExtensionID() < r.registryExtensions[j].ExtensionID()
|
||||
})
|
||||
|
||||
allowedExtensions := ExtensionRegistryListAllowedExtensions()
|
||||
if allowedExtensions != nil {
|
||||
filteredExtensions := []graphqlbackend.RegistryExtension{}
|
||||
for i := range r.registryExtensions {
|
||||
ext := r.registryExtensions[i]
|
||||
if _, ok := allowedExtensions[ext.ExtensionID()]; ok {
|
||||
filteredExtensions = append(filteredExtensions, ext)
|
||||
}
|
||||
}
|
||||
r.registryExtensions = filteredExtensions
|
||||
}
|
||||
})
|
||||
return r.registryExtensions, r.err
|
||||
}
|
||||
|
||||
func (r *registryExtensionConnectionResolver) Nodes(ctx context.Context) ([]graphqlbackend.RegistryExtension, error) {
|
||||
xs, _ := r.compute(ctx)
|
||||
if r.args.First != nil && len(xs) > int(*r.args.First) {
|
||||
xs = xs[:int(*r.args.First)]
|
||||
}
|
||||
return xs, nil
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/client"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
)
|
||||
|
||||
func TestRegistryExtensionConnectionResolver(t *testing.T) {
|
||||
enableLegacyExtensions()
|
||||
defer conf.Mock(nil)
|
||||
envvar.MockSourcegraphDotComMode(true)
|
||||
defer envvar.MockSourcegraphDotComMode(false)
|
||||
|
||||
stringSlicePtr := func(s []string) *[]string { return &s }
|
||||
extensionIDs := func(xs []graphqlbackend.RegistryExtension) (ids []string) {
|
||||
for _, x := range xs {
|
||||
ids = append(ids, x.ExtensionID())
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("extensionIDs", func(t *testing.T) {
|
||||
r := registryExtensionConnectionResolver{
|
||||
args: graphqlbackend.RegistryExtensionConnectionArgs{
|
||||
ExtensionIDs: stringSlicePtr([]string{"a/a"}),
|
||||
},
|
||||
listRemoteRegistryExtensions: func(_ context.Context, query string) ([]*registry.Extension, error) {
|
||||
return []*registry.Extension{
|
||||
{ExtensionID: "a/b", Manifest: strptr(`{}`)},
|
||||
{ExtensionID: "a/a", Manifest: strptr(`{}`)},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
nodes, err := r.Nodes(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ids, want := extensionIDs(nodes), []string{"a/a"}; !reflect.DeepEqual(ids, want) {
|
||||
t.Errorf("got ids %v, want %v", ids, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func strptr(s string) *string { return &s }
|
||||
@ -1,52 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
"github.com/graph-gophers/graphql-go/relay"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
graphqlbackend.NodeToRegistryExtension = func(node any) (graphqlbackend.RegistryExtension, bool) {
|
||||
n, ok := node.(*registryExtensionRemoteResolver)
|
||||
return n, ok
|
||||
}
|
||||
|
||||
graphqlbackend.RegistryExtensionByID = registryExtensionByID
|
||||
}
|
||||
|
||||
// RegistryExtensionID identifies a registry extension.
|
||||
type RegistryExtensionID struct {
|
||||
RemoteID *registryExtensionRemoteID `json:"r,omitempty"`
|
||||
}
|
||||
|
||||
func MarshalRegistryExtensionID(id RegistryExtensionID) graphql.ID {
|
||||
return relay.MarshalID("RegistryExtension", id)
|
||||
}
|
||||
|
||||
func UnmarshalRegistryExtensionID(id graphql.ID) (registryExtensionID RegistryExtensionID, err error) {
|
||||
err = relay.UnmarshalSpec(id, ®istryExtensionID)
|
||||
return
|
||||
}
|
||||
|
||||
func registryExtensionByID(ctx context.Context, db database.DB, id graphql.ID) (graphqlbackend.RegistryExtension, error) {
|
||||
registryExtensionID, err := UnmarshalRegistryExtensionID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case registryExtensionID.RemoteID != nil:
|
||||
x, err := getRemoteRegistryExtension(ctx, "uuid", registryExtensionID.RemoteID.UUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ®istryExtensionRemoteResolver{v: x}, nil
|
||||
default:
|
||||
return nil, errors.New("invalid registry extension ID")
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
"github.com/sourcegraph/sourcegraph/internal/jsonc"
|
||||
)
|
||||
|
||||
// extensionManifest implements the GraphQL type ExtensionManifest.
|
||||
type extensionManifest struct {
|
||||
raw string
|
||||
}
|
||||
|
||||
// NewExtensionManifest creates a new resolver for the GraphQL type ExtensionManifest with the given
|
||||
// raw contents of an extension manifest.
|
||||
func NewExtensionManifest(raw *string) graphqlbackend.ExtensionManifest {
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
return &extensionManifest{raw: *raw}
|
||||
}
|
||||
|
||||
func (r *extensionManifest) Raw() string { return r.raw }
|
||||
|
||||
func (r *extensionManifest) JSONFields(args *struct{ Fields []string }) graphqlbackend.JSONValue {
|
||||
return jsonValueWithFields(r.raw, args.Fields)
|
||||
}
|
||||
|
||||
func jsonValueWithFields(jsoncStr string, fields []string) graphqlbackend.JSONValue {
|
||||
o := map[string]json.RawMessage{}
|
||||
jsonc.Unmarshal(jsoncStr, &o) // ignore error and treat as empty object
|
||||
|
||||
keepField := func(field string) bool {
|
||||
for _, f := range fields {
|
||||
if f == field {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for field := range o {
|
||||
if !keepField(field) {
|
||||
delete(o, field)
|
||||
}
|
||||
}
|
||||
return graphqlbackend.JSONValue{Value: o}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJSONValueWithFields(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
jsoncStr string
|
||||
fields []string
|
||||
want string
|
||||
}{
|
||||
"invalid json top-level": {
|
||||
jsoncStr: `{`,
|
||||
fields: []string{"x"},
|
||||
want: `{}`,
|
||||
},
|
||||
"invalid json in field": {
|
||||
jsoncStr: `{"a": {"b": }, "c": 3}`,
|
||||
fields: []string{"a", "c"},
|
||||
want: `{}`,
|
||||
},
|
||||
"subset of fields": {
|
||||
jsoncStr: `{"a": 1, "b": {"c": 3,}, "d": true,}`,
|
||||
fields: []string{"d", "b", "x"},
|
||||
want: `{"b":{"c":3},"d":true}`,
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
v := jsonValueWithFields(test.jsoncStr, test.fields)
|
||||
got, err := v.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != test.want {
|
||||
t.Errorf("got %s, want %s", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/client"
|
||||
)
|
||||
|
||||
// registryExtensionRemoteResolver implements the GraphQL type RegistryExtension with data from a
|
||||
// remote registry.
|
||||
type registryExtensionRemoteResolver struct {
|
||||
v *registry.Extension
|
||||
}
|
||||
|
||||
var _ graphqlbackend.RegistryExtension = ®istryExtensionRemoteResolver{}
|
||||
|
||||
func (r *registryExtensionRemoteResolver) ID() graphql.ID {
|
||||
return MarshalRegistryExtensionID(RegistryExtensionID{
|
||||
RemoteID: ®istryExtensionRemoteID{Registry: r.v.RegistryURL, UUID: r.v.UUID},
|
||||
})
|
||||
}
|
||||
|
||||
// registryExtensionRemoteID identifies a registry extension on a remote registry. It is encoded in
|
||||
// RegistryExtensionID.
|
||||
type registryExtensionRemoteID struct {
|
||||
Registry string `json:"r"`
|
||||
UUID string `json:"u"`
|
||||
}
|
||||
|
||||
func (r *registryExtensionRemoteResolver) ExtensionID() string { return r.v.ExtensionID }
|
||||
|
||||
func (r *registryExtensionRemoteResolver) Manifest(context.Context) (graphqlbackend.ExtensionManifest, error) {
|
||||
return NewExtensionManifest(r.v.Manifest), nil
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
|
||||
registry "github.com/sourcegraph/sourcegraph/cmd/frontend/registry/client"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
)
|
||||
|
||||
// getRemoteRegistryURL returns the remote registry URL from site configuration, or nil if there is
|
||||
// none. If an error exists while parsing the value in site configuration, the error is returned.
|
||||
func getRemoteRegistryURL() (*url.URL, error) {
|
||||
pc := conf.Extensions()
|
||||
if pc == nil || pc.RemoteRegistryURL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return url.Parse(pc.RemoteRegistryURL)
|
||||
}
|
||||
|
||||
// IsRemoteExtensionAllowed reports whether to allow usage of the remote extension with the given
|
||||
// extension ID.
|
||||
//
|
||||
// It can be overridden to use custom logic.
|
||||
var IsRemoteExtensionAllowed = func(extensionID string) bool {
|
||||
// By default, all remote extensions are allowed.
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRemoteExtensionPublisherAllowed reports whether to allow usage of the remote extension created by
|
||||
// certain publisher by extension ID.
|
||||
//
|
||||
// It can be overridden to use custom logic.
|
||||
var IsRemoteExtensionPublisherAllowed = func(p registry.Publisher) bool {
|
||||
// By default, all remote extensions are allowed.
|
||||
return true
|
||||
}
|
||||
|
||||
// getRemoteRegistryExtension gets the remote registry extension and rewrites its fields to be from
|
||||
// the frame-of-reference of this site. The field is either "uuid" or "extensionID".
|
||||
func getRemoteRegistryExtension(ctx context.Context, field, value string) (*registry.Extension, error) {
|
||||
registryURL, err := getRemoteRegistryURL()
|
||||
if registryURL == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var x *registry.Extension
|
||||
switch field {
|
||||
case "uuid":
|
||||
x, err = registry.GetByUUID(ctx, registryURL, value)
|
||||
case "extensionID":
|
||||
x, err = registry.GetByExtensionID(ctx, registryURL, value)
|
||||
default:
|
||||
panic("unexpected field: " + field)
|
||||
}
|
||||
if x != nil {
|
||||
x.RegistryURL = registryURL.String()
|
||||
}
|
||||
|
||||
if x != nil && !IsRemoteExtensionAllowed(x.ExtensionID) {
|
||||
return nil, errors.Errorf("extension is not allowed in site configuration: %q", x.ExtensionID)
|
||||
}
|
||||
|
||||
if x != nil && !IsRemoteExtensionPublisherAllowed(x.Publisher) {
|
||||
return nil, errors.Errorf("Only extensions authored by Sourcegraph are allowed in this site configuration")
|
||||
}
|
||||
|
||||
return x, err
|
||||
}
|
||||
|
||||
// listRemoteRegistryExtensions lists the remote registry extensions and rewrites their fields to be
|
||||
// from the frame-of-reference of this site.
|
||||
func listRemoteRegistryExtensions(ctx context.Context, query string) ([]*registry.Extension, error) {
|
||||
registryURL, err := getRemoteRegistryURL()
|
||||
if registryURL == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
xs, err := registry.List(ctx, registryURL, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, x := range xs {
|
||||
x.RegistryURL = registryURL.String()
|
||||
}
|
||||
return xs, nil
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
func enableLegacyExtensions() {
|
||||
conf.Mock(&conf.Unified{SiteConfiguration: schema.SiteConfiguration{
|
||||
ExperimentalFeatures: &schema.ExperimentalFeatures{
|
||||
EnableLegacyExtensions: true,
|
||||
},
|
||||
}})
|
||||
}
|
||||
@ -2,52 +2,9 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/envvar"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
// This list is different from the builtinExtensions since it only includes code
|
||||
// intel extensions.
|
||||
var codeIntelExtensions = map[string]bool{
|
||||
"sourcegraph/apex": true,
|
||||
"sourcegraph/clojure": true,
|
||||
"sourcegraph/cobol": true,
|
||||
"sourcegraph/cpp": true,
|
||||
"sourcegraph/csharp": true,
|
||||
"sourcegraph/cuda": true,
|
||||
"sourcegraph/dart": true,
|
||||
"sourcegraph/elixir": true,
|
||||
"sourcegraph/erlang": true,
|
||||
"sourcegraph/go": true,
|
||||
"sourcegraph/graphql": true,
|
||||
"sourcegraph/groovy": true,
|
||||
"sourcegraph/haskell": true,
|
||||
"sourcegraph/java": true,
|
||||
"sourcegraph/jsonnet": true,
|
||||
"sourcegraph/kotlin": true,
|
||||
"sourcegraph/lisp": true,
|
||||
"sourcegraph/lua": true,
|
||||
"sourcegraph/ocaml": true,
|
||||
"sourcegraph/pascal": true,
|
||||
"sourcegraph/perl": true,
|
||||
"sourcegraph/php": true,
|
||||
"sourcegraph/powershell": true,
|
||||
"sourcegraph/protobuf": true,
|
||||
"sourcegraph/python": true,
|
||||
"sourcegraph/r": true,
|
||||
"sourcegraph/ruby": true,
|
||||
"sourcegraph/rust": true,
|
||||
"sourcegraph/scala": true,
|
||||
"sourcegraph/shell": true,
|
||||
"sourcegraph/starlark": true,
|
||||
"sourcegraph/swift": true,
|
||||
"sourcegraph/tcl": true,
|
||||
"sourcegraph/thrift": true,
|
||||
"sourcegraph/typescript": true,
|
||||
"sourcegraph/verilog": true,
|
||||
"sourcegraph/vhdl": true,
|
||||
}
|
||||
|
||||
func ExtensionRegistryReadEnabled() error {
|
||||
// We need to allow read access to the extension registry of sourcegraph.com to allow instances
|
||||
// on older versions to fetch extensions.
|
||||
@ -55,34 +12,5 @@ func ExtensionRegistryReadEnabled() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if conf.ExperimentalFeatures().EnableLegacyExtensions {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Errorf("extensions are disabled")
|
||||
}
|
||||
|
||||
// The extensions list query (`extensionRegistry.extensions`) will be called by the native
|
||||
// integration, a deployment method where we do not have control over updating the client.
|
||||
//
|
||||
// An example for this is the GitLab native integration that ships with GitLab releases.
|
||||
// When enabled, it will make a GraphQL query to Sourcegraph to get a list of valid
|
||||
// extensions.
|
||||
//
|
||||
// Since we want to support code navigation in the native integrations after 4.0, we are
|
||||
// special-casing this API and make it return only code intel legacy extensions.
|
||||
//
|
||||
// Since the dotcom instance will be connected to from instances before 4.0, we'll need
|
||||
// to keep the behavior of being able to list all extensions there.
|
||||
//
|
||||
// This method returns nil if all extensions are allowed to be listed.
|
||||
func ExtensionRegistryListAllowedExtensions() map[string]bool {
|
||||
err := ExtensionRegistryReadEnabled()
|
||||
|
||||
if err != nil {
|
||||
return codeIntelExtensions
|
||||
}
|
||||
|
||||
// nil means all extensions are allowed
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
)
|
||||
|
||||
func init() {
|
||||
graphqlbackend.ExtensionRegistry = func(db database.DB) graphqlbackend.ExtensionRegistryResolver {
|
||||
ExtensionRegistry.db = db
|
||||
return &ExtensionRegistry
|
||||
}
|
||||
}
|
||||
|
||||
// ExtensionRegistry is the implementation of the GraphQL type ExtensionRegistry.
|
||||
//
|
||||
// To supply implementations of extension registry functionality, use the fields on this value of
|
||||
// extensionRegistryResolver.
|
||||
var ExtensionRegistry extensionRegistryResolver
|
||||
|
||||
// extensionRegistryResolver implements the GraphQL type ExtensionRegistry.
|
||||
type extensionRegistryResolver struct {
|
||||
db database.DB
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
// PlatformConfiguration contains site configuration for the Sourcegraph platform.
|
||||
type PlatformConfiguration struct {
|
||||
RemoteRegistryURL string
|
||||
}
|
||||
|
||||
// DefaultRemoteRegistry is the default value for the site configuration property
|
||||
// "extensions"."remoteRegistry".
|
||||
//
|
||||
// It is intentionally not set in the OSS build.
|
||||
var DefaultRemoteRegistry string
|
||||
|
||||
// Extensions returns the configuration for the Sourcegraph platform, or nil if it is disabled.
|
||||
func Extensions() *PlatformConfiguration {
|
||||
cfg := Get()
|
||||
|
||||
x := cfg.Extensions
|
||||
if x == nil {
|
||||
if DefaultRemoteRegistry == "" {
|
||||
// There is no reasonable default behavior for extensions given that there is no remote
|
||||
// registry, so consider them disabled.
|
||||
return nil
|
||||
}
|
||||
|
||||
x = &schema.Extensions{}
|
||||
}
|
||||
|
||||
if x.Disabled != nil && *x.Disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pc PlatformConfiguration
|
||||
|
||||
// If the "remoteRegistry" value is a string, use that. If false, then keep it empty. Otherwise
|
||||
// use the default.
|
||||
if s, ok := x.RemoteRegistry.(string); ok {
|
||||
pc.RemoteRegistryURL = s
|
||||
} else if b, ok := x.RemoteRegistry.(bool); ok && !b {
|
||||
// Nothing to do.
|
||||
} else {
|
||||
pc.RemoteRegistryURL = DefaultRemoteRegistry
|
||||
}
|
||||
|
||||
if v, _ := strconv.ParseBool(os.Getenv("OFFLINE")); v {
|
||||
pc.RemoteRegistryURL = ""
|
||||
}
|
||||
|
||||
return &pc
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
func TestExtensions(t *testing.T) {
|
||||
check := func(t *testing.T, want *PlatformConfiguration) {
|
||||
t.Helper()
|
||||
got := Extensions()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("no config and no DefaultRemoteRegistry", func(t *testing.T) {
|
||||
DefaultRemoteRegistry = ""
|
||||
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{}})
|
||||
check(t, nil)
|
||||
})
|
||||
|
||||
t.Run("no config but valid DefaultRemoteRegistry", func(t *testing.T) {
|
||||
DefaultRemoteRegistry = "x"
|
||||
defer func() { DefaultRemoteRegistry = "" }()
|
||||
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{}})
|
||||
check(t, &PlatformConfiguration{RemoteRegistryURL: "x"})
|
||||
})
|
||||
|
||||
t.Run("empty config, valid DefaultRemoteRegistry", func(t *testing.T) {
|
||||
DefaultRemoteRegistry = "x"
|
||||
defer func() { DefaultRemoteRegistry = "" }()
|
||||
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{}}})
|
||||
check(t, &PlatformConfiguration{RemoteRegistryURL: "x"})
|
||||
})
|
||||
|
||||
t.Run("config extensions.disabled", func(t *testing.T) {
|
||||
DefaultRemoteRegistry = "x"
|
||||
defer func() { DefaultRemoteRegistry = "" }()
|
||||
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{Disabled: boolPtr(true)}}})
|
||||
check(t, nil)
|
||||
})
|
||||
|
||||
t.Run("config extensions.disabled false", func(t *testing.T) {
|
||||
DefaultRemoteRegistry = "x"
|
||||
defer func() { DefaultRemoteRegistry = "" }()
|
||||
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{Disabled: boolPtr(false)}}})
|
||||
check(t, &PlatformConfiguration{RemoteRegistryURL: "x"})
|
||||
})
|
||||
|
||||
t.Run("config extensions.remoteRegistry overrides DefaultRemoteRegistry", func(t *testing.T) {
|
||||
DefaultRemoteRegistry = "x"
|
||||
defer func() { DefaultRemoteRegistry = "" }()
|
||||
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{RemoteRegistry: "y"}}})
|
||||
check(t, &PlatformConfiguration{RemoteRegistryURL: "y"})
|
||||
})
|
||||
|
||||
t.Run("config extensions.remoteRegistry false", func(t *testing.T) {
|
||||
DefaultRemoteRegistry = "x"
|
||||
defer func() { DefaultRemoteRegistry = "" }()
|
||||
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{Extensions: &schema.Extensions{RemoteRegistry: false}}})
|
||||
check(t, &PlatformConfiguration{RemoteRegistryURL: ""})
|
||||
})
|
||||
|
||||
t.Run("OFFLINE env var", func(t *testing.T) {
|
||||
os.Setenv("OFFLINE", "1")
|
||||
defer os.Unsetenv("OFFLINE")
|
||||
DefaultRemoteRegistry = "x"
|
||||
defer func() { DefaultRemoteRegistry = "" }()
|
||||
Mock(&Unified{SiteConfiguration: schema.SiteConfiguration{}})
|
||||
check(t, &PlatformConfiguration{RemoteRegistryURL: ""})
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user