[ui] Add a simple plugins management page.

Allows the user to activate and deactivate individual plugins.
This page is hidden by default behind a flag.
Changes made on this page are not persisted, and will be lost on reload.

Change-Id: Ie499e0cd18429b903ba71058bf28bdbd0210d295
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 28c2fb3..9f216b7 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -27,6 +27,7 @@
 @import "hiring_banner";
 @import "viz_page";
 @import "widgets_page";
+@import "plugins_page";
 @import "widgets/anchor";
 @import "widgets/button";
 @import "widgets/checkbox";
diff --git a/ui/src/assets/plugins_page.scss b/ui/src/assets/plugins_page.scss
new file mode 100644
index 0000000..3000353
--- /dev/null
+++ b/ui/src/assets/plugins_page.scss
@@ -0,0 +1,55 @@
+// Copyright (C) 2023 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 "widgets/theme";
+
+.pf-plugins-page {
+  padding: 20px;
+  font-size: 16px;
+  overflow: auto;
+
+  .pf-plugins-topbar {
+    margin-bottom: 10px;
+    & > button {
+      margin-right: 4px;
+    }
+  }
+
+  h1 {
+    margin: 32px 0 8px 0;
+    font-size: 28px;
+  }
+
+  .pf-plugins-grid {
+    display: inline-grid;
+    grid-template-columns: auto auto auto;
+    row-gap: 6px;
+    column-gap: 10px;
+  }
+
+  .pf-tag {
+    padding: 3px 6px;
+    display: inline;
+    width: 100px;
+    text-align: center;
+    border-radius: $pf-border-radius;
+
+    &.pf-active {
+      background: lightgreen;
+    }
+    &.pf-inactive {
+      background: lightcoral;
+    }
+  }
+}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 9ab3d09..52f0c1a 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -47,6 +47,7 @@
 import {HomePage} from './home_page';
 import {InsightsPage} from './insights_page';
 import {MetricsPage} from './metrics_page';
+import {PluginsPage} from './plugins_page';
 import {postMessageHandler} from './post_message_handler';
 import {QueryPage} from './query_page';
 import {RecordPage, updateAvailableAdbDevices} from './record_page';
@@ -230,6 +231,7 @@
     '/info': TraceInfoPage,
     '/widgets': WidgetsPage,
     '/viz': VizPage,
+    '/plugins': PluginsPage,
   });
   router.onRouteChanged = routeChange;
 
diff --git a/ui/src/frontend/plugins_page.ts b/ui/src/frontend/plugins_page.ts
new file mode 100644
index 0000000..d5f93ad
--- /dev/null
+++ b/ui/src/frontend/plugins_page.ts
@@ -0,0 +1,79 @@
+// Copyright (C) 2023 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 {pluginManager, pluginRegistry} from '../common/plugins';
+import {raf} from '../core/raf_scheduler';
+import {PluginDescriptor} from '../public';
+import {Button} from '../widgets/button';
+
+import {createPage} from './pages';
+
+export const PluginsPage = createPage({
+  view() {
+    return m(
+        '.pf-plugins-page',
+        m('h1', 'Plugins'),
+        m(
+            '.pf-plugins-topbar',
+            m(Button, {
+              minimal: false,
+              label: 'Deactivate All',
+              onclick: () => {
+                for (const plugin of pluginRegistry.values()) {
+                  pluginManager.deactivatePlugin(plugin.pluginId);
+                }
+                raf.scheduleFullRedraw();
+              },
+            }),
+            m(Button, {
+              minimal: false,
+              label: 'Activate All',
+              onclick: () => {
+                for (const plugin of pluginRegistry.values()) {
+                  pluginManager.activatePlugin(plugin.pluginId);
+                }
+                raf.scheduleFullRedraw();
+              },
+            }),
+            ),
+        m(
+            '.pf-plugins-grid',
+            Array.from(pluginRegistry.values()).map((plugin) => {
+              return renderPluginRow(plugin);
+            }),
+            ));
+  },
+});
+
+function renderPluginRow(plugin: PluginDescriptor): m.Children {
+  const isActive = pluginManager.isActive(plugin.pluginId);
+  return [
+    plugin.pluginId,
+    isActive ? m('.pf-tag.pf-active', 'Active') :
+               m('.pf-tag.pf-inactive', 'Inactive'),
+    m(Button, {
+      label: isActive ? 'Deactivate' : 'Activate',
+      onclick: () => {
+        if (isActive) {
+          pluginManager.deactivatePlugin(plugin.pluginId);
+        } else {
+          pluginManager.activatePlugin(plugin.pluginId);
+        }
+        raf.scheduleFullRedraw();
+      },
+    }),
+  ];
+}
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 6d3e79b..a02af3b 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -75,6 +75,13 @@
   defaultValue: false,
 });
 
+const PLUGINS_PAGE_IN_NAV_FLAG = featureFlags.register({
+  id: 'showPluginsPageInNav',
+  name: 'Show plugins page',
+  description: 'Show a link to the plugins page in the side bar.',
+  defaultValue: false,
+});
+
 const INSIGHTS_PAGE_IN_NAV_FLAG = featureFlags.register({
   id: 'showInsightsPageInNav',
   name: 'Show insights page',
@@ -141,6 +148,12 @@
         i: 'widgets',
         isVisible: () => WIDGETS_PAGE_IN_NAV_FLAG.get(),
       },
+      {
+        t: 'Plugins',
+        a: navigatePlugins,
+        i: 'extension',
+        isVisible: () => PLUGINS_PAGE_IN_NAV_FLAG.get(),
+      },
     ],
   },
 
@@ -461,6 +474,11 @@
   Router.navigate('#!/widgets');
 }
 
+function navigatePlugins(e: Event) {
+  e.preventDefault();
+  Router.navigate('#!/plugins');
+}
+
 function navigateQuery(e: Event) {
   e.preventDefault();
   Router.navigate('#!/query');