This commit is contained in:
Eugene Pankov
2021-10-24 21:50:55 +02:00
parent c61c816e32
commit eae970f969
50 changed files with 1521 additions and 1385 deletions

12
.bumpversion.cfg Normal file
View File

@@ -0,0 +1,12 @@
[bumpversion]
current_version = 1.0.0
commit = True
tag = True
[bumpversion:file:frontend/package.json]
search = "version": "{current_version}"
replace = "version": "{new_version}"
[bumpversion:file:backend/pyproject.toml]
search = version = "{current_version}"
replace = version = "{new_version}"

View File

@@ -11,3 +11,6 @@ insert_final_newline = true
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.ts]
indent_size = 2

36
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Lint
on: [push, pull_request]
jobs:
Lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
with:
fetch-depth: 0
- name: Installing Node
uses: actions/setup-node@v2.4.0
with:
node-version: 14
- name: Install frontend deps
working-directory: frontend
run: |
npm i -g yarn@1.19.1
yarn
- name: Lint frontend
working-directory: frontend
run: yarn lint
- name: Install backend deps
working-directory: backend
run: |
pip3 install poetry
poetry install
- name: Lint backend
working-directory: frontend
run: flake8 .

7
backend/.flake8 Normal file
View File

@@ -0,0 +1,7 @@
[flake8]
ignore=E501,D103,C901,D203,W504,S607,S603,S404,S606,S322,S410,S320,B010
exclude = .git,__pycache__,help,static,misc,locale,templates,tests,deployment,migrations,elements/ai/scripts
max-complexity = 40
builtins = _
per-file-ignores = scripts/*:T001,E402
select = C,E,F,W,B,B902

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "tabby-web" name = "tabby-web"
version = "0.1.0" version = "1.0.0"
description = "" description = ""
authors = ["Your Name <you@example.com>"] authors = ["Your Name <you@example.com>"]

View File

@@ -1,214 +0,0 @@
import fsspec
import os
import asyncio
import random
from django.conf import settings
from django.contrib.auth import logout
from dataclasses import dataclass
from pathlib import Path
from rest_framework import fields, status
from rest_framework.exceptions import APIException, PermissionDenied, NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework_dataclasses.serializers import DataclassSerializer
from social_django.models import UserSocialAuth
from typing import List
from urllib.parse import urlparse
from .gateway import GatewayAdminConnection
from .sponsors import check_is_sponsor, check_is_sponsor_cached
from .models import Config, Gateway, User
@dataclass
class AppVersion:
version: str
plugins: List[str]
class AppVersionSerializer(DataclassSerializer):
class Meta:
dataclass = AppVersion
class GatewaySerializer(ModelSerializer):
url = fields.SerializerMethodField()
auth_token = fields.CharField()
class Meta:
fields = '__all__'
model = Gateway
def get_url(self, gw):
return f'{"wss" if gw.secure else "ws"}://{gw.host}:{gw.port}/'
class ConfigSerializer(ModelSerializer):
name = fields.CharField(required=False)
class Meta:
model = Config
read_only_fields = ('user', 'created_at', 'modified_at')
fields = '__all__'
class ConfigViewSet(ModelViewSet):
queryset = Config.objects.all()
serializer_class = ConfigSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
if self.request.user.is_authenticated:
return Config.objects.filter(user=self.request.user)
return Config.objects.none()
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class AppVersionViewSet(ListModelMixin, GenericViewSet):
serializer_class = AppVersionSerializer
lookup_field = 'id'
lookup_value_regex = r'[\w\d.-]+'
queryset = ''
def _get_versions(self):
fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme)
return [
self._get_version(x['name'])
for x in fs.listdir(settings.APP_DIST_STORAGE)
if x['type'] == 'directory'
]
def _get_version(self, dir):
fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme)
plugins = [
os.path.basename(x['name'])
for x in fs.listdir(dir)
if x['type'] == 'directory' and os.path.basename(x['name'])
not in [
'tabby-web-container',
'tabby-web-demo',
]
]
return AppVersion(
version=os.path.basename(dir),
plugins=plugins,
)
def list(self, request, *args, **kwargs):
return Response(
self.serializer_class(
self._get_versions(),
many=True,
).data
)
class UserSerializer(ModelSerializer):
id = fields.IntegerField()
is_pro = fields.SerializerMethodField()
is_sponsor = fields.SerializerMethodField()
github_username = fields.SerializerMethodField()
class Meta:
model = User
fields = (
'id',
'username',
'active_config',
'custom_connection_gateway',
'custom_connection_gateway_token',
'config_sync_token',
'is_pro',
'is_sponsor',
'github_username',
)
read_only_fields = ('id', 'username')
def get_is_pro(self, obj):
return obj.force_pro or not settings.GITHUB_ELIGIBLE_SPONSORSHIPS or check_is_sponsor_cached(obj)
def get_is_sponsor(self, obj):
return check_is_sponsor_cached(obj)
def get_github_username(self, obj):
social_auth = UserSocialAuth.objects.filter(user=obj, provider='github').first()
if not social_auth:
return None
return social_auth.extra_data.get('login')
class UserViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def get_object(self):
if self.request.user.is_authenticated:
return self.request.user
raise PermissionDenied()
class LogoutView(APIView):
def post(self, request, format=None):
logout(request)
return Response(None)
class InstanceInfoSerializer(Serializer):
login_enabled = fields.BooleanField()
homepage_enabled = fields.BooleanField()
class InstanceInfoViewSet(RetrieveModelMixin, GenericViewSet):
queryset = '' # type: ignore
serializer_class = InstanceInfoSerializer
def get_object(self):
return {
'login_enabled': settings.ENABLE_LOGIN,
'homepage_enabled': settings.ENABLE_HOMEPAGE,
}
class NoGatewaysError(APIException):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
default_detail = 'No connection gateways available.'
default_code = 'no_gateways'
class ChooseGatewayViewSet(RetrieveModelMixin, GenericViewSet):
queryset = Gateway.objects.filter(enabled=True)
serializer_class = GatewaySerializer
async def _authorize_client(self, gw):
c = GatewayAdminConnection(gw)
await c.connect()
token = await c.authorize_client()
await c.close()
return token
def get_object(self):
gateways = list(self.queryset)
random.shuffle(gateways)
if not len(gateways):
raise NotFound()
loop = asyncio.new_event_loop()
try:
for gw in gateways:
try:
gw.auth_token = loop.run_until_complete(self._authorize_client(gw))
except ConnectionError:
continue
return gw
raise NoGatewaysError()
finally:
loop.close()

View File

@@ -0,0 +1,18 @@
from django.urls import path, include
from rest_framework import routers
from . import app_version, auth, config, gateway, info, user
router = routers.DefaultRouter(trailing_slash=False)
router.register('api/1/configs', config.ConfigViewSet)
router.register('api/1/versions', app_version.AppVersionViewSet, basename='app-versions')
urlpatterns = [
path('api/1/auth/logout', auth.LogoutView.as_view()),
path('api/1/user', user.UserViewSet.as_view({'get': 'retrieve', 'put': 'update'})),
path('api/1/instance-info', info.InstanceInfoViewSet.as_view({'get': 'retrieve'})),
path('api/1/gateways/choose', gateway.ChooseGatewayViewSet.as_view({'post': 'retrieve'})),
path('', include(router.urls)),
]

View File

@@ -0,0 +1,64 @@
import fsspec
import os
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from dataclasses import dataclass
from rest_framework.response import Response
from rest_framework.mixins import ListModelMixin
from rest_framework.viewsets import GenericViewSet
from rest_framework_dataclasses.serializers import DataclassSerializer
from typing import List
from urllib.parse import urlparse
@dataclass
class AppVersion:
version: str
plugins: List[str]
class AppVersionSerializer(DataclassSerializer):
class Meta:
dataclass = AppVersion
class AppVersionViewSet(ListModelMixin, GenericViewSet):
serializer_class = AppVersionSerializer
lookup_field = 'id'
lookup_value_regex = r'[\w\d.-]+'
queryset = ''
def _get_versions(self):
fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme)
return [
self._get_version(x['name'])
for x in fs.listdir(settings.APP_DIST_STORAGE)
if x['type'] == 'directory'
]
def _get_version(self, dir):
fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme)
plugins = [
os.path.basename(x['name'])
for x in fs.listdir(dir)
if x['type'] == 'directory' and os.path.basename(x['name'])
not in [
'tabby-web-container',
'tabby-web-demo',
]
]
return AppVersion(
version=os.path.basename(dir),
plugins=plugins,
)
@method_decorator(cache_page(60))
def list(self, request, *args, **kwargs):
return Response(
self.serializer_class(
self._get_versions(),
many=True,
).data
)

View File

@@ -0,0 +1,9 @@
from django.contrib.auth import logout
from rest_framework.response import Response
from rest_framework.views import APIView
class LogoutView(APIView):
def post(self, request, format=None):
logout(request)
return Response(None)

View File

@@ -0,0 +1,28 @@
from rest_framework import fields
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from rest_framework.serializers import ModelSerializer
from ..models import Config
class ConfigSerializer(ModelSerializer):
name = fields.CharField(required=False)
class Meta:
model = Config
read_only_fields = ('user', 'created_at', 'modified_at')
fields = '__all__'
class ConfigViewSet(ModelViewSet):
queryset = Config.objects.all()
serializer_class = ConfigSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
if self.request.user.is_authenticated:
return Config.objects.filter(user=self.request.user)
return Config.objects.none()
def perform_create(self, serializer):
serializer.save(user=self.request.user)

View File

@@ -0,0 +1,58 @@
import asyncio
import random
from rest_framework import fields, status
from rest_framework.exceptions import APIException, NotFound
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.viewsets import GenericViewSet
from rest_framework.serializers import ModelSerializer
from ..gateway import GatewayAdminConnection
from ..models import Gateway
class GatewaySerializer(ModelSerializer):
url = fields.SerializerMethodField()
auth_token = fields.CharField()
class Meta:
fields = '__all__'
model = Gateway
def get_url(self, gw):
return f'{"wss" if gw.secure else "ws"}://{gw.host}:{gw.port}/'
class NoGatewaysError(APIException):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
default_detail = 'No connection gateways available.'
default_code = 'no_gateways'
class ChooseGatewayViewSet(RetrieveModelMixin, GenericViewSet):
queryset = Gateway.objects.filter(enabled=True)
serializer_class = GatewaySerializer
async def _authorize_client(self, gw):
c = GatewayAdminConnection(gw)
await c.connect()
token = await c.authorize_client()
await c.close()
return token
def get_object(self):
gateways = list(self.queryset)
random.shuffle(gateways)
if not len(gateways):
raise NotFound()
loop = asyncio.new_event_loop()
try:
for gw in gateways:
try:
gw.auth_token = loop.run_until_complete(self._authorize_client(gw))
except ConnectionError:
continue
return gw
raise NoGatewaysError()
finally:
loop.close()

View File

@@ -0,0 +1,21 @@
from django.conf import settings
from rest_framework import fields
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.viewsets import GenericViewSet
from rest_framework.serializers import Serializer
class InstanceInfoSerializer(Serializer):
login_enabled = fields.BooleanField()
homepage_enabled = fields.BooleanField()
class InstanceInfoViewSet(RetrieveModelMixin, GenericViewSet):
queryset = '' # type: ignore
serializer_class = InstanceInfoSerializer
def get_object(self):
return {
'login_enabled': settings.ENABLE_LOGIN,
'homepage_enabled': settings.ENABLE_HOMEPAGE,
}

View File

@@ -0,0 +1,55 @@
from django.conf import settings
from rest_framework import fields
from rest_framework.exceptions import PermissionDenied
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin
from rest_framework.viewsets import GenericViewSet
from rest_framework.serializers import ModelSerializer
from social_django.models import UserSocialAuth
from ..sponsors import check_is_sponsor_cached
from ..models import User
class UserSerializer(ModelSerializer):
id = fields.IntegerField()
is_pro = fields.SerializerMethodField()
is_sponsor = fields.SerializerMethodField()
github_username = fields.SerializerMethodField()
class Meta:
model = User
fields = (
'id',
'username',
'active_config',
'custom_connection_gateway',
'custom_connection_gateway_token',
'config_sync_token',
'is_pro',
'is_sponsor',
'github_username',
)
read_only_fields = ('id', 'username')
def get_is_pro(self, obj):
return obj.force_pro or not settings.GITHUB_ELIGIBLE_SPONSORSHIPS or check_is_sponsor_cached(obj)
def get_is_sponsor(self, obj):
return check_is_sponsor_cached(obj)
def get_github_username(self, obj):
social_auth = UserSocialAuth.objects.filter(user=obj, provider='github').first()
if not social_auth:
return None
return social_auth.extra_data.get('login')
class UserViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def get_object(self):
if self.request.user.is_authenticated:
return self.request.user
raise PermissionDenied()

View File

@@ -39,7 +39,7 @@ def check_is_sponsor(user: User) -> bool:
query = ''' query = '''
query { query {
user (login: "eugeny") { viewer {
sponsorshipsAsSponsor(%s) { sponsorshipsAsSponsor(%s) {
pageInfo { pageInfo {
startCursor startCursor

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,23 +1,17 @@
from django.urls import path, re_path, include from django.urls import path, include
from rest_framework import routers
from . import api from . import api
from . import views from . import views
router = routers.DefaultRouter(trailing_slash=False)
router.register('api/1/configs', api.ConfigViewSet)
router.register('api/1/versions', api.AppVersionViewSet, basename='app-versions')
urlpatterns = [ urlpatterns = [
path('api/1/auth/logout', api.LogoutView.as_view()), *[
path('api/1/user', api.UserViewSet.as_view({'get': 'retrieve', 'put': 'update'})), path(p, views.IndexView.as_view())
path('api/1/instance-info', api.InstanceInfoViewSet.as_view({'get': 'retrieve'})), for p in ['', 'login', 'app', 'about', 'features']
path('api/1/gateways/choose', api.ChooseGatewayViewSet.as_view({'post': 'retrieve'})), ],
re_path('^(|login|app|about|features)$', views.IndexView.as_view()),
path('terminal', views.TerminalView.as_view()),
path('app-dist/<version>/<path:path>', views.AppDistView.as_view()), path('app-dist/<version>/<path:path>', views.AppDistView.as_view()),
path('', include(router.urls)), path('terminal', views.TerminalView.as_view()),
path('', include(api.urlpatterns)),
] ]

View File

@@ -4,8 +4,8 @@ import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tabby.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tabby.settings')
django.setup() django.setup()
from channels.routing import ProtocolTypeRouter from channels.routing import ProtocolTypeRouter # noqa
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application # noqa
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
'http': get_asgi_application(), 'http': get_asgi_application(),

View File

@@ -1,7 +1,7 @@
import logging import logging
from tabby.app.models import User from tabby.app.models import User
from django.conf import settings from django.conf import settings
from django.contrib.auth import hashers, logout, login from django.contrib.auth import login
from pyga.requests import Tracker, Page, Session, Visitor from pyga.requests import Tracker, Page, Session, Visitor

View File

@@ -2,7 +2,6 @@ parser: '@typescript-eslint/parser'
parserOptions: parserOptions:
project: project:
- tsconfig.json - tsconfig.json
- '*/tsconfig.typings.json'
extends: extends:
- 'plugin:@typescript-eslint/all' - 'plugin:@typescript-eslint/all'
plugins: plugins:
@@ -18,7 +17,7 @@ rules:
- never - never
'@typescript-eslint/indent': '@typescript-eslint/indent':
- error - error
- 4 - 2
'@typescript-eslint/explicit-member-accessibility': '@typescript-eslint/explicit-member-accessibility':
- error - error
- accessibility: no-public - accessibility: no-public
@@ -121,3 +120,10 @@ rules:
'@typescript-eslint/no-unsafe-argument': off '@typescript-eslint/no-unsafe-argument': off
'@typescript-eslint/restrict-plus-operands': off '@typescript-eslint/restrict-plus-operands': off
'@typescript-eslint/space-infix-ops': off '@typescript-eslint/space-infix-ops': off
'@typescript-eslint/explicit-module-boundary-types': off
overrides:
- files: '*.service.ts'
rules:
'@typescript-eslint/explicit-module-boundary-types':
- error

