@perses-dev/dashboards 0.0.0-snapshot-panel-actions-520389b → 0.0.0-snapshot-timeseries-panel-actions-e28c1fe
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/cjs/components/DownloadButton/DownloadButton.js +3 -33
- package/dist/cjs/components/DownloadButton/serializeDashboard.js +64 -0
- package/dist/cjs/components/Panel/Panel.js +73 -48
- package/dist/cjs/components/Panel/PanelActions.js +38 -71
- package/dist/cjs/components/Panel/PanelHeader.js +7 -43
- package/dist/components/DownloadButton/DownloadButton.d.ts.map +1 -1
- package/dist/components/DownloadButton/DownloadButton.js +3 -33
- package/dist/components/DownloadButton/DownloadButton.js.map +1 -1
- package/dist/components/DownloadButton/serializeDashboard.d.ts +8 -0
- package/dist/components/DownloadButton/serializeDashboard.d.ts.map +1 -0
- package/dist/components/DownloadButton/serializeDashboard.js +56 -0
- package/dist/components/DownloadButton/serializeDashboard.js.map +1 -0
- package/dist/components/Panel/Panel.d.ts +0 -5
- package/dist/components/Panel/Panel.d.ts.map +1 -1
- package/dist/components/Panel/Panel.js +75 -50
- package/dist/components/Panel/Panel.js.map +1 -1
- package/dist/components/Panel/PanelActions.d.ts +5 -6
- package/dist/components/Panel/PanelActions.d.ts.map +1 -1
- package/dist/components/Panel/PanelActions.js +40 -73
- package/dist/components/Panel/PanelActions.js.map +1 -1
- package/dist/components/Panel/PanelHeader.d.ts +2 -5
- package/dist/components/Panel/PanelHeader.d.ts.map +1 -1
- package/dist/components/Panel/PanelHeader.js +7 -43
- package/dist/components/Panel/PanelHeader.js.map +1 -1
- package/package.json +5 -5
|
@@ -25,8 +25,8 @@ const _material = require("@mui/material");
|
|
|
25
25
|
const _components = require("@perses-dev/components");
|
|
26
26
|
const _DownloadOutline = /*#__PURE__*/ _interop_require_default(require("mdi-material-ui/DownloadOutline"));
|
|
27
27
|
const _react = /*#__PURE__*/ _interop_require_wildcard(require("react"));
|
|
28
|
-
const _yaml = require("yaml");
|
|
29
28
|
const _context = require("../../context");
|
|
29
|
+
const _serializeDashboard = require("./serializeDashboard");
|
|
30
30
|
function _interop_require_default(obj) {
|
|
31
31
|
return obj && obj.__esModule ? obj : {
|
|
32
32
|
default: obj
|
|
@@ -83,43 +83,13 @@ function DownloadButton() {
|
|
|
83
83
|
};
|
|
84
84
|
const handleItemClick = (format, shape)=>()=>{
|
|
85
85
|
setAnchorEl(null);
|
|
86
|
-
|
|
87
|
-
switch(format){
|
|
88
|
-
case 'json':
|
|
89
|
-
type = 'application/json';
|
|
90
|
-
content = JSON.stringify(dashboard, null, 2);
|
|
91
|
-
break;
|
|
92
|
-
case 'yaml':
|
|
93
|
-
{
|
|
94
|
-
type = 'application/yaml';
|
|
95
|
-
if (shape === 'cr') {
|
|
96
|
-
const name = dashboard.metadata.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
97
|
-
content = (0, _yaml.stringify)({
|
|
98
|
-
apiVersion: 'perses.dev/v1alpha1',
|
|
99
|
-
kind: 'PersesDashboard',
|
|
100
|
-
metadata: {
|
|
101
|
-
labels: {
|
|
102
|
-
'app.kubernetes.io/name': 'perses-dashboard',
|
|
103
|
-
'app.kubernetes.io/instance': name,
|
|
104
|
-
'app.kubernetes.io/part-of': 'perses-operator'
|
|
105
|
-
},
|
|
106
|
-
name
|
|
107
|
-
},
|
|
108
|
-
namespace: dashboard.metadata.project,
|
|
109
|
-
spec: dashboard.spec
|
|
110
|
-
});
|
|
111
|
-
} else {
|
|
112
|
-
content = (0, _yaml.stringify)(dashboard);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
86
|
+
const { contentType, content } = (0, _serializeDashboard.serializeDashboard)(dashboard, format, shape);
|
|
117
87
|
if (!hiddenLinkRef || !hiddenLinkRef.current) return;
|
|
118
88
|
// Create blob URL
|
|
119
89
|
const hiddenLinkUrl = URL.createObjectURL(new Blob([
|
|
120
90
|
content
|
|
121
91
|
], {
|
|
122
|
-
type
|
|
92
|
+
type: contentType
|
|
123
93
|
}));
|
|
124
94
|
// Simulate click
|
|
125
95
|
hiddenLinkRef.current.download = `${dashboard.metadata.name}${shape === 'cr' ? '-cr' : ''}.${format}`;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright 2023 The Perses Authors
|
|
2
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
// you may not use this file except in compliance with the License.
|
|
4
|
+
// You may obtain a copy of the License at
|
|
5
|
+
//
|
|
6
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
"use strict";
|
|
14
|
+
Object.defineProperty(exports, "__esModule", {
|
|
15
|
+
value: true
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(exports, "serializeDashboard", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function() {
|
|
20
|
+
return serializeDashboard;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const _yaml = require("yaml");
|
|
24
|
+
function serializeYaml(dashboard, shape) {
|
|
25
|
+
let content;
|
|
26
|
+
if (shape === 'cr') {
|
|
27
|
+
const name = dashboard.metadata.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
28
|
+
content = (0, _yaml.stringify)({
|
|
29
|
+
apiVersion: 'perses.dev/v1alpha1',
|
|
30
|
+
kind: 'PersesDashboard',
|
|
31
|
+
metadata: {
|
|
32
|
+
labels: {
|
|
33
|
+
'app.kubernetes.io/name': 'perses-dashboard',
|
|
34
|
+
'app.kubernetes.io/instance': name,
|
|
35
|
+
'app.kubernetes.io/part-of': 'perses-operator'
|
|
36
|
+
},
|
|
37
|
+
name,
|
|
38
|
+
namespace: dashboard.metadata.project
|
|
39
|
+
},
|
|
40
|
+
spec: dashboard.spec
|
|
41
|
+
}, {
|
|
42
|
+
schema: 'yaml-1.1'
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
content = (0, _yaml.stringify)(dashboard, {
|
|
46
|
+
schema: 'yaml-1.1'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
contentType: 'application/yaml',
|
|
51
|
+
content
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function serializeDashboard(dashboard, format, shape) {
|
|
55
|
+
switch(format){
|
|
56
|
+
case 'json':
|
|
57
|
+
return {
|
|
58
|
+
contentType: 'application/json',
|
|
59
|
+
content: JSON.stringify(dashboard, null, 2)
|
|
60
|
+
};
|
|
61
|
+
case 'yaml':
|
|
62
|
+
return serializeYaml(dashboard, shape);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -33,41 +33,8 @@ function _interop_require_default(obj) {
|
|
|
33
33
|
default: obj
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
// Function to extract project name from URL (kept as fallback)
|
|
37
|
-
const extractProjectNameFromUrl = ()=>{
|
|
38
|
-
try {
|
|
39
|
-
if (process.env.NODE_ENV === 'test') {
|
|
40
|
-
return 'test-project';
|
|
41
|
-
}
|
|
42
|
-
if (typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
|
|
43
|
-
const urlPath = window.location.pathname;
|
|
44
|
-
if (urlPath === '/' || urlPath === '') {
|
|
45
|
-
return 'dev-project';
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const urlPath = window.location.pathname;
|
|
49
|
-
// Split the path and look for the project name after "/projects/"
|
|
50
|
-
const pathSegments = urlPath.split('/').filter((segment)=>segment.length > 0);
|
|
51
|
-
const projectsIndex = pathSegments.findIndex((segment)=>segment === 'projects');
|
|
52
|
-
if (projectsIndex !== -1 && projectsIndex + 1 < pathSegments.length) {
|
|
53
|
-
const projectName = pathSegments[projectsIndex + 1];
|
|
54
|
-
if (projectName && projectName.trim().length > 0) {
|
|
55
|
-
return projectName;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// Fallback: try to extract from URL parameters
|
|
59
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
60
|
-
const projectParam = urlParams.get('project');
|
|
61
|
-
if (projectParam && projectParam.trim().length > 0) {
|
|
62
|
-
return projectParam;
|
|
63
|
-
}
|
|
64
|
-
return 'unknown-project';
|
|
65
|
-
} catch {
|
|
66
|
-
return 'unknown-project';
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
36
|
const Panel = /*#__PURE__*/ (0, _react.memo)(function Panel(props) {
|
|
70
|
-
const { definition, readHandlers, editHandlers, onMouseEnter, onMouseLeave, sx, panelOptions, panelGroupItemId,
|
|
37
|
+
const { definition, readHandlers, editHandlers, onMouseEnter, onMouseLeave, sx, panelOptions, panelGroupItemId, ...others } = props;
|
|
71
38
|
// Make sure we have an ID we can use for aria attributes
|
|
72
39
|
const generatedPanelId = (0, _components.useId)('Panel');
|
|
73
40
|
const headerId = `${generatedPanelId}-header`;
|
|
@@ -87,12 +54,7 @@ const Panel = /*#__PURE__*/ (0, _react.memo)(function Panel(props) {
|
|
|
87
54
|
]);
|
|
88
55
|
const chartsTheme = (0, _components.useChartsTheme)();
|
|
89
56
|
const { queryResults } = (0, _pluginsystem.useDataQueriesContext)();
|
|
90
|
-
|
|
91
|
-
const projectName = (0, _react.useMemo)(()=>{
|
|
92
|
-
return providedProjectName || extractProjectNameFromUrl();
|
|
93
|
-
}, [
|
|
94
|
-
providedProjectName
|
|
95
|
-
]);
|
|
57
|
+
const { getPlugin } = (0, _pluginsystem.usePluginRegistry)();
|
|
96
58
|
const panelPropsForActions = (0, _react.useMemo)(()=>{
|
|
97
59
|
return {
|
|
98
60
|
spec: definition.spec.plugin.spec,
|
|
@@ -101,14 +63,78 @@ const Panel = /*#__PURE__*/ (0, _react.memo)(function Panel(props) {
|
|
|
101
63
|
data: query.data
|
|
102
64
|
})),
|
|
103
65
|
contentDimensions,
|
|
104
|
-
definition
|
|
105
|
-
projectName
|
|
66
|
+
definition
|
|
106
67
|
};
|
|
107
68
|
}, [
|
|
108
69
|
definition,
|
|
109
70
|
contentDimensions,
|
|
110
|
-
queryResults
|
|
111
|
-
|
|
71
|
+
queryResults
|
|
72
|
+
]);
|
|
73
|
+
// Load plugin actions from the plugin
|
|
74
|
+
const [pluginActions, setPluginActions] = (0, _react.useState)([]);
|
|
75
|
+
(0, _react.useEffect)(()=>{
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
const loadPluginActions = async ()=>{
|
|
78
|
+
const panelPluginKind = definition.spec.plugin.kind;
|
|
79
|
+
const panelProps = panelPropsForActions;
|
|
80
|
+
if (!panelPluginKind || !panelProps) {
|
|
81
|
+
if (!cancelled) {
|
|
82
|
+
setPluginActions([]);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
// Add defensive check for getPlugin availability
|
|
88
|
+
if (!getPlugin || typeof getPlugin !== 'function') {
|
|
89
|
+
if (!cancelled) {
|
|
90
|
+
setPluginActions([]);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const plugin = await getPlugin('Panel', panelPluginKind);
|
|
95
|
+
if (cancelled) return;
|
|
96
|
+
// More defensive checking for plugin and actions
|
|
97
|
+
if (!plugin || typeof plugin !== 'object' || !plugin.actions || !Array.isArray(plugin.actions) || plugin.actions.length === 0) {
|
|
98
|
+
if (!cancelled) {
|
|
99
|
+
setPluginActions([]);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Render plugin actions in header location
|
|
104
|
+
const headerActions = plugin.actions.filter((action)=>!action.location || action.location === 'header').map((action, index)=>{
|
|
105
|
+
const ActionComponent = action.component;
|
|
106
|
+
try {
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
return /*#__PURE__*/ (0, _jsxruntime.jsx)(ActionComponent, {
|
|
109
|
+
...panelProps
|
|
110
|
+
}, `plugin-action-${index}`);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn(`Failed to render plugin action ${index}:`, error);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}).filter((item)=>Boolean(item));
|
|
116
|
+
if (!cancelled) {
|
|
117
|
+
setPluginActions(headerActions);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (!cancelled) {
|
|
121
|
+
console.warn('Failed to load plugin actions:', error);
|
|
122
|
+
setPluginActions([]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// Use setTimeout to defer the async operation to the next tick
|
|
127
|
+
const timeoutId = setTimeout(()=>{
|
|
128
|
+
loadPluginActions();
|
|
129
|
+
}, 0);
|
|
130
|
+
return ()=>{
|
|
131
|
+
cancelled = true;
|
|
132
|
+
clearTimeout(timeoutId);
|
|
133
|
+
};
|
|
134
|
+
}, [
|
|
135
|
+
definition.spec.plugin.kind,
|
|
136
|
+
panelPropsForActions,
|
|
137
|
+
getPlugin
|
|
112
138
|
]);
|
|
113
139
|
const handleMouseEnter = (e)=>{
|
|
114
140
|
onMouseEnter?.(e);
|
|
@@ -144,12 +170,10 @@ const Panel = /*#__PURE__*/ (0, _react.memo)(function Panel(props) {
|
|
|
144
170
|
title: definition.spec.display.name,
|
|
145
171
|
description: definition.spec.display.description,
|
|
146
172
|
queryResults: queryResults,
|
|
147
|
-
panelPluginKind: definition.spec.plugin.kind,
|
|
148
173
|
readHandlers: readHandlers,
|
|
149
174
|
editHandlers: editHandlers,
|
|
150
175
|
links: definition.spec.links,
|
|
151
|
-
|
|
152
|
-
panelProps: panelPropsForActions,
|
|
176
|
+
pluginActions: pluginActions,
|
|
153
177
|
sx: {
|
|
154
178
|
paddingX: `${chartsTheme.container.padding.default}px`
|
|
155
179
|
}
|
|
@@ -171,7 +195,8 @@ const Panel = /*#__PURE__*/ (0, _react.memo)(function Panel(props) {
|
|
|
171
195
|
children: /*#__PURE__*/ (0, _jsxruntime.jsx)(_components.ErrorBoundary, {
|
|
172
196
|
FallbackComponent: _components.ErrorAlert,
|
|
173
197
|
resetKeys: [
|
|
174
|
-
definition.spec
|
|
198
|
+
definition.spec,
|
|
199
|
+
queryResults
|
|
175
200
|
],
|
|
176
201
|
children: /*#__PURE__*/ (0, _jsxruntime.jsx)(_PanelContent.PanelContent, {
|
|
177
202
|
definition: definition,
|
|
@@ -48,72 +48,10 @@ const ConditionalBox = (0, _material.styled)(_material.Box)({
|
|
|
48
48
|
flexGrow: 1,
|
|
49
49
|
justifyContent: 'flex-end'
|
|
50
50
|
});
|
|
51
|
-
const PanelActions = ({ editHandlers, readHandlers, extra, title, description, descriptionTooltipId, links,
|
|
51
|
+
const PanelActions = ({ editHandlers, readHandlers, extra, title, description, descriptionTooltipId, links, panelContentProps, pluginActions = [] })=>{
|
|
52
52
|
const { isFetching, errors } = (0, _pluginsystem.useDataQueriesContext)();
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
(0, _react.useEffect)(()=>{
|
|
56
|
-
let cancelled = false;
|
|
57
|
-
const loadPluginActions = async ()=>{
|
|
58
|
-
if (!panelPluginKind || !panelProps) {
|
|
59
|
-
if (!cancelled) {
|
|
60
|
-
setPluginActions([]);
|
|
61
|
-
}
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
// Add defensive check for getPlugin availability
|
|
66
|
-
if (!getPlugin || typeof getPlugin !== 'function') {
|
|
67
|
-
if (!cancelled) {
|
|
68
|
-
setPluginActions([]);
|
|
69
|
-
}
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const plugin = await getPlugin('Panel', panelPluginKind);
|
|
73
|
-
if (cancelled) return;
|
|
74
|
-
// More defensive checking for plugin and actions
|
|
75
|
-
if (!plugin || typeof plugin !== 'object' || !plugin.actions || !Array.isArray(plugin.actions) || plugin.actions.length === 0) {
|
|
76
|
-
if (!cancelled) {
|
|
77
|
-
setPluginActions([]);
|
|
78
|
-
}
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
// Render plugin actions in header location
|
|
82
|
-
const headerActions = plugin.actions.filter((action)=>!action.location || action.location === 'header').map((action, index)=>{
|
|
83
|
-
const ActionComponent = action.component;
|
|
84
|
-
try {
|
|
85
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
-
return /*#__PURE__*/ (0, _jsxruntime.jsx)(ActionComponent, {
|
|
87
|
-
...panelProps
|
|
88
|
-
}, `plugin-action-${index}`);
|
|
89
|
-
} catch (error) {
|
|
90
|
-
console.warn(`Failed to render plugin action ${index}:`, error);
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}).filter((item)=>Boolean(item));
|
|
94
|
-
if (!cancelled) {
|
|
95
|
-
setPluginActions(headerActions);
|
|
96
|
-
}
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (!cancelled) {
|
|
99
|
-
console.warn('Failed to load plugin actions:', error);
|
|
100
|
-
setPluginActions([]);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
// Use setTimeout to defer the async operation to the next tick
|
|
105
|
-
const timeoutId = setTimeout(()=>{
|
|
106
|
-
loadPluginActions();
|
|
107
|
-
}, 0);
|
|
108
|
-
return ()=>{
|
|
109
|
-
cancelled = true;
|
|
110
|
-
clearTimeout(timeoutId);
|
|
111
|
-
};
|
|
112
|
-
}, [
|
|
113
|
-
panelPluginKind,
|
|
114
|
-
panelProps,
|
|
115
|
-
getPlugin
|
|
116
|
-
]);
|
|
53
|
+
// Only destructure queryResults since we removed panelPluginKind and spec
|
|
54
|
+
const { queryResults } = panelContentProps;
|
|
117
55
|
const descriptionAction = (0, _react.useMemo)(()=>{
|
|
118
56
|
if (description && description.trim().length > 0) {
|
|
119
57
|
return /*#__PURE__*/ (0, _jsxruntime.jsx)(_components.InfoTooltip, {
|
|
@@ -144,16 +82,38 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
|
|
|
144
82
|
});
|
|
145
83
|
const extraActions = editHandlers === undefined && extra;
|
|
146
84
|
const queryStateIndicator = (0, _react.useMemo)(()=>{
|
|
147
|
-
|
|
148
|
-
|
|
85
|
+
// Check if any query is fetching
|
|
86
|
+
const isQueryFetching = queryResults.some((q)=>q.isFetching);
|
|
87
|
+
// Get query-specific errors
|
|
88
|
+
const queryErrors = queryResults.filter((q)=>q.error);
|
|
89
|
+
// Convert QueryData[] to TimeSeriesData format for data availability check
|
|
90
|
+
const timeSeriesData = queryResults && queryResults.length > 0 ? {
|
|
91
|
+
series: queryResults.flatMap((q)=>{
|
|
92
|
+
// Only extract series if the data type is TimeSeriesData
|
|
93
|
+
if (q.data && typeof q.data === 'object' && 'series' in q.data) {
|
|
94
|
+
return q.data.series || [];
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
})
|
|
98
|
+
} : undefined;
|
|
99
|
+
const hasData = timeSeriesData && timeSeriesData.series && timeSeriesData.series.length > 0;
|
|
100
|
+
// Show loading indicator if queries are fetching AND we already have data
|
|
101
|
+
if ((isFetching || isQueryFetching) && hasData) {
|
|
102
|
+
// If the panel has no data, the panel content will show the loading overlay.
|
|
103
|
+
// Therefore, show the circular loading indicator only in case the panel doesn't display the loading overlay already.
|
|
149
104
|
return /*#__PURE__*/ (0, _jsxruntime.jsx)(_material.CircularProgress, {
|
|
150
105
|
"aria-label": "loading",
|
|
151
106
|
size: "1.125rem"
|
|
152
107
|
});
|
|
153
108
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
109
|
+
// Check for errors from both context and query results
|
|
110
|
+
const contextErrors = (errors || []).filter((error)=>error !== null);
|
|
111
|
+
const allErrors = [
|
|
112
|
+
...contextErrors,
|
|
113
|
+
...queryErrors.map((q)=>q.error)
|
|
114
|
+
].filter(Boolean);
|
|
115
|
+
if (allErrors.length > 0) {
|
|
116
|
+
const errorTexts = allErrors.map((e)=>{
|
|
157
117
|
if (typeof e === 'string') return e;
|
|
158
118
|
if (e && typeof e === 'object') {
|
|
159
119
|
const errorObj = e;
|
|
@@ -200,6 +160,7 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
|
|
|
200
160
|
]);
|
|
201
161
|
const editActions = (0, _react.useMemo)(()=>{
|
|
202
162
|
if (editHandlers !== undefined) {
|
|
163
|
+
// If there are edit handlers, always just show the edit buttons
|
|
203
164
|
return /*#__PURE__*/ (0, _jsxruntime.jsxs)(_jsxruntime.Fragment, {
|
|
204
165
|
children: [
|
|
205
166
|
/*#__PURE__*/ (0, _jsxruntime.jsx)(_components.InfoTooltip, {
|
|
@@ -222,6 +183,8 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
|
|
|
222
183
|
children: /*#__PURE__*/ (0, _jsxruntime.jsx)(_ContentCopy.default, {
|
|
223
184
|
fontSize: "inherit",
|
|
224
185
|
sx: {
|
|
186
|
+
// Shrink this icon a little bit to look more consistent
|
|
187
|
+
// with the other icons in the header.
|
|
225
188
|
transform: 'scale(0.925)'
|
|
226
189
|
}
|
|
227
190
|
})
|
|
@@ -339,6 +302,7 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
|
|
|
339
302
|
extraActions,
|
|
340
303
|
" ",
|
|
341
304
|
readActions,
|
|
305
|
+
" ",
|
|
342
306
|
pluginActions,
|
|
343
307
|
/*#__PURE__*/ (0, _jsxruntime.jsx)(OverflowMenu, {
|
|
344
308
|
title: title,
|
|
@@ -351,6 +315,7 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
|
|
|
351
315
|
}),
|
|
352
316
|
/*#__PURE__*/ (0, _jsxruntime.jsxs)(ConditionalBox, {
|
|
353
317
|
sx: (theme)=>({
|
|
318
|
+
// flip the logic here; if the browser (or jsdom) does not support container queries, always show all icons
|
|
354
319
|
display: 'flex',
|
|
355
320
|
[theme.containerQueries(_constants.HEADER_ACTIONS_CONTAINER_NAME).down(_constants.HEADER_MEDIUM_WIDTH)]: {
|
|
356
321
|
display: 'none'
|
|
@@ -372,7 +337,9 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
|
|
|
372
337
|
extraActions,
|
|
373
338
|
" ",
|
|
374
339
|
readActions,
|
|
340
|
+
" ",
|
|
375
341
|
pluginActions,
|
|
342
|
+
" ",
|
|
376
343
|
editActions,
|
|
377
344
|
" ",
|
|
378
345
|
moveAction
|
|
@@ -385,7 +352,7 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
|
|
|
385
352
|
};
|
|
386
353
|
const OverflowMenu = ({ children, title })=>{
|
|
387
354
|
const [anchorPosition, setAnchorPosition] = (0, _react.useState)();
|
|
388
|
-
// do not show overflow menu if there is no content
|
|
355
|
+
// do not show overflow menu if there is no content (for example, edit actions are hidden)
|
|
389
356
|
const hasContent = /*#__PURE__*/ (0, _react.isValidElement)(children) || Array.isArray(children) && children.some(_react.isValidElement);
|
|
390
357
|
if (!hasContent) {
|
|
391
358
|
return undefined;
|
|
@@ -27,48 +27,15 @@ const _pluginsystem = require("@perses-dev/plugin-system");
|
|
|
27
27
|
const _react = require("react");
|
|
28
28
|
const _constants = require("../../constants");
|
|
29
29
|
const _PanelActions = require("./PanelActions");
|
|
30
|
-
function PanelHeader({ id, title: rawTitle, description: rawDescription, links, queryResults, readHandlers, editHandlers, sx, extra,
|
|
30
|
+
function PanelHeader({ id, title: rawTitle, description: rawDescription, links, queryResults, readHandlers, editHandlers, sx, extra, pluginActions, ...rest }) {
|
|
31
31
|
const titleElementId = `${id}-title`;
|
|
32
32
|
const descriptionTooltipId = `${id}-description`;
|
|
33
33
|
const title = (0, _pluginsystem.useReplaceVariablesInString)(rawTitle);
|
|
34
34
|
const description = (0, _pluginsystem.useReplaceVariablesInString)(rawDescription);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let stepMs = undefined;
|
|
40
|
-
let metadata = undefined;
|
|
41
|
-
queryResults.forEach((query)=>{
|
|
42
|
-
if (query.data && 'series' in query.data) {
|
|
43
|
-
const timeSeriesData = query.data;
|
|
44
|
-
// Collect series from this query
|
|
45
|
-
if (timeSeriesData.series && timeSeriesData.series.length > 0) {
|
|
46
|
-
allSeries.push(...timeSeriesData.series);
|
|
47
|
-
// Use the first query's metadata/timeRange/stepMs as the base
|
|
48
|
-
if (!timeRange && timeSeriesData.timeRange) {
|
|
49
|
-
timeRange = timeSeriesData.timeRange;
|
|
50
|
-
}
|
|
51
|
-
if (!stepMs && timeSeriesData.stepMs) {
|
|
52
|
-
stepMs = timeSeriesData.stepMs;
|
|
53
|
-
}
|
|
54
|
-
if (!metadata && timeSeriesData.metadata) {
|
|
55
|
-
metadata = timeSeriesData.metadata;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
// If we found series, create a combined TimeSeriesData object
|
|
61
|
-
if (allSeries.length > 0) {
|
|
62
|
-
const combinedData = {
|
|
63
|
-
series: allSeries,
|
|
64
|
-
timeRange,
|
|
65
|
-
stepMs,
|
|
66
|
-
metadata
|
|
67
|
-
};
|
|
68
|
-
return combinedData;
|
|
69
|
-
}
|
|
70
|
-
return undefined;
|
|
71
|
-
}, [
|
|
35
|
+
// Create the panelContentProps object with only queryResults
|
|
36
|
+
const panelContentProps = (0, _react.useMemo)(()=>({
|
|
37
|
+
queryResults
|
|
38
|
+
}), [
|
|
72
39
|
queryResults
|
|
73
40
|
]);
|
|
74
41
|
return /*#__PURE__*/ (0, _jsxruntime.jsx)(_material.CardHeader, {
|
|
@@ -99,14 +66,11 @@ function PanelHeader({ id, title: rawTitle, description: rawDescription, links,
|
|
|
99
66
|
description: description,
|
|
100
67
|
descriptionTooltipId: descriptionTooltipId,
|
|
101
68
|
links: links,
|
|
102
|
-
queryResults: timeSeriesDataForExport,
|
|
103
|
-
panelPluginKind: panelPluginKind,
|
|
104
69
|
readHandlers: readHandlers,
|
|
105
70
|
editHandlers: editHandlers,
|
|
106
71
|
extra: extra,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
panelProps: panelProps
|
|
72
|
+
panelContentProps: panelContentProps,
|
|
73
|
+
pluginActions: pluginActions
|
|
110
74
|
})
|
|
111
75
|
]
|
|
112
76
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DownloadButton.d.ts","sourceRoot":"","sources":["../../../src/components/DownloadButton/DownloadButton.tsx"],"names":[],"mappings":"AAgBA,OAAc,EAAE,YAAY,EAAU,MAAM,OAAO,CAAC;AAKpD,wBAAgB,cAAc,IAAI,YAAY,
|
|
1
|
+
{"version":3,"file":"DownloadButton.d.ts","sourceRoot":"","sources":["../../../src/components/DownloadButton/DownloadButton.tsx"],"names":[],"mappings":"AAgBA,OAAc,EAAE,YAAY,EAAU,MAAM,OAAO,CAAC;AAKpD,wBAAgB,cAAc,IAAI,YAAY,CA+D7C"}
|
|
@@ -15,8 +15,8 @@ import { ClickAwayListener, Menu, MenuItem, MenuList } from '@mui/material';
|
|
|
15
15
|
import { ToolbarIconButton } from '@perses-dev/components';
|
|
16
16
|
import DownloadIcon from 'mdi-material-ui/DownloadOutline';
|
|
17
17
|
import React, { useRef } from 'react';
|
|
18
|
-
import { stringify } from 'yaml';
|
|
19
18
|
import { useDashboard } from '../../context';
|
|
19
|
+
import { serializeDashboard } from './serializeDashboard';
|
|
20
20
|
// Button that enables downloading the dashboard as a JSON file
|
|
21
21
|
export function DownloadButton() {
|
|
22
22
|
const { dashboard } = useDashboard();
|
|
@@ -28,43 +28,13 @@ export function DownloadButton() {
|
|
|
28
28
|
};
|
|
29
29
|
const handleItemClick = (format, shape)=>()=>{
|
|
30
30
|
setAnchorEl(null);
|
|
31
|
-
|
|
32
|
-
switch(format){
|
|
33
|
-
case 'json':
|
|
34
|
-
type = 'application/json';
|
|
35
|
-
content = JSON.stringify(dashboard, null, 2);
|
|
36
|
-
break;
|
|
37
|
-
case 'yaml':
|
|
38
|
-
{
|
|
39
|
-
type = 'application/yaml';
|
|
40
|
-
if (shape === 'cr') {
|
|
41
|
-
const name = dashboard.metadata.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
42
|
-
content = stringify({
|
|
43
|
-
apiVersion: 'perses.dev/v1alpha1',
|
|
44
|
-
kind: 'PersesDashboard',
|
|
45
|
-
metadata: {
|
|
46
|
-
labels: {
|
|
47
|
-
'app.kubernetes.io/name': 'perses-dashboard',
|
|
48
|
-
'app.kubernetes.io/instance': name,
|
|
49
|
-
'app.kubernetes.io/part-of': 'perses-operator'
|
|
50
|
-
},
|
|
51
|
-
name
|
|
52
|
-
},
|
|
53
|
-
namespace: dashboard.metadata.project,
|
|
54
|
-
spec: dashboard.spec
|
|
55
|
-
});
|
|
56
|
-
} else {
|
|
57
|
-
content = stringify(dashboard);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
31
|
+
const { contentType, content } = serializeDashboard(dashboard, format, shape);
|
|
62
32
|
if (!hiddenLinkRef || !hiddenLinkRef.current) return;
|
|
63
33
|
// Create blob URL
|
|
64
34
|
const hiddenLinkUrl = URL.createObjectURL(new Blob([
|
|
65
35
|
content
|
|
66
36
|
], {
|
|
67
|
-
type
|
|
37
|
+
type: contentType
|
|
68
38
|
}));
|
|
69
39
|
// Simulate click
|
|
70
40
|
hiddenLinkRef.current.download = `${dashboard.metadata.name}${shape === 'cr' ? '-cr' : ''}.${format}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/DownloadButton/DownloadButton.tsx"],"sourcesContent":["// Copyright 2023 The Perses Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nimport { ClickAwayListener, Menu, MenuItem, MenuList } from '@mui/material';\nimport { ToolbarIconButton } from '@perses-dev/components';\nimport DownloadIcon from 'mdi-material-ui/DownloadOutline';\nimport React, { ReactElement, useRef } from 'react';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/components/DownloadButton/DownloadButton.tsx"],"sourcesContent":["// Copyright 2023 The Perses Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nimport { ClickAwayListener, Menu, MenuItem, MenuList } from '@mui/material';\nimport { ToolbarIconButton } from '@perses-dev/components';\nimport DownloadIcon from 'mdi-material-ui/DownloadOutline';\nimport React, { ReactElement, useRef } from 'react';\nimport { useDashboard } from '../../context';\nimport { serializeDashboard } from './serializeDashboard';\n\n// Button that enables downloading the dashboard as a JSON file\nexport function DownloadButton(): ReactElement {\n const { dashboard } = useDashboard();\n const hiddenLinkRef = useRef<HTMLAnchorElement>(null);\n const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);\n const open = Boolean(anchorEl);\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {\n setAnchorEl(event.currentTarget);\n };\n const handleItemClick = (format: 'json' | 'yaml', shape?: 'cr') => (): void => {\n setAnchorEl(null);\n\n const { contentType, content } = serializeDashboard(dashboard, format, shape);\n\n if (!hiddenLinkRef || !hiddenLinkRef.current) return;\n // Create blob URL\n const hiddenLinkUrl = URL.createObjectURL(new Blob([content], { type: contentType }));\n // Simulate click\n hiddenLinkRef.current.download = `${dashboard.metadata.name}${shape === 'cr' ? '-cr' : ''}.${format}`;\n hiddenLinkRef.current.href = hiddenLinkUrl;\n hiddenLinkRef.current.click();\n // Remove blob URL (for memory management)\n URL.revokeObjectURL(hiddenLinkUrl);\n };\n\n return (\n <>\n <ToolbarIconButton\n id=\"download-dashboard-button\"\n aria-controls={open ? 'basic-menu' : undefined}\n aria-haspopup=\"true\"\n aria-expanded={open ? 'true' : undefined}\n onClick={handleClick}\n >\n <DownloadIcon />\n </ToolbarIconButton>\n\n <Menu\n id=\"download-dashboard-formats\"\n anchorEl={anchorEl}\n open={open}\n hideBackdrop={true}\n onClose={() => setAnchorEl(null)}\n MenuListProps={{\n 'aria-labelledby': 'download-dashboard-button',\n }}\n >\n <div>\n <ClickAwayListener onClickAway={() => setAnchorEl(null)}>\n <MenuList>\n <MenuItem onClick={handleItemClick('json')}>JSON</MenuItem>\n <MenuItem onClick={handleItemClick('yaml')}>YAML</MenuItem>\n <MenuItem onClick={handleItemClick('yaml', 'cr')}>YAML (CR)</MenuItem>\n </MenuList>\n </ClickAwayListener>\n </div>\n </Menu>\n\n {/* Hidden link to download the dashboard as a JSON or YAML file */}\n {/* eslint-disable jsx-a11y/anchor-has-content */}\n {/* eslint-disable jsx-a11y/anchor-is-valid */}\n <a ref={hiddenLinkRef} style={{ display: 'none' }} />\n </>\n );\n}\n"],"names":["ClickAwayListener","Menu","MenuItem","MenuList","ToolbarIconButton","DownloadIcon","React","useRef","useDashboard","serializeDashboard","DownloadButton","dashboard","hiddenLinkRef","anchorEl","setAnchorEl","useState","open","Boolean","handleClick","event","currentTarget","handleItemClick","format","shape","contentType","content","current","hiddenLinkUrl","URL","createObjectURL","Blob","type","download","metadata","name","href","click","revokeObjectURL","id","aria-controls","undefined","aria-haspopup","aria-expanded","onClick","hideBackdrop","onClose","MenuListProps","div","onClickAway","a","ref","style","display"],"mappings":"AAAA,oCAAoC;AACpC,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,6CAA6C;AAC7C,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;;AAEjC,SAASA,iBAAiB,EAAEC,IAAI,EAAEC,QAAQ,EAAEC,QAAQ,QAAQ,gBAAgB;AAC5E,SAASC,iBAAiB,QAAQ,yBAAyB;AAC3D,OAAOC,kBAAkB,kCAAkC;AAC3D,OAAOC,SAAuBC,MAAM,QAAQ,QAAQ;AACpD,SAASC,YAAY,QAAQ,gBAAgB;AAC7C,SAASC,kBAAkB,QAAQ,uBAAuB;AAE1D,+DAA+D;AAC/D,OAAO,SAASC;IACd,MAAM,EAAEC,SAAS,EAAE,GAAGH;IACtB,MAAMI,gBAAgBL,OAA0B;IAChD,MAAM,CAACM,UAAUC,YAAY,GAAGR,MAAMS,QAAQ,CAAqB;IACnE,MAAMC,OAAOC,QAAQJ;IACrB,MAAMK,cAAc,CAACC;QACnBL,YAAYK,MAAMC,aAAa;IACjC;IACA,MAAMC,kBAAkB,CAACC,QAAyBC,QAAiB;YACjET,YAAY;YAEZ,MAAM,EAAEU,WAAW,EAAEC,OAAO,EAAE,GAAGhB,mBAAmBE,WAAWW,QAAQC;YAEvE,IAAI,CAACX,iBAAiB,CAACA,cAAcc,OAAO,EAAE;YAC9C,kBAAkB;YAClB,MAAMC,gBAAgBC,IAAIC,eAAe,CAAC,IAAIC,KAAK;gBAACL;aAAQ,EAAE;gBAAEM,MAAMP;YAAY;YAClF,iBAAiB;YACjBZ,cAAcc,OAAO,CAACM,QAAQ,GAAG,GAAGrB,UAAUsB,QAAQ,CAACC,IAAI,GAAGX,UAAU,OAAO,QAAQ,GAAG,CAAC,EAAED,QAAQ;YACrGV,cAAcc,OAAO,CAACS,IAAI,GAAGR;YAC7Bf,cAAcc,OAAO,CAACU,KAAK;YAC3B,0CAA0C;YAC1CR,IAAIS,eAAe,CAACV;QACtB;IAEA,qBACE;;0BACE,KAACvB;gBACCkC,IAAG;gBACHC,iBAAevB,OAAO,eAAewB;gBACrCC,iBAAc;gBACdC,iBAAe1B,OAAO,SAASwB;gBAC/BG,SAASzB;0BAET,cAAA,KAACb;;0BAGH,KAACJ;gBACCqC,IAAG;gBACHzB,UAAUA;gBACVG,MAAMA;gBACN4B,cAAc;gBACdC,SAAS,IAAM/B,YAAY;gBAC3BgC,eAAe;oBACb,mBAAmB;gBACrB;0BAEA,cAAA,KAACC;8BACC,cAAA,KAAC/C;wBAAkBgD,aAAa,IAAMlC,YAAY;kCAChD,cAAA,MAACX;;8CACC,KAACD;oCAASyC,SAAStB,gBAAgB;8CAAS;;8CAC5C,KAACnB;oCAASyC,SAAStB,gBAAgB;8CAAS;;8CAC5C,KAACnB;oCAASyC,SAAStB,gBAAgB,QAAQ;8CAAO;;;;;;;0BAS1D,KAAC4B;gBAAEC,KAAKtC;gBAAeuC,OAAO;oBAAEC,SAAS;gBAAO;;;;AAGtD"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DashboardResource, EphemeralDashboardResource } from '@perses-dev/core';
|
|
2
|
+
type SerializedDashboard = {
|
|
3
|
+
contentType: string;
|
|
4
|
+
content: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function serializeDashboard(dashboard: DashboardResource | EphemeralDashboardResource, format: 'json' | 'yaml', shape?: 'cr'): SerializedDashboard;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=serializeDashboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializeDashboard.d.ts","sourceRoot":"","sources":["../../../src/components/DownloadButton/serializeDashboard.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAGjF,KAAK,mBAAmB,GAAG;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AA+BF,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,iBAAiB,GAAG,0BAA0B,EACzD,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,KAAK,CAAC,EAAE,IAAI,GACX,mBAAmB,CAOrB"}
|