diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..7d6a185 --- /dev/null +++ b/.bumpversion.cfg @@ -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}" diff --git a/.editorconfig b/.editorconfig index c308ed0..e6ffe0e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,6 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false + +[*.ts] +indent_size = 2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..f26f04e --- /dev/null +++ b/.github/workflows/lint.yml @@ -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 . diff --git a/backend/.flake8 b/backend/.flake8 new file mode 100644 index 0000000..4375cfb --- /dev/null +++ b/backend/.flake8 @@ -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 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 7588af2..5a7eccc 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tabby-web" -version = "0.1.0" +version = "1.0.0" description = "" authors = ["Your Name "] diff --git a/backend/tabby/app/api.py b/backend/tabby/app/api.py deleted file mode 100644 index 97295f6..0000000 --- a/backend/tabby/app/api.py +++ /dev/null @@ -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() diff --git a/backend/tabby/app/api/__init__.py b/backend/tabby/app/api/__init__.py new file mode 100644 index 0000000..3bc1590 --- /dev/null +++ b/backend/tabby/app/api/__init__.py @@ -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)), +] diff --git a/backend/tabby/app/api/app_version.py b/backend/tabby/app/api/app_version.py new file mode 100644 index 0000000..eee339a --- /dev/null +++ b/backend/tabby/app/api/app_version.py @@ -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 + ) diff --git a/backend/tabby/app/api/auth.py b/backend/tabby/app/api/auth.py new file mode 100644 index 0000000..72f91a7 --- /dev/null +++ b/backend/tabby/app/api/auth.py @@ -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) diff --git a/backend/tabby/app/api/config.py b/backend/tabby/app/api/config.py new file mode 100644 index 0000000..6ba134b --- /dev/null +++ b/backend/tabby/app/api/config.py @@ -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) diff --git a/backend/tabby/app/api/gateway.py b/backend/tabby/app/api/gateway.py new file mode 100644 index 0000000..32e307b --- /dev/null +++ b/backend/tabby/app/api/gateway.py @@ -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() diff --git a/backend/tabby/app/api/info.py b/backend/tabby/app/api/info.py new file mode 100644 index 0000000..292a931 --- /dev/null +++ b/backend/tabby/app/api/info.py @@ -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, + } diff --git a/backend/tabby/app/api/user.py b/backend/tabby/app/api/user.py new file mode 100644 index 0000000..f7856e7 --- /dev/null +++ b/backend/tabby/app/api/user.py @@ -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() diff --git a/backend/tabby/app/sponsors.py b/backend/tabby/app/sponsors.py index 366495b..0358ecd 100644 --- a/backend/tabby/app/sponsors.py +++ b/backend/tabby/app/sponsors.py @@ -39,7 +39,7 @@ def check_is_sponsor(user: User) -> bool: query = ''' query { - user (login: "eugeny") { + viewer { sponsorshipsAsSponsor(%s) { pageInfo { startCursor diff --git a/backend/tabby/app/tests.py b/backend/tabby/app/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/backend/tabby/app/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/tabby/app/urls.py b/backend/tabby/app/urls.py index 698969c..38dd066 100644 --- a/backend/tabby/app/urls.py +++ b/backend/tabby/app/urls.py @@ -1,23 +1,17 @@ -from django.urls import path, re_path, include -from rest_framework import routers +from django.urls import path, include from . import api 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 = [ - path('api/1/auth/logout', api.LogoutView.as_view()), - path('api/1/user', api.UserViewSet.as_view({'get': 'retrieve', 'put': 'update'})), - path('api/1/instance-info', api.InstanceInfoViewSet.as_view({'get': 'retrieve'})), - 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(p, views.IndexView.as_view()) + for p in ['', 'login', 'app', 'about', 'features'] + ], path('app-dist//', views.AppDistView.as_view()), - path('', include(router.urls)), + path('terminal', views.TerminalView.as_view()), + + path('', include(api.urlpatterns)), ] diff --git a/backend/tabby/asgi.py b/backend/tabby/asgi.py index d6fc204..a87bce8 100644 --- a/backend/tabby/asgi.py +++ b/backend/tabby/asgi.py @@ -4,8 +4,8 @@ import os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tabby.settings') django.setup() -from channels.routing import ProtocolTypeRouter -from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter # noqa +from django.core.asgi import get_asgi_application # noqa application = ProtocolTypeRouter({ 'http': get_asgi_application(), diff --git a/backend/tabby/middleware.py b/backend/tabby/middleware.py index 8b1ce2e..a7f1a88 100644 --- a/backend/tabby/middleware.py +++ b/backend/tabby/middleware.py @@ -1,7 +1,7 @@ import logging from tabby.app.models import User 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 diff --git a/frontend/.eslintrc.yml b/frontend/.eslintrc.yml index a2921cb..78d9015 100644 --- a/frontend/.eslintrc.yml +++ b/frontend/.eslintrc.yml @@ -2,7 +2,6 @@ parser: '@typescript-eslint/parser' parserOptions: project: - tsconfig.json - - '*/tsconfig.typings.json' extends: - 'plugin:@typescript-eslint/all' plugins: @@ -18,7 +17,7 @@ rules: - never '@typescript-eslint/indent': - error - - 4 + - 2 '@typescript-eslint/explicit-member-accessibility': - error - accessibility: no-public @@ -121,3 +120,10 @@ rules: '@typescript-eslint/no-unsafe-argument': off '@typescript-eslint/restrict-plus-operands': 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 diff --git a/frontend/package.json b/frontend/package.json index 5e7d75f..4cb7bdb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "main": "index.js", "scripts": { + "lint": "eslint src", "build": "webpack --progress", "watch": "DEV=1 webpack --progress --watch", "build:server": "webpack --progress -c webpack.config.server.js", @@ -33,8 +34,8 @@ "@nguniversal/express-engine": "^11.1.0", "@tabby-gang/to-string-loader": "^1.1.7-beta.1", "@types/node": "^11.9.5", - "@typescript-eslint/eslint-plugin": "^4.28.4", - "@typescript-eslint/parser": "^4.28.4", + "@typescript-eslint/eslint-plugin": "^5.1.0", + "@typescript-eslint/parser": "^5.1.0", "apply-loader": "^2.0.0", "bootstrap": "^5.0.1", "buffer": "^6.0.3", @@ -66,6 +67,7 @@ "source-map-support": "^0.5.19", "source-sans-pro": "^2.45.0", "style-loader": "^0.23.1", + "three": "^0.119.0", "throng": "^5.0.0", "typescript": "~4.1", "val-loader": "^4.0.0", @@ -73,7 +75,6 @@ "webpack": "^5.38.1", "webpack-bundle-analyzer": "^4.4.2", "webpack-cli": "^4.7.2", - "three": "^0.119.0", "zone.js": "^0.11.4" } } diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 8834e15..29ad8e8 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -4,47 +4,47 @@ import { Resolve } from '@angular/router' import { Observable } from 'rxjs' export interface User { - id: number - active_config: number - active_version: string - custom_connection_gateway: string|null - custom_connection_gateway_token: string|null - config_sync_token: string - github_username: string - is_pro: boolean - is_sponsor: boolean + id: number + active_config: number + active_version: string + custom_connection_gateway: string|null + custom_connection_gateway_token: string|null + config_sync_token: string + github_username: string + is_pro: boolean + is_sponsor: boolean } export interface Config { - id: number - content: string - last_used_with_version: string - created_at: Date - modified_at: Date + id: number + content: string + last_used_with_version: string + created_at: Date + modified_at: Date } export interface Version { - version: string - plugins: string[] + version: string + plugins: string[] } export interface InstanceInfo { - login_enabled: boolean - homepage_enabled: boolean + login_enabled: boolean + homepage_enabled: boolean } export interface Gateway { - host: string - port: number - url: string - auth_token: string + host: string + port: number + url: string + auth_token: string } @Injectable({ providedIn: 'root' }) export class InstanceInfoResolver implements Resolve> { - constructor (private http: HttpClient) { } + constructor (private http: HttpClient) { } - resolve(): Observable { - return this.http.get('/api/1/instance-info').toPromise() - } + resolve (): Observable { + return this.http.get('/api/1/instance-info').toPromise() + } } diff --git a/frontend/src/app.component.ts b/frontend/src/app.component.ts index 5b44570..b9130a5 100644 --- a/frontend/src/app.component.ts +++ b/frontend/src/app.component.ts @@ -1,7 +1,8 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ import { Component } from '@angular/core' @Component({ - selector: 'app', - template: '', + selector: 'app', + template: '', }) export class AppComponent { } diff --git a/frontend/src/app.module.ts b/frontend/src/app.module.ts index d8435d9..bb07ec7 100644 --- a/frontend/src/app.module.ts +++ b/frontend/src/app.module.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ import { NgModule } from '@angular/core' import { BrowserModule } from '@angular/platform-browser' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' @@ -15,38 +16,38 @@ import { CommonAppModule } from 'src/common' import '@fortawesome/fontawesome-svg-core/styles.css' const ROUTES = [ - { - path: '', - loadChildren: () => import(/* webpackChunkName: "homepage" */'./homepage').then(m => m.HomepageModule), - }, - { - path: 'app', - loadChildren: () => import(/* webpackChunkName: "app" */'./app').then(m => m.ApplicationModule), - }, - { - path: 'login', - loadChildren: () => import(/* webpackChunkName: "login" */'./login').then(m => m.LoginModule), - }, + { + path: '', + loadChildren: () => import(/* webpackChunkName: "homepage" */'./homepage').then(m => m.HomepageModule), + }, + { + path: 'app', + loadChildren: () => import(/* webpackChunkName: "app" */'./app').then(m => m.ApplicationModule), + }, + { + path: 'login', + loadChildren: () => import(/* webpackChunkName: "login" */'./login').then(m => m.LoginModule), + }, ] @NgModule({ - imports: [ - BrowserModule.withServerTransition({ - appId: 'tabby', - }), - CommonAppModule.forRoot(), - TransferHttpCacheModule, - BrowserAnimationsModule, - CommonModule, - FormsModule, - FontAwesomeModule, - ClipboardModule, - HttpClientModule, - RouterModule.forRoot(ROUTES), - ], - declarations: [ - AppComponent, - ], - bootstrap: [AppComponent], + imports: [ + BrowserModule.withServerTransition({ + appId: 'tabby', + }), + CommonAppModule.forRoot(), + TransferHttpCacheModule, + BrowserAnimationsModule, + CommonModule, + FormsModule, + FontAwesomeModule, + ClipboardModule, + HttpClientModule, + RouterModule.forRoot(ROUTES), + ], + declarations: [ + AppComponent, + ], + bootstrap: [AppComponent], }) export class AppModule { } diff --git a/frontend/src/app.server.module.ts b/frontend/src/app.server.module.ts index e72ace9..84ab2e0 100644 --- a/frontend/src/app.server.module.ts +++ b/frontend/src/app.server.module.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ import { NgModule } from '@angular/core' import { ServerModule, ServerTransferStateModule } from '@angular/platform-server' import { AppModule } from './app.module' diff --git a/frontend/src/app/components/configModal.component.ts b/frontend/src/app/components/configModal.component.ts index d430c23..f9e6bf5 100644 --- a/frontend/src/app/components/configModal.component.ts +++ b/frontend/src/app/components/configModal.component.ts @@ -6,51 +6,51 @@ import { faCopy, faFile, faPlus, faTrash } from '@fortawesome/free-solid-svg-ico import { Config, Version } from 'src/api' @Component({ - selector: 'config-modal', - templateUrl: './configModal.component.pug', - // styleUrls: ['./settingsModal.component.scss'], + selector: 'config-modal', + templateUrl: './configModal.component.pug', + // styleUrls: ['./settingsModal.component.scss'], }) export class ConfigModalComponent { - _addIcon = faPlus - _copyIcon = faCopy - _deleteIcon = faTrash - _configIcon = faFile + _addIcon = faPlus + _copyIcon = faCopy + _deleteIcon = faTrash + _configIcon = faFile - constructor ( - private modalInstance: NgbActiveModal, - public appConnector: AppConnectorService, - public configService: ConfigService, - ) { - } + constructor ( + private modalInstance: NgbActiveModal, + public appConnector: AppConnectorService, + public configService: ConfigService, + ) { + } - async ngOnInit () { - } + cancel () { + this.modalInstance.dismiss() + } - cancel () { - this.modalInstance.dismiss() - } + async createNewConfig () { + const config = await this.configService.createNewConfig() + await this.configService.selectConfig(config) + this.modalInstance.dismiss() + } - async createNewConfig () { - const config = await this.configService.createNewConfig() - await this.configService.selectConfig(config) - this.modalInstance.dismiss() - } + async selectConfig (config: Config) { + await this.configService.selectConfig(config) + this.modalInstance.dismiss() + } - async selectConfig (config: Config) { - await this.configService.selectConfig(config) - this.modalInstance.dismiss() - } + async selectVersion (version: Version) { + await this.configService.selectVersion(version) + this.modalInstance.dismiss() + } - async selectVersion (version: Version) { - await this.configService.selectVersion(version) - this.modalInstance.dismiss() + async deleteConfig () { + if (!this.configService.activeConfig) { + return } - - async deleteConfig () { - if (confirm('Delete this config? This cannot be undone.')) { - await this.configService.deleteConfig(this.configService.activeConfig) - } - this.configService.selectDefaultConfig() - this.modalInstance.dismiss() + if (confirm('Delete this config? This cannot be undone.')) { + await this.configService.deleteConfig(this.configService.activeConfig) } + this.configService.selectDefaultConfig() + this.modalInstance.dismiss() + } } diff --git a/frontend/src/app/components/connectionList.component.ts b/frontend/src/app/components/connectionList.component.ts index f236d9f..b79a40f 100644 --- a/frontend/src/app/components/connectionList.component.ts +++ b/frontend/src/app/components/connectionList.component.ts @@ -3,18 +3,18 @@ import { AppConnectorService, SocketProxy } from '../services/appConnector.servi import { faCircle, faTimes } from '@fortawesome/free-solid-svg-icons' @Component({ - selector: 'connection-list', - templateUrl: './connectionList.component.pug', + selector: 'connection-list', + templateUrl: './connectionList.component.pug', }) export class ConnectionListComponent { - _circleIcon = faCircle - _closeIcon = faTimes + _circleIcon = faCircle + _closeIcon = faTimes - constructor ( - public appConnector: AppConnectorService, - ) { } + constructor ( + public appConnector: AppConnectorService, + ) { } - closeSocket (socket: SocketProxy) { - socket.close(new Error('Connection closed by user')) - } + closeSocket (socket: SocketProxy) { + socket.close(new Error('Connection closed by user')) + } } diff --git a/frontend/src/app/components/main.component.ts b/frontend/src/app/components/main.component.ts index 2c98b3e..fbf6ea2 100644 --- a/frontend/src/app/components/main.component.ts +++ b/frontend/src/app/components/main.component.ts @@ -12,96 +12,96 @@ import { combineLatest } from 'rxjs' import { Config, Version } from 'src/api' @Component({ - selector: 'main', - templateUrl: './main.component.pug', - styleUrls: ['./main.component.scss'], + selector: 'main', + templateUrl: './main.component.pug', + styleUrls: ['./main.component.scss'], }) export class MainComponent { - _logo = require('../../../assets/logo.svg') - _settingsIcon = faCog - _loginIcon = faSignInAlt - _logoutIcon = faSignOutAlt - _addIcon = faPlus - _configIcon = faFile - _saveIcon = faSave + _logo = require('../../../assets/logo.svg') + _settingsIcon = faCog + _loginIcon = faSignInAlt + _logoutIcon = faSignOutAlt + _addIcon = faPlus + _configIcon = faFile + _saveIcon = faSave - showApp = false + showApp = false - @ViewChild('iframe') iframe: ElementRef + @ViewChild('iframe') iframe: ElementRef - constructor ( - titleService: Title, - public appConnector: AppConnectorService, - private http: HttpClient, - public loginService: LoginService, - private ngbModal: NgbModal, - private config: ConfigService, - ) { - titleService.setTitle('Tabby') - window.addEventListener('message', this.connectorRequestHandler) + constructor ( + titleService: Title, + public appConnector: AppConnectorService, + private http: HttpClient, + public loginService: LoginService, + private ngbModal: NgbModal, + private config: ConfigService, + ) { + titleService.setTitle('Tabby') + 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 => { - if (event.data === 'request-connector') { - this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector - this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') - } + async ngAfterViewInit () { + await this.loginService.ready$.toPromise() + + 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 () { - await this.loginService.ready$.toPromise() + reloadApp (config: Config, version: Version) { + // TODO check config incompatibility + this.unloadApp() + setTimeout(() => { + this.appConnector.setState(config, version) + this.loadApp(config, version) + }) + } - combineLatest( - this.config.activeConfig$, - this.config.activeVersion$ - ).subscribe(([config, version]) => { - if (config && version) { - this.reloadApp(config, version) - } - }) + async openConfig () { + await this.ngbModal.open(ConfigModalComponent).result + } - await this.config.ready$.toPromise() - await this.config.selectDefaultConfig() - } + async openSettings () { + await this.ngbModal.open(SettingsModalComponent).result + } - 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() - } - } - - 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 = '/' - } + async logout () { + await this.http.post('/api/1/auth/logout', null).toPromise() + location.href = '/' + } } diff --git a/frontend/src/app/components/settingsModal.component.ts b/frontend/src/app/components/settingsModal.component.ts index 6522c96..6d7abc4 100644 --- a/frontend/src/app/components/settingsModal.component.ts +++ b/frontend/src/app/components/settingsModal.component.ts @@ -7,36 +7,36 @@ import { faGithub } from '@fortawesome/free-brands-svg-icons' import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons' @Component({ - selector: 'settings-modal', - templateUrl: './settingsModal.component.pug', + selector: 'settings-modal', + templateUrl: './settingsModal.component.pug', }) export class SettingsModalComponent { - user: User - customGatewayEnabled = false - _githubIcon = faGithub - _copyIcon = faCopy - _okIcon = faCheck + user: User + customGatewayEnabled = false + _githubIcon = faGithub + _copyIcon = faCopy + _okIcon = faCheck - constructor ( - public appConnector: AppConnectorService, - public commonService: CommonService, - private modalInstance: NgbActiveModal, - private loginService: LoginService, - ) { - this.user = { ...loginService.user } - this.customGatewayEnabled = !!this.user.custom_connection_gateway + constructor ( + public appConnector: AppConnectorService, + public commonService: CommonService, + private modalInstance: NgbActiveModal, + private loginService: LoginService, + ) { + if (!loginService.user) { + 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 () { - Object.assign(this.loginService.user, this.user) - this.modalInstance.close() - await this.loginService.updateUser() - } - - cancel () { - this.modalInstance.dismiss() - } + cancel () { + this.modalInstance.dismiss() + } } diff --git a/frontend/src/app/components/upgradeModal.component.ts b/frontend/src/app/components/upgradeModal.component.ts index d0289cd..7a9323c 100644 --- a/frontend/src/app/components/upgradeModal.component.ts +++ b/frontend/src/app/components/upgradeModal.component.ts @@ -8,28 +8,28 @@ import { CommonService, LoginService } from 'src/common' import { User } from 'src/api' @Component({ - selector: 'upgrade-modal', - templateUrl: './upgradeModal.component.pug', + selector: 'upgrade-modal', + templateUrl: './upgradeModal.component.pug', }) export class UpgradeModalComponent { - user: User - _githubIcon = faGithub - _loveIcon = faHeart - _giftIcon = faGift - canSkip = false + user: User + _githubIcon = faGithub + _loveIcon = faHeart + _giftIcon = faGift + canSkip = false - constructor ( - public appConnector: AppConnectorService, - public commonService: CommonService, - public loginService: LoginService, - private modalInstance: NgbActiveModal, - ) { - this.canSkip = !window.localStorage['upgrade-modal-skipped'] - } + constructor ( + public appConnector: AppConnectorService, + public commonService: CommonService, + public loginService: LoginService, + private modalInstance: NgbActiveModal, + ) { + this.canSkip = !window.localStorage['upgrade-modal-skipped'] + } - skipOnce () { - window.localStorage['upgrade-modal-skipped'] = true - window.sessionStorage['upgrade-skip-active'] = true - this.modalInstance.close(true) - } + skipOnce () { + window.localStorage['upgrade-modal-skipped'] = true + window.sessionStorage['upgrade-skip-active'] = true + this.modalInstance.close(true) + } } diff --git a/frontend/src/app/index.ts b/frontend/src/app/index.ts index da7c9a5..ffac54f 100644 --- a/frontend/src/app/index.ts +++ b/frontend/src/app/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ import { NgModule } from '@angular/core' import { NgbDropdownModule, NgbModalModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' import { CommonModule } from '@angular/common' @@ -15,33 +16,33 @@ import { InstanceInfoResolver } from 'src/api' import { CommonAppModule } from 'src/common' const ROUTES = [ - { - path: '', - component: MainComponent, - resolve: { - instanceInfo: InstanceInfoResolver, - }, + { + path: '', + component: MainComponent, + resolve: { + instanceInfo: InstanceInfoResolver, }, + }, ] @NgModule({ - imports: [ - CommonAppModule, - CommonModule, - FormsModule, - NgbDropdownModule, - NgbModalModule, - NgbTooltipModule, - ClipboardModule, - FontAwesomeModule, - RouterModule.forChild(ROUTES), - ], - declarations: [ - MainComponent, - ConfigModalComponent, - SettingsModalComponent, - ConnectionListComponent, - UpgradeModalComponent, - ], + imports: [ + CommonAppModule, + CommonModule, + FormsModule, + NgbDropdownModule, + NgbModalModule, + NgbTooltipModule, + ClipboardModule, + FontAwesomeModule, + RouterModule.forChild(ROUTES), + ], + declarations: [ + MainComponent, + ConfigModalComponent, + SettingsModalComponent, + ConnectionListComponent, + UpgradeModalComponent, + ], }) export class ApplicationModule { } diff --git a/frontend/src/app/services/appConnector.service.ts b/frontend/src/app/services/appConnector.service.ts index 4849965..c7842a2 100644 --- a/frontend/src/app/services/appConnector.service.ts +++ b/frontend/src/app/services/appConnector.service.ts @@ -8,217 +8,226 @@ import { UpgradeModalComponent } from '../components/upgradeModal.component' import { Config, Gateway, Version } from 'src/api' import { LoginService, CommonService } from 'src/common' +export interface ServiceMessage { + _: string + [k: string]: any +} + export class SocketProxy { - connect$ = new Subject() - data$ = new Subject() - error$ = new Subject() - close$ = new Subject() + connect$ = new Subject() + data$ = new Subject() + error$ = new Subject() + close$ = new Subject() - url: string - authToken: string - webSocket: WebSocket|null - initialBuffers: any[] = [] - options: { - host: string - port: number + url: string + authToken: string + webSocket: WebSocket|null + initialBuffers: any[] = [] + options: { + host: string + 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 { + 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 - 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) + this.options = options + if (this.loginService.user?.custom_connection_gateway) { + this.url = this.loginService.user.custom_connection_gateway } - - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async connect (options: any): Promise { - if (!this.loginService.user?.is_pro && this.appConnector.sockets.length > this.appConnector.connectionLimit && !window.sessionStorage['upgrade-skip-active']) { - 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.loginService.user?.custom_connection_gateway_token) { + this.authToken = this.loginService.user.custom_connection_gateway_token } - - handleServiceMessage (msg) { - 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) - } + 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 + } } - - sendServiceMessage (msg) { - this.webSocket.send(JSON.stringify(msg)) + try { + this.webSocket = new WebSocket(this.url) + } catch (err) { + this.close(err) + return } - - write (chunk: Buffer): void { - if (!this.webSocket?.readyState) { - this.initialBuffers.push(chunk) - } else { - this.webSocket.send(chunk) - } + this.webSocket.onerror = () => { + this.close(new Error(`Failed to connect to the connection gateway at ${this.url}`)) + return } - - 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() + 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() + } + } + + 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' }) export class AppConnectorService { - private configUpdate = new Subject() - private config: Config - private version: Version - connectionLimit = 3 - sockets: SocketProxy[] = [] + private configUpdate = new Subject() + private config: Config + private version: Version + connectionLimit = 3 + sockets: SocketProxy[] = [] - constructor ( - private injector: Injector, - private http: HttpClient, - private commonService: CommonService, - private zone: NgZone, - private loginService: LoginService, - ) { + constructor ( + private injector: Injector, + private http: HttpClient, + private commonService: CommonService, + private zone: NgZone, + private loginService: LoginService, + ) { - this.configUpdate.pipe(debounceTime(1000)).subscribe(async content => { - if (this.loginService.user) { - const result = await this.http.patch(`/api/1/configs/${this.config.id}`, { content }).toPromise() - Object.assign(this.config, result) - } - }) - } - - setState (config: Config, version: Version) { - this.config = config - this.version = version - } - - async loadConfig (): Promise { - return this.config.content - } - - async saveConfig (content: string): Promise { - this.configUpdate.next(content) - this.config.content = content - } - - getAppVersion (): string { - return this.version.version - } - - getDistURL (): string { - return this.commonService.backendURL + '/app-dist' - } - - getPluginsToLoad (): string[] { - const loadOrder = [ - 'tabby-core', - 'tabby-settings', - 'tabby-terminal', - 'tabby-ssh', - 'tabby-community-color-schemes', - 'tabby-web', - ] - - return [ - ...loadOrder.filter(x => this.version.plugins.includes(x)), - ...this.version.plugins.filter(x => !loadOrder.includes(x)), - ] - } - - createSocket () { - return this.zone.run(() => { - const socket = new SocketProxy(this.injector) - this.sockets.push(socket) - socket.close$.subscribe(() => { - this.sockets = this.sockets.filter(x => x !== socket) - }) - return socket - }) - } - - async chooseConnectionGateway (): Promise { - try { - return await this.http.post('/api/1/gateways/choose', {}).toPromise() - } catch (err){ - if (err.status === 503) { - throw new Error('All connections gateway are unavailable right now') - } - throw err - } + this.configUpdate.pipe(debounceTime(1000)).subscribe(async content => { + if (this.loginService.user) { + const result = await this.http.patch(`/api/1/configs/${this.config.id}`, { content }).toPromise() + Object.assign(this.config, result) + } + }) + } + + setState (config: Config, version: Version): void { + this.config = config + this.version = version + } + + async loadConfig (): Promise { + return this.config.content + } + + async saveConfig (content: string): Promise { + this.configUpdate.next(content) + this.config.content = content + } + + getAppVersion (): string { + return this.version.version + } + + getDistURL (): string { + return this.commonService.backendURL + '/app-dist' + } + + getPluginsToLoad (): string[] { + const loadOrder = [ + 'tabby-core', + 'tabby-settings', + 'tabby-terminal', + 'tabby-ssh', + 'tabby-community-color-schemes', + 'tabby-web', + ] + + return [ + ...loadOrder.filter(x => this.version.plugins.includes(x)), + ...this.version.plugins.filter(x => !loadOrder.includes(x)), + ] + } + + createSocket (): SocketProxy { + return this.zone.run(() => { + const socket = new SocketProxy(this.injector) + this.sockets.push(socket) + socket.close$.subscribe(() => { + this.sockets = this.sockets.filter(x => x !== socket) + }) + return socket + }) + } + + async chooseConnectionGateway (): Promise { + try { + return this.http.post('/api/1/gateways/choose', {}).toPromise() + } catch (err){ + if (err.status === 503) { + throw new Error('All connections gateway are unavailable right now') + } + throw err } + } } diff --git a/frontend/src/common/index.ts b/frontend/src/common/index.ts index ecc54da..1136316 100644 --- a/frontend/src/common/index.ts +++ b/frontend/src/common/index.ts @@ -1,22 +1,23 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ import { ModuleWithProviders, NgModule } from '@angular/core' import { HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http' import { BackendXsrfInterceptor, UniversalInterceptor } from './interceptor' @NgModule({ - imports: [ - HttpClientXsrfModule, - ], + imports: [ + HttpClientXsrfModule, + ], }) export class CommonAppModule { - static forRoot (): ModuleWithProviders { - return { - ngModule: CommonAppModule, - providers: [ - { provide: HTTP_INTERCEPTORS, useClass: UniversalInterceptor, multi: true }, - { provide: HTTP_INTERCEPTORS, useClass: BackendXsrfInterceptor, multi: true }, - ] - } + static forRoot (): ModuleWithProviders { + return { + ngModule: CommonAppModule, + providers: [ + { provide: HTTP_INTERCEPTORS, useClass: UniversalInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: BackendXsrfInterceptor, multi: true }, + ], } + } } export { LoginService } from './services/login.service' diff --git a/frontend/src/common/interceptor.ts b/frontend/src/common/interceptor.ts index 5e6bf65..724cf8d 100644 --- a/frontend/src/common/interceptor.ts +++ b/frontend/src/common/interceptor.ts @@ -5,34 +5,34 @@ import { CommonService } from './services/common.service' @Injectable({ providedIn: 'root' }) export class UniversalInterceptor implements HttpInterceptor { - constructor (private commonService: CommonService) { } + constructor (private commonService: CommonService) { } - intercept (request: HttpRequest, next: HttpHandler): Observable> { - if (!request.url.startsWith('//') && request.url.startsWith('/')) { - const endpoint = request.url - request = request.clone({ - url: `${this.commonService.backendURL}${endpoint}`, - withCredentials: true, - }) - } - return next.handle(request) + intercept (request: HttpRequest, next: HttpHandler): Observable> { + if (!request.url.startsWith('//') && request.url.startsWith('/')) { + const endpoint = request.url + request = request.clone({ + url: `${this.commonService.backendURL}${endpoint}`, + withCredentials: true, + }) } + return next.handle(request) + } } @Injectable({ providedIn: 'root' }) export class BackendXsrfInterceptor implements HttpInterceptor { - constructor ( - private commonService: CommonService, - private tokenExtractor: HttpXsrfTokenExtractor, - ) { } + constructor ( + private commonService: CommonService, + private tokenExtractor: HttpXsrfTokenExtractor, + ) { } - intercept (req: HttpRequest, next: HttpHandler): Observable> { - if (this.commonService.backendURL && req.url.startsWith(this.commonService.backendURL)) { - let token = this.tokenExtractor.getToken() as string; - if (token !== null) { - req = req.clone({ setHeaders: { 'X-XSRF-TOKEN': token } }); - } - } - return next.handle(req); + intercept (req: HttpRequest, next: HttpHandler): Observable> { + if (this.commonService.backendURL && req.url.startsWith(this.commonService.backendURL)) { + const token = this.tokenExtractor.getToken() + if (token !== null) { + req = req.clone({ setHeaders: { 'X-XSRF-TOKEN': token } }) + } } + return next.handle(req) + } } diff --git a/frontend/src/common/services/common.service.ts b/frontend/src/common/services/common.service.ts index d061f94..cb3f01b 100644 --- a/frontend/src/common/services/common.service.ts +++ b/frontend/src/common/services/common.service.ts @@ -2,24 +2,24 @@ import { Inject, Injectable, Optional } from '@angular/core' @Injectable({ providedIn: 'root' }) export class CommonService { - backendURL: string + backendURL: string - constructor (@Inject('BACKEND_URL') @Optional() ssrBackendURL: string) { - const tag = (document.querySelector('meta[property=x-tabby-web-backend-url]') as HTMLMetaElement) - if (ssrBackendURL) { - this.backendURL = ssrBackendURL - tag.content = ssrBackendURL - } else { - if (tag.content && !tag.content.startsWith('{{')) { - this.backendURL = tag.content - } else { - this.backendURL = '' - } - } - - console.log(this.backendURL) - if (this.backendURL.endsWith('/')) { - this.backendURL = this.backendURL.slice(0, -1) - } + constructor (@Inject('BACKEND_URL') @Optional() ssrBackendURL: string) { + const tag = document.querySelector('meta[property=x-tabby-web-backend-url]')! as HTMLMetaElement + if (ssrBackendURL) { + this.backendURL = ssrBackendURL + tag.content = ssrBackendURL + } else { + if (tag.content && !tag.content.startsWith('{{')) { + this.backendURL = tag.content + } else { + this.backendURL = '' + } } + + console.log(this.backendURL) + if (this.backendURL.endsWith('/')) { + this.backendURL = this.backendURL.slice(0, -1) + } + } } diff --git a/frontend/src/common/services/config.service.ts b/frontend/src/common/services/config.service.ts index d34535a..e81de86 100644 --- a/frontend/src/common/services/config.service.ts +++ b/frontend/src/common/services/config.service.ts @@ -8,113 +8,113 @@ import { LoginService } from './login.service' @Injectable({ providedIn: 'root' }) export class ConfigService { - activeConfig$ = new Subject() - activeVersion$ = new Subject() - user: User + activeConfig$ = new Subject() + activeVersion$ = new Subject() + user: User - configs: Config[] = [] - versions: Version[] = [] - ready$ = new AsyncSubject() + configs: Config[] = [] + versions: Version[] = [] + ready$ = new AsyncSubject() - get activeConfig (): Config { return this._activeConfig } - get activeVersion (): Version { return this._activeVersion } + get activeConfig (): Config | null { return this._activeConfig } + get activeVersion (): Version | null { return this._activeVersion } - private _activeConfig: Config|null = null - private _activeVersion: Version|null = null + private _activeConfig: Config|null = null + private _activeVersion: Version|null = null - constructor ( - private http: HttpClient, - private loginService: LoginService, - ) { - this.init() + constructor ( + private http: HttpClient, + private loginService: LoginService, + ) { + this.init() + } + + async updateUser (): Promise { + if (!this.loginService.user) { + return + } + await this.http.put('/api/1/user', this.user).toPromise() + } + + async createNewConfig (): Promise { + 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 { + 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 { + this._activeVersion = version + this.activeVersion$.next(version) + } + + async selectConfig (config: Config): Promise { + 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 () { - if (!this.loginService.user) { - return - } - await this.http.put('/api/1/user', this.user).toPromise() + 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 (): Promise { + 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 { + 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 { - 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 () { - 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() - } + this.ready$.next() + this.ready$.complete() + } } diff --git a/frontend/src/common/services/login.service.ts b/frontend/src/common/services/login.service.ts index 41c15a3..afe3487 100644 --- a/frontend/src/common/services/login.service.ts +++ b/frontend/src/common/services/login.service.ts @@ -6,28 +6,28 @@ import { User } from '../../api' @Injectable({ providedIn: 'root' }) export class LoginService { - user: User | null - ready$ = new AsyncSubject() + user: User | null + ready$ = new AsyncSubject() - constructor (private http: HttpClient) { - this.init() + constructor (private http: HttpClient) { + this.init() + } + + async updateUser (): Promise { + 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 () { - 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 - } - - this.ready$.next() - this.ready$.complete() - } + this.ready$.next() + this.ready$.complete() + } } diff --git a/frontend/src/homepage/components/demoTerminal.component.ts b/frontend/src/homepage/components/demoTerminal.component.ts index b70d9c3..ef1ebf4 100644 --- a/frontend/src/homepage/components/demoTerminal.component.ts +++ b/frontend/src/homepage/components/demoTerminal.component.ts @@ -6,97 +6,97 @@ import { Version } from 'src/api' import { CommonService } from 'src/common' class DemoConnector { - constructor ( - targetWindow: Window, - private commonService: CommonService, - private version: Version, - ) { - targetWindow['tabbyWebDemoDataPath'] = `${this.getDistURL()}/${version.version}/tabby-web-demo/data` - } + constructor ( + targetWindow: Window, + private commonService: CommonService, + private version: Version, + ) { + targetWindow['tabbyWebDemoDataPath'] = `${this.getDistURL()}/${version.version}/tabby-web-demo/data` + } - async loadConfig (): Promise { - return `{ + async loadConfig (): Promise { + return `{ recoverTabs: false, web: { preventAccidentalTabClosure: false, }, }` - } + } - // eslint-disable-next-line @typescript-eslint/no-empty-function - async saveConfig (_content: string): Promise { } + // eslint-disable-next-line @typescript-eslint/no-empty-function + async saveConfig (_content: string): Promise { } - getAppVersion (): string { - return this.version.version - } + getAppVersion (): string { + return this.version.version + } - getDistURL (): string { - return this.commonService.backendURL + '/app-dist' - } + getDistURL (): string { + return this.commonService.backendURL + '/app-dist' + } - getPluginsToLoad (): string[] { - return [ - 'tabby-core', - 'tabby-settings', - 'tabby-terminal', - 'tabby-community-color-schemes', - 'tabby-ssh', - 'tabby-telnet', - 'tabby-web', - 'tabby-web-demo', - ] - } + getPluginsToLoad (): string[] { + return [ + 'tabby-core', + 'tabby-settings', + 'tabby-terminal', + 'tabby-community-color-schemes', + 'tabby-ssh', + 'tabby-telnet', + 'tabby-web', + 'tabby-web-demo', + ] + } - createSocket () { - return new DemoSocketProxy() - } + createSocket () { + return new DemoSocketProxy() + } } export class DemoSocketProxy { - connect$ = new Subject() - data$ = new Subject() - error$ = new Subject() - close$ = new Subject() + connect$ = new Subject() + data$ = new Subject() + error$ = new Subject() + close$ = new Subject() - async connect (options) { - this.error$.next(new Error('This web demo can\'t actually access Internet, but feel free to download the release and try it out!')) - } + 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!')) + } } @Component({ - selector: 'demo-terminal', - template: '', - styleUrls: ['./demoTerminal.component.scss'], + selector: 'demo-terminal', + template: '', + styleUrls: ['./demoTerminal.component.scss'], }) export class DemoTerminalComponent { - @ViewChild('iframe') iframe: ElementRef - connector: DemoConnector + @ViewChild('iframe') iframe: ElementRef + connector: DemoConnector - constructor ( - private http: HttpClient, - private commonService: CommonService, - ) { - window.addEventListener('message', this.connectorRequestHandler) + constructor ( + private http: HttpClient, + private commonService: CommonService, + ) { + 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 - connectorRequestHandler = event => { - if (event.data === 'request-connector') { - this.iframe.nativeElement.contentWindow['__connector__'] = this.connector - this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*') - } - } + async ngAfterViewInit (): Promise { + const versions = await this.http.get('/api/1/versions').toPromise() + 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' + } - async ngAfterViewInit (): Promise { - const versions = await this.http.get('/api/1/versions').toPromise() - 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) - } + ngOnDestroy (): void { + window.removeEventListener('message', this.connectorRequestHandler) + } } diff --git a/frontend/src/homepage/components/home.component.ts b/frontend/src/homepage/components/home.component.ts index 431cfea..13a1c84 100644 --- a/frontend/src/homepage/components/home.component.ts +++ b/frontend/src/homepage/components/home.component.ts @@ -6,60 +6,60 @@ import { InstanceInfo } from 'src/api' @Component({ - selector: 'home', - templateUrl: './home.component.pug', - styleUrls: ['./home.component.scss'], + selector: 'home', + templateUrl: './home.component.pug', + styleUrls: ['./home.component.scss'], }) export class HomeComponent { - githubURL = 'https://github.com/Eugeny/tabby' - releaseURL = `${this.githubURL}/releases/latest` - donationURL = 'https://ko-fi.com/eugeny' + githubURL = 'https://github.com/Eugeny/tabby' + releaseURL = `${this.githubURL}/releases/latest` + donationURL = 'https://ko-fi.com/eugeny' - _logo = require('../../../assets/logo.svg') - _downloadIcon = faDownload - _loginIcon = faSignInAlt - _donateIcon = faCoffee + _logo = require('../../../assets/logo.svg') + _downloadIcon = faDownload + _loginIcon = faSignInAlt + _donateIcon = faCoffee - navLinks = [ - { - title: 'About Tabby', - link: '/' - }, - { - title: 'Features', - link: '/features' - }, - ] + navLinks = [ + { + title: 'About Tabby', + link: '/', + }, + { + title: 'Features', + link: '/features', + }, + ] - instanceInfo: InstanceInfo + instanceInfo: InstanceInfo - background: Waves|undefined + background: Waves|undefined - constructor ( - public route: ActivatedRoute, - public router: Router, - ) { - this.instanceInfo = route.snapshot.data.instanceInfo - if (!this.instanceInfo.homepage_enabled) { - router.navigate(['/app']) - } + constructor ( + public route: ActivatedRoute, + public router: Router, + ) { + this.instanceInfo = route.snapshot.data.instanceInfo + if (!this.instanceInfo.homepage_enabled) { + router.navigate(['/app']) } + } - async ngAfterViewInit (): Promise { - this.background = new Waves({ - el: 'body', - mouseControls: true, - touchControls: true, - gyroControls: false, - minHeight: 200.00, - minWidth: 200.00, - scale: 1.00, - scaleMobile: 1.00, - color: 0x70f - }) - } + async ngAfterViewInit (): Promise { + this.background = new Waves({ + el: 'body', + mouseControls: true, + touchControls: true, + gyroControls: false, + minHeight: 200.00, + minWidth: 200.00, + scale: 1.00, + scaleMobile: 1.00, + color: 0x70f, + }) + } - ngOnDestroy () { - this.background?.destroy() - } + ngOnDestroy () { + this.background?.destroy() + } } diff --git a/frontend/src/homepage/components/homeFeatures.component.ts b/frontend/src/homepage/components/homeFeatures.component.ts index cab2ce0..1013356 100644 --- a/frontend/src/homepage/components/homeFeatures.component.ts +++ b/frontend/src/homepage/components/homeFeatures.component.ts @@ -1,23 +1,23 @@ import { Component } from '@angular/core' @Component({ - selector: 'home-features', - templateUrl: './homeFeatures.component.pug', - styleUrls: ['./homeFeatures.component.scss'], + selector: 'home-features', + templateUrl: './homeFeatures.component.pug', + styleUrls: ['./homeFeatures.component.scss'], }) export class HomeFeaturesComponent { - screenshots = { - progress: require('assets/screenshots/progress.png'), - zmodem: require('assets/screenshots/zmodem.png'), - colors: require('assets/screenshots/colors.png'), - hotkeys: require('assets/screenshots/hotkeys.png'), - ports: require('assets/screenshots/ports.png'), - ssh2: require('assets/screenshots/ssh2.png'), - fonts: require('assets/screenshots/fonts.png'), - history: require('assets/screenshots/history.png'), - paste: require('assets/screenshots/paste.png'), - quake: require('assets/screenshots/quake.png'), - split: require('assets/screenshots/split.png'), - profiles: require('assets/screenshots/profiles.png'), - } + screenshots = { + progress: require('assets/screenshots/progress.png'), + zmodem: require('assets/screenshots/zmodem.png'), + colors: require('assets/screenshots/colors.png'), + hotkeys: require('assets/screenshots/hotkeys.png'), + ports: require('assets/screenshots/ports.png'), + ssh2: require('assets/screenshots/ssh2.png'), + fonts: require('assets/screenshots/fonts.png'), + history: require('assets/screenshots/history.png'), + paste: require('assets/screenshots/paste.png'), + quake: require('assets/screenshots/quake.png'), + split: require('assets/screenshots/split.png'), + profiles: require('assets/screenshots/profiles.png'), + } } diff --git a/frontend/src/homepage/components/homeIndex.component.ts b/frontend/src/homepage/components/homeIndex.component.ts index a384996..5e19833 100644 --- a/frontend/src/homepage/components/homeIndex.component.ts +++ b/frontend/src/homepage/components/homeIndex.component.ts @@ -3,22 +3,22 @@ import { faDownload } from '@fortawesome/free-solid-svg-icons' import { faGithub } from '@fortawesome/free-brands-svg-icons' @Component({ - selector: 'home-index', - templateUrl: './homeIndex.component.pug', - styleUrls: ['./homeIndex.component.scss'], + selector: 'home-index', + templateUrl: './homeIndex.component.pug', + styleUrls: ['./homeIndex.component.scss'], }) export class HomeIndexComponent { - githubURL = 'https://github.com/Eugeny/tabby' - releaseURL = `${this.githubURL}/releases/latest` + githubURL = 'https://github.com/Eugeny/tabby' + releaseURL = `${this.githubURL}/releases/latest` - _downloadIcon = faDownload - _githubIcon = faGithub + _downloadIcon = faDownload + _githubIcon = faGithub - screenshots = { - window: require('assets/screenshots/window.png'), - tabs: require('assets/screenshots/tabs.png'), - ssh: require('assets/screenshots/ssh.png'), - serial: require('assets/screenshots/serial.png'), - win: require('assets/screenshots/win.png'), - } + screenshots = { + window: require('assets/screenshots/window.png'), + tabs: require('assets/screenshots/tabs.png'), + ssh: require('assets/screenshots/ssh.png'), + serial: require('assets/screenshots/serial.png'), + win: require('assets/screenshots/win.png'), + } } diff --git a/frontend/src/homepage/index.ts b/frontend/src/homepage/index.ts index ddc07a7..8c024aa 100644 --- a/frontend/src/homepage/index.ts +++ b/frontend/src/homepage/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ import { NgModule } from '@angular/core' import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap' import { CommonModule } from '@angular/common' @@ -14,40 +15,40 @@ import { InstanceInfoResolver } from 'src/api' import { CommonAppModule } from 'src/common' const ROUTES = [ - { - path: '', - component: HomeComponent, - resolve: { - instanceInfo: InstanceInfoResolver, - }, - children: [ - { - path: '', - component: HomeIndexComponent, - }, - { - path: 'features', - component: HomeFeaturesComponent, - }, - ], + { + path: '', + component: HomeComponent, + resolve: { + instanceInfo: InstanceInfoResolver, }, + children: [ + { + path: '', + component: HomeIndexComponent, + }, + { + path: 'features', + component: HomeFeaturesComponent, + }, + ], + }, ] @NgModule({ - imports: [ - CommonAppModule, - CommonModule, - FormsModule, - NgbNavModule, - FontAwesomeModule, - NgxImageZoomModule, - RouterModule.forChild(ROUTES), - ], - declarations: [ - HomeComponent, - HomeIndexComponent, - HomeFeaturesComponent, - DemoTerminalComponent, - ], + imports: [ + CommonAppModule, + CommonModule, + FormsModule, + NgbNavModule, + FontAwesomeModule, + NgxImageZoomModule, + RouterModule.forChild(ROUTES), + ], + declarations: [ + HomeComponent, + HomeIndexComponent, + HomeFeaturesComponent, + DemoTerminalComponent, + ], }) export class HomepageModule { } diff --git a/frontend/src/homepage/vanta/_base.js b/frontend/src/homepage/vanta/_base.js index 19e016d..149009b 100644 --- a/frontend/src/homepage/vanta/_base.js +++ b/frontend/src/homepage/vanta/_base.js @@ -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 win = typeof window == 'object' -if (win && !window.VANTA) window.VANTA = {} -const VANTA = (win && window.VANTA) || {} +if (win && !window.VANTA) {window.VANTA = {}} +const VANTA = win && window.VANTA || {} VANTA.register = (name, Effect) => { return VANTA[name] = (opts) => new Effect(opts) } 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 = { // enableZoom: false, // userPanSpeed: 3, @@ -33,14 +34,14 @@ import { AxisHelper, js, MOUSE, OrbitControls, Scene, WebGLRenderer } from 'thre // } // Namespace for errors -const error = function() { +const error = function () { Array.prototype.unshift.call(arguments, '[VANTA]') return console.error.apply(this, arguments) } VANTA.VantaBase = class VantaBase { - constructor(userOptions = {}) { - if (!win) return false + constructor (userOptions = {}) { + if (!win) {return false} VANTA.current = this this.windowMouseMoveWrapper = this.windowMouseMoveWrapper.bind(this) this.windowTouchWrapper = this.windowTouchWrapper.bind(this) @@ -49,7 +50,7 @@ VANTA.VantaBase = class VantaBase { this.animationLoop = this.animationLoop.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({ mouseControls: true, touchControls: true, @@ -61,19 +62,19 @@ VANTA.VantaBase = class VantaBase { }, defaultOptions) if (userOptions instanceof HTMLElement || typeof userOptions === 'string') { - userOptions = {el: userOptions} + userOptions = { el: userOptions } } extend(this.options, userOptions) // Set element this.el = this.options.el if (this.el == null) { - error("Instance needs \"el\" param!") + error('Instance needs "el" param!') } else if (!(this.options.el instanceof HTMLElement)) { const selector = this.el this.el = q(selector) if (!this.el) { - error("Cannot find element", selector) + error('Cannot find element', selector) return } } @@ -121,12 +122,12 @@ VANTA.VantaBase = class VantaBase { } } - setOptions(userOptions={}){ + setOptions (userOptions={}){ extend(this.options, userOptions) this.triggerMouseMove() } - prepareEl() { + prepareEl () { let i, child // wrapInner for text nodes, so text nodes can be put into foreground if (typeof Node !== 'undefined' && Node.TEXT_NODE) { @@ -156,27 +157,27 @@ VANTA.VantaBase = class VantaBase { } } - applyCanvasStyles(canvasEl, opts={}){ + applyCanvasStyles (canvasEl, opts={}){ extend(canvasEl.style, { position: 'absolute', zIndex: 0, top: 0, left: 0, - background: '' + background: '', }) extend(canvasEl.style, opts) canvasEl.classList.add('vanta-canvas') } - initThree() { + initThree () { if (!WebGLRenderer) { - console.warn("[VANTA] No THREE defined on window") + console.warn('[VANTA] No THREE defined on window') return } // Set renderer this.renderer = new WebGLRenderer({ alpha: true, - antialias: true + antialias: true, }) this.el.appendChild(this.renderer.domElement) this.applyCanvasStyles(this.renderer.domElement) @@ -187,7 +188,7 @@ VANTA.VantaBase = class VantaBase { this.scene = new Scene() } - getCanvasElement() { + getCanvasElement () { if (this.renderer) { return this.renderer.domElement // js } @@ -196,49 +197,49 @@ VANTA.VantaBase = class VantaBase { } } - getCanvasRect() { + getCanvasRect () { const canvas = this.getCanvasElement() - if (!canvas) return false + if (!canvas) {return false} return canvas.getBoundingClientRect() } - windowMouseMoveWrapper(e){ + windowMouseMoveWrapper (e){ const rect = this.getCanvasRect() - if (!rect) return false + if (!rect) {return false} const x = e.clientX - rect.left const y = e.clientY - rect.top if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) { this.mouseX = x 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() - if (!rect) return false + if (!rect) {return false} if (e.touches.length === 1) { const x = e.touches[0].clientX - rect.left const y = e.touches[0].clientY - rect.top if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) { this.mouseX = x 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() - if (!rect) return false + if (!rect) {return false} const x = Math.round(e.alpha * 2) - rect.left const y = Math.round(e.beta * 2) - rect.top if (x>=0 && y>=0 && x<=rect.width && y<=rect.height) { this.mouseX = x 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 (this.options.mouseEase) { x = this.mouseEaseX @@ -254,10 +255,10 @@ VANTA.VantaBase = class VantaBase { } const xNorm = x / this.width // 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) if (mobileCheck() && 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.height = Math.max(this.el.offsetHeight, this.options.minHeight) } - initMouse() { + initMouse () { // Init mouseX and mouseY - if ((!this.mouseX && !this.mouseY) || - (this.mouseX === this.options.minWidth/2 && this.mouseY === this.options.minHeight/2)) { + if (!this.mouseX && !this.mouseY || + this.mouseX === this.options.minWidth/2 && this.mouseY === this.options.minHeight/2) { this.mouseX = this.width/2 this.mouseY = this.height/2 this.triggerMouseMove(this.mouseX, this.mouseY) } } - resize() { + resize () { this.setSize() if (this.camera) { this.camera.aspect = this.width / this.height - if (typeof this.camera.updateProjectionMatrix === "function") { + if (typeof this.camera.updateProjectionMatrix === 'function') { this.camera.updateProjectionMatrix() } } @@ -289,28 +290,28 @@ VANTA.VantaBase = class VantaBase { this.renderer.setSize(this.width, this.height) 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 elRect = this.el.getBoundingClientRect() - const scrollTop = (window.pageYOffset || + const scrollTop = window.pageYOffset || (document.documentElement || document.body.parentNode || document.body).scrollTop - ) + const offsetTop = elRect.top + scrollTop const minScrollTop = offsetTop - window.innerHeight const maxScrollTop = offsetTop + elHeight return minScrollTop <= scrollTop && scrollTop <= maxScrollTop } - animationLoop() { + animationLoop () { // Step time this.t || (this.t = 0) this.t += 1 // Uniform time this.t2 || (this.t2 = 0) - this.t2 += (this.options.speed || 1) + this.t2 += this.options.speed || 1 if (this.uniforms) { 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 if (this.isOnScreen() || this.options.forceAnimate) { - if (typeof this.onUpdate === "function") { + if (typeof this.onUpdate === 'function') { this.onUpdate() } if (this.scene && this.camera) { @@ -336,8 +337,8 @@ VANTA.VantaBase = class VantaBase { } // if (this.stats) this.stats.update() // if (this.renderStats) this.renderStats.update(this.renderer) - if (this.fps && this.fps.update) this.fps.update() - if (typeof this.afterRender === "function") this.afterRender() + if (this.fps && this.fps.update) {this.fps.update()} + if (typeof this.afterRender === 'function') {this.afterRender()} } return this.req = window.requestAnimationFrame(this.animationLoop) } @@ -350,28 +351,28 @@ VANTA.VantaBase = class VantaBase { // } // } - restart() { + restart () { // Restart the effect without destroying the renderer if (this.scene) { while (this.scene.children.length) { this.scene.remove(this.scene.children[0]) } } - if (typeof this.onRestart === "function") { + if (typeof this.onRestart === 'function') { this.onRestart() } this.init() } - init() { - if (typeof this.onInit === "function") { + init () { + if (typeof this.onInit === 'function') { this.onInit() } // this.setupControls() } - destroy() { - if (typeof this.onDestroy === "function") { + destroy () { + if (typeof this.onDestroy === 'function') { this.onDestroy() } const rm = window.removeEventListener diff --git a/frontend/src/homepage/vanta/vanta.waves.js b/frontend/src/homepage/vanta/vanta.waves.js index 2f670a5..5dac482 100644 --- a/frontend/src/homepage/vanta/vanta.waves.js +++ b/frontend/src/homepage/vanta/vanta.waves.js @@ -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 { rn, ri, sample } from 'vanta/src/helpers.js' -import { Geometry, MeshPhongMaterial, Vector3, Face3, Mesh, AmbientLight, EdgesGeometry, LineBasicMaterial, LineSegments, PerspectiveCamera, PointLight, DoubleSide } from 'three/src/Three' +import { rn, ri } from 'vanta/src/helpers.js' +import { Geometry, MeshPhongMaterial, Vector3, Face3, Mesh, AmbientLight, PerspectiveCamera, PointLight, DoubleSide } from 'three/src/Three' import { FaceColors } from 'three/src/Three.Legacy' const defaultOptions = { @@ -8,49 +11,46 @@ const defaultOptions = { shininess: 30, waveHeight: 15, waveSpeed: 1, - zoom: 1 + zoom: 1, } export class Waves extends VantaBase { - static initClass() { - this.prototype.ww = 100; - this.prototype.hh = 80; - this.prototype.waveNoise = 4; // Choppiness of water - } - constructor(userOptions) { - super(userOptions) + static initClass () { + this.prototype.ww = 100 + this.prototype.hh = 80 + this.prototype.waveNoise = 4 // Choppiness of water } - getMaterial() { + getMaterial () { const options = { color: this.options.color, shininess: this.options.shininess, flatShading: true, vertexColors: FaceColors, // Allow coloring individual faces - side: DoubleSide - }; - return new MeshPhongMaterial(options); + side: DoubleSide, + } + return new MeshPhongMaterial(options) } - onInit() { - let i, j; - const CELLSIZE = 18; - const material = this.getMaterial(); - const geometry = new Geometry(); + onInit () { + let i, j + const CELLSIZE = 18 + const material = this.getMaterial() + const geometry = new Geometry() // Add vertices - this.gg = []; + this.gg = [] for (i=0; i<=this.ww; i++){ - this.gg[i] = []; + this.gg[i] = [] for (j=0; j<=this.hh; j++){ - const id = geometry.vertices.length; + const id = geometry.vertices.length const newVertex = new Vector3( - (i - (this.ww * 0.5)) * CELLSIZE, + (i - this.ww * 0.5) * CELLSIZE, rn(0, this.waveNoise) - 10, - ((this.hh * 0.5) - j) * CELLSIZE - ); - geometry.vertices.push(newVertex); - this.gg[i][j] = id; + (this.hh * 0.5 - j) * CELLSIZE + ) + geometry.vertices.push(newVertex) + this.gg[i][j] = id } } @@ -64,7 +64,7 @@ export class Waves extends VantaBase { const b = this.gg[i][j-1] const c = this.gg[i-1][j] const a = this.gg[i-1][j-1] - if (ri(0,1)) { + if (ri(0, 1)) { face1 = new Face3( a, b, c ) face2 = new Face3( b, c, d ) } else { @@ -75,8 +75,8 @@ export class Waves extends VantaBase { } } - this.plane = new Mesh(geometry, material); - this.scene.add(this.plane); + this.plane = new Mesh(geometry, material) + this.scene.add(this.plane) // WIREFRAME // lightColor = 0x55aaee @@ -88,55 +88,55 @@ export class Waves extends VantaBase { // @scene.add( @wireframe ) // LIGHTS - const ambience = new AmbientLight( 0xffffff, 0.9 ); - this.scene.add(ambience); + const ambience = new AmbientLight( 0xffffff, 0.9 ) + this.scene.add(ambience) - const pointLight = new PointLight( 0xffffff, 0.9 ); - pointLight.position.set(-100,250,-100); - this.scene.add(pointLight); + const pointLight = new PointLight( 0xffffff, 0.9 ) + pointLight.position.set(-100, 250, -100) + this.scene.add(pointLight) // CAMERA this.camera = new PerspectiveCamera( 35, this.width / this.height, - 50, 10000); + 50, 10000) - const xOffset = -10; - const zOffset = -10; - this.cameraPosition = new Vector3( 250+xOffset, 200, 400+zOffset ); - this.cameraTarget = new Vector3( 150+xOffset, -30, 200+zOffset ); - this.camera.position.copy(this.cameraPosition); - this.scene.add(this.camera); + const xOffset = -10 + const zOffset = -10 + this.cameraPosition = new Vector3( 250+xOffset, 200, 400+zOffset ) + this.cameraTarget = new Vector3( 150+xOffset, -30, 200+zOffset ) + this.camera.position.copy(this.cameraPosition) + this.scene.add(this.camera) } - onUpdate() { + onUpdate () { // Update options - let diff; - this.plane.material.color.set(this.options.color); - this.plane.material.shininess = this.options.shininess; - this.camera.ox = this.cameraPosition.x / this.options.zoom; - this.camera.oy = this.cameraPosition.y / this.options.zoom; - this.camera.oz = this.cameraPosition.z / this.options.zoom; + let diff + this.plane.material.color.set(this.options.color) + this.plane.material.shininess = this.options.shininess + this.camera.ox = this.cameraPosition.x / this.options.zoom + this.camera.oy = this.cameraPosition.y / this.options.zoom + this.camera.oz = this.cameraPosition.z / this.options.zoom 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) { - diff = c.tx - c.position.x; - c.position.x += diff * 0.02; + diff = c.tx - c.position.x + c.position.x += diff * 0.02 } if (Math.abs(c.ty - c.position.y) > 0.01) { - diff = c.ty - c.position.y; - c.position.y += diff * 0.02; + diff = c.ty - c.position.y + c.position.y += diff * 0.02 } if (Math.abs(c.tz - c.position.z) > 0.01) { - diff = c.tz - c.position.z; - c.position.z += diff * 0.02; + diff = c.tz - c.position.z + c.position.z += diff * 0.02 } - c.lookAt( this.cameraTarget ); + c.lookAt( this.cameraTarget ) // Fix flickering problems // c.near = Math.max((c.position.y * 0.5) - 20, 1); @@ -144,24 +144,24 @@ export class Waves extends VantaBase { // WAVES 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 - v.oy = v.y; + v.oy = v.y } else { - const s = this.options.waveSpeed; - 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 trochoidDelta = Math.pow(delta + 1, 2) / 4; - v.y = v.oy + (trochoidDelta * this.options.waveHeight); + const s = this.options.waveSpeed + 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 trochoidDelta = Math.pow(delta + 1, 2) / 4 + 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.computeFaceNormals(); - this.plane.geometry.verticesNeedUpdate = true; - this.plane.geometry.normalsNeedUpdate = true; + this.plane.geometry.dynamic = true + this.plane.geometry.computeFaceNormals() + this.plane.geometry.verticesNeedUpdate = true + this.plane.geometry.normalsNeedUpdate = true // @scene.remove( @wireframe ) // geo = new EdgesGeometry(@plane.geometry) @@ -170,21 +170,21 @@ export class Waves extends VantaBase { // @scene.add( @wireframe ) if (this.wireframe) { - this.wireframe.geometry.fromGeometry(this.plane.geometry); - this.wireframe.geometry.computeFaceNormals(); + this.wireframe.geometry.fromGeometry(this.plane.geometry) + this.wireframe.geometry.computeFaceNormals() } } - onMouseMove(x,y) { - const c = this.camera; + onMouseMove (x, y) { + const c = this.camera if (!c.oy) { - c.oy = c.position.y; - c.ox = c.position.x; - c.oz = c.position.z; + c.oy = c.position.y + c.ox = c.position.x + c.oz = c.position.z } - c.tx = c.ox + (((x-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); + c.tx = c.ox + (x-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 } } diff --git a/frontend/src/login/components/login.component.ts b/frontend/src/login/components/login.component.ts index 53106b1..69ad589 100644 --- a/frontend/src/login/components/login.component.ts +++ b/frontend/src/login/components/login.component.ts @@ -4,29 +4,29 @@ import { LoginService, CommonService } from 'src/common' import { faGithub, faGitlab, faGoogle, faMicrosoft } from '@fortawesome/free-brands-svg-icons' @Component({ - selector: 'login', - templateUrl: './login.component.pug', - styleUrls: ['./login.component.scss'], + selector: 'login', + templateUrl: './login.component.pug', + styleUrls: ['./login.component.scss'], }) export class LoginComponent { - loggedIn: any - ready = false + loggedIn: any + ready = false - providers = [ - { name: 'GitHub', icon: faGithub, cls: 'btn-primary', id: 'github' }, - { name: 'GitLab', icon: faGitlab, cls: 'btn-warning', id: 'gitlab' }, - { name: 'Google', icon: faGoogle, cls: 'btn-secondary', id: 'google-oauth2' }, - { name: 'Microsoft', icon: faMicrosoft, cls: 'btn-light', id: 'microsoft-graph' }, - ] + providers = [ + { name: 'GitHub', icon: faGithub, cls: 'btn-primary', id: 'github' }, + { name: 'GitLab', icon: faGitlab, cls: 'btn-warning', id: 'gitlab' }, + { name: 'Google', icon: faGoogle, cls: 'btn-secondary', id: 'google-oauth2' }, + { name: 'Microsoft', icon: faMicrosoft, cls: 'btn-light', id: 'microsoft-graph' }, + ] - constructor ( - private loginService: LoginService, - public commonService: CommonService, - ) { } + constructor ( + private loginService: LoginService, + public commonService: CommonService, + ) { } - async ngOnInit () { - await this.loginService.ready$.toPromise() - this.loggedIn = !!this.loginService.user - this.ready = true - } + async ngOnInit () { + await this.loginService.ready$.toPromise() + this.loggedIn = !!this.loginService.user + this.ready = true + } } diff --git a/frontend/src/login/index.ts b/frontend/src/login/index.ts index d925250..2667b94 100644 --- a/frontend/src/login/index.ts +++ b/frontend/src/login/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ import { NgModule } from '@angular/core' import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap' import { CommonModule } from '@angular/common' @@ -11,27 +12,27 @@ import { InstanceInfoResolver } from 'src/api' import { CommonAppModule } from 'src/common' const ROUTES = [ - { - path: '', - component: LoginComponent, - resolve: { - instanceInfo: InstanceInfoResolver, - }, + { + path: '', + component: LoginComponent, + resolve: { + instanceInfo: InstanceInfoResolver, }, + }, ] @NgModule({ - imports: [ - CommonAppModule, - CommonModule, - FormsModule, - NgbNavModule, - FontAwesomeModule, - NgxImageZoomModule, - RouterModule.forChild(ROUTES), - ], - declarations: [ - LoginComponent, - ], + imports: [ + CommonAppModule, + CommonModule, + FormsModule, + NgbNavModule, + FontAwesomeModule, + NgxImageZoomModule, + RouterModule.forChild(ROUTES), + ], + declarations: [ + LoginComponent, + ], }) export class LoginModule { } diff --git a/frontend/src/server.ts b/frontend/src/server.ts index 1b827b4..8257836 100644 --- a/frontend/src/server.ts +++ b/frontend/src/server.ts @@ -18,63 +18,68 @@ enableProdMode() import { AppServerModule } from './app.server.module' const engine = ngExpressEngine({ - bootstrap: AppServerModule, + bootstrap: AppServerModule, }) const hardlinks = { - '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', - 'terms-of-use': 'https://github.com/Eugeny/tabby/wiki/Terms-of-Use-of-Tabby-Web', + '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', + 'terms-of-use': 'https://github.com/Eugeny/tabby/wiki/Terms-of-Use-of-Tabby-Web', } function start () { - const app = express() + const app = express() - const PORT = process.env.PORT || 8000 - const DIST_FOLDER = join(process.cwd(), 'build') + const PORT = process.env.PORT ?? 8000 + const DIST_FOLDER = join(process.cwd(), 'build') - app.engine('html', engine) + app.engine('html', engine) - app.set('view engine', 'html') - app.set('views', DIST_FOLDER) + app.set('view engine', 'html') + app.set('views', DIST_FOLDER) - app.use('/static', express.static(DIST_FOLDER, { - maxAge: '1y', - })) + app.use('/static', express.static(DIST_FOLDER, { + maxAge: '1y', + })) - app.get(['/', '/app', '/login', '/features'], (req, res) => { - res.render( - 'index', - { - req, - providers: [ - { provide: 'BACKEND_URL', useValue: process.env.BACKEND_URL ?? '' }, - ], - }, - (err: Error, html: string) => { - html = html.replace('{{backendURL}}', process.env.BACKEND_URL ?? '') - res.status(err ? 500 : 200).send(html || err.message) - }, - ) - }) + app.get(['/', '/app', '/login', '/features'], (req, res) => { + res.render( + 'index', + { + req, + providers: [ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + { provide: 'BACKEND_URL', useValue: process.env.BACKEND_URL ?? '' }, + ], + }, + (err?: Error, html?: string) => { + 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) => { - res.sendFile(join(DIST_FOLDER, 'terminal.html')) - }) + app.get(['/terminal'], (req, res) => { + res.sendFile(join(DIST_FOLDER, 'terminal.html')) + }) - for (const [key, value] of Object.entries(hardlinks)) { - app.get(`/go/${key}`, (req, res) => res.redirect(value)) - } + for (const [key, value] of Object.entries(hardlinks)) { + app.get(`/go/${key}`, (req, res) => res.redirect(value)) + } - process.umask(0o002) - app.listen(PORT, () => { - console.log(`Node Express server listening on http://localhost:${PORT}`) - }) + process.umask(0o002) + app.listen(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({ - workers: WORKERS, - lifetime: Infinity, - start, + workers: WORKERS, + lifetime: Infinity, + start, }) diff --git a/frontend/src/ssr-polyfills.ts b/frontend/src/ssr-polyfills.ts index 513fc21..be7ea41 100644 --- a/frontend/src/ssr-polyfills.ts +++ b/frontend/src/ssr-polyfills.ts @@ -1,41 +1,41 @@ -import * as domino from 'domino'; -import * as fs from 'fs'; -import * as path from 'path'; +import * as domino from 'domino' +import * as fs from 'fs' +import * as path from 'path' -const template = fs.readFileSync(path.join(process.cwd(), 'build', 'index.html')).toString(); -const win = domino.createWindow(template); +const template = fs.readFileSync(path.join(process.cwd(), 'build', 'index.html')).toString() +const win = domino.createWindow(template) -global['window'] = win; +global['window'] = win Object.defineProperty(win.document.body.style, 'transform', { value: () => { return { enumerable: true, - configurable: true - }; + configurable: true, + } }, -}); +}) Object.defineProperty(win.document.body.style, 'z-index', { value: () => { - return { - enumerable: true, - configurable: true - }; + return { + enumerable: true, + configurable: true, + } }, -}); +}) -global['document'] = win.document; -global['CSS'] = null; +global['document'] = win.document +global['CSS'] = null // global['atob'] = win.atob; global['atob'] = (base64: string) => { - 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; + return Buffer.from(base64, 'base64').toString() } -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() diff --git a/frontend/src/terminal.ts b/frontend/src/terminal.ts index 20e8e09..de4fe1b 100644 --- a/frontend/src/terminal.ts +++ b/frontend/src/terminal.ts @@ -1,72 +1,72 @@ import './terminal-styles.scss' async function start () { - window['__filename'] = '' + window['__filename'] = '' - await new Promise(resolve => { - window.addEventListener('message', event => { - if (event.data === 'connector-ready') { - resolve() - } - }) - window.parent.postMessage('request-connector', '*') + await new Promise(resolve => { + window.addEventListener('message', event => { + if (event.data === 'connector-ready') { + resolve() + } }) + 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) { - console.log(`Loading ${url}`) - const e = document.createElement('script') - window['module'] = { exports: {} } as any - window['exports'] = window['module'].exports - await new Promise(resolve => { - e.onload = resolve - e.src = url - document.querySelector('head').appendChild(e) - }) - return window['module'].exports - } + async function webRequire (url) { + console.log(`Loading ${url}`) + const e = document.createElement('script') + window['module'] = { exports: {} } as any + window['exports'] = window['module'].exports + await new Promise(resolve => { + e.onload = resolve + e.src = url + document.head.appendChild(e) + }) + return window['module'].exports + } - async function prefetchURL (url) { - await (await fetch(url)).text() - } + async function prefetchURL (url) { + await (await fetch(url)).text() + } - const baseUrl = `${connector.getDistURL()}/${appVersion}` - const coreURLs = [ - `${baseUrl}/tabby-web-container/dist/preload.js`, - `${baseUrl}/tabby-web-container/dist/bundle.js`, - ] + const baseUrl = `${connector.getDistURL()}/${appVersion}` + const coreURLs = [ + `${baseUrl}/tabby-web-container/dist/preload.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) { - await webRequire(url) - } + for (const url of coreURLs) { + 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 pluginModules = await tabby.loadPlugins(pluginURLs, (current, total) => { + const pluginURLs = connector.getPluginsToLoad().map(x => `${baseUrl}/${x}`) + const pluginModules = await tabby.loadPlugins(pluginURLs, (current, total) => { (document.querySelector('.progress .bar') as HTMLElement).style.width = `${100 * current / total}%` // eslint-disable-line - }) + }) - const config = connector.loadConfig() - tabby.bootstrap({ - packageModules: pluginModules, - bootstrapData: { - config, - executable: 'web', - isFirstWindow: true, - windowID: 1, - installedPlugins: [], - userPluginsPath: '/', - }, - debugMode: false, - connector, - }) + const config = connector.loadConfig() + tabby.bootstrap({ + packageModules: pluginModules, + bootstrapData: { + config, + executable: 'web', + isFirstWindow: true, + windowID: 1, + installedPlugins: [], + userPluginsPath: '/', + }, + debugMode: false, + connector, + }) } start() diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index cc168b5..c469460 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -17,6 +17,7 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "declaration": true, + "strictNullChecks": true, "lib": [ "dom", "es5", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 620244b..3a3f8f5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -481,11 +481,16 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57" 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" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818" 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@*": version "16.4.2" 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" integrity sha512-vVjM0SVzgaOUpflq4GYBvCpozes8OgIIS5gVXVka+OfK3hvnkC1i93U8WiY2OtNE4XUWyyy/86Kf6e0IHTQw1Q== -"@typescript-eslint/eslint-plugin@^4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.4.tgz#e73c8cabbf3f08dee0e1bda65ed4e622ae8f8921" - integrity sha512-s1oY4RmYDlWMlcV0kKPBaADn46JirZzvvH7c2CtAqxCY96S538JRBAzt83RrfkDheV/+G/vWNK0zek+8TB3Gmw== +"@typescript-eslint/eslint-plugin@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.1.0.tgz#381c188dfab12f7a2c7b6a8ba2402d6273eadeaa" + integrity sha512-bekODL3Tqf36Yz8u+ilha4zGxL9mdB6LIsIoMAvvC5FAuWo4NpZYXtCbv7B2CeR1LhI/lLtLk+q4tbtxuoVuCg== dependencies: - "@typescript-eslint/experimental-utils" "4.28.4" - "@typescript-eslint/scope-manager" "4.28.4" - debug "^4.3.1" + "@typescript-eslint/experimental-utils" "5.1.0" + "@typescript-eslint/scope-manager" "5.1.0" + debug "^4.3.2" functional-red-black-tree "^1.0.1" - regexpp "^3.1.0" + ignore "^5.1.8" + regexpp "^3.2.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.4.tgz#9c70c35ebed087a5c70fb0ecd90979547b7fec96" - integrity sha512-OglKWOQRWTCoqMSy6pm/kpinEIgdcXYceIcH3EKWUl4S8xhFtN34GQRaAvTIZB9DD94rW7d/U7tUg3SYeDFNHA== +"@typescript-eslint/experimental-utils@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.1.0.tgz#918a1a3d30404cc1f8edcfdf0df200804ef90d31" + integrity sha512-ovE9qUiZMOMgxQAESZsdBT+EXIfx/YUYAbwGUI6V03amFdOOxI9c6kitkgRvLkJaLusgMZ2xBhss+tQ0Y1HWxA== dependencies: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.28.4" - "@typescript-eslint/types" "4.28.4" - "@typescript-eslint/typescript-estree" "4.28.4" + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.1.0" + "@typescript-eslint/types" "5.1.0" + "@typescript-eslint/typescript-estree" "5.1.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@^4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.4.tgz#bc462dc2779afeefdcf49082516afdc3e7b96fab" - integrity sha512-4i0jq3C6n+og7/uCHiE6q5ssw87zVdpUj1k6VlVYMonE3ILdFApEzTWgppSRG4kVNB/5jxnH+gTeKLMNfUelQA== +"@typescript-eslint/parser@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.1.0.tgz#6c7f837d210d2bc0a811e7ea742af414f4e00908" + integrity sha512-vx1P+mhCtYw3+bRHmbalq/VKP2Y3gnzNgxGxfEWc6OFpuEL7iQdAeq11Ke3Rhy8NjgB+AHsIWEwni3e+Y7djKA== dependencies: - "@typescript-eslint/scope-manager" "4.28.4" - "@typescript-eslint/types" "4.28.4" - "@typescript-eslint/typescript-estree" "4.28.4" - debug "^4.3.1" + "@typescript-eslint/scope-manager" "5.1.0" + "@typescript-eslint/types" "5.1.0" + "@typescript-eslint/typescript-estree" "5.1.0" + debug "^4.3.2" -"@typescript-eslint/scope-manager@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.4.tgz#bdbce9b6a644e34f767bd68bc17bb14353b9fe7f" - integrity sha512-ZJBNs4usViOmlyFMt9X9l+X0WAFcDH7EdSArGqpldXu7aeZxDAuAzHiMAeI+JpSefY2INHrXeqnha39FVqXb8w== +"@typescript-eslint/scope-manager@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.1.0.tgz#6f1f26ad66a8f71bbb33b635e74fec43f76b44df" + integrity sha512-yYlyVjvn5lvwCL37i4hPsa1s0ORsjkauhTqbb8MnpvUs7xykmcjGqwlNZ2Q5QpoqkJ1odlM2bqHqJwa28qV6Tw== dependencies: - "@typescript-eslint/types" "4.28.4" - "@typescript-eslint/visitor-keys" "4.28.4" + "@typescript-eslint/types" "5.1.0" + "@typescript-eslint/visitor-keys" "5.1.0" -"@typescript-eslint/types@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.4.tgz#41acbd79b5816b7c0dd7530a43d97d020d3aeb42" - integrity sha512-3eap4QWxGqkYuEmVebUGULMskR6Cuoc/Wii0oSOddleP4EGx1tjLnZQ0ZP33YRoMDCs5O3j56RBV4g14T4jvww== +"@typescript-eslint/types@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.1.0.tgz#a8a75ddfc611660de6be17d3ad950302385607a9" + integrity sha512-sEwNINVxcB4ZgC6Fe6rUyMlvsB2jvVdgxjZEjQUQVlaSPMNamDOwO6/TB98kFt4sYYfNhdhTPBEQqNQZjMMswA== -"@typescript-eslint/typescript-estree@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.4.tgz#252e6863278dc0727244be9e371eb35241c46d00" - integrity sha512-z7d8HK8XvCRyN2SNp+OXC2iZaF+O2BTquGhEYLKLx5k6p0r05ureUtgEfo5f6anLkhCxdHtCf6rPM1p4efHYDQ== +"@typescript-eslint/typescript-estree@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.1.0.tgz#132aea34372df09decda961cb42457433aa6e83d" + integrity sha512-SSz+l9YrIIsW4s0ZqaEfnjl156XQ4VRmJsbA0ZE1XkXrD3cRpzuZSVCyqeCMR3EBjF27IisWakbBDGhGNIOvfQ== dependencies: - "@typescript-eslint/types" "4.28.4" - "@typescript-eslint/visitor-keys" "4.28.4" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" + "@typescript-eslint/types" "5.1.0" + "@typescript-eslint/visitor-keys" "5.1.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.4.tgz#92dacfefccd6751cbb0a964f06683bfd72d0c4d3" - integrity sha512-NIAXAdbz1XdOuzqkJHjNKXKj8QQ4cv5cxR/g0uQhCYf/6//XrmfpaYsM7PnBcNbfvTDLUkqQ5TPNm1sozDdTWg== +"@typescript-eslint/visitor-keys@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.1.0.tgz#e01a01b27eb173092705ae983aa1451bd1842630" + integrity sha512-uqNXepKBg81JVwjuqAxYrXa1Ql/YDzM+8g/pS+TCPxba0wZttl8m5DkrasbfnmJGHs4lQ2jTbcZ5azGhI7kK+w== dependencies: - "@typescript-eslint/types" "4.28.4" - eslint-visitor-keys "^2.0.0" + "@typescript-eslint/types" "5.1.0" + eslint-visitor-keys "^3.0.0" "@webassemblyjs/ast@1.11.1": version "1.11.1" @@ -1508,7 +1514,7 @@ debug@2.6.9: dependencies: 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" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" 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" 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: version "7.31.0" 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: type-fest "^0.20.2" -globby@^11.0.3: +globby@^11.0.4: version "11.0.4" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" 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" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.4: +ignore@^5.1.4, ignore@^5.1.8: version "5.1.8" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" 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: 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: version "7.0.0" 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" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regexpp@^3.1.0: +regexpp@^3.1.0, regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==