View File

@@ -3,6 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"lint": "eslint src",
"build": "webpack --progress", "build": "webpack --progress",
"watch": "DEV=1 webpack --progress --watch", "watch": "DEV=1 webpack --progress --watch",
"build:server": "webpack --progress -c webpack.config.server.js", "build:server": "webpack --progress -c webpack.config.server.js",
@@ -33,8 +34,8 @@
"@nguniversal/express-engine": "^11.1.0", "@nguniversal/express-engine": "^11.1.0",
"@tabby-gang/to-string-loader": "^1.1.7-beta.1", "@tabby-gang/to-string-loader": "^1.1.7-beta.1",
"@types/node": "^11.9.5", "@types/node": "^11.9.5",
"@typescript-eslint/eslint-plugin": "^4.28.4", "@typescript-eslint/eslint-plugin": "^5.1.0",
"@typescript-eslint/parser": "^4.28.4", "@typescript-eslint/parser": "^5.1.0",
"apply-loader": "^2.0.0", "apply-loader": "^2.0.0",
"bootstrap": "^5.0.1", "bootstrap": "^5.0.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
@@ -66,6 +67,7 @@
"source-map-support": "^0.5.19", "source-map-support": "^0.5.19",
"source-sans-pro": "^2.45.0", "source-sans-pro": "^2.45.0",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"three": "^0.119.0",
"throng": "^5.0.0", "throng": "^5.0.0",
"typescript": "~4.1", "typescript": "~4.1",
"val-loader": "^4.0.0", "val-loader": "^4.0.0",
@@ -73,7 +75,6 @@
"webpack": "^5.38.1", "webpack": "^5.38.1",
"webpack-bundle-analyzer": "^4.4.2", "webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.2", "webpack-cli": "^4.7.2",
"three": "^0.119.0",
"zone.js": "^0.11.4" "zone.js": "^0.11.4"
} }
} }

View File

@@ -4,47 +4,47 @@ import { Resolve } from '@angular/router'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
export interface User { export interface User {
id: number id: number
active_config: number active_config: number
active_version: string active_version: string
custom_connection_gateway: string|null custom_connection_gateway: string|null
custom_connection_gateway_token: string|null custom_connection_gateway_token: string|null
config_sync_token: string config_sync_token: string
github_username: string github_username: string
is_pro: boolean is_pro: boolean
is_sponsor: boolean is_sponsor: boolean
} }
export interface Config { export interface Config {
id: number id: number
content: string content: string
last_used_with_version: string last_used_with_version: string
created_at: Date created_at: Date
modified_at: Date modified_at: Date
} }
export interface Version { export interface Version {
version: string version: string
plugins: string[] plugins: string[]
} }
export interface InstanceInfo { export interface InstanceInfo {
login_enabled: boolean login_enabled: boolean
homepage_enabled: boolean homepage_enabled: boolean
} }
export interface Gateway { export interface Gateway {
host: string host: string
port: number port: number
url: string url: string
auth_token: string auth_token: string
} }
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class InstanceInfoResolver implements Resolve<Observable<InstanceInfo>> { export class InstanceInfoResolver implements Resolve<Observable<InstanceInfo>> {
constructor (private http: HttpClient) { } constructor (private http: HttpClient) { }
resolve(): Observable<InstanceInfo> { resolve (): Observable<InstanceInfo> {
return this.http.get('/api/1/instance-info').toPromise() return this.http.get('/api/1/instance-info').toPromise()
} }
} }

View File

