diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fdd7fb0..3764addb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
## 3.1.0 (unreleased)
- **checklist**: use new GraphNG component, show units in graphs, update help texts
+- **all**: replace Angular.js based config component with React, ensure Grafana 8.0 compatibility
- **dashboards**: PCP Vector Host Overview: add pmproxy URL and hostspec variables
- **dashboards**: mark all dashboards as readonly
- **vector, bpftrace**: use `pcp://127.0.0.1` as default hostspec (no functional change)
diff --git a/Makefile b/Makefile
index 6d7adbe6..3ca5242d 100644
--- a/Makefile
+++ b/Makefile
@@ -96,6 +96,7 @@ test-e2e: test-e2e-start-container ## Run End-to-End tests
test-e2e-ui: test-e2e-start-container ## Run End-to-End tests with a browser UI
GRAFANA_URL="http://127.0.0.1:3001" HEADLESS=false node_modules/.bin/jest --config jest.config.e2e.js --runInBand
+
##@ Helpers
sign: ## Sign the plugin
diff --git a/docs/installation.rst b/docs/installation.rst
index 736eb01e..98025d3d 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -37,7 +37,7 @@ Replace X.Y.Z with the version of grafana-pcp you wish to install.
.. code-block:: console
- $ podman run -e GF_INSTALL_PLUGINS="https://github.com/performancecopilot/grafana-pcp/releases/download/vX.Y.Z/performancecopilot-pcp-app-X.Y.Z.zip;performancecopilot-pcp-app" -p 3000:3000 grafana/grafana
+ $ podman run -e GF_INSTALL_PLUGINS="https://github.com/performancecopilot/grafana-pcp/releases/download/vX.Y.Z/performancecopilot-pcp-app-X.Y.Z.zip;performancecopilot-pcp-app" -p 3000:3000 docker.io/grafana/grafana
.. code-block:: console
diff --git a/src/components/appconfig/config.html b/src/components/appconfig/config.html
deleted file mode 100644
index 77697726..00000000
--- a/src/components/appconfig/config.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
Performance Co-Pilot App
-This app integrates metrics from Performance Co-Pilot.
-
-It includes the following datasources:
-
- - PCP Redis for fast, scalable time series aggregation across multiple hosts
- - PCP Vector for live, on-host metrics analysis, with container support
- - PCP bpftrace for system introspection using bpftrace scripts
-
-
-
- Plugin enabled. Please configure the datasources now.
-
diff --git a/src/components/appconfig/config.ts b/src/components/appconfig/config.ts
deleted file mode 100644
index 1d55d5b8..00000000
--- a/src/components/appconfig/config.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { PluginMeta } from '@grafana/data';
-import { getBackendSrv } from '@grafana/runtime';
-import appPluginConfig from '../../plugin.json';
-import './css/config.css';
-
-export class PCPAppConfigCtrl {
- static templateUrl = 'components/appconfig/config.html';
- appEditCtrl: any;
- appModel?: PluginMeta;
-
- constructor() {
- this.appEditCtrl.setPostUpdateHook(this.postUpdate.bind(this));
- }
-
- async getPcpFolder() {
- return await getBackendSrv()
- .fetch({
- method: 'GET',
- url: '/api/folders/performancecopilot-pcp-app',
- showErrorAlert: false,
- })
- .toPromise();
- }
-
- async createPcpFolder(): Promise {
- try {
- const folder = await this.getPcpFolder();
- return folder.data.id;
- } catch (error) {
- // folder does not exist
- const folder = await getBackendSrv().post('/api/folders', {
- uid: 'performancecopilot-pcp-app',
- title: 'Performance Co-Pilot',
- });
- return folder.id;
- }
- }
-
- async moveDashboardToFolder(dashboardUid: string, folderId: number) {
- const getDashboardResponse = await getBackendSrv().get(`/api/dashboards/uid/${dashboardUid}`);
- if (getDashboardResponse.meta.folderId !== folderId) {
- await getBackendSrv().post('/api/dashboards/db', {
- dashboard: getDashboardResponse.dashboard,
- folderId,
- overwrite: true,
- });
- }
- }
-
- async moveDashboardsToPcpFolder() {
- const pcpFolderId = await this.createPcpFolder();
- const dashboardUids = appPluginConfig.includes
- .filter(i => i.type === 'dashboard')
- .map(d => d.path?.match(/\/([^/]*)\.json/)![1]);
-
- await Promise.all(dashboardUids.map(dashboardUid => this.moveDashboardToFolder(dashboardUid!, pcpFolderId)));
- }
-
- async deletePcpFolderIfEmpty() {
- let folder;
- try {
- folder = await this.getPcpFolder();
- } catch (error) {
- // folder does not exist
- return;
- }
-
- const searchResponse = await getBackendSrv().get('/api/search', { folderIds: folder.data.id });
- if (searchResponse.length === 0) {
- // delete folder only if empty
- await getBackendSrv().delete('/api/folders/performancecopilot-pcp-app');
- }
- }
-
- async postUpdate() {
- if (this.appModel?.enabled) {
- await this.moveDashboardsToPcpFolder();
- } else {
- await this.deletePcpFolderIfEmpty();
- }
- }
-}
diff --git a/src/components/appconfig/config.tsx b/src/components/appconfig/config.tsx
new file mode 100644
index 00000000..72121e6d
--- /dev/null
+++ b/src/components/appconfig/config.tsx
@@ -0,0 +1,85 @@
+import { AppPluginMeta, PluginConfigPageProps } from '@grafana/data';
+import { BackendSrv, getBackendSrv } from '@grafana/runtime';
+import { Button, Icon } from '@grafana/ui';
+import { css } from 'emotion';
+import React, { PureComponent } from 'react';
+import { AppSettings } from './types';
+
+interface Props extends PluginConfigPageProps> {}
+
+export class AppConfig extends PureComponent {
+ private backendSrv: BackendSrv;
+
+ constructor(props: Props) {
+ super(props);
+ this.backendSrv = getBackendSrv();
+ this.onEnable = this.onEnable.bind(this);
+ this.onDisable = this.onDisable.bind(this);
+ }
+
+ async updatePluginSettings(settings: { enabled: boolean; jsonData: any; pinned: boolean }) {
+ return this.backendSrv.post(`/api/plugins/${this.props.plugin.meta.id}/settings`, settings);
+ }
+
+ async onEnable() {
+ await this.updatePluginSettings({ enabled: true, jsonData: {}, pinned: true });
+ window.location.reload();
+ }
+
+ async onDisable() {
+ await this.updatePluginSettings({ enabled: false, jsonData: {}, pinned: false });
+ window.location.reload();
+ }
+
+ render() {
+ const isEnabled = this.props.plugin.meta.enabled;
+ return (
+ <>
+ Performance Co-Pilot App
+ This app integrates metrics from Performance Co-Pilot.
+
+
+ It includes the following datasources:
+
+ -
+ PCP Redis for fast, scalable time series aggregation across multiple hosts
+
+ -
+ PCP Vector for live, on-host metrics analysis, with container support
+
+ -
+ PCP bpftrace for system introspection using bpftrace scripts
+
+
+ {isEnabled && (
+
+ {' '}
+ Plugin enabled. Please configure the datasources now.
+
+ )}
+
+ {isEnabled ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+ }
+}
diff --git a/src/components/appconfig/css/config.css b/src/components/appconfig/css/config.css
deleted file mode 100644
index 3cfde78b..00000000
--- a/src/components/appconfig/css/config.css
+++ /dev/null
@@ -1,14 +0,0 @@
-ul.pcp {
- margin-left: 2em;
-}
-
-.pcp.pluginconfig-message {
- margin-top: 1.5em;
-}
-
-.pcp-icon-success {
- color: #10a345;
- font-size: 24px;
- text-decoration: none;
- vertical-align: sub;
-}
\ No newline at end of file
diff --git a/src/components/appconfig/types.ts b/src/components/appconfig/types.ts
new file mode 100644
index 00000000..9222e355
--- /dev/null
+++ b/src/components/appconfig/types.ts
@@ -0,0 +1 @@
+export interface AppSettings {}
diff --git a/src/module.ts b/src/module.ts
index c351345c..0e89426b 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -1,9 +1,8 @@
import { AppPlugin } from '@grafana/data';
-import { PCPAppConfigCtrl } from './components/appconfig/config';
+import { AppConfig } from './components/appconfig/config';
+import { AppSettings } from './components/appconfig/types';
import { Search } from './components/search/Search';
-export { PCPAppConfigCtrl as ConfigCtrl };
-
-class CustomApp extends AppPlugin<{}> {}
-
-export const plugin = new CustomApp().setRootPage(Search);
+export const plugin = new AppPlugin()
+ .addConfigPage({ id: 'config', title: 'Config', icon: 'cog', body: AppConfig })
+ .setRootPage(Search);