diff --git a/frontend/src/app.module.ts b/frontend/src/app.module.ts index 305671d..d8435d9 100644 --- a/frontend/src/app.module.ts +++ b/frontend/src/app.module.ts @@ -1,5 +1,4 @@ import { NgModule } from '@angular/core' -import { NgbDropdownModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap' import { BrowserModule } from '@angular/platform-browser' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { CommonModule } from '@angular/common' @@ -40,8 +39,6 @@ const ROUTES = [ BrowserAnimationsModule, CommonModule, FormsModule, - NgbDropdownModule, - NgbModalModule, FontAwesomeModule, ClipboardModule, HttpClientModule, diff --git a/frontend/src/app/components/main.component.pug b/frontend/src/app/components/main.component.pug index cd38f91..cd8f6ae 100644 --- a/frontend/src/app/components/main.component.pug +++ b/frontend/src/app/components/main.component.pug @@ -1,14 +1,40 @@ .sidebar img.logo(src='{{_logo}}') - button.btn.mt-auto((click)='openConfig()') - fa-icon([icon]='_configIcon', [fixedWidth]='true') + button.btn.mt-auto( + (click)='openConfig()', + placement='right', + ngbTooltip='Manage configs' + ) + fa-icon([icon]='_configIcon', [fixedWidth]='true', size='lg') - button.btn((click)='openSettings()') - fa-icon([icon]='_settingsIcon', [fixedWidth]='true') + button.btn( + (click)='openSettings()', + *ngIf='loginService.user', + placement='right', + ngbTooltip='Settings' + ) + fa-icon([icon]='_settingsIcon', [fixedWidth]='true', size='lg') - button.btn.mt-3((click)='logout()') - fa-icon([icon]='_logoutIcon', [fixedWidth]='true') + a.btn.mt-3( + href='/login', + *ngIf='!loginService.user', + placement='right', + ngbTooltip='Log in' + ) + fa-icon([icon]='_loginIcon', [fixedWidth]='true', size='lg') -.terminal([hidden]='!showApp') - iframe(#iframe) + button.btn.mt-3( + (click)='logout()', + *ngIf='loginService.user', + placement='right', + ngbTooltip='Log out' + ) + fa-icon([icon]='_logoutIcon', [fixedWidth]='true', size='lg') + +.terminal + iframe(#iframe, [hidden]='!showApp') + .alert.alert-warning.d-flex.border-0.m-0(*ngIf='!loginService.user') + fa-icon.me-2([icon]='_saveIcon', [fixedWidth]='true') + div + div To save profiles and settings, #[a(href='/login') log in]. diff --git a/frontend/src/app/components/main.component.scss b/frontend/src/app/components/main.component.scss index f3b1e00..3f56a0c 100644 --- a/frontend/src/app/components/main.component.scss +++ b/frontend/src/app/components/main.component.scss @@ -25,7 +25,7 @@ margin-bottom: 20px; } - >button, >[ngbdropdown] > button { + >.btn { width: 64px; height: 64px; background: transparent; @@ -34,6 +34,10 @@ &::after { display: none; } + + &:hover { + color: white; + } } } @@ -41,18 +45,19 @@ flex: 1 1 0; overflow: hidden; position: relative; -} + display: flex; + flex-direction: column; -iframe { - background: $body-bg; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - border: none; -} + > * { + flex: none; + } + > iframe { + background: $body-bg; + border: none; + flex: 1 1 0; + } +} .config-menu { .header { diff --git a/frontend/src/app/components/main.component.ts b/frontend/src/app/components/main.component.ts index 3c0139f..2c98b3e 100644 --- a/frontend/src/app/components/main.component.ts +++ b/frontend/src/app/components/main.component.ts @@ -3,14 +3,13 @@ import { HttpClient } from '@angular/common/http' import { Title } from '@angular/platform-browser' import { AppConnectorService } from '../services/appConnector.service' -import { faCog, faFile, faPlus, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' +import { faCog, faFile, faPlus, faSave, faSignInAlt, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { SettingsModalComponent } from './settingsModal.component' import { ConfigModalComponent } from './configModal.component' import { ConfigService, LoginService } from 'src/common' import { combineLatest } from 'rxjs' import { Config, Version } from 'src/api' -import { Router } from '@angular/router' @Component({ selector: 'main', @@ -20,9 +19,11 @@ import { Router } from '@angular/router' export class MainComponent { _logo = require('../../../assets/logo.svg') _settingsIcon = faCog + _loginIcon = faSignInAlt _logoutIcon = faSignOutAlt _addIcon = faPlus _configIcon = faFile + _saveIcon = faSave showApp = false @@ -35,7 +36,6 @@ export class MainComponent { public loginService: LoginService, private ngbModal: NgbModal, private config: ConfigService, - private router: Router, ) { titleService.setTitle('Tabby') window.addEventListener('message', this.connectorRequestHandler) @@ -50,10 +50,6 @@ export class MainComponent { async ngAfterViewInit () { await this.loginService.ready$.toPromise() - if (!this.loginService.user) { - this.router.navigate(['/login']) - return - } combineLatest( this.config.activeConfig$, @@ -63,7 +59,7 @@ export class MainComponent { this.reloadApp(config, version) } }) - this.config + await this.config.ready$.toPromise() await this.config.selectDefaultConfig() } @@ -80,9 +76,11 @@ export class MainComponent { async loadApp (config, version) { this.showApp = true this.iframe.nativeElement.src = '/terminal' - await this.http.patch(`/api/1/configs/${config.id}`, { - last_used_with_version: version.version, - }).toPromise() + if (this.loginService.user) { + await this.http.patch(`/api/1/configs/${config.id}`, { + last_used_with_version: version.version, + }).toPromise() + } } reloadApp (config: Config, version: Version) { diff --git a/frontend/src/app/index.ts b/frontend/src/app/index.ts index 2f58d07..da7c9a5 100644 --- a/frontend/src/app/index.ts +++ b/frontend/src/app/index.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core' -import { NgbDropdownModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap' +import { NgbDropdownModule, NgbModalModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' import { CommonModule } from '@angular/common' import { FormsModule } from '@angular/forms' import { RouterModule } from '@angular/router' @@ -31,6 +31,7 @@ const ROUTES = [ FormsModule, NgbDropdownModule, NgbModalModule, + NgbTooltipModule, ClipboardModule, FontAwesomeModule, RouterModule.forChild(ROUTES), diff --git a/frontend/src/app/services/appConnector.service.ts b/frontend/src/app/services/appConnector.service.ts index 5380a71..4849965 100644 --- a/frontend/src/app/services/appConnector.service.ts +++ b/frontend/src/app/services/appConnector.service.ts @@ -39,7 +39,7 @@ export class SocketProxy { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async connect (options: any): Promise { - if (!this.loginService.user.is_pro && this.appConnector.sockets.length > this.appConnector.connectionLimit && !window.sessionStorage['upgrade-skip-active']) { + if (!this.loginService.user?.is_pro && this.appConnector.sockets.length > this.appConnector.connectionLimit && !window.sessionStorage['upgrade-skip-active']) { let skipped = false try { skipped = await this.zone.run(() => this.ngbModal.open(UpgradeModalComponent)).result @@ -51,8 +51,8 @@ export class SocketProxy { } this.options = options - this.url = this.loginService.user.custom_connection_gateway - this.authToken = this.loginService.user.custom_connection_gateway_token + this.url = this.loginService.user?.custom_connection_gateway + this.authToken = this.loginService.user?.custom_connection_gateway_token if (!this.url) { try { const gateway = await this.appConnector.chooseConnectionGateway() @@ -151,11 +151,14 @@ export class AppConnectorService { private http: HttpClient, private commonService: CommonService, private zone: NgZone, + private loginService: LoginService, ) { this.configUpdate.pipe(debounceTime(1000)).subscribe(async content => { - const result = await this.http.patch(`/api/1/configs/${this.config.id}`, { content }).toPromise() - Object.assign(this.config, result) + if (this.loginService.user) { + const result = await this.http.patch(`/api/1/configs/${this.config.id}`, { content }).toPromise() + Object.assign(this.config, result) + } }) } diff --git a/frontend/src/common/services/config.service.ts b/frontend/src/common/services/config.service.ts index 06c639c..d34535a 100644 --- a/frontend/src/common/services/config.service.ts +++ b/frontend/src/common/services/config.service.ts @@ -30,14 +30,29 @@ export class ConfigService { } async updateUser () { + if (!this.loginService.user) { + return + } await this.http.put('/api/1/user', this.user).toPromise() } async createNewConfig (): Promise { - const config = await this.http.post('/api/1/configs', { + const configData = { content: '{}', last_used_with_version: this._activeVersion?.version ?? this.getLatestStableVersion().version, - }).toPromise() + } + if (!this.loginService.user) { + const config = { + id: Date.now(), + name: `Temporary config at ${new Date()}`, + created_at: new Date(), + modified_at: new Date(), + ...configData, + } + this.configs.push(config) + return config + } + const config = await this.http.post('/api/1/configs', configData).toPromise() this.configs.push(config) return config } @@ -47,8 +62,11 @@ export class ConfigService { } async duplicateActiveConfig () { - const copy = {...this._activeConfig, pk: undefined} - this.configs.push(await this.http.post('/api/1/configs', copy).toPromise()) + let copy = {...this._activeConfig, pk: undefined, id: undefined} + if (this.loginService.user) { + copy = await this.http.post('/api/1/configs', copy).toPromise() + } + this.configs.push(copy) } async selectVersion (version: Version) { @@ -66,23 +84,29 @@ export class ConfigService { this._activeConfig = config this.activeConfig$.next(config) this.selectVersion(matchingVersion) - this.loginService.user.active_config = config.id - await this.loginService.updateUser() + if (this.loginService.user) { + this.loginService.user.active_config = config.id + await this.loginService.updateUser() + } } async selectDefaultConfig () { await this.ready$.toPromise() await this.loginService.ready$.toPromise() - this.selectConfig(this.configs.find(c => c.id === this.loginService.user.active_config) ?? this.configs[0]) + this.selectConfig(this.configs.find(c => c.id === this.loginService.user?.active_config) ?? this.configs[0]) } async deleteConfig (config: Config) { - await this.http.delete(`/api/1/configs/${config.id}`).toPromise() + if (this.loginService.user) { + await this.http.delete(`/api/1/configs/${config.id}`).toPromise() + } this.configs = this.configs.filter(x => x.id !== config.id) } private async init () { - this.configs = await this.http.get('/api/1/configs').toPromise() + if (this.loginService.user) { + this.configs = await this.http.get('/api/1/configs').toPromise() + } this.versions = await this.http.get('/api/1/versions').toPromise() this.versions.sort((a, b) => -semverCompare(a.version, b.version)) diff --git a/frontend/src/common/services/login.service.ts b/frontend/src/common/services/login.service.ts index 187d2e3..41c15a3 100644 --- a/frontend/src/common/services/login.service.ts +++ b/frontend/src/common/services/login.service.ts @@ -6,7 +6,7 @@ import { User } from '../../api' @Injectable({ providedIn: 'root' }) export class LoginService { - user: User + user: User | null ready$ = new AsyncSubject() constructor (private http: HttpClient) { @@ -14,6 +14,9 @@ export class LoginService { } async updateUser () { + if (!this.user) { + return + } await this.http.put('/api/1/user', this.user).toPromise() } diff --git a/frontend/theme/index.scss b/frontend/theme/index.scss index d3211cb..d28d3db 100644 --- a/frontend/theme/index.scss +++ b/frontend/theme/index.scss @@ -88,3 +88,7 @@ a, button { lib-ngx-image-zoom { display: flex; } + +ngb-tooltip-window { + z-index: 1; +}