diff --git a/.docker/lt.dockerfile b/.docker/lt.dockerfile new file mode 100644 index 0000000..44c1b1f --- /dev/null +++ b/.docker/lt.dockerfile @@ -0,0 +1,3 @@ +ARG NODEJS_IMAGE +FROM ${NODEJS_IMAGE} +RUN npm i -g localtunnel@2.0.2 && npm cache clean --force diff --git a/nginx.conf b/.docker/nginx.conf similarity index 100% rename from nginx.conf rename to .docker/nginx.conf diff --git a/Dockerfile b/.docker/web.dockerfile similarity index 79% rename from Dockerfile rename to .docker/web.dockerfile index cd4e7c7..6835e84 100644 --- a/Dockerfile +++ b/.docker/web.dockerfile @@ -1,4 +1,5 @@ -FROM node:20.11.0-slim as builder +ARG NODEJS_IMAGE +FROM ${NODEJS_IMAGE} as builder ENV NODE_ENV=production @@ -20,6 +21,4 @@ RUN npm run build:prod FROM nginx:1.25.4-alpine COPY --from=builder /app/dist/har-viewer/browser /usr/share/nginx/html -COPY nginx.conf /etc/nginx/nginx.conf - -EXPOSE 80 +COPY .docker/nginx.conf /etc/nginx/nginx.conf diff --git a/.env b/.env index 536f20e..d474c3e 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ COMPOSE_PROJECT_NAME=har-viewer +NODEJS_IMAGE=node:20.11.0-slim diff --git a/README.md b/README.md index 20052ab..514b572 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,25 @@ > Возможно будет дорабатываться если ну прям надо ## [Открыть](https://sunriselink.github.io/har-viewer) + +## Запуск в Docker + +Для сборки и запуска проекта выполнить команду: + +```shell +docker compose up -d --build web +``` + +Проект будет доступен по адресу `http://localhost:8080`. + +Если требуется проверить работу Service Worker'а, то надо подключиться к HTTPS туннелю. Для этого требуется запустить еще один контейнер: + +```shell +docker compose up -d --build lt +``` + +Адрес туннеля можно достать из логов командой: + +```shell +docker compose logs lt +``` diff --git a/docker-compose.yml b/docker-compose.yml index 732ee0d..8a7ec32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,18 @@ version: "3" services: web: - build: . + build: + context: . + dockerfile: .docker/web.dockerfile + args: + - NODEJS_IMAGE=${NODEJS_IMAGE} ports: - "8080:80" + + lt: + build: + context: .docker + dockerfile: lt.dockerfile + args: + - NODEJS_IMAGE=${NODEJS_IMAGE} + command: lt --port 80 --local-host web diff --git a/src/app/app.component.html b/src/app/app.component.html index 89ee774..b9e68b9 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,18 +1,3 @@ -
- @if (harLog(); as har) { - - } @else { -
- -
Or drop file here
-
- } -
+ diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 9230ca2..5d4e87f 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,25 +1,3 @@ :host { display: block; - height: 100%; -} - -.upload { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - &__tip { - color: rgba(0, 0, 0, 0.5); - text-align: center; - margin-top: 16px; - } -} - -.drop { - min-height: 100%; -} - -.file-over { - background-color: rgba(0, 0, 0, 0.35); } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 09082f1..ad481cb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,14 +1,6 @@ -import { ChangeDetectionStrategy, Component, inject, Signal, signal } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { BehaviorSubject, Observable, of, switchMap } from 'rxjs'; -import { FileUploaderComponent } from './components/file-uploader/file-uploader.component'; -import { HarViewerComponent } from './components/har-viewer/har-viewer.component'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; import { VersionComponent } from './components/version/version.component'; -import { FileDropZoneDirective } from './directives/file-drop-zone.directive'; -import { HarReaderService } from './services/har-reader.service'; -import { IHAR } from './types/har-log'; -import { Unsafe } from './types/unsafe'; -import { catchAndLogError } from './utils/catch-and-log-error'; @Component({ selector: 'app-root', @@ -16,30 +8,6 @@ import { catchAndLogError } from './utils/catch-and-log-error'; templateUrl: './app.component.html', styleUrl: './app.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [FileDropZoneDirective, FileUploaderComponent, VersionComponent, HarViewerComponent], + imports: [VersionComponent, RouterOutlet], }) -export class AppComponent { - protected readonly fileOver = signal(false); - - protected readonly harFile$ = new BehaviorSubject>(null); - protected readonly harLog = this.createHARSignal(); - - protected readonly harReader = inject(HarReaderService); - - protected loadHAR(file: File): void { - this.harFile$.next(file); - } - - private createHARSignal(): Signal> { - const harLog$ = this.harFile$.pipe(switchMap(file => this.readFile(file))); - return toSignal(harLog$); - } - - private readFile(file: Unsafe): Observable> { - if (!file) { - return of(null); - } - - return this.harReader.readHAR(file).pipe(catchAndLogError()); - } -} +export class AppComponent {} diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 0d586c0..a285371 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,10 +1,26 @@ import { ApplicationConfig, isDevMode } from '@angular/core'; +import { provideRouter, Routes } from '@angular/router'; import { provideServiceWorker } from '@angular/service-worker'; import { environment } from '../environments/environment'; import { APP_ENVIRONMENT_TOKEN } from '../environments/environment.interface'; +import { FileHandlerComponent } from './pages/file-handler/file-handler.component'; +import { HarPageComponent } from './pages/har-page/har-page.component'; + +const routes: Routes = [ + { + path: '', + pathMatch: 'full', + component: HarPageComponent, + }, + { + path: 'file_handler', + component: FileHandlerComponent, + }, +]; export const appConfig: ApplicationConfig = { providers: [ + provideRouter(routes), { provide: APP_ENVIRONMENT_TOKEN, useValue: environment, diff --git a/src/app/components/file-uploader/file-uploader.component.html b/src/app/components/file-uploader/file-uploader.component.html index 348bdfb..64916c8 100644 --- a/src/app/components/file-uploader/file-uploader.component.html +++ b/src/app/components/file-uploader/file-uploader.component.html @@ -1,4 +1,4 @@ diff --git a/src/app/components/file-uploader/file-uploader.component.ts b/src/app/components/file-uploader/file-uploader.component.ts index 0087f42..1f8c804 100644 --- a/src/app/components/file-uploader/file-uploader.component.ts +++ b/src/app/components/file-uploader/file-uploader.component.ts @@ -11,6 +11,9 @@ export class FileUploaderComponent { @Input() public buttonText = 'SELECT FILE'; + @Input() + public accept?: string; + @Output() public fileSelect = new EventEmitter(); diff --git a/src/app/pages/file-handler/file-handler.component.ts b/src/app/pages/file-handler/file-handler.component.ts new file mode 100644 index 0000000..5c690d5 --- /dev/null +++ b/src/app/pages/file-handler/file-handler.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { LaunchQueueService } from '../../services/launch-queue.service'; + +@Component({ + selector: 'app-file-handler', + template: '', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FileHandlerComponent implements OnInit { + private readonly launchQueueService = inject(LaunchQueueService); + private readonly router = inject(Router); + + public ngOnInit(): void { + this.launchQueueService.tryCreateConsumer(); + this.router.navigateByUrl('/', { replaceUrl: true }); + } +} diff --git a/src/app/pages/har-page/har-page.component.html b/src/app/pages/har-page/har-page.component.html new file mode 100644 index 0000000..8ae0927 --- /dev/null +++ b/src/app/pages/har-page/har-page.component.html @@ -0,0 +1,21 @@ +
+ @if (harLog(); as har) { + + } @else { +
+ + +
Or drop file here
+
+ } +
diff --git a/src/app/pages/har-page/har-page.component.scss b/src/app/pages/har-page/har-page.component.scss new file mode 100644 index 0000000..9230ca2 --- /dev/null +++ b/src/app/pages/har-page/har-page.component.scss @@ -0,0 +1,25 @@ +:host { + display: block; + height: 100%; +} + +.upload { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + &__tip { + color: rgba(0, 0, 0, 0.5); + text-align: center; + margin-top: 16px; + } +} + +.drop { + min-height: 100%; +} + +.file-over { + background-color: rgba(0, 0, 0, 0.35); +} diff --git a/src/app/pages/har-page/har-page.component.ts b/src/app/pages/har-page/har-page.component.ts new file mode 100644 index 0000000..f6f1a98 --- /dev/null +++ b/src/app/pages/har-page/har-page.component.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit, Signal, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { BehaviorSubject, Observable, of, switchMap } from 'rxjs'; +import { FileUploaderComponent } from '../../components/file-uploader/file-uploader.component'; +import { HarViewerComponent } from '../../components/har-viewer/har-viewer.component'; +import { FileDropZoneDirective } from '../../directives/file-drop-zone.directive'; +import { HarReaderService } from '../../services/har-reader.service'; +import { LaunchQueueService } from '../../services/launch-queue.service'; +import { IHAR } from '../../types/har-log'; +import { Unsafe } from '../../types/unsafe'; +import { catchAndLogError } from '../../utils/catch-and-log-error'; + +@Component({ + selector: 'app-har-page', + standalone: true, + templateUrl: './har-page.component.html', + styleUrl: './har-page.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FileDropZoneDirective, FileUploaderComponent, HarViewerComponent], +}) +export class HarPageComponent implements OnInit { + protected readonly fileOver = signal(false); + + protected readonly harFile$ = new BehaviorSubject>(null); + protected readonly harLog = this.createHARSignal(); + + private readonly harReader = inject(HarReaderService); + private readonly launchQueueService = inject(LaunchQueueService); + + public ngOnInit(): void { + this.launchQueueService.handleFile().subscribe(file => this.loadHAR(file)); + } + + protected loadHAR(file: File): void { + this.harFile$.next(file); + } + + private createHARSignal(): Signal> { + const harLog$ = this.harFile$.pipe(switchMap(file => this.readFile(file))); + return toSignal(harLog$); + } + + private readFile(file: Unsafe): Observable> { + if (!file) { + return of(null); + } + + return this.harReader.readHAR(file).pipe(catchAndLogError()); + } +} diff --git a/src/app/services/launch-queue.service.ts b/src/app/services/launch-queue.service.ts new file mode 100644 index 0000000..6ac5953 --- /dev/null +++ b/src/app/services/launch-queue.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { EMPTY, Observable, ReplaySubject } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class LaunchQueueService { + private readonly file$ = new ReplaySubject(1); + + private hasConsumer = false; + + public tryCreateConsumer(): void { + if (!this.isSupported()) { + console.warn('LaunchQueue is not supported'); + return; + } + + this.createConsumer(); + } + + public handleFile(): Observable { + return this.hasConsumer ? this.file$.asObservable() : EMPTY; + } + + private isSupported(): boolean { + return !!window.launchQueue; + } + + private createConsumer(): void { + this.hasConsumer = true; + + window.launchQueue.setConsumer(async params => { + if (params.files?.length) { + const file = await params.files[0].getFile(); + + this.file$.next(file); + this.file$.complete(); + } + }); + } +} + +declare global { + interface Window { + launchQueue: { + setConsumer: (consumer: (params: LaunchParams) => void) => void; + }; + } + interface LaunchParams { + readonly files?: FileSystemFileHandle[]; + } +} diff --git a/src/manifest.webmanifest b/src/manifest.webmanifest index 266ecc5..f84cb30 100644 --- a/src/manifest.webmanifest +++ b/src/manifest.webmanifest @@ -6,6 +6,14 @@ "display": "standalone", "scope": "./", "start_url": "./", + "file_handlers": [ + { + "action": "/file_handler", + "accept": { + "application/json": [".har"] + } + } + ], "icons": [ { "src": "assets/icons/icon-72x72.png",