@@ -2,22 +2,32 @@ import { UpdateExtensionSettingsArgs } from '@sourcegraph/extensions-client-comm
22import { Controller as ExtensionsContextController } from '@sourcegraph/extensions-client-common/lib/controller'
33import { ConfiguredExtension } from '@sourcegraph/extensions-client-common/lib/extensions/extension'
44import { gql , graphQLContent } from '@sourcegraph/extensions-client-common/lib/graphql'
5- import { ConfigurationSubject , gqlToCascade , Settings } from '@sourcegraph/extensions-client-common/lib/settings'
5+ import {
6+ ConfigurationCascade ,
7+ ConfigurationCascadeOrError ,
8+ ConfigurationSubject ,
9+ gqlToCascade ,
10+ mergeSettings ,
11+ Settings ,
12+ } from '@sourcegraph/extensions-client-common/lib/settings'
613import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
14+ import { applyEdits } from '@sqs/jsonc-parser'
15+ import * as JSONC from '@sqs/jsonc-parser'
16+ import { removeProperty , setProperty } from '@sqs/jsonc-parser/lib/edit'
717import { isEqual } from 'lodash'
818import AddIcon from 'mdi-react/AddIcon'
919import Alert from 'mdi-react/AlertIcon'
1020import InfoIcon from 'mdi-react/InformationIcon'
1121import MenuDown from 'mdi-react/MenuDownIcon'
1222import Menu from 'mdi-react/MenuIcon'
1323import SettingsIcon from 'mdi-react/SettingsIcon'
14- import { combineLatest , Observable , Subject , throwError } from 'rxjs'
24+ import { combineLatest , from , Observable , Subject , throwError } from 'rxjs'
1525import { distinctUntilChanged , map , mapTo , mergeMap , startWith , switchMap , take , tap } from 'rxjs/operators'
1626import { MessageTransports } from 'sourcegraph/module/protocol/jsonrpc2/connection'
1727import { TextDocumentDecoration } from 'sourcegraph/module/protocol/plainTypes'
1828import uuid from 'uuid'
1929import { Disposable } from 'vscode-languageserver'
20- import storage from '../../browser/storage'
30+ import storage , { StorageItems } from '../../browser/storage'
2131import { GQL } from '../../types/gqlschema'
2232import { ExtensionConnectionInfo , onFirstMessage } from '../messaging'
2333import { canFetchForURL } from '../util/context'
@@ -128,6 +138,45 @@ export const applyDecoration = ({
128138 return mergeDisposables ( ...disposables )
129139}
130140
141+ const storageConfigurationCascade : Observable <
142+ ConfigurationCascade < ConfigurationSubject , Settings >
143+ > = storage . observeSync ( 'clientSettings' ) . pipe (
144+ map ( clientSettingsString => JSONC . parse ( clientSettingsString || '' ) ) ,
145+ map ( clientSettings => ( {
146+ subjects : [
147+ {
148+ subject : {
149+ id : 'Client' ,
150+ settingsURL : 'N/A' ,
151+ viewerCanAdminister : true ,
152+ __typename : 'Client' ,
153+ displayName : 'Client' ,
154+ } as ConfigurationSubject ,
155+ settings : clientSettings ,
156+ } ,
157+ ] ,
158+ merged : clientSettings || { } ,
159+ } ) )
160+ )
161+
162+ const mergeCascades = (
163+ cascadeOrError : ConfigurationCascadeOrError < ConfigurationSubject , Settings > ,
164+ cascade : ConfigurationCascade < ConfigurationSubject , Settings >
165+ ) : ConfigurationCascadeOrError < ConfigurationSubject , Settings > => ( {
166+ subjects :
167+ cascadeOrError . subjects === null
168+ ? cascade . subjects
169+ : isErrorLike ( cascadeOrError . subjects )
170+ ? cascadeOrError . subjects
171+ : [ ...cascadeOrError . subjects , ...cascade . subjects ] ,
172+ merged :
173+ cascadeOrError . merged === null
174+ ? cascade . merged
175+ : isErrorLike ( cascadeOrError . merged )
176+ ? cascadeOrError . merged
177+ : mergeSettings ( [ cascadeOrError . merged , cascade . merged ] ) ,
178+ } )
179+
131180const configurationCascadeFragment = gql `
132181 fragment ConfigurationCascadeFields on ConfigurationCascade {
133182 subjects {
@@ -169,7 +218,7 @@ const configurationCascadeRefreshes = new Subject<void>()
169218 * Always represents the entire configuration cascade; i.e., it contains the
170219 * individual configs from the various config subjects (orgs, user, etc.).
171220 */
172- export const configurationCascade = combineLatest (
221+ export const gqlConfigurationCascade = combineLatest (
173222 storage . observeSync ( 'sourcegraphURL' ) ,
174223 configurationCascadeRefreshes . pipe (
175224 mapTo ( null ) ,
@@ -194,23 +243,50 @@ export const configurationCascade = combineLatest(
194243 if ( ! data || ! data . viewerConfiguration ) {
195244 throw createAggregateError ( errors )
196245 }
246+
247+ for ( const subject of data . viewerConfiguration . subjects ) {
248+ // User/org/global settings cannot be edited from the
249+ // browser extension (only client settings can).
250+ subject . viewerCanAdminister = false
251+ }
252+
197253 return data . viewerConfiguration
198254 } )
199255 )
200256 )
201257)
202258
259+ const EMPTY_CONFIGURATION_CASCADE : ConfigurationCascade = { subjects : [ ] , merged : { } }
260+
261+ /**
262+ * The active configuration cascade.
263+ *
264+ * - For unauthenticated users, this is the GraphQL settings plus client settings (which are stored locally in the
265+ * browser extension.
266+ * - For authenticated users, this is just the GraphQL settings (client settings are ignored to simplify the UX).
267+ */
268+ export const configurationCascade : Observable <
269+ ConfigurationCascadeOrError < ConfigurationSubject , Settings >
270+ > = combineLatest ( gqlConfigurationCascade , storageConfigurationCascade ) . pipe (
271+ map ( ( [ gqlCascade , storageCascade ] ) =>
272+ mergeCascades (
273+ gqlToCascade ( gqlCascade ) ,
274+ gqlCascade . subjects . some ( subject => subject . __typename === 'User' )
275+ ? EMPTY_CONFIGURATION_CASCADE
276+ : storageCascade
277+ )
278+ ) ,
279+ distinctUntilChanged ( ( a , b ) => isEqual ( a , b ) )
280+ )
281+
203282export function createExtensionsContextController (
204283 sourcegraphUrl : string
205284) : ExtensionsContextController < ConfigurationSubject , Settings > {
206285 const sourcegraphLanguageServerURL = new URL ( sourcegraphUrl )
207286 sourcegraphLanguageServerURL . pathname = '.api/xlang'
208287
209288 return new ExtensionsContextController < ConfigurationSubject , Settings > ( {
210- configurationCascade : configurationCascade . pipe (
211- map ( gqlCascade => gqlToCascade ( gqlCascade ) ) ,
212- distinctUntilChanged ( ( a , b ) => isEqual ( a , b ) )
213- ) ,
289+ configurationCascade,
214290 updateExtensionSettings,
215291 queryGraphQL : ( request , variables , requestMightContainPrivateInfo ) =>
216292 storage . observeSync ( 'sourcegraphURL' ) . pipe (
@@ -250,11 +326,11 @@ export function createExtensionsContextController(
250326}
251327
252328// TODO(sqs): copied from sourcegraph/sourcegraph temporarily
253- function updateExtensionSettings ( subject : string , args : UpdateExtensionSettingsArgs ) : Observable < void > {
254- return configurationCascade . pipe (
329+ function updateUserSettings ( subject : string , args : UpdateExtensionSettingsArgs ) : Observable < void > {
330+ return gqlConfigurationCascade . pipe (
255331 take ( 1 ) ,
256- switchMap ( configurationCascade => {
257- const subjectConfig = configurationCascade . subjects . find ( s => s . id === subject )
332+ switchMap ( gqlConfigurationCascade => {
333+ const subjectConfig = gqlConfigurationCascade . subjects . find ( s => s . id === subject )
258334 if ( ! subjectConfig ) {
259335 throw new Error ( `no configuration subject: ${ subject } ` )
260336 }
@@ -308,3 +384,44 @@ function editConfiguration(subject: GQL.ID, lastID: number | null, edit: GQL.ICo
308384function toGQLKeyPath ( keyPath : ( string | number ) [ ] ) : GQL . IKeyPathSegment [ ] {
309385 return keyPath . map ( v => ( typeof v === 'string' ? { property : v } : { index : v } ) )
310386}
387+
388+ const updateClientSettings = ( subjectID : 'Client' , args : UpdateExtensionSettingsArgs ) : Observable < void > =>
389+ from (
390+ new Promise < StorageItems > ( resolve => storage . getSync ( storageItems => resolve ( storageItems ) ) ) . then (
391+ storageItems => {
392+ let clientSettings = storageItems . clientSettings
393+
394+ const format = { tabSize : 2 , insertSpaces : true , eol : '\n' }
395+
396+ if ( 'edit' in args && args . edit ) {
397+ clientSettings = applyEdits (
398+ clientSettings ,
399+ // TODO(chris): remove `.slice()` (which guards against
400+ // mutation) once
401+ // https://github.com/Microsoft/node-jsonc-parser/pull/12
402+ // is merged in.
403+ setProperty ( clientSettings , args . edit . path . slice ( ) , args . edit . value , format )
404+ )
405+ } else if ( 'extensionID' in args ) {
406+ clientSettings = applyEdits (
407+ clientSettings ,
408+ typeof args . enabled === 'boolean'
409+ ? setProperty ( clientSettings , [ 'extensions' , args . extensionID ] , args . enabled , format )
410+ : removeProperty ( clientSettings , [ 'extensions' , args . extensionID ] , format )
411+ )
412+ }
413+ return new Promise < undefined > ( resolve =>
414+ storage . setSync ( { clientSettings } , ( ) => {
415+ resolve ( undefined )
416+ } )
417+ )
418+ }
419+ )
420+ )
421+
422+ export const updateExtensionSettings = ( subjectID : string , args : UpdateExtensionSettingsArgs ) : Observable < void > => {
423+ if ( subjectID === 'Client' ) {
424+ return updateClientSettings ( subjectID , args )
425+ }
426+ return updateUserSettings ( subjectID , args )
427+ }
0 commit comments