@perses-dev/dashboards 0.0.0-snapshot-panel-actions-520389b → 0.0.0-snapshot-ts-panel-actions-90e9ef0

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,29 +54,6 @@ 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
- ]);
96
- const panelPropsForActions = (0, _react.useMemo)(()=>{
97
- return {
98
- spec: definition.spec.plugin.spec,
99
- queryResults: queryResults.map((query)=>({
100
- definition: query.definition,
101
- data: query.data
102
- })),
103
- contentDimensions,
104
- definition,
105
- projectName
106
- };
107
- }, [
108
- definition,
109
- contentDimensions,
110
- queryResults,
111
- projectName
112
- ]);
113
57
  const handleMouseEnter = (e)=>{
114
58
  onMouseEnter?.(e);
115
59
  };
@@ -144,12 +88,9 @@ const Panel = /*#__PURE__*/ (0, _react.memo)(function Panel(props) {
144
88
  title: definition.spec.display.name,
145
89
  description: definition.spec.display.description,
146
90
  queryResults: queryResults,
147
- panelPluginKind: definition.spec.plugin.kind,
148
91
  readHandlers: readHandlers,
149
92
  editHandlers: editHandlers,
150
93
  links: definition.spec.links,
151
- projectName: projectName,
152
- panelProps: panelPropsForActions,
153
94
  sx: {
154
95
  paddingX: `${chartsTheme.container.padding.default}px`
155
96
  }
@@ -171,7 +112,8 @@ const Panel = /*#__PURE__*/ (0, _react.memo)(function Panel(props) {
171
112
  children: /*#__PURE__*/ (0, _jsxruntime.jsx)(_components.ErrorBoundary, {
172
113
  FallbackComponent: _components.ErrorAlert,
173
114
  resetKeys: [
174
- definition.spec
115
+ definition.spec,
116
+ queryResults
175
117
  ],
176
118
  children: /*#__PURE__*/ (0, _jsxruntime.jsx)(_PanelContent.PanelContent, {
177
119
  definition: definition,
@@ -24,7 +24,6 @@ const _jsxruntime = require("react/jsx-runtime");
24
24
  const _material = require("@mui/material");
25
25
  const _react = require("react");
26
26
  const _components = require("@perses-dev/components");
27
- const _pluginsystem = require("@perses-dev/plugin-system");
28
27
  const _ArrowCollapse = /*#__PURE__*/ _interop_require_default(require("mdi-material-ui/ArrowCollapse"));
29
28
  const _ArrowExpand = /*#__PURE__*/ _interop_require_default(require("mdi-material-ui/ArrowExpand"));
30
29
  const _PencilOutline = /*#__PURE__*/ _interop_require_default(require("mdi-material-ui/PencilOutline"));
@@ -48,72 +47,7 @@ const ConditionalBox = (0, _material.styled)(_material.Box)({
48
47
  flexGrow: 1,
49
48
  justifyContent: 'flex-end'
50
49
  });
51
- const PanelActions = ({ editHandlers, readHandlers, extra, title, description, descriptionTooltipId, links, queryResults, panelPluginKind, projectName: _propsProjectName, panelProps })=>{
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
- ]);
50
+ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, descriptionTooltipId, links, queryResults })=>{
117
51
  const descriptionAction = (0, _react.useMemo)(()=>{
118
52
  if (description && description.trim().length > 0) {
119
53
  return /*#__PURE__*/ (0, _jsxruntime.jsx)(_components.InfoTooltip, {
@@ -144,23 +78,19 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
144
78
  });
145
79
  const extraActions = editHandlers === undefined && extra;
146
80
  const queryStateIndicator = (0, _react.useMemo)(()=>{
147
- const hasData = queryResults && queryResults.series && queryResults.series.length > 0;
81
+ const hasData = queryResults.some((q)=>q.data);
82
+ const isFetching = queryResults.some((q)=>q.isFetching);
83
+ const queryErrors = queryResults.filter((q)=>q.error);
148
84
  if (isFetching && hasData) {
85
+ // If the panel has no data, the panel content will show the loading overlay.
86
+ // Therefore, show the circular loading indicator only in case the panel doesn't display the loading overlay already.
149
87
  return /*#__PURE__*/ (0, _jsxruntime.jsx)(_material.CircularProgress, {
150
88
  "aria-label": "loading",
151
89
  size: "1.125rem"
152
90
  });
153
- }
154
- const validErrors = (errors || []).filter((error)=>error !== null);
155
- if (validErrors.length > 0) {
156
- const errorTexts = validErrors.map((e)=>{
157
- if (typeof e === 'string') return e;
158
- if (e && typeof e === 'object') {
159
- const errorObj = e;
160
- return errorObj.message ?? errorObj.toString?.() ?? 'Unknown error';
161
- }
162
- return 'Unknown error';
163
- }).join('\n');
91
+ } else if (queryErrors.length > 0) {
92
+ const errorTexts = queryErrors.map((q)=>q.error).map((e)=>e?.message ?? e?.toString() ?? 'Unknown error') // eslint-disable-line @typescript-eslint/no-explicit-any
93
+ .join('\n');
164
94
  return /*#__PURE__*/ (0, _jsxruntime.jsx)(_components.InfoTooltip, {
165
95
  description: errorTexts,
166
96
  children: /*#__PURE__*/ (0, _jsxruntime.jsx)(_HeaderIconButton.HeaderIconButton, {
@@ -173,9 +103,7 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
173
103
  });
174
104
  }
175
105
  }, [
176
- queryResults,
177
- isFetching,
178
- errors
106
+ queryResults
179
107
  ]);
180
108
  const readActions = (0, _react.useMemo)(()=>{
181
109
  if (readHandlers !== undefined) {
@@ -200,6 +128,7 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
200
128
  ]);
201
129
  const editActions = (0, _react.useMemo)(()=>{
202
130
  if (editHandlers !== undefined) {
131
+ // If there are edit handlers, always just show the edit buttons
203
132
  return /*#__PURE__*/ (0, _jsxruntime.jsxs)(_jsxruntime.Fragment, {
204
133
  children: [
205
134
  /*#__PURE__*/ (0, _jsxruntime.jsx)(_components.InfoTooltip, {
@@ -222,6 +151,8 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
222
151
  children: /*#__PURE__*/ (0, _jsxruntime.jsx)(_ContentCopy.default, {
223
152
  fontSize: "inherit",
224
153
  sx: {
154
+ // Shrink this icon a little bit to look more consistent
155
+ // with the other icons in the header.
225
156
  transform: 'scale(0.925)'
226
157
  }
227
158
  })
@@ -311,7 +242,6 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
311
242
  editActions
312
243
  ]
313
244
  }),
314
- pluginActions,
315
245
  moveAction
316
246
  ]
317
247
  })
@@ -339,7 +269,6 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
339
269
  extraActions,
340
270
  " ",
341
271
  readActions,
342
- pluginActions,
343
272
  /*#__PURE__*/ (0, _jsxruntime.jsx)(OverflowMenu, {
344
273
  title: title,
345
274
  children: editActions
@@ -351,6 +280,7 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
351
280
  }),
352
281
  /*#__PURE__*/ (0, _jsxruntime.jsxs)(ConditionalBox, {
353
282
  sx: (theme)=>({
283
+ // flip the logic here; if the browser (or jsdom) does not support container queries, always show all icons
354
284
  display: 'flex',
355
285
  [theme.containerQueries(_constants.HEADER_ACTIONS_CONTAINER_NAME).down(_constants.HEADER_MEDIUM_WIDTH)]: {
356
286
  display: 'none'
@@ -372,7 +302,7 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
372
302
  extraActions,
373
303
  " ",
374
304
  readActions,
375
- pluginActions,
305
+ " ",
376
306
  editActions,
377
307
  " ",
378
308
  moveAction
@@ -385,7 +315,7 @@ const PanelActions = ({ editHandlers, readHandlers, extra, title, description, d
385
315
  };
386
316
  const OverflowMenu = ({ children, title })=>{
387
317
  const [anchorPosition, setAnchorPosition] = (0, _react.useState)();
388
- // do not show overflow menu if there is no content
318
+ // do not show overflow menu if there is no content (for example, edit actions are hidden)
389
319
  const hasContent = /*#__PURE__*/ (0, _react.isValidElement)(children) || Array.isArray(children) && children.some(_react.isValidElement);
390
320
  if (!hasContent) {
391
321
  return undefined;
@@ -24,53 +24,13 @@ const _jsxruntime = require("react/jsx-runtime");
24
24
  const _material = require("@mui/material");
25
25
  const _components = require("@perses-dev/components");
26
26
  const _pluginsystem = require("@perses-dev/plugin-system");
27
- const _react = require("react");
28
27
  const _constants = require("../../constants");
29
28
  const _PanelActions = require("./PanelActions");
30
- function PanelHeader({ id, title: rawTitle, description: rawDescription, links, queryResults, readHandlers, editHandlers, sx, extra, panelPluginKind, projectName, panelProps, ...rest }) {
29
+ function PanelHeader({ id, title: rawTitle, description: rawDescription, links, queryResults, readHandlers, editHandlers, sx, extra, ...rest }) {
31
30
  const titleElementId = `${id}-title`;
32
31
  const descriptionTooltipId = `${id}-description`;
33
32
  const title = (0, _pluginsystem.useReplaceVariablesInString)(rawTitle);
34
33
  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
- }, [
72
- queryResults
73
- ]);
74
34
  return /*#__PURE__*/ (0, _jsxruntime.jsx)(_material.CardHeader, {
75
35
  id: id,
76
36
  component: "header",
@@ -99,14 +59,10 @@ function PanelHeader({ id, title: rawTitle, description: rawDescription, links,
99
59
  description: description,
100
60
  descriptionTooltipId: descriptionTooltipId,
101
61
  links: links,
102
- queryResults: timeSeriesDataForExport,
103
- panelPluginKind: panelPluginKind,
62
+ queryResults: queryResults,
104
63
  readHandlers: readHandlers,
105
64
  editHandlers: editHandlers,
106
- extra: extra,
107
- projectName: projectName,
108
- // ========== ADDED: Pass panel props for actions ==========
109
- panelProps: panelProps
65
+ extra: extra
110
66
  })
111
67
  ]
112
68
  }),
@@ -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"}
@@ -0,0 +1,56 @@
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
+ import { stringify } from 'yaml';
14
+ function serializeYaml(dashboard, shape) {
15
+ let content;
16
+ if (shape === 'cr') {
17
+ const name = dashboard.metadata.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
18
+ content = stringify({
19
+ apiVersion: 'perses.dev/v1alpha1',
20
+ kind: 'PersesDashboard',
21
+ metadata: {
22
+ labels: {
23
+ 'app.kubernetes.io/name': 'perses-dashboard',
24
+ 'app.kubernetes.io/instance': name,
25
+ 'app.kubernetes.io/part-of': 'perses-operator'
26
+ },
27
+ name,
28
+ namespace: dashboard.metadata.project
29
+ },
30
+ spec: dashboard.spec
31
+ }, {
32
+ schema: 'yaml-1.1'
33
+ });
34
+ } else {
35
+ content = stringify(dashboard, {
36
+ schema: 'yaml-1.1'
37
+ });
38
+ }
39
+ return {
40
+ contentType: 'application/yaml',
41
+ content
42
+ };
43
+ }
44
+ export function serializeDashboard(dashboard, format, shape) {
45
+ switch(format){
46
+ case 'json':
47
+ return {
48
+ contentType: 'application/json',
49
+ content: JSON.stringify(dashboard, null, 2)
50
+ };
51
+ case 'yaml':
52
+ return serializeYaml(dashboard, shape);
53
+ }
54
+ }
55
+
56
+ //# sourceMappingURL=serializeDashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/DownloadButton/serializeDashboard.ts"],"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 { DashboardResource, EphemeralDashboardResource } from '@perses-dev/core';\nimport { stringify } from 'yaml';\n\ntype SerializedDashboard = {\n contentType: string;\n content: string;\n};\n\nfunction serializeYaml(dashboard: DashboardResource | EphemeralDashboardResource, shape?: 'cr'): SerializedDashboard {\n let content: string;\n\n if (shape === 'cr') {\n const name = dashboard.metadata.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');\n content = stringify(\n {\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 namespace: dashboard.metadata.project,\n },\n spec: dashboard.spec,\n },\n { schema: 'yaml-1.1' }\n );\n } else {\n content = stringify(dashboard, { schema: 'yaml-1.1' });\n }\n\n return { contentType: 'application/yaml', content };\n}\n\nexport function serializeDashboard(\n dashboard: DashboardResource | EphemeralDashboardResource,\n format: 'json' | 'yaml',\n shape?: 'cr'\n): SerializedDashboard {\n switch (format) {\n case 'json':\n return { contentType: 'application/json', content: JSON.stringify(dashboard, null, 2) };\n case 'yaml':\n return serializeYaml(dashboard, shape);\n }\n}\n"],"names":["stringify","serializeYaml","dashboard","shape","content","name","metadata","toLowerCase","replace","apiVersion","kind","labels","namespace","project","spec","schema","contentType","serializeDashboard","format","JSON"],"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;AAGjC,SAASA,SAAS,QAAQ,OAAO;AAOjC,SAASC,cAAcC,SAAyD,EAAEC,KAAY;IAC5F,IAAIC;IAEJ,IAAID,UAAU,MAAM;QAClB,MAAME,OAAOH,UAAUI,QAAQ,CAACD,IAAI,CAACE,WAAW,GAAGC,OAAO,CAAC,eAAe;QAC1EJ,UAAUJ,UACR;YACES,YAAY;YACZC,MAAM;YACNJ,UAAU;gBACRK,QAAQ;oBACN,0BAA0B;oBAC1B,8BAA8BN;oBAC9B,6BAA6B;gBAC/B;gBACAA;gBACAO,WAAWV,UAAUI,QAAQ,CAACO,OAAO;YACvC;YACAC,MAAMZ,UAAUY,IAAI;QACtB,GACA;YAAEC,QAAQ;QAAW;IAEzB,OAAO;QACLX,UAAUJ,UAAUE,WAAW;YAAEa,QAAQ;QAAW;IACtD;IAEA,OAAO;QAAEC,aAAa;QAAoBZ;IAAQ;AACpD;AAEA,OAAO,SAASa,mBACdf,SAAyD,EACzDgB,MAAuB,EACvBf,KAAY;IAEZ,OAAQe;QACN,KAAK;YACH,OAAO;gBAAEF,aAAa;gBAAoBZ,SAASe,KAAKnB,SAAS,CAACE,WAAW,MAAM;YAAG;QACxF,KAAK;YACH,OAAOD,cAAcC,WAAWC;IACpC;AACF"}