1// Copyright (C) 2025 The Android Open Source Project 2// 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 15import m from 'mithril'; 16 17import {PageWithTraceAttrs} from '../../../public/page'; 18import {Button} from '../../../widgets/button'; 19import {SqlModules, SqlTable} from '../../dev.perfetto.SqlModules/sql_modules'; 20import {ColumnControllerRow} from './column_controller'; 21import {QueryNode} from '../query_node'; 22import {showModal} from '../../../widgets/modal'; 23import {DataSourceViewer} from './data_source_viewer'; 24import {PopupMenu} from '../../../widgets/menu'; 25import {Icons} from '../../../base/semantic_icons'; 26import {Intent} from '../../../widgets/common'; 27 28export interface QueryBuilderTable { 29 name: string; 30 asSqlTable: SqlTable; 31 columnOptions: ColumnControllerRow; 32 sql: string; 33} 34 35export interface QueryBuilderAttrs extends PageWithTraceAttrs { 36 readonly sqlModules: SqlModules; 37 readonly rootNodes: QueryNode[]; 38 readonly selectedNode?: QueryNode; 39 40 readonly onRootNodeCreated: (node: QueryNode) => void; 41 readonly onNodeSelected: (node?: QueryNode) => void; 42 readonly renderNodeActionsMenuItems: (node: QueryNode) => m.Children; 43 readonly addSourcePopupMenu: () => m.Children; 44} 45 46interface NodeAttrs { 47 readonly node: QueryNode; 48 isSelected: boolean; 49 readonly onNodeSelected: (node: QueryNode) => void; 50 readonly renderNodeActionsMenuItems: (node: QueryNode) => m.Children; 51} 52 53class NodeBox implements m.ClassComponent<NodeAttrs> { 54 view({attrs}: m.CVnode<NodeAttrs>) { 55 const {node, isSelected, onNodeSelected} = attrs; 56 return m( 57 '.node-box', 58 { 59 style: { 60 border: isSelected ? '2px solid yellow' : '2px solid blue', 61 borderRadius: '5px', 62 padding: '10px', 63 cursor: 'pointer', 64 backgroundColor: 'lightblue', 65 }, 66 onclick: () => onNodeSelected(node), 67 }, 68 node.getTitle(), 69 m( 70 PopupMenu, 71 { 72 trigger: m(Button, { 73 iconFilled: true, 74 icon: Icons.MoreVert, 75 }), 76 }, 77 attrs.renderNodeActionsMenuItems(node), 78 ), 79 ); 80 } 81} 82 83export class QueryBuilder implements m.ClassComponent<QueryBuilderAttrs> { 84 view({attrs}: m.CVnode<QueryBuilderAttrs>) { 85 const { 86 trace, 87 rootNodes, 88 onNodeSelected, 89 selectedNode, 90 renderNodeActionsMenuItems, 91 } = attrs; 92 93 const renderNodesPanel = (): m.Children => { 94 const nodes: m.Child[] = []; 95 const numRoots = rootNodes.length; 96 97 if (numRoots === 0) { 98 nodes.push( 99 m( 100 '', 101 {style: {gridColumn: 3, gridRow: 2}}, 102 m( 103 PopupMenu, 104 { 105 trigger: m(Button, { 106 icon: Icons.Add, 107 intent: Intent.Primary, 108 style: { 109 height: '100px', 110 width: '100px', 111 display: 'flex', 112 justifyContent: 'center', 113 alignItems: 'center', 114 fontSize: '48px', 115 }, 116 }), 117 }, 118 attrs.addSourcePopupMenu(), 119 ), 120 ), 121 ); 122 } else { 123 let col = 1; 124 rootNodes.forEach((rootNode) => { 125 let row = 1; 126 let curNode: QueryNode | undefined = rootNode; 127 while (curNode) { 128 const localCurNode = curNode; 129 nodes.push( 130 m( 131 '', 132 {style: {display: 'flex', gridColumn: col, gridRow: row}}, 133 m(NodeBox, { 134 node: localCurNode, 135 isSelected: selectedNode === localCurNode, 136 onNodeSelected, 137 renderNodeActionsMenuItems, 138 }), 139 ), 140 ); 141 row++; 142 curNode = curNode.nextNode; 143 } 144 col += 1; 145 }); 146 } 147 148 return m( 149 '', 150 { 151 style: { 152 display: 'grid', 153 gridTemplateColumns: `repeat(${numRoots} - 1, 1fr)`, 154 gridTemplateRows: 'repeat(3, 1fr)', 155 gap: '10px', 156 }, 157 }, 158 nodes, 159 ); 160 }; 161 162 const renderDataSourceViewer = () => { 163 return attrs.selectedNode 164 ? m(DataSourceViewer, {trace, queryNode: attrs.selectedNode}) 165 : undefined; 166 }; 167 168 return m( 169 '', 170 { 171 style: { 172 display: 'grid', 173 gridTemplateColumns: '50% 50%', 174 gridTemplateRows: '50% 50%', 175 gap: '10px', 176 }, 177 }, 178 m('', {style: {gridColumn: 1}}, renderNodesPanel()), 179 m('', {style: {gridColumn: 2}}, renderDataSourceViewer()), 180 ); 181 } 182} 183 184export const createModal = ( 185 title: string, 186 content: () => m.Children, 187 onAdd: () => void, 188) => { 189 showModal({ 190 title, 191 buttons: [{text: 'Add node', action: onAdd}], 192 content, 193 }); 194}; 195