• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2024 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';
16import SqlModulesPlugin from '../dev.perfetto.SqlModules';
17
18import {PageWithTraceAttrs} from '../../public/page';
19import {DataVisualiser} from './data_visualiser/data_visualiser';
20import {QueryBuilder} from './query_builder/builder';
21import {Button} from '../../widgets/button';
22import {Intent} from '../../widgets/common';
23import {NodeType, QueryNode} from './query_node';
24import {MenuItem} from '../../widgets/menu';
25import {Icons} from '../../base/semantic_icons';
26import {VisViewSource} from './data_visualiser/view_source';
27import {PopupMenu} from '../../widgets/menu';
28import {createModal} from './query_builder/builder';
29import {
30  StdlibTableAttrs,
31  StdlibTableNode,
32  StdlibTableSource,
33} from './query_builder/sources/stdlib_table';
34import {
35  SlicesSource,
36  SlicesSourceAttrs,
37  SlicesSourceNode,
38} from './query_builder/sources/slices_source';
39import {
40  SqlSource,
41  SqlSourceAttrs,
42  SqlSourceNode,
43} from './query_builder/sources/sql_source';
44
45export interface ExplorePageState {
46  rootNodes: QueryNode[];
47  selectedNode?: QueryNode; // Selected Query Node on which to perform actions
48  activeViewSource?: VisViewSource; // View Source of activeQueryNode
49  mode: ExplorePageModes;
50}
51
52export enum ExplorePageModes {
53  QUERY_BUILDER,
54  DATA_VISUALISER,
55}
56
57export const ExplorePageModeToLabel: Record<ExplorePageModes, string> = {
58  [ExplorePageModes.QUERY_BUILDER]: 'Query Builder',
59  [ExplorePageModes.DATA_VISUALISER]: 'Visualise Data',
60};
61
62interface ExplorePageAttrs extends PageWithTraceAttrs {
63  readonly sqlModulesPlugin: SqlModulesPlugin;
64  readonly state: ExplorePageState;
65}
66
67export class ExplorePage implements m.ClassComponent<ExplorePageAttrs> {
68  renderNodeActionsMenuItems(node: QueryNode, state: ExplorePageState) {
69    // TODO: Split into operations on graph (like delete or duplicate) and
70    // operations on node (like edit).
71    return [
72      m(MenuItem, {
73        label: 'Visualise Data',
74        icon: Icons.Chart,
75        onclick: () => {
76          state.selectedNode = node;
77          state.mode = ExplorePageModes.DATA_VISUALISER;
78        },
79      }),
80      m(MenuItem, {
81        label: 'Edit',
82        onclick: async () => {
83          const attrsCopy = node.getState();
84          switch (node.type) {
85            case NodeType.kStdlibTable:
86              createModal(
87                'Standard library table',
88                () => m(StdlibTableSource, attrsCopy as StdlibTableAttrs),
89                () => {
90                  // TODO: Support editing non root nodes.
91                  state.rootNodes[state.rootNodes.indexOf(node)] =
92                    new StdlibTableNode(attrsCopy as StdlibTableAttrs);
93                  state.selectedNode = node;
94                },
95              );
96              node = new StdlibTableNode(attrsCopy as StdlibTableAttrs);
97              break;
98            case NodeType.kSimpleSlices:
99              createModal(
100                'Slices',
101                () => m(SlicesSource, attrsCopy as SlicesSourceAttrs),
102                () => {
103                  // TODO: Support editing non root nodes.
104                  state.rootNodes[state.rootNodes.indexOf(node)] =
105                    new SlicesSourceNode(attrsCopy as SlicesSourceAttrs);
106                  state.selectedNode = node;
107                },
108              );
109              break;
110            case NodeType.kSqlSource:
111              createModal(
112                'SQL',
113                () => m(SqlSource, attrsCopy as SqlSourceAttrs),
114                () => {
115                  // TODO: Support editing non root nodes.
116                  state.rootNodes[state.rootNodes.indexOf(node)] =
117                    new SqlSourceNode(attrsCopy as SqlSourceAttrs);
118                  state.selectedNode = node;
119                },
120              );
121          }
122        },
123      }),
124      m(MenuItem, {
125        label: 'Duplicate',
126        onclick: async () => {
127          state.rootNodes.push(cloneQueryNode(node));
128        },
129      }),
130      m(MenuItem, {
131        label: 'Delete',
132        onclick: async () => {
133          const idx = state.rootNodes.indexOf(node);
134          if (idx !== -1) {
135            state.rootNodes.splice(idx, 1);
136            state.selectedNode = node;
137          }
138        },
139      }),
140    ];
141  }
142
143  view({attrs}: m.CVnode<ExplorePageAttrs>) {
144    const {trace, state} = attrs;
145
146    return m(
147      '.page.explore-page',
148      m(
149        '.explore-page__header',
150        m('h1', `${ExplorePageModeToLabel[state.mode]}`),
151        m('span', {style: {flexGrow: 1}}),
152        state.mode === ExplorePageModes.QUERY_BUILDER
153          ? m(
154              '',
155              m(
156                PopupMenu,
157                {
158                  trigger: m(Button, {
159                    label: 'Add new node',
160                    icon: Icons.Add,
161                    intent: Intent.Primary,
162                  }),
163                },
164                addSourcePopupMenu(attrs),
165              ),
166              m(Button, {
167                label: 'Clear All Query Nodes',
168                intent: Intent.Primary,
169                onclick: () => {
170                  state.rootNodes = [];
171                  state.selectedNode = undefined;
172                },
173                style: {marginLeft: '10px'},
174              }),
175            )
176          : m(Button, {
177              label: 'Back to Query Builder',
178              intent: Intent.Primary,
179              onclick: () => {
180                state.mode = ExplorePageModes.QUERY_BUILDER;
181              },
182            }),
183      ),
184
185      state.mode === ExplorePageModes.QUERY_BUILDER &&
186        m(QueryBuilder, {
187          trace,
188          sqlModules: attrs.sqlModulesPlugin.getSqlModules(),
189          onRootNodeCreated(arg) {
190            state.rootNodes.push(arg);
191            state.selectedNode = arg;
192          },
193          onNodeSelected(arg) {
194            state.selectedNode = arg;
195          },
196          renderNodeActionsMenuItems: (node: QueryNode) =>
197            this.renderNodeActionsMenuItems(node, state),
198          rootNodes: state.rootNodes,
199          selectedNode: state.selectedNode,
200          addSourcePopupMenu: () => addSourcePopupMenu(attrs),
201        }),
202      state.mode === ExplorePageModes.DATA_VISUALISER &&
203        state.rootNodes.length !== 0 &&
204        m(DataVisualiser, {
205          trace,
206          state,
207        }),
208    );
209  }
210}
211
212function addSourcePopupMenu(attrs: ExplorePageAttrs): m.Children {
213  const {trace, state} = attrs;
214  const sqlModules = attrs.sqlModulesPlugin.getSqlModules();
215  return [
216    m(MenuItem, {
217      label: 'Standard library table',
218      onclick: async () => {
219        const stdlibTableAttrs: StdlibTableAttrs = {
220          filters: [],
221          sourceCols: [],
222          groupByColumns: [],
223          aggregations: [],
224          trace,
225          sqlModules,
226          modal: () =>
227            createModal(
228              'Standard library table',
229              () => m(StdlibTableSource, stdlibTableAttrs),
230              () => {
231                const newNode = new StdlibTableNode(stdlibTableAttrs);
232                state.rootNodes.push(newNode);
233                state.selectedNode = newNode;
234              },
235            ),
236        };
237        // Adding trivial modal to open the table selection.
238        createModal(
239          'Standard library table',
240          () => m(StdlibTableSource, stdlibTableAttrs),
241          () => {},
242        );
243      },
244    }),
245    m(MenuItem, {
246      label: 'Custom slices',
247      onclick: () => {
248        const newSimpleSlicesAttrs: SlicesSourceAttrs = {
249          sourceCols: [],
250          filters: [],
251          groupByColumns: [],
252          aggregations: [],
253        };
254        createModal(
255          'Slices',
256          () => m(SlicesSource, newSimpleSlicesAttrs),
257          () => {
258            const newNode = new SlicesSourceNode(newSimpleSlicesAttrs);
259            state.rootNodes.push(newNode);
260            state.selectedNode = newNode;
261          },
262        );
263      },
264    }),
265    m(MenuItem, {
266      label: 'Custom SQL',
267      onclick: () => {
268        const newSqlSourceAttrs: SqlSourceAttrs = {
269          sourceCols: [],
270          filters: [],
271          groupByColumns: [],
272          aggregations: [],
273        };
274        createModal(
275          'SQL',
276          () => m(SqlSource, newSqlSourceAttrs),
277          () => {
278            const newNode = new SqlSourceNode(newSqlSourceAttrs);
279            state.rootNodes.push(newNode);
280            state.selectedNode = newNode;
281          },
282        );
283      },
284    }),
285  ];
286}
287
288function cloneQueryNode(node: QueryNode): QueryNode {
289  const attrsCopy = node.getState();
290  switch (node.type) {
291    case NodeType.kStdlibTable:
292      return new StdlibTableNode(attrsCopy as StdlibTableAttrs);
293    case NodeType.kSimpleSlices:
294      return new SlicesSourceNode(attrsCopy as SlicesSourceAttrs);
295    case NodeType.kSqlSource:
296      return new SqlSourceNode(attrsCopy as SqlSourceAttrs);
297  }
298}
299