1/*
2 * Copyright (C) 2022 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 */
16import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
17import {Component, ElementRef, Inject, Input, ViewChild} from '@angular/core';
18import {MatSelectChange} from '@angular/material/select';
19import {TimestampClickDetail, ViewerEvents} from 'viewers/common/viewer_events';
20import {timeButtonStyle} from 'viewers/components/styles/clickable_property.styles';
21import {currentElementStyle} from 'viewers/components/styles/current_element.styles';
22import {selectedElementStyle} from 'viewers/components/styles/selected_element.styles';
23import {viewerCardStyle} from 'viewers/components/styles/viewer_card.styles';
24import {UiData, UiDataMessage} from './ui_data';
25
26@Component({
27  selector: 'viewer-protolog',
28  template: `
29    <div class="card-grid container">
30      <div class="log-view">
31        <div class="filters">
32          <div class="log-level">
33            <select-with-filter
34              label="Log level"
35              flex="3"
36              [options]="uiData.allLogLevels"
37              outerFilterWidth="225"
38              (selectChange)="onLogLevelsChange($event)">
39            </select-with-filter>
40          </div>
41          <div class="tag">
42            <select-with-filter
43              label="Tags"
44              flex="2"
45              [options]="uiData.allTags"
46              outerFilterWidth="150"
47              innerFilterWidth="150"
48              (selectChange)="onTagsChange($event)">
49            </select-with-filter>
50          </div>
51          <div class="source-file">
52            <select-with-filter
53              label="Source files"
54              flex="4"
55              [options]="uiData.allSourceFiles"
56              outerFilterWidth="300"
57              innerFilterWidth="300"
58              (selectChange)="onSourceFilesChange($event)">
59            </select-with-filter>
60          </div>
61          <div class="text">
62            <mat-form-field appearance="fill" (keydown.enter)="$event.target.blur()">
63              <mat-label>Search text</mat-label>
64              <input matInput name="protologTextInput" [(ngModel)]="searchString" (input)="onSearchStringChange()" />
65            </mat-form-field>
66          </div>
67
68          <button
69            color="primary"
70            mat-stroked-button
71            class="go-to-current-time"
72            (click)="onGoToCurrentTimeClick()">
73            Go to Current Time
74          </button>
75        </div>
76        <cdk-virtual-scroll-viewport
77          protologVirtualScroll
78          class="scroll-messages"
79          [scrollItems]="uiData.messages">
80          <div
81            *cdkVirtualFor="let message of uiData.messages; let i = index"
82            class="message"
83            [attr.item-id]="i"
84            [class.current]="isCurrentMessage(i)"
85            [class.selected]="isSelectedMessage(i)"
86            (click)="onMessageClicked(i)">
87            <div class="time">
88              <button
89                mat-button
90                color="primary"
91                (click)="onTimestampClicked(message)">
92                {{ message.time.formattedValue() }}
93              </button>
94            </div>
95            <div class="log-level">
96              <span class="mat-body-1">{{ message.level }}</span>
97            </div>
98            <div class="tag">
99              <span class="mat-body-1">{{ message.tag }}</span>
100            </div>
101            <div class="source-file">
102              <span class="mat-body-1">{{ message.at }}</span>
103            </div>
104            <div class="text">
105              <span class="mat-body-1">{{ message.text }}</span>
106            </div>
107          </div>
108        </cdk-virtual-scroll-viewport>
109      </div>
110    </div>
111  `,
112  styles: [
113    `
114      .container {
115        display: flex;
116        flex-direction: column;
117      }
118
119      .filters {
120        display: flex;
121        flex-direction: row;
122        margin-top: 16px;
123      }
124
125      .scroll-messages {
126        height: 100%;
127        flex: 1;
128      }
129
130      .message {
131        display: flex;
132        flex-direction: row;
133        overflow-wrap: anywhere;
134      }
135
136      .time {
137        flex: 2;
138      }
139
140      .log-level {
141        flex: 1;
142      }
143
144      .filters .log-level {
145        flex: 3;
146      }
147
148      .tag {
149        flex: 2;
150      }
151
152      .source-file {
153        flex: 4;
154      }
155
156      .text {
157        flex: 10;
158      }
159
160      .filters .text mat-form-field {
161        width: 80%;
162      }
163
164      .go-to-current-time {
165        margin-top: 4px;
166        font-size: 12px;
167        height: 65%;
168        width: fit-content;
169      }
170
171      .filters div {
172        margin: 4px;
173      }
174
175      .message div {
176        margin: 4px;
177      }
178
179      mat-form-field {
180        width: 100%;
181        font-size: 12px;
182      }
183    `,
184    selectedElementStyle,
185    currentElementStyle,
186    timeButtonStyle,
187    viewerCardStyle,
188  ],
189})
190export class ViewerProtologComponent {
191  uiData: UiData = UiData.EMPTY;
192
193  private searchString = '';
194  private lastClicked = '';
195  private lastSelectedMessage: undefined | number;
196
197  @ViewChild(CdkVirtualScrollViewport)
198  scrollComponent?: CdkVirtualScrollViewport;
199
200  constructor(@Inject(ElementRef) private elementRef: ElementRef) {}
201
202  @Input()
203  set inputData(data: UiData) {
204    this.uiData = data;
205    if (
206      this.lastSelectedMessage === undefined &&
207      this.uiData.currentMessageIndex !== undefined &&
208      this.scrollComponent &&
209      this.lastClicked !==
210        this.uiData.messages[
211          this.uiData.currentMessageIndex
212        ].time.formattedValue()
213    ) {
214      this.scrollComponent.scrollToIndex(this.uiData.currentMessageIndex);
215    }
216
217    this.lastSelectedMessage = undefined;
218  }
219
220  onLogLevelsChange(event: MatSelectChange) {
221    this.emitEvent(ViewerEvents.LogLevelsFilterChanged, event.value);
222  }
223
224  onTagsChange(event: MatSelectChange) {
225    this.emitEvent(ViewerEvents.TagsFilterChanged, event.value);
226  }
227
228  onSourceFilesChange(event: MatSelectChange) {
229    this.emitEvent(ViewerEvents.SourceFilesFilterChanged, event.value);
230  }
231
232  onSearchStringChange() {
233    this.emitEvent(ViewerEvents.SearchStringFilterChanged, this.searchString);
234  }
235
236  onGoToCurrentTimeClick() {
237    if (this.uiData.currentMessageIndex !== undefined && this.scrollComponent) {
238      this.scrollComponent.scrollToIndex(this.uiData.currentMessageIndex);
239    }
240  }
241
242  onTimestampClicked(message: UiDataMessage) {
243    this.emitEvent(
244      ViewerEvents.TimestampClick,
245      new TimestampClickDetail(message.time.getValue(), message.traceIndex),
246    );
247  }
248
249  onMessageClicked(index: number) {
250    this.lastSelectedMessage = index;
251    this.emitEvent(ViewerEvents.LogClicked, index);
252  }
253
254  isCurrentMessage(index: number): boolean {
255    return index === this.uiData.currentMessageIndex;
256  }
257
258  isSelectedMessage(index: number): boolean {
259    return index === this.uiData.selectedMessageIndex;
260  }
261
262  private emitEvent(event: string, data: any) {
263    const customEvent = new CustomEvent(event, {
264      bubbles: true,
265      detail: data,
266    });
267    this.elementRef.nativeElement.dispatchEvent(customEvent);
268  }
269}
270