@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import { Component } from '@angular/core' import { Component } from '@angular/core'
@Component({ @Component({
selector: 'app', selector: 'app',
template: '<router-outlet></router-outlet>', template: '<router-outlet></router-outlet>',
}) })
export class AppComponent { } export class AppComponent { }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
@@ -15,38 +16,38 @@ import { CommonAppModule } from 'src/common'
import '@fortawesome/fontawesome-svg-core/styles.css' import '@fortawesome/fontawesome-svg-core/styles.css'
const ROUTES = [ const ROUTES = [
{ {
path: '', path: '',
loadChildren: () => import(/* webpackChunkName: "homepage" */'./homepage').then(m => m.HomepageModule), loadChildren: () => import(/* webpackChunkName: "homepage" */'./homepage').then(m => m.HomepageModule),
}, },
{ {
path: 'app', path: 'app',
loadChildren: () => import(/* webpackChunkName: "app" */'./app').then(m => m.ApplicationModule), loadChildren: () => import(/* webpackChunkName: "app" */'./app').then(m => m.ApplicationModule),
}, },
{ {
path: 'login', path: 'login',
loadChildren: () => import(/* webpackChunkName: "login" */'./login').then(m => m.LoginModule), loadChildren: () => import(/* webpackChunkName: "login" */'./login').then(m => m.LoginModule),
}, },
] ]
@NgModule({ @NgModule({
imports: [ imports: [
BrowserModule.withServerTransition({ BrowserModule.withServerTransition({
appId: 'tabby', appId: 'tabby',
}), }),
CommonAppModule.forRoot(), CommonAppModule.forRoot(),
TransferHttpCacheModule, TransferHttpCacheModule,
BrowserAnimationsModule, BrowserAnimationsModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
FontAwesomeModule, FontAwesomeModule,
ClipboardModule, ClipboardModule,
HttpClientModule, HttpClientModule,
RouterModule.forRoot(ROUTES), RouterModule.forRoot(ROUTES),
], ],
declarations: [ declarations: [
AppComponent, AppComponent,
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule { } export class AppModule { }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server' import { ServerModule, ServerTransferStateModule } from '@angular/platform-server'
import { AppModule } from './app.module' import { AppModule } from './app.module'

View File

@@ -6,51 +6,51 @@ import { faCopy, faFile, faPlus, faTrash } from '@fortawesome/free-solid-svg-ico
import { Config, Version } from 'src/api' import { Config, Version } from 'src/api'
@Component({ @Component({
selector: 'config-modal', selector: 'config-modal',
templateUrl: './configModal.component.pug', templateUrl: './configModal.component.pug',
// styleUrls: ['./settingsModal.component.scss'], // styleUrls: ['./settingsModal.component.scss'],
}) })
export class ConfigModalComponent { export class ConfigModalComponent {
_addIcon = faPlus _addIcon = faPlus
_copyIcon = faCopy _copyIcon = faCopy
_deleteIcon = faTrash _deleteIcon = faTrash
_configIcon = faFile _configIcon = faFile
constructor ( constructor (
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
public appConnector: AppConnectorService, public appConnector: AppConnectorService,
public configService: ConfigService, public configService: ConfigService,
) { ) {
} }
async ngOnInit () { cancel () {
} this.modalInstance.dismiss()
}
cancel () { async createNewConfig () {
this.modalInstance.dismiss() const config = await this.configService.createNewConfig()
} await this.configService.selectConfig(config)
this.modalInstance.dismiss()
}
async createNewConfig () { async selectConfig (config: Config) {
const config = await this.configService.createNewConfig() await this.configService.selectConfig(config)
await this.configService.selectConfig(config) this.modalInstance.dismiss()
this.modalInstance.dismiss() }
}
async selectConfig (config: Config) { async selectVersion (version: Version) {
await this.configService.selectConfig(config) await this.configService.selectVersion(version)
this.modalInstance.dismiss() this.modalInstance.dismiss()
} }
async selectVersion (version: Version) { async deleteConfig () {
await this.configService.selectVersion(version) if (!this.configService.activeConfig) {
this.modalInstance.dismiss() return
} }
if (confirm('Delete this config? This cannot be undone.')) {
async deleteConfig () { await this.configService.deleteConfig(this.configService.activeConfig)
if (confirm('Delete this config? This cannot be undone.')) {
await this.configService.deleteConfig(this.configService.activeConfig)
}
this.configService.selectDefaultConfig()
this.modalInstance.dismiss()
} }
this.configService.selectDefaultConfig()
this.modalInstance.dismiss()
}
} }

View File

@@ -3,18 +3,18 @@ import { AppConnectorService, SocketProxy } from '../services/appConnector.servi
import { faCircle, faTimes } from '@fortawesome/free-solid-svg-icons' import { faCircle, faTimes } from '@fortawesome/free-solid-svg-icons'
@Component({ @Component({
selector: 'connection-list', selector: 'connection-list',
templateUrl: './connectionList.component.pug', templateUrl: './connectionList.component.pug',
}) })
export class ConnectionListComponent { export class ConnectionListComponent {
_circleIcon = faCircle _circleIcon = faCircle
_closeIcon = faTimes _closeIcon = faTimes
constructor ( constructor (
public appConnector: AppConnectorService, public appConnector: AppConnectorService,
) { } ) { }
closeSocket (socket: SocketProxy) { closeSocket (socket: SocketProxy) {
socket.close(new Error('Connection closed by user')) socket.close(new Error('Connection closed by user'))
} }
} }

View File

@@ -12,96 +12,96 @@ import { combineLatest } from 'rxjs'
import { Config, Version } from 'src/api' import { Config, Version } from 'src/api'
@Component({ @Component({
selector: 'main', selector: 'main',
templateUrl: './main.component.pug', templateUrl: './main.component.pug',
styleUrls: ['./main.component.scss'], styleUrls: ['./main.component.scss'],
}) })
export class MainComponent { export class MainComponent {
_logo = require('../../../assets/logo.svg') _logo = require('../../../assets/logo.svg')
_settingsIcon = faCog _settingsIcon = faCog
_loginIcon = faSignInAlt _loginIcon = faSignInAlt
_logoutIcon = faSignOutAlt _logoutIcon = faSignOutAlt
_addIcon = faPlus _addIcon = faPlus
_configIcon = faFile _configIcon = faFile
_saveIcon = faSave _saveIcon = faSave
showApp = false showApp = false
@ViewChild('iframe') iframe: ElementRef @ViewChild('iframe') iframe: ElementRef
constructor ( constructor (
titleService: Title, titleService: Title,
public appConnector: AppConnectorService, public appConnector: AppConnectorService,
private http: HttpClient, private http: HttpClient,
public loginService: LoginService, public loginService: LoginService,
private ngbModal: NgbModal, private ngbModal: NgbModal,
private config: ConfigService, private config: ConfigService,
) { ) {
titleService.setTitle('Tabby') titleService.setTitle('Tabby')
window.addEventListener('message', this.connectorRequestHandler) 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', '*')
} }
}
connectorRequestHandler = event => { async ngAfterViewInit () {
if (event.data === 'request-connector') { await this.loginService.ready$.toPromise()
this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector
this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') combineLatest(
} this.config.activeConfig$,
this.config.activeVersion$
).subscribe(([config, version]) => {
if (config && version) {
this.reloadApp(config, version)
}
})
await this.config.ready$.toPromise()
await this.config.selectDefaultConfig()
}
ngOnDestroy () {
window.removeEventListener('message', this.connectorRequestHandler)
}
unloadApp () {
this.showApp = false
this.iframe.nativeElement.src = 'about:blank'
}
async loadApp (config, version) {
this.showApp = true
this.iframe.nativeElement.src = '/terminal'
if (this.loginService.user) {
await this.http.patch(`/api/1/configs/${config.id}`, {
last_used_with_version: version.version,
}).toPromise()
} }
}
async ngAfterViewInit () { reloadApp (config: Config, version: Version) {
await this.loginService.ready$.toPromise() // TODO check config incompatibility
this.unloadApp()
setTimeout(() => {
this.appConnector.setState(config, version)
this.loadApp(config, version)
})
}
combineLatest( async openConfig () {
this.config.activeConfig$, await this.ngbModal.open(ConfigModalComponent).result
this.config.activeVersion$ }
).subscribe(([config, version]) => {
if (config && version) {
this.reloadApp(config, version)
}
})
await this.config.ready$.toPromise() async openSettings () {
await this.config.selectDefaultConfig() await this.ngbModal.open(SettingsModalComponent).result
} }
ngOnDestroy () { async logout () {
window.removeEventListener('message', this.connectorRequestHandler) await this.http.post('/api/1/auth/logout', null).toPromise()
} location.href = '/'
}
unloadApp () {
this.showApp = false
this.iframe.nativeElement.src = 'about:blank'
}
async loadApp (config, version) {
this.showApp = true
this.iframe.nativeElement.src = '/terminal'
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) {
// TODO check config incompatibility
this.unloadApp()
setTimeout(() => {
this.appConnector.setState(config, version)
this.loadApp(config, version)
})
}
async openConfig () {
await this.ngbModal.open(ConfigModalComponent).result
}
async openSettings () {
await this.ngbModal.open(SettingsModalComponent).result
}
async logout () {
await this.http.post('/api/1/auth/logout', null).toPromise()
location.href = '/'
}
} }

View File

@@ -7,36 +7,36 @@ import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons' import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'
@Component({ @Component({
selector: 'settings-modal', selector: 'settings-modal',
templateUrl: './settingsModal.component.pug', templateUrl: './settingsModal.component.pug',
}) })
export class SettingsModalComponent { export class SettingsModalComponent {
user: User user: User
customGatewayEnabled = false customGatewayEnabled = false
_githubIcon = faGithub _githubIcon = faGithub
_copyIcon = faCopy _copyIcon = faCopy
_okIcon = faCheck _okIcon = faCheck
constructor ( constructor (
public appConnector: AppConnectorService, public appConnector: AppConnectorService,
public commonService: CommonService, public commonService: CommonService,
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
private loginService: LoginService, private loginService: LoginService,
) { ) {
this.user = { ...loginService.user } if (!loginService.user) {
this.customGatewayEnabled = !!this.user.custom_connection_gateway return
} }
this.user = { ...loginService.user }
this.customGatewayEnabled = !!this.user.custom_connection_gateway
}
async ngOnInit () { async apply () {
} Object.assign(this.loginService.user, this.user)
this.modalInstance.close()
await this.loginService.updateUser()
}
async apply () { cancel () {
Object.assign(this.loginService.user, this.user) this.modalInstance.dismiss()
this.modalInstance.close() }
await this.loginService.updateUser()
}
cancel () {
this.modalInstance.dismiss()
}
} }

View File

@@ -8,28 +8,28 @@ import { CommonService, LoginService } from 'src/common'
import { User } from 'src/api' import { User } from 'src/api'
@Component({ @Component({
selector: 'upgrade-modal', selector: 'upgrade-modal',
templateUrl: './upgradeModal.component.pug', templateUrl: './upgradeModal.component.pug',
}) })
export class UpgradeModalComponent { export class UpgradeModalComponent {
user: User user: User
_githubIcon = faGithub _githubIcon = faGithub
_loveIcon = faHeart _loveIcon = faHeart
_giftIcon = faGift _giftIcon = faGift
canSkip = false canSkip = false
constructor ( constructor (
public appConnector: AppConnectorService, public appConnector: AppConnectorService,
public commonService: CommonService, public commonService: CommonService,
public loginService: LoginService, public loginService: LoginService,
private modalInstance: NgbActiveModal, private modalInstance: NgbActiveModal,
) { ) {
this.canSkip = !window.localStorage['upgrade-modal-skipped'] this.canSkip = !window.localStorage['upgrade-modal-skipped']
} }
skipOnce () { skipOnce () {
window.localStorage['upgrade-modal-skipped'] = true window.localStorage['upgrade-modal-skipped'] = true
window.sessionStorage['upgrade-skip-active'] = true window.sessionStorage['upgrade-skip-active'] = true
this.modalInstance.close(true) this.modalInstance.close(true)
} }
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { NgbDropdownModule, NgbModalModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule, NgbModalModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
@@ -15,33 +16,33 @@ import { InstanceInfoResolver } from 'src/api'
import { CommonAppModule } from 'src/common' import { CommonAppModule } from 'src/common'
const ROUTES = [ const ROUTES = [
{ {
path: '', path: '',
component: MainComponent, component: MainComponent,
resolve: { resolve: {
instanceInfo: InstanceInfoResolver, instanceInfo: InstanceInfoResolver,
},
}, },
},
] ]
@NgModule({ @NgModule({
imports: [ imports: [
CommonAppModule, CommonAppModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
NgbDropdownModule, NgbDropdownModule,
NgbModalModule, NgbModalModule,
NgbTooltipModule, NgbTooltipModule,
ClipboardModule, ClipboardModule,
FontAwesomeModule, FontAwesomeModule,
RouterModule.forChild(ROUTES), RouterModule.forChild(ROUTES),
], ],
declarations: [ declarations: [
MainComponent, MainComponent,
ConfigModalComponent, ConfigModalComponent,
SettingsModalComponent, SettingsModalComponent,
ConnectionListComponent, ConnectionListComponent,
UpgradeModalComponent, UpgradeModalComponent,
], ],
}) })
export class ApplicationModule { } export class ApplicationModule { }

View File

@@ -8,217 +8,226 @@ import { UpgradeModalComponent } from '../components/upgradeModal.component'
import { Config, Gateway, Version } from 'src/api' import { Config, Gateway, Version } from 'src/api'
import { LoginService, CommonService } from 'src/common' import { LoginService, CommonService } from 'src/common'
export interface ServiceMessage {
_: string
[k: string]: any
}
export class SocketProxy { export class SocketProxy {
connect$ = new Subject<void>() connect$ = new Subject<void>()
data$ = new Subject<Uint8Array>() data$ = new Subject<Uint8Array>()
error$ = new Subject<Error>() error$ = new Subject<Error>()
close$ = new Subject<void>() close$ = new Subject<void>()
url: string url: string
authToken: string authToken: string
webSocket: WebSocket|null webSocket: WebSocket|null
initialBuffers: any[] = [] initialBuffers: any[] = []
options: { options: {
host: string host: string
port: number port: number
}
private appConnector: AppConnectorService
private loginService: LoginService
private ngbModal: NgbModal
private zone: NgZone
constructor (
injector: Injector,
) {
this.appConnector = injector.get(AppConnectorService)
this.loginService = injector.get(LoginService)
this.ngbModal = injector.get(NgbModal)
this.zone = injector.get(NgZone)
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async connect (options: any): Promise<void> {
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
} catch { }
if (!skipped) {
this.close(new Error('Connection limit reached'))
return
}
} }
private appConnector: AppConnectorService this.options = options
private loginService: LoginService if (this.loginService.user?.custom_connection_gateway) {
private ngbModal: NgbModal this.url = this.loginService.user.custom_connection_gateway
private zone: NgZone
constructor (
injector: Injector,
) {
this.appConnector = injector.get(AppConnectorService)
this.loginService = injector.get(LoginService)
this.ngbModal = injector.get(NgbModal)
this.zone = injector.get(NgZone)
} }
if (this.loginService.user?.custom_connection_gateway_token) {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types this.authToken = this.loginService.user.custom_connection_gateway_token
async connect (options: any): Promise<void> {
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
} catch { }
if (!skipped) {
this.close(new Error('Connection limit reached'))
return
}
}
this.options = options
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()
this.url = gateway.url
this.authToken = gateway.auth_token
} catch (err) {
this.close(err)
return
}
}
try {
this.webSocket = new WebSocket(this.url)
} catch (err) {
this.close(err)
return
}
this.webSocket.onerror = err => {
this.close(new Error(`Failed to connect to the connection gateway at ${this.url}`))
return
}
this.webSocket.onmessage = async event => {
if (typeof(event.data) === 'string') {
this.handleServiceMessage(JSON.parse(event.data))
} else {
this.data$.next(Buffer.from(await event.data.arrayBuffer()))
}
}
this.webSocket.onclose = () => {
this.close()
}
} }
if (!this.url) {
handleServiceMessage (msg) { try {
if (msg._ === 'hello') { const gateway = await this.appConnector.chooseConnectionGateway()
this.sendServiceMessage({ this.url = gateway.url
_: 'hello', this.authToken = gateway.auth_token
version: 1, } catch (err) {
auth_token: this.authToken, this.close(err)
}) return
} else if (msg._ === 'ready') { }
this.sendServiceMessage({
_: 'connect',
host: this.options.host,
port: this.options.port,
})
} else if (msg._ === 'connected') {
this.connect$.next()
this.connect$.complete()
for (const b of this.initialBuffers) {
this.webSocket.send(b)
}
this.initialBuffers = []
} else if (msg._ === 'error') {
console.error('Connection gateway error', msg)
this.close(new Error(msg.details))
} else {
console.warn('Unknown service message', msg)
}
} }
try {
sendServiceMessage (msg) { this.webSocket = new WebSocket(this.url)
this.webSocket.send(JSON.stringify(msg)) } catch (err) {
this.close(err)
return
} }
this.webSocket.onerror = () => {
write (chunk: Buffer): void { this.close(new Error(`Failed to connect to the connection gateway at ${this.url}`))
if (!this.webSocket?.readyState) { return
this.initialBuffers.push(chunk)
} else {
this.webSocket.send(chunk)
}
} }
this.webSocket.onmessage = async event => {
close (error?: Error): void { if (typeof event.data === 'string') {
this.webSocket?.close() this.handleServiceMessage(JSON.parse(event.data))
if (error) { } else {
this.error$.next(error) this.data$.next(Buffer.from(await event.data.arrayBuffer()))
} }
this.connect$.complete()
this.data$.complete()
this.error$.complete()
this.close$.next()
this.close$.complete()
} }
this.webSocket.onclose = () => {
this.close()
}
}
handleServiceMessage (msg: ServiceMessage): void {
if (msg._ === 'hello') {
this.sendServiceMessage({
_: 'hello',
version: 1,
auth_token: this.authToken,
})
} else if (msg._ === 'ready') {
this.sendServiceMessage({
_: 'connect',
host: this.options.host,
port: this.options.port,
})
} else if (msg._ === 'connected') {
this.connect$.next()
this.connect$.complete()
for (const b of this.initialBuffers) {
this.webSocket?.send(b)
}
this.initialBuffers = []
} else if (msg._ === 'error') {
console.error('Connection gateway error', msg)
this.close(new Error(msg.details))
} else {
console.warn('Unknown service message', msg)
}
}
sendServiceMessage (msg: ServiceMessage): void {
this.webSocket?.send(JSON.stringify(msg))
}
write (chunk: Buffer): void {
if (!this.webSocket?.readyState) {
this.initialBuffers.push(chunk)
} else {
this.webSocket.send(chunk)
}
}
close (error?: Error): void {
this.webSocket?.close()
if (error) {
this.error$.next(error)
}
this.connect$.complete()
this.data$.complete()
this.error$.complete()
this.close$.next()
this.close$.complete()
}
} }
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AppConnectorService { export class AppConnectorService {
private configUpdate = new Subject<string>() private configUpdate = new Subject<string>()
private config: Config private config: Config
private version: Version private version: Version
connectionLimit = 3 connectionLimit = 3
sockets: SocketProxy[] = [] sockets: SocketProxy[] = []
constructor ( constructor (
private injector: Injector, private injector: Injector,
private http: HttpClient, private http: HttpClient,
private commonService: CommonService, private commonService: CommonService,
private zone: NgZone, private zone: NgZone,
private loginService: LoginService, private loginService: LoginService,
) { ) {
this.configUpdate.pipe(debounceTime(1000)).subscribe(async content => { this.configUpdate.pipe(debounceTime(1000)).subscribe(async content => {
if (this.loginService.user) { if (this.loginService.user) {
const result = await this.http.patch(`/api/1/configs/${this.config.id}`, { content }).toPromise() const result = await this.http.patch(`/api/1/configs/${this.config.id}`, { content }).toPromise()
Object.assign(this.config, result) Object.assign(this.config, result)
} }
}) })
} }
setState (config: Config, version: Version) { setState (config: Config, version: Version): void {
this.config = config this.config = config
this.version = version this.version = version
} }
async loadConfig (): Promise<string> { async loadConfig (): Promise<string> {
return this.config.content return this.config.content
} }
async saveConfig (content: string): Promise<void> { async saveConfig (content: string): Promise<void> {
this.configUpdate.next(content) this.configUpdate.next(content)
this.config.content = content this.config.content = content
} }
getAppVersion (): string { getAppVersion (): string {
return this.version.version return this.version.version
} }
getDistURL (): string { getDistURL (): string {
return this.commonService.backendURL + '/app-dist' return this.commonService.backendURL + '/app-dist'
} }
getPluginsToLoad (): string[] { getPluginsToLoad (): string[] {
const loadOrder = [ const loadOrder = [
'tabby-core', 'tabby-core',
'tabby-settings', 'tabby-settings',
'tabby-terminal', 'tabby-terminal',
'tabby-ssh', 'tabby-ssh',
'tabby-community-color-schemes', 'tabby-community-color-schemes',
'tabby-web', 'tabby-web',
] ]
return [ return [
...loadOrder.filter(x => this.version.plugins.includes(x)), ...loadOrder.filter(x => this.version.plugins.includes(x)),
...this.version.plugins.filter(x => !loadOrder.includes(x)), ...this.version.plugins.filter(x => !loadOrder.includes(x)),
] ]
} }
createSocket () { createSocket (): SocketProxy {
return this.zone.run(() => { return this.zone.run(() => {
const socket = new SocketProxy(this.injector) const socket = new SocketProxy(this.injector)
this.sockets.push(socket) this.sockets.push(socket)
socket.close$.subscribe(() => { socket.close$.subscribe(() => {
this.sockets = this.sockets.filter(x => x !== socket) this.sockets = this.sockets.filter(x => x !== socket)
}) })
return socket return socket
}) })
} }
async chooseConnectionGateway (): Promise<Gateway> { async chooseConnectionGateway (): Promise<Gateway> {
try { try {
return await this.http.post('/api/1/gateways/choose', {}).toPromise() return this.http.post('/api/1/gateways/choose', {}).toPromise()
} catch (err){ } catch (err){
if (err.status === 503) { if (err.status === 503) {
throw new Error('All connections gateway are unavailable right now') throw new Error('All connections gateway are unavailable right now')
} }
throw err throw err
}
} }
}
} }

View File

@@ -1,22 +1,23 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import { ModuleWithProviders, NgModule } from '@angular/core' import { ModuleWithProviders, NgModule } from '@angular/core'
import { HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http' import { HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http'
import { BackendXsrfInterceptor, UniversalInterceptor } from './interceptor' import { BackendXsrfInterceptor, UniversalInterceptor } from './interceptor'
@NgModule({ @NgModule({
imports: [ imports: [
HttpClientXsrfModule, HttpClientXsrfModule,
], ],
}) })
export class CommonAppModule { export class CommonAppModule {
static forRoot (): ModuleWithProviders<CommonAppModule> { static forRoot (): ModuleWithProviders<CommonAppModule> {
return { return {
ngModule: CommonAppModule, ngModule: CommonAppModule,
providers: [ providers: [
{ provide: HTTP_INTERCEPTORS, useClass: UniversalInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: UniversalInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: BackendXsrfInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: BackendXsrfInterceptor, multi: true },
] ],
}
} }
}
} }
export { LoginService } from './services/login.service' export { LoginService } from './services/login.service'

View File

@@ -5,34 +5,34 @@ import { CommonService } from './services/common.service'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class UniversalInterceptor implements HttpInterceptor { export class UniversalInterceptor implements HttpInterceptor {
constructor (private commonService: CommonService) { } constructor (private commonService: CommonService) { }
intercept (request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept (request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!request.url.startsWith('//') && request.url.startsWith('/')) { if (!request.url.startsWith('//') && request.url.startsWith('/')) {
const endpoint = request.url const endpoint = request.url
request = request.clone({ request = request.clone({
url: `${this.commonService.backendURL}${endpoint}`, url: `${this.commonService.backendURL}${endpoint}`,
withCredentials: true, withCredentials: true,
}) })
}
return next.handle(request)
} }
return next.handle(request)
}
} }
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class BackendXsrfInterceptor implements HttpInterceptor { export class BackendXsrfInterceptor implements HttpInterceptor {
constructor ( constructor (
private commonService: CommonService, private commonService: CommonService,
private tokenExtractor: HttpXsrfTokenExtractor, private tokenExtractor: HttpXsrfTokenExtractor,
) { } ) { }
intercept (req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept (req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.commonService.backendURL && req.url.startsWith(this.commonService.backendURL)) { if (this.commonService.backendURL && req.url.startsWith(this.commonService.backendURL)) {
let token = this.tokenExtractor.getToken() as string; const token = this.tokenExtractor.getToken()
if (token !== null) { if (token !== null) {
req = req.clone({ setHeaders: { 'X-XSRF-TOKEN': token } }); req = req.clone({ setHeaders: { 'X-XSRF-TOKEN': token } })
} }
}
return next.handle(req);
} }
return next.handle(req)
}
} }

View File

@@ -2,24 +2,24 @@ import { Inject, Injectable, Optional } from '@angular/core'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CommonService { export class CommonService {
backendURL: string backendURL: string
constructor (@Inject('BACKEND_URL') @Optional() ssrBackendURL: string) { constructor (@Inject('BACKEND_URL') @Optional() ssrBackendURL: string) {
const tag = (document.querySelector('meta[property=x-tabby-web-backend-url]') as HTMLMetaElement) const tag = document.querySelector('meta[property=x-tabby-web-backend-url]')! as HTMLMetaElement
if (ssrBackendURL) { if (ssrBackendURL) {
this.backendURL = ssrBackendURL this.backendURL = ssrBackendURL
tag.content = ssrBackendURL tag.content = ssrBackendURL
} else { } else {
if (tag.content && !tag.content.startsWith('{{')) { if (tag.content && !tag.content.startsWith('{{')) {
this.backendURL = tag.content this.backendURL = tag.content
} else { } else {
this.backendURL = '' this.backendURL = ''
} }
}
console.log(this.backendURL)
if (this.backendURL.endsWith('/')) {
this.backendURL = this.backendURL.slice(0, -1)
}
} }
console.log(this.backendURL)
if (this.backendURL.endsWith('/')) {
this.backendURL = this.backendURL.slice(0, -1)
}
}
} }

View File

@@ -8,113 +8,113 @@ import { LoginService } from './login.service'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ConfigService { export class ConfigService {
activeConfig$ = new Subject<Config>() activeConfig$ = new Subject<Config>()
activeVersion$ = new Subject<Version>() activeVersion$ = new Subject<Version>()
user: User user: User
configs: Config[] = [] configs: Config[] = []
versions: Version[] = [] versions: Version[] = []
ready$ = new AsyncSubject<void>() ready$ = new AsyncSubject<void>()
get activeConfig (): Config { return this._activeConfig } get activeConfig (): Config | null { return this._activeConfig }
get activeVersion (): Version { return this._activeVersion } get activeVersion (): Version | null { return this._activeVersion }
private _activeConfig: Config|null = null private _activeConfig: Config|null = null
private _activeVersion: Version|null = null private _activeVersion: Version|null = null
constructor ( constructor (
private http: HttpClient, private http: HttpClient,
private loginService: LoginService, private loginService: LoginService,
) { ) {
this.init() this.init()
}
async updateUser (): Promise<void> {
if (!this.loginService.user) {
return
}
await this.http.put('/api/1/user', this.user).toPromise()
}
async createNewConfig (): Promise<Config> {
const configData = {
content: '{}',
last_used_with_version: this._activeVersion?.version ?? this.getLatestStableVersion().version,
}
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
}
getLatestStableVersion (): Version {
return this.versions[0]
}
async duplicateActiveConfig (): Promise<void> {
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 as any)
}
async selectVersion (version: Version): Promise<void> {
this._activeVersion = version
this.activeVersion$.next(version)
}
async selectConfig (config: Config): Promise<void> {
let matchingVersion = this.versions.find(x => x.version === config.last_used_with_version)
if (!matchingVersion) {
// TODO ask to upgrade
matchingVersion = this.versions[0]
} }
async updateUser () { this._activeConfig = config
if (!this.loginService.user) { this.activeConfig$.next(config)
return this.selectVersion(matchingVersion)
} if (this.loginService.user) {
await this.http.put('/api/1/user', this.user).toPromise() this.loginService.user.active_config = config.id
await this.loginService.updateUser()
}
}
async selectDefaultConfig (): Promise<void> {
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])
}
async deleteConfig (config: Config): Promise<void> {
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 () {
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))
if (!this.configs.length) {
await this.createNewConfig()
} }
async createNewConfig (): Promise<Config> { this.ready$.next()
const configData = { this.ready$.complete()
content: '{}', }
last_used_with_version: this._activeVersion?.version ?? this.getLatestStableVersion().version,
}
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
}
getLatestStableVersion () {
return this.versions[0]
}
async duplicateActiveConfig () {
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) {
this._activeVersion = version
this.activeVersion$.next(version)
}
async selectConfig (config: Config) {
let matchingVersion = this.versions.find(x => x.version === config.last_used_with_version)
if (!matchingVersion) {
// TODO ask to upgrade
matchingVersion = this.versions[0]
}
this._activeConfig = config
this.activeConfig$.next(config)
this.selectVersion(matchingVersion)
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])
}
async deleteConfig (config: Config) {
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 () {
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))
if (!this.configs.length) {
await this.createNewConfig()
}
this.ready$.next()
this.ready$.complete()
}
} }

