profilesSettingsTab.component.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
  2. import { v4 as uuidv4 } from 'uuid'
  3. import slugify from 'slugify'
  4. import deepClone from 'clone-deep'
  5. import { Component, Inject } from '@angular/core'
  6. import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
  7. import { ConfigService, HostAppService, Profile, SelectorService, ProfilesService, PromptModalComponent, PlatformService, BaseComponent, PartialProfile, ProfileProvider, TranslateService, Platform, AppHotkeyProvider } from 'tabby-core'
  8. import { EditProfileModalComponent } from './editProfileModal.component'
  9. interface ProfileGroup {
  10. name?: string
  11. profiles: PartialProfile<Profile>[]
  12. editable: boolean
  13. collapsed: boolean
  14. }
  15. _('Filter')
  16. _('Ungrouped')
  17. /** @hidden */
  18. @Component({
  19. templateUrl: './profilesSettingsTab.component.pug',
  20. styleUrls: ['./profilesSettingsTab.component.scss'],
  21. })
  22. export class ProfilesSettingsTabComponent extends BaseComponent {
  23. profiles: PartialProfile<Profile>[] = []
  24. builtinProfiles: PartialProfile<Profile>[] = []
  25. templateProfiles: PartialProfile<Profile>[] = []
  26. profileGroups: ProfileGroup[]
  27. filter = ''
  28. Platform = Platform
  29. constructor (
  30. public config: ConfigService,
  31. public hostApp: HostAppService,
  32. @Inject(ProfileProvider) public profileProviders: ProfileProvider<Profile>[],
  33. private profilesService: ProfilesService,
  34. private selector: SelectorService,
  35. private ngbModal: NgbModal,
  36. private platform: PlatformService,
  37. private translate: TranslateService,
  38. ) {
  39. super()
  40. this.profileProviders.sort((a, b) => a.name.localeCompare(b.name))
  41. }
  42. async ngOnInit (): Promise<void> {
  43. this.refresh()
  44. this.builtinProfiles = (await this.profilesService.getProfiles()).filter(x => x.isBuiltin)
  45. this.templateProfiles = this.builtinProfiles.filter(x => x.isTemplate)
  46. this.builtinProfiles = this.builtinProfiles.filter(x => !x.isTemplate)
  47. this.refresh()
  48. this.subscribeUntilDestroyed(this.config.changed$, () => this.refresh())
  49. }
  50. launchProfile (profile: PartialProfile<Profile>): void {
  51. this.profilesService.openNewTabForProfile(profile)
  52. }
  53. async newProfile (base?: PartialProfile<Profile>): Promise<void> {
  54. if (!base) {
  55. let profiles = [...this.templateProfiles, ...this.builtinProfiles, ...this.profiles]
  56. profiles = profiles.filter(x => !this.isProfileBlacklisted(x))
  57. profiles.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0))
  58. base = await this.selector.show(
  59. this.translate.instant('Select a base profile to use as a template'),
  60. profiles.map(p => ({
  61. icon: p.icon,
  62. description: this.profilesService.getDescription(p) ?? undefined,
  63. name: p.group ? `${p.group} / ${p.name}` : p.name,
  64. result: p,
  65. })),
  66. )
  67. }
  68. const profile: PartialProfile<Profile> = deepClone(base)
  69. delete profile.id
  70. if (base.isTemplate) {
  71. profile.name = ''
  72. } else if (!base.isBuiltin) {
  73. profile.name = this.translate.instant('{name} copy', base)
  74. }
  75. profile.isBuiltin = false
  76. profile.isTemplate = false
  77. const result = await this.showProfileEditModal(profile)
  78. if (!result) {
  79. return
  80. }
  81. Object.assign(profile, result)
  82. if (!profile.name) {
  83. const cfgProxy = this.profilesService.getConfigProxyForProfile(profile)
  84. profile.name = this.profilesService.providerForProfile(profile)?.getSuggestedName(cfgProxy) ?? this.translate.instant('{name} copy', base)
  85. }
  86. profile.id = `${profile.type}:custom:${slugify(profile.name)}:${uuidv4()}`
  87. this.config.store.profiles = [profile, ...this.config.store.profiles]
  88. await this.config.save()
  89. }
  90. async editProfile (profile: PartialProfile<Profile>): Promise<void> {
  91. const result = await this.showProfileEditModal(profile)
  92. if (!result) {
  93. return
  94. }
  95. Object.assign(profile, result)
  96. await this.config.save()
  97. }
  98. async showProfileEditModal (profile: PartialProfile<Profile>): Promise<PartialProfile<Profile>|null> {
  99. const modal = this.ngbModal.open(
  100. EditProfileModalComponent,
  101. { size: 'lg' },
  102. )
  103. const provider = this.profilesService.providerForProfile(profile)
  104. if (!provider) {
  105. throw new Error('Cannot edit a profile without a provider')
  106. }
  107. modal.componentInstance.profile = deepClone(profile)
  108. modal.componentInstance.profileProvider = provider
  109. const result = await modal.result.catch(() => null)
  110. if (!result) {
  111. return null
  112. }
  113. // Fully replace the config
  114. for (const k in profile) {
  115. // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  116. delete profile[k]
  117. }
  118. result.type = provider.id
  119. return result
  120. }
  121. async deleteProfile (profile: PartialProfile<Profile>): Promise<void> {
  122. if ((await this.platform.showMessageBox(
  123. {
  124. type: 'warning',
  125. message: this.translate.instant('Delete "{name}"?', profile),
  126. buttons: [
  127. this.translate.instant('Delete'),
  128. this.translate.instant('Keep'),
  129. ],
  130. defaultId: 1,
  131. cancelId: 1,
  132. },
  133. )).response === 0) {
  134. this.profilesService.providerForProfile(profile)?.deleteProfile(
  135. this.profilesService.getConfigProxyForProfile(profile))
  136. this.config.store.profiles = this.config.store.profiles.filter(x => x !== profile)
  137. const profileHotkeyName = AppHotkeyProvider.getProfileHotkeyName(profile)
  138. if (this.config.store.hotkeys.profile.hasOwnProperty(profileHotkeyName)) {
  139. const profileHotkeys = deepClone(this.config.store.hotkeys.profile)
  140. // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  141. delete profileHotkeys[profileHotkeyName]
  142. this.config.store.hotkeys.profile = profileHotkeys
  143. }
  144. await this.config.save()
  145. }
  146. }
  147. refresh (): void {
  148. this.profiles = this.config.store.profiles
  149. this.profileGroups = []
  150. const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
  151. for (const profile of this.profiles) {
  152. // Group null, undefined and empty together
  153. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
  154. let group = this.profileGroups.find(x => x.name === (profile.group || ''))
  155. if (!group) {
  156. group = {
  157. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
  158. name: profile.group || '',
  159. profiles: [],
  160. editable: true,
  161. collapsed: profileGroupCollapsed[profile.group ?? ''] ?? false,
  162. }
  163. this.profileGroups.push(group)
  164. }
  165. group.profiles.push(profile)
  166. }
  167. this.profileGroups.sort((a, b) => a.name?.localeCompare(b.name ?? '') ?? -1)
  168. const builtIn = {
  169. name: this.translate.instant('Built-in'),
  170. profiles: this.builtinProfiles,
  171. editable: false,
  172. collapsed: false,
  173. }
  174. builtIn.collapsed = profileGroupCollapsed[builtIn.name ?? ''] ?? false
  175. this.profileGroups.push(builtIn)
  176. }
  177. async editGroup (group: ProfileGroup): Promise<void> {
  178. const modal = this.ngbModal.open(PromptModalComponent)
  179. modal.componentInstance.prompt = this.translate.instant('New name')
  180. modal.componentInstance.value = group.name
  181. const result = await modal.result
  182. if (result) {
  183. for (const profile of this.profiles.filter(x => x.group === group.name)) {
  184. profile.group = result.value
  185. }
  186. this.config.store.profiles = this.profiles
  187. await this.config.save()
  188. }
  189. }
  190. async deleteGroup (group: ProfileGroup): Promise<void> {
  191. if ((await this.platform.showMessageBox(
  192. {
  193. type: 'warning',
  194. message: this.translate.instant('Delete "{name}"?', group),
  195. buttons: [
  196. this.translate.instant('Delete'),
  197. this.translate.instant('Keep'),
  198. ],
  199. defaultId: 1,
  200. cancelId: 1,
  201. },
  202. )).response === 0) {
  203. if ((await this.platform.showMessageBox(
  204. {
  205. type: 'warning',
  206. message: this.translate.instant('Delete the group\'s profiles?'),
  207. buttons: [
  208. this.translate.instant('Move to "Ungrouped"'),
  209. this.translate.instant('Delete'),
  210. ],
  211. defaultId: 0,
  212. cancelId: 0,
  213. },
  214. )).response === 0) {
  215. for (const profile of this.profiles.filter(x => x.group === group.name)) {
  216. delete profile.group
  217. }
  218. } else {
  219. this.config.store.profiles = this.config.store.profiles.filter(x => x.group !== group.name)
  220. }
  221. await this.config.save()
  222. }
  223. }
  224. isGroupVisible (group: ProfileGroup): boolean {
  225. return !this.filter || group.profiles.some(x => this.isProfileVisible(x))
  226. }
  227. isProfileVisible (profile: PartialProfile<Profile>): boolean {
  228. return !this.filter || (profile.name + '$' + (this.getDescription(profile) ?? '')).toLowerCase().includes(this.filter.toLowerCase())
  229. }
  230. getDescription (profile: PartialProfile<Profile>): string|null {
  231. return this.profilesService.getDescription(profile)
  232. }
  233. getTypeLabel (profile: PartialProfile<Profile>): string {
  234. const name = this.profilesService.providerForProfile(profile)?.name
  235. if (name === 'Local terminal') {
  236. return ''
  237. }
  238. return name ? this.translate.instant(name) : this.translate.instant('Unknown')
  239. }
  240. getTypeColorClass (profile: PartialProfile<Profile>): string {
  241. return {
  242. ssh: 'secondary',
  243. serial: 'success',
  244. telnet: 'info',
  245. 'split-layout': 'primary',
  246. }[this.profilesService.providerForProfile(profile)?.id ?? ''] ?? 'warning'
  247. }
  248. toggleGroupCollapse (group: ProfileGroup): void {
  249. group.collapsed = !group.collapsed
  250. const profileGroupCollapsed = JSON.parse(window.localStorage.profileGroupCollapsed ?? '{}')
  251. profileGroupCollapsed[group.name ?? ''] = group.collapsed
  252. window.localStorage.profileGroupCollapsed = JSON.stringify(profileGroupCollapsed)
  253. }
  254. async editDefaults (provider: ProfileProvider<Profile>): Promise<void> {
  255. const modal = this.ngbModal.open(
  256. EditProfileModalComponent,
  257. { size: 'lg' },
  258. )
  259. const model = this.config.store.profileDefaults[provider.id] ?? {}
  260. model.type = provider.id
  261. modal.componentInstance.profile = Object.assign({}, model)
  262. modal.componentInstance.profileProvider = provider
  263. modal.componentInstance.defaultsMode = true
  264. const result = await modal.result
  265. // Fully replace the config
  266. for (const k in model) {
  267. // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  268. delete model[k]
  269. }
  270. Object.assign(model, result)
  271. this.config.store.profileDefaults[provider.id] = model
  272. await this.config.save()
  273. }
  274. blacklistProfile (profile: PartialProfile<Profile>): void {
  275. this.config.store.profileBlacklist = [...this.config.store.profileBlacklist, profile.id]
  276. this.config.save()
  277. }
  278. unblacklistProfile (profile: PartialProfile<Profile>): void {
  279. this.config.store.profileBlacklist = this.config.store.profileBlacklist.filter(x => x !== profile.id)
  280. this.config.save()
  281. }
  282. isProfileBlacklisted (profile: PartialProfile<Profile>): boolean {
  283. return profile.id && this.config.store.profileBlacklist.includes(profile.id)
  284. }
  285. getQuickConnectProviders (): ProfileProvider<Profile>[] {
  286. return this.profileProviders.filter(x => x.supportsQuickConnect)
  287. }
  288. }