mirror of
https://github.com/Eugeny/tabby
synced 2024-12-04 18:40:16 +00:00
Merge pull request #8726 from Clem-Fern/profiles-rework
This commit is contained in:
commit
4684b0d6f5
39 changed files with 786 additions and 274 deletions
|
@ -16,7 +16,7 @@ export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess'
|
|||
export { HostWindowService } from './hostWindow'
|
||||
export { HostAppService, Platform } from './hostApp'
|
||||
export { FileProvider } from './fileProvider'
|
||||
export { ProfileProvider, Profile, PartialProfile, ProfileSettingsComponent } from './profileProvider'
|
||||
export { ProfileProvider, ConnectableProfileProvider, QuickConnectProfileProvider, Profile, ConnectableProfile, PartialProfile, ProfileSettingsComponent, ProfileGroup, PartialProfileGroup } from './profileProvider'
|
||||
export { PromptModalComponent } from '../components/promptModal.component'
|
||||
export * from './commands'
|
||||
|
||||
|
|
|
@ -21,6 +21,10 @@ export interface Profile {
|
|||
isTemplate: boolean
|
||||
}
|
||||
|
||||
export interface ConnectableProfile extends Profile {
|
||||
clearServiceMessagesOnConnect: boolean
|
||||
}
|
||||
|
||||
export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
|
||||
[K in keyof T]?: T[K]
|
||||
}, 'options'>, 'type'>, 'name'> & {
|
||||
|
@ -31,6 +35,21 @@ export type PartialProfile<T extends Profile> = Omit<Omit<Omit<{
|
|||
}
|
||||
}
|
||||
|
||||
export interface ProfileGroup {
|
||||
id: string
|
||||
name: string
|
||||
profiles: PartialProfile<Profile>[]
|
||||
defaults: any
|
||||
editable: boolean
|
||||
}
|
||||
|
||||
export type PartialProfileGroup<T extends ProfileGroup> = Omit<Omit<{
|
||||
[K in keyof T]?: T[K]
|
||||
}, 'id'>, 'name'> & {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ProfileSettingsComponent<P extends Profile> {
|
||||
profile: P
|
||||
save?: () => void
|
||||
|
@ -39,7 +58,6 @@ export interface ProfileSettingsComponent<P extends Profile> {
|
|||
export abstract class ProfileProvider<P extends Profile> {
|
||||
id: string
|
||||
name: string
|
||||
supportsQuickConnect = false
|
||||
settingsComponent?: new (...args: any[]) => ProfileSettingsComponent<P>
|
||||
configDefaults = {}
|
||||
|
||||
|
@ -53,13 +71,15 @@ export abstract class ProfileProvider<P extends Profile> {
|
|||
|
||||
abstract getDescription (profile: PartialProfile<P>): string
|
||||
|
||||
quickConnect (query: string): PartialProfile<P>|null {
|
||||
return null
|
||||
}
|
||||
|
||||
intoQuickConnectString (profile: P): string|null {
|
||||
return null
|
||||
}
|
||||
|
||||
deleteProfile (profile: P): void { }
|
||||
}
|
||||
|
||||
export abstract class ConnectableProfileProvider<P extends ConnectableProfile> extends ProfileProvider<P> {}
|
||||
|
||||
export abstract class QuickConnectProfileProvider<P extends ConnectableProfile> extends ConnectableProfileProvider<P> {
|
||||
|
||||
abstract quickConnect (query: string): PartialProfile<P>|null
|
||||
|
||||
abstract intoQuickConnectString (profile: P): string|null
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export class CoreCommandProvider extends CommandProvider {
|
|||
}
|
||||
|
||||
async activate () {
|
||||
const profile = await this.profilesService.showProfileSelector()
|
||||
const profile = await this.profilesService.showProfileSelector().catch(() => null)
|
||||
if (profile) {
|
||||
this.profilesService.launchProfile(profile)
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ export class SelectorModalComponent<T> {
|
|||
{ sort: true },
|
||||
).search(f)
|
||||
|
||||
this.options.filter(x => x.freeInputPattern).forEach(freeOption => {
|
||||
this.options.filter(x => x.freeInputPattern).sort(firstBy<SelectorOption<T>, number>(x => x.weight ?? 0)).forEach(freeOption => {
|
||||
if (!this.filteredOptions.includes(freeOption)) {
|
||||
this.filteredOptions.push(freeOption)
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ hotkeys:
|
|||
profile-selectors:
|
||||
__nonStructural: true
|
||||
profiles: []
|
||||
groups: []
|
||||
profileDefaults:
|
||||
__nonStructural: true
|
||||
ssh:
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'
|
|||
import { TranslateService } from '@ngx-translate/core'
|
||||
import { ProfilesService } from './services/profiles.service'
|
||||
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
|
||||
import { PartialProfile, Profile } from './api'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
|
@ -268,7 +267,7 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
|||
return [
|
||||
...this.hotkeys,
|
||||
...profiles.map(profile => ({
|
||||
id: `profile.${AppHotkeyProvider.getProfileHotkeyName(profile)}`,
|
||||
id: `profile.${ProfilesService.getProfileHotkeyName(profile)}`,
|
||||
name: this.translate.instant('New tab: {profile}', { profile: profile.name }),
|
||||
})),
|
||||
...this.profilesService.getProviders().map(provider => ({
|
||||
|
@ -278,7 +277,4 @@ export class AppHotkeyProvider extends HotkeyProvider {
|
|||
]
|
||||
}
|
||||
|
||||
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
|
||||
return (profile.id ?? profile.name).replace(/\./g, '-')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
|||
import { DropZoneDirective } from './directives/dropZone.directive'
|
||||
import { CdkAutoDropGroup } from './directives/cdkAutoDropGroup.directive'
|
||||
|
||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ProfilesService, ProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api'
|
||||
import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService, FileProvider, ProfilesService, ProfileProvider, QuickConnectProfileProvider, SelectorOption, Profile, SelectorService, CommandProvider } from './api'
|
||||
|
||||
import { AppService } from './services/app.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
|
@ -177,7 +177,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
|||
if (hotkey.startsWith('profile.')) {
|
||||
const id = hotkey.substring(hotkey.indexOf('.') + 1)
|
||||
const profiles = await profilesService.getProfiles()
|
||||
const profile = profiles.find(x => AppHotkeyProvider.getProfileHotkeyName(x) === id)
|
||||
const profile = profiles.find(x => ProfilesService.getProfileHotkeyName(x) === id)
|
||||
if (profile) {
|
||||
profilesService.openNewTabForProfile(profile)
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
|||
this.showSelector(provider)
|
||||
}
|
||||
if (hotkey === 'command-selector') {
|
||||
commands.showSelector()
|
||||
commands.showSelector().catch(() => {return})
|
||||
}
|
||||
|
||||
if (hotkey === 'profile-selector') {
|
||||
|
@ -214,7 +214,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
|||
callback: () => this.profilesService.openNewTabForProfile(p),
|
||||
}))
|
||||
|
||||
if (provider.supportsQuickConnect) {
|
||||
if (provider instanceof QuickConnectProfileProvider) {
|
||||
options.push({
|
||||
name: this.translate.instant('Quick connect'),
|
||||
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
||||
|
@ -229,7 +229,7 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
|
|||
})
|
||||
}
|
||||
|
||||
await this.selector.show(this.translate.instant('Select profile'), options)
|
||||
await this.selector.show(this.translate.instant('Select profile'), options).catch(() => {return})
|
||||
}
|
||||
|
||||
static forRoot (): ModuleWithProviders<AppModule> {
|
||||
|
|
|
@ -101,7 +101,7 @@ export class CommandService {
|
|||
context.tab = tab.getFocusedTab() ?? undefined
|
||||
}
|
||||
const commands = await this.getCommands(context)
|
||||
await this.selector.show(
|
||||
return this.selector.show(
|
||||
this.translate.instant('Commands'),
|
||||
commands.map(c => ({
|
||||
name: c.label,
|
||||
|
@ -109,6 +109,6 @@ export class CommandService {
|
|||
description: c.sublabel,
|
||||
icon: c.icon,
|
||||
})),
|
||||
)
|
||||
).then(() => {return})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { PlatformService } from '../api/platform'
|
|||
import { HostAppService } from '../api/hostApp'
|
||||
import { Vault, VaultService } from './vault.service'
|
||||
import { serializeFunction } from '../utils'
|
||||
import { PartialProfileGroup, ProfileGroup } from '../api/profileProvider'
|
||||
const deepmerge = require('deepmerge')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
|
@ -364,6 +365,45 @@ export class ConfigService {
|
|||
}
|
||||
config.version = 4
|
||||
}
|
||||
if (config.version < 5) {
|
||||
const groups: PartialProfileGroup<ProfileGroup>[] = []
|
||||
for (const p of config.profiles ?? []) {
|
||||
if (!(p.group ?? '').trim()) {
|
||||
continue
|
||||
}
|
||||
|
||||
let group = groups.find(x => x.name === p.group)
|
||||
if (!group) {
|
||||
group = {
|
||||
id: `${uuidv4()}`,
|
||||
name: `${p.group}`,
|
||||
}
|
||||
groups.push(group)
|
||||
}
|
||||
p.group = group.id
|
||||
}
|
||||
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
for (const g of groups) {
|
||||
if (profileGroupCollapsed[g.name]) {
|
||||
const collapsed = profileGroupCollapsed[g.name]
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete profileGroupCollapsed[g.name]
|
||||
profileGroupCollapsed[g.id] = collapsed
|
||||
}
|
||||
}
|
||||
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
|
||||
|
||||
config.groups = groups
|
||||
config.version = 5
|
||||
}
|
||||
if (config.version < 6) {
|
||||
if (config.ssh.clearServiceMessagesOnConnect === false) {
|
||||
config.profileDefaults.ssh.clearServiceMessagesOnConnect = false
|
||||
delete config.ssh?.clearServiceMessagesOnConnect
|
||||
}
|
||||
config.version = 6
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeDecryptConfig (store) {
|
||||
|
|
|
@ -13,8 +13,9 @@ export class FileProvidersService {
|
|||
) { }
|
||||
|
||||
async selectAndStoreFile (description: string): Promise<string> {
|
||||
const p = await this.selectProvider()
|
||||
return p.selectAndStoreFile(description)
|
||||
return this.selectProvider().then(p => {
|
||||
return p.selectAndStoreFile(description)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieveFile (key: string): Promise<Buffer> {
|
||||
|
|
|
@ -2,12 +2,15 @@ import { Injectable, Inject } from '@angular/core'
|
|||
import { TranslateService } from '@ngx-translate/core'
|
||||
import { NewTabParameters } from './tabs.service'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { PartialProfile, Profile, ProfileProvider } from '../api/profileProvider'
|
||||
import { QuickConnectProfileProvider, PartialProfile, PartialProfileGroup, Profile, ProfileGroup, ProfileProvider } from '../api/profileProvider'
|
||||
import { SelectorOption } from '../api/selector'
|
||||
import { AppService } from './app.service'
|
||||
import { configMerge, ConfigProxy, ConfigService } from './config.service'
|
||||
import { NotificationsService } from './notifications.service'
|
||||
import { SelectorService } from './selector.service'
|
||||
import deepClone from 'clone-deep'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import slugify from 'slugify'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProfilesService {
|
||||
|
@ -36,6 +39,126 @@ export class ProfilesService {
|
|||
@Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[],
|
||||
) { }
|
||||
|
||||
/*
|
||||
* Methods used to interract with ProfileProvider
|
||||
*/
|
||||
|
||||
getProviders (): ProfileProvider<Profile>[] {
|
||||
return [...this.profileProviders]
|
||||
}
|
||||
|
||||
providerForProfile <T extends Profile> (profile: PartialProfile<T>): ProfileProvider<T>|null {
|
||||
const provider = this.profileProviders.find(x => x.id === profile.type) ?? null
|
||||
return provider as unknown as ProfileProvider<T>|null
|
||||
}
|
||||
|
||||
getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
|
||||
profile = this.getConfigProxyForProfile(profile)
|
||||
return this.providerForProfile(profile)?.getDescription(profile) ?? null
|
||||
}
|
||||
|
||||
/*
|
||||
* Methods used to interract with Profile
|
||||
*/
|
||||
|
||||
/*
|
||||
* Return ConfigProxy for a given Profile
|
||||
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
|
||||
* arg: skipGroupDefaults -> do not merge parent group provider defaults in ConfigProxy
|
||||
*/
|
||||
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): T {
|
||||
const defaults = this.getProfileDefaults(profile, options).reduce(configMerge, {})
|
||||
return new ConfigProxy(profile, defaults) as unknown as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an Array of Profiles
|
||||
* arg: includeBuiltin (default: true) -> include BuiltinProfiles
|
||||
* arg: clone (default: false) -> return deepclone Array
|
||||
*/
|
||||
async getProfiles (options?: { includeBuiltin?: boolean, clone?: boolean }): Promise<PartialProfile<Profile>[]> {
|
||||
let list = this.config.store.profiles ?? []
|
||||
if (options?.includeBuiltin ?? true) {
|
||||
const lists = await Promise.all(this.config.enabledServices(this.profileProviders).map(x => x.getBuiltinProfiles()))
|
||||
list = [
|
||||
...this.config.store.profiles ?? [],
|
||||
...lists.reduce((a, b) => a.concat(b), []),
|
||||
]
|
||||
}
|
||||
|
||||
const sortKey = p => `${this.resolveProfileGroupName(p.group ?? '')} / ${p.name}`
|
||||
list.sort((a, b) => sortKey(a).localeCompare(sortKey(b)))
|
||||
list.sort((a, b) => (a.isBuiltin ? 1 : 0) - (b.isBuiltin ? 1 : 0))
|
||||
return options?.clone ? deepClone(list) : list
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new Profile in config
|
||||
* arg: genId (default: true) -> generate uuid in before pushing Profile into config
|
||||
*/
|
||||
async newProfile (profile: PartialProfile<Profile>, options?: { genId?: boolean }): Promise<void> {
|
||||
if (options?.genId ?? true) {
|
||||
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
|
||||
}
|
||||
|
||||
const cProfile = this.config.store.profiles.find(p => p.id === profile.id)
|
||||
if (cProfile) {
|
||||
throw new Error(`Cannot insert new Profile, duplicated Id: ${profile.id}`)
|
||||
}
|
||||
|
||||
this.config.store.profiles.push(profile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a Profile in config
|
||||
*/
|
||||
async writeProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||
const cProfile = this.config.store.profiles.find(p => p.id === profile.id)
|
||||
if (cProfile) {
|
||||
if (!profile.group) {
|
||||
delete cProfile.group
|
||||
}
|
||||
|
||||
Object.assign(cProfile, profile)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a Profile from config
|
||||
*/
|
||||
async deleteProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||
this.providerForProfile(profile)?.deleteProfile(this.getConfigProxyForProfile(profile))
|
||||
this.config.store.profiles = this.config.store.profiles.filter(p => p.id !== profile.id)
|
||||
|
||||
const profileHotkeyName = ProfilesService.getProfileHotkeyName(profile)
|
||||
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
|
||||
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete profileHotkeys[profileHotkeyName]
|
||||
this.config.store.hotkeys.profile = profileHotkeys
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all Profiles from config using option filter
|
||||
* arg: filter (p: PartialProfile<Profile>) => boolean -> predicate used to decide which profiles have to be deleted
|
||||
*/
|
||||
async bulkDeleteProfiles (filter: (p: PartialProfile<Profile>) => boolean): Promise<void> {
|
||||
for (const profile of this.config.store.profiles.filter(filter)) {
|
||||
this.providerForProfile(profile)?.deleteProfile(this.getConfigProxyForProfile(profile))
|
||||
|
||||
const profileHotkeyName = ProfilesService.getProfileHotkeyName(profile)
|
||||
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
|
||||
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete profileHotkeys[profileHotkeyName]
|
||||
this.config.store.hotkeys.profile = profileHotkeys
|
||||
}
|
||||
}
|
||||
|
||||
this.config.store.profiles = this.config.store.profiles.filter(x => !filter(x))
|
||||
}
|
||||
|
||||
async openNewTabForProfile <P extends Profile> (profile: PartialProfile<P>): Promise<BaseTabComponent|null> {
|
||||
const params = await this.newTabParametersForProfile(profile)
|
||||
if (params) {
|
||||
|
@ -63,52 +186,40 @@ export class ProfilesService {
|
|||
return params
|
||||
}
|
||||
|
||||
getProviders (): ProfileProvider<Profile>[] {
|
||||
return [...this.profileProviders]
|
||||
async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||
await this.openNewTabForProfile(profile)
|
||||
|
||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||
if (this.config.store.terminal.showRecentProfiles > 0) {
|
||||
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
||||
recentProfiles.unshift(profile)
|
||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||
} else {
|
||||
recentProfiles = []
|
||||
}
|
||||
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
|
||||
}
|
||||
|
||||
async getProfiles (): Promise<PartialProfile<Profile>[]> {
|
||||
const lists = await Promise.all(this.config.enabledServices(this.profileProviders).map(x => x.getBuiltinProfiles()))
|
||||
let list = lists.reduce((a, b) => a.concat(b), [])
|
||||
list = [
|
||||
...this.config.store.profiles ?? [],
|
||||
...list,
|
||||
]
|
||||
const sortKey = p => `${p.group ?? ''} / ${p.name}`
|
||||
list.sort((a, b) => sortKey(a).localeCompare(sortKey(b)))
|
||||
list.sort((a, b) => (a.isBuiltin ? 1 : 0) - (b.isBuiltin ? 1 : 0))
|
||||
return list
|
||||
static getProfileHotkeyName (profile: PartialProfile<Profile>): string {
|
||||
return (profile.id ?? profile.name).replace(/\./g, '-')
|
||||
}
|
||||
|
||||
providerForProfile <T extends Profile> (profile: PartialProfile<T>): ProfileProvider<T>|null {
|
||||
const provider = this.profileProviders.find(x => x.id === profile.type) ?? null
|
||||
return provider as unknown as ProfileProvider<T>|null
|
||||
}
|
||||
|
||||
getDescription <P extends Profile> (profile: PartialProfile<P>): string|null {
|
||||
profile = this.getConfigProxyForProfile(profile)
|
||||
return this.providerForProfile(profile)?.getDescription(profile) ?? null
|
||||
}
|
||||
/*
|
||||
* Methods used to interract with Profile Selector
|
||||
*/
|
||||
|
||||
selectorOptionForProfile <P extends Profile, T> (profile: PartialProfile<P>): SelectorOption<T> {
|
||||
const fullProfile = this.getConfigProxyForProfile(profile)
|
||||
const provider = this.providerForProfile(fullProfile)
|
||||
const freeInputEquivalent = provider?.intoQuickConnectString(fullProfile) ?? undefined
|
||||
const freeInputEquivalent = provider instanceof QuickConnectProfileProvider ? provider.intoQuickConnectString(fullProfile) ?? undefined : undefined
|
||||
return {
|
||||
...profile,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
group: profile.group || '',
|
||||
group: this.resolveProfileGroupName(profile.group ?? ''),
|
||||
freeInputEquivalent,
|
||||
description: provider?.getDescription(fullProfile),
|
||||
}
|
||||
}
|
||||
|
||||
getRecentProfiles (): PartialProfile<Profile>[] {
|
||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||
return recentProfiles
|
||||
}
|
||||
|
||||
showProfileSelector (): Promise<PartialProfile<Profile>|null> {
|
||||
if (this.selector.active) {
|
||||
return Promise.resolve(null)
|
||||
|
@ -118,12 +229,12 @@ export class ProfilesService {
|
|||
try {
|
||||
const recentProfiles = this.getRecentProfiles()
|
||||
|
||||
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
|
||||
let options: SelectorOption<void>[] = recentProfiles.map((p, i) => ({
|
||||
...this.selectorOptionForProfile(p),
|
||||
group: this.translate.instant('Recent'),
|
||||
icon: 'fas fa-history',
|
||||
color: p.color,
|
||||
weight: -2,
|
||||
weight: i - (recentProfiles.length + 1),
|
||||
callback: async () => {
|
||||
if (p.id) {
|
||||
p = (await this.getProfiles()).find(x => x.id === p.id) ?? p
|
||||
|
@ -177,30 +288,38 @@ export class ProfilesService {
|
|||
})
|
||||
} catch { }
|
||||
|
||||
this.getProviders().filter(x => x.supportsQuickConnect).forEach(provider => {
|
||||
options.push({
|
||||
name: this.translate.instant('Quick connect'),
|
||||
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
||||
description: `(${provider.name.toUpperCase()})`,
|
||||
icon: 'fas fa-arrow-right',
|
||||
weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0,
|
||||
callback: query => {
|
||||
const profile = provider.quickConnect(query)
|
||||
resolve(profile)
|
||||
},
|
||||
})
|
||||
this.getProviders().forEach(provider => {
|
||||
if (provider instanceof QuickConnectProfileProvider) {
|
||||
options.push({
|
||||
name: this.translate.instant('Quick connect'),
|
||||
freeInputPattern: this.translate.instant('Connect to "%s"...'),
|
||||
description: `(${provider.name.toUpperCase()})`,
|
||||
icon: 'fas fa-arrow-right',
|
||||
weight: provider.id !== this.config.store.defaultQuickConnectProvider ? 1 : 0,
|
||||
callback: query => {
|
||||
const profile = provider.quickConnect(query)
|
||||
resolve(profile)
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await this.selector.show(this.translate.instant('Select profile or enter an address'), options)
|
||||
await this.selector.show(this.translate.instant('Select profile or enter an address'), options).catch(() => reject())
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getRecentProfiles (): PartialProfile<Profile>[] {
|
||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||
return recentProfiles
|
||||
}
|
||||
|
||||
async quickConnect (query: string): Promise<PartialProfile<Profile>|null> {
|
||||
for (const provider of this.getProviders()) {
|
||||
if (provider.supportsQuickConnect) {
|
||||
if (provider instanceof QuickConnectProfileProvider) {
|
||||
const profile = provider.quickConnect(query)
|
||||
if (profile) {
|
||||
return profile
|
||||
|
@ -211,27 +330,178 @@ export class ProfilesService {
|
|||
return null
|
||||
}
|
||||
|
||||
getConfigProxyForProfile <T extends Profile> (profile: PartialProfile<T>, skipUserDefaults = false): T {
|
||||
/*
|
||||
* Methods used to interract with Profile/ProfileGroup/Global defaults
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return global defaults for a given profile provider
|
||||
* Always return something, empty object if no defaults found
|
||||
*/
|
||||
getProviderDefaults (provider: ProfileProvider<Profile>): any {
|
||||
const defaults = this.config.store.profileDefaults
|
||||
return defaults[provider.id] ?? {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set global defaults for a given profile provider
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
setProviderDefaults (provider: ProfileProvider<Profile>, pdefaults: any): void {
|
||||
this.config.store.profileDefaults[provider.id] = pdefaults
|
||||
}
|
||||
|
||||
/**
|
||||
* Return defaults for a given profile
|
||||
* Always return something, empty object if no defaults found
|
||||
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
|
||||
* arg: skipGroupDefaults -> do not merge parent group provider defaults in ConfigProxy
|
||||
*/
|
||||
getProfileDefaults (profile: PartialProfile<Profile>, options?: { skipGlobalDefaults?: boolean, skipGroupDefaults?: boolean }): any[] {
|
||||
const provider = this.providerForProfile(profile)
|
||||
const defaults = [
|
||||
|
||||
return [
|
||||
this.profileDefaults,
|
||||
provider?.configDefaults ?? {},
|
||||
!provider || skipUserDefaults ? {} : this.config.store.profileDefaults[provider.id] ?? {},
|
||||
].reduce(configMerge, {})
|
||||
return new ConfigProxy(profile, defaults) as unknown as T
|
||||
provider && !options?.skipGlobalDefaults ? this.getProviderDefaults(provider) : {},
|
||||
provider && !options?.skipGlobalDefaults && !options?.skipGroupDefaults ? this.getProviderProfileGroupDefaults(profile.group ?? '', provider) : {},
|
||||
]
|
||||
}
|
||||
|
||||
async launchProfile (profile: PartialProfile<Profile>): Promise<void> {
|
||||
await this.openNewTabForProfile(profile)
|
||||
/*
|
||||
* Methods used to interract with ProfileGroup
|
||||
*/
|
||||
|
||||
let recentProfiles: PartialProfile<Profile>[] = JSON.parse(window.localStorage['recentProfiles'] ?? '[]')
|
||||
if (this.config.store.terminal.showRecentProfiles > 0) {
|
||||
recentProfiles = recentProfiles.filter(x => x.group !== profile.group || x.name !== profile.name)
|
||||
recentProfiles.unshift(profile)
|
||||
recentProfiles = recentProfiles.slice(0, this.config.store.terminal.showRecentProfiles)
|
||||
} else {
|
||||
recentProfiles = []
|
||||
/**
|
||||
* Synchronously return an Array of the existing ProfileGroups
|
||||
* Does not return builtin groups
|
||||
*/
|
||||
getSyncProfileGroups (): PartialProfileGroup<ProfileGroup>[] {
|
||||
return deepClone(this.config.store.groups ?? [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an Array of the existing ProfileGroups
|
||||
* arg: includeProfiles (default: false) -> if false, does not fill up the profiles field of ProfileGroup
|
||||
* arg: includeNonUserGroup (default: false) -> if false, does not add built-in and ungrouped groups
|
||||
*/
|
||||
async getProfileGroups (options?: { includeProfiles?: boolean, includeNonUserGroup?: boolean }): Promise<PartialProfileGroup<ProfileGroup>[]> {
|
||||
let profiles: PartialProfile<Profile>[] = []
|
||||
if (options?.includeProfiles) {
|
||||
profiles = await this.getProfiles({ includeBuiltin: options.includeNonUserGroup, clone: true })
|
||||
}
|
||||
window.localStorage['recentProfiles'] = JSON.stringify(recentProfiles)
|
||||
|
||||
let groups: PartialProfileGroup<ProfileGroup>[] = this.getSyncProfileGroups()
|
||||
groups = groups.map(x => {
|
||||
x.editable = true
|
||||
|
||||
if (options?.includeProfiles) {
|
||||
x.profiles = profiles.filter(p => p.group === x.id)
|
||||
profiles = profiles.filter(p => p.group !== x.id)
|
||||
}
|
||||
|
||||
return x
|
||||
})
|
||||
|
||||
if (options?.includeNonUserGroup) {
|
||||
const builtInGroups: PartialProfileGroup<ProfileGroup>[] = []
|
||||
builtInGroups.push({
|
||||
id: 'built-in',
|
||||
name: this.translate.instant('Built-in'),
|
||||
editable: false,
|
||||
profiles: [],
|
||||
})
|
||||
|
||||
const ungrouped: PartialProfileGroup<ProfileGroup> = {
|
||||
id: 'ungrouped',
|
||||
name: this.translate.instant('Ungrouped'),
|
||||
editable: false,
|
||||
}
|
||||
|
||||
if (options.includeProfiles) {
|
||||
for (const profile of profiles.filter(p => p.isBuiltin)) {
|
||||
let group: PartialProfileGroup<ProfileGroup> | undefined = builtInGroups.find(g => g.id === slugify(profile.group ?? 'built-in'))
|
||||
if (!group) {
|
||||
group = {
|
||||
id: `${slugify(profile.group!)}`,
|
||||
name: `${profile.group!}`,
|
||||
editable: false,
|
||||
profiles: [],
|
||||
}
|
||||
builtInGroups.push(group)
|
||||
}
|
||||
|
||||
group.profiles!.push(profile)
|
||||
}
|
||||
|
||||
ungrouped.profiles = profiles.filter(p => !p.isBuiltin)
|
||||
}
|
||||
|
||||
groups = groups.concat(builtInGroups)
|
||||
groups.push(ungrouped)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new ProfileGroup in config
|
||||
* arg: genId (default: true) -> generate uuid in before pushing Profile into config
|
||||
*/
|
||||
async newProfileGroup (group: PartialProfileGroup<ProfileGroup>, options?: { genId?: boolean }): Promise<void> {
|
||||
if (options?.genId ?? true) {
|
||||
group.id = `${uuidv4()}`
|
||||
}
|
||||
|
||||
const cProfileGroup = this.config.store.groups.find(p => p.id === group.id)
|
||||
if (cProfileGroup) {
|
||||
throw new Error(`Cannot insert new ProfileGroup, duplicated Id: ${group.id}`)
|
||||
}
|
||||
|
||||
this.config.store.groups.push(group)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a ProfileGroup in config
|
||||
*/
|
||||
async writeProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
|
||||
delete group.profiles
|
||||
delete group.editable
|
||||
|
||||
const cGroup = this.config.store.groups.find(g => g.id === group.id)
|
||||
if (cGroup) {
|
||||
Object.assign(cGroup, group)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a ProfileGroup from config
|
||||
*/
|
||||
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>, options?: { deleteProfiles?: boolean }): Promise<void> {
|
||||
this.config.store.groups = this.config.store.groups.filter(g => g.id !== group.id)
|
||||
if (options?.deleteProfiles) {
|
||||
await this.bulkDeleteProfiles((p) => p.group === group.id)
|
||||
} else {
|
||||
for (const profile of this.config.store.profiles.filter(x => x.group === group.id)) {
|
||||
delete profile.group
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and return ProfileGroup Name from ProfileGroup ID
|
||||
*/
|
||||
resolveProfileGroupName (groupId: string): string {
|
||||
return this.config.store.groups.find(g => g.id === groupId)?.name ?? groupId
|
||||
}
|
||||
|
||||
/**
|
||||
* Return defaults for a given group ID and provider
|
||||
* Always return something, empty object if no defaults found
|
||||
* arg: skipUserDefaults -> do not merge global provider defaults in ConfigProxy
|
||||
*/
|
||||
getProviderProfileGroupDefaults (groupId: string, provider: ProfileProvider<Profile>): any {
|
||||
return this.getSyncProfileGroups().find(g => g.id === groupId)?.defaults?.[provider.id] ?? {}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -285,7 +285,7 @@ export class VaultFileProvider extends FileProvider {
|
|||
icon: 'fas fa-file',
|
||||
result: f,
|
||||
})),
|
||||
])
|
||||
]).catch(() => null)
|
||||
if (result) {
|
||||
return `${this.prefix}${result.key.id}`
|
||||
}
|
||||
|
|
|
@ -149,7 +149,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
|||
click: async () => {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = this.translate.instant('Profile name')
|
||||
const name = (await modal.result)?.value
|
||||
const name = (await modal.result.catch(() => null))?.value
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
|
@ -262,7 +262,7 @@ export class ProfilesContextMenu extends TabContextMenuItemProvider {
|
|||
}
|
||||
|
||||
async switchTabProfile (tab: BaseTabComponent) {
|
||||
const profile = await this.profilesService.showProfileSelector()
|
||||
const profile = await this.profilesService.showProfileSelector().catch(() => null)
|
||||
if (!profile) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ import { SerialPortStream } from '@serialport/stream'
|
|||
import { LogService, NotificationsService } from 'tabby-core'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { Injector, NgZone } from '@angular/core'
|
||||
import { BaseSession, BaseTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal'
|
||||
import { BaseSession, ConnectableTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor, UTF8SplitterMiddleware } from 'tabby-terminal'
|
||||
import { SerialService } from './services/serial.service'
|
||||
|
||||
export interface SerialProfile extends BaseTerminalProfile {
|
||||
export interface SerialProfile extends ConnectableTerminalProfile {
|
||||
options: SerialProfileOptions
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
|
|||
import slugify from 'slugify'
|
||||
import deepClone from 'clone-deep'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ProfileProvider, NewTabParameters, SelectorService, HostAppService, Platform, TranslateService } from 'tabby-core'
|
||||
import { NewTabParameters, SelectorService, HostAppService, Platform, TranslateService, ConnectableProfileProvider } from 'tabby-core'
|
||||
import { SerialProfileSettingsComponent } from './components/serialProfileSettings.component'
|
||||
import { SerialTabComponent } from './components/serialTab.component'
|
||||
import { SerialService } from './services/serial.service'
|
||||
import { BAUD_RATES, SerialProfile } from './api'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SerialProfilesService extends ProfileProvider<SerialProfile> {
|
||||
export class SerialProfilesService extends ConnectableProfileProvider<SerialProfile> {
|
||||
id = 'serial'
|
||||
name = _('Serial')
|
||||
settingsComponent = SerialProfileSettingsComponent
|
||||
|
@ -32,6 +32,7 @@ export class SerialProfilesService extends ProfileProvider<SerialProfile> {
|
|||
slowSend: false,
|
||||
input: { backspace: 'backspace' },
|
||||
},
|
||||
clearServiceMessagesOnConnect: false,
|
||||
}
|
||||
|
||||
constructor (
|
||||
|
|
|
@ -59,7 +59,7 @@ export class ConfigSyncSettingsTabComponent extends BaseComponent {
|
|||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = this.translate.instant('Name for the new config')
|
||||
modal.componentInstance.value = name
|
||||
name = (await modal.result)?.value
|
||||
name = (await modal.result.catch(() => null))?.value
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
.modal-header
|
||||
h3.m-0 {{group.name}}
|
||||
|
||||
.modal-body
|
||||
.row
|
||||
.col-12.col-lg-4
|
||||
.mb-3
|
||||
label(translate) Name
|
||||
input.form-control(
|
||||
type='text',
|
||||
autofocus,
|
||||
[(ngModel)]='group.name',
|
||||
)
|
||||
|
||||
.col-12.col-lg-8
|
||||
.form-line.content-box
|
||||
.header
|
||||
.title(translate) Default profile group settings
|
||||
.description(translate) These apply to all profiles of a given type in this group
|
||||
|
||||
.list-group.mt-3.mb-3.content-box
|
||||
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
(click)='editDefaults(provider)',
|
||||
*ngFor='let provider of providers'
|
||||
) {{provider.name|translate}}
|
||||
.me-auto
|
||||
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); deleteDefaults(provider)')
|
||||
i.fas.fa-trash-arrow-up
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-primary((click)='save()', translate) Save
|
||||
button.btn.btn-danger((click)='cancel()', translate) Cancel
|
|
@ -0,0 +1,54 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigProxy, ProfileGroup, Profile, ProfileProvider, PlatformService, TranslateService } from 'tabby-core'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
templateUrl: './editProfileGroupModal.component.pug',
|
||||
})
|
||||
export class EditProfileGroupModalComponent<G extends ProfileGroup> {
|
||||
@Input() group: G & ConfigProxy
|
||||
@Input() providers: ProfileProvider<Profile>[]
|
||||
|
||||
constructor (
|
||||
private modalInstance: NgbActiveModal,
|
||||
private platform: PlatformService,
|
||||
private translate: TranslateService,
|
||||
) {}
|
||||
|
||||
save () {
|
||||
this.modalInstance.close({ group: this.group })
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.modalInstance.dismiss()
|
||||
}
|
||||
|
||||
editDefaults (provider: ProfileProvider<Profile>) {
|
||||
this.modalInstance.close({ group: this.group, provider })
|
||||
}
|
||||
|
||||
async deleteDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
||||
if ((await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: this.translate.instant('Restore settings to inherited defaults ?'),
|
||||
buttons: [
|
||||
this.translate.instant('Delete'),
|
||||
this.translate.instant('Keep'),
|
||||
],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
},
|
||||
)).response === 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete this.group.defaults?.[provider.id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface EditProfileGroupModalComponentResult<G extends ProfileGroup> {
|
||||
group: G
|
||||
provider?: ProfileProvider<Profile>
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
.modal-header(*ngIf='!defaultsMode')
|
||||
.modal-header(*ngIf='defaultsMode === "disabled"')
|
||||
h3.m-0 {{profile.name}}
|
||||
|
||||
.modal-header(*ngIf='defaultsMode')
|
||||
.modal-header(*ngIf='defaultsMode !== "disabled"')
|
||||
h3.m-0(
|
||||
translate='Defaults for {type}',
|
||||
[translateParams]='{type: profileProvider.name}'
|
||||
|
@ -10,7 +10,7 @@
|
|||
.modal-body
|
||||
.row
|
||||
.col-12.col-lg-4
|
||||
.mb-3(*ngIf='!defaultsMode')
|
||||
.mb-3(*ngIf='defaultsMode === "disabled"')
|
||||
label(translate) Name
|
||||
input.form-control(
|
||||
type='text',
|
||||
|
@ -18,17 +18,20 @@
|
|||
[(ngModel)]='profile.name',
|
||||
)
|
||||
|
||||
.mb-3(*ngIf='!defaultsMode')
|
||||
.mb-3(*ngIf='defaultsMode === "disabled"')
|
||||
label(translate) Group
|
||||
input.form-control(
|
||||
type='text',
|
||||
alwaysVisibleTypeahead,
|
||||
placeholder='Ungrouped',
|
||||
[(ngModel)]='profile.group',
|
||||
[(ngModel)]='profileGroup',
|
||||
[ngbTypeahead]='groupTypeahead',
|
||||
[inputFormatter]="groupFormatter",
|
||||
[resultFormatter]="groupFormatter",
|
||||
[editable]="false"
|
||||
)
|
||||
|
||||
.mb-3(*ngIf='!defaultsMode')
|
||||
.mb-3(*ngIf='defaultsMode === "disabled"')
|
||||
label(translate) Icon
|
||||
.input-group
|
||||
input.form-control(
|
||||
|
@ -74,9 +77,15 @@
|
|||
)
|
||||
option(ngValue='auto', translate) Auto
|
||||
option(ngValue='keep', translate) Keep
|
||||
option(*ngIf='profile.type == "serial" || profile.type == "telnet" || profile.type == "ssh"', ngValue='reconnect', translate) Reconnect
|
||||
option(*ngIf='isConnectable()', ngValue='reconnect', translate) Reconnect
|
||||
option(ngValue='close', translate) Close
|
||||
|
||||
|
||||
.form-line(*ngIf='isConnectable()')
|
||||
.header
|
||||
.title(translate) Clear terminal after connection
|
||||
toggle(
|
||||
[(ngModel)]='profile.clearServiceMessagesOnConnect',
|
||||
)
|
||||
.mb-4
|
||||
|
||||
.col-12.col-lg-8(*ngIf='this.profileProvider.settingsComponent')
|
||||
|
|
|
@ -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 { ConfigProxy, ConfigService, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS } from 'tabby-core'
|
||||
import { ConfigProxy, PartialProfileGroup, Profile, ProfileProvider, ProfileSettingsComponent, ProfilesService, TAB_COLORS, ProfileGroup, ConnectableProfileProvider } from 'tabby-core'
|
||||
|
||||
const iconsData = require('../../../tabby-core/src/icons.json')
|
||||
const iconsClassList = Object.keys(iconsData).map(
|
||||
|
@ -19,8 +19,9 @@ export class EditProfileModalComponent<P extends Profile> {
|
|||
@Input() profile: P & ConfigProxy
|
||||
@Input() profileProvider: ProfileProvider<P>
|
||||
@Input() settingsComponent: new () => ProfileSettingsComponent<P>
|
||||
@Input() defaultsMode = false
|
||||
groupNames: string[]
|
||||
@Input() defaultsMode: 'enabled'|'group'|'disabled' = 'disabled'
|
||||
@Input() profileGroup: PartialProfileGroup<ProfileGroup> | undefined
|
||||
groups: PartialProfileGroup<ProfileGroup>[]
|
||||
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
|
||||
|
||||
private _profile: Profile
|
||||
|
@ -30,14 +31,14 @@ export class EditProfileModalComponent<P extends Profile> {
|
|||
private injector: Injector,
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private profilesService: ProfilesService,
|
||||
config: ConfigService,
|
||||
private modalInstance: NgbActiveModal,
|
||||
) {
|
||||
this.groupNames = [...new Set(
|
||||
(config.store.profiles as Profile[])
|
||||
.map(x => x.group)
|
||||
.filter(x => !!x),
|
||||
)].sort() as string[]
|
||||
if (this.defaultsMode === 'disabled') {
|
||||
this.profilesService.getProfileGroups().then(groups => {
|
||||
this.groups = groups
|
||||
this.profileGroup = groups.find(g => g.id === this.profile.group)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
colorsAutocomplete = text$ => text$.pipe(
|
||||
|
@ -56,7 +57,7 @@ export class EditProfileModalComponent<P extends Profile> {
|
|||
|
||||
ngOnInit () {
|
||||
this._profile = this.profile
|
||||
this.profile = this.profilesService.getConfigProxyForProfile(this.profile, this.defaultsMode)
|
||||
this.profile = this.profilesService.getConfigProxyForProfile(this.profile, { skipGlobalDefaults: this.defaultsMode === 'enabled', skipGroupDefaults: this.defaultsMode === 'group' })
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
|
@ -72,13 +73,15 @@ export class EditProfileModalComponent<P extends Profile> {
|
|||
}
|
||||
}
|
||||
|
||||
groupTypeahead = (text$: Observable<string>) =>
|
||||
groupTypeahead: OperatorFunction<string, readonly PartialProfileGroup<ProfileGroup>[]> = (text$: Observable<string>) =>
|
||||
text$.pipe(
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
map(q => this.groupNames.filter(x => !q || x.toLowerCase().includes(q.toLowerCase()))),
|
||||
map(q => this.groups.filter(g => !q || g.name.toLowerCase().includes(q.toLowerCase()))),
|
||||
)
|
||||
|
||||
groupFormatter = (g: PartialProfileGroup<ProfileGroup>) => g.name
|
||||
|
||||
iconSearch: OperatorFunction<string, string[]> = (text$: Observable<string>) =>
|
||||
text$.pipe(
|
||||
debounceTime(200),
|
||||
|
@ -86,7 +89,12 @@ export class EditProfileModalComponent<P extends Profile> {
|
|||
)
|
||||
|
||||
save () {
|
||||
this.profile.group ||= undefined
|
||||
if (!this.profileGroup) {
|
||||
this.profile.group = undefined
|
||||
} else {
|
||||
this.profile.group = this.profileGroup.id
|
||||
}
|
||||
|
||||
this.settingsComponentInstance?.save?.()
|
||||
this.profile.__cleanup()
|
||||
this.modalInstance.close(this._profile)
|
||||
|
@ -95,4 +103,9 @@ export class EditProfileModalComponent<P extends Profile> {
|
|||
cancel () {
|
||||
this.modalInstance.dismiss()
|
||||
}
|
||||
|
||||
isConnectable (): boolean {
|
||||
return this.profileProvider instanceof ConnectableProfileProvider
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,9 +27,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||
i.fas.fa-fw.fa-search
|
||||
input.form-control(type='search', [placeholder]='"Filter"|translate', [(ngModel)]='filter')
|
||||
|
||||
button.btn.btn-primary.flex-shrink-0.ms-3((click)='newProfile()')
|
||||
i.fas.fa-fw.fa-plus
|
||||
span(translate) New profile
|
||||
div(ngbDropdown).d-inline-block.flex-shrink-0.ms-3
|
||||
button.btn.btn-primary(ngbDropdownToggle)
|
||||
i.fas.fa-fw.fa-plus
|
||||
span(translate) New
|
||||
div(ngbDropdownMenu)
|
||||
button(ngbDropdownItem, (click)='newProfile()')
|
||||
i.fas.fa-fw.fa-plus
|
||||
span(translate) New profile
|
||||
button(ngbDropdownItem, (click)='newProfileGroup()')
|
||||
i.fas.fa-fw.fa-plus
|
||||
span(translate) New profile Group
|
||||
|
||||
.list-group.mt-3.mb-3
|
||||
ng-container(*ngFor='let group of profileGroups')
|
||||
|
@ -37,17 +45,17 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
(click)='toggleGroupCollapse(group)'
|
||||
)
|
||||
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed')
|
||||
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed')
|
||||
.fa.fa-fw.fa-chevron-right(*ngIf='group.collapsed && group.profiles?.length > 0')
|
||||
.fa.fa-fw.fa-chevron-down(*ngIf='!group.collapsed && group.profiles?.length > 0')
|
||||
span.ms-3.me-auto {{group.name || ("Ungrouped"|translate)}}
|
||||
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
|
||||
*ngIf='group.editable && group.name',
|
||||
(click)='$event.stopPropagation(); editGroup(group)'
|
||||
(click)='$event.stopPropagation(); editProfileGroup(group)'
|
||||
)
|
||||
i.fas.fa-pencil-alt
|
||||
button.btn.btn-sm.btn-link.hover-reveal.ms-2(
|
||||
*ngIf='group.editable && group.name',
|
||||
(click)='$event.stopPropagation(); deleteGroup(group)'
|
||||
(click)='$event.stopPropagation(); deleteProfileGroup(group)'
|
||||
)
|
||||
i.fas.fa-trash-alt
|
||||
ng-container(*ngIf='!group.collapsed')
|
||||
|
@ -67,7 +75,7 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||
|
||||
.me-auto
|
||||
|
||||
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); launchProfile(profile)')
|
||||
button.btn.btn-link.hover-reveal.ms-1(*ngIf='!profile.isTemplate', (click)='$event.stopPropagation(); launchProfile(profile)')
|
||||
i.fas.fa-play
|
||||
|
||||
.ms-1.hover-reveal(ngbDropdown, placement='bottom-right top-right auto')
|
||||
|
@ -169,9 +177,12 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||
.description(translate) These apply to all profiles of a given type
|
||||
|
||||
.list-group.mt-3.mb-3.content-box
|
||||
a.list-group-item.list-group-item-action(
|
||||
a.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
(click)='editDefaults(provider)',
|
||||
*ngFor='let provider of profileProviders'
|
||||
) {{provider.name|translate}}
|
||||
.me-auto
|
||||
button.btn.btn-link.hover-reveal.ms-1((click)='$event.stopPropagation(); deleteDefaults(provider)')
|
||||
i.fas.fa-trash-arrow-up
|
||||
|
||||
div([ngbNavOutlet]='nav')
|
||||
|
|
|
@ -1,32 +1,27 @@
|
|||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import slugify from 'slugify'
|
||||
import deepClone from 'clone-deep'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, AppHotkeyProvider } from 'tabby-core'
|
||||
import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, ProfileGroup, PartialProfileGroup, QuickConnectProfileProvider } from 'tabby-core'
|
||||
import { EditProfileModalComponent } from './editProfileModal.component'
|
||||
|
||||
interface ProfileGroup {
|
||||
name?: string
|
||||
profiles: PartialProfile<Profile>[]
|
||||
editable: boolean
|
||||
collapsed: boolean
|
||||
}
|
||||
import { EditProfileGroupModalComponent, EditProfileGroupModalComponentResult } from './editProfileGroupModal.component'
|
||||
|
||||
_('Filter')
|
||||
_('Ungrouped')
|
||||
|
||||
interface CollapsableProfileGroup extends ProfileGroup {
|
||||
collapsed: boolean
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
templateUrl: './profilesSettingsTab.component.pug',
|
||||
styleUrls: ['./profilesSettingsTab.component.scss'],
|
||||
})
|
||||
export class ProfilesSettingsTabComponent extends BaseComponent {
|
||||
profiles: PartialProfile<Profile>[] = []
|
||||
builtinProfiles: PartialProfile<Profile>[] = []
|
||||
templateProfiles: PartialProfile<Profile>[] = []
|
||||
profileGroups: ProfileGroup[]
|
||||
profileGroups: PartialProfileGroup<CollapsableProfileGroup>[]
|
||||
filter = ''
|
||||
Platform = Platform
|
||||
|
||||
|
@ -59,7 +54,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
|
||||
async newProfile (base?: PartialProfile<Profile>): Promise<void> {
|
||||
if (!base) {
|
||||
let profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
|
||||
let profiles = await this.profilesService.getProfiles()
|
||||
profiles = profiles.filter(x => !this.isProfileBlacklisted(x))
|
||||
profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
|
||||
base = await this.selector.show(
|
||||
|
@ -67,10 +62,13 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
profiles.map(p => ({
|
||||
icon: p.icon,
|
||||
description: this.profilesService.getDescription(p) ?? undefined,
|
||||
name: p.group ? `${p.group} / ${p.name}` : p.name,
|
||||
name: p.group ? `${this.profilesService.resolveProfileGroupName(p.group)} / ${p.name}` : p.name,
|
||||
result: p,
|
||||
})),
|
||||
)
|
||||
).catch(() => undefined)
|
||||
if (!base) {
|
||||
return
|
||||
}
|
||||
}
|
||||
const profile: PartialProfile<Profile> = deepClone(base)
|
||||
delete profile.id
|
||||
|
@ -90,8 +88,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
|
||||
profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base)
|
||||
}
|
||||
profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
|
||||
this.config.store.profiles = [profile, ...this.config.store.profiles]
|
||||
await this.profilesService.newProfile(profile)
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
|
@ -101,6 +98,7 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
return
|
||||
}
|
||||
Object.assign(profile, result)
|
||||
await this.profilesService.writeProfile(profile)
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
|
@ -144,69 +142,80 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
cancelId: 1,
|
||||
},
|
||||
)).response === 0) {
|
||||
this.profilesService.providerForProfile(profile)?.deleteProfile(
|
||||
this.profilesService.getConfigProxyForProfile(profile))
|
||||
this.config.store.profiles = this.config.store.profiles.filter(x => x !== profile)
|
||||
const profileHotkeyName = AppHotkeyProvider.getProfileHotkeyName(profile)
|
||||
if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
|
||||
const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete profileHotkeys[profileHotkeyName]
|
||||
this.config.store.hotkeys.profile = profileHotkeys
|
||||
}
|
||||
await this.profilesService.deleteProfile(profile)
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
refresh (): void {
|
||||
this.profiles = this.config.store.profiles
|
||||
this.profileGroups = []
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
|
||||
for (const profile of this.profiles) {
|
||||
// Group null, undefined and empty together
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
let group = this.profileGroups.find(x => x.name === (profile.group || ''))
|
||||
if (!group) {
|
||||
group = {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
name: profile.group || '',
|
||||
profiles: [],
|
||||
editable: true,
|
||||
collapsed: profileGroupCollapsed[profile.group ?? ''] ?? false,
|
||||
}
|
||||
this.profileGroups.push(group)
|
||||
}
|
||||
group.profiles.push(profile)
|
||||
}
|
||||
|
||||
this.profileGroups.sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? -1)
|
||||
|
||||
const builtIn = {
|
||||
name: this.translate.instant('Built-in'),
|
||||
profiles: this.builtinProfiles,
|
||||
editable: false,
|
||||
collapsed: false,
|
||||
}
|
||||
builtIn.collapsed = profileGroupCollapsed[builtIn.name ?? ''] ?? false
|
||||
this.profileGroups.push(builtIn)
|
||||
}
|
||||
|
||||
async editGroup (group: ProfileGroup): Promise<void> {
|
||||
async newProfileGroup (): Promise<void> {
|
||||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = this.translate.instant('New name')
|
||||
modal.componentInstance.value = group.name
|
||||
const result = await modal.result
|
||||
if (result) {
|
||||
for (const profile of this.profiles.filter(x => x.group === group.name)) {
|
||||
profile.group = result.value
|
||||
}
|
||||
this.config.store.profiles = this.profiles
|
||||
modal.componentInstance.prompt = this.translate.instant('New group name')
|
||||
const result = await modal.result.catch(() => null)
|
||||
if (result?.value.trim()) {
|
||||
await this.profilesService.newProfileGroup({ id: '', name: result.value })
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
async deleteGroup (group: ProfileGroup): Promise<void> {
|
||||
async editProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<void> {
|
||||
const result = await this.showProfileGroupEditModal(group)
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
Object.assign(group, result)
|
||||
await this.profilesService.writeProfileGroup(ProfilesSettingsTabComponent.collapsableIntoPartialProfileGroup(group))
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
async showProfileGroupEditModal (group: PartialProfileGroup<CollapsableProfileGroup>): Promise<PartialProfileGroup<CollapsableProfileGroup>|null> {
|
||||
const modal = this.ngbModal.open(
|
||||
EditProfileGroupModalComponent,
|
||||
{ size: 'lg' },
|
||||
)
|
||||
|
||||
modal.componentInstance.group = deepClone(group)
|
||||
modal.componentInstance.providers = this.profileProviders
|
||||
|
||||
const result: EditProfileGroupModalComponentResult<CollapsableProfileGroup> | null = await modal.result.catch(() => null)
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (result.provider) {
|
||||
return this.editProfileGroupDefaults(result.group, result.provider)
|
||||
}
|
||||
|
||||
return result.group
|
||||
}
|
||||
|
||||
private async editProfileGroupDefaults (group: PartialProfileGroup<CollapsableProfileGroup>, provider: ProfileProvider<Profile>): Promise<PartialProfileGroup<CollapsableProfileGroup>|null> {
|
||||
const modal = this.ngbModal.open(
|
||||
EditProfileModalComponent,
|
||||
{ size: 'lg' },
|
||||
)
|
||||
const model = group.defaults?.[provider.id] ?? {}
|
||||
model.type = provider.id
|
||||
modal.componentInstance.profile = Object.assign({}, model)
|
||||
modal.componentInstance.profileProvider = provider
|
||||
modal.componentInstance.defaultsMode = 'group'
|
||||
|
||||
const result = await modal.result.catch(() => null)
|
||||
if (result) {
|
||||
// Fully replace the config
|
||||
for (const k in model) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete model[k]
|
||||
}
|
||||
Object.assign(model, result)
|
||||
if (!group.defaults) {
|
||||
group.defaults = {}
|
||||
}
|
||||
group.defaults[provider.id] = model
|
||||
}
|
||||
return this.showProfileGroupEditModal(group)
|
||||
}
|
||||
|
||||
async deleteProfileGroup (group: PartialProfileGroup<ProfileGroup>): Promise<void> {
|
||||
if ((await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
|
@ -219,7 +228,8 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
cancelId: 1,
|
||||
},
|
||||
)).response === 0) {
|
||||
if ((await this.platform.showMessageBox(
|
||||
let deleteProfiles = false
|
||||
if ((group.profiles?.length ?? 0) > 0 && (await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: this.translate.instant('Delete the group\'s profiles?'),
|
||||
|
@ -230,19 +240,26 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
},
|
||||
)).response === 0) {
|
||||
for (const profile of this.profiles.filter(x => x.group === group.name)) {
|
||||
delete profile.group
|
||||
}
|
||||
} else {
|
||||
this.config.store.profiles = this.config.store.profiles.filter(x => x.group !== group.name)
|
||||
)).response !== 0) {
|
||||
deleteProfiles = true
|
||||
}
|
||||
|
||||
await this.profilesService.deleteProfileGroup(group, { deleteProfiles })
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
isGroupVisible (group: ProfileGroup): boolean {
|
||||
return !this.filter || group.profiles.some(x => this.isProfileVisible(x))
|
||||
async refresh (): Promise<void> {
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
const groups = await this.profilesService.getProfileGroups({ includeNonUserGroup: true, includeProfiles: true })
|
||||
groups.sort((a, b) => a.name.localeCompare(b.name))
|
||||
groups.sort((a, b) => (a.id === 'built-in' || !a.editable ? 1 : 0) - (b.id === 'built-in' || !b.editable ? 1 : 0))
|
||||
groups.sort((a, b) => (a.id === 'ungrouped' ? 0 : 1) - (b.id === 'ungrouped' ? 0 : 1))
|
||||
this.profileGroups = groups.map(g => ProfilesSettingsTabComponent.intoPartialCollapsableProfileGroup(g, profileGroupCollapsed[g.id] ?? false))
|
||||
}
|
||||
|
||||
isGroupVisible (group: PartialProfileGroup<ProfileGroup>): boolean {
|
||||
return !this.filter || (group.profiles ?? []).some(x => this.isProfileVisible(x))
|
||||
}
|
||||
|
||||
isProfileVisible (profile: PartialProfile<Profile>): boolean {
|
||||
|
@ -270,11 +287,12 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
}[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
|
||||
}
|
||||
|
||||
toggleGroupCollapse (group: ProfileGroup): void {
|
||||
toggleGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void {
|
||||
if (group.profiles?.length === 0) {
|
||||
return
|
||||
}
|
||||
group.collapsed = !group.collapsed
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
profileGroupCollapsed[group.name ?? ''] = group.collapsed
|
||||
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
|
||||
this.saveProfileGroupCollapse(group)
|
||||
}
|
||||
|
||||
async editDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
||||
|
@ -282,21 +300,40 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
EditProfileModalComponent,
|
||||
{ size: 'lg' },
|
||||
)
|
||||
const model = this.config.store.profileDefaults[provider.id] ?? {}
|
||||
const model = this.profilesService.getProviderDefaults(provider)
|
||||
model.type = provider.id
|
||||
modal.componentInstance.profile = Object.assign({}, model)
|
||||
modal.componentInstance.profileProvider = provider
|
||||
modal.componentInstance.defaultsMode = true
|
||||
const result = await modal.result
|
||||
|
||||
// Fully replace the config
|
||||
for (const k in model) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete model[k]
|
||||
modal.componentInstance.defaultsMode = 'enabled'
|
||||
const result = await modal.result.catch(() => null)
|
||||
if (result) {
|
||||
// Fully replace the config
|
||||
for (const k in model) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete model[k]
|
||||
}
|
||||
Object.assign(model, result)
|
||||
this.profilesService.setProviderDefaults(provider, model)
|
||||
await this.config.save()
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDefaults (provider: ProfileProvider<Profile>): Promise<void> {
|
||||
if ((await this.platform.showMessageBox(
|
||||
{
|
||||
type: 'warning',
|
||||
message: this.translate.instant('Restore settings to defaults ?'),
|
||||
buttons: [
|
||||
this.translate.instant('Delete'),
|
||||
this.translate.instant('Keep'),
|
||||
],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
},
|
||||
)).response === 0) {
|
||||
this.profilesService.setProviderDefaults(provider, {})
|
||||
await this.config.save()
|
||||
}
|
||||
Object.assign(model, result)
|
||||
this.config.store.profileDefaults[provider.id] = model
|
||||
await this.config.save()
|
||||
}
|
||||
|
||||
blacklistProfile (profile: PartialProfile<Profile>): void {
|
||||
|
@ -314,6 +351,29 @@ export class ProfilesSettingsTabComponent extends BaseComponent {
|
|||
}
|
||||
|
||||
getQuickConnectProviders (): ProfileProvider<Profile>[] {
|
||||
return this.profileProviders.filter(x => x.supportsQuickConnect)
|
||||
return this.profileProviders.filter(x => x instanceof QuickConnectProfileProvider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save ProfileGroup collapse state in localStorage
|
||||
*/
|
||||
private saveProfileGroupCollapse (group: PartialProfileGroup<CollapsableProfileGroup>): void {
|
||||
const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
|
||||
profileGroupCollapsed[group.id] = group.collapsed
|
||||
window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
|
||||
}
|
||||
|
||||
private static collapsableIntoPartialProfileGroup (group: PartialProfileGroup<CollapsableProfileGroup>): PartialProfileGroup<ProfileGroup> {
|
||||
const g: any = { ...group }
|
||||
delete g.collapsed
|
||||
return g
|
||||
}
|
||||
|
||||
private static intoPartialCollapsableProfileGroup (group: PartialProfileGroup<ProfileGroup>, collapsed: boolean): PartialProfileGroup<CollapsableProfileGroup> {
|
||||
const collapsableGroup = {
|
||||
...group,
|
||||
collapsed,
|
||||
}
|
||||
return collapsableGroup
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,9 +35,11 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
|||
|
||||
async enableVault () {
|
||||
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
|
||||
const newPassphrase = await modal.result
|
||||
await this.vault.setEnabled(true, newPassphrase)
|
||||
this.vaultContents = await this.vault.load(newPassphrase)
|
||||
const newPassphrase = await modal.result.catch(() => null)
|
||||
if (newPassphrase) {
|
||||
await this.vault.setEnabled(true, newPassphrase)
|
||||
this.vaultContents = await this.vault.load(newPassphrase)
|
||||
}
|
||||
}
|
||||
|
||||
async disableVault () {
|
||||
|
@ -65,8 +67,10 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
|||
return
|
||||
}
|
||||
const modal = this.ngbModal.open(SetVaultPassphraseModalComponent)
|
||||
const newPassphrase = await modal.result
|
||||
this.vault.save(this.vaultContents, newPassphrase)
|
||||
const newPassphrase = await modal.result.catch(() => null)
|
||||
if (newPassphrase) {
|
||||
this.vault.save(this.vaultContents, newPassphrase)
|
||||
}
|
||||
}
|
||||
|
||||
async toggleConfigEncrypted () {
|
||||
|
@ -118,7 +122,7 @@ export class VaultSettingsTabComponent extends BaseComponent {
|
|||
modal.componentInstance.prompt = this.translate.instant('New name')
|
||||
modal.componentInstance.value = secret.key.description
|
||||
|
||||
const description = (await modal.result)?.value
|
||||
const description = (await modal.result.catch(() => null))?.value
|
||||
if (!description) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'
|
|||
import TabbyCorePlugin, { ToolbarButtonProvider, HotkeyProvider, ConfigProvider, HotkeysService, AppService } from 'tabby-core'
|
||||
|
||||
import { EditProfileModalComponent } from './components/editProfileModal.component'
|
||||
import { EditProfileGroupModalComponent } from './components/editProfileGroupModal.component'
|
||||
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
|
||||
import { HotkeySettingsTabComponent } from './components/hotkeySettingsTab.component'
|
||||
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'
|
||||
|
@ -48,6 +49,7 @@ import { HotkeySettingsTabProvider, WindowSettingsTabProvider, VaultSettingsTabP
|
|||
],
|
||||
declarations: [
|
||||
EditProfileModalComponent,
|
||||
EditProfileGroupModalComponent,
|
||||
HotkeyInputModalComponent,
|
||||
HotkeySettingsTabComponent,
|
||||
MultiHotkeyInputComponent,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal'
|
||||
import { ConnectableTerminalProfile, InputProcessingOptions, LoginScriptsOptions } from 'tabby-terminal'
|
||||
|
||||
export enum SSHAlgorithmType {
|
||||
HMAC = 'hmac',
|
||||
|
@ -7,7 +7,7 @@ export enum SSHAlgorithmType {
|
|||
HOSTKEY = 'serverHostKey',
|
||||
}
|
||||
|
||||
export interface SSHProfile extends BaseTerminalProfile {
|
||||
export interface SSHProfile extends ConnectableTerminalProfile {
|
||||
options: SSHProfileOptions
|
||||
}
|
||||
|
||||
|
|
|
@ -113,8 +113,8 @@ export class SFTPPanelComponent {
|
|||
|
||||
async openCreateDirectoryModal (): Promise<void> {
|
||||
const modal = this.ngbModal.open(SFTPCreateDirectoryModalComponent)
|
||||
const directoryName = await modal.result
|
||||
if (directoryName !== '') {
|
||||
const directoryName = await modal.result.catch(() => null)
|
||||
if (directoryName?.trim()) {
|
||||
this.sftp.mkdir(path.join(this.path, directoryName)).then(() => {
|
||||
this.notifications.notice('The directory was created successfully')
|
||||
this.navigate(path.join(this.path, directoryName))
|
||||
|
|
|
@ -75,7 +75,7 @@ export class SSHProfileSettingsComponent {
|
|||
modal.componentInstance.prompt = `Password for ${this.profile.options.user}@${this.profile.options.host}`
|
||||
modal.componentInstance.password = true
|
||||
try {
|
||||
const result = await modal.result
|
||||
const result = await modal.result.catch(() => null)
|
||||
if (result?.value) {
|
||||
this.passwordStorage.savePassword(this.profile, result.value)
|
||||
this.hasSavedPassword = true
|
||||
|
@ -89,11 +89,13 @@ export class SSHProfileSettingsComponent {
|
|||
}
|
||||
|
||||
async addPrivateKey () {
|
||||
const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`)
|
||||
this.profile.options.privateKeys = [
|
||||
...this.profile.options.privateKeys!,
|
||||
ref,
|
||||
]
|
||||
const ref = await this.fileProviders.selectAndStoreFile(`private key for ${this.profile.name}`).catch(() => null)
|
||||
if (ref) {
|
||||
this.profile.options.privateKeys = [
|
||||
...this.profile.options.privateKeys!,
|
||||
ref,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
removePrivateKey (path: string) {
|
||||
|
|
|
@ -61,12 +61,4 @@ h3 SSH
|
|||
(ngModelChange)='config.save()'
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title(translate) Clear terminal after connection
|
||||
toggle(
|
||||
[(ngModel)]='config.store.ssh.clearServiceMessagesOnConnect',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
|
||||
.alert.alert-info(translate) SSH connection management is now done through the "Profiles & connections" tab
|
||||
|
|
|
@ -83,7 +83,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
|
|||
|
||||
const jumpSession = await this.setupOneSession(
|
||||
this.injector,
|
||||
this.profilesService.getConfigProxyForProfile(jumpConnection),
|
||||
this.profilesService.getConfigProxyForProfile<SSHProfile>(jumpConnection),
|
||||
)
|
||||
|
||||
jumpSession.ref()
|
||||
|
@ -163,10 +163,6 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
|
|||
|
||||
await session.start()
|
||||
|
||||
if (this.config.store.ssh.clearServiceMessagesOnConnect) {
|
||||
this.frontend?.clear()
|
||||
}
|
||||
|
||||
this.session?.resize(this.size.columns, this.size.rows)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ export class SSHConfigProvider extends ConfigProvider {
|
|||
x11Display: null,
|
||||
knownHosts: [],
|
||||
verifyHostKeys: true,
|
||||
clearServiceMessagesOnConnect: true,
|
||||
},
|
||||
hotkeys: {
|
||||
'restart-ssh-session': [],
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable, InjectFlags, Injector } from '@angular/core'
|
||||
import { ProfileProvider, NewTabParameters, PartialProfile, TranslateService } from 'tabby-core'
|
||||
import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core'
|
||||
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
|
||||
import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
|
||||
import { SSHTabComponent } from './components/sshTab.component'
|
||||
|
@ -8,10 +8,9 @@ import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
|
|||
import { SSHProfileImporter } from './api/importer'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SSHProfilesService extends ProfileProvider<SSHProfile> {
|
||||
export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile> {
|
||||
id = 'ssh'
|
||||
name = 'SSH'
|
||||
supportsQuickConnect = true
|
||||
settingsComponent = SSHProfileSettingsComponent
|
||||
configDefaults = {
|
||||
options: {
|
||||
|
@ -45,6 +44,7 @@ export class SSHProfilesService extends ProfileProvider<SSHProfile> {
|
|||
reuseSession: true,
|
||||
input: { backspace: 'backspace' },
|
||||
},
|
||||
clearServiceMessagesOnConnect: true,
|
||||
}
|
||||
|
||||
constructor (
|
||||
|
|
|
@ -34,7 +34,7 @@ export class SSHMultiplexerService {
|
|||
if (!jumpConnection) {
|
||||
return key
|
||||
}
|
||||
const jumpProfile = this.profilesService.getConfigProxyForProfile(jumpConnection)
|
||||
const jumpProfile = this.profilesService.getConfigProxyForProfile<SSHProfile>(jumpConnection)
|
||||
key += '$' + await this.getMultiplexerKey(jumpProfile)
|
||||
}
|
||||
return key
|
||||
|
|
|
@ -210,7 +210,6 @@ export class SSHSession {
|
|||
if (!await this.verifyHostKey(handshake)) {
|
||||
this.ssh.end()
|
||||
reject(new Error('Host key verification failed'))
|
||||
return
|
||||
}
|
||||
this.logger.info('Handshake complete:', handshake)
|
||||
resolve()
|
||||
|
@ -300,7 +299,7 @@ export class SSHSession {
|
|||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = `Username for ${this.profile.options.host}`
|
||||
try {
|
||||
const result = await modal.result
|
||||
const result = await modal.result.catch(() => null)
|
||||
this.authUsername = result?.value ?? null
|
||||
} catch {
|
||||
this.authUsername = 'root'
|
||||
|
@ -428,11 +427,7 @@ export class SSHSession {
|
|||
const modal = this.ngbModal.open(HostKeyPromptModalComponent)
|
||||
modal.componentInstance.selector = selector
|
||||
modal.componentInstance.digest = this.hostKeyDigest
|
||||
try {
|
||||
return await modal.result
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return modal.result.catch(() => false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -495,7 +490,7 @@ export class SSHSession {
|
|||
modal.componentInstance.showRememberCheckbox = true
|
||||
|
||||
try {
|
||||
const result = await modal.result
|
||||
const result = await modal.result.catch(() => null)
|
||||
if (result) {
|
||||
if (result.remember) {
|
||||
this.savedPassword = result.value
|
||||
|
|
|
@ -53,6 +53,6 @@ export class CommonSFTPContextMenu extends SFTPContextMenuItemProvider {
|
|||
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
|
||||
modal.componentInstance.item = item
|
||||
modal.componentInstance.sftp = session
|
||||
await modal.result
|
||||
await modal.result.catch(() => {return})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Injectable } from '@angular/core'
|
||||
import { ProfileProvider, NewTabParameters, PartialProfile, TranslateService } from 'tabby-core'
|
||||
import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core'
|
||||
import { TelnetProfileSettingsComponent } from './components/telnetProfileSettings.component'
|
||||
import { TelnetTabComponent } from './components/telnetTab.component'
|
||||
import { TelnetProfile } from './session'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
|
||||
export class TelnetProfilesService extends QuickConnectProfileProvider<TelnetProfile> {
|
||||
id = 'telnet'
|
||||
name = 'Telnet'
|
||||
supportsQuickConnect = true
|
||||
|
@ -21,6 +21,7 @@ export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
|
|||
scripts: [],
|
||||
input: { backspace: 'backspace' },
|
||||
},
|
||||
clearServiceMessagesOnConnect: false,
|
||||
}
|
||||
|
||||
constructor (private translate: TranslateService) { super() }
|
||||
|
@ -95,4 +96,12 @@ export class TelnetProfilesService extends ProfileProvider<TelnetProfile> {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
intoQuickConnectString (profile: TelnetProfile): string | null {
|
||||
let s = profile.options.host
|
||||
if (profile.options.port !== 23) {
|
||||
s = `${s}:${profile.options.port}`
|
||||
}
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@ import colors from 'ansi-colors'
|
|||
import stripAnsi from 'strip-ansi'
|
||||
import { Injector } from '@angular/core'
|
||||
import { LogService } from 'tabby-core'
|
||||
import { BaseSession, BaseTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
|
||||
import { BaseSession, ConnectableTerminalProfile, InputProcessingOptions, InputProcessor, LoginScriptsOptions, SessionMiddleware, StreamProcessingOptions, TerminalStreamProcessor } from 'tabby-terminal'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
|
||||
|
||||
export interface TelnetProfile extends BaseTerminalProfile {
|
||||
export interface TelnetProfile extends ConnectableTerminalProfile {
|
||||
options: TelnetProfileOptions
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Injector, Component } from '@angular/core'
|
|||
|
||||
import { first } from 'rxjs'
|
||||
|
||||
import { BaseTerminalProfile } from './interfaces'
|
||||
import { ConnectableTerminalProfile } from './interfaces'
|
||||
import { BaseTerminalTabComponent } from './baseTerminalTab.component'
|
||||
import { GetRecoveryTokenOptions, RecoveryToken } from 'tabby-core'
|
||||
|
||||
|
@ -13,7 +13,7 @@ import { GetRecoveryTokenOptions, RecoveryToken } from 'tabby-core'
|
|||
* A class to base your custom connectable terminal tabs on
|
||||
*/
|
||||
@Component({ template: '' })
|
||||
export abstract class ConnectableTerminalTabComponent<P extends BaseTerminalProfile> extends BaseTerminalTabComponent<P> {
|
||||
export abstract class ConnectableTerminalTabComponent<P extends ConnectableTerminalProfile> extends BaseTerminalTabComponent<P> {
|
||||
|
||||
protected reconnectOffered = false
|
||||
protected isDisconnectedByHand = false
|
||||
|
@ -57,6 +57,9 @@ export abstract class ConnectableTerminalTabComponent<P extends BaseTerminalProf
|
|||
async initializeSession (): Promise<void> {
|
||||
this.reconnectOffered = false
|
||||
this.isDisconnectedByHand = false
|
||||
if (this.profile.clearServiceMessagesOnConnect) {
|
||||
this.frontend?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Profile } from 'tabby-core'
|
||||
import { ConnectableProfile, Profile } from 'tabby-core'
|
||||
|
||||
export interface ResizeEvent {
|
||||
columns: number
|
||||
|
@ -19,3 +19,5 @@ export interface TerminalColorScheme {
|
|||
export interface BaseTerminalProfile extends Profile {
|
||||
terminalColorScheme?: TerminalColorScheme
|
||||
}
|
||||
|
||||
export interface ConnectableTerminalProfile extends BaseTerminalProfile, ConnectableProfile {}
|
||||
|
|
|
@ -175,7 +175,7 @@ export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
|
|||
const modal = this.ngbModal.open(PromptModalComponent)
|
||||
modal.componentInstance.prompt = this.translate.instant('New profile name')
|
||||
modal.componentInstance.value = tab.profile.name
|
||||
const name = (await modal.result)?.value
|
||||
const name = (await modal.result.catch(() => null))?.value
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue