bigcyy commented on code in PR #3900:
URL: https://github.com/apache/hertzbeat/pull/3900#discussion_r2608924242


##########
web-app/src/app/shared/components/sql-editor/sql-editor.component.ts:
##########
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, EventEmitter, forwardRef, Input, OnDestroy, Output } from 
'@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+declare const monaco: any;
+
+export interface SqlValidationError {
+  message: string;
+  startLine: number;
+  startColumn: number;
+  endLine: number;
+  endColumn: number;
+}
+
+export interface LogTableColumn {
+  name: string;
+  type: string;
+  description?: string;
+}
+
+@Component({
+  selector: 'app-sql-editor',
+  templateUrl: './sql-editor.component.html',
+  styleUrls: ['./sql-editor.component.less'],
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      useExisting: forwardRef(() => SqlEditorComponent),
+      multi: true
+    }
+  ]
+})
+export class SqlEditorComponent implements OnDestroy, ControlValueAccessor {
+  @Input() height: string = '120px';
+  @Input() tableName: string = 'hertzbeat_logs';
+
+  @Output() readonly editorInit = new EventEmitter<any>();
+  @Output() readonly validationChange = new 
EventEmitter<SqlValidationError[]>();
+
+  code: string = '';
+  private editorInstance: any;
+  private validationTimeout: any;
+  editorOption: any = {
+    language: 'sql',
+    theme: 'vs',
+    minimap: { enabled: false },
+    lineNumbers: 'on',
+    scrollBeyondLastLine: false,
+    automaticLayout: true,
+    folding: false,
+    wordWrap: 'on',
+    fontSize: 13,
+    tabSize: 2,
+    suggestOnTriggerCharacters: true,
+    quickSuggestions: true,
+    wordBasedSuggestions: false,
+    fixedOverflowWidgets: true,
+    overviewRulerLanes: 0
+  };
+
+  private completionProvider: any;
+  private onChange: (value: string) => void = () => {};
+  private onTouched: () => void = () => {};
+
+  private readonly LOG_TABLE_COLUMNS: LogTableColumn[] = [
+    { name: 'time_unix_nano', type: 'TimestampNanosecond', description: 'Log 
timestamp in nanoseconds' },
+    { name: 'observed_time_unix_nano', type: 'TimestampNanosecond', 
description: 'Observed timestamp in nanoseconds' },
+    { name: 'severity_number', type: 'Int32', description: 'Severity level 
number (1-24)' },
+    { name: 'severity_text', type: 'String', description: 'Severity text 
(DEBUG, INFO, WARN, ERROR, etc.)' },
+    { name: 'body', type: 'Json', description: 'Log message body content' },
+    { name: 'trace_id', type: 'String', description: 'Distributed tracing 
trace ID' },
+    { name: 'span_id', type: 'String', description: 'Distributed tracing span 
ID' },
+    { name: 'trace_flags', type: 'Int32', description: 'Trace flags' },
+    { name: 'attributes', type: 'Json', description: 'Log attributes as JSON' 
},
+    { name: 'resource', type: 'Json', description: 'Resource information as 
JSON' },
+    { name: 'instrumentation_scope', type: 'Json', description: 
'Instrumentation scope information' },
+    { name: 'dropped_attributes_count', type: 'Int32', description: 'Number of 
dropped attributes' }
+  ];
+
+  ngOnDestroy(): void {
+    if (this.completionProvider) {
+      this.completionProvider.dispose();
+    }
+    if (this.validationTimeout) {
+      clearTimeout(this.validationTimeout);
+    }
+  }
+
+  onEditorInit(editor: any): void {
+    this.editorInstance = editor;
+    this.registerCompletionProvider();
+    this.configureSuggestWidget(editor);
+    this.editorInit.emit(editor);
+  }
+
+  private configureSuggestWidget(editor: any): void {
+    if (typeof monaco === 'undefined') {
+      return;
+    }
+    try {
+      editor.updateOptions({
+        suggest: {
+          maxVisibleSuggestions: 8,
+          showStatusBar: false,
+          preview: false
+        }
+      });
+    } catch (e) {
+      console.warn('Failed to configure suggest widget:', e);
+    }
+  }
+
+  writeValue(value: string): void {
+    this.code = value || '';
+  }
+
+  registerOnChange(fn: (value: string) => void): void {
+    this.onChange = fn;
+  }
+
+  registerOnTouched(fn: () => void): void {
+    this.onTouched = fn;
+  }
+
+  onCodeChange(value: string): void {
+    this.code = value;
+    this.onChange(value);
+    this.onTouched();
+    this.validateSqlWithDebounce(value);
+  }
+
+  private validateSqlWithDebounce(sql: string): void {
+    if (this.validationTimeout) {
+      clearTimeout(this.validationTimeout);
+    }
+    this.validationTimeout = setTimeout(() => {
+      const errors = this.validateSql(sql);
+      this.setEditorMarkers(errors);
+      this.validationChange.emit(errors);
+    }, 500);
+  }
+
+  private validateSql(sql: string): SqlValidationError[] {
+    const errors: SqlValidationError[] = [];
+    if (!sql || !sql.trim()) {
+      return errors;
+    }
+
+    const upperSql = sql.toUpperCase().trim();
+    const lines = sql.split('\n');
+
+    if (!upperSql.startsWith('SELECT')) {
+      errors.push({
+        message: 'SQL query must start with SELECT',
+        startLine: 1,
+        startColumn: 1,
+        endLine: 1,
+        endColumn: Math.min(sql.indexOf(' ') + 1 || sql.length, 
lines[0].length + 1)
+      });
+    }
+
+    if (!upperSql.includes('FROM')) {
+      const lastLine = lines.length;
+      const lastLineLength = lines[lastLine - 1].length;
+      errors.push({
+        message: 'SQL query must contain FROM clause',
+        startLine: lastLine,
+        startColumn: 1,
+        endLine: lastLine,
+        endColumn: lastLineLength + 1
+      });
+    }
+
+    if (upperSql.includes('FROM')) {
+      const tablePattern = new RegExp(`FROM\\s+${this.tableName}`, 'i');

Review Comment:
   down



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to