diff --git a/.github/workflows/deploy-displacement.yml b/.github/workflows/deploy-displacement.yml new file mode 100644 index 000000000..edf34a59a --- /dev/null +++ b/.github/workflows/deploy-displacement.yml @@ -0,0 +1,26 @@ +name: Deploy Displacement SearchUI + +on: + push: + branches: + - displacement + +jobs: + deploy: + runs-on: ubuntu-latest + environment: displacement + permissions: + id-token: write + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: build + uses: ./.github/workflows/search-ui-deploy-composite + with: + maturity: ${{ vars.MATURITY }} + cdn-id: ${{ vars.CDN_ID }} + s3-bucket: ${{ vars.S3_BUCKET }} + aws-account-id: ${{ secrets.AWS_ACCOUNT_ID }} diff --git a/.gitignore b/.gitignore index f8b7f38f2..d5d09e198 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ # dependencies /node_modules - +bun.lockb # IDEs and editors /.idea .project diff --git a/angular.json b/angular.json index 9a7e6c912..f6864aa14 100644 --- a/angular.json +++ b/angular.json @@ -87,9 +87,14 @@ "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true + }, + "local": { + "sourceMap": true, + "optimization": false, + "extractLicenses": false } }, - "defaultConfiguration": "" + "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", @@ -99,6 +104,9 @@ "configurations": { "production": { "buildTarget": "search-ui:build:production" + }, + "local": { + "buildTarget": "search-ui:build:local" } } }, diff --git a/build/github-actions-oidc.yml b/build/github-actions-oidc.yml index 3723cbfc2..d255ea48c 100644 --- a/build/github-actions-oidc.yml +++ b/build/github-actions-oidc.yml @@ -42,22 +42,10 @@ Resources: - s3:ListBucket - s3:PutObject Resource: - - arn:aws:s3:::asf-search-ui-dev - - arn:aws:s3:::asf-search-ui-dev/* - - arn:aws:s3:::asf-search-ui-test - - arn:aws:s3:::asf-search-ui-test/* + - arn:aws:s3:::asf-search-* + - arn:aws:s3:::asf-search-*/* - arn:aws:s3:::search-ui-custom-deployments - arn:aws:s3:::search-ui-custom-deployments/* - - arn:aws:s3:::asf-search-ui-4 - - arn:aws:s3:::asf-search-ui-4/* - - arn:aws:s3:::asf-search-ui-3 - - arn:aws:s3:::asf-search-ui-3/* - - arn:aws:s3:::asf-search-ui-2 - - arn:aws:s3:::asf-search-ui-2/* - - arn:aws:s3:::asf-search-ui-1 - - arn:aws:s3:::asf-search-ui-1/* - - arn:aws:s3:::asf-search-ui-andy-2 - - arn:aws:s3:::asf-search-ui-andy-2/* - PolicyName: CloudfrontInvalidation PolicyDocument: Version: '2012-10-17' diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1c8d38dd1..97ce40adf 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit, OnDestroy, AfterViewInit, ViewChild, Inject } from '@angular/core'; +import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, Inject, HostListener } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { MatSidenav } from '@angular/material/sidenav'; import { MatIconRegistry } from '@angular/material/icon'; @@ -18,6 +18,7 @@ import { HelpComponent } from '@components/help/help.component'; import { AppState } from '@store'; import * as scenesStore from '@store/scenes'; +import * as chartsStore from '@store/charts'; import * as filterStore from '@store/filters'; import * as searchStore from '@store/search'; import * as uiStore from '@store/ui'; @@ -34,20 +35,20 @@ import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, NativeDateAdapter } fro import { MAT_MOMENT_DATE_FORMATS } from "@angular/material-moment-adapter"; @Component({ - selector : 'app-root', + selector: 'app-root', templateUrl: './app.component.html', - styleUrls : ['./app.component.scss'], + styleUrls: ['./app.component.scss'], providers: [ { provide: DateAdapter, useClass: NativeDateAdapter }, - {provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS}, - {provide: MAT_DATE_LOCALE, useValue: 'en'}, + { provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS }, + { provide: MAT_DATE_LOCALE, useValue: 'en' }, ] }) export class AppComponent implements OnInit, OnDestroy, AfterViewInit { - @ViewChild('sidenav', {static: true}) sidenav: MatSidenav; + @ViewChild('sidenav', { static: true }) sidenav: MatSidenav; private queueStateKey = 'asf-queue-state-v1'; private customProductsQueueStateKey = 'asf-custom-products-queue-state-v2'; @@ -67,6 +68,8 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { public interactionTypes = models.MapInteractionModeType; public searchType: models.SearchType; + public kioskMode = false; + private helpTopic: string | null; private subs = new SubSink(); @@ -91,14 +94,22 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { private mapService: services.MapService, private hyp3Service: services.Hyp3Service, private themeService: services.ThemingService, + private drawService: services.DrawService, public translate: TranslateService, public language: services.AsfLanguageService, public _adapter: DateAdapter, private titleService: Title, + private pointHistoryService: services.PointHistoryService, @Inject(MAT_DATE_LOCALE) public _locale: string, - ) {} - + ) { } + + @HostListener('window:keydown.control./', ['$event']) + handleKeyDown(_event: KeyboardEvent) { + console.log('Toggling kiosk mode. Use "ctrl+/" to re-toggle') + this.store$.dispatch(new searchStore.setSearchKioskMode(!this.kioskMode)) + } public ngOnInit(): void { + console.log('To toggle kiosk mode, use "ctrl+/"') this.store$.dispatch(new hyp3Store.LoadCosts()); this.subs.add( @@ -125,89 +136,92 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { ); this.subs.add( - this.store$.select(uiStore.getHelpDialogTopic).subscribe(topic => { - const previousTopic = this.helpTopic; - this.helpTopic = topic; - - if (!topic || !!previousTopic) { - return; - } - - window.dataLayer = window.dataLayer || []; - window.dataLayer.push({ - 'event': 'open-help', - 'open-help': topic - }); + this.store$.select(searchStore.getKioskMode).subscribe(kioskMode => this.kioskMode = kioskMode) + ) + this.subs.add( + this.store$.select(uiStore.getHelpDialogTopic).subscribe(topic => { + const previousTopic = this.helpTopic; + this.helpTopic = topic; - const ref = this.dialog.open(HelpComponent, { - panelClass: 'help-panel-config', - data: {helpTopic: topic}, - width: '80vw', - height: '80vh', - maxWidth: '100%', - maxHeight: '100%' - }); + if (!topic || !!previousTopic) { + return; + } - ref.afterClosed().subscribe(_ => { - this.store$.dispatch(new uiStore.SetHelpDialogTopic(null)); - }); - }) + window.dataLayer = window.dataLayer || []; + window.dataLayer.push({ + 'event': 'open-help', + 'open-help': topic + }); + + const ref = this.dialog.open(HelpComponent, { + panelClass: 'help-panel-config', + data: { helpTopic: topic }, + width: '80vw', + height: '80vh', + maxWidth: '100%', + maxHeight: '100%' + }); + + ref.afterClosed().subscribe(_ => { + this.store$.dispatch(new uiStore.SetHelpDialogTopic(null)); + }); + }) ); this.subs.add( - this.store$.select(uiStore.getIsDownloadQueueOpen).subscribe(isDownloadQueueOpen => { - if (!isDownloadQueueOpen) { - return; - } - - this.store$.dispatch(new hyp3Store.LoadUser()); - - window.dataLayer = window.dataLayer || []; - window.dataLayer.push({ - 'event': 'open-download-queue', - 'open-download-queue': this.numberQueuedProducts - }); - - const ref = this.dialog.open(QueueComponent, { - id: 'dlQueueDialog', - maxWidth: '100vw', - maxHeight: '100vh' - }); + this.store$.select(uiStore.getIsDownloadQueueOpen).subscribe(isDownloadQueueOpen => { + if (!isDownloadQueueOpen) { + return; + } - this.subs.add( - ref.afterClosed().subscribe( - _ => this.store$.dispatch(new uiStore.SetIsDownloadQueueOpen(null)) - ) - ); - }) + this.store$.dispatch(new hyp3Store.LoadUser()); + + window.dataLayer = window.dataLayer || []; + window.dataLayer.push({ + 'event': 'open-download-queue', + 'open-download-queue': this.numberQueuedProducts + }); + + const ref = this.dialog.open(QueueComponent, { + id: 'dlQueueDialog', + maxWidth: '100vw', + maxHeight: '100vh' + }); + + this.subs.add( + ref.afterClosed().subscribe( + _ => this.store$.dispatch(new uiStore.SetIsDownloadQueueOpen(null)) + ) + ); + }) ); this.subs.add( - this.store$.select(uiStore.getIsOnDemandQueueOpen).subscribe(isOnDemandQueueOpen => { - if (!isOnDemandQueueOpen) { - return; - } - - this.store$.dispatch(new hyp3Store.LoadUser()); - - window.dataLayer = window.dataLayer || []; - window.dataLayer.push({ - 'event': 'open-processing-queue', - 'open-processing-queue': this.queuedCustomProducts.length - }); - - const ref = this.dialog.open(ProcessingQueueComponent, { - id: 'processingQueueDialog', - maxWidth: '100vw', - maxHeight: '100vh' - }); + this.store$.select(uiStore.getIsOnDemandQueueOpen).subscribe(isOnDemandQueueOpen => { + if (!isOnDemandQueueOpen) { + return; + } - this.subs.add( - ref.afterClosed().subscribe( - _ => this.store$.dispatch(new uiStore.SetIsOnDemandQueueOpen(null)) - ) - ); - }) + this.store$.dispatch(new hyp3Store.LoadUser()); + + window.dataLayer = window.dataLayer || []; + window.dataLayer.push({ + 'event': 'open-processing-queue', + 'open-processing-queue': this.queuedCustomProducts.length + }); + + const ref = this.dialog.open(ProcessingQueueComponent, { + id: 'processingQueueDialog', + maxWidth: '100vw', + maxHeight: '100vh' + }); + + this.subs.add( + ref.afterClosed().subscribe( + _ => this.store$.dispatch(new uiStore.SetIsOnDemandQueueOpen(null)) + ) + ); + }) ); this.store$.dispatch(new uiStore.LoadBanners()); @@ -253,8 +267,8 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { this.language.setProfileLanguage(profile.language); this.isAutoTheme = profile.theme === 'System Preferences'; - const presets = Object.entries(profile.defaultFilterPresets).map(([_, val2]) => val2).filter(val2 =>val2 !== '') - if(isDefaultSearch && presets.length > 0) { + const presets = Object.entries(profile.defaultFilterPresets).map(([_, val2]) => val2).filter(val2 => val2 !== '') + if (isDefaultSearch && presets.length > 0) { this.loadDefaultFilters(profile) } }) @@ -270,19 +284,19 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { this.store$.dispatch(new hyp3Store.LoadCosts()); this.store$.dispatch(new hyp3Store.LoadUser()); } -) + ) ); this.subs.add( this.actions$.pipe( - ofType(userStore.UserActionType.SET_PROFILE), - withLatestFrom(this.urlStateService.isDefaultSearch$), - filter(([action, isDefaultSearch]) => { - const hasCustomDefaults = Object.entries(action.payload.defaultFilterPresets).map(([_, val2]) => val2).filter(val2 =>val2 !== '').length > 0 - return isDefaultSearch && hasCustomDefaults; - }), - map(([action, _]) => action.payload.defaultFilterPresets) - ).subscribe( defaultFilters => + ofType(userStore.UserActionType.SET_PROFILE), + withLatestFrom(this.urlStateService.isDefaultSearch$), + filter(([action, isDefaultSearch]) => { + const hasCustomDefaults = Object.entries(action.payload.defaultFilterPresets).map(([_, val2]) => val2).filter(val2 => val2 !== '').length > 0 + return isDefaultSearch && hasCustomDefaults; + }), + map(([action, _]) => action.payload.defaultFilterPresets) + ).subscribe(defaultFilters => this.store$.dispatch(new filterStore.SetDefaultFilters(defaultFilters)) ) ); @@ -359,9 +373,9 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { this.store$.select(queueStore.getQueuedJobs), this.store$.select(hyp3Store.getProcessingOptions)] ).subscribe( - ([jobs, options]) => localStorage.setItem( - this.customProductsQueueStateKey, JSON.stringify({jobs, options}) - ) + ([jobs, options]) => localStorage.setItem( + this.customProductsQueueStateKey, JSON.stringify({ jobs, options }) + ) ) ); @@ -381,7 +395,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { }) ).subscribe( (mode) => { - this.store$.dispatch(new mapStore.SetMapInteractionMode(mode)); + this.store$.dispatch(new mapStore.SetMapInteractionMode(mode)); }) ); @@ -437,6 +451,41 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { this.domSanitizer.bypassSecurityTrustResourceUrl(`../assets/icons/${iconName}.svg`) ); }); + + this.subs.add( + combineLatest( + [ + this.drawService.polygon$, + this.store$.select(searchStore.getSearchType), + this.store$.select(uiStore.getIsResultsMenuOpen), + this.store$.select(scenesStore.getAreResultsLoaded) + ]).pipe( + filter(([_, searchType, _resultsOpen, resultsLoaded]) => { + // TODO: this seems to sometimes not work, sometimes clearing isn't actually setting resultsLoaded to false + return searchType == SearchType.DISPLACEMENT && !resultsLoaded}) + ).subscribe(([polygon, _, __]) => { + if (polygon) { + if (polygon?.getGeometry()?.getType() === 'Point') { + this.store$.dispatch(new searchStore.MakeSearch()); + this.store$.dispatch(new scenesStore.SetResultsLoaded(true)) + } + } + })) + + + this.subs.add(this.store$.select(chartsStore.getTimeseriesChartStates).pipe( + withLatestFrom(this.pointHistoryService.history$) + ).subscribe(([chartStates, history]) => { + if(Object.keys(chartStates).length === history.length) { + let data = [] + + for (const p of history) { + data.push({ point: p.point, seriesNumber: chartStates[p.wkt].seriesNumber, color: chartStates[p.wkt].color }) + } + this.mapService.setDisplacementLayer(data); + } + + })); } public ngAfterViewInit(): void { @@ -450,6 +499,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { } public onClearSearch(): void { + this.pointHistoryService.clear(); this.store$.dispatch(new scenesStore.ClearScenes()); this.store$.dispatch(new scenesStore.SetSelectedSarviewsEvent('')); this.mapService.clearDrawLayer(); @@ -489,7 +539,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { const queueItemsStr = localStorage.getItem(this.customProductsQueueStateKey); if (queueItemsStr) { - const {jobs, options} = JSON.parse(queueItemsStr); + const { jobs, options } = JSON.parse(queueItemsStr); this.store$.dispatch(new queueStore.AddJobs(jobs)); if (this.areOptionsByJobType(options)) { this.store$.dispatch(new hyp3Store.SetProcessingOptions(options)); @@ -522,8 +572,8 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { private updateMaxSearchResults(): void { const checkAmount = this.searchParams$.getlatestParams.pipe( distinctUntilChanged((previous, current) => { - for(let key of Object.keys(previous)) { - if(previous[key] !== current[key]) { + for (let key of Object.keys(previous)) { + if (previous[key] !== current[key]) { return false; } } @@ -531,8 +581,10 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { }), debounceTime(200), filter(_ => this.searchType !== SearchType.SARVIEWS_EVENTS - && this.searchType !== SearchType.CUSTOM_PRODUCTS), - map(params => ({...params, output: 'COUNT'})), + && this.searchType !== SearchType.CUSTOM_PRODUCTS + && this.searchType !== SearchType.DISPLACEMENT + ), + map(params => ({ ...params, output: 'COUNT' })), tap(_ => this.store$.dispatch(new searchStore.SearchAmountLoading()) ), @@ -541,15 +593,15 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { return this.sarviewsService.filteredSarviewsEvents$().pipe(map(events => events.length)); } return this.asfSearchApi.query(params).pipe( - catchError(resp => { - const { error } = resp; - if (!resp.ok || error && error.includes('VALIDATION_ERROR')) { - return of(0); - } - - return of(-1); - }) - ); + catchError(resp => { + const { error } = resp; + if (!resp.ok || error && error.includes('VALIDATION_ERROR')) { + return of(0); + } + + return of(-1); + }) + ); } ), ); @@ -607,7 +659,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { } private errorBanner(): models.Banner { - return { + return { id: 'Error', text: 'ASF is experiencing errors loading data. Please try again later.', name: 'Error', diff --git a/src/app/components/filters-dropdown/filters-dropdown.component.html b/src/app/components/filters-dropdown/filters-dropdown.component.html index 9e17fbc62..45fb71ca7 100644 --- a/src/app/components/filters-dropdown/filters-dropdown.component.html +++ b/src/app/components/filters-dropdown/filters-dropdown.component.html @@ -33,6 +33,10 @@ {{ 'EVENT_SEARCH' | translate }} + + @if(selectedSearchType === searchTypes.DISPLACEMENT) { + {{ 'DISPLACEMENT_SEARCH' | translate }} + } @@ -62,6 +66,9 @@ + @if(selectedSearchType === searchTypes.DISPLACEMENT) { + + }