@grafana/create-plugin 1.4.0 → 1.4.1-canary.250.42054cd.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/generate.plopfile.js +7 -6
- package/dist/constants.js +2 -0
- package/package.json +4 -2
- package/scripts/generate-scenes-app.js +24 -0
- package/src/commands/generate.plopfile.ts +10 -8
- package/src/commands/types.ts +8 -0
- package/src/constants.ts +2 -0
- package/templates/common/package.json +6 -5
- package/templates/scenes-app/README.md +23 -0
- package/templates/scenes-app/provisioning/datasources/default.yaml +6 -0
- package/templates/scenes-app/src/components/App/App.tsx +14 -0
- package/templates/scenes-app/src/components/App/index.tsx +1 -0
- package/templates/scenes-app/src/components/AppConfig/AppConfig.tsx +193 -0
- package/templates/scenes-app/src/components/AppConfig/index.tsx +1 -0
- package/templates/scenes-app/src/components/Routes/Routes.tsx +18 -0
- package/templates/scenes-app/src/components/Routes/index.tsx +1 -0
- package/templates/scenes-app/src/components/SecretInput/SecretInput.tsx +25 -0
- package/templates/scenes-app/src/components/SecretInput/index.tsx +1 -0
- package/templates/scenes-app/src/components/testIds.ts +22 -0
- package/templates/scenes-app/src/constants.ts +14 -0
- package/templates/scenes-app/src/img/logo.svg +1 -0
- package/templates/scenes-app/src/module.ts +10 -0
- package/templates/scenes-app/src/pages/Home/CustomSceneObject.tsx +31 -0
- package/templates/scenes-app/src/pages/Home/Home.tsx +49 -0
- package/templates/scenes-app/src/pages/Home/index.tsx +1 -0
- package/templates/scenes-app/src/pages/Home/scenes.tsx +100 -0
- package/templates/scenes-app/src/pages/WithDrilldown/WithDrilldown.tsx +97 -0
- package/templates/scenes-app/src/pages/WithDrilldown/index.tsx +1 -0
- package/templates/scenes-app/src/pages/WithDrilldown/panels.tsx +188 -0
- package/templates/scenes-app/src/pages/WithDrilldown/scenes.tsx +122 -0
- package/templates/scenes-app/src/pages/WithTabs/WithTabs.tsx +45 -0
- package/templates/scenes-app/src/pages/WithTabs/index.tsx +1 -0
- package/templates/scenes-app/src/plugin.json +54 -0
- package/templates/scenes-app/src/utils/utils.plugin.ts +17 -0
- package/templates/scenes-app/src/utils/utils.routing.ts +6 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { SceneApp, SceneAppPage } from '@grafana/scenes';
|
|
4
|
+
import { getBasicScene } from './scenes';
|
|
5
|
+
import { prefixRoute } from '../../utils/utils.routing';
|
|
6
|
+
import { DATASOURCE_REF, ROUTES } from '../../constants';
|
|
7
|
+
import { config } from '@grafana/runtime';
|
|
8
|
+
import { Alert } from '@grafana/ui';
|
|
9
|
+
|
|
10
|
+
const getScene = () => {
|
|
11
|
+
return new SceneApp({
|
|
12
|
+
pages: [
|
|
13
|
+
new SceneAppPage({
|
|
14
|
+
title: 'Home page',
|
|
15
|
+
subTitle:
|
|
16
|
+
'This scene showcases a basic scene functionality, including query runner, variable and a custom scene object.',
|
|
17
|
+
url: prefixRoute(ROUTES.Home),
|
|
18
|
+
getScene: () => {
|
|
19
|
+
return getBasicScene();
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
export const HomePage = () => {
|
|
26
|
+
const scene = useMemo(() => getScene(), []);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{!config.featureToggles.topnav && (
|
|
31
|
+
<Alert title="Missing topnav feature toggle">
|
|
32
|
+
Scenes are designed to work with the new navigation wrapper that will be standard in Grafana 10
|
|
33
|
+
</Alert>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
{!config.datasources[DATASOURCE_REF.uid] && (
|
|
37
|
+
<Alert title={`Missing ${DATASOURCE_REF.uid} datasource`}>
|
|
38
|
+
These demos depend on <b>testdata</b> datasource: <code>{JSON.stringify(DATASOURCE_REF)}</code>. See{' '}
|
|
39
|
+
<a href="https://github.com/grafana/grafana/tree/main/devenv#set-up-your-development-environment">
|
|
40
|
+
https://github.com/grafana/grafana/tree/main/devenv#set-up-your-development-environment
|
|
41
|
+
</a>{' '}
|
|
42
|
+
for more details.
|
|
43
|
+
</Alert>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
<scene.Component model={scene} />
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Home';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CustomVariable,
|
|
3
|
+
EmbeddedScene,
|
|
4
|
+
SceneControlsSpacer,
|
|
5
|
+
SceneFlexItem,
|
|
6
|
+
SceneFlexLayout,
|
|
7
|
+
SceneQueryRunner,
|
|
8
|
+
SceneRefreshPicker,
|
|
9
|
+
SceneTimePicker,
|
|
10
|
+
SceneTimeRange,
|
|
11
|
+
SceneVariableSet,
|
|
12
|
+
VariableValueSelectors,
|
|
13
|
+
VizPanel,
|
|
14
|
+
} from '@grafana/scenes';
|
|
15
|
+
import { DATASOURCE_REF } from '../../constants';
|
|
16
|
+
import { CustomSceneObject } from './CustomSceneObject';
|
|
17
|
+
|
|
18
|
+
export function getBasicScene(templatised = true, seriesToShow = '__server_names') {
|
|
19
|
+
const timeRange = new SceneTimeRange({
|
|
20
|
+
from: 'now-6h',
|
|
21
|
+
to: 'now',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Variable definition, using Grafana built-in TestData datasource
|
|
25
|
+
const customVariable = new CustomVariable({
|
|
26
|
+
name: 'seriesToShow',
|
|
27
|
+
label: 'Series to show',
|
|
28
|
+
value: '__server_names',
|
|
29
|
+
query: 'Server Names : __server_names, House locations : __house_locations',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Query runner definition, using Grafana built-in TestData datasource
|
|
33
|
+
const queryRunner = new SceneQueryRunner({
|
|
34
|
+
datasource: DATASOURCE_REF,
|
|
35
|
+
queries: [
|
|
36
|
+
{
|
|
37
|
+
refId: 'A',
|
|
38
|
+
datasource: DATASOURCE_REF,
|
|
39
|
+
scenarioId: 'random_walk',
|
|
40
|
+
seriesCount: 5,
|
|
41
|
+
// Query is using variable value
|
|
42
|
+
alias: templatised ? '${seriesToShow}' : seriesToShow,
|
|
43
|
+
min: 30,
|
|
44
|
+
max: 60,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
maxDataPoints: 100,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Custom object definition
|
|
51
|
+
const customObject = new CustomSceneObject({
|
|
52
|
+
counter: 5,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Query runner activation handler that will update query runner state when custom object state changes
|
|
56
|
+
queryRunner.addActivationHandler(() => {
|
|
57
|
+
const sub = customObject.subscribeToState((newState) => {
|
|
58
|
+
queryRunner.setState({
|
|
59
|
+
queries: [
|
|
60
|
+
{
|
|
61
|
+
...queryRunner.state.queries[0],
|
|
62
|
+
seriesCount: newState.counter,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
queryRunner.runQueries();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
sub.unsubscribe();
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return new EmbeddedScene({
|
|
75
|
+
$timeRange: timeRange,
|
|
76
|
+
$variables: new SceneVariableSet({ variables: templatised ? [customVariable] : [] }),
|
|
77
|
+
$data: queryRunner,
|
|
78
|
+
body: new SceneFlexLayout({
|
|
79
|
+
children: [
|
|
80
|
+
new SceneFlexItem({
|
|
81
|
+
body: new VizPanel({
|
|
82
|
+
pluginId: 'timeseries',
|
|
83
|
+
// Title is using variable value
|
|
84
|
+
title: templatised ? '${seriesToShow}' : seriesToShow,
|
|
85
|
+
}),
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
controls: [
|
|
90
|
+
new VariableValueSelectors({}),
|
|
91
|
+
new SceneControlsSpacer(),
|
|
92
|
+
customObject,
|
|
93
|
+
new SceneTimePicker({ isOnCanvas: true }),
|
|
94
|
+
new SceneRefreshPicker({
|
|
95
|
+
intervals: ['5s', '1m', '1h'],
|
|
96
|
+
isOnCanvas: true,
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { prefixRoute } from '../../utils/utils.routing';
|
|
3
|
+
import { DATASOURCE_REF, ROUTES } from '../../constants';
|
|
4
|
+
import {
|
|
5
|
+
EmbeddedScene,
|
|
6
|
+
SceneApp,
|
|
7
|
+
SceneAppPage,
|
|
8
|
+
SceneFlexItem,
|
|
9
|
+
SceneFlexLayout,
|
|
10
|
+
SceneQueryRunner,
|
|
11
|
+
} from '@grafana/scenes';
|
|
12
|
+
import { getHumidityOverviewScene, getTemperatureOverviewScene } from './scenes';
|
|
13
|
+
import { getRoomsTemperatureStats, getRoomsTemperatureTable } from './panels';
|
|
14
|
+
|
|
15
|
+
const roomsTemperatureQuery = {
|
|
16
|
+
refId: 'Rooms temperature',
|
|
17
|
+
datasource: DATASOURCE_REF,
|
|
18
|
+
scenarioId: 'random_walk',
|
|
19
|
+
seriesCount: 8,
|
|
20
|
+
alias: '__house_locations',
|
|
21
|
+
min: 10,
|
|
22
|
+
max: 27,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getScene = () =>
|
|
26
|
+
new EmbeddedScene({
|
|
27
|
+
$data: new SceneQueryRunner({
|
|
28
|
+
datasource: DATASOURCE_REF,
|
|
29
|
+
queries: [roomsTemperatureQuery],
|
|
30
|
+
maxDataPoints: 100,
|
|
31
|
+
}),
|
|
32
|
+
|
|
33
|
+
body: new SceneFlexLayout({
|
|
34
|
+
direction: 'column',
|
|
35
|
+
children: [
|
|
36
|
+
new SceneFlexItem({
|
|
37
|
+
height: 300,
|
|
38
|
+
body: getRoomsTemperatureTable(),
|
|
39
|
+
}),
|
|
40
|
+
new SceneFlexItem({
|
|
41
|
+
ySizing: 'fill',
|
|
42
|
+
body: getRoomsTemperatureStats(),
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const getDrilldownsAppScene = () => {
|
|
49
|
+
return new SceneApp({
|
|
50
|
+
pages: [
|
|
51
|
+
new SceneAppPage({
|
|
52
|
+
title: 'Page with drilldown',
|
|
53
|
+
subTitle: 'This scene showcases a basic drilldown functionality. Interact with room to see room details scene.',
|
|
54
|
+
|
|
55
|
+
url: prefixRoute(`${ROUTES.WithDrilldown}`),
|
|
56
|
+
hideFromBreadcrumbs: true,
|
|
57
|
+
getScene,
|
|
58
|
+
drilldowns: [
|
|
59
|
+
{
|
|
60
|
+
routePath: prefixRoute(`${ROUTES.WithDrilldown}`) + '/room/:roomName',
|
|
61
|
+
getPage(routeMatch, parent) {
|
|
62
|
+
const roomName = routeMatch.params.roomName;
|
|
63
|
+
|
|
64
|
+
return new SceneAppPage({
|
|
65
|
+
url: prefixRoute(`${ROUTES.WithDrilldown}`) + `/room/${roomName}/temperature`,
|
|
66
|
+
title: `${roomName} overview`,
|
|
67
|
+
subTitle: 'This scene is a particular room drilldown. It implements two tabs to organise the data.',
|
|
68
|
+
getParentPage: () => parent,
|
|
69
|
+
getScene: () => {
|
|
70
|
+
return new EmbeddedScene({ body: new SceneFlexLayout({ children: [] }) });
|
|
71
|
+
},
|
|
72
|
+
tabs: [
|
|
73
|
+
new SceneAppPage({
|
|
74
|
+
title: 'Temperature',
|
|
75
|
+
url: prefixRoute(`${ROUTES.WithDrilldown}`) + `/room/${roomName}/temperature`,
|
|
76
|
+
getScene: () => getTemperatureOverviewScene(roomName),
|
|
77
|
+
}),
|
|
78
|
+
new SceneAppPage({
|
|
79
|
+
title: 'Humidity',
|
|
80
|
+
url: prefixRoute(`${ROUTES.WithDrilldown}`) + `/room/${roomName}/humidity`,
|
|
81
|
+
getScene: () => getHumidityOverviewScene(roomName),
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const WithDrilldown = () => {
|
|
94
|
+
const scene = useMemo(() => getDrilldownsAppScene(), []);
|
|
95
|
+
|
|
96
|
+
return <scene.Component model={scene} />;
|
|
97
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './WithDrilldown';
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { ReducerID, ThresholdsMode } from '@grafana/data';
|
|
2
|
+
import {
|
|
3
|
+
SceneByFrameRepeater,
|
|
4
|
+
SceneDataNode,
|
|
5
|
+
SceneDataTransformer,
|
|
6
|
+
SceneFlexItem,
|
|
7
|
+
SceneFlexLayout,
|
|
8
|
+
VizPanel,
|
|
9
|
+
} from '@grafana/scenes';
|
|
10
|
+
import { ROUTES } from '../../constants';
|
|
11
|
+
import { prefixRoute } from '../../utils/utils.routing';
|
|
12
|
+
|
|
13
|
+
export function getRoomsTemperatureTable() {
|
|
14
|
+
return new VizPanel({
|
|
15
|
+
pluginId: 'table',
|
|
16
|
+
$data: new SceneDataTransformer({
|
|
17
|
+
transformations: [
|
|
18
|
+
{
|
|
19
|
+
id: 'reduce',
|
|
20
|
+
options: {
|
|
21
|
+
reducers: ['mean'],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'organize',
|
|
26
|
+
options: {
|
|
27
|
+
excludeByName: {},
|
|
28
|
+
indexByName: {},
|
|
29
|
+
renameByName: {
|
|
30
|
+
Field: 'Room',
|
|
31
|
+
Mean: 'Average temperature',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
}),
|
|
37
|
+
displayMode: 'transparent',
|
|
38
|
+
title: 'Room temperature overview',
|
|
39
|
+
hoverHeader: true,
|
|
40
|
+
options: {
|
|
41
|
+
sortBy: ['Average temperature'],
|
|
42
|
+
},
|
|
43
|
+
fieldConfig: {
|
|
44
|
+
defaults: {
|
|
45
|
+
custom: {
|
|
46
|
+
align: 'auto',
|
|
47
|
+
cellOptions: {
|
|
48
|
+
type: 'auto',
|
|
49
|
+
},
|
|
50
|
+
inspect: false,
|
|
51
|
+
},
|
|
52
|
+
mappings: [],
|
|
53
|
+
thresholds: {
|
|
54
|
+
mode: ThresholdsMode.Absolute,
|
|
55
|
+
steps: [
|
|
56
|
+
{
|
|
57
|
+
color: 'light-blue',
|
|
58
|
+
value: 0,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
color: 'orange',
|
|
62
|
+
value: 19,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
color: 'dark-red',
|
|
66
|
+
value: 26,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
color: {
|
|
71
|
+
mode: 'thresholds',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
overrides: [
|
|
75
|
+
{
|
|
76
|
+
matcher: {
|
|
77
|
+
id: 'byName',
|
|
78
|
+
options: 'Average temperature',
|
|
79
|
+
},
|
|
80
|
+
properties: [
|
|
81
|
+
{
|
|
82
|
+
id: 'unit',
|
|
83
|
+
value: 'celsius',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'custom.cellOptions',
|
|
87
|
+
value: {
|
|
88
|
+
type: 'color-background',
|
|
89
|
+
mode: 'basic',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'custom.width',
|
|
94
|
+
value: 200,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'custom.align',
|
|
98
|
+
value: 'center',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
matcher: {
|
|
104
|
+
id: 'byName',
|
|
105
|
+
options: 'Room',
|
|
106
|
+
},
|
|
107
|
+
properties: [
|
|
108
|
+
{
|
|
109
|
+
id: 'links',
|
|
110
|
+
value: [
|
|
111
|
+
{
|
|
112
|
+
title: 'Go to room overview',
|
|
113
|
+
url: prefixRoute(`${ROUTES.WithDrilldown}`) + '/room/${__value.text}/temperature',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getRoomsTemperatureStats() {
|
|
125
|
+
return new SceneByFrameRepeater({
|
|
126
|
+
body: new SceneFlexLayout({
|
|
127
|
+
direction: 'row',
|
|
128
|
+
wrap: 'wrap',
|
|
129
|
+
children: [],
|
|
130
|
+
}),
|
|
131
|
+
getLayoutChild: (data, frame) => {
|
|
132
|
+
return new SceneFlexItem({
|
|
133
|
+
height: '50%',
|
|
134
|
+
minWidth: '20%',
|
|
135
|
+
body: new VizPanel({
|
|
136
|
+
title: frame.name,
|
|
137
|
+
pluginId: 'stat',
|
|
138
|
+
|
|
139
|
+
$data: new SceneDataNode({
|
|
140
|
+
data: {
|
|
141
|
+
...data,
|
|
142
|
+
series: [frame],
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
fieldConfig: {
|
|
146
|
+
defaults: {
|
|
147
|
+
unit: 'celsius',
|
|
148
|
+
links: [
|
|
149
|
+
{
|
|
150
|
+
title: 'Go to room temperature overview',
|
|
151
|
+
url: prefixRoute(`${ROUTES.WithDrilldown}`) + '/room/${__field.name}/temperature',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
title: 'Go to room humidity overview',
|
|
155
|
+
url: prefixRoute(`${ROUTES.WithDrilldown}`) + '/room/${__field.name}/humidity',
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
overrides: [],
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getRoomTemperatureStatPanel(reducers: ReducerID[]) {
|
|
168
|
+
return new VizPanel({
|
|
169
|
+
pluginId: 'stat',
|
|
170
|
+
title: '',
|
|
171
|
+
$data: new SceneDataTransformer({
|
|
172
|
+
transformations: [
|
|
173
|
+
{
|
|
174
|
+
id: 'reduce',
|
|
175
|
+
options: {
|
|
176
|
+
reducers,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}),
|
|
181
|
+
fieldConfig: {
|
|
182
|
+
defaults: {
|
|
183
|
+
unit: 'celsius',
|
|
184
|
+
},
|
|
185
|
+
overrides: [],
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ReducerID } from '@grafana/data';
|
|
2
|
+
import {
|
|
3
|
+
EmbeddedScene,
|
|
4
|
+
SceneFlexLayout,
|
|
5
|
+
SceneQueryRunner,
|
|
6
|
+
VizPanel,
|
|
7
|
+
SceneTimeRange,
|
|
8
|
+
SceneTimePicker,
|
|
9
|
+
SceneControlsSpacer,
|
|
10
|
+
SceneRefreshPicker,
|
|
11
|
+
SceneFlexItem,
|
|
12
|
+
} from '@grafana/scenes';
|
|
13
|
+
import { DATASOURCE_REF } from '../../constants';
|
|
14
|
+
import { getRoomTemperatureStatPanel } from './panels';
|
|
15
|
+
|
|
16
|
+
export function getTemperatureOverviewScene(roomName: string) {
|
|
17
|
+
return new EmbeddedScene({
|
|
18
|
+
$timeRange: new SceneTimeRange({
|
|
19
|
+
from: 'now-12h',
|
|
20
|
+
to: 'now',
|
|
21
|
+
}),
|
|
22
|
+
$data: new SceneQueryRunner({
|
|
23
|
+
datasource: DATASOURCE_REF,
|
|
24
|
+
queries: [getRoomTemperatureQuery(roomName)],
|
|
25
|
+
maxDataPoints: 100,
|
|
26
|
+
}),
|
|
27
|
+
body: new SceneFlexLayout({
|
|
28
|
+
direction: 'column',
|
|
29
|
+
children: [
|
|
30
|
+
new SceneFlexItem({
|
|
31
|
+
height: 500,
|
|
32
|
+
body: new VizPanel({
|
|
33
|
+
title: 'Temperature over time',
|
|
34
|
+
pluginId: 'timeseries',
|
|
35
|
+
|
|
36
|
+
fieldConfig: {
|
|
37
|
+
defaults: {
|
|
38
|
+
unit: 'celsius',
|
|
39
|
+
},
|
|
40
|
+
overrides: [],
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
}),
|
|
44
|
+
new SceneFlexItem({
|
|
45
|
+
body: new SceneFlexLayout({
|
|
46
|
+
direction: 'row',
|
|
47
|
+
children: [
|
|
48
|
+
new SceneFlexItem({
|
|
49
|
+
body: getRoomTemperatureStatPanel([ReducerID.min]),
|
|
50
|
+
}),
|
|
51
|
+
new SceneFlexItem({
|
|
52
|
+
body: getRoomTemperatureStatPanel([ReducerID.max]),
|
|
53
|
+
}),
|
|
54
|
+
new SceneFlexItem({
|
|
55
|
+
body: getRoomTemperatureStatPanel([ReducerID.mean]),
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
}),
|
|
62
|
+
controls: [new SceneControlsSpacer(), new SceneTimePicker({ isOnCanvas: true }), new SceneRefreshPicker({})],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getHumidityOverviewScene(roomName: string) {
|
|
67
|
+
return new EmbeddedScene({
|
|
68
|
+
$timeRange: new SceneTimeRange({
|
|
69
|
+
from: 'now-12h',
|
|
70
|
+
to: 'now',
|
|
71
|
+
}),
|
|
72
|
+
$data: new SceneQueryRunner({
|
|
73
|
+
datasource: DATASOURCE_REF,
|
|
74
|
+
queries: [getRoomHumidityQuery(roomName)],
|
|
75
|
+
maxDataPoints: 100,
|
|
76
|
+
}),
|
|
77
|
+
body: new SceneFlexLayout({
|
|
78
|
+
direction: 'column',
|
|
79
|
+
children: [
|
|
80
|
+
new SceneFlexItem({
|
|
81
|
+
height: 500,
|
|
82
|
+
body: new VizPanel({
|
|
83
|
+
title: 'Humidity readings over time',
|
|
84
|
+
pluginId: 'timeseries',
|
|
85
|
+
|
|
86
|
+
fieldConfig: {
|
|
87
|
+
defaults: {
|
|
88
|
+
unit: 'humidity',
|
|
89
|
+
},
|
|
90
|
+
overrides: [],
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
94
|
+
],
|
|
95
|
+
}),
|
|
96
|
+
controls: [new SceneControlsSpacer(), new SceneTimePicker({ isOnCanvas: true }), new SceneRefreshPicker({})],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getRoomTemperatureQuery(roomName: string) {
|
|
101
|
+
return {
|
|
102
|
+
refId: 'Temp',
|
|
103
|
+
datasource: DATASOURCE_REF,
|
|
104
|
+
scenarioId: 'random_walk',
|
|
105
|
+
seriesCount: 1,
|
|
106
|
+
alias: roomName,
|
|
107
|
+
min: 10,
|
|
108
|
+
max: 30,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getRoomHumidityQuery(roomName: string) {
|
|
113
|
+
return {
|
|
114
|
+
refId: 'Humidity',
|
|
115
|
+
datasource: DATASOURCE_REF,
|
|
116
|
+
scenarioId: 'random_walk',
|
|
117
|
+
seriesCount: 1,
|
|
118
|
+
alias: roomName,
|
|
119
|
+
min: 30,
|
|
120
|
+
max: 60,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { SceneApp, SceneAppPage } from '@grafana/scenes';
|
|
3
|
+
import { ROUTES } from '../../constants';
|
|
4
|
+
import { prefixRoute } from '../../utils/utils.routing';
|
|
5
|
+
import { getBasicScene } from '../Home/scenes';
|
|
6
|
+
|
|
7
|
+
const getTab1Scene = () => {
|
|
8
|
+
return getBasicScene(false, '__server_names');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const getTab2Scene = () => {
|
|
12
|
+
return getBasicScene(false, '__house_locations');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const getScene = () =>
|
|
16
|
+
new SceneApp({
|
|
17
|
+
pages: [
|
|
18
|
+
new SceneAppPage({
|
|
19
|
+
title: 'Page with tabs',
|
|
20
|
+
subTitle: 'This scene showcases a basic tabs functionality.',
|
|
21
|
+
// Important: Mind the page route is ambiguous for the tabs to work properly
|
|
22
|
+
url: prefixRoute(`${ROUTES.WithTabs}`),
|
|
23
|
+
hideFromBreadcrumbs: true,
|
|
24
|
+
getScene: getTab1Scene,
|
|
25
|
+
tabs: [
|
|
26
|
+
new SceneAppPage({
|
|
27
|
+
title: 'Server names',
|
|
28
|
+
url: prefixRoute(`${ROUTES.WithTabs}`),
|
|
29
|
+
getScene: getTab1Scene,
|
|
30
|
+
}),
|
|
31
|
+
new SceneAppPage({
|
|
32
|
+
title: 'House locations',
|
|
33
|
+
url: prefixRoute(`${ROUTES.WithTabs}/tab-two`),
|
|
34
|
+
getScene: getTab2Scene,
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const PageWithTabs = () => {
|
|
42
|
+
const scene = useMemo(() => getScene(), []);
|
|
43
|
+
|
|
44
|
+
return <scene.Component model={scene} />;
|
|
45
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './WithTabs';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json",
|
|
3
|
+
"type": "app",
|
|
4
|
+
"name": "{{ titleCase pluginName }}",
|
|
5
|
+
"id": "{{ normalize_id pluginName orgName 'app' }}",{{#if hasBackend}}
|
|
6
|
+
"backend": true,
|
|
7
|
+
"executable": "gpx_{{ snakeCase pluginName }}",{{/if}}
|
|
8
|
+
"info": {
|
|
9
|
+
"keywords": [
|
|
10
|
+
"app"
|
|
11
|
+
],
|
|
12
|
+
"description": "{{ sentenceCase pluginDescription }}",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "{{ sentenceCase orgName }}"
|
|
15
|
+
},
|
|
16
|
+
"logos": {
|
|
17
|
+
"small": "img/logo.svg",
|
|
18
|
+
"large": "img/logo.svg"
|
|
19
|
+
},
|
|
20
|
+
"screenshots": [],
|
|
21
|
+
"version": "%VERSION%",
|
|
22
|
+
"updated": "%TODAY%"
|
|
23
|
+
},
|
|
24
|
+
"includes": [
|
|
25
|
+
{
|
|
26
|
+
"type": "page",
|
|
27
|
+
"name": "Home",
|
|
28
|
+
"path": "/a/%PLUGIN_ID%/home",
|
|
29
|
+
"role": "Admin",
|
|
30
|
+
"addToNav": true,
|
|
31
|
+
"defaultNav": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"type": "page",
|
|
35
|
+
"name": "Page with tabs",
|
|
36
|
+
"path": "/a/%PLUGIN_ID%/page-with-tabs",
|
|
37
|
+
"role": "Admin",
|
|
38
|
+
"addToNav": true,
|
|
39
|
+
"defaultNav": false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "page",
|
|
43
|
+
"name": "Page with drilldown",
|
|
44
|
+
"path": "/a/%PLUGIN_ID%/page-with-drilldown",
|
|
45
|
+
"role": "Admin",
|
|
46
|
+
"addToNav": true,
|
|
47
|
+
"defaultNav": false
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"grafanaDependency": ">={{ grafanaVersion }}",
|
|
52
|
+
"plugins": []
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { AppRootProps } from '@grafana/data';
|
|
3
|
+
|
|
4
|
+
// This is used to be able to retrieve the root plugin props anywhere inside the app.
|
|
5
|
+
export const PluginPropsContext = React.createContext<AppRootProps | null>(null);
|
|
6
|
+
|
|
7
|
+
export const usePluginProps = () => {
|
|
8
|
+
const pluginProps = useContext(PluginPropsContext);
|
|
9
|
+
|
|
10
|
+
return pluginProps;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const usePluginMeta = () => {
|
|
14
|
+
const pluginProps = usePluginProps();
|
|
15
|
+
|
|
16
|
+
return pluginProps?.meta;
|
|
17
|
+
};
|