From 908f90cd5283f3182fcaed2b0cb01473cdde16ac Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sun, 11 Jul 2021 00:06:52 +0200 Subject: [PATCH] automatically clean up defaults from the config file --- tabby-core/src/api/index.ts | 2 +- tabby-core/src/api/profileProvider.ts | 1 + tabby-core/src/services/config.service.ts | 39 +++++++++++---- tabby-core/src/services/profiles.service.ts | 24 ++++++++- tabby-serial/src/api.ts | 1 - tabby-serial/src/profiles.ts | 18 +++++++ .../components/editProfileModal.component.ts | 14 ++++-- .../profilesSettingsTab.component.ts | 7 +++ .../sshProfileSettings.component.ts | 32 +++--------- tabby-ssh/src/profiles.ts | 50 ++++++++++++++++++- tabby-telnet/src/profiles.ts | 11 ++++ 11 files changed, 155 insertions(+), 44 deletions(-) diff --git a/tabby-core/src/api/index.ts b/tabby-core/src/api/index.ts index d9eee66a..879dc10a 100644 --- a/tabby-core/src/api/index.ts +++ b/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' diff --git a/tabby-core/src/api/profileProvider.ts b/tabby-core/src/api/profileProvider.ts index 6bb1b257..397cd374 100644 --- a/tabby-core/src/api/profileProvider.ts +++ b/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 diff --git a/tabby-core/src/services/config.service.ts b/tabby-core/src/services/config.service.ts index aa1953eb..8f1d68f6 100644 --- a/tabby-core/src/services/config.service.ts +++ b/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) { } + __setValue (_key: string, _value: any) { } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function - getDefault (_key: string): any { } + __getDefault (_key: string): any { } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function + __cleanup () { } } @Injectable({ providedIn: 'root' }) @@ -177,6 +193,7 @@ export class ConfigService { } async save (): Promise { + this.store.__cleanup() // Scrub undefined values let cleanStore = JSON.parse(JSON.stringify(this._store)) cleanStore = await this.maybeEncryptConfig(cleanStore) diff --git a/tabby-core/src/services/profiles.service.ts b/tabby-core/src/services/profiles.service.ts index 98e1bf3e..c9f80fbc 100644 --- a/tabby-core/src/services/profiles.service.ts +++ b/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 { + 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|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 + } } diff --git a/tabby-serial/src/api.ts b/tabby-serial/src/api.ts index 03fcaadb..67624899 100644 --- a/tabby-serial/src/api.ts +++ b/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 = [ diff --git a/tabby-serial/src/profiles.ts b/tabby-serial/src/profiles.ts index b3cf04df..639119d4 100644 --- a/tabby-serial/src/profiles.ts +++ b/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, diff --git a/tabby-settings/src/components/editProfileModal.component.ts b/tabby-settings/src/components/editProfileModal.component.ts index 3e2bcea0..d0a289f6 100644 --- a/tabby-settings/src/components/editProfileModal.component.ts +++ b/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 () { diff --git a/tabby-settings/src/components/profilesSettingsTab.component.ts b/tabby-settings/src/components/profilesSettingsTab.component.ts index 57886b06..fbddb892 100644 --- a/tabby-settings/src/components/profilesSettingsTab.component.ts +++ b/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() } diff --git a/tabby-ssh/src/components/sshProfileSettings.component.ts b/tabby-ssh/src/components/sshProfileSettings.component.ts index e7a30c9e..868130cb 100644 --- a/tabby-ssh/src/components/sshProfileSettings.component.ts +++ b/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 = {} - defaultAlgorithms: Record = {} algorithms: Record> = {} 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 diff --git a/tabby-ssh/src/profiles.ts b/tabby-ssh/src/profiles.ts index eba437bb..3d4dfb93 100644 --- a/tabby-ssh/src/profiles.ts +++ b/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 = {} 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 { diff --git a/tabby-telnet/src/profiles.ts b/tabby-telnet/src/profiles.ts index 44b906aa..0163d64c 100644 --- a/tabby-telnet/src/profiles.ts +++ b/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 { return [