diff --git a/.github/workflows/deploy-andy.yml b/.github/workflows/deploy-andy.yml new file mode 100644 index 000000000..8328b7f5c --- /dev/null +++ b/.github/workflows/deploy-andy.yml @@ -0,0 +1,26 @@ +name: Deploy andy SearchUI + +on: + push: + branches: + - andy/* + +jobs: + deploy: + runs-on: ubuntu-latest + environment: dev-andy + 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/.github/workflows/deploy-andy2.yml b/.github/workflows/deploy-andy2.yml new file mode 100644 index 000000000..6c71389c9 --- /dev/null +++ b/.github/workflows/deploy-andy2.yml @@ -0,0 +1,26 @@ +name: Deploy dev SearchUI + +on: + push: + branches: + - andy2/* + +jobs: + deploy: + runs-on: ubuntu-latest + environment: dev-andy2 + 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/.github/workflows/deploy-greg.yml b/.github/workflows/deploy-greg.yml new file mode 100644 index 000000000..c71807213 --- /dev/null +++ b/.github/workflows/deploy-greg.yml @@ -0,0 +1,26 @@ +name: Deploy greg SearchUI + +on: + push: + branches: + - greg/* + +jobs: + deploy: + runs-on: ubuntu-latest + environment: dev-greg + 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/.github/workflows/deploy-kim.yml b/.github/workflows/deploy-kim.yml new file mode 100644 index 000000000..65d350778 --- /dev/null +++ b/.github/workflows/deploy-kim.yml @@ -0,0 +1,26 @@ +name: Deploy kim SearchUI + +on: + push: + branches: + - kim/* + +jobs: + deploy: + runs-on: ubuntu-latest + environment: dev-kim + 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/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml new file mode 100644 index 000000000..b985af511 --- /dev/null +++ b/.github/workflows/deploy-test.yml @@ -0,0 +1,26 @@ +name: Deploy test SearchUI + +on: + push: + branches: + - test + +jobs: + deploy: + runs-on: ubuntu-latest + environment: test + 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/.github/workflows/deploy-tyler.yml b/.github/workflows/deploy-tyler.yml new file mode 100644 index 000000000..2d1fb9146 --- /dev/null +++ b/.github/workflows/deploy-tyler.yml @@ -0,0 +1,26 @@ +name: Deploy tyler SearchUI + +on: + push: + branches: + - tyler/* + +jobs: + deploy: + runs-on: ubuntu-latest + environment: dev-tyler + 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/.github/workflows/deploy-will.yml b/.github/workflows/deploy-will.yml new file mode 100644 index 000000000..720250b1f --- /dev/null +++ b/.github/workflows/deploy-will.yml @@ -0,0 +1,26 @@ +name: Deploy will SearchUI + +on: + push: + branches: + - will/* + +jobs: + deploy: + runs-on: ubuntu-latest + environment: dev-will + 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/.github/workflows/deploy-yoreley.yml b/.github/workflows/deploy-yoreley.yml new file mode 100644 index 000000000..8b4e325c6 --- /dev/null +++ b/.github/workflows/deploy-yoreley.yml @@ -0,0 +1,26 @@ +name: Deploy yoreley SearchUI + +on: + push: + branches: + - yoreley/* + +jobs: + deploy: + runs-on: ubuntu-latest + environment: dev-yoreley + 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/.github/workflows/search-ui-deploy-composite/action.yml b/.github/workflows/search-ui-deploy-composite/action.yml new file mode 100644 index 000000000..6ed4da3a0 --- /dev/null +++ b/.github/workflows/search-ui-deploy-composite/action.yml @@ -0,0 +1,65 @@ +name: Composite search-ui deploy action + +inputs: + maturity: + required: true + type: string + cdn-id: + required: true + type: string + s3-bucket: + required: true + type: string + aws-account-id: + required: true + type: string + +runs: + using: "composite" + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Configure AWS credentials from Test account + uses: aws-actions/configure-aws-credentials@v3 + with: + role-to-assume: arn:aws:iam::${{ inputs.aws-account-id }}:role/GitHub_Actions_Role_SearchUI_${{ inputs.maturity }} + aws-region: us-east-1 + + - name: Fetch the caller identity + shell: bash + run: | + aws sts get-caller-identity + + - name: Install dependencies + shell: bash + run: | + cp src/app/services/envs/env-${{ inputs.maturity }}.ts src/app/services/env.ts + echo "{\"hash\":\"${{ github.sha }}\"}" > src/assets/commit-hash.json + npm install + + - name: Angular Build + shell: bash + run: | + npm run build + + - name: Deploy to AWS + shell: bash + run: | + cd dist/search-ui + aws s3 sync . "s3://${{ inputs.s3-bucket }}" + aws cloudfront create-invalidation \ + --distribution-id ${{ inputs.cdn-id }} \ + --paths \ + /index.html \ + /manifest.json \ + /ngsw.json \ + /favicon.ico \ + /assets/i18n/* \ + /assets/* \ + /docs/* diff --git a/angular.json b/angular.json index 3aecab225..9a7e6c912 100644 --- a/angular.json +++ b/angular.json @@ -24,7 +24,8 @@ "tsConfig": "src/tsconfig.app.json", "assets": [ "src/favicon.ico", - "src/assets" + "src/assets", + "src/googlec521d4cf42937ace.html" ], "styles": [ "./node_modules/ol/ol.css", diff --git a/build/github-actions-oidc.yml b/build/github-actions-oidc.yml new file mode 100644 index 000000000..3723cbfc2 --- /dev/null +++ b/build/github-actions-oidc.yml @@ -0,0 +1,80 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: GitHub OIDC for when GitHub wants to communicate with AWS. +Resources: + + # This is the bare-bones role. + GitHubActionsRole: + Type: AWS::IAM::Role + Properties: + RoleName: GitHub_Actions_Role_SearchUI_test + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com + Action: sts:AssumeRoleWithWebIdentity + Condition: + StringLike: + 'token.actions.githubusercontent.com:sub': ['repo:asfadmin/Discovery-SearchUI:*'] + StringEqualsIgnoreCase: + 'token.actions.githubusercontent.com:aud': sts.amazonaws.com + Policies: + - PolicyName: OidcSafetyPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: OidcSafeties + Effect: Deny + Action: + - sts:AssumeRole + Resource: "*" + - PolicyName: GitHubActionsDeployPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: AllowS3SyncActions + Effect: Allow + Action: + - s3:DeleteObject + - s3:GetBucketLocation + - s3:GetObject + - 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:::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' + Statement: + - Sid: AllowInvalidations + Effect: Allow + Action: + - cloudfront:CreateInvalidation + Resource: "*" + + + # This is the OIDC provider hookup itself. This tells AWS to delegate authN GitHub + GitHubActionsOidcProvider: + Type: AWS::IAM::OIDCProvider + Properties: + ClientIdList: + - sts.amazonaws.com + ThumbprintList: + - 6938fd4d98bab03faadb97b34396831e3780aea1 + Url: https://token.actions.githubusercontent.com diff --git a/buildspec.yml b/buildspec.yml index b3973eafd..17d627cd3 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -7,7 +7,7 @@ phases: commands: - n 18 - npm set progress=false - - npm install -g @angular/cli@15.2.7 + - npm install -g @angular/cli@17.2.7 pre_build: commands: - cp src/app/services/envs/env-${MATURITY}.ts src/app/services/env.ts diff --git a/package.json b/package.json index 0c0b60bf6..ce3252e04 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "ng build", + "build": "ng build --configuration production", "test": "ng test", "lint": "eslint -c .eslintrc.js --ext .ts src", "e2e": "ng e2e" diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0d932ed0f..1c8d38dd1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -268,6 +268,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { } this.store$.dispatch(new hyp3Store.LoadCosts()); + this.store$.dispatch(new hyp3Store.LoadUser()); } ) ); @@ -530,9 +531,7 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit { }), debounceTime(200), filter(_ => this.searchType !== SearchType.SARVIEWS_EVENTS - && this.searchType !== SearchType.CUSTOM_PRODUCTS - && this.searchType !== SearchType.BASELINE - && this.searchType !== SearchType.SBAS), + && this.searchType !== SearchType.CUSTOM_PRODUCTS), map(params => ({...params, output: 'COUNT'})), tap(_ => this.store$.dispatch(new searchStore.SearchAmountLoading()) diff --git a/src/app/components/header/header-buttons/preferences/preferences.component.html b/src/app/components/header/header-buttons/preferences/preferences.component.html index 26f9e710a..8f924f148 100644 --- a/src/app/components/header/header-buttons/preferences/preferences.component.html +++ b/src/app/components/header/header-buttons/preferences/preferences.component.html @@ -71,16 +71,16 @@ {{ 'DEFAULT_MAP_LAYER' | translate }} - - - {{ mapLayerTypes.SATELLITE | uppercase | translate }} - - - {{ mapLayerTypes.STREET | uppercase | translate }} - - + + + {{ mapLayerTypes.SATELLITE | uppercase | translate }} + + + {{ mapLayerTypes.STREET | uppercase | translate }} + + @@ -120,6 +120,27 @@ refresh + @if(!this.env.isProd) { + + Debug Status + + + NOT_STARTED + + + PENDING + + + APPROVED + + + REJECTED + + + + } diff --git a/src/app/components/header/header-buttons/preferences/preferences.component.ts b/src/app/components/header/header-buttons/preferences/preferences.component.ts index 39b13bf08..5cb15078f 100644 --- a/src/app/components/header/header-buttons/preferences/preferences.component.ts +++ b/src/app/components/header/header-buttons/preferences/preferences.component.ts @@ -2,6 +2,7 @@ import {Component, EventEmitter, OnDestroy, OnInit, Output} from '@angular/core' import { Store } from '@ngrx/store'; import { AppState } from '@store'; import * as userStore from '@store/user'; +import * as hyp3Store from '@store/hyp3'; import { MatDialogRef } from '@angular/material/dialog'; import { @@ -34,6 +35,7 @@ export class PreferencesComponent implements OnInit, OnDestroy { public defaultMaxConcurrentDownloads: number; public defaultProductTypes: ProductType[]; public hyp3BackendUrl: string; + public hyp3DebugStatus: string; public defaultLanguage: string; public maxResults = [250, 1000, 5000]; @@ -63,6 +65,7 @@ export class PreferencesComponent implements OnInit, OnDestroy { private dialogRef: MatDialogRef, private store$: Store, private hyp3: Hyp3Service, + public env: services.EnvironmentService, private themeService: ThemingService, public translate: TranslateService, public language: AsfLanguageService, @@ -186,6 +189,10 @@ export class PreferencesComponent implements OnInit, OnDestroy { this.setTheme(`theme-${this.currentTheme}`); } } + public onDebugStatus(status: models.ApplicationStatus) { + this.hyp3DebugStatus = status; + this.store$.dispatch(new hyp3Store.SetDebugStatus(status)) + } public setTheme(themeName: string) { this.themeService.setTheme(themeName); diff --git a/src/app/components/header/processing-queue/confirmation/confirmation.component.ts b/src/app/components/header/processing-queue/confirmation/confirmation.component.ts index 16771dfee..155a1b47f 100644 --- a/src/app/components/header/processing-queue/confirmation/confirmation.component.ts +++ b/src/app/components/header/processing-queue/confirmation/confirmation.component.ts @@ -1,21 +1,55 @@ import { Component, OnInit, Inject } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@store'; +import { of, from } from 'rxjs'; +import { tap, catchError, concatMap, finalize } from 'rxjs/operators'; + +import * as queueStore from '@store/queue'; +import * as hyp3Store from '@store/hyp3'; +import * as searchStore from '@store/search'; + +import * as models from '@models'; +import * as services from '@services'; + @Component({ selector: 'app-confirmation', templateUrl: './confirmation.component.html', styleUrls: ['./confirmation.component.scss'] }) export class ConfirmationComponent implements OnInit { + public allJobs: models.QueuedHyp3Job[] = []; public jobTypesWithQueued = []; + public processingOptions: models.Hyp3ProcessingOptions; + public projectName: string; + public validateOnly: boolean; + + public isQueueSubmitProcessing = false; + public progress = null; constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data + public hyp3: services.Hyp3Service, + private store$: Store, + private notificationService: services.NotificationService, + @Inject(MAT_DIALOG_DATA) public data: models.ConfirmationDialogData ) { } ngOnInit(): void { - this.jobTypesWithQueued = this.data; + this.jobTypesWithQueued = this.data.jobTypesWithQueued; + this.processingOptions = this.data.processingOptions; + this.validateOnly = this.data.validateOnly; + this.allJobs = this.jobTypesWithQueued.reduce((total, jobs) => { + total = [...total, ...jobs.jobs]; + + return total; + }, []); + this.store$.dispatch(new hyp3Store.SetProcessingProjectName(null)); + + this.store$.select(hyp3Store.getProcessingProjectName).subscribe(name => { + this.projectName = name; + }); } public onToggleJobType(tabQueue): void { @@ -54,6 +88,98 @@ export class ConfirmationComponent implements OnInit { } public onSubmitQueue(): void { - this.dialogRef.close(this.jobTypesWithQueued); + const jobTypesWithQueued = this.jobTypesWithQueued; + + const hyp3JobsBatch = this.hyp3.formatJobs(jobTypesWithQueued, { + projectName: this.projectName, + processingOptions: this.processingOptions + }); + + const batchSize = 20; + const hyp3JobRequestBatches = this.chunk(hyp3JobsBatch, batchSize); + const total = hyp3JobRequestBatches.length; + let current = 0; + + this.isQueueSubmitProcessing = true; + this.progress = null; + + from(hyp3JobRequestBatches).pipe( + concatMap(batch => this.hyp3.submitJobBatch$({ jobs: batch, validate_only: this.validateOnly }).pipe( + catchError(resp => { + if (resp.error) { + if (resp.error.detail === 'No authorization token provided' || resp.error.detail === 'Provided apikey is not valid') { + this.notificationService.error('Your authorization has expired. Please sign in again.', 'Error', { + timeOut: 0, + extendedTimeOut: 0, + closeButton: true, + }); + } else { + this.notificationService.error( resp.error.detail, 'Error', { + timeOut: 0, + extendedTimeOut: 0, + closeButton: true, + }); + } + } + + return of({jobs: null}); + }), + )), + tap(_ => { + current += 1; + this.progress = Math.floor((current / total) * 100); + }), + finalize(() => { + this.progress = null; + this.isQueueSubmitProcessing = false; + + this.store$.dispatch(new hyp3Store.LoadUser()); + let numJobsSubmitted: number + + if (this.allJobs.length !== hyp3JobsBatch.length) { + numJobsSubmitted = Math.abs(hyp3JobsBatch.length - this.allJobs.length); + } else { + numJobsSubmitted = hyp3JobsBatch.length; + } + + const jobText = numJobsSubmitted > 1 ? `${numJobsSubmitted} Jobs` : 'Job'; + + this.notificationService.info(`Click to view Submitted Products.`, `${jobText} Submitted`, { + closeButton: true, + disableTimeOut: true, + }).onTap.subscribe(() => { + const searchType = models.SearchType.CUSTOM_PRODUCTS; + this.store$.dispatch(new searchStore.SetSearchType(searchType)); + }); + + this.dialogRef.close(this.jobTypesWithQueued); + }), + ).subscribe( + (resp: any) => { + if (resp.jobs === null) { + return; + } + + const successfulJobs = resp.jobs.map(job => ({ + granules: job.job_parameters.granules.map(g => ({name: g})), + job_type: models.hyp3JobTypes[job.job_type] + })); + + this.store$.dispatch(new queueStore.RemoveJobs(successfulJobs)); + } + ); + } + + private chunk(arr, chunkSize) { + if (chunkSize <= 0) { + throw new Error('Invalid chunk size'); + } + + const R = []; + for (let i = 0, len = arr.length; i < len; i += chunkSize) { + R.push(arr.slice(i, i + chunkSize)); + } + + return R; } } diff --git a/src/app/components/header/processing-queue/processing-queue-jobs/processing-queue-jobs.component.html b/src/app/components/header/processing-queue/processing-queue-jobs/processing-queue-jobs.component.html index 5167bd16a..bb52b154b 100644 --- a/src/app/components/header/processing-queue/processing-queue-jobs/processing-queue-jobs.component.html +++ b/src/app/components/header/processing-queue/processing-queue-jobs/processing-queue-jobs.component.html @@ -91,7 +91,7 @@
- {{ 'YOUR_JOBS_QUEUE_IS_EMPTY' | translate }}. + {{ 'YOUR_JOBS_QUEUE_IS_EMPTY' | translate }}