• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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