Browse Source

automatically clean up defaults from the config file

Eugene Pankov 4 years ago
parent
commit
908f90cd52

+ 1 - 1
tabby-core/src/api/index.ts

@@ -20,7 +20,7 @@ export { ProfileProvider, Profile, ProfileSettingsComponent } from './profilePro
 export { PromptModalComponent } from '../components/promptModal.component'
 
 export { AppService } from '../services/app.service'
-export { ConfigService } from '../services/config.service'
+export { ConfigService, configMerge, ConfigProxy } from '../services/config.service'
 export { DockingService, Screen } from '../services/docking.service'
 export { Logger, ConsoleLogger, LogService } from '../services/log.service'
 export { HomeBaseService } from '../services/homeBase.service'

+ 1 - 0
tabby-core/src/api/profileProvider.ts

@@ -29,6 +29,7 @@ export abstract class ProfileProvider {
     name: string
     supportsQuickConnect = false
     settingsComponent: new (...args: any[]) => ProfileSettingsComponent
+    configDefaults = {}
 
     abstract getBuiltinProfiles (): Promise<Profile[]>
 

+ 28 - 11
tabby-core/src/services/config.service.ts

@@ -1,3 +1,4 @@
+import deepEqual from 'deep-equal'
 import { v4 as uuidv4 } from 'uuid'
 import * as yaml from 'js-yaml'
 import { Observable, Subject, AsyncSubject } from 'rxjs'
@@ -8,7 +9,8 @@ import { HostAppService } from '../api/hostApp'
 import { Vault, VaultService } from './vault.service'
 const deepmerge = require('deepmerge')
 
-const configMerge = (a, b) => deepmerge(a, b, { arrayMerge: (_d, s) => s }) // eslint-disable-line @typescript-eslint/no-var-requires
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export const configMerge = (a, b) => deepmerge(a, b, { arrayMerge: (_d, s) => s }) // eslint-disable-line @typescript-eslint/no-var-requires
 
 const LATEST_VERSION = 1
 
@@ -46,24 +48,24 @@ export class ConfigProxy {
                     {
                         enumerable: true,
                         configurable: false,
-                        get: () => this.getValue(key),
+                        get: () => this.__getValue(key),
                         set: (value) => {
-                            this.setValue(key, value)
+                            this.__setValue(key, value)
                         },
                     }
                 )
             }
         }
 
-        this.getValue = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
+        this.__getValue = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
             if (real[key] !== undefined) {
                 return real[key]
             } else {
-                return this.getDefault(key)
+                return this.__getDefault(key)
             }
         }
 
