如何在Angular项目中基于Angular Material实现类Kibana日志高级搜索栏?
实现Angular版Kibana风格高级日志搜索栏
Kibana的搜索核心是Lucene风格的结构化查询,结合Angular Material组件可以快速搭建出功能匹配的搜索栏,以下是完整实现方案:
核心功能对齐
先明确要覆盖的Kibana搜索核心特性:
- 字段+运算符+值的结构化查询(如
level: ERROR、timestamp > "2024-01-01") - 实时语法解析与错误标记
- 字段/运算符/常用值的下拉联想
- 布尔逻辑(AND/OR/NOT)支持
- 查询模板的保存与加载
基于Angular Material的代码示例
1. 组件模板(search-bar.component.html)
用Material的输入框、芯片、自动完成组件搭建UI:
<mat-card class="search-bar-wrapper"> <mat-form-field appearance="outline" class="w-full"> <mat-label>日志高级搜索</mat-label> <input matInput [(ngModel)]="searchQuery" (input)="parseQuery()" (keydown.enter)="executeSearch()" [matAutocomplete]="auto" > <mat-chip-list #chipList> <mat-chip *ngFor="let token of parsedTokens" [color]="token.valid ? 'primary' : 'warn'" [removable]="true" (removed)="removeToken(token)" > {{ token.display }} <mat-icon matChipRemove>cancel</mat-icon> </mat-chip> </mat-chip-list> </mat-form-field> <!-- 联想下拉面板 --> <mat-autocomplete #auto="matAutocomplete" [displayWith]="displayOption"> <mat-option *ngFor="let option of autocompleteOptions" (onSelectionChange)="insertOption(option)" > {{ option.label }} </mat-option> </mat-autocomplete> <!-- 操作按钮组 --> <div class="search-actions"> <button mat-raised-button color="primary" (click)="executeSearch()">执行搜索</button> <button mat-raised-button (click)="saveQuery()">保存查询</button> <button mat-raised-button (click)="loadSavedQueries()">加载历史</button> </div> </mat-card>
2. 组件逻辑(search-bar.component.ts)
实现语法解析、联想、查询执行核心功能:
import { Component, ViewChild } from '@angular/core'; import { MatAutocomplete } from '@angular/material/autocomplete'; import { MatChipList } from '@angular/material/chips'; interface ParsedToken { display: string; field?: string; operator?: string; value?: string; valid: boolean; } interface AutocompleteOption { label: string; type: 'field' | 'operator' | 'value'; value: string; } @Component({ selector: 'app-log-search-bar', templateUrl: './search-bar.component.html', styleUrls: ['./search-bar.component.scss'] }) export class LogSearchBarComponent { searchQuery = ''; parsedTokens: ParsedToken[] = []; autocompleteOptions: AutocompleteOption[] = []; @ViewChild('auto') autocomplete!: MatAutocomplete; @ViewChild('chipList') chipList!: MatChipList; // 预定义日志元数据(可从后端动态拉取) private availableFields = ['level', 'timestamp', 'service', 'message', 'user_id']; private availableOperators = ['=', '>', '<', '>=', '<=', ':', '~']; private recentValues = ['ERROR', 'INFO', '2024-01-01', 'auth-service']; parseQuery() { this.parsedTokens = []; // 按空格拆分,忽略引号内的空格 const parts = this.searchQuery.split(/\s+(?=(?:[^"]*"[^"]*")*[^"]*$)/); parts.forEach(part => { // 匹配字段+运算符格式 const fieldOpMatch = part.match(/^([\w_]+)([:=><~]+)/); if (fieldOpMatch) { this.parsedTokens.push({ display: part, field: fieldOpMatch[1], operator: fieldOpMatch[2], value: part.slice(fieldOpMatch[0].length), valid: this.availableFields.includes(fieldOpMatch[1]) && this.availableOperators.includes(fieldOpMatch[2]) }); } // 匹配布尔关键字 else if (['AND', 'OR', 'NOT'].includes(part.toUpperCase())) { this.parsedTokens.push({ display: part.toUpperCase(), valid: true }); } // 普通字符串或无效语法 else { this.parsedTokens.push({ display: part, valid: false }); } }); this.updateAutocomplete(); } updateAutocomplete() { const lastSegment = this.searchQuery.split(/\s+/).pop() || ''; this.autocompleteOptions = []; // 联想字段 this.availableFields.forEach(field => { if (field.startsWith(lastSegment.toLowerCase())) { this.autocompleteOptions.push({ label: `${field}:`, type: 'field', value: `${field}: ` }); } }); // 联想运算符(当输入的是字段名时) if (lastSegment.match(/^[\w_]+$/)) { this.availableOperators.forEach(op => { this.autocompleteOptions.push({ label: op, type: 'operator', value: op }); }); } // 联想常用值 this.recentValues.forEach(val => { if (val.toLowerCase().includes(lastSegment.toLowerCase())) { this.autocompleteOptions.push({ label: val, type: 'value', value: val }); } }); } insertOption(option: AutocompleteOption) { const segments = this.searchQuery.split(/\s+/); segments.pop(); this.searchQuery = [...segments, option.value].join(' ').trim() + ' '; this.parseQuery(); } removeToken(token: ParsedToken) { this.searchQuery = this.searchQuery.replace(new RegExp(`\\s*${token.display}\\s*`), ' ').trim(); this.parseQuery(); } executeSearch() { // 将解析后的token转换为后端API可识别的参数 const queryParams = this.parsedTokens .filter(t => t.field) .map(t => ({ field: t.field, operator: t.operator, value: t.value?.replace(/^"|"$/g, '') // 移除引号 })); console.log('执行搜索参数:', queryParams); // 对接日志API:this.logService.search(queryParams).subscribe(...) } saveQuery() { const savedQueries = JSON.parse(localStorage.getItem('saved_log_queries') || '[]'); savedQueries.push({ name: `查询${savedQueries.length + 1}`, query: this.searchQuery, createdAt: new Date().toISOString() }); localStorage.setItem('saved_log_queries', JSON.stringify(savedQueries)); } loadSavedQueries() { const savedQueries = JSON.parse(localStorage.getItem('saved_log_queries') || '[]'); console.log('已保存查询:', savedQueries); // 可扩展为弹出选择框供用户选择加载 } displayOption(option: AutocompleteOption) { return option.label; } }
3. 样式调整(search-bar.component.scss)
贴近Kibana的简洁风格:
.search-bar-wrapper { padding: 16px; max-width: 900px; margin: 24px auto; } .w-full { width: 100%; } .search-actions { margin-top: 16px; display: flex; gap: 8px; justify-content: flex-end; } mat-chip { margin: 4px; font-size: 0.9rem; }
扩展优化方向
- 集成Monaco Editor替换原生输入框,实现专业的Lucene语法高亮
- 对接后端日志元数据接口,动态获取可用字段和热门值
- 添加查询语法实时校验,在输入框内标记错误位置
- 实现时间范围快速选择器(如「最近1小时」「今天」)
- 支持查询模板的导入导出
内容的提问来源于stack exchange,提问作者Saurabh Sharma 20bcs074
相关产品推荐
相关产品推荐

