1/* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {NgTemplateOutlet} from '@angular/common'; 18import { 19 Component, 20 ElementRef, 21 EventEmitter, 22 Inject, 23 Input, 24 Output, 25 ViewChild, 26} from '@angular/core'; 27import {FormControl, Validators} from '@angular/forms'; 28import {assertDefined} from 'common/assert_utils'; 29import {Analytics} from 'logging/analytics'; 30 31@Component({ 32 selector: 'active-search', 33 template: ` 34 <span class="header"> 35 <span class="mat-body-2"> {{label}} </span> 36 <button 37 mat-button 38 class="query-button end-align-button clear-button" 39 color="primary" 40 (click)="clearQueryClick.emit()" 41 *ngIf="canClear"> 42 <mat-icon> delete </mat-icon> 43 <span> Clear </span> 44 </button> 45 </span> 46 <mat-form-field appearance="outline" class="query-field padded-field"> 47 <textarea matInput [formControl]="searchQueryControl" (keydown)="onTextAreaKeydown($event)" [readonly]="runningQuery"></textarea> 48 <mat-error *ngIf="searchQueryControl.invalid && searchQueryControl.value">Enter valid SQL query.</mat-error> 49 </mat-form-field> 50 51 <div class="query-actions"> 52 <div *ngIf="runningQuery" class="running-query-message"> 53 <mat-icon class="material-symbols-outlined"> timer </mat-icon> 54 <span class="mat-body-2 message-with-spinner"> 55 <span>Calculating results </span> 56 <mat-spinner [diameter]="20"></mat-spinner> 57 </span> 58 </div> 59 <span *ngIf="lastQueryExecutionTime" class="query-execution-time mat-body-1"> 60 Executed in {{lastQueryExecutionTime}} 61 </span> 62 <button 63 mat-flat-button 64 class="query-button search-button" 65 color="primary" 66 (click)="onSearchQueryClick()" 67 [disabled]="searchQueryDisabled()"> Run Search Query </button> 68 </div> 69 <div class="current-search" *ngIf="executedQuery"> 70 <span class="query"> 71 <span class="mat-body-2"> Last executed: </span> 72 <span class="mat-body-1"> {{executedQuery}} </span> 73 </span> 74 <ng-container 75 *ngIf="!lastTraceFailed" 76 [ngTemplateOutlet]="saveQueryField" 77 [ngTemplateOutletContext]="{query: executedQuery, control: saveQueryNameControl}"></ng-container> 78 </div> 79 <button 80 *ngIf="canAdd" 81 [disabled]="!executedQuery || lastTraceFailed" 82 mat-stroked-button 83 class="query-button add-button" 84 color="primary" 85 (click)="addQueryClick.emit()"> + Add Query </button> 86 `, 87 styles: [ 88 ` 89 .header { 90 justify-content: space-between; 91 display: flex; 92 align-items: center; 93 } 94 .query-field { 95 height: fit-content; 96 } 97 .query-field textarea { 98 height: 300px; 99 } 100 .query-button { 101 width: fit-content; 102 line-height: 24px; 103 padding: 0 10px; 104 } 105 .end-align-button { 106 align-self: end; 107 } 108 .query-actions { 109 display: flex; 110 flex-direction: row; 111 justify-content: end; 112 column-gap: 10px; 113 align-items: center; 114 } 115 .running-query-message { 116 display: flex; 117 flex-direction: row; 118 align-items: center; 119 color: #FF8A00; 120 } 121 .current-search { 122 padding: 10px 0px; 123 } 124 .current-search .query { 125 display: flex; 126 flex-direction: column; 127 } 128 .message-with-spinner { 129 display: flex; 130 flex-direction: row; 131 align-items: center; 132 justify-content: space-between; 133 } 134 `, 135 ], 136}) 137export class ActiveSearchComponent { 138 @Input() canClear = false; 139 @Input() canAdd = false; 140 @Input() isSearchInitialized = false; 141 @Input() lastTraceFailed = false; 142 @Input() executedQuery: string | undefined; 143 @Input() saveQueryField: NgTemplateOutlet | undefined; 144 @Input() label: string | undefined; 145 @Input() lastQueryExecutionTime: string | undefined; 146 @Input() saveQueryNameControl: FormControl | undefined; 147 @Input() runningQuery = false; 148 149 @Output() clearQueryClick = new EventEmitter(); 150 @Output() searchQueryClick = new EventEmitter<string>(); 151 @Output() addQueryClick = new EventEmitter(); 152 153 @ViewChild(HTMLTextAreaElement) textArea: HTMLTextAreaElement | undefined; 154 155 searchQueryControl = new FormControl('', Validators.required); 156 157 constructor( 158 @Inject(ElementRef) readonly elementRef: ElementRef<HTMLElement>, 159 ) {} 160 161 updateText(text: string) { 162 this.searchQueryControl.setValue(text); 163 this.textArea?.focus(); 164 } 165 166 searchQueryDisabled(): boolean { 167 return ( 168 this.searchQueryControl.invalid || 169 this.runningQuery || 170 !this.isSearchInitialized 171 ); 172 } 173 174 onTextAreaKeydown(event: KeyboardEvent) { 175 event.stopPropagation(); 176 if ( 177 event.key === 'Enter' && 178 !event.shiftKey && 179 !this.searchQueryDisabled() 180 ) { 181 event.preventDefault(); 182 this.onSearchQueryClick(); 183 } 184 } 185 186 onSearchQueryClick() { 187 Analytics.TraceSearch.logQueryRequested('new'); 188 this.searchQueryClick.emit(assertDefined(this.searchQueryControl.value)); 189 } 190} 191