-        this.getDefault = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
+        this.__getDefault = (key: string) => { // eslint-disable-line @typescript-eslint/unbound-method
             if (isNonStructuralObjectMember(defaults[key])) {
                 real[key] = { ...defaults[key] }
                 delete real[key].__nonStructural
@@ -73,22 +75,36 @@ export class ConfigProxy {
             }
         }
 
-        this.setValue = (key: string, value: any) => { // eslint-disable-line @typescript-eslint/unbound-method
-            if (value === this.getDefault(key)) {
+        this.__setValue = (key: string, value: any) => { // eslint-disable-line @typescript-eslint/unbound-method
+            if (deepEqual(value, this.__getDefault(key))) {
                 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
                 delete real[key]
             } else {
                 real[key] = value
             }
         }
+
+        this.__cleanup = () => { // eslint-disable-line @typescript-eslint/unbound-method
+            // Trigger removal of default values
+            for (const key in defaults) {
+                if (isStructuralMember(defaults[key])) {
+                    this[key].__cleanup()
+                } else {
+                    const v = this.__getValue(key)
+                    this.__setValue(key, v)
+                }
+            }
+        }
     }
 
     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
-    getValue (_key: string): any { }
+    __getValue (_key: string): any { }
+    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
+    __setValue (_key: string, _value: any) { }
     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
-    setValue (_key: string, _value: any) { }
+    __getDefault (_key: string): any { }
     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
-    getDefault (_key: string): any { }
+    __cleanup () { }
 }
 
 @Injectable({ providedIn: 'root' })
@@ -177,6 +193,7 @@ export class ConfigService {
     }
 
     async save (): Promise<void> {
+        this.store.__cleanup()
         // Scrub undefined values
         let cleanStore = JSON.parse(JSON.stringify(this._store))
         cleanStore = await this.maybeEncryptConfig(cleanStore)

+ 23 - 1
tabby-core/src/services/profiles.service.ts

@@ -4,12 +4,26 @@ import { BaseTabComponent } from '../components/baseTab.component'
 import { Profile, ProfileProvider } from '../api/profileProvider'
 import { SelectorOption } from '../api/selector'
 import { AppService } from './app.service'
-import { ConfigService } from './config.service'
+import { configMerge, ConfigProxy, ConfigService } from './config.service'
 import { NotificationsService } from './notifications.service'
 import { SelectorService } from './selector.service'
 
 @Injectable({ providedIn: 'root' })
 export class ProfilesService {
+    private profileDefaults = {
+        id: '',
+        type: '',
+        name: '',
+        group: '',
+        options: {},
+        icon: '',
+        color: '',
+        disableDynamicTitle: false,
+        weight: 0,
+        isBuiltin: false,
+        isTemplate: false,
+    }
+
     constructor (
         private app: AppService,
         private config: ConfigService,
@@ -19,6 +33,7 @@ export class ProfilesService {
     ) { }
 
     async openNewTabForProfile (profile: Profile): Promise<BaseTabComponent|null> {
+        profile = this.getConfigProxyForProfile(profile)
         const params = await this.newTabParametersForProfile(profile)
         if (params) {
             const tab = this.app.openNewTab(params)
@@ -33,6 +48,7 @@ export class ProfilesService {
     }
 
     async newTabParametersForProfile (profile: Profile): Promise<NewTabParameters<BaseTabComponent>|null> {
+        profile = this.getConfigProxyForProfile(profile)
         return this.providerForProfile(profile)?.getNewTabParameters(profile) ?? null
     }
 
@@ -150,4 +166,10 @@ export class ProfilesService {
         this.notifications.error(`Could not parse "${query}"`)
         return null
     }
+
+    getConfigProxyForProfile (profile: Profile): Profile {
+        const provider = this.providerForProfile(profile)
+        const defaults = configMerge(this.profileDefaults, provider?.configDefaults ?? {})
+        return new ConfigProxy(profile, defaults) as unknown as Profile
+    }
 }

+ 0 - 1
tabby-serial/src/api.ts

@@ -19,7 +19,6 @@ export interface SerialProfileOptions extends StreamProcessingOptions, LoginScri
     xon?: boolean
     xoff?: boolean
     xany?: boolean
-    color?: string
 }
 
 export const BAUD_RATES = [

+ 18 - 0
tabby-serial/src/profiles.ts

@@ -13,6 +13,24 @@ export class SerialProfilesService extends ProfileProvider {
     id = 'serial'
     name = 'Serial'
     settingsComponent = SerialProfileSettingsComponent
+    configDefaults = {
+        options: {
+            port: null,
+            baudrate: null,
+            databits: 8,
+            stopbits: 1,
+            parity: 'none',
+            rtscts: false,
+            xon: false,
+            xoff: false,
+            xany: false,
+            inputMode: 'local-echo',
+            outputMode: null,
+            inputNewlines: null,
+            outputNewlines: 'crlf',
+            scripts: [],
+        },
+    }
 
     constructor (
         private selector: SelectorService,

+ 11 - 3
tabby-settings/src/components/editProfileModal.component.ts

@@ -2,7 +2,7 @@
 import { Observable, OperatorFunction, debounceTime, map, distinctUntilChanged } from 'rxjs'
 import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Injector } from '@angular/core'
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
-import { ConfigService, Profile, ProfileProvider, ProfileSettingsComponent } from 'tabby-core'
+import { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService } from 'tabby-core'
 
 const iconsData = require('../../../tabby-core/src/icons.json')
 const iconsClassList = Object.keys(iconsData).map(
@@ -16,17 +16,19 @@ const iconsClassList = Object.keys(iconsData).map(
     template: require('./editProfileModal.component.pug'),
 })
 export class EditProfileModalComponent {
-    @Input() profile: Profile
+    @Input() profile: Profile|ConfigProxy
     @Input() profileProvider: ProfileProvider
     @Input() settingsComponent: new () => ProfileSettingsComponent
     groupNames: string[]
     @ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
 
+    private _profile: Profile
     private settingsComponentInstance: ProfileSettingsComponent
 
     constructor (
         private injector: Injector,
         private componentFactoryResolver: ComponentFactoryResolver,
+        private profilesService: ProfilesService,
         config: ConfigService,
         private modalInstance: NgbActiveModal,
     ) {
@@ -37,6 +39,11 @@ export class EditProfileModalComponent {
         )].sort() as string[]
     }
 
+    ngOnInit () {
+        this._profile = this.profile
+        this.profile = this.profilesService.getConfigProxyForProfile(this.profile)
+    }
+
     ngAfterViewInit () {
         setTimeout(() => {
             const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.profileProvider.settingsComponent)
@@ -63,7 +70,8 @@ export class EditProfileModalComponent {
     save () {
         this.profile.group ||= undefined
         this.settingsComponentInstance.save?.()
-        this.modalInstance.close(this.profile)
+        this.profile.__cleanup()
+        this.modalInstance.close(this._profile)
     }
 
     cancel () {

+ 7 - 0
tabby-settings/src/components/profilesSettingsTab.component.ts

@@ -82,7 +82,14 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
         modal.componentInstance.profile = Object.assign({}, profile)
         modal.componentInstance.profileProvider = this.profilesService.providerForProfile(profile)
         const result = await modal.result
+
+        // Fully replace the config
+        for (const k in profile) {
+            // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+            delete profile[k]
+        }
         Object.assign(profile, result)
+
         await this.config.save()
     }
 

+ 6 - 26
tabby-ssh/src/components/sshProfileSettings.component.ts

@@ -4,8 +4,8 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 
 import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core'
 import { PasswordStorageService } from '../services/passwordStorage.service'
-import { ForwardedPortConfig, SSHAlgorithmType, ALGORITHM_BLACKLIST, SSHProfile } from '../api'
-import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
+import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api'
+import { SSHProfilesService } from '../profiles'
 
 /** @hidden */
 @Component({
@@ -18,7 +18,6 @@ export class SSHProfileSettingsComponent {
     useProxyCommand: boolean
 
     supportedAlgorithms: Record<string, string> = {}
-    defaultAlgorithms: Record<string, string[]> = {}
     algorithms: Record<string, Record<string, boolean>> = {}
     jumpHosts: SSHProfile[]
 
@@ -28,36 +27,16 @@ export class SSHProfileSettingsComponent {
         private passwordStorage: PasswordStorageService,
         private ngbModal: NgbModal,
         private fileProviders: FileProvidersService,
+        sshProfilesService: SSHProfilesService,
     ) {
-        for (const k of Object.values(SSHAlgorithmType)) {
-            const supportedAlg = {
-                [SSHAlgorithmType.KEX]: 'SUPPORTED_KEX',
-                [SSHAlgorithmType.HOSTKEY]: 'SUPPORTED_SERVER_HOST_KEY',
-                [SSHAlgorithmType.CIPHER]: 'SUPPORTED_CIPHER',
-                [SSHAlgorithmType.HMAC]: 'SUPPORTED_MAC',
-            }[k]
-            const defaultAlg = {
-                [SSHAlgorithmType.KEX]: 'DEFAULT_KEX',
-                [SSHAlgorithmType.HOSTKEY]: 'DEFAULT_SERVER_HOST_KEY',
-                [SSHAlgorithmType.CIPHER]: 'DEFAULT_CIPHER',
-                [SSHAlgorithmType.HMAC]: 'DEFAULT_MAC',
-            }[k]
-            this.supportedAlgorithms[k] = ALGORITHMS[supportedAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x)).sort()
-            this.defaultAlgorithms[k] = ALGORITHMS[defaultAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
-        }
+        this.supportedAlgorithms = sshProfilesService.supportedAlgorithms
     }
 
     async ngOnInit () {
         this.jumpHosts = this.config.store.profiles.filter(x => x.type === 'ssh' && x !== this.profile)
-        this.profile.options.algorithms = this.profile.options.algorithms ?? {}
         for (const k of Object.values(SSHAlgorithmType)) {
-            // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-            if (!this.profile.options.algorithms[k]) {
-                this.profile.options.algorithms[k] = this.defaultAlgorithms[k]
-            }
-
             this.algorithms[k] = {}
-            for (const alg of this.profile.options.algorithms[k]) {
+            for (const alg of this.profile.options.algorithms?.[k] ?? []) {
                 this.algorithms[k][alg] = true
             }
         }
@@ -108,6 +87,7 @@ export class SSHProfileSettingsComponent {
             this.profile.options.algorithms![k] = Object.entries(this.algorithms[k])
                 .filter(([_, v]) => !!v)
                 .map(([key, _]) => key)
+            this.profile.options.algorithms![k].sort()
         }
         if (!this.useProxyCommand) {
             this.profile.options.proxyCommand = undefined

+ 49 - 1
tabby-ssh/src/profiles.ts

@@ -3,7 +3,9 @@ import { ProfileProvider, Profile, NewTabParameters } from 'tabby-core'
 import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
 import { SSHTabComponent } from './components/sshTab.component'
 import { PasswordStorageService } from './services/passwordStorage.service'
-import { SSHProfile } from './api'
+import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
+
+import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
 
 @Injectable({ providedIn: 'root' })
 export class SSHProfilesService extends ProfileProvider {
@@ -11,11 +13,57 @@ export class SSHProfilesService extends ProfileProvider {
     name = 'SSH'
     supportsQuickConnect = true
     settingsComponent = SSHProfileSettingsComponent
+    configDefaults = {
+        options: {
+            host: null,
+            port: 22,
+            user: 'root',
+            auth: null,
+            password: null,
+            privateKeys: [],
+            keepaliveInterval: null,
+            keepaliveCountMax: null,
+            readyTimeout: null,
+            x11: false,
+            skipBanner: false,
+            jumpHost: null,
+            agentForward: false,
+            warnOnClose: null,
+            algorithms: {
+                hmac: [],
+                kex: [],
+                cipher: [],
+                serverHostKey: [],
+            },
+            proxyCommand: null,
+            forwardedPorts: [],
+            scripts: [],
+        },
+    }
+
+    supportedAlgorithms: Record<string, string> = {}
 
     constructor (
         private passwordStorage: PasswordStorageService
     ) {
         super()
+        for (const k of Object.values(SSHAlgorithmType)) {
+            const supportedAlg = {
+                [SSHAlgorithmType.KEX]: 'SUPPORTED_KEX',
+                [SSHAlgorithmType.HOSTKEY]: 'SUPPORTED_SERVER_HOST_KEY',
+                [SSHAlgorithmType.CIPHER]: 'SUPPORTED_CIPHER',
+                [SSHAlgorithmType.HMAC]: 'SUPPORTED_MAC',
+            }[k]
+            const defaultAlg = {
+                [SSHAlgorithmType.KEX]: 'DEFAULT_KEX',
+                [SSHAlgorithmType.HOSTKEY]: 'DEFAULT_SERVER_HOST_KEY',
+                [SSHAlgorithmType.CIPHER]: 'DEFAULT_CIPHER',
+                [SSHAlgorithmType.HMAC]: 'DEFAULT_MAC',
+            }[k]
+            this.supportedAlgorithms[k] = ALGORITHMS[supportedAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x)).sort()
+            this.configDefaults.options.algorithms[k] = ALGORITHMS[defaultAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
+            this.configDefaults.options.algorithms[k].sort()
+        }
     }
 
     async getBuiltinProfiles (): Promise<Profile[]> {

+ 11 - 0
tabby-telnet/src/profiles.ts

@@ -10,6 +10,17 @@ export class TelnetProfilesService extends ProfileProvider {
     name = 'Telnet'
     supportsQuickConnect = false
     settingsComponent = TelnetProfileSettingsComponent
+    configDefaults = {
+        options: {
+            host: null,
+            port: 23,
+            inputMode: 'local-echo',
+            outputMode: null,
+            inputNewlines: null,
+            outputNewlines: 'crlf',
+            scripts: [],
+        },
+    }
 
     async getBuiltinProfiles (): Promise<TelnetProfile[]> {
         return [