This commit is contained in:
Eugene Pankov
2021-06-27 16:28:49 +02:00
parent 0689c984ff
commit df108d9071
24 changed files with 404 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
{
"name": "terminus-web-container",
"name": "tabby-web",
"version": "1.0.0",
"main": "index.js",
"scripts": {
@@ -17,6 +17,7 @@
"@angular/forms": "^11.0.0",
"@angular/platform-browser": "^11.0.0",
"@angular/platform-browser-dynamic": "^11.0.0",
"@angular/router": "^11.0.0",
"@fortawesome/angular-fontawesome": "0.8",
"@fortawesome/fontawesome-free": "^5.7.2",
"@fortawesome/fontawesome-svg-core": "^1.2.35",

View File

@@ -4,12 +4,21 @@ import { BrowserModule } from '@angular/platform-browser'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router'
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http'
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'
import { AppComponent } from './components/app.component'
import { MainComponent } from './components/main.component'
import { ConfigModalComponent } from './components/configModal.component'
import { SettingsModalComponent } from './components/settingsModal.component'
import { HomeComponent } from './components/home.component'
import { LoginComponent } from './components/login.component'
const ROUTES = [
{ path: '', component: HomeComponent },
{ path: 'app', component: MainComponent },
{ path: 'login', component: LoginComponent },
]
@NgModule({
imports: [
@@ -22,10 +31,13 @@ import { SettingsModalComponent } from './components/settingsModal.component'
NgbDropdownModule,
NgbModalModule,
FontAwesomeModule,
RouterModule.forRoot(ROUTES),
],
declarations: [
AppComponent,
MainComponent,
HomeComponent,
LoginComponent,
ConfigModalComponent,
SettingsModalComponent,
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

View File

@@ -1,10 +1 @@
main(*ngIf='ready && loggedIn')
.login-view(*ngIf='ready && !loggedIn')
.buttons
a.btn(
*ngFor='let provider of providers',
[class]='provider.cls',
href='/api/1/auth/social/login/{{provider.id}}'
)
fa-icon([icon]='provider.icon', [fixedWidth]='true')
span Log in with {{provider.name}}
router-outlet

View File

@@ -1,33 +1,9 @@
import { Component } from '@angular/core'
import { LoginService } from '../services/login.service'
import { faGithub, faGitlab, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons'
@Component({
selector: 'app',
templateUrl: './app.component.pug',
styleUrls: ['./app.component.scss'],
// styleUrls: ['./app.component.scss'],
})
export class AppComponent {
loggedIn: any
ready = false
providers = [
{ name: 'GitHub', icon: faGithub, cls: 'btn-primary', id: 'github' },
{ name: 'GitLab', icon: faGitlab, cls: 'btn-warning', id: 'gitlab' },
{ name: 'Google', icon: faGoogle, cls: 'btn-secondary', id: 'google-oauth2' },
{ name: 'Microsoft', icon: faMicrosoft, cls: 'btn-light', id: 'microsoft-graph' },
]
constructor (
private loginService: LoginService,
) {
this.providers = [this.providers[0]] // only keep GH for now
}
async ngOnInit () {
await this.loginService.ready$.toPromise()
this.loggedIn = !!this.loginService.user
this.ready = true
}
}

View File

@@ -0,0 +1,96 @@
.top-half
.navbar
img.brand(src='{{_logo}}')
.me-auto
a.btn.btn-primary([href]='releaseURL', target='_blank')
fa-icon([icon]='_downloadIcon', [fixedWidth]='true')
span Download
a.btn.btn-secondary(routerLink='/login')
fa-icon([icon]='_loginIcon', [fixedWidth]='true')
span Login
.container
.intro
h1 Hey.
div My name is Eugene and I've built a nice terminal app, #[em.ms-1 just for you].
div Crossplatform, local, SSH, serial - it's all there.
div Go on, try it out 👇
iframe(#iframe)
.bottom-half
.container
.d-flex.m-auto
a.btn.btn-lg.btn-primary.ms-auto.me-3([href]='releaseURL', target='_blank')
fa-icon([icon]='_downloadIcon', [fixedWidth]='true')
span Latest release
a.btn.btn-lg.btn-secondary.me-auto([href]='githubURL', target='_blank')
fa-icon([icon]='_githubIcon', [fixedWidth]='true')
span GitHub
.quotes
.quote
.text "reasonable"
.author — Michael
.quote
.text "cool"
.author — some dude on my ko-fi
.quote
.text "very cool"
.author — datsukan
.section.section-a
.container
.row
.col
img.screenshot([src]='screenshots.window')
.col
h1 The important stuff
ul
li Runs on #[strong Windows, Mac and Linux]
li Integrated #[strong SSH client] with a connection manager
li Integrated #[strong serial terminal]
li PowerShell, PS Core, WSL, Git-Bash, Cygwin, Cmder and CMD support
li Full #[strong Unicode support] including double-width characters
li File transfer from/to SSH sessions via #[strong SFTP and Zmodem]
li Theming and color schemes
li Fully #[strong configurable shortcuts] and multi-chord shortcuts
li #[strong Remembers your tabs] and split panes
li Proper shell experience on Windows including #[strong tab completion]
li Integrated #[strong encrypted container] for SSH secrets and configuration
.section.section-b
.container
.row
.col
h1 Terminal features
ul
li Multiple #[strong nested panes]
li #[strong Progress bars] and activity notifications for tabs
li Terminus remembers open tabs and panes where you left off
li Tabs on #[strong any side of the window]
li Optional #[strong quake mode] (terminal docked to a side of the screen)
li Optional #[strong global hotkey] to focus/hide the terminal
li Bracketed paste
.col
img.screenshot([src]='screenshots.tabs')
.section.section-a
.container
.row
.col
img.screenshot([src]='screenshots.ssh')
.col
h1 SSH
ul
li SSH2 client with a connection manager
li #[strong SFTP and Zmodem] file transfers
li #[strong X11] and #[strong port forwarding]
li Jump hosts
li #[strong Agent forwarding] - including Pageant and Windows native OpenSSH Agent
li Login scripts
li Optional built-in #[strong password manager] with a master passphrase
li #[strong Proxy command] support

View File

@@ -0,0 +1,95 @@
@import "../theme/vars.scss";
:host {
font-size: 20px;
}
.top-half {
background: linear-gradient(#0c141c, #15202b);
}
.navbar {
display: flex;
padding: 15px 30px;
a, button {
margin-left: 10px;
}
}
.brand {
width: 32px;
}
h1 {
font-family: $font-family-monospace;
font-weight: bold;
font-size: 48px;
text-shadow: 0 0 1px black;
}
.intro {
width: 60vw;
margin: auto;
}
iframe {
display: block;
margin: auto;
position: relative;
top: 20vw;
margin-top: -18vw;
width: 60vw;
height: 42vw;
overflow: hidden;
border-radius: 5px;
box-shadow: 0 0 2px black, 0 0 50px #6ef2ff05, 0 0 150px #6854ff14;
}
.bottom-half {
padding-top: 24vw;
}
.quotes {
margin: 30px 0;
display: flex;
justify-content: center;
text-align: center;
.quote {
margin: 0 30px;
.text {
font-size: 50px;
font-style: italic;
}
.author {
font-size: 14px;
}
}
}
strong {
background: #9400ff57;
font-weight: normal;
padding: 2px 7px;
text-shadow: 0 0 2px black;
}
.section {
padding: 30px 0;
.screenshot {
min-width: 0;
max-width: 100%;
}
}
.section-a {
background: rgba(0, 0, 0, .5);
}
.section-b {
}

View File

@@ -0,0 +1,81 @@
import * as semverGT from 'semver/functions/gt'
import { HttpClient } from '@angular/common/http'
import { Component, ElementRef, ViewChild } from '@angular/core'
import { Version } from '../api'
import { faDownload, faSignInAlt } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
class DemoConnector {
constructor (targetWindow: Window, private version: Version) {
targetWindow['terminusWebDemoDataPath'] = `${this.getDistURL()}/${version.version}/terminus-web-demo/data`
}
async loadConfig (): Promise<string> {
return '{}'
}
async saveConfig (content: string): Promise<void> {
}
getAppVersion (): string {
return this.version.version
}
getDistURL (): string {
return '/app-dist'
}
getPluginsToLoad (): string[] {
return [
'terminus-core',
'terminus-settings',
'terminus-terminal',
'terminus-community-color-schemes',
'terminus-web',
'terminus-web-demo',
]
}
}
@Component({
selector: 'home',
templateUrl: './home.component.pug',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent {
@ViewChild('iframe') iframe: ElementRef
connector: DemoConnector
githubURL = 'https://github.com/Eugeny/terminus'
releaseURL = `${this.githubURL}/releases/latest`
_logo = require('../assets/logo.svg')
_downloadIcon = faDownload
_loginIcon = faSignInAlt
_githubIcon = faGithub
screenshots = {
window: require('../assets/screenshots/window.png'),
tabs: require('../assets/screenshots/tabs.png'),
ssh: require('../assets/screenshots/ssh.png'),
}
constructor (private http: HttpClient) {
window.addEventListener('message', this.connectorRequestHandler)
}
connectorRequestHandler = event => {
if (event.data === 'request-connector') {
this.iframe.nativeElement.contentWindow['__connector__'] = this.connector
this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*')
}
}
async ngAfterViewInit () {
const versions = await this.http.get('/api/1/versions').toPromise()
versions.sort((a, b) => semverGT(a, b))
this.connector = new DemoConnector(this.iframe.nativeElement.contentWindow, versions[0])
this.iframe.nativeElement.src = '/terminal'
}
ngOnDestroy () {
window.removeEventListener('message', this.connectorRequestHandler)
}
}

View File

@@ -0,0 +1,10 @@
main(*ngIf='ready && loggedIn')
.login-view(*ngIf='ready && !loggedIn')
.buttons
a.btn(
*ngFor='let provider of providers',
[class]='provider.cls',
href='/api/1/auth/social/login/{{provider.id}}'
)
fa-icon([icon]='provider.icon', [fixedWidth]='true')
span Log in with {{provider.name}}

View File

@@ -0,0 +1,33 @@
import { Component } from '@angular/core'
import { LoginService } from '../services/login.service'
import { faGithub, faGitlab, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons'
@Component({
selector: 'login',
templateUrl: './login.component.pug',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent {
loggedIn: any
ready = false
providers = [
{ name: 'GitHub', icon: faGithub, cls: 'btn-primary', id: 'github' },
{ name: 'GitLab', icon: faGitlab, cls: 'btn-warning', id: 'gitlab' },
{ name: 'Google', icon: faGoogle, cls: 'btn-secondary', id: 'google-oauth2' },
{ name: 'Microsoft', icon: faMicrosoft, cls: 'btn-light', id: 'microsoft-graph' },
]
constructor (
private loginService: LoginService,
) {
this.providers = [this.providers[0]] // only keep GH for now
}
async ngOnInit () {
await this.loginService.ready$.toPromise()
this.loggedIn = !!this.loginService.user
this.ready = true
}
}

View File

@@ -10,6 +10,7 @@ import { ConfigModalComponent } from './configModal.component'
import { ConfigService } from '../services/config.service'
import { combineLatest } from 'rxjs'
import { Config, Version } from '../api'
import { Router } from '@angular/router'
@Component({
selector: 'main',
@@ -35,16 +36,25 @@ export class MainComponent {
public loginService: LoginService,
private ngbModal: NgbModal,
private config: ConfigService,
private router: Router,
) {
window.addEventListener('message', event => {
if (event.data === 'request-connector') {
this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector
this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*')
}
})
window.addEventListener('message', this.connectorRequestHandler)
}
connectorRequestHandler = event => {
if (event.data === 'request-connector') {
this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector
this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*')
}
}
async ngAfterViewInit () {
await this.loginService.ready$.toPromise()
if (!this.loginService.user) {
this.router.navigate(['/login'])
return
}
combineLatest(
this.config.activeConfig$,
this.config.activeVersion$
@@ -58,6 +68,10 @@ export class MainComponent {
await this.config.selectDefaultConfig()
}
ngOnDestroy () {
window.removeEventListener('message', this.connectorRequestHandler)
}
unloadApp () {
this.showApp = false
this.iframe.nativeElement.src = 'about:blank'

View File

@@ -1,8 +1,8 @@
doctype html
html
head
base(href='/')
meta(name='viewport', content='initial-scale=1, minimal-ui, shrink-to-fit=no')
link(href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400", rel="stylesheet")
script(src='/build/index.js', defer)
title Terminus
body

View File

@@ -119,6 +119,21 @@ export class AppConnectorService {
return this.version.version
}
getDistURL (): string {
return '../app-dist'
}
getPluginsToLoad (): string[] {
return [
'terminus-core',
'terminus-settings',
'terminus-terminal',
'terminus-ssh',
'terminus-community-color-schemes',
'terminus-web',
]
}
createSocket () {
return new SocketProxy(this)
}

View File

@@ -6,4 +6,7 @@ body {
overscroll-behavior: none;
}
@import "~source-code-pro/source-code-pro.css";
@import "~source-sans-pro/source-sans-pro.css";
@import "./theme/index.scss"

View File

@@ -27,35 +27,32 @@ async function start () {
return window['module'].exports
}
const baseUrl = `../app-dist/${appVersion}`
const baseUrl = `${connector.getDistURL()}/${appVersion}`
await webRequire(`${baseUrl}/web/dist/preload.js`)
await webRequire(`${baseUrl}/web/dist/bundle.js`)
const terminus = window['Terminus']
const pluginModules = []
for (const plugin of [
'terminus-core',
'terminus-settings',
'terminus-terminal',
'terminus-ssh',
'terminus-community-color-schemes',
'terminus-web',
]) {
for (const plugin of connector.getPluginsToLoad()) {
pluginModules.push(await terminus.loadPlugin(`${baseUrl}/${plugin}`))
}
document.querySelector('app-root')['style'].display = 'flex'
const config = connector.loadConfig()
terminus.bootstrap(pluginModules, {
config,
executable: 'web',
isFirstWindow: true,
windowID: 1,
installedPlugins: [],
userPluginsPath: '/',
terminus.bootstrap({
packageModules: pluginModules,
bootstrapData: {
config,
executable: 'web',
isFirstWindow: true,
windowID: 1,
installedPlugins: [],
userPluginsPath: '/',
},
debugMode: false,
connector,
})
}
start()

View File

@@ -66,3 +66,13 @@
.modal-footer {
background: #00000030;
}
a, button {
fa-icon {
opacity: .75;
}
fa-icon + * {
margin-left: 5px;
}
}

View File

@@ -51,7 +51,6 @@ class AppVersionViewSet(ListModelMixin, GenericViewSet):
lookup_field = 'id'
lookup_value_regex = r'[\w\d.-]+'
queryset = ''
permission_classes = [IsAuthenticated]
def _get_versions(self):
return [AppVersion(version=x) for x in os.listdir(settings.APP_DIST_PATH)]

View File

@@ -14,7 +14,8 @@ urlpatterns = [
path('api/1/auth/logout', api.LogoutView.as_view()),
path('api/1/user', api.UserViewSet.as_view({'get': 'retrieve', 'put': 'update'})),
path('', views.IndexView.as_view()),
re_path('^(|login|app)$', views.IndexView.as_view()),
path('terminal', views.TerminalView.as_view()),
path('app-dist/<version>/<path:path>', views.AppDistView.as_view()),
path('build/<path:path>', views.BuildView.as_view()),

View File

@@ -138,7 +138,7 @@ AUTHENTICATION_BACKENDS = (
SOCIAL_AUTH_GITHUB_SCOPE = ['read:user', 'user:email']
LOGIN_REDIRECT_URL = '/'
LOGIN_REDIRECT_URL = '/app'
APP_DIST_PATH = BASE_DIR / 'app-dist'

View File

@@ -72,6 +72,13 @@
dependencies:
tslib "^2.0.0"
"@angular/router@^11.0.0":
version "11.2.14"
resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.2.14.tgz#4a8eb6d010a1a45c8e7f0c3b5b705959bdb69294"
integrity sha512-3aYBmj+zrEL9yf/ntIQxHIYaWShZOBKP3U07X2mX+TPMpGlvHDnR7L6bWhQVZwewzMMz7YVR16ldg50IFuAlfA==
dependencies:
tslib "^2.0.0"
"@babel/code-frame@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"