diff --git a/app/src/global.scss b/app/src/global.scss index ce51dd7e..9dbabb0e 100644 --- a/app/src/global.scss +++ b/app/src/global.scss @@ -17,6 +17,10 @@ body { } .btn { + display: inline-flex; + align-items: center; + flex-wrap: nowrap; + & > svg { pointer-events: none; } @@ -104,3 +108,14 @@ ngb-typeahead-window { max-height: 60vh; overflow: auto; } + + +.hover-reveal { + opacity: 0; + + .hover-reveal-parent:hover &, + *:hover > &, + &:hover { + opacity: 1; + } + } diff --git a/terminus-core/src/components/selectorModal.component.scss b/terminus-core/src/components/selectorModal.component.scss index 3dd4b243..4c40045c 100644 --- a/terminus-core/src/components/selectorModal.component.scss +++ b/terminus-core/src/components/selectorModal.component.scss @@ -17,3 +17,8 @@ .title { margin-left: 10px; } + +input { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} diff --git a/terminus-core/src/theme.scss b/terminus-core/src/theme.scss index 1bea9dd5..f958ad3a 100644 --- a/terminus-core/src/theme.scss +++ b/terminus-core/src/theme.scss @@ -239,10 +239,6 @@ hotkey-input-modal { border: none; border-top: 1px solid rgba(255, 255, 255, .1); - &:not(.combi) { - padding: $list-group-item-padding-y $list-group-item-padding-x; - } - &:first-child { border-top: none; } diff --git a/terminus-core/src/theme.vars.scss b/terminus-core/src/theme.vars.scss index 3412f3ba..e24c27ea 100644 --- a/terminus-core/src/theme.vars.scss +++ b/terminus-core/src/theme.vars.scss @@ -46,7 +46,6 @@ $body-color: #ccc; $body-bg: #131d27; $body-bg2: #20333e; - $font-family-sans-serif: "Source Sans Pro"; $font-family-monospace: "Source Code Pro"; $font-size-base: 14rem / 16; @@ -55,6 +54,12 @@ $font-size-sm: .85rem; $line-height-base: 1.6; +$border-radius: .4rem; +$border-radius-lg: .6rem; +$border-radius-sm: .2rem; + +// ----- + $headings-color: #ced9e2; $headings-font-weight: lighter; diff --git a/terminus-settings/src/components/settingsTab.component.scss b/terminus-settings/src/components/settingsTab.component.scss index 3a1ee18b..3c2925b0 100644 --- a/terminus-settings/src/components/settingsTab.component.scss +++ b/terminus-settings/src/components/settingsTab.component.scss @@ -17,6 +17,7 @@ > .nav { padding: 20px 10px; width: 190px; + flex: none; overflow-y: auto; } diff --git a/terminus-ssh/src/api.ts b/terminus-ssh/src/api.ts index 609c4a30..85eb50f8 100644 --- a/terminus-ssh/src/api.ts +++ b/terminus-ssh/src/api.ts @@ -416,11 +416,6 @@ export class SSHSession extends BaseSession { } } -export interface SSHConnectionGroup { - name: string - connections: SSHConnection[] -} - export const ALGORITHM_BLACKLIST = [ // cause native crashes in node crypto, use EC instead 'diffie-hellman-group-exchange-sha256', diff --git a/terminus-ssh/src/components/editConnectionModal.component.pug b/terminus-ssh/src/components/editConnectionModal.component.pug index 85b5b1c5..57c732cd 100644 --- a/terminus-ssh/src/components/editConnectionModal.component.pug +++ b/terminus-ssh/src/components/editConnectionModal.component.pug @@ -17,6 +17,7 @@ type='text', placeholder='Ungrouped', [(ngModel)]='connection.group', + [ngbTypeahead]='groupTypeahead', ) .d-flex.w-100(*ngIf='!useProxyCommand') diff --git a/terminus-ssh/src/components/editConnectionModal.component.ts b/terminus-ssh/src/components/editConnectionModal.component.ts index 30af201e..3f3f304b 100644 --- a/terminus-ssh/src/components/editConnectionModal.component.ts +++ b/terminus-ssh/src/components/editConnectionModal.component.ts @@ -1,6 +1,9 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Component } from '@angular/core' import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { Observable } from 'rxjs' +import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators' + import { ElectronService, HostAppService, ConfigService } from 'terminus-core' import { PasswordStorageService } from '../services/passwordStorage.service' import { SSHConnection, LoginScript, SSHAlgorithmType, ALGORITHM_BLACKLIST } from '../api' @@ -20,6 +23,8 @@ export class EditConnectionModalComponent { defaultAlgorithms: Record = {} algorithms: Record> = {} + private groupNames: string[] + constructor ( public config: ConfigService, private modalInstance: NgbActiveModal, @@ -44,8 +49,18 @@ export class EditConnectionModalComponent { this.supportedAlgorithms[k] = ALGORITHMS[supportedAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x)) this.defaultAlgorithms[k] = ALGORITHMS[defaultAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x)) } + + this.groupNames = [...new Set(config.store.ssh.connections.map(x => x.group))] as string[] + this.groupNames = this.groupNames.filter(x => x).sort() } + groupTypeahead = (text$: Observable) => + text$.pipe( + debounceTime(200), + distinctUntilChanged(), + map(q => this.groupNames.filter(x => !q || x.toLowerCase().includes(q.toLowerCase()))) + ) + async ngOnInit () { this.hasSavedPassword = !!await this.passwordStorage.loadPassword(this.connection) this.connection.algorithms = this.connection.algorithms ?? {} diff --git a/terminus-ssh/src/components/sshSettingsTab.component.pug b/terminus-ssh/src/components/sshSettingsTab.component.pug index 1aaccd9f..84988b6f 100644 --- a/terminus-ssh/src/components/sshSettingsTab.component.pug +++ b/terminus-ssh/src/components/sshSettingsTab.component.pug @@ -1,33 +1,55 @@ -h3 Connections +.d-flex.align-items-center.mb-3 + h3.m-0 SSH Connections -.list-group.list-group-flush.mt-3.mb-3 + button.btn.btn-primary.ml-auto((click)='createConnection()') + i.fas.fa-fw.fa-plus + span.ml-2 Add connection + +.input-group.mb-3 + .input-group-prepend + .input-group-text + i.fas.fa-fw.fa-search + input.form-control(type='search', placeholder='Filter', [(ngModel)]='filter') + +.list-group.list-group-light.mt-3.mb-3 ng-container(*ngFor='let group of childGroups') - .list-group-item.list-group-item-action.d-flex.align-items-center( - (click)='groupCollapsed[group.name] = !groupCollapsed[group.name]' - ) - .fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]') - .fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]') - span.ml-3.mr-auto {{group.name || "Ungrouped"}} - button.btn.btn-outline-info.ml-2((click)='editGroup(group)') - i.fas.fa-edit - button.btn.btn-outline-danger.ml-1((click)='deleteGroup(group)') - i.fas.fa-trash - ng-container(*ngIf='!groupCollapsed[group.name]') - .list-group-item.list-group-item-action.pl-5.d-flex.align-items-center( - *ngFor='let connection of group.connections', - (click)='editConnection(connection)' + ng-container(*ngIf='isGroupVisible(group)') + .list-group-item.list-group-item-action.d-flex.align-items-center( + (click)='groupCollapsed[group.name] = !groupCollapsed[group.name]' ) - .mr-auto - div {{connection.name}} - .text-muted {{connection.host}} - button.btn.btn-outline-info.ml-1((click)='$event.stopPropagation(); copyConnection(connection)') - i.fas.fa-copy - button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteConnection(connection)') + .fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]') + .fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]') + span.ml-3.mr-auto {{group.name || "Ungrouped"}} + button.btn.btn-sm.btn-link.hover-reveal.ml-2( + [class.invisible]='!group.name', + (click)='$event.stopPropagation(); editGroup(group)' + ) + i.fas.fa-edit + button.btn.btn-sm.btn-link.hover-reveal.ml-2( + [class.invisible]='!group.name', + (click)='$event.stopPropagation(); deleteGroup(group)' + ) i.fas.fa-trash -button.btn.btn-primary((click)='createConnection()') - i.fas.fa-fw.fa-plus - span.ml-2 Add connection + ng-container(*ngIf='!groupCollapsed[group.name]') + ng-container(*ngFor='let connection of group.connections') + .list-group-item.list-group-item-action.pl-5.d-flex.align-items-center( + *ngIf='isConnectionVisible(connection)', + (click)='editConnection(connection)' + ) + .mr-3 {{connection.name}} + .mr-auto.text-muted {{connection.host}} + + .hover-reveal(ngbDropdown, placement='bottom-right') + button.btn.btn-link(ngbDropdownToggle, (click)='$event.stopPropagation()') + i.fas.fa-fw.fa-ellipsis-v + div(ngbDropdownMenu) + button.dropdown-item((click)='$event.stopPropagation(); copyConnection(connection)') + i.fas.fa-copy + span Duplicate + button.dropdown-item((click)='$event.stopPropagation(); deleteConnection(connection)') + i.fas.fa-trash + span Delete h3.mt-5 Options diff --git a/terminus-ssh/src/components/sshSettingsTab.component.scss b/terminus-ssh/src/components/sshSettingsTab.component.scss new file mode 100644 index 00000000..f360a956 --- /dev/null +++ b/terminus-ssh/src/components/sshSettingsTab.component.scss @@ -0,0 +1,3 @@ +.list-group-item { + padding: 0.3rem 1rem; +} diff --git a/terminus-ssh/src/components/sshSettingsTab.component.ts b/terminus-ssh/src/components/sshSettingsTab.component.ts index c9a113db..eea85b26 100644 --- a/terminus-ssh/src/components/sshSettingsTab.component.ts +++ b/terminus-ssh/src/components/sshSettingsTab.component.ts @@ -4,18 +4,25 @@ import { Component } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { ConfigService, ElectronService, HostAppService } from 'terminus-core' import { PasswordStorageService } from '../services/passwordStorage.service' -import { SSHConnection, SSHConnectionGroup } from '../api' +import { SSHConnection } from '../api' import { EditConnectionModalComponent } from './editConnectionModal.component' import { PromptModalComponent } from './promptModal.component' +interface SSHConnectionGroup { + name: string|null + connections: SSHConnection[] +} + /** @hidden */ @Component({ template: require('./sshSettingsTab.component.pug'), + styles: [require('./sshSettingsTab.component.scss')], }) export class SSHSettingsTabComponent { connections: SSHConnection[] childGroups: SSHConnectionGroup[] groupCollapsed: Record = {} + filter = '' constructor ( public config: ConfigService, @@ -133,7 +140,7 @@ export class SSHSettingsTabComponent { let group = this.childGroups.find(x => x.name === connection.group) if (!group) { group = { - name: connection.group!, + name: connection.group, connections: [], } this.childGroups.push(group) @@ -141,4 +148,12 @@ export class SSHSettingsTabComponent { group.connections.push(connection) } } + + isGroupVisible (group: SSHConnectionGroup): boolean { + return !this.filter || group.connections.some(x => this.isConnectionVisible(x)) + } + + isConnectionVisible (connection: SSHConnection): boolean { + return !this.filter || `${connection.name}$${connection.host}`.toLowerCase().includes(this.filter.toLowerCase()) + } } diff --git a/terminus-terminal/src/components/appearanceSettingsTab.component.pug b/terminus-terminal/src/components/appearanceSettingsTab.component.pug index f1ed1f71..7607ac44 100644 --- a/terminus-terminal/src/components/appearanceSettingsTab.component.pug +++ b/terminus-terminal/src/components/appearanceSettingsTab.component.pug @@ -114,7 +114,7 @@ h3.mb-3 Appearance .header .title Custom CSS -textarea.form-control( +textarea.form-control.mb-5( [(ngModel)]='config.store.appearance.css', (ngModelChange)='saveConfiguration()', )