mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-06 11:29:53 +00:00
reworked login scripts - fixes #1349
This commit is contained in:
parent
5053743b1b
commit
9502240480
@ -17,6 +17,7 @@
|
|||||||
"@types/fs-extra": "^9.0.12",
|
"@types/fs-extra": "^9.0.12",
|
||||||
"@types/js-yaml": "^4.0.2",
|
"@types/js-yaml": "^4.0.2",
|
||||||
"@types/node": "16.0.1",
|
"@types/node": "16.0.1",
|
||||||
|
"@types/sortablejs": "^1.10.7",
|
||||||
"@types/webpack-env": "^1.16.2",
|
"@types/webpack-env": "^1.16.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||||
"@typescript-eslint/parser": "^4.28.2",
|
"@typescript-eslint/parser": "^4.28.2",
|
||||||
@ -40,6 +41,7 @@
|
|||||||
"json-loader": "0.5.7",
|
"json-loader": "0.5.7",
|
||||||
"lru-cache": "^6.0.0",
|
"lru-cache": "^6.0.0",
|
||||||
"macos-release": "^2.5.0",
|
"macos-release": "^2.5.0",
|
||||||
|
"ngx-sortablejs": "^11.1.0",
|
||||||
"ngx-toastr": "^14.0.0",
|
"ngx-toastr": "^14.0.0",
|
||||||
"node-abi": "^2.30.0",
|
"node-abi": "^2.30.0",
|
||||||
"node-sass": "^6.0.1",
|
"node-sass": "^6.0.1",
|
||||||
@ -55,6 +57,7 @@
|
|||||||
"sass-loader": "^12.1.0",
|
"sass-loader": "^12.1.0",
|
||||||
"shelljs": "0.8.4",
|
"shelljs": "0.8.4",
|
||||||
"slugify": "^1.5.3",
|
"slugify": "^1.5.3",
|
||||||
|
"sortablejs": "^1.14.0",
|
||||||
"source-code-pro": "^2.38.0",
|
"source-code-pro": "^2.38.0",
|
||||||
"source-map-loader": "^3.0.0",
|
"source-map-loader": "^3.0.0",
|
||||||
"source-sans-pro": "3.6.0",
|
"source-sans-pro": "3.6.0",
|
||||||
|
@ -6,6 +6,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
|
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
|
||||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||||
import { DndModule } from 'ng2-dnd'
|
import { DndModule } from 'ng2-dnd'
|
||||||
|
import { SortablejsModule } from 'ngx-sortablejs'
|
||||||
|
|
||||||
import { AppRootComponent } from './components/appRoot.component'
|
import { AppRootComponent } from './components/appRoot.component'
|
||||||
import { CheckboxComponent } from './components/checkbox.component'
|
import { CheckboxComponent } from './components/checkbox.component'
|
||||||
@ -77,6 +78,7 @@ const PROVIDERS = [
|
|||||||
NgxFilesizeModule,
|
NgxFilesizeModule,
|
||||||
PerfectScrollbarModule,
|
PerfectScrollbarModule,
|
||||||
DndModule.forRoot(),
|
DndModule.forRoot(),
|
||||||
|
SortablejsModule.forRoot({ animation: 150 }),
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppRootComponent as any,
|
AppRootComponent as any,
|
||||||
@ -118,6 +120,7 @@ const PROVIDERS = [
|
|||||||
DropZoneDirective,
|
DropZoneDirective,
|
||||||
FastHtmlBindDirective,
|
FastHtmlBindDirective,
|
||||||
AlwaysVisibleTypeaheadDirective,
|
AlwaysVisibleTypeaheadDirective,
|
||||||
|
SortablejsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||||
|
@ -56,7 +56,7 @@ h3.mb-3 Profiles
|
|||||||
*ngIf='group.editable && group.name',
|
*ngIf='group.editable && group.name',
|
||||||
(click)='$event.stopPropagation(); deleteGroup(group)'
|
(click)='$event.stopPropagation(); deleteGroup(group)'
|
||||||
)
|
)
|
||||||
i.fas.fa-trash
|
i.fas.fa-trash-alt
|
||||||
ng-container(*ngIf='!group.collapsed')
|
ng-container(*ngIf='!group.collapsed')
|
||||||
ng-container(*ngFor='let profile of group.profiles')
|
ng-container(*ngFor='let profile of group.profiles')
|
||||||
.list-group-item.pl-5.d-flex.align-items-center(
|
.list-group-item.pl-5.d-flex.align-items-center(
|
||||||
@ -85,10 +85,10 @@ h3.mb-3 Profiles
|
|||||||
button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); newProfile(profile)')
|
button.btn.btn-link.hover-reveal.ml-1((click)='$event.stopPropagation(); newProfile(profile)')
|
||||||
i.fas.fa-copy
|
i.fas.fa-copy
|
||||||
|
|
||||||
button.btn.btn-link.text-danger.hover-reveal.ml-1(
|
button.btn.btn-link.hover-reveal.ml-1(
|
||||||
*ngIf='!profile.isBuiltin',
|
*ngIf='!profile.isBuiltin',
|
||||||
(click)='$event.stopPropagation(); deleteProfile(profile)'
|
(click)='$event.stopPropagation(); deleteProfile(profile)'
|
||||||
)
|
)
|
||||||
i.fas.fa-trash
|
i.fas.fa-trash-alt
|
||||||
|
|
||||||
.ml-1(class='badge badge-{{getTypeColorClass(profile)}}') {{getTypeLabel(profile)}}
|
.ml-1(class='badge badge-{{getTypeColorClass(profile)}}') {{getTypeLabel(profile)}}
|
||||||
|
@ -189,6 +189,6 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||||||
li(ngbNavItem)
|
li(ngbNavItem)
|
||||||
a(ngbNavLink) Login scripts
|
a(ngbNavLink) Login scripts
|
||||||
ng-template(ngbNavContent)
|
ng-template(ngbNavContent)
|
||||||
login-scripts-settings([options]='profile.options')
|
login-scripts-settings([options]='profile.options', #loginScriptsSettings)
|
||||||
|
|
||||||
div([ngbNavOutlet]='nav')
|
div([ngbNavOutlet]='nav')
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { Component } from '@angular/core'
|
import { Component, ViewChild } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core'
|
import { ConfigService, FileProvidersService, Platform, HostAppService, PromptModalComponent } from 'tabby-core'
|
||||||
|
import { LoginScriptsSettingsComponent } from 'tabby-terminal'
|
||||||
import { PasswordStorageService } from '../services/passwordStorage.service'
|
import { PasswordStorageService } from '../services/passwordStorage.service'
|
||||||
import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api'
|
import { ForwardedPortConfig, SSHAlgorithmType, SSHProfile } from '../api'
|
||||||
import { SSHProfilesService } from '../profiles'
|
import { SSHProfilesService } from '../profiles'
|
||||||
@ -20,6 +21,7 @@ export class SSHProfileSettingsComponent {
|
|||||||
supportedAlgorithms: Record<string, string> = {}
|
supportedAlgorithms: Record<string, string> = {}
|
||||||
algorithms: Record<string, Record<string, boolean>> = {}
|
algorithms: Record<string, Record<string, boolean>> = {}
|
||||||
jumpHosts: SSHProfile[]
|
jumpHosts: SSHProfile[]
|
||||||
|
@ViewChild('loginScriptsSettings') loginScriptsSettings: LoginScriptsSettingsComponent|null
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public hostApp: HostAppService,
|
public hostApp: HostAppService,
|
||||||
@ -92,6 +94,7 @@ export class SSHProfileSettingsComponent {
|
|||||||
if (!this.useProxyCommand) {
|
if (!this.useProxyCommand) {
|
||||||
this.profile.options.proxyCommand = undefined
|
this.profile.options.proxyCommand = undefined
|
||||||
}
|
}
|
||||||
|
this.loginScriptsSettings?.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
onForwardAdded (fw: ForwardedPortConfig) {
|
onForwardAdded (fw: ForwardedPortConfig) {
|
||||||
|
@ -24,17 +24,17 @@
|
|||||||
"@types/deep-equal": "^1.0.0",
|
"@types/deep-equal": "^1.0.0",
|
||||||
"@types/shell-escape": "^0.2.0",
|
"@types/shell-escape": "^0.2.0",
|
||||||
"ansi-colors": "^4.1.1",
|
"ansi-colors": "^4.1.1",
|
||||||
|
"binstring": "^0.2.1",
|
||||||
|
"buffer-replace": "^1.0.0",
|
||||||
|
"cli-spinner": "^0.2.10",
|
||||||
"dataurl": "0.1.0",
|
"dataurl": "0.1.0",
|
||||||
"deep-equal": "2.0.5",
|
"deep-equal": "2.0.5",
|
||||||
|
"hexer": "^1.5.0",
|
||||||
"ps-node": "^0.1.6",
|
"ps-node": "^0.1.6",
|
||||||
"cli-spinner": "^0.2.10",
|
|
||||||
"runes": "^0.4.2",
|
"runes": "^0.4.2",
|
||||||
"shell-escape": "^0.2.0",
|
"shell-escape": "^0.2.0",
|
||||||
"utils-decorators": "^1.8.1",
|
"utils-decorators": "^1.8.1",
|
||||||
"xterm": "^4.9.0-beta.7",
|
"xterm": "^4.9.0-beta.7",
|
||||||
"binstring": "^0.2.1",
|
|
||||||
"buffer-replace": "^1.0.0",
|
|
||||||
"hexer": "^1.5.0",
|
|
||||||
"xterm-addon-fit": "^0.5.0",
|
"xterm-addon-fit": "^0.5.0",
|
||||||
"xterm-addon-ligatures": "^0.5.0",
|
"xterm-addon-ligatures": "^0.5.0",
|
||||||
"xterm-addon-search": "^0.8.0",
|
"xterm-addon-search": "^0.8.0",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import deepClone from 'clone-deep'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { Logger } from 'tabby-core'
|
import { Logger } from 'tabby-core'
|
||||||
|
|
||||||
@ -18,11 +19,28 @@ export class LoginScriptProcessor {
|
|||||||
private outputToSession = new Subject<Buffer>()
|
private outputToSession = new Subject<Buffer>()
|
||||||
private remainingScripts: LoginScript[] = []
|
private remainingScripts: LoginScript[] = []
|
||||||
|
|
||||||
|
private escapeSeqMap = {
|
||||||
|
a: '\x07',
|
||||||
|
b: '\x08',
|
||||||
|
e: '\x1b',
|
||||||
|
f: '\x0c',
|
||||||
|
n: '\x0a',
|
||||||
|
r: '\x0d',
|
||||||
|
t: '\x09',
|
||||||
|
v: '\x0b',
|
||||||
|
}
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private logger: Logger,
|
private logger: Logger,
|
||||||
options: LoginScriptsOptions
|
options: LoginScriptsOptions
|
||||||
) {
|
) {
|
||||||
this.remainingScripts = options.scripts ?? []
|
this.remainingScripts = deepClone(options.scripts ?? [])
|
||||||
|
for (const script of this.remainingScripts) {
|
||||||
|
if (!script.isRegex) {
|
||||||
|
script.expect = this.unescape(script.expect)
|
||||||
|
}
|
||||||
|
script.send = this.unescape(script.send)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
feedFromSession (data: Buffer): boolean {
|
feedFromSession (data: Buffer): boolean {
|
||||||
@ -34,25 +52,17 @@ export class LoginScriptProcessor {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let match = false
|
let match = false
|
||||||
let cmd = ''
|
|
||||||
if (script.isRegex) {
|
if (script.isRegex) {
|
||||||
const re = new RegExp(script.expect, 'g')
|
const re = new RegExp(script.expect, 'g')
|
||||||
if (re.exec(dataString)) {
|
match = re.test(dataString)
|
||||||
cmd = dataString.replace(re, script.send)
|
|
||||||
match = true
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (dataString.includes(script.expect)) {
|
match = dataString.includes(script.expect)
|
||||||
cmd = script.send
|
|
||||||
match = true
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
this.logger.info('Executing script: "' + cmd + '"')
|
found = true
|
||||||
this.outputToSession.next(Buffer.from(cmd + '\n'))
|
this.logger.info('Executing script:', script)
|
||||||
|
this.outputToSession.next(Buffer.from(script.send + '\n'))
|
||||||
this.remainingScripts = this.remainingScripts.filter(x => x !== script)
|
this.remainingScripts = this.remainingScripts.filter(x => x !== script)
|
||||||
} else {
|
} else {
|
||||||
if (script.optional) {
|
if (script.optional) {
|
||||||
@ -83,4 +93,13 @@ export class LoginScriptProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unescape (line: string): string {
|
||||||
|
line = line.replace(/\\((x\d{2})|(u\d{4}))/g, (match, g) => {
|
||||||
|
return String.fromCharCode(parseInt(g.substr(1), 16))
|
||||||
|
})
|
||||||
|
return line.replace(/\\(.)/g, (match, g) => {
|
||||||
|
return this.escapeSeqMap[g] || g
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,43 @@
|
|||||||
table(*ngIf='options.scripts.length > 0')
|
div([sortablejs]='scripts')
|
||||||
tr
|
.input-group.flex-grow-1(*ngFor='let script of scripts')
|
||||||
th String to expect
|
input.form-control(
|
||||||
th String to be sent
|
type='text',
|
||||||
th.pl-2 Regex
|
placeholder='Expect',
|
||||||
th.pl-2 Optional
|
[(ngModel)]='script.expect'
|
||||||
th.pl-2 Actions
|
)
|
||||||
tr(*ngFor='let script of options.scripts')
|
input.form-control(
|
||||||
td.pr-2
|
type='text',
|
||||||
input.form-control(
|
placeholder='Send',
|
||||||
type='text',
|
[(ngModel)]='script.send'
|
||||||
[(ngModel)]='script.expect'
|
)
|
||||||
)
|
.input-group-append.input-group-text.p-0.border-0
|
||||||
td
|
.hover-reveal(ngbDropdown)
|
||||||
input.form-control(
|
button.btn.btn-link.btn-sm(ngbDropdownToggle)
|
||||||
type='text',
|
i.fas.fa-fw.fa-cog
|
||||||
[(ngModel)]='script.send'
|
.dropdown-menu-right(ngbDropdownMenu)
|
||||||
)
|
a.dropdown-item(
|
||||||
td.pl-2
|
href='#',
|
||||||
checkbox(
|
(click)='script.isRegex = false',
|
||||||
[(ngModel)]='script.isRegex',
|
[class.active]='!script.isRegex',
|
||||||
)
|
)
|
||||||
td.pl-2
|
i.fas.fa-fw([class.fa-check]='!script.isRegex')
|
||||||
checkbox(
|
span Exact match
|
||||||
[(ngModel)]='script.optional',
|
a.dropdown-item(
|
||||||
)
|
href='#',
|
||||||
td.pl-2
|
(click)='script.isRegex = true',
|
||||||
.input-group.flex-nowrap
|
[class.active]='script.isRegex',
|
||||||
button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)')
|
)
|
||||||
i.fas.fa-arrow-up
|
i.fas.fa-fw([class.fa-check]='script.isRegex')
|
||||||
button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)')
|
span Regex
|
||||||
i.fas.fa-arrow-down
|
a.dropdown-item(
|
||||||
button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)')
|
href='#',
|
||||||
i.fas.fa-trash
|
(click)='script.optional = !script.optional',
|
||||||
|
[class.active]='script.optional',
|
||||||
|
)
|
||||||
|
i.fas.fa-fw([class.fa-check]='script.optional')
|
||||||
|
span Optional
|
||||||
|
button.btn.btn-link.btn-sm.hover-reveal((click)='deleteScript(script)')
|
||||||
|
i.fas.fa-fw.fa-trash-alt
|
||||||
|
|
||||||
button.btn.btn-outline-info.mt-2((click)='addScript()')
|
button.btn.btn-outline-info.mt-2((click)='addScript()')
|
||||||
i.fas.fa-plus
|
i.fas.fa-plus
|
||||||
|
@ -11,29 +11,14 @@ import { LoginScript, LoginScriptsOptions } from '../api/loginScriptProcessing'
|
|||||||
})
|
})
|
||||||
export class LoginScriptsSettingsComponent {
|
export class LoginScriptsSettingsComponent {
|
||||||
@Input() options: LoginScriptsOptions
|
@Input() options: LoginScriptsOptions
|
||||||
|
scripts: LoginScript[]
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private platform: PlatformService,
|
private platform: PlatformService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.options.scripts ??= []
|
this.scripts = this.options.scripts ?? []
|
||||||
}
|
|
||||||
|
|
||||||
moveScriptUp (script: LoginScript) {
|
|
||||||
const index = this.options.scripts!.indexOf(script)
|
|
||||||
if (index > 0) {
|
|
||||||
this.options.scripts!.splice(index, 1)
|
|
||||||
this.options.scripts!.splice(index - 1, 0, script)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
moveScriptDown (script: LoginScript) {
|
|
||||||
const index = this.options.scripts!.indexOf(script)
|
|
||||||
if (index >= 0 && index < this.options.scripts!.length - 1) {
|
|
||||||
this.options.scripts!.splice(index, 1)
|
|
||||||
this.options.scripts!.splice(index + 1, 0, script)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteScript (script: LoginScript) {
|
async deleteScript (script: LoginScript) {
|
||||||
@ -46,11 +31,15 @@ export class LoginScriptsSettingsComponent {
|
|||||||
defaultId: 1,
|
defaultId: 1,
|
||||||
}
|
}
|
||||||
)).response === 1) {
|
)).response === 1) {
|
||||||
this.options.scripts = this.options.scripts!.filter(x => x !== script)
|
this.scripts = this.scripts.filter(x => x !== script)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addScript () {
|
addScript () {
|
||||||
this.options.scripts!.push({ expect: '', send: '' })
|
this.scripts.push({ expect: '', send: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
this.options.scripts = this.scripts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,3 +120,4 @@ export * from './api/interfaces'
|
|||||||
export * from './api/streamProcessing'
|
export * from './api/streamProcessing'
|
||||||
export * from './api/loginScriptProcessing'
|
export * from './api/loginScriptProcessing'
|
||||||
export * from './session'
|
export * from './session'
|
||||||
|
export { LoginScriptsSettingsComponent, StreamProcessingSettingsComponent }
|
||||||
|
17
yarn.lock
17
yarn.lock
@ -509,6 +509,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/sortablejs@^1.10.7":
|
||||||
|
version "1.10.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.10.7.tgz#ab9039c85429f0516955ec6dbc0bb20139417b15"
|
||||||
|
integrity sha512-lGCwwgpj8zW/ZmaueoPVSP7nnc9t8VqVWXS+ASX3eoUUENmiazv0rlXyTRludXzuX9ALjPsMqBu85TgJNWbTOg==
|
||||||
|
|
||||||
"@types/verror@^1.10.3":
|
"@types/verror@^1.10.3":
|
||||||
version "1.10.4"
|
version "1.10.4"
|
||||||
resolved "https://registry.npmjs.org/@types/verror/-/verror-1.10.4.tgz"
|
resolved "https://registry.npmjs.org/@types/verror/-/verror-1.10.4.tgz"
|
||||||
@ -5036,6 +5041,13 @@ neo-async@^2.6.0, neo-async@^2.6.2:
|
|||||||
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
|
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||||
|
|
||||||
|
ngx-sortablejs@^11.1.0:
|
||||||
|
version "11.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ngx-sortablejs/-/ngx-sortablejs-11.1.0.tgz#14e50c48db908c1cb4b37722b28c2d3867c6140a"
|
||||||
|
integrity sha512-eM4dHwWSmXDcvF5gUmyMMQ0qqcqBXWCSZ9IRpqx4UkBKfo4N7pk/QuYh5io2fXVHWVFDaxW1yhn2FNpqxV6Jqw==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
ngx-toastr@^14.0.0:
|
ngx-toastr@^14.0.0:
|
||||||
version "14.0.0"
|
version "14.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-14.0.0.tgz#20e4737ef330b892a453768cd98b980558aeb286"
|
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-14.0.0.tgz#20e4737ef330b892a453768cd98b980558aeb286"
|
||||||
@ -7142,6 +7154,11 @@ socks@^1.1.10:
|
|||||||
ip "^1.1.4"
|
ip "^1.1.4"
|
||||||
smart-buffer "^1.0.13"
|
smart-buffer "^1.0.13"
|
||||||
|
|
||||||
|
sortablejs@^1.14.0:
|
||||||
|
version "1.14.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
|
||||||
|
integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
|
||||||
|
|
||||||
sorted-object@~2.0.1:
|
sorted-object@~2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmjs.org/sorted-object/-/sorted-object-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/sorted-object/-/sorted-object-2.0.1.tgz"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user