View File

@@ -6,28 +6,28 @@ import { User } from '../../api'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class LoginService { export class LoginService {
user: User | null user: User | null
ready$ = new AsyncSubject<void>() ready$ = new AsyncSubject<void>()
constructor (private http: HttpClient) { constructor (private http: HttpClient) {
this.init() this.init()
}
async updateUser (): Promise<void> {
if (!this.user) {
return
}
await this.http.put('/api/1/user', this.user).toPromise()
}
private async init () {
try {
this.user = await this.http.get('/api/1/user').toPromise()
} catch {
this.user = null
} }
async updateUser () { this.ready$.next()
if (!this.user) { this.ready$.complete()
return }
}
await this.http.put('/api/1/user', this.user).toPromise()
}
private async init () {
try {
this.user = await this.http.get('/api/1/user').toPromise()
} catch {
this.user = null
}
this.ready$.next()
this.ready$.complete()
}
} }

View File

@@ -6,97 +6,97 @@ import { Version } from 'src/api'
import { CommonService } from 'src/common' import { CommonService } from 'src/common'
class DemoConnector { class DemoConnector {
constructor ( constructor (
targetWindow: Window, targetWindow: Window,
private commonService: CommonService, private commonService: CommonService,
private version: Version, private version: Version,
) { ) {
targetWindow['tabbyWebDemoDataPath'] = `${this.getDistURL()}/${version.version}/tabby-web-demo/data` targetWindow['tabbyWebDemoDataPath'] = `${this.getDistURL()}/${version.version}/tabby-web-demo/data`
} }
async loadConfig (): Promise<string> { async loadConfig (): Promise<string> {
return `{ return `{
recoverTabs: false, recoverTabs: false,
web: { web: {
preventAccidentalTabClosure: false, preventAccidentalTabClosure: false,
}, },
}` }`
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
async saveConfig (_content: string): Promise<void> { } async saveConfig (_content: string): Promise<void> { }
getAppVersion (): string { getAppVersion (): string {
return this.version.version return this.version.version
} }
getDistURL (): string { getDistURL (): string {
return this.commonService.backendURL + '/app-dist' return this.commonService.backendURL + '/app-dist'
} }
getPluginsToLoad (): string[] { getPluginsToLoad (): string[] {
return [ return [
'tabby-core', 'tabby-core',
'tabby-settings', 'tabby-settings',
'tabby-terminal', 'tabby-terminal',
'tabby-community-color-schemes', 'tabby-community-color-schemes',
'tabby-ssh', 'tabby-ssh',
'tabby-telnet', 'tabby-telnet',
'tabby-web', 'tabby-web',
'tabby-web-demo', 'tabby-web-demo',
] ]
} }
createSocket () { createSocket () {
return new DemoSocketProxy() return new DemoSocketProxy()
} }
} }
export class DemoSocketProxy { export class DemoSocketProxy {
connect$ = new Subject<void>() connect$ = new Subject<void>()
data$ = new Subject<Buffer>() data$ = new Subject<Buffer>()
error$ = new Subject<Buffer>() error$ = new Subject<Buffer>()
close$ = new Subject<Buffer>() close$ = new Subject<Buffer>()
async connect (options) { async connect () {
this.error$.next(new Error('This web demo can\'t actually access Internet, but feel free to download the release and try it out!')) this.error$.next(new Error('This web demo can\'t actually access Internet, but feel free to download the release and try it out!'))
} }
} }
@Component({ @Component({
selector: 'demo-terminal', selector: 'demo-terminal',
template: '<iframe #iframe></iframe>', template: '<iframe #iframe></iframe>',
styleUrls: ['./demoTerminal.component.scss'], styleUrls: ['./demoTerminal.component.scss'],
}) })
export class DemoTerminalComponent { export class DemoTerminalComponent {
@ViewChild('iframe') iframe: ElementRef @ViewChild('iframe') iframe: ElementRef
connector: DemoConnector connector: DemoConnector
constructor ( constructor (
private http: HttpClient, private http: HttpClient,
private commonService: CommonService, private commonService: CommonService,
) { ) {
window.addEventListener('message', this.connectorRequestHandler) window.addEventListener('message', this.connectorRequestHandler)
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
connectorRequestHandler = event => {
if (event.data === 'request-connector') {
this.iframe.nativeElement.contentWindow['__connector__'] = this.connector
this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*')
} }
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async ngAfterViewInit (): Promise<void> {
connectorRequestHandler = event => { const versions = await this.http.get('/api/1/versions').toPromise()
if (event.data === 'request-connector') { versions.sort((a, b) => -semverCompare(a.version, b.version))
this.iframe.nativeElement.contentWindow['__connector__'] = this.connector this.connector = new DemoConnector(this.iframe.nativeElement.contentWindow, this.commonService, versions[0])
this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') this.iframe.nativeElement.src = '/terminal'
} }
}
async ngAfterViewInit (): Promise<void> { ngOnDestroy (): void {
const versions = await this.http.get('/api/1/versions').toPromise() window.removeEventListener('message', this.connectorRequestHandler)
versions.sort((a, b) => -semverCompare(a.version, b.version)) }
this.connector = new DemoConnector(this.iframe.nativeElement.contentWindow, this.commonService, versions[0])
this.iframe.nativeElement.src = '/terminal'
}
ngOnDestroy (): void {
window.removeEventListener('message', this.connectorRequestHandler)
}
} }

View File

@@ -6,60 +6,60 @@ import { InstanceInfo } from 'src/api'
@Component({ @Component({
selector: 'home', selector: 'home',
templateUrl: './home.component.pug', templateUrl: './home.component.pug',
styleUrls: ['./home.component.scss'], styleUrls: ['./home.component.scss'],
}) })
export class HomeComponent { export class HomeComponent {
githubURL = 'https://github.com/Eugeny/tabby' githubURL = 'https://github.com/Eugeny/tabby'
releaseURL = `${this.githubURL}/releases/latest` releaseURL = `${this.githubURL}/releases/latest`
donationURL = 'https://ko-fi.com/eugeny' donationURL = 'https://ko-fi.com/eugeny'
_logo = require('../../../assets/logo.svg') _logo = require('../../../assets/logo.svg')
_downloadIcon = faDownload _downloadIcon = faDownload
_loginIcon = faSignInAlt _loginIcon = faSignInAlt
_donateIcon = faCoffee _donateIcon = faCoffee
navLinks = [ navLinks = [
{ {
title: 'About Tabby', title: 'About Tabby',
link: '/' link: '/',
}, },
{ {
title: 'Features', title: 'Features',
link: '/features' link: '/features',
}, },
] ]
instanceInfo: InstanceInfo instanceInfo: InstanceInfo
background: Waves|undefined background: Waves|undefined
constructor ( constructor (
public route: ActivatedRoute, public route: ActivatedRoute,
public router: Router, public router: Router,
) { ) {
this.instanceInfo = route.snapshot.data.instanceInfo this.instanceInfo = route.snapshot.data.instanceInfo
if (!this.instanceInfo.homepage_enabled) { if (!this.instanceInfo.homepage_enabled) {
router.navigate(['/app']) router.navigate(['/app'])
}
} }
}
async ngAfterViewInit (): Promise<void> { async ngAfterViewInit (): Promise<void> {
this.background = new Waves({ this.background = new Waves({
el: 'body', el: 'body',
mouseControls: true, mouseControls: true,
touchControls: true, touchControls: true,
gyroControls: false, gyroControls: false,
minHeight: 200.00, minHeight: 200.00,
minWidth: 200.00, minWidth: 200.00,
scale: 1.00, scale: 1.00,
scaleMobile: 1.00, scaleMobile: 1.00,
color: 0x70f color: 0x70f,
}) })
} }
ngOnDestroy () { ngOnDestroy () {
this.background?.destroy() this.background?.destroy()
} }
} }

View File

@@ -1,23 +1,23 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
@Component({ @Component({
selector: 'home-features', selector: 'home-features',
templateUrl: './homeFeatures.component.pug', templateUrl: './homeFeatures.component.pug',
styleUrls: ['./homeFeatures.component.scss'], styleUrls: ['./homeFeatures.component.scss'],
}) })
export class HomeFeaturesComponent { export class HomeFeaturesComponent {
screenshots = { screenshots = {
progress: require('assets/screenshots/progress.png'), progress: require('assets/screenshots/progress.png'),
zmodem: require('assets/screenshots/zmodem.png'), zmodem: require('assets/screenshots/zmodem.png'),
colors: require('assets/screenshots/colors.png'), colors: require('assets/screenshots/colors.png'),
hotkeys: require('assets/screenshots/hotkeys.png'), hotkeys: require('assets/screenshots/hotkeys.png'),
ports: require('assets/screenshots/ports.png'), ports: require('assets/screenshots/ports.png'),
ssh2: require('assets/screenshots/ssh2.png'), ssh2: require('assets/screenshots/ssh2.png'),
fonts: require('assets/screenshots/fonts.png'), fonts: require('assets/screenshots/fonts.png'),
history: require('assets/screenshots/history.png'), history: require('assets/screenshots/history.png'),
paste: require('assets/screenshots/paste.png'), paste: require('assets/screenshots/paste.png'),
quake: require('assets/screenshots/quake.png'), quake: require('assets/screenshots/quake.png'),
split: require('assets/screenshots/split.png'), split: require('assets/screenshots/split.png'),
profiles: require('assets/screenshots/profiles.png'), profiles: require('assets/screenshots/profiles.png'),
} }
} }

View File

@@ -3,22 +3,22 @@ import { faDownload } from '@fortawesome/free-solid-svg-icons'
import { faGithub } from '@fortawesome/free-brands-svg-icons' import { faGithub } from '@fortawesome/free-brands-svg-icons'
@Component({ @Component({
selector: 'home-index', selector: 'home-index',
templateUrl: './homeIndex.component.pug', templateUrl: './homeIndex.component.pug',
styleUrls: ['./homeIndex.component.scss'], styleUrls: ['./homeIndex.component.scss'],
}) })
export class HomeIndexComponent { export class HomeIndexComponent {
githubURL = 'https://github.com/Eugeny/tabby' githubURL = 'https://github.com/Eugeny/tabby'
releaseURL = `${this.githubURL}/releases/latest` releaseURL = `${this.githubURL}/releases/latest`
_downloadIcon = faDownload _downloadIcon = faDownload
_githubIcon = faGithub _githubIcon = faGithub
screenshots = { screenshots = {
window: require('assets/screenshots/window.png'), window: require('assets/screenshots/window.png'),
tabs: require('assets/screenshots/tabs.png'), tabs: require('assets/screenshots/tabs.png'),
ssh: require('assets/screenshots/ssh.png'), ssh: require('assets/screenshots/ssh.png'),
serial: require('assets/screenshots/serial.png'), serial: require('assets/screenshots/serial.png'),
win: require('assets/screenshots/win.png'), win: require('assets/screenshots/win.png'),
} }
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap' import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
@@ -14,40 +15,40 @@ import { InstanceInfoResolver } from 'src/api'
import { CommonAppModule } from 'src/common' import { CommonAppModule } from 'src/common'
const ROUTES = [ const ROUTES = [
{ {
path: '', path: '',
component: HomeComponent, component: HomeComponent,
resolve: { resolve: {
instanceInfo: InstanceInfoResolver, instanceInfo: InstanceInfoResolver,
},
children: [
{
path: '',
component: HomeIndexComponent,
},
{
path: 'features',
component: HomeFeaturesComponent,
},
],
}, },
children: [
{
path: '',
component: HomeIndexComponent,
},
{
path: 'features',
component: HomeFeaturesComponent,
},
],
},
] ]
@NgModule({ @NgModule({
imports: [ imports: [
CommonAppModule, CommonAppModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
NgbNavModule, NgbNavModule,
FontAwesomeModule, FontAwesomeModule,
NgxImageZoomModule, NgxImageZoomModule,
RouterModule.forChild(ROUTES), RouterModule.forChild(ROUTES),
], ],
declarations: [ declarations: [
HomeComponent, HomeComponent,
HomeIndexComponent, HomeIndexComponent,
HomeFeaturesComponent, HomeFeaturesComponent,
DemoTerminalComponent, DemoTerminalComponent,
], ],
}) })
export class HomepageModule { } export class HomepageModule { }

View File

@@ -1,17 +1,18 @@
import {extend, mobileCheck, q, color2Hex} from 'vanta/src/helpers.js' /* eslint-disable */
import { extend, mobileCheck, q, color2Hex } from 'vanta/src/helpers.js'
// const DEBUGMODE = window.location.toString().indexOf('VANTADEBUG') !== -1 // const DEBUGMODE = window.location.toString().indexOf('VANTADEBUG') !== -1
const win = typeof window == 'object' const win = typeof window == 'object'
if (win && !window.VANTA) window.VANTA = {} if (win && !window.VANTA) {window.VANTA = {}}
const VANTA = (win && window.VANTA) || {} const VANTA = win && window.VANTA || {}
VANTA.register = (name, Effect) => { VANTA.register = (name, Effect) => {
return VANTA[name] = (opts) => new Effect(opts) return VANTA[name] = (opts) => new Effect(opts)
} }
VANTA.version = '0.5.21' VANTA.version = '0.5.21'
export {VANTA} export { VANTA }
import { AxisHelper, js, MOUSE, OrbitControls, Scene, WebGLRenderer } from 'three/src/Three' import { Scene, WebGLRenderer } from 'three/src/Three'
// const ORBITCONTROLS = { // const ORBITCONTROLS = {
// enableZoom: false, // enableZoom: false,
// userPanSpeed: 3, // userPanSpeed: 3,
@@ -33,14 +34,14 @@ import { AxisHelper, js, MOUSE, OrbitControls, Scene, WebGLRenderer } from 'thre
// } // }
// Namespace for errors // Namespace for errors
const error = function() { const error = function () {
Array.prototype.unshift.call(arguments, '[VANTA]') Array.prototype.unshift.call(arguments, '[VANTA]')
return console.error.apply(this, arguments) return console.error.apply(this, arguments)
} }
VANTA.VantaBase = class VantaBase { VANTA.VantaBase = class VantaBase {
constructor(userOptions = {}) { constructor (userOptions = {}) {
if (!win) return false if (!win) {return false}
VANTA.current = this VANTA.current = this
this.windowMouseMoveWrapper = this.windowMouseMoveWrapper.bind(this) this.windowMouseMoveWrapper = this.windowMouseMoveWrapper.bind(this)
this.windowTouchWrapper = this.windowTouchWrapper.bind(this) this.windowTouchWrapper = this.windowTouchWrapper.bind(this)
@@ -49,7 +50,7 @@ VANTA.VantaBase = class VantaBase {
this.animationLoop = this.animationLoop.bind(this) this.animationLoop = this.animationLoop.bind(this)
this.restart = this.restart.bind(this) this.restart = this.restart.bind(this)
const defaultOptions = (typeof this.getDefaultOptions === 'function') ? this.getDefaultOptions() : this.defaultOptions const defaultOptions = typeof this.getDefaultOptions === 'function' ? this.getDefaultOptions() : this.defaultOptions
this.options = extend({ this.options = extend({
mouseControls: true, mouseControls: true,
touchControls: true, touchControls: true,
@@ -61,19 +62,19 @@ VANTA.VantaBase = class VantaBase {
}, defaultOptions) }, defaultOptions)
if (userOptions instanceof HTMLElement || typeof userOptions === 'string') { if (userOptions instanceof HTMLElement || typeof userOptions === 'string') {
userOptions = {el: userOptions} userOptions = { el: userOptions }
} }
extend(this.options, userOptions) extend(this.options, userOptions)
// Set element // Set element
this.el = this.options.el this.el = this.options.el
if (this.el == null) { if (this.el == null) {
error("Instance needs \"el\" param!") error('Instance needs "el" param!')
} else if (!(this.options.el instanceof HTMLElement)) { } else if (!(this.options.el instanceof HTMLElement)) {
const selector = this.el const selector = this.el
this.el = q(selector) this.el = q(selector)
if (!this.el) { if (!this.el) {
error("Cannot find element", selector) error('Cannot find element', selector)
return return
} }
} }
@@ -121,12 +122,12 @@ VANTA.VantaBase = class VantaBase {
} }
} }
setOptions(userOptions={}){ setOptions (userOptions={}){
extend(this.options, userOptions) extend(this.options, userOptions)
this.triggerMouseMove() this.triggerMouseMove()
} }
prepareEl() { prepareEl () {
let i, child let i, child
// wrapInner for text nodes, so text nodes can be put into foreground // wrapInner for text nodes, so text nodes can be put into foreground
if (typeof Node !== 'undefined' && Node.TEXT_NODE) { if (typeof Node !== 'undefined' && Node.TEXT_NODE) {
@@ -156,27 +157,27 @@ VANTA.VantaBase = class VantaBase {
} }
} }
applyCanvasStyles(canvasEl, opts={}){ applyCanvasStyles (canvasEl, opts={}){
extend(canvasEl.style, { extend(canvasEl.style, {
position: 'absolute', position: 'absolute',
zIndex: 0, zIndex: 0,
top: 0, top: 0,
left: 0, left: 0,
background: '' background: '',
}) })
extend(canvasEl.style, opts) extend(canvasEl.style, opts)
canvasEl.classList.add('vanta-canvas') canvasEl.classList.add('vanta-canvas')
} }
initThree() { initThree () {
if (!WebGLRenderer) { if (!WebGLRenderer) {
console.warn("[VANTA] No THREE defined on window") console.warn('[VANTA] No THREE defined on window')
return return
} }
// Set renderer // Set renderer
this.renderer = new WebGLRenderer({ this.renderer = new WebGLRenderer({
alpha: true, alpha: true,
antialias: true antialias: true,
}) })
this.el.appendChild(this.renderer.domElement) this.el.appendChild(this.renderer.domElement)
this.applyCanvasStyles(this.renderer.domElement) this.applyCanvasStyles(this.renderer.domElement)
@@ -187,7 +188,7 @@ VANTA.VantaBase = class VantaBase {
this.scene = new Scene() this.scene = new Scene()
} }
getCanvasElement() { getCanvasElement () {
if (this.renderer) { if (this.renderer) {
return this.renderer.domElement // js return this.renderer.domElement // js
} }
@@ -196,49 +197,49 @@ VANTA.VantaBase = class VantaBase {
} }
} }
getCanvasRect() { getCanvasRect () {
const canvas = this.getCanvasElement() const canvas = this.getCanvasElement()
if (!canvas) return false if (!canvas) {return false}
return canvas.getBoundingClientRect() return canvas.getBoundingClientRect()
} }
windowMouseMoveWrapper(e){ windowMouseMoveWrapper (e){
const rect = this.getCanvasRect() const rect = this.getCanvasRect()
if (!rect) return false if (!rect) {return false}
const x = e.clientX - rect.left const x = e.clientX - rect.left
const y = e.clientY - rect.top const y = e.clientY - rect.top
if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) { if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) {
this.mouseX = x this.mouseX = x
this.mouseY = y this.mouseY = y
if (!this.options.mouseEase) this.triggerMouseMove(x, y) if (!this.options.mouseEase) {this.triggerMouseMove(x, y)}
} }
} }
windowTouchWrapper(e){ windowTouchWrapper (e){
const rect = this.getCanvasRect() const rect = this.getCanvasRect()
if (!rect) return false if (!rect) {return false}
if (e.touches.length === 1) { if (e.touches.length === 1) {
const x = e.touches[0].clientX - rect.left const x = e.touches[0].clientX - rect.left
const y = e.touches[0].clientY - rect.top const y = e.touches[0].clientY - rect.top
if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) { if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) {
this.mouseX = x this.mouseX = x
this.mouseY = y this.mouseY = y
if (!this.options.mouseEase) this.triggerMouseMove(x, y) if (!this.options.mouseEase) {this.triggerMouseMove(x, y)}
} }
} }
} }
windowGyroWrapper(e){ windowGyroWrapper (e){
const rect = this.getCanvasRect() const rect = this.getCanvasRect()
if (!rect) return false if (!rect) {return false}
const x = Math.round(e.alpha * 2) - rect.left const x = Math.round(e.alpha * 2) - rect.left
const y = Math.round(e.beta * 2) - rect.top const y = Math.round(e.beta * 2) - rect.top
if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) { if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) {
this.mouseX = x this.mouseX = x
this.mouseY = y this.mouseY = y
if (!this.options.mouseEase) this.triggerMouseMove(x, y) if (!this.options.mouseEase) {this.triggerMouseMove(x, y)}
} }
} }
triggerMouseMove(x, y) { triggerMouseMove (x, y) {
if (x === undefined && y === undefined) { // trigger at current position if (x === undefined && y === undefined) { // trigger at current position
if (this.options.mouseEase) { if (this.options.mouseEase) {
x = this.mouseEaseX x = this.mouseEaseX
@@ -254,10 +255,10 @@ VANTA.VantaBase = class VantaBase {
} }
const xNorm = x / this.width // 0 to 1 const xNorm = x / this.width // 0 to 1
const yNorm = y / this.height // 0 to 1 const yNorm = y / this.height // 0 to 1
typeof this.onMouseMove === "function" ? this.onMouseMove(xNorm, yNorm) : void 0 typeof this.onMouseMove === 'function' ? this.onMouseMove(xNorm, yNorm) : void 0
} }
setSize() { setSize () {
this.scale || (this.scale = 1) this.scale || (this.scale = 1)
if (mobileCheck() && this.options.scaleMobile) { if (mobileCheck() && this.options.scaleMobile) {
this.scale = this.options.scaleMobile this.scale = this.options.scaleMobile
@@ -267,21 +268,21 @@ VANTA.VantaBase = class VantaBase {
this.width = Math.max(this.el.offsetWidth, this.options.minWidth) this.width = Math.max(this.el.offsetWidth, this.options.minWidth)
this.height = Math.max(this.el.offsetHeight, this.options.minHeight) this.height = Math.max(this.el.offsetHeight, this.options.minHeight)
} }
initMouse() { initMouse () {
// Init mouseX and mouseY // Init mouseX and mouseY
if ((!this.mouseX && !this.mouseY) || if (!this.mouseX && !this.mouseY ||
(this.mouseX === this.options.minWidth/2 && this.mouseY === this.options.minHeight/2)) { this.mouseX === this.options.minWidth/2 && this.mouseY === this.options.minHeight/2) {
this.mouseX = this.width/2 this.mouseX = this.width/2
this.mouseY = this.height/2 this.mouseY = this.height/2
this.triggerMouseMove(this.mouseX, this.mouseY) this.triggerMouseMove(this.mouseX, this.mouseY)
} }
} }
resize() { resize () {
this.setSize() this.setSize()
if (this.camera) { if (this.camera) {
this.camera.aspect = this.width / this.height this.camera.aspect = this.width / this.height
if (typeof this.camera.updateProjectionMatrix === "function") { if (typeof this.camera.updateProjectionMatrix === 'function') {
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix()
} }
} }
@@ -289,28 +290,28 @@ VANTA.VantaBase = class VantaBase {
this.renderer.setSize(this.width, this.height) this.renderer.setSize(this.width, this.height)
this.renderer.setPixelRatio(window.devicePixelRatio / this.scale) this.renderer.setPixelRatio(window.devicePixelRatio / this.scale)
} }
typeof this.onResize === "function" ? this.onResize() : void 0 typeof this.onResize === 'function' ? this.onResize() : void 0
} }
isOnScreen() { isOnScreen () {
const elHeight = this.el.offsetHeight const elHeight = this.el.offsetHeight
const elRect = this.el.getBoundingClientRect() const elRect = this.el.getBoundingClientRect()
const scrollTop = (window.pageYOffset || const scrollTop = window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body).scrollTop (document.documentElement || document.body.parentNode || document.body).scrollTop
)
const offsetTop = elRect.top + scrollTop const offsetTop = elRect.top + scrollTop
const minScrollTop = offsetTop - window.innerHeight const minScrollTop = offsetTop - window.innerHeight
const maxScrollTop = offsetTop + elHeight const maxScrollTop = offsetTop + elHeight
return minScrollTop <= scrollTop && scrollTop <= maxScrollTop return minScrollTop <= scrollTop && scrollTop <= maxScrollTop
} }
animationLoop() { animationLoop () {
// Step time // Step time
this.t || (this.t = 0) this.t || (this.t = 0)
this.t += 1 this.t += 1
// Uniform time // Uniform time
this.t2 || (this.t2 = 0) this.t2 || (this.t2 = 0)
this.t2 += (this.options.speed || 1) this.t2 += this.options.speed || 1
if (this.uniforms) { if (this.uniforms) {
this.uniforms.iTime.value = this.t2 * 0.016667 // iTime is in seconds this.uniforms.iTime.value = this.t2 * 0.016667 // iTime is in seconds
} }
@@ -327,7 +328,7 @@ VANTA.VantaBase = class VantaBase {
// Only animate if element is within view // Only animate if element is within view
if (this.isOnScreen() || this.options.forceAnimate) { if (this.isOnScreen() || this.options.forceAnimate) {
if (typeof this.onUpdate === "function") { if (typeof this.onUpdate === 'function') {
this.onUpdate() this.onUpdate()
} }
if (this.scene && this.camera) { if (this.scene && this.camera) {
@@ -336,8 +337,8 @@ VANTA.VantaBase = class VantaBase {
} }
// if (this.stats) this.stats.update() // if (this.stats) this.stats.update()
// if (this.renderStats) this.renderStats.update(this.renderer) // if (this.renderStats) this.renderStats.update(this.renderer)
if (this.fps && this.fps.update) this.fps.update() if (this.fps && this.fps.update) {this.fps.update()}
if (typeof this.afterRender === "function") this.afterRender() if (typeof this.afterRender === 'function') {this.afterRender()}
} }
return this.req = window.requestAnimationFrame(this.animationLoop) return this.req = window.requestAnimationFrame(this.animationLoop)
} }
@@ -350,28 +351,28 @@ VANTA.VantaBase = class VantaBase {
// } // }
// } // }
restart() { restart () {
// Restart the effect without destroying the renderer // Restart the effect without destroying the renderer
if (this.scene) { if (this.scene) {
while (this.scene.children.length) { while (this.scene.children.length) {
this.scene.remove(this.scene.children[0]) this.scene.remove(this.scene.children[0])
} }
} }
if (typeof this.onRestart === "function") { if (typeof this.onRestart === 'function') {
this.onRestart() this.onRestart()
} }
this.init() this.init()
} }
init() { init () {
if (typeof this.onInit === "function") { if (typeof this.onInit === 'function') {
this.onInit() this.onInit()
} }
// this.setupControls() // this.setupControls()
} }
destroy() { destroy () {
if (typeof this.onDestroy === "function") { if (typeof this.onDestroy === 'function') {
this.onDestroy() this.onDestroy()
} }
const rm = window.removeEventListener const rm = window.removeEventListener

View File

@@ -1,6 +1,9 @@
/* eslint-disable @typescript-eslint/init-declarations */
/* eslint-disable @typescript-eslint/prefer-for-of */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import VantaBase, { VANTA } from './_base' import VantaBase, { VANTA } from './_base'
import { rn, ri, sample } from 'vanta/src/helpers.js' import { rn, ri } from 'vanta/src/helpers.js'
import { Geometry, MeshPhongMaterial, Vector3, Face3, Mesh, AmbientLight, EdgesGeometry, LineBasicMaterial, LineSegments, PerspectiveCamera, PointLight, DoubleSide } from 'three/src/Three' import { Geometry, MeshPhongMaterial, Vector3, Face3, Mesh, AmbientLight, PerspectiveCamera, PointLight, DoubleSide } from 'three/src/Three'
import { FaceColors } from 'three/src/Three.Legacy' import { FaceColors } from 'three/src/Three.Legacy'
const defaultOptions = { const defaultOptions = {
@@ -8,49 +11,46 @@ const defaultOptions = {
shininess: 30, shininess: 30,
waveHeight: 15, waveHeight: 15,
waveSpeed: 1, waveSpeed: 1,
zoom: 1 zoom: 1,
} }
export class Waves extends VantaBase { export class Waves extends VantaBase {
static initClass() { static initClass () {
this.prototype.ww = 100; this.prototype.ww = 100
this.prototype.hh = 80; this.prototype.hh = 80
this.prototype.waveNoise = 4; // Choppiness of water this.prototype.waveNoise = 4 // Choppiness of water
}
constructor(userOptions) {
super(userOptions)
} }
getMaterial() { getMaterial () {
const options = { const options = {
color: this.options.color, color: this.options.color,
shininess: this.options.shininess, shininess: this.options.shininess,
flatShading: true, flatShading: true,
vertexColors: FaceColors, // Allow coloring individual faces vertexColors: FaceColors, // Allow coloring individual faces
side: DoubleSide side: DoubleSide,
}; }
return new MeshPhongMaterial(options); return new MeshPhongMaterial(options)
} }
onInit() { onInit () {
let i, j; let i, j
const CELLSIZE = 18; const CELLSIZE = 18
const material = this.getMaterial(); const material = this.getMaterial()
const geometry = new Geometry(); const geometry = new Geometry()
// Add vertices // Add vertices
this.gg = []; this.gg = []
for (i=0; i<=this.ww; i++){ for (i=0; i<=this.ww; i++){
this.gg[i] = []; this.gg[i] = []
for (j=0; j<=this.hh; j++){ for (j=0; j<=this.hh; j++){
const id = geometry.vertices.length; const id = geometry.vertices.length
const newVertex = new Vector3( const newVertex = new Vector3(
(i - (this.ww * 0.5)) * CELLSIZE, (i - this.ww * 0.5) * CELLSIZE,
rn(0, this.waveNoise) - 10, rn(0, this.waveNoise) - 10,
((this.hh * 0.5) - j) * CELLSIZE (this.hh * 0.5 - j) * CELLSIZE
); )
geometry.vertices.push(newVertex); geometry.vertices.push(newVertex)
this.gg[i][j] = id; this.gg[i][j] = id
} }
} }
@@ -64,7 +64,7 @@ export class Waves extends VantaBase {
const b = this.gg[i][j-1] const b = this.gg[i][j-1]
const c = this.gg[i-1][j] const c = this.gg[i-1][j]
const a = this.gg[i-1][j-1] const a = this.gg[i-1][j-1]
if (ri(0,1)) { if (ri(0, 1)) {
face1 = new Face3( a, b, c ) face1 = new Face3( a, b, c )
face2 = new Face3( b, c, d ) face2 = new Face3( b, c, d )
} else { } else {
@@ -75,8 +75,8 @@ export class Waves extends VantaBase {
} }
} }
this.plane = new Mesh(geometry, material); this.plane = new Mesh(geometry, material)
this.scene.add(this.plane); this.scene.add(this.plane)
// WIREFRAME // WIREFRAME
// lightColor = 0x55aaee // lightColor = 0x55aaee
@@ -88,55 +88,55 @@ export class Waves extends VantaBase {
// @scene.add( @wireframe ) // @scene.add( @wireframe )
// LIGHTS // LIGHTS
const ambience = new AmbientLight( 0xffffff, 0.9 ); const ambience = new AmbientLight( 0xffffff, 0.9 )
this.scene.add(ambience); this.scene.add(ambience)
const pointLight = new PointLight( 0xffffff, 0.9 ); const pointLight = new PointLight( 0xffffff, 0.9 )
pointLight.position.set(-100,250,-100); pointLight.position.set(-100, 250, -100)
this.scene.add(pointLight); this.scene.add(pointLight)
// CAMERA // CAMERA
this.camera = new PerspectiveCamera( this.camera = new PerspectiveCamera(
35, 35,
this.width / this.height, this.width / this.height,
50, 10000); 50, 10000)
const xOffset = -10; const xOffset = -10
const zOffset = -10; const zOffset = -10
this.cameraPosition = new Vector3( 250+xOffset, 200, 400+zOffset ); this.cameraPosition = new Vector3( 250+xOffset, 200, 400+zOffset )
this.cameraTarget = new Vector3( 150+xOffset, -30, 200+zOffset ); this.cameraTarget = new Vector3( 150+xOffset, -30, 200+zOffset )
this.camera.position.copy(this.cameraPosition); this.camera.position.copy(this.cameraPosition)
this.scene.add(this.camera); this.scene.add(this.camera)
} }
onUpdate() { onUpdate () {
// Update options // Update options
let diff; let diff
this.plane.material.color.set(this.options.color); this.plane.material.color.set(this.options.color)
this.plane.material.shininess = this.options.shininess; this.plane.material.shininess = this.options.shininess
this.camera.ox = this.cameraPosition.x / this.options.zoom; this.camera.ox = this.cameraPosition.x / this.options.zoom
this.camera.oy = this.cameraPosition.y / this.options.zoom; this.camera.oy = this.cameraPosition.y / this.options.zoom
this.camera.oz = this.cameraPosition.z / this.options.zoom; this.camera.oz = this.cameraPosition.z / this.options.zoom
if (this.controls != null) { if (this.controls != null) {
this.controls.update(); this.controls.update()
} }
const c = this.camera; const c = this.camera
if (Math.abs(c.tx - c.position.x) > 0.01) { if (Math.abs(c.tx - c.position.x) > 0.01) {
diff = c.tx - c.position.x; diff = c.tx - c.position.x
c.position.x += diff * 0.02; c.position.x += diff * 0.02
} }
if (Math.abs(c.ty - c.position.y) > 0.01) { if (Math.abs(c.ty - c.position.y) > 0.01) {
diff = c.ty - c.position.y; diff = c.ty - c.position.y
c.position.y += diff * 0.02; c.position.y += diff * 0.02
} }
if (Math.abs(c.tz - c.position.z) > 0.01) { if (Math.abs(c.tz - c.position.z) > 0.01) {
diff = c.tz - c.position.z; diff = c.tz - c.position.z
c.position.z += diff * 0.02; c.position.z += diff * 0.02
} }
c.lookAt( this.cameraTarget ); c.lookAt( this.cameraTarget )
// Fix flickering problems // Fix flickering problems
// c.near = Math.max((c.position.y * 0.5) - 20, 1); // c.near = Math.max((c.position.y * 0.5) - 20, 1);
@@ -144,24 +144,24 @@ export class Waves extends VantaBase {
// WAVES // WAVES
for (let i = 0; i < this.plane.geometry.vertices.length; i++) { for (let i = 0; i < this.plane.geometry.vertices.length; i++) {
const v = this.plane.geometry.vertices[i]; const v = this.plane.geometry.vertices[i]
if (!v.oy) { // INIT if (!v.oy) { // INIT
v.oy = v.y; v.oy = v.y
} else { } else {
const s = this.options.waveSpeed; const s = this.options.waveSpeed
const crossChop = Math.sqrt(s) * Math.cos(-v.x - (v.z*0.7)); // + s * (i % 229) / 229 * 5 const crossChop = Math.sqrt(s) * Math.cos(-v.x - v.z*0.7) // + s * (i % 229) / 229 * 5
const delta = Math.sin((((s*this.t*0.02) - (s*v.x*0.025)) + (s*v.z*0.015) + crossChop)); const delta = Math.sin(s*this.t*0.02 - s*v.x*0.025 + s*v.z*0.015 + crossChop)
const trochoidDelta = Math.pow(delta + 1, 2) / 4; const trochoidDelta = Math.pow(delta + 1, 2) / 4
v.y = v.oy + (trochoidDelta * this.options.waveHeight); v.y = v.oy + trochoidDelta * this.options.waveHeight
} }
} }
// @wireframe.geometry.vertices[i].y = v.y // @wireframe.geometry.vertices[i].y = v.y
this.plane.geometry.dynamic = true; this.plane.geometry.dynamic = true
this.plane.geometry.computeFaceNormals(); this.plane.geometry.computeFaceNormals()
this.plane.geometry.verticesNeedUpdate = true; this.plane.geometry.verticesNeedUpdate = true
this.plane.geometry.normalsNeedUpdate = true; this.plane.geometry.normalsNeedUpdate = true
// @scene.remove( @wireframe ) // @scene.remove( @wireframe )
// geo = new EdgesGeometry(@plane.geometry) // geo = new EdgesGeometry(@plane.geometry)
@@ -170,21 +170,21 @@ export class Waves extends VantaBase {
// @scene.add( @wireframe ) // @scene.add( @wireframe )
if (this.wireframe) { if (this.wireframe) {
this.wireframe.geometry.fromGeometry(this.plane.geometry); this.wireframe.geometry.fromGeometry(this.plane.geometry)
this.wireframe.geometry.computeFaceNormals(); this.wireframe.geometry.computeFaceNormals()
} }
} }
onMouseMove(x,y) { onMouseMove (x, y) {
const c = this.camera; const c = this.camera
if (!c.oy) { if (!c.oy) {
c.oy = c.position.y; c.oy = c.position.y
c.ox = c.position.x; c.ox = c.position.x
c.oz = c.position.z; c.oz = c.position.z
} }
c.tx = c.ox + (((x-0.5) * 100) / this.options.zoom); c.tx = c.ox + (x-0.5) * 100 / this.options.zoom
c.ty = c.oy + (((y-0.5) * -100) / this.options.zoom); c.ty = c.oy + (y-0.5) * -100 / this.options.zoom
return c.tz = c.oz + (((x-0.5) * -50) / this.options.zoom); return c.tz = c.oz + (x-0.5) * -50 / this.options.zoom
} }
} }

View File

@@ -4,29 +4,29 @@ import { LoginService, CommonService } from 'src/common'
import { faGithub, faGitlab, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons' import { faGithub, faGitlab, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons'
@Component({ @Component({
selector: 'login', selector: 'login',
templateUrl: './login.component.pug', templateUrl: './login.component.pug',
styleUrls: ['./login.component.scss'], styleUrls: ['./login.component.scss'],
}) })
export class LoginComponent { export class LoginComponent {
loggedIn: any loggedIn: any
ready = false ready = false
providers = [ providers = [
{ name: 'GitHub', icon: faGithub, cls: 'btn-primary', id: 'github' }, { name: 'GitHub', icon: faGithub, cls: 'btn-primary', id: 'github' },
{ name: 'GitLab', icon: faGitlab, cls: 'btn-warning', id: 'gitlab' }, { name: 'GitLab', icon: faGitlab, cls: 'btn-warning', id: 'gitlab' },
{ name: 'Google', icon: faGoogle, cls: 'btn-secondary', id: 'google-oauth2' }, { name: 'Google', icon: faGoogle, cls: 'btn-secondary', id: 'google-oauth2' },
{ name: 'Microsoft', icon: faMicrosoft, cls: 'btn-light', id: 'microsoft-graph' }, { name: 'Microsoft', icon: faMicrosoft, cls: 'btn-light', id: 'microsoft-graph' },
] ]
constructor ( constructor (
private loginService: LoginService, private loginService: LoginService,
public commonService: CommonService, public commonService: CommonService,
) { } ) { }
async ngOnInit () { async ngOnInit () {
await this.loginService.ready$.toPromise() await this.loginService.ready$.toPromise()
this.loggedIn = !!this.loginService.user this.loggedIn = !!this.loginService.user
this.ready = true this.ready = true
} }
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap' import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
@@ -11,27 +12,27 @@ import { InstanceInfoResolver } from 'src/api'
import { CommonAppModule } from 'src/common' import { CommonAppModule } from 'src/common'
const ROUTES = [ const ROUTES = [
{ {
path: '', path: '',
component: LoginComponent, component: LoginComponent,
resolve: { resolve: {
instanceInfo: InstanceInfoResolver, instanceInfo: InstanceInfoResolver,
},
}, },
},
] ]
@NgModule({ @NgModule({
imports: [ imports: [
CommonAppModule, CommonAppModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
NgbNavModule, NgbNavModule,
FontAwesomeModule, FontAwesomeModule,
NgxImageZoomModule, NgxImageZoomModule,
RouterModule.forChild(ROUTES), RouterModule.forChild(ROUTES),
], ],
declarations: [ declarations: [
LoginComponent, LoginComponent,
], ],
}) })
export class LoginModule { } export class LoginModule { }

View File

@@ -18,63 +18,68 @@ enableProdMode()
import { AppServerModule } from './app.server.module' import { AppServerModule } from './app.server.module'
const engine = ngExpressEngine({ const engine = ngExpressEngine({
bootstrap: AppServerModule, bootstrap: AppServerModule,
}) })
const hardlinks = { const hardlinks = {
'cwd-detection': 'https://github.com/Eugeny/tabby/wiki/Shell-working-directory-reporting', 'cwd-detection': 'https://github.com/Eugeny/tabby/wiki/Shell-working-directory-reporting',
'privacy-policy': 'https://github.com/Eugeny/tabby/wiki/Privacy-Policy-for-Tabby-Web', 'privacy-policy': 'https://github.com/Eugeny/tabby/wiki/Privacy-Policy-for-Tabby-Web',
'terms-of-use': 'https://github.com/Eugeny/tabby/wiki/Terms-of-Use-of-Tabby-Web', 'terms-of-use': 'https://github.com/Eugeny/tabby/wiki/Terms-of-Use-of-Tabby-Web',
} }
function start () { function start () {
const app = express() const app = express()
const PORT = process.env.PORT || 8000 const PORT = process.env.PORT ?? 8000
const DIST_FOLDER = join(process.cwd(), 'build') const DIST_FOLDER = join(process.cwd(), 'build')
app.engine('html', engine) app.engine('html', engine)
app.set('view engine', 'html') app.set('view engine', 'html')
app.set('views', DIST_FOLDER) app.set('views', DIST_FOLDER)
app.use('/static', express.static(DIST_FOLDER, { app.use('/static', express.static(DIST_FOLDER, {
maxAge: '1y', maxAge: '1y',
})) }))
app.get(['/', '/app', '/login', '/features'], (req, res) => { app.get(['/', '/app', '/login', '/features'], (req, res) => {
res.render( res.render(
'index', 'index',
{ {
req, req,
providers: [ providers: [
{ provide: 'BACKEND_URL', useValue: process.env.BACKEND_URL ?? '' }, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
], { provide: 'BACKEND_URL', useValue: process.env.BACKEND_URL ?? '' },
}, ],
(err: Error, html: string) => { },
html = html.replace('{{backendURL}}', process.env.BACKEND_URL ?? '') (err?: Error, html?: string) => {
res.status(err ? 500 : 200).send(html || err.message) if (html) {
}, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
) html = html.replace('{{backendURL}}', process.env.BACKEND_URL ?? '')
}) }
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
res.status(err ? 500 : 200).send(html ?? err!.message)
},
)
})
app.get(['/terminal'], (req, res) => { app.get(['/terminal'], (req, res) => {
res.sendFile(join(DIST_FOLDER, 'terminal.html')) res.sendFile(join(DIST_FOLDER, 'terminal.html'))
}) })
for (const [key, value] of Object.entries(hardlinks)) { for (const [key, value] of Object.entries(hardlinks)) {
app.get(`/go/${key}`, (req, res) => res.redirect(value)) app.get(`/go/${key}`, (req, res) => res.redirect(value))
} }
process.umask(0o002) process.umask(0o002)
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`) console.log(`Node Express server listening on http://localhost:${PORT}`)
}) })
} }
const WORKERS = process.env.WEB_CONCURRENCY || 4 const WORKERS = process.env.WEB_CONCURRENCY ?? 4
throng({ throng({
workers: WORKERS, workers: WORKERS,
lifetime: Infinity, lifetime: Infinity,
start, start,
}) })

View File

@@ -1,41 +1,41 @@
import * as domino from 'domino'; import * as domino from 'domino'
import * as fs from 'fs'; import * as fs from 'fs'
import * as path from 'path'; import * as path from 'path'
const template = fs.readFileSync(path.join(process.cwd(), 'build', 'index.html')).toString(); const template = fs.readFileSync(path.join(process.cwd(), 'build', 'index.html')).toString()
const win = domino.createWindow(template); const win = domino.createWindow(template)
global['window'] = win; global['window'] = win
Object.defineProperty(win.document.body.style, 'transform', { Object.defineProperty(win.document.body.style, 'transform', {
value: () => { value: () => {
return { return {
enumerable: true, enumerable: true,
configurable: true configurable: true,
}; }
}, },
}); })
Object.defineProperty(win.document.body.style, 'z-index', { Object.defineProperty(win.document.body.style, 'z-index', {
value: () => { value: () => {
return { return {
enumerable: true, enumerable: true,
configurable: true configurable: true,
}; }
}, },
}); })
global['document'] = win.document; global['document'] = win.document
global['CSS'] = null; global['CSS'] = null
// global['atob'] = win.atob; // global['atob'] = win.atob;
global['atob'] = (base64: string) => { global['atob'] = (base64: string) => {
return Buffer.from(base64, 'base64').toString(); return Buffer.from(base64, 'base64').toString()
};
function setDomTypes() {
// Make all Domino types available as types in the global env.
Object.assign(global, domino['impl']);
(global as any)['KeyboardEvent'] = domino['impl'].Event;
} }
setDomTypes(); function setDomTypes () {
// Make all Domino types available as types in the global env.
Object.assign(global, domino['impl']);
(global as any)['KeyboardEvent'] = domino['impl'].Event
}
setDomTypes()

View File

@@ -1,72 +1,72 @@
import './terminal-styles.scss' import './terminal-styles.scss'
async function start () { async function start () {
window['__filename'] = '' window['__filename'] = ''
await new Promise<void>(resolve => { await new Promise<void>(resolve => {
window.addEventListener('message', event => { window.addEventListener('message', event => {
if (event.data === 'connector-ready') { if (event.data === 'connector-ready') {
resolve() resolve()
} }
})
window.parent.postMessage('request-connector', '*')
}) })
window.parent.postMessage('request-connector', '*')
})
const connector = window['__connector__'] const connector = window['__connector__']
const appVersion = connector.getAppVersion() const appVersion = connector.getAppVersion()
async function webRequire (url) { async function webRequire (url) {
console.log(`Loading ${url}`) console.log(`Loading ${url}`)
const e = document.createElement('script') const e = document.createElement('script')
window['module'] = { exports: {} } as any window['module'] = { exports: {} } as any
window['exports'] = window['module'].exports window['exports'] = window['module'].exports
await new Promise(resolve => { await new Promise(resolve => {
e.onload = resolve e.onload = resolve
e.src = url e.src = url
document.querySelector('head').appendChild(e) document.head.appendChild(e)
}) })
return window['module'].exports return window['module'].exports
} }
async function prefetchURL (url) { async function prefetchURL (url) {
await (await fetch(url)).text() await (await fetch(url)).text()
} }
const baseUrl = `${connector.getDistURL()}/${appVersion}` const baseUrl = `${connector.getDistURL()}/${appVersion}`
const coreURLs = [ const coreURLs = [
`${baseUrl}/tabby-web-container/dist/preload.js`, `${baseUrl}/tabby-web-container/dist/preload.js`,
`${baseUrl}/tabby-web-container/dist/bundle.js`, `${baseUrl}/tabby-web-container/dist/bundle.js`,
] ]
await Promise.all(coreURLs.map(prefetchURL)) await Promise.all(coreURLs.map(prefetchURL))
for (const url of coreURLs) { for (const url of coreURLs) {
await webRequire(url) await webRequire(url)
} }
document.querySelector('app-root')['style'].display = 'flex' document.querySelector('app-root')!['style'].display = 'flex'
const tabby = window['Tabby'] const tabby = window['Tabby']
const pluginURLs = connector.getPluginsToLoad().map(x => `${baseUrl}/${x}`) const pluginURLs = connector.getPluginsToLoad().map(x => `${baseUrl}/${x}`)
const pluginModules = await tabby.loadPlugins(pluginURLs, (current, total) => { const pluginModules = await tabby.loadPlugins(pluginURLs, (current, total) => {
(document.querySelector('.progress .bar') as HTMLElement).style.width = `${100 * current / total}%` // eslint-disable-line (document.querySelector('.progress .bar') as HTMLElement).style.width = `${100 * current / total}%` // eslint-disable-line
}) })
const config = connector.loadConfig() const config = connector.loadConfig()
tabby.bootstrap({ tabby.bootstrap({
packageModules: pluginModules, packageModules: pluginModules,
bootstrapData: { bootstrapData: {
config, config,
executable: 'web', executable: 'web',
isFirstWindow: true, isFirstWindow: true,
windowID: 1, windowID: 1,
installedPlugins: [], installedPlugins: [],
userPluginsPath: '/', userPluginsPath: '/',
}, },
debugMode: false, debugMode: false,
connector, connector,
}) })
} }
start() start()

View File

@@ -17,6 +17,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"declaration": true, "declaration": true,
"strictNullChecks": true,
"lib": [ "lib": [
"dom", "dom",
"es5", "es5",

View File

@@ -481,11 +481,16 @@
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57"
integrity sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w== integrity sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==
"@types/json-schema@*", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8": "@types/json-schema@*", "@types/json-schema@^7.0.8":
version "7.0.8" version "7.0.8"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818"
integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg== integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==
"@types/json-schema@^7.0.9":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
"@types/node@*": "@types/node@*":
version "16.4.2" version "16.4.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.2.tgz#0a95d7fd950cb1eaca0ce11031d72e8f680b775a" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.2.tgz#0a95d7fd950cb1eaca0ce11031d72e8f680b775a"
@@ -496,74 +501,75 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.5.tgz#011eece9d3f839a806b63973e228f85967b79ed3" resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.5.tgz#011eece9d3f839a806b63973e228f85967b79ed3"
integrity sha512-vVjM0SVzgaOUpflq4GYBvCpozes8OgIIS5gVXVka+OfK3hvnkC1i93U8WiY2OtNE4XUWyyy/86Kf6e0IHTQw1Q== integrity sha512-vVjM0SVzgaOUpflq4GYBvCpozes8OgIIS5gVXVka+OfK3hvnkC1i93U8WiY2OtNE4XUWyyy/86Kf6e0IHTQw1Q==
"@typescript-eslint/eslint-plugin@^4.28.4": "@typescript-eslint/eslint-plugin@^5.1.0":
version "4.28.4" version "5.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.4.tgz#e73c8cabbf3f08dee0e1bda65ed4e622ae8f8921" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.1.0.tgz#381c188dfab12f7a2c7b6a8ba2402d6273eadeaa"
integrity sha512-s1oY4RmYDlWMlcV0kKPBaADn46JirZzvvH7c2CtAqxCY96S538JRBAzt83RrfkDheV/+G/vWNK0zek+8TB3Gmw== integrity sha512-bekODL3Tqf36Yz8u+ilha4zGxL9mdB6LIsIoMAvvC5FAuWo4NpZYXtCbv7B2CeR1LhI/lLtLk+q4tbtxuoVuCg==
dependencies: dependencies:
"@typescript-eslint/experimental-utils" "4.28.4" "@typescript-eslint/experimental-utils" "5.1.0"
"@typescript-eslint/scope-manager" "4.28.4" "@typescript-eslint/scope-manager" "5.1.0"
debug "^4.3.1" debug "^4.3.2"
functional-red-black-tree "^1.0.1" functional-red-black-tree "^1.0.1"
regexpp "^3.1.0" ignore "^5.1.8"
regexpp "^3.2.0"
semver "^7.3.5" semver "^7.3.5"
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/experimental-utils@4.28.4": "@typescript-eslint/experimental-utils@5.1.0":
version "4.28.4" version "5.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.4.tgz#9c70c35ebed087a5c70fb0ecd90979547b7fec96" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.1.0.tgz#918a1a3d30404cc1f8edcfdf0df200804ef90d31"
integrity sha512-OglKWOQRWTCoqMSy6pm/kpinEIgdcXYceIcH3EKWUl4S8xhFtN34GQRaAvTIZB9DD94rW7d/U7tUg3SYeDFNHA== integrity sha512-ovE9qUiZMOMgxQAESZsdBT+EXIfx/YUYAbwGUI6V03amFdOOxI9c6kitkgRvLkJaLusgMZ2xBhss+tQ0Y1HWxA==
dependencies: dependencies:
"@types/json-schema" "^7.0.7" "@types/json-schema" "^7.0.9"
"@typescript-eslint/scope-manager" "4.28.4" "@typescript-eslint/scope-manager" "5.1.0"
"@typescript-eslint/types" "4.28.4" "@typescript-eslint/types" "5.1.0"
"@typescript-eslint/typescript-estree" "4.28.4" "@typescript-eslint/typescript-estree" "5.1.0"
eslint-scope "^5.1.1" eslint-scope "^5.1.1"
eslint-utils "^3.0.0" eslint-utils "^3.0.0"
"@typescript-eslint/parser@^4.28.4": "@typescript-eslint/parser@^5.1.0":
version "4.28.4" version "5.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.4.tgz#bc462dc2779afeefdcf49082516afdc3e7b96fab" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.1.0.tgz#6c7f837d210d2bc0a811e7ea742af414f4e00908"
integrity sha512-4i0jq3C6n+og7/uCHiE6q5ssw87zVdpUj1k6VlVYMonE3ILdFApEzTWgppSRG4kVNB/5jxnH+gTeKLMNfUelQA== integrity sha512-vx1P+mhCtYw3+bRHmbalq/VKP2Y3gnzNgxGxfEWc6OFpuEL7iQdAeq11Ke3Rhy8NjgB+AHsIWEwni3e+Y7djKA==
dependencies: dependencies:
"@typescript-eslint/scope-manager" "4.28.4" "@typescript-eslint/scope-manager" "5.1.0"
"@typescript-eslint/types" "4.28.4" "@typescript-eslint/types" "5.1.0"
"@typescript-eslint/typescript-estree" "4.28.4" "@typescript-eslint/typescript-estree" "5.1.0"
debug "^4.3.1" debug "^4.3.2"
"@typescript-eslint/scope-manager@4.28.4": "@typescript-eslint/scope-manager@5.1.0":
version "4.28.4" version "5.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.4.tgz#bdbce9b6a644e34f767bd68bc17bb14353b9fe7f" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.1.0.tgz#6f1f26ad66a8f71bbb33b635e74fec43f76b44df"
integrity sha512-ZJBNs4usViOmlyFMt9X9l+X0WAFcDH7EdSArGqpldXu7aeZxDAuAzHiMAeI+JpSefY2INHrXeqnha39FVqXb8w== integrity sha512-yYlyVjvn5lvwCL37i4hPsa1s0ORsjkauhTqbb8MnpvUs7xykmcjGqwlNZ2Q5QpoqkJ1odlM2bqHqJwa28qV6Tw==
dependencies: dependencies:
"@typescript-eslint/types" "4.28.4" "@typescript-eslint/types" "5.1.0"
"@typescript-eslint/visitor-keys" "4.28.4" "@typescript-eslint/visitor-keys" "5.1.0"
"@typescript-eslint/types@4.28.4": "@typescript-eslint/types@5.1.0":
version "4.28.4" version "5.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.4.tgz#41acbd79b5816b7c0dd7530a43d97d020d3aeb42" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.1.0.tgz#a8a75ddfc611660de6be17d3ad950302385607a9"
integrity sha512-3eap4QWxGqkYuEmVebUGULMskR6Cuoc/Wii0oSOddleP4EGx1tjLnZQ0ZP33YRoMDCs5O3j56RBV4g14T4jvww== integrity sha512-sEwNINVxcB4ZgC6Fe6rUyMlvsB2jvVdgxjZEjQUQVlaSPMNamDOwO6/TB98kFt4sYYfNhdhTPBEQqNQZjMMswA==
"@typescript-eslint/typescript-estree@4.28.4": "@typescript-eslint/typescript-estree@5.1.0":
version "4.28.4" version "5.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.4.tgz#252e6863278dc0727244be9e371eb35241c46d00" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.1.0.tgz#132aea34372df09decda961cb42457433aa6e83d"
integrity sha512-z7d8HK8XvCRyN2SNp+OXC2iZaF+O2BTquGhEYLKLx5k6p0r05ureUtgEfo5f6anLkhCxdHtCf6rPM1p4efHYDQ== integrity sha512-SSz+l9YrIIsW4s0ZqaEfnjl156XQ4VRmJsbA0ZE1XkXrD3cRpzuZSVCyqeCMR3EBjF27IisWakbBDGhGNIOvfQ==
dependencies: dependencies:
"@typescript-eslint/types" "4.28.4" "@typescript-eslint/types" "5.1.0"
"@typescript-eslint/visitor-keys" "4.28.4" "@typescript-eslint/visitor-keys" "5.1.0"
debug "^4.3.1" debug "^4.3.2"
globby "^11.0.3" globby "^11.0.4"
is-glob "^4.0.1" is-glob "^4.0.3"
semver "^7.3.5" semver "^7.3.5"
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@4.28.4": "@typescript-eslint/visitor-keys@5.1.0":
version "4.28.4" version "5.1.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.4.tgz#92dacfefccd6751cbb0a964f06683bfd72d0c4d3" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.1.0.tgz#e01a01b27eb173092705ae983aa1451bd1842630"
integrity sha512-NIAXAdbz1XdOuzqkJHjNKXKj8QQ4cv5cxR/g0uQhCYf/6//XrmfpaYsM7PnBcNbfvTDLUkqQ5TPNm1sozDdTWg== integrity sha512-uqNXepKBg81JVwjuqAxYrXa1Ql/YDzM+8g/pS+TCPxba0wZttl8m5DkrasbfnmJGHs4lQ2jTbcZ5azGhI7kK+w==
dependencies: dependencies:
"@typescript-eslint/types" "4.28.4" "@typescript-eslint/types" "5.1.0"
eslint-visitor-keys "^2.0.0" eslint-visitor-keys "^3.0.0"
"@webassemblyjs/ast@1.11.1": "@webassemblyjs/ast@1.11.1":
version "1.11.1" version "1.11.1"
@@ -1508,7 +1514,7 @@ debug@2.6.9:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@^4.0.1, debug@^4.1.1, debug@^4.3.1: debug@^4.0.1, debug@^4.1.1, debug@^4.3.2:
version "4.3.2" version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@@ -1791,6 +1797,11 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint-visitor-keys@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.0.0.tgz#e32e99c6cdc2eb063f204eda5db67bfe58bb4186"
integrity sha512-mJOZa35trBTb3IyRmo8xmKBZlxf+N7OnUl4+ZhJHs/r+0770Wh/LEACE2pqMGMe27G/4y8P2bYGk4J70IC5k1Q==
eslint@^7.31.0: eslint@^7.31.0:
version "7.31.0" version "7.31.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca"
@@ -2231,7 +2242,7 @@ globals@^13.6.0, globals@^13.9.0:
dependencies: dependencies:
type-fest "^0.20.2" type-fest "^0.20.2"
globby@^11.0.3: globby@^11.0.4:
version "11.0.4" version "11.0.4"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
@@ -2423,7 +2434,7 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.4: ignore@^5.1.4, ignore@^5.1.8:
version "5.1.8" version "5.1.8"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
@@ -2565,6 +2576,13 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
dependencies: dependencies:
is-extglob "^2.1.1" is-extglob "^2.1.1"
is-glob@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0: is-number@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
@@ -3879,7 +3897,7 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regexpp@^3.1.0: regexpp@^3.1.0, regexpp@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==