| // Copyright (C) 2024 The Android Open Source Project | |
| // | |
| // Licensed 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 m from 'mithril'; | |
| import {sqliteString} from '../../../../base/string_utils'; | |
| import {Duration, Time} from '../../../../base/time'; | |
| import type {Trace} from '../../../../public/trace'; | |
| import {type SqlValue, STR} from '../../../../trace_processor/query_result'; | |
| import { | |
| asSchedSqlId, | |
| asThreadStateSqlId, | |
| asUpid, | |
| asUtid, | |
| } from '../../../sql_utils/core_types'; | |
| import {renderError} from '../../../../widgets/error'; | |
| import {DurationWidget} from '../../duration'; | |
| import {showProcessDetailsMenuItem} from '../../process'; | |
| import {SchedRef} from '../../sched'; | |
| import {showThreadDetailsMenuItem} from '../../thread'; | |
| import {ThreadStateRef} from '../../thread_state'; | |
| import {Timestamp} from '../../timestamp'; | |
| import type { | |
| RenderedCell, | |
| TableColumn, | |
| RenderCellContext, | |
| ListColumnsContext, | |
| } from './table_column'; | |
| import { | |
| getStandardContextMenuItems, | |
| renderStandardCell, | |
| } from './render_cell_utils'; | |
| import {type SqlColumn, sqlColumnId, SqlExpression} from './sql_column'; | |
| import { | |
| type PerfettoSqlType, | |
| PerfettoSqlTypes, | |
| } from '../../../../trace_processor/perfetto_sql_type'; | |
| import {parseJsonWithBigints} from '../../../../base/json_utils'; | |
| import {Anchor} from '../../../../widgets/anchor'; | |
| import {MenuItem, PopupMenu} from '../../../../widgets/menu'; | |
| import {Icons} from '../../../../base/semantic_icons'; | |
| import {copyToClipboard} from '../../../../base/clipboard'; | |
| import type {Args} from '../../../sql_utils/args'; | |
| import {sqlValueToReadableString} from '../../../../trace_processor/sql_utils'; | |
| import type { | |
| SqlTableDefinition, | |
| SqlTableDescription, | |
| } from './table_description'; | |
| import {TrackEventRef} from '../../track_event_ref'; | |
| // Converts a raw SqlTableDefinition (just data) into a SqlTableDescription | |
| // with fully constructed TableColumn objects that have rendering logic. | |
| export function resolveTableDefinition( | |
| trace: Trace, | |
| def: SqlTableDefinition, | |
| ): SqlTableDescription { | |
| return { | |
| imports: def.imports, | |
| prefix: def.prefix, | |
| name: def.name, | |
| displayName: def.displayName, | |
| columns: def.columns.map((col) => | |
| createTableColumn({ | |
| trace, | |
| column: col.column, | |
| type: col.type, | |
| startsHidden: col.startsHidden, | |
| }), | |
| ), | |
| }; | |
| } | |
| export function createTableColumn(args: { | |
| trace: Trace; | |
| column: SqlColumn; | |
| type?: PerfettoSqlType; | |
| startsHidden?: boolean; | |
| }): TableColumn { | |
| if (args.type?.kind === 'timestamp') { | |
| return new TimestampColumn(args.trace, args.column, { | |
| startsHidden: args.startsHidden, | |
| }); | |
| } | |
| if (args.type?.kind === 'duration') { | |
| return new DurationColumn(args.trace, args.column, { | |
| startsHidden: args.startsHidden, | |
| }); | |
| } | |
| if (args.type?.kind === 'arg_set_id') { | |
| return new ArgSetIdColumn(args.column, {startsHidden: args.startsHidden}); | |
| } | |
| if (args.type?.kind === 'id' || args.type?.kind === 'joinid') { | |
| if (args.type.source.column === 'id') { | |
| switch (args.type.source?.table.toLowerCase()) { | |
| case 'slice': | |
| return sliceIdColumn(args.trace, args.column, { | |
| type: args.type.kind, | |
| startsHidden: args.startsHidden, | |
| }); | |
| case 'thread': | |
| return threadIdColumn(args.trace, args.column, { | |
| type: args.type.kind, | |
| startsHidden: args.startsHidden, | |
| }); | |
| case 'process': | |
| return processIdColumn(args.trace, args.column, { | |
| type: args.type.kind, | |
| startsHidden: args.startsHidden, | |
| }); | |
| case 'thread_state': | |
| return threadStateIdColumn(args.trace, args.column, { | |
| startsHidden: args.startsHidden, | |
| }); | |
| case 'sched': | |
| return schedIdColumn(args.trace, args.column, { | |
| startsHidden: args.startsHidden, | |
| }); | |
| case 'track': | |
| return trackIdColumn(args.trace, args.column, { | |
| startsHidden: args.startsHidden, | |
| }); | |
| } | |
| } | |
| } | |
| return new StandardColumn(args.column, args.type, { | |
| startsHidden: args.startsHidden, | |
| }); | |
| } | |
| function wrongTypeError(type: string, name: SqlColumn, value: SqlValue) { | |
| return renderError( | |
| `Wrong type for ${type} column ${sqlColumnId( | |
| name, | |
| )}: bigint expected, ${typeof value} found`, | |
| ); | |
| } | |
| export type ColumnParams = { | |
| startsHidden?: boolean; | |
| }; | |
| export type IdColumnParams = ColumnParams & { | |
| // Whether this column is a primary key (ID) for this table or whether it's a reference | |
| // to another table's primary key. | |
| type?: 'id' | 'joinid'; | |
| // Whether the column is guaranteed not to have null values. | |
| // (this will allow us to upgrage the joins on this column to more performant INNER JOINs). | |
| notNull?: boolean; | |
| }; | |
| export class StandardColumn implements TableColumn { | |
| constructor( | |
| public readonly column: SqlColumn, | |
| public readonly type: PerfettoSqlType | undefined, | |
| private params?: ColumnParams, | |
| ) {} | |
| renderCell(value: SqlValue, context?: RenderCellContext) { | |
| return renderStandardCell(value, this.column, context); | |
| } | |
| initialColumns(): TableColumn[] { | |
| return this.params?.startsHidden ? [] : [this]; | |
| } | |
| } | |
| export class TimestampColumn implements TableColumn { | |
| public readonly type = PerfettoSqlTypes.TIMESTAMP; | |
| constructor( | |
| public readonly trace: Trace, | |
| public readonly column: SqlColumn, | |
| private params?: ColumnParams, | |
| ) {} | |
| renderCell(value: SqlValue, context?: RenderCellContext) { | |
| if (typeof value === 'number') { | |
| value = BigInt(Math.round(value)); | |
| } | |
| if (typeof value !== 'bigint') { | |
| return renderStandardCell(value, this.column, context); | |
| } | |
| return { | |
| content: m(Timestamp, { | |
| trace: this.trace, | |
| ts: Time.fromRaw(value), | |
| }), | |
| menu: [ | |
| context && getStandardContextMenuItems(value, this.column, context), | |
| ], | |
| isNumerical: true, | |
| }; | |
| } | |
| initialColumns(): TableColumn[] { | |
| return this.params?.startsHidden ? [] : [this]; | |
| } | |
| } | |
| export class DurationColumn implements TableColumn { | |
| public readonly type = PerfettoSqlTypes.DURATION; | |
| constructor( | |
| public readonly trace: Trace, | |
| public column: SqlColumn, | |
| private params?: ColumnParams, | |
| ) {} | |
| renderCell(value: SqlValue, context?: RenderCellContext) { | |
| if (typeof value === 'number') { | |
| value = BigInt(Math.round(value)); | |
| } | |
| if (typeof value !== 'bigint') { | |
| return renderStandardCell(value, this.column, context); | |
| } | |
| return { | |
| content: m(DurationWidget, { | |
| trace: this.trace, | |
| dur: Duration.fromRaw(value), | |
| }), | |
| menu: [ | |
| context && getStandardContextMenuItems(value, this.column, context), | |
| ], | |
| isNumerical: true, | |
| }; | |
| } | |
| initialColumns(): TableColumn[] { | |
| return this.params?.startsHidden ? [] : [this]; | |
| } | |
| } | |
| export class IdColumn implements TableColumn { | |
| public readonly type: PerfettoSqlType; | |
| constructor( | |
| public readonly trace: Trace, | |
| public readonly column: SqlColumn, | |
| private readonly args: { | |
| table: { | |
| name: string; | |
| columns: {name: string; type: PerfettoSqlType; showWithId?: boolean}[]; | |
| }; | |
| render: (id: bigint) => {content: m.Children; menu?: m.Children}; | |
| } & IdColumnParams, | |
| ) { | |
| this.type = { | |
| kind: args.type === 'id' ? 'id' : 'joinid', | |
| source: {table: args.table.name, column: 'id'}, | |
| }; | |
| } | |
| renderCell(value: SqlValue, context?: RenderCellContext): RenderedCell { | |
| const id = value; | |
| if (context === undefined || id === null) { | |
| return renderStandardCell(id, this.column, context); | |
| } | |
| if (typeof id !== 'bigint') { | |
| return {content: wrongTypeError('id', this.column, id)}; | |
| } | |
| const rendered = this.args.render(id); | |
| return { | |
| content: rendered.content, | |
| menu: [ | |
| rendered.menu, | |
| getStandardContextMenuItems(id, this.column, context), | |
| ], | |
| isNumerical: true, | |
| }; | |
| } | |
| listDerivedColumns() { | |
| if (this.args.type === 'id') return undefined; | |
| return async () => { | |
| const result = new Map<string, TableColumn>(); | |
| for (const col of this.args.table.columns) { | |
| result.set( | |
| col.name, | |
| createTableColumn({ | |
| trace: this.trace, | |
| column: this.getChildColumn(col.name), | |
| type: col.type, | |
| }), | |
| ); | |
| } | |
| return result; | |
| }; | |
| } | |
| initialColumns(): TableColumn[] { | |
| if (this.args.startsHidden) return []; | |
| const result: TableColumn[] = [this]; | |
| for (const col of this.args.table.columns) { | |
| if (col.showWithId) { | |
| result.push( | |
| createTableColumn({ | |
| trace: this.trace, | |
| column: this.getChildColumn(col.name), | |
| type: col.type, | |
| }), | |
| ); | |
| } | |
| } | |
| return result; | |
| } | |
| private getChildColumn(name: string): SqlColumn { | |
| return { | |
| column: name, | |
| source: { | |
| table: this.args.table.name, | |
| joinOn: {id: this.column}, | |
| innerJoin: this.args.notNull === true, | |
| }, | |
| }; | |
| } | |
| } | |
| export function sliceIdColumn( | |
| trace: Trace, | |
| column: SqlColumn, | |
| params?: IdColumnParams, | |
| ): IdColumn { | |
| return new IdColumn(trace, column, { | |
| table: { | |
| name: 'slice', | |
| columns: [ | |
| {name: 'ts', type: PerfettoSqlTypes.TIMESTAMP}, | |
| {name: 'dur', type: PerfettoSqlTypes.DURATION}, | |
| {name: 'name', type: PerfettoSqlTypes.STRING}, | |
| { | |
| name: 'parent_id', | |
| type: {kind: 'joinid', source: {table: 'slice', column: 'id'}}, | |
| }, | |
| ], | |
| }, | |
| render: (id) => ({ | |
| content: m(TrackEventRef, { | |
| trace, | |
| table: 'slice', | |
| id: Number(id), | |
| name: `${id}`, | |
| switchToCurrentSelectionTab: false, | |
| }), | |
| }), | |
| ...params, | |
| }); | |
| } | |
| export function schedIdColumn( | |
| trace: Trace, | |
| column: SqlColumn, | |
| params?: IdColumnParams, | |
| ): IdColumn { | |
| return new IdColumn(trace, column, { | |
| table: { | |
| name: 'sched', | |
| columns: [ | |
| {name: 'ts', type: PerfettoSqlTypes.TIMESTAMP}, | |
| {name: 'dur', type: PerfettoSqlTypes.DURATION}, | |
| {name: 'cpu', type: PerfettoSqlTypes.INT}, | |
| { | |
| name: 'utid', | |
| type: {kind: 'joinid', source: {table: 'thread', column: 'id'}}, | |
| }, | |
| {name: 'priority', type: PerfettoSqlTypes.INT}, | |
| ], | |
| }, | |
| render: (id) => ({ | |
| content: m(SchedRef, { | |
| trace, | |
| id: asSchedSqlId(Number(id)), | |
| name: `${id}`, | |
| switchToCurrentSelectionTab: false, | |
| }), | |
| }), | |
| ...params, | |
| }); | |
| } | |
| export function threadStateIdColumn( | |
| trace: Trace, | |
| column: SqlColumn, | |
| params?: IdColumnParams, | |
| ): IdColumn { | |
| return new IdColumn(trace, column, { | |
| table: { | |
| name: 'thread_state', | |
| columns: [ | |
| {name: 'ts', type: PerfettoSqlTypes.TIMESTAMP}, | |
| {name: 'dur', type: PerfettoSqlTypes.DURATION}, | |
| {name: 'cpu', type: PerfettoSqlTypes.INT}, | |
| { | |
| name: 'utid', | |
| type: {kind: 'joinid', source: {table: 'thread', column: 'id'}}, | |
| }, | |
| {name: 'state', type: PerfettoSqlTypes.STRING}, | |
| {name: 'io_wait', type: PerfettoSqlTypes.BOOLEAN}, | |
| {name: 'blocked_function', type: PerfettoSqlTypes.STRING}, | |
| { | |
| name: 'waker_utid', | |
| type: {kind: 'joinid', source: {table: 'thread', column: 'id'}}, | |
| }, | |
| { | |
| name: 'waker_id', | |
| type: {kind: 'joinid', source: {table: 'thread_state', column: 'id'}}, | |
| }, | |
| ], | |
| }, | |
| render: (id) => ({ | |
| content: m(ThreadStateRef, { | |
| trace, | |
| id: asThreadStateSqlId(Number(id)), | |
| name: `${id}`, | |
| switchToCurrentSelectionTab: false, | |
| }), | |
| }), | |
| ...params, | |
| }); | |
| } | |
| export function threadIdColumn( | |
| trace: Trace, | |
| column: SqlColumn, | |
| params?: IdColumnParams, | |
| ): IdColumn { | |
| return new IdColumn(trace, column, { | |
| table: { | |
| name: 'thread', | |
| columns: [ | |
| {name: 'tid', type: PerfettoSqlTypes.INT, showWithId: true}, | |
| {name: 'name', type: PerfettoSqlTypes.STRING, showWithId: true}, | |
| {name: 'start_ts', type: PerfettoSqlTypes.TIMESTAMP}, | |
| {name: 'end_ts', type: PerfettoSqlTypes.TIMESTAMP}, | |
| { | |
| name: 'upid', | |
| type: {kind: 'joinid', source: {table: 'process', column: 'id'}}, | |
| }, | |
| {name: 'is_main_thread', type: PerfettoSqlTypes.BOOLEAN}, | |
| ], | |
| }, | |
| render: (id) => ({ | |
| content: `${id}`, | |
| menu: showThreadDetailsMenuItem(trace, asUtid(Number(id))), | |
| }), | |
| ...params, | |
| }); | |
| } | |
| export function processIdColumn( | |
| trace: Trace, | |
| column: SqlColumn, | |
| params?: IdColumnParams, | |
| ): IdColumn { | |
| return new IdColumn(trace, column, { | |
| table: { | |
| name: 'process', | |
| columns: [ | |
| {name: 'pid', type: PerfettoSqlTypes.INT, showWithId: true}, | |
| {name: 'name', type: PerfettoSqlTypes.STRING, showWithId: true}, | |
| {name: 'start_ts', type: PerfettoSqlTypes.TIMESTAMP}, | |
| {name: 'end_ts', type: PerfettoSqlTypes.TIMESTAMP}, | |
| { | |
| name: 'parent_upid', | |
| type: {kind: 'joinid', source: {table: 'process', column: 'id'}}, | |
| }, | |
| {name: 'is_main_thread', type: PerfettoSqlTypes.BOOLEAN}, | |
| ], | |
| }, | |
| render: (id) => ({ | |
| content: `${id}`, | |
| menu: showProcessDetailsMenuItem(trace, asUpid(Number(id))), | |
| }), | |
| ...params, | |
| }); | |
| } | |
| export function trackIdColumn( | |
| trace: Trace, | |
| column: SqlColumn, | |
| params?: IdColumnParams, | |
| ): IdColumn { | |
| return new IdColumn(trace, column, { | |
| table: { | |
| name: 'track', | |
| columns: [ | |
| {name: 'name', type: PerfettoSqlTypes.STRING, showWithId: true}, | |
| {name: 'type', type: PerfettoSqlTypes.STRING}, | |
| {name: 'dimension_arg_set_id', type: PerfettoSqlTypes.ARG_SET_ID}, | |
| { | |
| name: 'parent_id', | |
| type: {kind: 'joinid', source: {table: 'track', column: 'id'}}, | |
| }, | |
| {name: 'source_arg_set_id', type: PerfettoSqlTypes.ARG_SET_ID}, | |
| {name: 'machine_id', type: PerfettoSqlTypes.INT}, | |
| {name: 'track_group_id', type: PerfettoSqlTypes.INT}, | |
| ], | |
| }, | |
| render: (id) => ({ | |
| content: `${id}`, | |
| }), | |
| ...params, | |
| }); | |
| } | |
| class ArgColumn implements TableColumn { | |
| public readonly column: SqlColumn; | |
| public readonly display: SqlColumn; | |
| public readonly type: PerfettoSqlType | undefined = undefined; | |
| private id: string; | |
| constructor( | |
| private argSetId: SqlColumn, | |
| private key: string, | |
| ) { | |
| this.id = `${sqlColumnId(this.argSetId)}[${this.key}]`; | |
| this.column = new SqlExpression( | |
| (cols: string[]) => `COALESCE(${cols[0]}, ${cols[1]}, ${cols[2]})`, | |
| [ | |
| this.getRawColumn('string_value'), | |
| this.getRawColumn('int_value'), | |
| this.getRawColumn('real_value'), | |
| ], | |
| this.id, | |
| ); | |
| this.display = new SqlExpression( | |
| (cols: string[]) => `json_object( | |
| 'id', ${cols[0]}, | |
| 'int_value', ${cols[1]}, | |
| 'real_value', ${cols[2]}, | |
| 'string_value', ${cols[3]}, | |
| 'display_value', ${cols[4]} | |
| )`, | |
| ( | |
| [ | |
| 'id', | |
| 'int_value', | |
| 'real_value', | |
| 'string_value', | |
| 'display_value', | |
| ] as const | |
| ).map((c) => this.getRawColumn(c)), | |
| ); | |
| } | |
| private getRawColumn( | |
| type: | |
| | 'string_value' | |
| | 'int_value' | |
| | 'real_value' | |
| | 'id' | |
| | 'type' | |
| | 'display_value', | |
| ): SqlColumn { | |
| return { | |
| column: type, | |
| source: { | |
| table: 'args', | |
| joinOn: { | |
| arg_set_id: this.argSetId, | |
| key: `${sqliteString(this.key)}`, | |
| }, | |
| }, | |
| id: `${this.id}.${type.replace(/_value$/g, '')}`, | |
| }; | |
| } | |
| renderCell(value: SqlValue, context?: RenderCellContext): RenderedCell { | |
| if (context === undefined) { | |
| return renderStandardCell(value, this.column, context); | |
| } | |
| if (typeof value !== 'string') { | |
| return { | |
| content: renderError( | |
| `Wrong type: expected string, ${typeof value} found`, | |
| ), | |
| }; | |
| } | |
| const argValue = parseJsonWithBigints(value); | |
| if (argValue['id'] === null) { | |
| return renderStandardCell(null, this.getRawColumn('id'), context); | |
| } | |
| if (argValue['int_value'] !== null) { | |
| return renderStandardCell( | |
| argValue['int_value'], | |
| this.getRawColumn('int_value'), | |
| context, | |
| ); | |
| } else if (argValue['real_value'] !== null) { | |
| return renderStandardCell( | |
| argValue['real_value'], | |
| this.getRawColumn('real_value'), | |
| context, | |
| ); | |
| } else { | |
| return renderStandardCell( | |
| argValue['string_value'], | |
| this.getRawColumn('string_value'), | |
| context, | |
| ); | |
| } | |
| } | |
| } | |
| export class ArgSetIdColumn implements TableColumn { | |
| public readonly type = PerfettoSqlTypes.ARG_SET_ID; | |
| constructor( | |
| public readonly column: SqlColumn, | |
| private params?: ColumnParams, | |
| ) {} | |
| renderCell(value: SqlValue, context: RenderCellContext) { | |
| return renderStandardCell(value, this.column, context); | |
| } | |
| listDerivedColumns(context: ListColumnsContext) { | |
| return async () => { | |
| const queryResult = await context.trace.engine.query(` | |
| SELECT | |
| DISTINCT args.key | |
| FROM (${context.getSqlQuery({arg_set_id: this.column})}) data | |
| JOIN args USING (arg_set_id) | |
| `); | |
| const it = queryResult.iter({key: STR}); | |
| const result = new Map(); | |
| for (; it.valid(); it.next()) { | |
| result.set(it.key, argTableColumn(this.column, it.key)); | |
| } | |
| return result; | |
| }; | |
| } | |
| initialColumns() { | |
| return this.params?.startsHidden === true | |
| ? [] | |
| : [new PrintArgsColumn(this.column), this]; | |
| } | |
| } | |
| export function argTableColumn(argSetId: SqlColumn, key: string): TableColumn { | |
| return new ArgColumn(argSetId, key); | |
| } | |
| export class PrintArgsColumn implements TableColumn { | |
| public readonly column: SqlColumn; | |
| public readonly type = undefined; | |
| constructor(public readonly argSetIdColumn: SqlColumn) { | |
| this.column = new SqlExpression( | |
| (cols: string[]) => `__intrinsic_arg_set_to_json(${cols[0]})`, | |
| [argSetIdColumn], | |
| `print_args(${sqlColumnId(argSetIdColumn)})`, | |
| ); | |
| } | |
| renderCell(value: SqlValue, context?: RenderCellContext): RenderedCell { | |
| if (value === null) { | |
| return { | |
| content: '{}', | |
| }; | |
| } | |
| if (typeof value !== 'string') { | |
| return { | |
| content: renderError( | |
| `Unexpected type: expected string, got ${typeof value}`, | |
| ), | |
| }; | |
| } | |
| let data: Args; | |
| try { | |
| data = parseJsonWithBigints(value) as Args; | |
| } catch (e) { | |
| return { | |
| content: renderError(`Failed to parse JSON: ${e}`), | |
| }; | |
| } | |
| const content: m.Children[] = []; | |
| // Condense single-key nested objects into a flattened key: {a: {b: 1}} becomes {a.b: 1}. | |
| const condense = (key: string, value: Args): {key: string; value: Args} => { | |
| if ( | |
| value !== null && | |
| typeof value === 'object' && | |
| !Array.isArray(value) | |
| ) { | |
| const entries = Object.entries(value); | |
| if (entries.length === 1) { | |
| const [nestedKey, nestedValue] = entries[0]; | |
| return condense(`${key}.${nestedKey}`, nestedValue); | |
| } | |
| } | |
| return {key, value}; | |
| }; | |
| const renderJsonValue = (value: Args, prefix?: string) => { | |
| if (value === null) { | |
| content.push(m('i', 'null')); | |
| } else if (typeof value === 'object' && !Array.isArray(value)) { | |
| content.push('{'); | |
| Object.entries(value).forEach(([rawKey, rawVal], idx) => { | |
| if (idx > 0) { | |
| content.push(', '); | |
| } | |
| const {key, value: val} = condense(rawKey, rawVal); | |
| const isLeaf = typeof val !== 'object' || val === null; | |
| const fullKey = prefix === undefined ? key : `${prefix}.${key}`; | |
| const argColumn = argTableColumn(this.argSetIdColumn, fullKey); | |
| const keyElement = | |
| isLeaf && context | |
| ? m( | |
| PopupMenu, | |
| { | |
| trigger: m(Anchor, key), | |
| }, | |
| m(MenuItem, { | |
| icon: Icons.Add, | |
| label: 'Add column', | |
| disabled: !context.hasColumn(argColumn), | |
| onclick: () => { | |
| context.addColumn(argColumn); | |
| }, | |
| }), | |
| m(MenuItem, { | |
| icon: Icons.Copy, | |
| label: 'Copy full key', | |
| onclick: () => { | |
| copyToClipboard(fullKey); | |
| }, | |
| }), | |
| ) | |
| : key; | |
| content.push(keyElement, ': '); | |
| renderJsonValue(val, fullKey); | |
| }); | |
| content.push('}'); | |
| } else if (Array.isArray(value)) { | |
| content.push('['); | |
| value.forEach((item, idx) => { | |
| if (idx > 0) { | |
| content.push(', '); | |
| } | |
| renderJsonValue(item, `${prefix}[${idx}]`); | |
| }); | |
| content.push(']'); | |
| } else if (typeof value === 'boolean') { | |
| content.push(value ? 'true' : 'false'); | |
| } else if (typeof value === 'string') { | |
| content.push(`"${value.replace(/"/g, '\\"')}"`); | |
| } else { | |
| content.push(sqlValueToReadableString(value)); | |
| } | |
| }; | |
| renderJsonValue(data); | |
| return { | |
| content, | |
| }; | |
| } | |
| initialColumns() { | |
| return []; | |
| } | |
| } |