mirror of
https://github.com/Eugeny/tabby.git
synced 2024-12-23 10:32:29 +03:00
sftp improvements, fixes #3992
This commit is contained in:
parent
96ce42461d
commit
c946decbca
@ -152,3 +152,9 @@ ngb-typeahead-window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-wrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Observable, Subscription } from 'rxjs'
|
import { Observable, Subscription, Subject } from 'rxjs'
|
||||||
|
|
||||||
interface CancellableEvent {
|
interface CancellableEvent {
|
||||||
element: HTMLElement
|
element: HTMLElement
|
||||||
@ -38,17 +38,21 @@ export class SubscriptionContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class BaseComponent {
|
export class BaseComponent {
|
||||||
private subscriptionContainer = new SubscriptionContainer()
|
protected get destroyed$ (): Observable<void> { return this._destroyed }
|
||||||
|
private _destroyed = new Subject<void>()
|
||||||
|
private _subscriptionContainer = new SubscriptionContainer()
|
||||||
|
|
||||||
addEventListenerUntilDestroyed (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void {
|
addEventListenerUntilDestroyed (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void {
|
||||||
this.subscriptionContainer.addEventListener(element, event, handler, options)
|
this._subscriptionContainer.addEventListener(element, event, handler, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeUntilDestroyed <T> (observable: Observable<T>, handler: (v: T) => void): void {
|
subscribeUntilDestroyed <T> (observable: Observable<T>, handler: (v: T) => void): void {
|
||||||
this.subscriptionContainer.subscribe(observable, handler)
|
this._subscriptionContainer.subscribe(observable, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy (): void {
|
ngOnDestroy (): void {
|
||||||
this.subscriptionContainer.cancelAll()
|
this._destroyed.next()
|
||||||
|
this._destroyed.complete()
|
||||||
|
this._subscriptionContainer.cancelAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import * as fs from 'mz/fs'
|
import * as fs from 'mz/fs'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import * as C from 'constants'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-imports, no-duplicate-imports
|
||||||
|
import { posix as posixPath } from 'path'
|
||||||
import * as sshpk from 'sshpk'
|
import * as sshpk from 'sshpk'
|
||||||
import colors from 'ansi-colors'
|
import colors from 'ansi-colors'
|
||||||
import stripAnsi from 'strip-ansi'
|
import stripAnsi from 'strip-ansi'
|
||||||
@ -138,6 +141,15 @@ interface AuthMethod {
|
|||||||
path?: string
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SFTPFile {
|
||||||
|
name: string
|
||||||
|
fullPath: string
|
||||||
|
isDirectory: boolean
|
||||||
|
isSymlink: boolean
|
||||||
|
mode: number
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
export class SFTPFileHandle {
|
export class SFTPFileHandle {
|
||||||
position = 0
|
position = 0
|
||||||
|
|
||||||
@ -191,22 +203,52 @@ export class SFTPFileHandle {
|
|||||||
export class SFTPSession {
|
export class SFTPSession {
|
||||||
constructor (private sftp: SFTPWrapper, private zone: NgZone) { }
|
constructor (private sftp: SFTPWrapper, private zone: NgZone) { }
|
||||||
|
|
||||||
readdir (p: string): Promise<FileEntry[]> {
|
async readdir (p: string): Promise<SFTPFile[]> {
|
||||||
return wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
|
const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
|
||||||
|
return entries.map(entry => this._makeFile(
|
||||||
|
posixPath.join(p, entry.filename), entry,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
readlink (p: string): Promise<string> {
|
readlink (p: string): Promise<string> {
|
||||||
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
|
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
|
||||||
}
|
}
|
||||||
|
|
||||||
stat (p: string): Promise<Stats> {
|
async stat (p: string): Promise<SFTPFile> {
|
||||||
return wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
|
const stats = await wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
|
||||||
|
return {
|
||||||
|
name: posixPath.basename(p),
|
||||||
|
fullPath: p,
|
||||||
|
isDirectory: stats.isDirectory(),
|
||||||
|
isSymlink: stats.isSymbolicLink(),
|
||||||
|
mode: stats.mode,
|
||||||
|
size: stats.size,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async open (p: string, mode: string): Promise<SFTPFileHandle> {
|
async open (p: string, mode: string): Promise<SFTPFileHandle> {
|
||||||
const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
|
const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
|
||||||
return new SFTPFileHandle(this.sftp, handle, this.zone)
|
return new SFTPFileHandle(this.sftp, handle, this.zone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rmdir (p: string): Promise<void> {
|
||||||
|
await promisify((f: any) => this.sftp.rmdir(p, f))()
|
||||||
|
}
|
||||||
|
|
||||||
|
async unlink (p: string): Promise<void> {
|
||||||
|
await promisify((f: any) => this.sftp.unlink(p, f))()
|
||||||
|
}
|
||||||
|
|
||||||
|
private _makeFile (p: string, entry: FileEntry): SFTPFile {
|
||||||
|
return {
|
||||||
|
fullPath: p,
|
||||||
|
name: posixPath.basename(p),
|
||||||
|
isDirectory: (entry.attrs.mode & C.S_IFDIR) === C.S_IFDIR,
|
||||||
|
isSymlink: (entry.attrs.mode & C.S_IFLNK) === C.S_IFLNK,
|
||||||
|
mode: entry.attrs.mode,
|
||||||
|
size: entry.attrs.size,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SSHSession extends BaseSession {
|
export class SSHSession extends BaseSession {
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
.modal-body
|
||||||
|
label Deleting
|
||||||
|
.no-wrap {{progressMessage}}
|
||||||
|
|
||||||
|
.modal-footer
|
||||||
|
button.btn.btn-outline-danger((click)='cancel()') Cancel
|
49
terminus-ssh/src/components/sftpDeleteModal.component.ts
Normal file
49
terminus-ssh/src/components/sftpDeleteModal.component.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { BaseComponent } from 'terminus-core'
|
||||||
|
import { SFTPFile, SFTPSession } from '../api'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Component({
|
||||||
|
template: require('./sftpDeleteModal.component.pug'),
|
||||||
|
})
|
||||||
|
export class SFTPDeleteModalComponent extends BaseComponent {
|
||||||
|
sftp: SFTPSession
|
||||||
|
item: SFTPFile
|
||||||
|
progressMessage = ''
|
||||||
|
cancelled = false
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private modalInstance: NgbActiveModal,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit (): void {
|
||||||
|
this.destroyed$.subscribe(() => this.cancel())
|
||||||
|
this.run(this.item).then(() => {
|
||||||
|
this.modalInstance.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel (): void {
|
||||||
|
this.cancelled = true
|
||||||
|
this.modalInstance.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
async run (file: SFTPFile): Promise<void> {
|
||||||
|
this.progressMessage = file.fullPath
|
||||||
|
|
||||||
|
if (file.isDirectory) {
|
||||||
|
for (const child of await this.sftp.readdir(file.fullPath)) {
|
||||||
|
await this.run(child)
|
||||||
|
if (this.cancelled) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.sftp.rmdir(file.fullPath)
|
||||||
|
} else {
|
||||||
|
this.sftp.unlink(file.fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,9 +25,10 @@
|
|||||||
div Go up
|
div Go up
|
||||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||||
*ngFor='let item of fileList',
|
*ngFor='let item of fileList',
|
||||||
|
(contextmenu)='showContextMenu(item, $event)',
|
||||||
(click)='open(item)'
|
(click)='open(item)'
|
||||||
)
|
)
|
||||||
i.fa-fw([class]='getIcon(item)')
|
i.fa-fw([class]='getIcon(item)')
|
||||||
div {{item.filename}}
|
div {{item.name}}
|
||||||
.mr-auto
|
.mr-auto
|
||||||
.mode {{getModeString(item)}}
|
.mode {{getModeString(item)}}
|
||||||
|
@ -24,6 +24,10 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.breadcrumb-item:first-child {
|
.breadcrumb-item:first-child {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
import { Component, Input, Output, EventEmitter } from '@angular/core'
|
||||||
import type { FileEntry } from 'ssh2-streams'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { SSHSession, SFTPSession } from '../api'
|
import { SSHSession, SFTPSession, SFTPFile } from '../api'
|
||||||
import { posix as path } from 'path'
|
import { posix as path } from 'path'
|
||||||
import * as C from 'constants'
|
import * as C from 'constants'
|
||||||
import { FileUpload, PlatformService } from 'terminus-core'
|
import { FileUpload, PlatformService } from 'terminus-core'
|
||||||
|
import { SFTPDeleteModalComponent } from './sftpDeleteModal.component'
|
||||||
|
|
||||||
interface PathSegment {
|
interface PathSegment {
|
||||||
name: string
|
name: string
|
||||||
@ -20,21 +21,24 @@ export class SFTPPanelComponent {
|
|||||||
@Input() session: SSHSession
|
@Input() session: SSHSession
|
||||||
@Output() closed = new EventEmitter<void>()
|
@Output() closed = new EventEmitter<void>()
|
||||||
sftp: SFTPSession
|
sftp: SFTPSession
|
||||||
fileList: FileEntry[]|null = null
|
fileList: SFTPFile[]|null = null
|
||||||
path = '/'
|
@Input() path = '/'
|
||||||
|
@Output() pathChange = new EventEmitter<string>()
|
||||||
pathSegments: PathSegment[] = []
|
pathSegments: PathSegment[] = []
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private platform: PlatformService,
|
private platform: PlatformService,
|
||||||
|
private ngbModal: NgbModal,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async ngOnInit (): Promise<void> {
|
async ngOnInit (): Promise<void> {
|
||||||
this.sftp = await this.session.openSFTP()
|
this.sftp = await this.session.openSFTP()
|
||||||
this.navigate('/')
|
this.navigate(this.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigate (newPath: string): Promise<void> {
|
async navigate (newPath: string): Promise<void> {
|
||||||
this.path = newPath
|
this.path = newPath
|
||||||
|
this.pathChange.next(this.path)
|
||||||
|
|
||||||
let p = newPath
|
let p = newPath
|
||||||
this.pathSegments = []
|
this.pathSegments = []
|
||||||
@ -49,17 +53,17 @@ export class SFTPPanelComponent {
|
|||||||
this.fileList = null
|
this.fileList = null
|
||||||
this.fileList = await this.sftp.readdir(this.path)
|
this.fileList = await this.sftp.readdir(this.path)
|
||||||
|
|
||||||
const dirKey = a => (a.attrs.mode & C.S_IFDIR) === C.S_IFDIR ? 1 : 0
|
const dirKey = a => a.isDirectory ? 1 : 0
|
||||||
this.fileList.sort((a, b) =>
|
this.fileList.sort((a, b) =>
|
||||||
dirKey(b) - dirKey(a) ||
|
dirKey(b) - dirKey(a) ||
|
||||||
a.filename.localeCompare(b.filename))
|
a.name.localeCompare(b.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
getIcon (item: FileEntry): string {
|
getIcon (item: SFTPFile): string {
|
||||||
if ((item.attrs.mode & C.S_IFDIR) === C.S_IFDIR) {
|
if (item.isDirectory) {
|
||||||
return 'fas fa-folder text-info'
|
return 'fas fa-folder text-info'
|
||||||
}
|
}
|
||||||
if ((item.attrs.mode & C.S_IFLNK) === C.S_IFLNK) {
|
if (item.isSymlink) {
|
||||||
return 'fas fa-link text-warning'
|
return 'fas fa-link text-warning'
|
||||||
}
|
}
|
||||||
return 'fas fa-file'
|
return 'fas fa-file'
|
||||||
@ -69,20 +73,19 @@ export class SFTPPanelComponent {
|
|||||||
this.navigate(path.dirname(this.path))
|
this.navigate(path.dirname(this.path))
|
||||||
}
|
}
|
||||||
|
|
||||||
async open (item: FileEntry): Promise<void> {
|
async open (item: SFTPFile): Promise<void> {
|
||||||
const itemPath = path.join(this.path, item.filename)
|
if (item.isDirectory) {
|
||||||
if ((item.attrs.mode & C.S_IFDIR) === C.S_IFDIR) {
|
this.navigate(item.fullPath)
|
||||||
this.navigate(path.join(this.path, item.filename))
|
} else if (item.isSymlink) {
|
||||||
} else if ((item.attrs.mode & C.S_IFLNK) === C.S_IFLNK) {
|
const target = await this.sftp.readlink(item.fullPath)
|
||||||
const target = await this.sftp.readlink(itemPath)
|
|
||||||
const stat = await this.sftp.stat(target)
|
const stat = await this.sftp.stat(target)
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory) {
|
||||||
this.navigate(itemPath)
|
this.navigate(item.fullPath)
|
||||||
} else {
|
} else {
|
||||||
this.download(itemPath, stat.size)
|
this.download(item.fullPath, stat.size)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.download(itemPath, item.attrs.size)
|
this.download(item.fullPath, item.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +142,7 @@ export class SFTPPanelComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getModeString (item: FileEntry): string {
|
getModeString (item: SFTPFile): string {
|
||||||
const s = 'SGdrwxrwxrwx'
|
const s = 'SGdrwxrwxrwx'
|
||||||
const e = ' ---------'
|
const e = ' ---------'
|
||||||
const c = [
|
const c = [
|
||||||
@ -150,11 +153,38 @@ export class SFTPPanelComponent {
|
|||||||
]
|
]
|
||||||
let result = ''
|
let result = ''
|
||||||
for (let i = 0; i < c.length; i++) {
|
for (let i = 0; i < c.length; i++) {
|
||||||
result += item.attrs.mode & c[i] ? s[i] : e[i]
|
result += item.mode & c[i] ? s[i] : e[i]
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showContextMenu (item: SFTPFile, event: MouseEvent): void {
|
||||||
|
event.preventDefault()
|
||||||
|
this.platform.popupContextMenu([
|
||||||
|
{
|
||||||
|
click: async () => {
|
||||||
|
if ((await this.platform.showMessageBox({
|
||||||
|
type: 'warning',
|
||||||
|
message: `Delete ${item.fullPath}?`,
|
||||||
|
defaultId: 0,
|
||||||
|
buttons: ['Delete', 'Cancel'],
|
||||||
|
})).response === 0) {
|
||||||
|
await this.deleteItem(item)
|
||||||
|
this.navigate(this.path)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: 'Delete',
|
||||||
|
},
|
||||||
|
], event)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteItem (item: SFTPFile): Promise<void> {
|
||||||
|
const modal = this.ngbModal.open(SFTPDeleteModalComponent)
|
||||||
|
modal.componentInstance.item = item
|
||||||
|
modal.componentInstance.sftp = this.sftp
|
||||||
|
await modal.result
|
||||||
|
}
|
||||||
|
|
||||||
close (): void {
|
close (): void {
|
||||||
this.closed.emit()
|
this.closed.emit()
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,17 @@
|
|||||||
|
|
||||||
button.btn.btn-secondary.mr-2((click)='openSFTP()', *ngIf='session && session.open')
|
button.btn.btn-secondary.mr-2((click)='openSFTP()', *ngIf='session && session.open')
|
||||||
span SFTP
|
span SFTP
|
||||||
|
span.badge.badge-info.ml-2
|
||||||
|
i.fas.fa-flask
|
||||||
|
span Experimental
|
||||||
|
|
||||||
button.btn.btn-secondary((click)='showPortForwarding()', *ngIf='session && session.open')
|
button.btn.btn-secondary((click)='showPortForwarding()', *ngIf='session && session.open')
|
||||||
i.fas.fa-plug
|
i.fas.fa-plug
|
||||||
span Ports
|
span Ports
|
||||||
|
|
||||||
sftp-panel.bg-dark(
|
sftp-panel.bg-dark(
|
||||||
|
@panelSlide,
|
||||||
|
[(path)]='sftpPath',
|
||||||
*ngIf='sftpPanelVisible',
|
*ngIf='sftpPanelVisible',
|
||||||
(click)='$event.stopPropagation()',
|
(click)='$event.stopPropagation()',
|
||||||
[session]='session',
|
[session]='session',
|
||||||
|
@ -21,6 +21,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
connection?: SSHConnection
|
connection?: SSHConnection
|
||||||
session: SSHSession|null = null
|
session: SSHSession|null = null
|
||||||
sftpPanelVisible = false
|
sftpPanelVisible = false
|
||||||
|
sftpPath = '/'
|
||||||
private sessionStack: SSHSession[] = []
|
private sessionStack: SSHSession[] = []
|
||||||
private recentInputs = ''
|
private recentInputs = ''
|
||||||
private reconnectOffered = false
|
private reconnectOffered = false
|
||||||
|
@ -14,13 +14,14 @@ import { PromptModalComponent } from './components/promptModal.component'
|
|||||||
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
|
||||||
import { SSHTabComponent } from './components/sshTab.component'
|
import { SSHTabComponent } from './components/sshTab.component'
|
||||||
import { SFTPPanelComponent } from './components/sftpPanel.component'
|
import { SFTPPanelComponent } from './components/sftpPanel.component'
|
||||||
|
import { SFTPDeleteModalComponent } from './components/sftpDeleteModal.component'
|
||||||
|
|
||||||
import { ButtonProvider } from './buttonProvider'
|
import { ButtonProvider } from './buttonProvider'
|
||||||
import { SSHConfigProvider } from './config'
|
import { SSHConfigProvider } from './config'
|
||||||
import { SSHSettingsTabProvider } from './settings'
|
import { SSHSettingsTabProvider } from './settings'
|
||||||
import { RecoveryProvider } from './recoveryProvider'
|
import { RecoveryProvider } from './recoveryProvider'
|
||||||
import { SSHHotkeyProvider } from './hotkeys'
|
import { SSHHotkeyProvider } from './hotkeys'
|
||||||
import { WinSCPContextMenu } from './tabContextMenu'
|
import { SFTPContextMenu } from './tabContextMenu'
|
||||||
import { SSHCLIHandler } from './cli'
|
import { SSHCLIHandler } from './cli'
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@ -39,12 +40,13 @@ import { SSHCLIHandler } from './cli'
|
|||||||
{ provide: SettingsTabProvider, useClass: SSHSettingsTabProvider, multi: true },
|
{ provide: SettingsTabProvider, useClass: SSHSettingsTabProvider, multi: true },
|
||||||
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
|
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
|
||||||
{ provide: HotkeyProvider, useClass: SSHHotkeyProvider, multi: true },
|
{ provide: HotkeyProvider, useClass: SSHHotkeyProvider, multi: true },
|
||||||
{ provide: TabContextMenuItemProvider, useClass: WinSCPContextMenu, multi: true },
|
{ provide: TabContextMenuItemProvider, useClass: SFTPContextMenu, multi: true },
|
||||||
{ provide: CLIHandler, useClass: SSHCLIHandler, multi: true },
|
{ provide: CLIHandler, useClass: SSHCLIHandler, multi: true },
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
EditConnectionModalComponent,
|
EditConnectionModalComponent,
|
||||||
PromptModalComponent,
|
PromptModalComponent,
|
||||||
|
SFTPDeleteModalComponent,
|
||||||
SSHPortForwardingModalComponent,
|
SSHPortForwardingModalComponent,
|
||||||
SSHSettingsTabComponent,
|
SSHSettingsTabComponent,
|
||||||
SSHTabComponent,
|
SSHTabComponent,
|
||||||
@ -52,6 +54,7 @@ import { SSHCLIHandler } from './cli'
|
|||||||
declarations: [
|
declarations: [
|
||||||
EditConnectionModalComponent,
|
EditConnectionModalComponent,
|
||||||
PromptModalComponent,
|
PromptModalComponent,
|
||||||
|
SFTPDeleteModalComponent,
|
||||||
SSHPortForwardingModalComponent,
|
SSHPortForwardingModalComponent,
|
||||||
SSHPortForwardingConfigComponent,
|
SSHPortForwardingConfigComponent,
|
||||||
SSHSettingsTabComponent,
|
SSHSettingsTabComponent,
|
||||||
|
@ -6,7 +6,7 @@ import { SSHService } from './services/ssh.service'
|
|||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WinSCPContextMenu extends TabContextMenuItemProvider {
|
export class SFTPContextMenu extends TabContextMenuItemProvider {
|
||||||
weight = 10
|
weight = 10
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -19,24 +19,44 @@ import { TerminalDecorator } from './decorator'
|
|||||||
export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
||||||
static template: string = require<string>('../components/baseTerminalTab.component.pug')
|
static template: string = require<string>('../components/baseTerminalTab.component.pug')
|
||||||
static styles: string[] = [require<string>('../components/baseTerminalTab.component.scss')]
|
static styles: string[] = [require<string>('../components/baseTerminalTab.component.scss')]
|
||||||
static animations: AnimationTriggerMetadata[] = [trigger('slideInOut', [
|
static animations: AnimationTriggerMetadata[] = [
|
||||||
transition(':enter', [
|
trigger('toolbarSlide', [
|
||||||
style({
|
transition(':enter', [
|
||||||
transform: 'translateY(-25%)',
|
style({
|
||||||
opacity: '0',
|
transform: 'translateY(-25%)',
|
||||||
}),
|
opacity: '0',
|
||||||
animate('100ms ease-out', style({
|
}),
|
||||||
transform: 'translateY(0%)',
|
animate('100ms ease-out', style({
|
||||||
opacity: '1',
|
transform: 'translateY(0%)',
|
||||||
})),
|
opacity: '1',
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
transition(':leave', [
|
||||||
|
animate('100ms ease-out', style({
|
||||||
|
transform: 'translateY(-25%)',
|
||||||
|
opacity: '0',
|
||||||
|
})),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
transition(':leave', [
|
trigger('panelSlide', [
|
||||||
animate('100ms ease-out', style({
|
transition(':enter', [
|
||||||
transform: 'translateY(-25%)',
|
style({
|
||||||
opacity: '0',
|
transform: 'translateY(25%)',
|
||||||
})),
|
opacity: '0',
|
||||||
|
}),
|
||||||
|
animate('100ms ease-out', style({
|
||||||
|
transform: 'translateY(0%)',
|
||||||
|
opacity: '1',
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
transition(':leave', [
|
||||||
|
animate('100ms ease-out', style({
|
||||||
|
transform: 'translateY(25%)',
|
||||||
|
opacity: '0',
|
||||||
|
})),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
])]
|
]
|
||||||
|
|
||||||
session: BaseSession|null = null
|
session: BaseSession|null = null
|
||||||
savedState?: any
|
savedState?: any
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.content(#content, [style.opacity]='frontendIsReady ? 1 : 0')
|
.content(#content, [style.opacity]='frontendIsReady ? 1 : 0')
|
||||||
search-panel(
|
search-panel(
|
||||||
*ngIf='showSearchPanel',
|
*ngIf='showSearchPanel',
|
||||||
@slideInOut,
|
@toolbarSlide,
|
||||||
[frontend]='frontend',
|
[frontend]='frontend',
|
||||||
(close)='showSearchPanel = false'
|
(close)='showSearchPanel = false'
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user