@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.
@@ -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
- let type, content = '';
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, projectName: providedProjectName, ...others } = props;
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
- // Use provided project name or extract from URL as fallback
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
- projectName
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
- projectName: projectName,
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, queryResults, panelPluginKind, projectName: _propsProjectName, panelProps })=>{
51
+ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, descriptionTooltipId, links, panelContentProps, pluginActions = [] })=>{
52
52
  const { isFetching, errors } = (0, _pluginsystem.useDataQueriesContext)();
53
- const { getPlugin } = (0, _pluginsystem.usePluginRegistry)();
54
- const [pluginActions, setPluginActions] = (0, _react.useState)([]);
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
- const hasData = queryResults && queryResults.series && queryResults.series.length > 0;
148
- if (isFetching && hasData) {
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
- const validErrors = (errors || []).filter((error)=>error !== null);
155
- if (validErrors.length > 0) {
156
- const errorTexts = validErrors.map((e)=>{
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, panelPluginKind, projectName, panelProps, ...rest }) {
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
- const timeSeriesDataForExport = (0, _react.useMemo)(()=>{
36
- // Collect all series from all queries
37
- const allSeries = [];
38
- let timeRange = undefined;
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
- projectName: projectName,
108
- // ========== ADDED: Pass panel props for actions ==========
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,CAgG7C"}
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
- let type, content = '';
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 { stringify } from 'yaml';\nimport { useDashboard } from '../../context';\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 let type,\n content = '';\n\n switch (format) {\n case 'json':\n type = 'application/json';\n content = JSON.stringify(dashboard, null, 2);\n break;\n case 'yaml':\n {\n type = 'application/yaml';\n\n if (shape === 'cr') {\n const name = dashboard.metadata.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');\n content = stringify({\n apiVersion: 'perses.dev/v1alpha1',\n kind: 'PersesDashboard',\n metadata: {\n labels: {\n 'app.kubernetes.io/name': 'perses-dashboard',\n 'app.kubernetes.io/instance': name,\n 'app.kubernetes.io/part-of': 'perses-operator',\n },\n name,\n },\n namespace: dashboard.metadata.project,\n spec: dashboard.spec,\n });\n } else {\n content = stringify(dashboard);\n }\n }\n break;\n }\n\n if (!hiddenLinkRef || !hiddenLinkRef.current) return;\n // Create blob URL\n const hiddenLinkUrl = URL.createObjectURL(new Blob([content], { type }));\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","stringify","useDashboard","DownloadButton","dashboard","hiddenLinkRef","anchorEl","setAnchorEl","useState","open","Boolean","handleClick","event","currentTarget","handleItemClick","format","shape","type","content","JSON","name","metadata","toLowerCase","replace","apiVersion","kind","labels","namespace","project","spec","current","hiddenLinkUrl","URL","createObjectURL","Blob","download","href","click","revokeObjectURL","id","aria-controls","undefined","aria-haspopup","aria-expanded","onClick","hideBackdrop","onClose","MenuListProps","div","onClickAway","a","ref","style","display"],"mappings":"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"}
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"}