1/* 2 * Copyright (C) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { BaseElement, element } from '../../base-ui/BaseElement.js'; 17import { queryCustomizeSelect, querySelectTraceStats } from '../database/SqlLite.js'; 18import { LitTable } from '../../base-ui/table/lit-table.js'; 19import '../../base-ui/table/lit-table.js'; 20import { LitTableColumn } from '../../base-ui/table/lit-table-column.js'; 21import { info } from '../../log/Log.js'; 22import { LitProgressBar } from '../../base-ui/progress-bar/LitProgressBar.js'; 23import { PageNation } from '../../base-ui/chart/pagenation/PageNation.js'; 24import { PaginationBox } from '../../base-ui/chart/pagenation/pagination-box.js'; 25import { SpStatisticsHttpUtil } from '../../statistics/util/SpStatisticsHttpUtil.js'; 26 27@element('sp-query-sql') 28export class SpQuerySQL extends BaseElement { 29 private queryTableEl: LitTable | undefined; 30 private queryText: string | undefined; 31 private resultText: string | undefined; 32 private notSupportList: Array<string> | undefined; 33 private querySize: HTMLElement | undefined; 34 private keyList: Array<string> | undefined; 35 private selector: HTMLTextAreaElement | undefined; 36 private isSupportSql: boolean = true; 37 private querySelectTables: string = ''; 38 private response: HTMLDivElement | undefined; 39 private statDataArray: any = []; 40 private sliceData: any = []; 41 private querySqlErrorText: string = ''; 42 private progressLoad: LitProgressBar | undefined; 43 private pagination: PaginationBox | undefined; 44 private pageSize: number = 200000; 45 private maxPageSize: number = 500000; 46 47 initElements(): void { 48 this.progressLoad = this.shadowRoot?.querySelector('.load-query-sql') as LitProgressBar; 49 this.selector = this.shadowRoot?.querySelector('.sql-select') as HTMLTextAreaElement; 50 this.queryTableEl = new LitTable(); 51 this.querySize = this.shadowRoot?.querySelector('.query_size') as HTMLElement; 52 this.response = this.shadowRoot?.querySelector('#dataResult') as HTMLDivElement; 53 this.pagination = this.shadowRoot?.querySelector('.pagination-box') as PaginationBox; 54 this.notSupportList?.push('insert', 'delete', 'update', 'drop', 'alter', 'truncate', 'create'); 55 let htmlDivElement = this.queryTableEl.shadowRoot?.querySelector('.table') as HTMLDivElement; 56 htmlDivElement.style.overflowX = 'scroll'; 57 58 window.addEventListener('resize', () => { 59 this.freshTableHeadResizeStyle(); 60 }); 61 62 let copyButtonEl = this.shadowRoot?.querySelector('#copy-button') as HTMLButtonElement; 63 copyButtonEl.addEventListener('click', () => { 64 this.copyTableData(); 65 }); 66 67 let closeButtonEl = this.shadowRoot?.querySelector('#close-button') as HTMLButtonElement; 68 closeButtonEl.addEventListener('click', () => { 69 this.querySize!.textContent = 'Query result - 0 counts'; 70 this.queryTableEl!.dataSource = []; 71 this.response!.innerHTML = ''; 72 }); 73 } 74 75 freshTableHeadResizeStyle() { 76 let th = this.queryTableEl!.shadowRoot?.querySelector<HTMLDivElement>('.th'); 77 if (th) { 78 let td = th.querySelectorAll<HTMLDivElement>('.td'); 79 let firstChild = this.queryTableEl!.shadowRoot?.querySelector<HTMLDivElement>('.body')!.firstElementChild; 80 if (firstChild) { 81 let bodyList = firstChild.querySelectorAll<HTMLDivElement>('.td'); 82 for (let index = 0; index < bodyList.length; index++) { 83 td[index].style.width = bodyList[index].offsetWidth + 'px'; 84 td[index].style.overflow = 'hidden'; 85 } 86 } 87 } 88 } 89 90 async copyTableData() { 91 let copyResult = ''; 92 for (let keyListKey of this.keyList!) { 93 copyResult += keyListKey + '\t'; 94 } 95 copyResult += '\n'; 96 let copyData = []; 97 if (this.statDataArray.length > this.maxPageSize) { 98 copyData = this.sliceData; 99 } else { 100 copyData = this.statDataArray; 101 } 102 for (const value of copyData) { 103 this.keyList?.forEach((key) => { 104 copyResult += value[key] + '\t'; 105 }); 106 copyResult += '\n'; 107 } 108 await navigator.clipboard.writeText(copyResult); 109 } 110 111 selectEventListener = (event: KeyboardEvent) => { 112 let that = this; 113 if (event.ctrlKey && event.keyCode == 13) { 114 SpStatisticsHttpUtil.addOrdinaryVisitAction({ 115 event: 'query', 116 action: 'query', 117 }); 118 if (!this.isSupportSql) { 119 this.querySize!.textContent = this.querySqlErrorText; 120 this.queryTableEl!.dataSource = []; 121 this.response!.innerHTML = ''; 122 return; 123 } 124 this.progressLoad!.loading = true; 125 let startData = new Date().getTime(); 126 this.getInputSqlResult(this.selector!.value).then((resultList) => { 127 let dur = new Date().getTime() - startData; 128 this.statDataArray = []; 129 this.keyList = []; 130 for (let index = 0; index < resultList.length; index++) { 131 const dataResult = resultList[index]; 132 let keys = Object.keys(dataResult); 133 // @ts-ignore 134 let values = Object.values(dataResult); 135 let jsonText = '{'; 136 for (let keyIndex = 0; keyIndex < keys.length; keyIndex++) { 137 let key = keys[keyIndex]; 138 if (this.keyList.indexOf(key) <= -1) { 139 this.keyList.push(key); 140 } 141 let value = values[keyIndex]; 142 if (typeof value == 'string') { 143 value = value.replace(/</gi, '<').replace(/>/gi, '>'); 144 } 145 jsonText += '"' + key + '"' + ': ' + '"' + value + '"'; 146 if (keyIndex != keys.length - 1) { 147 jsonText += ','; 148 } else { 149 jsonText += '}'; 150 } 151 } 152 this.statDataArray.push(JSON.parse(jsonText)); 153 } 154 155 this.queryTableEl!.innerHTML = ''; 156 this.queryText = this.selector!.value; 157 this.initDataElement(); 158 this.response!.appendChild(this.queryTableEl!); 159 setTimeout(() => { 160 let total = this.statDataArray.length; 161 if (total > this.maxPageSize) { 162 that.pagination!.style.opacity = '1'; 163 new PageNation(this.pagination, { 164 current: 1, 165 total: total, 166 pageSize: this.pageSize, 167 change(num: number) { 168 that.sliceData = that.statDataArray!.slice((num - 1) * that.pageSize, num * that.pageSize); 169 that.queryTableEl!.recycleDataSource = that.sliceData; 170 }, 171 }); 172 } else { 173 that.pagination!.style.opacity = '0'; 174 this.queryTableEl!.recycleDataSource = this.statDataArray; 175 } 176 177 this.freshTableHeadResizeStyle(); 178 new ResizeObserver(() => { 179 if (this.parentElement?.clientHeight != 0) { 180 this.queryTableEl!.style.height = '100%'; 181 this.queryTableEl!.reMeauseHeight(); 182 } 183 }).observe(this.parentElement!); 184 info('metric query Sql result Data size is: ', this.statDataArray!.length); 185 this.initData(); 186 this.progressLoad!.loading = false; 187 }, 200); 188 }); 189 } 190 }; 191 192 reset() { 193 this.pagination!.style.opacity = '0'; 194 this.response!.innerHTML = ''; 195 this.keyList = []; 196 this.statDataArray = []; 197 this.selector!.value = ''; 198 this.querySize!.textContent = 'Query result - ' + ' 0 counts'; 199 this.resizeSqlHeight().then(() => {}); 200 } 201 202 initDataTableStyle(styleTable: HTMLDivElement): void { 203 for (let index = 0; index < styleTable.children.length; index++) { 204 // @ts-ignore 205 styleTable.children[index].style.backgroundColor = 'var(--dark-background5,#F6F6F6)'; 206 } 207 } 208 209 async initMetricData(): Promise<any> { 210 if (!this.selector || this.selector.value == null) { 211 return []; 212 } 213 if (this.queryText == '' || this.queryText == null) { 214 let statList = await querySelectTraceStats(); 215 for (let index = 0; index < statList.length; index++) { 216 const statsResult = statList[index]; 217 let indexArray = { 218 event_name: statsResult.event_name, 219 start_type: statsResult.stat_type, 220 count: statsResult.count, 221 serverity: statsResult.serverity, 222 source: statsResult.source, 223 }; 224 } 225 if (this.querySize) { 226 this.querySize!.textContent = 'Query result - ' + statList.length + ' counts'; 227 } 228 this.resultText = 'Query result - ' + statList.length + ' counts'; 229 } else { 230 return this.statDataArray; 231 } 232 } 233 234 checkSupportSqlAbility(): boolean { 235 let noSupportChart = ['insert', 'delete', 'update', 'drop', 'alter', 'truncate', 'create']; 236 let result = noSupportChart.filter((item) => { 237 return this.selector!.value.indexOf(item) > -1; 238 }); 239 if (result.length > 0) { 240 this.querySqlErrorText = 241 'Error: Statement contains a change action keyword,The change operation is not supported.'; 242 this.isSupportSql = false; 243 return true; 244 } else { 245 return false; 246 } 247 } 248 249 checkSafetySelectSql(): boolean { 250 let split = this.selector?.value.trim().split(' '); 251 if (split) { 252 this.querySqlErrorText = 'Error: Incomplete query statement: ' + this.selector!.value; 253 this.isSupportSql = false; 254 return !split[0].toLowerCase().startsWith('select'); 255 } 256 return false; 257 } 258 259 getSelectSqlField(): string { 260 if (this.selector!.value.indexOf('from') < 0) { 261 return ''; 262 } 263 let splitSql = this.selector?.value.split('from'); 264 if (splitSql) { 265 if (splitSql[0].indexOf('*') > -1) { 266 return '*'; 267 } else { 268 let fields = splitSql[0].split(','); 269 return fields[0]; 270 } 271 } 272 return ''; 273 } 274 275 getSelectSqlTableName(str: string): Array<string> { 276 if (this.selector!.value.indexOf(str) < 0) { 277 return []; 278 } 279 let tableNameList = []; 280 let splitSql = this.selector?.value.split(str); 281 if (splitSql) { 282 for (let index = 1; index < splitSql?.length; index++) { 283 let splitSqlItem = splitSql[index].trim(); 284 let tableItem = splitSqlItem.split(' '); 285 let tableName = tableItem[0].trim(); 286 tableNameList.push(tableName); 287 if (tableName.indexOf('(') >= 0) { 288 tableNameList.pop(); 289 } else if (tableName.indexOf(')') >= 0) { 290 tableNameList.pop(); 291 let unitTableName = tableName.split(')'); 292 let tableNewName = unitTableName[0]; 293 tableNameList.push(tableNewName); 294 } 295 } 296 } 297 return tableNameList; 298 } 299 300 initDataElement() { 301 if (this.keyList) { 302 info('Metric query Table Colum size is: ', this.keyList.length); 303 this.keyList.forEach((item) => { 304 let htmlElement = document.createElement('lit-table-column') as LitTableColumn; 305 htmlElement.setAttribute('title', item); 306 htmlElement.setAttribute('data-index', item); 307 htmlElement.setAttribute('key', item); 308 htmlElement.setAttribute('align', 'flex-start'); 309 htmlElement.setAttribute('height', '32px'); 310 this.queryTableEl!.appendChild(htmlElement); 311 }); 312 } 313 } 314 315 connectedCallback() { 316 let selectQuery = this.shadowRoot?.querySelector('.query_select'); 317 if (selectQuery) { 318 let querySql = selectQuery.textContent; 319 } 320 // Listen to the sql execution of the query 321 this.addEventListener('keydown', this.selectEventListener); 322 this.selector!.addEventListener('input', this.inputSqlListener); 323 this.selector!.addEventListener('change', this.inputSqlListener); 324 this.selector!.addEventListener('keydown', this.deleteSqlListener); 325 } 326 327 deleteSqlListener = (event: KeyboardEvent) => { 328 if (event.key == 'Backspace') { 329 this.resizeSqlHeight().then(() => {}); 330 } 331 }; 332 333 async resizeSqlHeight() { 334 let valueLength = this.selector?.value.split('\n').length; 335 let rowNumber = Number(valueLength) - 1; 336 let selectHeight = '3.2em'; 337 if (rowNumber > 0) { 338 if (rowNumber <= 10) { 339 let allLength = 1.2 * rowNumber + 2; 340 selectHeight = allLength + 'em'; 341 } else { 342 selectHeight = '14em'; 343 } 344 } 345 // @ts-ignore 346 this.selector?.style.height = selectHeight; 347 } 348 349 inputSqlListener = async (event: Event) => { 350 this.resizeSqlHeight().then(() => {}); 351 let startData = new Date().getTime(); 352 if (this.selector!.value.trim() == '') { 353 this.querySqlErrorText = 'Please enter a query'; 354 this.querySize!.textContent = this.querySqlErrorText; 355 return; 356 } 357 this.checkSafetySelectSql(); 358 this.checkSupportSqlAbility(); 359 if (this.selector!.value.length < 15) { 360 return; 361 } 362 this.querySelectTables = this.getSelectSqlTableName('from') 363 .concat(this.getSelectSqlTableName('join')) 364 .toLocaleString(); 365 info('metric query sql table size is: ', this.querySelectTables.length); 366 this.isSupportSql = true; 367 }; 368 369 async getInputSqlResult(sql: string): Promise<any> { 370 return await queryCustomizeSelect(sql); 371 } 372 373 disconnectedCallback() { 374 this.removeEventListener('keydown', this.selectEventListener); 375 this.selector!.removeEventListener('input', this.inputSqlListener); 376 this.selector!.removeEventListener('change', this.inputSqlListener); 377 this.selector!.removeEventListener('keydown', this.deleteSqlListener); 378 } 379 380 initData() { 381 if (this.statDataArray.length > 0) { 382 this.querySize!.textContent = 'Error: ' + this.selector?.value; 383 } 384 if (this.isSupportSql) { 385 let sqlField = this.keyList?.length == 0 ? '*' : this.keyList?.toLocaleString(); 386 this.querySize!.textContent = 'Query result - ' + this.statDataArray.length + ' counts'; 387 } else { 388 this.querySize!.textContent = this.querySqlErrorText; 389 } 390 391 let queryHeadStyle: HTMLDivElement | undefined | null = this.queryTableEl?.shadowRoot?.querySelector( 392 'div.th' 393 ) as HTMLDivElement; 394 if (queryHeadStyle && queryHeadStyle.hasChildNodes()) { 395 for (let index = 0; index < queryHeadStyle.children.length; index++) { 396 // @ts-ignore 397 queryHeadStyle.children[index].style.gridArea = null; 398 } 399 } 400 401 this.queryTableEl!.style.height = '100%'; 402 } 403 404 static get observedAttributes() { 405 return ['queryStr']; 406 } 407 408 attributeChangedCallback(name: string, oldValue: string, newValue: string) { 409 let queryDataSty: HTMLDivElement | undefined | null = this.queryTableEl?.shadowRoot?.querySelector( 410 'div.tbody' 411 ) as HTMLDivElement; 412 if (queryDataSty && queryDataSty.hasChildNodes()) { 413 for (let index = 0; index < queryDataSty.children.length; index++) { 414 // @ts-ignore 415 queryDataSty.children[index].style.backgroundColor = 'var(--dark-background5,#F6F6F6)'; 416 } 417 } 418 } 419 420 private _queryStr?: string; 421 422 get queryStr(): string { 423 return this.queryStr; 424 } 425 426 set queryStr(value: string) { 427 this._queryStr = value; 428 } 429 430 initHtml(): string { 431 return ` 432 <style> 433 :host{ 434 width: 100%; 435 height: 100%; 436 font-size: 16px; 437 background-color: var(--dark-background5,#F6F6F6); 438 margin: 0; 439 padding: 0; 440 } 441 442 .sql-select{ 443 box-sizing: border-box; 444 width: 95%; 445 font-family: Helvetica,serif; 446 font-size: inherit; 447 color: var(--dark-color1,#212121); 448 text-align: left; 449 line-height: 1.2em; 450 font-weight: 400; 451 height: 3.2em; 452 margin-left: 10px; 453 resize: vertical; 454 border-width: 2px; 455 } 456 457 .query{ 458 display: flex; 459 flex-direction: column; 460 background-color: var(--dark-background5,#F6F6F6); 461 position: absolute; 462 top: 0; 463 bottom: 0; 464 left: 0; 465 right: 0; 466 } 467 468 .query-message{ 469 background-color: var(--dark-background3,#FFFFFF); 470 padding: 1% 2%; 471 margin: 2% 2.5% 0 2.5%; 472 border-radius: 16px; 473 width: 90%; 474 } 475 476 .request{ 477 display: flex; 478 flex-direction: column; 479 position: relative; 480 } 481 482 .response{ 483 flex-grow: 1; 484 margin-bottom: 1%; 485 display: flex; 486 flex-direction: column; 487 min-height: inherit; 488 max-height: 70vh; 489 } 490 491 #dataResult{ 492 flex-grow: 1; 493 overflow-y: auto; 494 overflow-x: visible; 495 margin-bottom: 1%; 496 border-radius: 16px; 497 } 498 499 p{ 500 display: table-cell; 501 padding: 7px 10px; 502 font-size:0.875em; 503 line-height: 20px; 504 font-weight: 400; 505 text-align: left; 506 } 507 508 #response-json{ 509 margin-top: 20px; 510 background-color: var(--dark-background5,#F6F6F6); 511 margin-left: 10px; 512 flex-grow: 1; 513 scroll-y: visible; 514 } 515 516 .sql-select{ 517 background-color: var(--dark-background5, #F6F6F6); 518 } 519 ::-webkit-scrollbar 520 { 521 width: 8px; 522 background-color: var(--dark-background3,#FFFFFF); 523 } 524 ::-webkit-scrollbar-thumb 525 { 526 border-radius: 6px; 527 background-color: var(--dark-background7,rgba(0,0,0,0.1)); 528 } 529 530 .load-query-sql{ 531 width: 95%; 532 bottom: 0; 533 } 534 535 #copy-button{ 536 margin-right: 10%; 537 cursor:pointer; 538 opacity: 0.6; 539 } 540 541 #close-button{ 542 margin-right: 5%; 543 cursor:pointer; 544 opacity: 0.6; 545 } 546 547 .button-option{ 548 border-radius: 15px; 549 background-color: #0A59F7; 550 width: 120px; 551 height: 25px; 552 font-family: Helvetica-Bold; 553 color: var(--dark-background3,#FFFFFF); 554 text-align: center; 555 line-height: 20px; 556 font-weight: 400; 557 border:0 solid; 558 } 559 .pagination-box { 560 opacity: 0; 561 } 562 563 </style> 564 <div class="query"> 565 <div class="query-message request"> 566 <p class="query_select" style="color: #999999">Enter query and press cmd/ctrl + Enter</p> 567 <textarea class="sql-select"></textarea> 568 <lit-progress-bar class="load-query-sql"></lit-progress-bar> 569 </div> 570 <div class="query-message response"> 571 <div style="display: flex;justify-content: space-between"> 572 <p class="query_size" style="color: #999999">Query result - 0 counts</p> 573 <div style="display: flex; align-items: center"> 574 <button id="copy-button" class="button-option">Copy as.tsv</button> 575 <button id="close-button" class="button-option">Close</button> 576 </div> 577 </div> 578 <div id="dataResult"></div> 579 <pagination-box class="pagination-box"></pagination-box> 580 </div> 581 </div> 582 `; 583 } 584} 585