mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Canvas] Move away from lib/workpad_service (#104183)
* Move away from lib/workpad_service * Adds stubs * Fix types. Swap fetching zip to workpad service * Fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
eb57dd4a7e
commit
694f8caeb3
29 changed files with 645 additions and 455 deletions
|
@ -30,6 +30,7 @@ interface Params {
|
|||
|
||||
const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/;
|
||||
const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/;
|
||||
const ZIP_CONTENT = /^(application\/zip)(;.*)?$/;
|
||||
|
||||
const removedUndefined = (obj: Record<string, any> | undefined) => {
|
||||
return omitBy(obj, (v) => v === undefined);
|
||||
|
@ -153,7 +154,7 @@ export class Fetch {
|
|||
const contentType = response.headers.get('Content-Type') || '';
|
||||
|
||||
try {
|
||||
if (NDJSON_CONTENT.test(contentType)) {
|
||||
if (NDJSON_CONTENT.test(contentType) || ZIP_CONTENT.test(contentType)) {
|
||||
body = await response.blob();
|
||||
} else if (JSON_CONTENT.test(contentType)) {
|
||||
body = await response.json();
|
||||
|
|
|
@ -17,30 +17,6 @@ export const ErrorStrings = {
|
|||
},
|
||||
}),
|
||||
},
|
||||
downloadWorkpad: {
|
||||
getDownloadFailureErrorMessage: () =>
|
||||
i18n.translate('xpack.canvas.error.downloadWorkpad.downloadFailureErrorMessage', {
|
||||
defaultMessage: "Couldn't download workpad",
|
||||
}),
|
||||
getDownloadRenderedWorkpadFailureErrorMessage: () =>
|
||||
i18n.translate(
|
||||
'xpack.canvas.error.downloadWorkpad.downloadRenderedWorkpadFailureErrorMessage',
|
||||
{
|
||||
defaultMessage: "Couldn't download rendered workpad",
|
||||
}
|
||||
),
|
||||
getDownloadRuntimeFailureErrorMessage: () =>
|
||||
i18n.translate('xpack.canvas.error.downloadWorkpad.downloadRuntimeFailureErrorMessage', {
|
||||
defaultMessage: "Couldn't download Shareable Runtime",
|
||||
}),
|
||||
getDownloadZippedRuntimeFailureErrorMessage: () =>
|
||||
i18n.translate(
|
||||
'xpack.canvas.error.downloadWorkpad.downloadZippedRuntimeFailureErrorMessage',
|
||||
{
|
||||
defaultMessage: "Couldn't download ZIP file",
|
||||
}
|
||||
),
|
||||
},
|
||||
esPersist: {
|
||||
getSaveFailureTitle: () =>
|
||||
i18n.translate('xpack.canvas.error.esPersist.saveFailureTitle', {
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
export { useCloneWorkpad } from './use_clone_workpad';
|
||||
export { useCreateWorkpad } from './use_create_workpad';
|
||||
export { useDeleteWorkpads } from './use_delete_workpad';
|
||||
export { useDownloadWorkpad } from './use_download_workpad';
|
||||
export { useFindTemplates } from './use_find_templates';
|
||||
export { useFindWorkpads } from './use_find_workpad';
|
||||
export { useImportWorkpad } from './use_upload_workpad';
|
||||
|
|
|
@ -11,7 +11,8 @@ import { useSelector } from 'react-redux';
|
|||
import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
|
||||
import type { State } from '../../../../types';
|
||||
import { usePlatformService } from '../../../services';
|
||||
import { useCloneWorkpad, useDownloadWorkpad } from '../hooks';
|
||||
import { useCloneWorkpad } from '../hooks';
|
||||
import { useDownloadWorkpad } from '../../hooks';
|
||||
|
||||
import { WorkpadTable as Component } from './workpad_table.component';
|
||||
import { WorkpadsContext } from './my_workpads';
|
||||
|
|
|
@ -10,7 +10,8 @@ import { useSelector } from 'react-redux';
|
|||
|
||||
import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
|
||||
import type { State } from '../../../../types';
|
||||
import { useDeleteWorkpads, useDownloadWorkpad } from '../hooks';
|
||||
import { useDeleteWorkpads } from '../hooks';
|
||||
import { useDownloadWorkpad } from '../../hooks';
|
||||
|
||||
import {
|
||||
WorkpadTableTools as Component,
|
||||
|
|
|
@ -5,8 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { downloadWorkpad as downloadWorkpadFn } from '../../../lib/download_workpad';
|
||||
|
||||
export const useDownloadWorkpad = () =>
|
||||
useCallback((workpadId: string) => downloadWorkpadFn(workpadId), []);
|
||||
export * from './workpad';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { useDownloadWorkpad, useDownloadRenderedWorkpad } from './use_download_workpad';
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import fileSaver from 'file-saver';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useNotifyService, useWorkpadService } from '../../../services';
|
||||
import { CanvasWorkpad } from '../../../../types';
|
||||
import { CanvasRenderedWorkpad } from '../../../../shareable_runtime/types';
|
||||
|
||||
const strings = {
|
||||
getDownloadFailureErrorMessage: () =>
|
||||
i18n.translate('xpack.canvas.error.downloadWorkpad.downloadFailureErrorMessage', {
|
||||
defaultMessage: "Couldn't download workpad",
|
||||
}),
|
||||
getDownloadRenderedWorkpadFailureErrorMessage: () =>
|
||||
i18n.translate(
|
||||
'xpack.canvas.error.downloadWorkpad.downloadRenderedWorkpadFailureErrorMessage',
|
||||
{
|
||||
defaultMessage: "Couldn't download rendered workpad",
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const useDownloadWorkpad = () => {
|
||||
const notifyService = useNotifyService();
|
||||
const workpadService = useWorkpadService();
|
||||
const download = useDownloadWorkpadBlob();
|
||||
|
||||
return useCallback(
|
||||
async (workpadId: string) => {
|
||||
try {
|
||||
const workpad = await workpadService.get(workpadId);
|
||||
|
||||
download(workpad, `canvas-workpad-${workpad.name}-${workpad.id}`);
|
||||
} catch (err) {
|
||||
notifyService.error(err, { title: strings.getDownloadFailureErrorMessage() });
|
||||
}
|
||||
},
|
||||
[workpadService, notifyService, download]
|
||||
);
|
||||
};
|
||||
|
||||
export const useDownloadRenderedWorkpad = () => {
|
||||
const notifyService = useNotifyService();
|
||||
const download = useDownloadWorkpadBlob();
|
||||
|
||||
return useCallback(
|
||||
async (workpad: CanvasRenderedWorkpad) => {
|
||||
try {
|
||||
download(workpad, `canvas-embed-workpad-${workpad.name}-${workpad.id}`);
|
||||
} catch (err) {
|
||||
notifyService.error(err, {
|
||||
title: strings.getDownloadRenderedWorkpadFailureErrorMessage(),
|
||||
});
|
||||
}
|
||||
},
|
||||
[notifyService, download]
|
||||
);
|
||||
};
|
||||
|
||||
const useDownloadWorkpadBlob = () => {
|
||||
return useCallback((workpad: CanvasWorkpad | CanvasRenderedWorkpad, filename: string) => {
|
||||
const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' });
|
||||
fileSaver.saveAs(jsonBlob, `${filename}.json`);
|
||||
}, []);
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import {
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
|
@ -24,35 +24,21 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { arrayBufferFetch } from '../../../../../common/lib/fetch';
|
||||
import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants';
|
||||
import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types';
|
||||
import {
|
||||
downloadRenderedWorkpad,
|
||||
downloadRuntime,
|
||||
downloadZippedRuntime,
|
||||
} from '../../../../lib/download_workpad';
|
||||
import { useDownloadRenderedWorkpad } from '../../../hooks';
|
||||
import { useDownloadRuntime, useDownloadZippedRuntime } from './hooks';
|
||||
import { ZIP, CANVAS, HTML } from '../../../../../i18n/constants';
|
||||
import { OnCloseFn } from '../share_menu.component';
|
||||
import { WorkpadStep } from './workpad_step';
|
||||
import { RuntimeStep } from './runtime_step';
|
||||
import { SnippetsStep } from './snippets_step';
|
||||
import { useNotifyService, usePlatformService } from '../../../../services';
|
||||
import { useNotifyService } from '../../../../services';
|
||||
|
||||
const strings = {
|
||||
getCopyShareConfigMessage: () =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', {
|
||||
defaultMessage: 'Copied share markup to clipboard',
|
||||
}),
|
||||
getShareableZipErrorTitle: (workpadName: string) =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', {
|
||||
defaultMessage:
|
||||
"Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.",
|
||||
values: {
|
||||
ZIP,
|
||||
workpadName,
|
||||
},
|
||||
}),
|
||||
getUnknownExportErrorMessage: (type: string) =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', {
|
||||
defaultMessage: 'Unknown export type: {type}',
|
||||
|
@ -121,33 +107,33 @@ export const ShareWebsiteFlyout: FC<Props> = ({
|
|||
renderedWorkpad,
|
||||
}) => {
|
||||
const notifyService = useNotifyService();
|
||||
const platformService = usePlatformService();
|
||||
const onCopy = () => {
|
||||
notifyService.info(strings.getCopyShareConfigMessage());
|
||||
};
|
||||
|
||||
const onDownload = (type: 'share' | 'shareRuntime' | 'shareZip') => {
|
||||
switch (type) {
|
||||
case 'share':
|
||||
downloadRenderedWorkpad(renderedWorkpad);
|
||||
return;
|
||||
case 'shareRuntime':
|
||||
downloadRuntime(platformService.getBasePath());
|
||||
case 'shareZip':
|
||||
const basePath = platformService.getBasePath();
|
||||
arrayBufferFetch
|
||||
.post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad))
|
||||
.then((blob) => downloadZippedRuntime(blob.data))
|
||||
.catch((err: Error) => {
|
||||
notifyService.error(err, {
|
||||
title: strings.getShareableZipErrorTitle(renderedWorkpad.name),
|
||||
});
|
||||
});
|
||||
return;
|
||||
default:
|
||||
throw new Error(strings.getUnknownExportErrorMessage(type));
|
||||
}
|
||||
};
|
||||
const onCopy = useCallback(() => notifyService.info(strings.getCopyShareConfigMessage()), [
|
||||
notifyService,
|
||||
]);
|
||||
|
||||
const downloadRenderedWorkpad = useDownloadRenderedWorkpad();
|
||||
const downloadRuntime = useDownloadRuntime();
|
||||
const downloadZippedRuntime = useDownloadZippedRuntime();
|
||||
|
||||
const onDownload = useCallback(
|
||||
(type: 'share' | 'shareRuntime' | 'shareZip') => {
|
||||
switch (type) {
|
||||
case 'share':
|
||||
downloadRenderedWorkpad(renderedWorkpad);
|
||||
return;
|
||||
case 'shareRuntime':
|
||||
downloadRuntime();
|
||||
return;
|
||||
case 'shareZip':
|
||||
downloadZippedRuntime(renderedWorkpad);
|
||||
return;
|
||||
default:
|
||||
throw new Error(strings.getUnknownExportErrorMessage(type));
|
||||
}
|
||||
},
|
||||
[downloadRenderedWorkpad, downloadRuntime, downloadZippedRuntime, renderedWorkpad]
|
||||
);
|
||||
|
||||
const link = (
|
||||
<EuiLink
|
||||
|
|
|
@ -5,22 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, withProps } from 'recompose';
|
||||
import React, { FC } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
getWorkpad,
|
||||
getRenderedWorkpad,
|
||||
getRenderedWorkpadExpressions,
|
||||
} from '../../../../state/selectors/workpad';
|
||||
import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './flyout.component';
|
||||
|
||||
import { ShareWebsiteFlyout as FlyoutComponent } from './flyout.component';
|
||||
import { State, CanvasWorkpad } from '../../../../../types';
|
||||
import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types';
|
||||
import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers';
|
||||
|
||||
import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/';
|
||||
import { OnCloseFn } from '../share_menu.component';
|
||||
|
||||
export { OnDownloadFn, OnCopyFn } from './flyout.component';
|
||||
|
||||
const getUnsupportedRenderers = (state: State) => {
|
||||
|
@ -35,12 +34,6 @@ const getUnsupportedRenderers = (state: State) => {
|
|||
return renderers;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: State) => ({
|
||||
renderedWorkpad: getRenderedWorkpad(state),
|
||||
unsupportedRenderers: getUnsupportedRenderers(state),
|
||||
workpad: getWorkpad(state),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
onClose: OnCloseFn;
|
||||
renderedWorkpad: CanvasRenderedWorkpad;
|
||||
|
@ -48,14 +41,18 @@ interface Props {
|
|||
workpad: CanvasWorkpad;
|
||||
}
|
||||
|
||||
export const ShareWebsiteFlyout = compose<ComponentProps, Pick<Props, 'onClose'>>(
|
||||
connect(mapStateToProps),
|
||||
withKibana,
|
||||
withProps(
|
||||
({ unsupportedRenderers, renderedWorkpad, onClose, workpad }: Props): ComponentProps => ({
|
||||
renderedWorkpad,
|
||||
unsupportedRenderers,
|
||||
onClose,
|
||||
})
|
||||
)
|
||||
)(Component);
|
||||
export const ShareWebsiteFlyout: FC<Pick<Props, 'onClose'>> = ({ onClose }) => {
|
||||
const { renderedWorkpad, unsupportedRenderers } = useSelector((state: State) => ({
|
||||
renderedWorkpad: getRenderedWorkpad(state),
|
||||
unsupportedRenderers: getUnsupportedRenderers(state),
|
||||
workpad: getWorkpad(state),
|
||||
}));
|
||||
|
||||
return (
|
||||
<FlyoutComponent
|
||||
onClose={onClose}
|
||||
unsupportedRenderers={unsupportedRenderers}
|
||||
renderedWorkpad={renderedWorkpad}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './use_download_runtime';
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import fileSaver from 'file-saver';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../../../../../common/lib/constants';
|
||||
import { ZIP } from '../../../../../../i18n/constants';
|
||||
|
||||
import { usePlatformService, useNotifyService, useWorkpadService } from '../../../../../services';
|
||||
import { CanvasRenderedWorkpad } from '../../../../../../shareable_runtime/types';
|
||||
|
||||
const strings = {
|
||||
getDownloadRuntimeFailureErrorMessage: () =>
|
||||
i18n.translate('xpack.canvas.error.downloadWorkpad.downloadRuntimeFailureErrorMessage', {
|
||||
defaultMessage: "Couldn't download Shareable Runtime",
|
||||
}),
|
||||
getDownloadZippedRuntimeFailureErrorMessage: () =>
|
||||
i18n.translate('xpack.canvas.error.downloadWorkpad.downloadZippedRuntimeFailureErrorMessage', {
|
||||
defaultMessage: "Couldn't download ZIP file",
|
||||
}),
|
||||
getShareableZipErrorTitle: (workpadName: string) =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', {
|
||||
defaultMessage:
|
||||
"Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.",
|
||||
values: {
|
||||
ZIP,
|
||||
workpadName,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const useDownloadRuntime = () => {
|
||||
const platformService = usePlatformService();
|
||||
const notifyService = useNotifyService();
|
||||
|
||||
const downloadRuntime = useCallback(() => {
|
||||
try {
|
||||
const path = `${platformService.getBasePath()}${API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD}`;
|
||||
window.open(path);
|
||||
return;
|
||||
} catch (err) {
|
||||
notifyService.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() });
|
||||
}
|
||||
}, [platformService, notifyService]);
|
||||
|
||||
return downloadRuntime;
|
||||
};
|
||||
|
||||
export const useDownloadZippedRuntime = () => {
|
||||
const workpadService = useWorkpadService();
|
||||
const notifyService = useNotifyService();
|
||||
|
||||
const downloadZippedRuntime = useCallback(
|
||||
(workpad: CanvasRenderedWorkpad) => {
|
||||
const downloadZip = async () => {
|
||||
try {
|
||||
let runtimeZipBlob: Blob | undefined;
|
||||
try {
|
||||
runtimeZipBlob = await workpadService.getRuntimeZip(workpad);
|
||||
} catch (err) {
|
||||
notifyService.error(err, {
|
||||
title: strings.getShareableZipErrorTitle(workpad.name),
|
||||
});
|
||||
}
|
||||
|
||||
if (runtimeZipBlob) {
|
||||
fileSaver.saveAs(runtimeZipBlob, 'canvas-workpad-embed.zip');
|
||||
}
|
||||
} catch (err) {
|
||||
notifyService.error(err, {
|
||||
title: strings.getDownloadZippedRuntimeFailureErrorMessage(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
downloadZip();
|
||||
},
|
||||
[notifyService, workpadService]
|
||||
);
|
||||
return downloadZippedRuntime;
|
||||
};
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, withProps } from 'recompose';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { CanvasWorkpad, State } from '../../../../types';
|
||||
import { downloadWorkpad } from '../../../lib/download_workpad';
|
||||
import { withServices, WithServicesProps } from '../../../services';
|
||||
import { getPages, getWorkpad } from '../../../state/selectors/workpad';
|
||||
import { Props as ComponentProps, ShareMenu as Component } from './share_menu.component';
|
||||
|
||||
const strings = {
|
||||
getUnknownExportErrorMessage: (type: string) =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', {
|
||||
defaultMessage: 'Unknown export type: {type}',
|
||||
values: {
|
||||
type,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: State) => ({
|
||||
workpad: getWorkpad(state),
|
||||
pageCount: getPages(state).length,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
workpad: CanvasWorkpad;
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
export const ShareMenu = compose<ComponentProps, {}>(
|
||||
connect(mapStateToProps),
|
||||
withServices,
|
||||
withProps(
|
||||
({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => {
|
||||
const {
|
||||
reporting: { start: reporting },
|
||||
} = services;
|
||||
|
||||
return {
|
||||
sharingServices: { reporting },
|
||||
sharingData: { workpad, pageCount },
|
||||
onExport: (type) => {
|
||||
switch (type) {
|
||||
case 'pdf':
|
||||
// notifications are automatically handled by the Reporting plugin
|
||||
break;
|
||||
case 'json':
|
||||
downloadWorkpad(workpad.id);
|
||||
return;
|
||||
default:
|
||||
throw new Error(strings.getUnknownExportErrorMessage(type));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
)
|
||||
)(Component);
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { State } from '../../../../types';
|
||||
import { useReportingService } from '../../../services';
|
||||
import { getPages, getWorkpad } from '../../../state/selectors/workpad';
|
||||
import { useDownloadWorkpad } from '../../hooks';
|
||||
import { ShareMenu as ShareMenuComponent } from './share_menu.component';
|
||||
|
||||
const strings = {
|
||||
getUnknownExportErrorMessage: (type: string) =>
|
||||
i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', {
|
||||
defaultMessage: 'Unknown export type: {type}',
|
||||
values: {
|
||||
type,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const ShareMenu: FC = () => {
|
||||
const { workpad, pageCount } = useSelector((state: State) => ({
|
||||
workpad: getWorkpad(state),
|
||||
pageCount: getPages(state).length,
|
||||
}));
|
||||
|
||||
const reportingService = useReportingService();
|
||||
const downloadWorkpad = useDownloadWorkpad();
|
||||
|
||||
const sharingServices = {
|
||||
reporting: reportingService.start,
|
||||
};
|
||||
|
||||
const sharingData = {
|
||||
workpad,
|
||||
pageCount,
|
||||
};
|
||||
|
||||
const onExport = useCallback(
|
||||
(type: string) => {
|
||||
switch (type) {
|
||||
case 'pdf':
|
||||
// notifications are automatically handled by the Reporting plugin
|
||||
break;
|
||||
case 'json':
|
||||
downloadWorkpad(workpad.id);
|
||||
return;
|
||||
default:
|
||||
throw new Error(strings.getUnknownExportErrorMessage(type));
|
||||
}
|
||||
},
|
||||
[downloadWorkpad, workpad]
|
||||
);
|
||||
|
||||
return (
|
||||
<ShareMenuComponent
|
||||
sharingServices={sharingServices}
|
||||
sharingData={sharingData}
|
||||
onExport={onExport}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import fileSaver from 'file-saver';
|
||||
import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants';
|
||||
import { ErrorStrings } from '../../i18n';
|
||||
|
||||
// TODO: clint - convert this whole file to hooks
|
||||
import { pluginServices } from '../services';
|
||||
|
||||
// @ts-expect-error untyped local
|
||||
import * as workpadService from './workpad_service';
|
||||
import { CanvasRenderedWorkpad } from '../../shareable_runtime/types';
|
||||
|
||||
const { downloadWorkpad: strings } = ErrorStrings;
|
||||
|
||||
export const downloadWorkpad = async (workpadId: string) => {
|
||||
try {
|
||||
const workpad = await workpadService.get(workpadId);
|
||||
const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' });
|
||||
fileSaver.saveAs(jsonBlob, `canvas-workpad-${workpad.name}-${workpad.id}.json`);
|
||||
} catch (err) {
|
||||
const notifyService = pluginServices.getServices().notify;
|
||||
notifyService.error(err, { title: strings.getDownloadFailureErrorMessage() });
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadRenderedWorkpad = async (renderedWorkpad: CanvasRenderedWorkpad) => {
|
||||
try {
|
||||
const jsonBlob = new Blob([JSON.stringify(renderedWorkpad)], { type: 'application/json' });
|
||||
fileSaver.saveAs(
|
||||
jsonBlob,
|
||||
`canvas-embed-workpad-${renderedWorkpad.name}-${renderedWorkpad.id}.json`
|
||||
);
|
||||
} catch (err) {
|
||||
const notifyService = pluginServices.getServices().notify;
|
||||
notifyService.error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() });
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadRuntime = async (basePath: string) => {
|
||||
try {
|
||||
const path = `${basePath}${API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD}`;
|
||||
window.open(path);
|
||||
return;
|
||||
} catch (err) {
|
||||
const notifyService = pluginServices.getServices().notify;
|
||||
notifyService.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() });
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadZippedRuntime = async (data: any) => {
|
||||
try {
|
||||
const zip = new Blob([data], { type: 'octet/stream' });
|
||||
fileSaver.saveAs(zip, 'canvas-workpad-embed.zip');
|
||||
} catch (err) {
|
||||
const notifyService = pluginServices.getServices().notify;
|
||||
notifyService.error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() });
|
||||
}
|
||||
};
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO: clint - move to workpad service.
|
||||
import {
|
||||
API_ROUTE_WORKPAD,
|
||||
API_ROUTE_WORKPAD_ASSETS,
|
||||
API_ROUTE_WORKPAD_STRUCTURES,
|
||||
DEFAULT_WORKPAD_CSS,
|
||||
} from '../../common/lib/constants';
|
||||
import { fetch } from '../../common/lib/fetch';
|
||||
import { pluginServices } from '../services';
|
||||
|
||||
/*
|
||||
Remove any top level keys from the workpad which will be rejected by validation
|
||||
*/
|
||||
const validKeys = [
|
||||
'@created',
|
||||
'@timestamp',
|
||||
'assets',
|
||||
'colors',
|
||||
'css',
|
||||
'variables',
|
||||
'height',
|
||||
'id',
|
||||
'isWriteable',
|
||||
'name',
|
||||
'page',
|
||||
'pages',
|
||||
'width',
|
||||
];
|
||||
|
||||
const sanitizeWorkpad = function (workpad) {
|
||||
const workpadKeys = Object.keys(workpad);
|
||||
|
||||
for (const key of workpadKeys) {
|
||||
if (!validKeys.includes(key)) {
|
||||
delete workpad[key];
|
||||
}
|
||||
}
|
||||
|
||||
return workpad;
|
||||
};
|
||||
|
||||
const getApiPath = function () {
|
||||
const platformService = pluginServices.getServices().platform;
|
||||
const basePath = platformService.getBasePath();
|
||||
return `${basePath}${API_ROUTE_WORKPAD}`;
|
||||
};
|
||||
|
||||
const getApiPathStructures = function () {
|
||||
const platformService = pluginServices.getServices().platform;
|
||||
const basePath = platformService.getBasePath();
|
||||
return `${basePath}${API_ROUTE_WORKPAD_STRUCTURES}`;
|
||||
};
|
||||
|
||||
const getApiPathAssets = function () {
|
||||
const platformService = pluginServices.getServices().platform;
|
||||
const basePath = platformService.getBasePath();
|
||||
return `${basePath}${API_ROUTE_WORKPAD_ASSETS}`;
|
||||
};
|
||||
|
||||
export function create(workpad) {
|
||||
return fetch.post(getApiPath(), {
|
||||
...sanitizeWorkpad({ ...workpad }),
|
||||
assets: workpad.assets || {},
|
||||
variables: workpad.variables || [],
|
||||
});
|
||||
}
|
||||
|
||||
export async function createFromTemplate(templateId) {
|
||||
return fetch.post(getApiPath(), {
|
||||
templateId,
|
||||
});
|
||||
}
|
||||
|
||||
export function get(workpadId) {
|
||||
return fetch.get(`${getApiPath()}/${workpadId}`).then(({ data: workpad }) => {
|
||||
// shim old workpads with new properties
|
||||
return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad };
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: I think this function is never used. Look into and remove the corresponding route as well
|
||||
export function update(id, workpad) {
|
||||
return fetch.put(`${getApiPath()}/${id}`, sanitizeWorkpad({ ...workpad }));
|
||||
}
|
||||
|
||||
export function updateWorkpad(id, workpad) {
|
||||
return fetch.put(`${getApiPathStructures()}/${id}`, sanitizeWorkpad({ ...workpad }));
|
||||
}
|
||||
|
||||
export function updateAssets(id, workpadAssets) {
|
||||
return fetch.put(`${getApiPathAssets()}/${id}`, workpadAssets);
|
||||
}
|
||||
|
||||
export function remove(id) {
|
||||
return fetch.delete(`${getApiPath()}/${id}`);
|
||||
}
|
||||
|
||||
export function find(searchTerm) {
|
||||
const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0;
|
||||
|
||||
return fetch
|
||||
.get(`${getApiPath()}/find?name=${validSearchTerm ? searchTerm : ''}&perPage=10000`)
|
||||
.then(({ data: workpads }) => workpads);
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useWorkpadPersist } from './use_workpad_persist';
|
||||
|
||||
const mockGetState = jest.fn();
|
||||
const mockUpdateWorkpad = jest.fn();
|
||||
const mockUpdateAssets = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
const mockNotifyError = jest.fn();
|
||||
|
||||
// Mock the hooks and actions used by the UseWorkpad hook
|
||||
jest.mock('react-redux', () => ({
|
||||
useSelector: (selector: any) => selector(mockGetState()),
|
||||
}));
|
||||
|
||||
jest.mock('../../../services', () => ({
|
||||
useWorkpadService: () => ({
|
||||
updateWorkpad: mockUpdateWorkpad,
|
||||
updateAssets: mockUpdateAssets,
|
||||
update: mockUpdate,
|
||||
}),
|
||||
useNotifyService: () => ({
|
||||
error: mockNotifyError,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useWorkpadPersist', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('initial render does not persist state', () => {
|
||||
const state = {
|
||||
persistent: {
|
||||
workpad: { some: 'workpad' },
|
||||
},
|
||||
assets: {
|
||||
asset1: 'some asset',
|
||||
asset2: 'other asset',
|
||||
},
|
||||
};
|
||||
|
||||
mockGetState.mockReturnValue(state);
|
||||
|
||||
renderHook(useWorkpadPersist);
|
||||
|
||||
expect(mockUpdateWorkpad).not.toBeCalled();
|
||||
expect(mockUpdateAssets).not.toBeCalled();
|
||||
expect(mockUpdate).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('changes to workpad cause a workpad update', () => {
|
||||
const state = {
|
||||
persistent: {
|
||||
workpad: { some: 'workpad' },
|
||||
},
|
||||
assets: {
|
||||
asset1: 'some asset',
|
||||
asset2: 'other asset',
|
||||
},
|
||||
};
|
||||
|
||||
mockGetState.mockReturnValue(state);
|
||||
|
||||
const { rerender } = renderHook(useWorkpadPersist);
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
persistent: {
|
||||
workpad: { new: 'workpad' },
|
||||
},
|
||||
};
|
||||
mockGetState.mockReturnValue(newState);
|
||||
|
||||
rerender();
|
||||
|
||||
expect(mockUpdateWorkpad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('changes to assets cause an asset update', () => {
|
||||
const state = {
|
||||
persistent: {
|
||||
workpad: { some: 'workpad' },
|
||||
},
|
||||
assets: {
|
||||
asset1: 'some asset',
|
||||
asset2: 'other asset',
|
||||
},
|
||||
};
|
||||
|
||||
mockGetState.mockReturnValue(state);
|
||||
|
||||
const { rerender } = renderHook(useWorkpadPersist);
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
assets: {
|
||||
asset1: 'some asset',
|
||||
},
|
||||
};
|
||||
mockGetState.mockReturnValue(newState);
|
||||
|
||||
rerender();
|
||||
|
||||
expect(mockUpdateAssets).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('changes to both assets and workpad causes a full update', () => {
|
||||
const state = {
|
||||
persistent: {
|
||||
workpad: { some: 'workpad' },
|
||||
},
|
||||
assets: {
|
||||
asset1: 'some asset',
|
||||
asset2: 'other asset',
|
||||
},
|
||||
};
|
||||
|
||||
mockGetState.mockReturnValue(state);
|
||||
|
||||
const { rerender } = renderHook(useWorkpadPersist);
|
||||
|
||||
const newState = {
|
||||
persistent: {
|
||||
workpad: { new: 'workpad' },
|
||||
},
|
||||
assets: {
|
||||
asset1: 'some asset',
|
||||
},
|
||||
};
|
||||
mockGetState.mockReturnValue(newState);
|
||||
|
||||
rerender();
|
||||
|
||||
expect(mockUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('non changes causes no updated', () => {
|
||||
const state = {
|
||||
persistent: {
|
||||
workpad: { some: 'workpad' },
|
||||
},
|
||||
assets: {
|
||||
asset1: 'some asset',
|
||||
asset2: 'other asset',
|
||||
},
|
||||
};
|
||||
mockGetState.mockReturnValue(state);
|
||||
|
||||
const { rerender } = renderHook(useWorkpadPersist);
|
||||
|
||||
rerender();
|
||||
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
expect(mockUpdateWorkpad).not.toHaveBeenCalled();
|
||||
expect(mockUpdateAssets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('non write permissions causes no updates', () => {
|
||||
const state = {
|
||||
persistent: {
|
||||
workpad: { some: 'workpad' },
|
||||
},
|
||||
assets: {
|
||||
asset1: 'some asset',
|
||||
asset2: 'other asset',
|
||||
},
|
||||
transient: {
|
||||
canUserWrite: false,
|
||||
},
|
||||
};
|
||||
mockGetState.mockReturnValue(state);
|
||||
|
||||
const { rerender } = renderHook(useWorkpadPersist);
|
||||
|
||||
const newState = {
|
||||
persistent: {
|
||||
workpad: { new: 'workpad value' },
|
||||
},
|
||||
assets: {
|
||||
asset3: 'something',
|
||||
},
|
||||
transient: {
|
||||
canUserWrite: false,
|
||||
},
|
||||
};
|
||||
mockGetState.mockReturnValue(newState);
|
||||
|
||||
rerender();
|
||||
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
expect(mockUpdateWorkpad).not.toHaveBeenCalled();
|
||||
expect(mockUpdateAssets).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CanvasWorkpad, State } from '../../../../types';
|
||||
import { getWorkpad, getFullWorkpadPersisted } from '../../../state/selectors/workpad';
|
||||
import { canUserWrite } from '../../../state/selectors/app';
|
||||
import { getAssetIds } from '../../../state/selectors/assets';
|
||||
import { useWorkpadService, useNotifyService } from '../../../services';
|
||||
|
||||
const strings = {
|
||||
getSaveFailureTitle: () =>
|
||||
i18n.translate('xpack.canvas.error.esPersist.saveFailureTitle', {
|
||||
defaultMessage: "Couldn't save your changes to Elasticsearch",
|
||||
}),
|
||||
getTooLargeErrorMessage: () =>
|
||||
i18n.translate('xpack.canvas.error.esPersist.tooLargeErrorMessage', {
|
||||
defaultMessage:
|
||||
'The server gave a response that the workpad data was too large. This usually means uploaded image assets that are too large for Kibana or a proxy. Try removing some assets in the asset manager.',
|
||||
}),
|
||||
getUpdateFailureTitle: () =>
|
||||
i18n.translate('xpack.canvas.error.esPersist.updateFailureTitle', {
|
||||
defaultMessage: "Couldn't update workpad",
|
||||
}),
|
||||
};
|
||||
|
||||
export const useWorkpadPersist = () => {
|
||||
const service = useWorkpadService();
|
||||
const notifyService = useNotifyService();
|
||||
const notifyError = useCallback(
|
||||
(err: any) => {
|
||||
const statusCode = err.response && err.response.status;
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return notifyService.error(err.response, {
|
||||
title: strings.getSaveFailureTitle(),
|
||||
});
|
||||
case 413:
|
||||
return notifyService.error(strings.getTooLargeErrorMessage(), {
|
||||
title: strings.getSaveFailureTitle(),
|
||||
});
|
||||
default:
|
||||
return notifyService.error(err, {
|
||||
title: strings.getUpdateFailureTitle(),
|
||||
});
|
||||
}
|
||||
},
|
||||
[notifyService]
|
||||
);
|
||||
|
||||
// Watch for workpad state or workpad assets to change and then persist those changes
|
||||
const [workpad, assetIds, fullWorkpad, canWrite]: [
|
||||
CanvasWorkpad,
|
||||
Array<string | number>,
|
||||
CanvasWorkpad,
|
||||
boolean
|
||||
] = useSelector((state: State) => [
|
||||
getWorkpad(state),
|
||||
getAssetIds(state),
|
||||
getFullWorkpadPersisted(state),
|
||||
canUserWrite(state),
|
||||
]);
|
||||
|
||||
const previousWorkpad = usePrevious(workpad);
|
||||
const previousAssetIds = usePrevious(assetIds);
|
||||
|
||||
const workpadChanged = previousWorkpad && workpad !== previousWorkpad;
|
||||
const assetsChanged = previousAssetIds && !isEqual(assetIds, previousAssetIds);
|
||||
|
||||
useEffect(() => {
|
||||
if (canWrite) {
|
||||
if (workpadChanged && assetsChanged) {
|
||||
service.update(workpad.id, fullWorkpad).catch(notifyError);
|
||||
}
|
||||
if (workpadChanged) {
|
||||
service.updateWorkpad(workpad.id, workpad).catch(notifyError);
|
||||
} else if (assetsChanged) {
|
||||
service.updateAssets(workpad.id, fullWorkpad.assets).catch(notifyError);
|
||||
}
|
||||
}
|
||||
}, [service, workpad, fullWorkpad, workpadChanged, assetsChanged, canWrite, notifyError]);
|
||||
};
|
|
@ -20,6 +20,7 @@ import { useWorkpad } from './hooks/use_workpad';
|
|||
import { useRestoreHistory } from './hooks/use_restore_history';
|
||||
import { useWorkpadHistory } from './hooks/use_workpad_history';
|
||||
import { usePageSync } from './hooks/use_page_sync';
|
||||
import { useWorkpadPersist } from './hooks/use_workpad_persist';
|
||||
import { WorkpadPageRouteProps, WorkpadRouteProps, WorkpadPageRouteParams } from '.';
|
||||
import { WorkpadRoutingContextComponent } from './workpad_routing_context';
|
||||
import { WorkpadPresentationHelper } from './workpad_presentation_helper';
|
||||
|
@ -88,6 +89,7 @@ export const WorkpadHistoryManager: FC = ({ children }) => {
|
|||
useRestoreHistory();
|
||||
useWorkpadHistory();
|
||||
usePageSync();
|
||||
useWorkpadPersist();
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
|
|
@ -14,6 +14,9 @@ import {
|
|||
API_ROUTE_WORKPAD,
|
||||
DEFAULT_WORKPAD_CSS,
|
||||
API_ROUTE_TEMPLATES,
|
||||
API_ROUTE_WORKPAD_ASSETS,
|
||||
API_ROUTE_WORKPAD_STRUCTURES,
|
||||
API_ROUTE_SHAREABLE_ZIP,
|
||||
} from '../../../common/lib/constants';
|
||||
import { CanvasWorkpad } from '../../../types';
|
||||
|
||||
|
@ -93,5 +96,25 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart,
|
|||
remove: (id: string) => {
|
||||
return coreStart.http.delete(`${getApiPath()}/${id}`);
|
||||
},
|
||||
update: (id, workpad) => {
|
||||
return coreStart.http.put(`${getApiPath()}/${id}`, {
|
||||
body: JSON.stringify({ ...sanitizeWorkpad({ ...workpad }) }),
|
||||
});
|
||||
},
|
||||
updateWorkpad: (id, workpad) => {
|
||||
return coreStart.http.put(`${API_ROUTE_WORKPAD_STRUCTURES}/${id}`, {
|
||||
body: JSON.stringify({ ...sanitizeWorkpad({ ...workpad }) }),
|
||||
});
|
||||
},
|
||||
updateAssets: (id, assets) => {
|
||||
return coreStart.http.put(`${API_ROUTE_WORKPAD_ASSETS}/${id}`, {
|
||||
body: JSON.stringify(assets),
|
||||
});
|
||||
},
|
||||
getRuntimeZip: (workpad) => {
|
||||
return coreStart.http.post<Blob>(API_ROUTE_SHAREABLE_ZIP, {
|
||||
body: JSON.stringify(workpad),
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -26,13 +26,14 @@ const defaultContextValue = {
|
|||
search: {},
|
||||
};
|
||||
|
||||
const context = createContext<CanvasServices>(defaultContextValue as CanvasServices);
|
||||
export const ServicesContext = createContext<CanvasServices>(defaultContextValue as CanvasServices);
|
||||
|
||||
export const useServices = () => useContext(context);
|
||||
export const useServices = () => useContext(ServicesContext);
|
||||
export const useEmbeddablesService = () => useServices().embeddables;
|
||||
export const useExpressionsService = () => useServices().expressions;
|
||||
export const useNavLinkService = () => useServices().navLink;
|
||||
export const useLabsService = () => useServices().labs;
|
||||
export const useReportingService = () => useServices().reporting;
|
||||
|
||||
export const withServices = <Props extends WithServicesProps>(type: ComponentType<Props>) => {
|
||||
const EnhancedType: FC<Props> = (props) =>
|
||||
|
@ -53,5 +54,5 @@ export const LegacyServicesProvider: FC<{
|
|||
reporting: specifiedProviders.reporting.getService(),
|
||||
labs: specifiedProviders.labs.getService(),
|
||||
};
|
||||
return <context.Provider value={value}>{children}</context.Provider>;
|
||||
return <ServicesContext.Provider value={value}>{children}</ServicesContext.Provider>;
|
||||
};
|
||||
|
|
|
@ -97,4 +97,18 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({
|
|||
action('workpadService.remove')(id);
|
||||
return Promise.resolve();
|
||||
},
|
||||
update: (id, workpad) => {
|
||||
action('worpadService.update')(workpad, id);
|
||||
return Promise.resolve();
|
||||
},
|
||||
updateWorkpad: (id, workpad) => {
|
||||
action('workpadService.updateWorkpad')(workpad, id);
|
||||
return Promise.resolve();
|
||||
},
|
||||
updateAssets: (id, assets) => {
|
||||
action('workpadService.updateAssets')(assets, id);
|
||||
return Promise.resolve();
|
||||
},
|
||||
getRuntimeZip: (workpad) =>
|
||||
Promise.resolve(new Blob([JSON.stringify(workpad)], { type: 'application/json' })),
|
||||
});
|
||||
|
|
|
@ -96,4 +96,9 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = () => ({
|
|||
createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()),
|
||||
find: findNoWorkpads(),
|
||||
remove: (_id: string) => Promise.resolve(),
|
||||
update: (id, workpad) => Promise.resolve(),
|
||||
updateWorkpad: (id, workpad) => Promise.resolve(),
|
||||
updateAssets: (id, assets) => Promise.resolve(),
|
||||
getRuntimeZip: (workpad) =>
|
||||
Promise.resolve(new Blob([JSON.stringify(workpad)], { type: 'application/json' })),
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { CanvasWorkpad, CanvasTemplate } from '../../types';
|
||||
import { CanvasRenderedWorkpad } from '../../shareable_runtime/types';
|
||||
|
||||
export type FoundWorkpads = Array<Pick<CanvasWorkpad, 'name' | 'id' | '@timestamp' | '@created'>>;
|
||||
export type FoundWorkpad = FoundWorkpads[number];
|
||||
|
@ -24,4 +25,8 @@ export interface CanvasWorkpadService {
|
|||
find: (term: string) => Promise<WorkpadFindResponse>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
findTemplates: () => Promise<TemplateFindResponse>;
|
||||
update: (id: string, workpad: CanvasWorkpad) => Promise<void>;
|
||||
updateWorkpad: (id: string, workpad: CanvasWorkpad) => Promise<void>;
|
||||
updateAssets: (id: string, assets: CanvasWorkpad['assets']) => Promise<void>;
|
||||
getRuntimeZip: (workpad: CanvasRenderedWorkpad) => Promise<Blob>;
|
||||
}
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { ErrorStrings } from '../../../i18n';
|
||||
import { getWorkpad, getFullWorkpadPersisted, getWorkpadPersisted } from '../selectors/workpad';
|
||||
import { getAssetIds } from '../selectors/assets';
|
||||
import { appReady } from '../actions/app';
|
||||
import { setWorkpad, setRefreshInterval, resetWorkpad } from '../actions/workpad';
|
||||
import { setAssets, resetAssets } from '../actions/assets';
|
||||
import * as transientActions from '../actions/transient';
|
||||
import * as resolvedArgsActions from '../actions/resolved_args';
|
||||
import { update, updateAssets, updateWorkpad } from '../../lib/workpad_service';
|
||||
import { pluginServices } from '../../services';
|
||||
import { canUserWrite } from '../selectors/app';
|
||||
|
||||
const { esPersist: strings } = ErrorStrings;
|
||||
|
||||
const workpadChanged = (before, after) => {
|
||||
const workpad = getWorkpad(before);
|
||||
return getWorkpad(after) !== workpad;
|
||||
};
|
||||
|
||||
const assetsChanged = (before, after) => {
|
||||
const assets = getAssetIds(before);
|
||||
return !isEqual(assets, getAssetIds(after));
|
||||
};
|
||||
|
||||
export const esPersistMiddleware = ({ getState }) => {
|
||||
// these are the actions we don't want to trigger a persist call
|
||||
const skippedActions = [
|
||||
appReady, // there's no need to resave the workpad once we've loaded it.
|
||||
resetWorkpad, // used for resetting the workpad in state
|
||||
setWorkpad, // used for loading and creating workpads
|
||||
setAssets, // used when loading assets
|
||||
resetAssets, // used when creating new workpads
|
||||
setRefreshInterval, // used to set refresh time interval which is a transient value
|
||||
...Object.values(resolvedArgsActions), // no resolved args affect persisted values
|
||||
...Object.values(transientActions), // no transient actions cause persisted state changes
|
||||
].map((a) => a.toString());
|
||||
|
||||
return (next) => (action) => {
|
||||
// if the action is in the skipped list, do not persist
|
||||
if (skippedActions.indexOf(action.type) >= 0) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
// capture state before and after the action
|
||||
const curState = getState();
|
||||
next(action);
|
||||
const newState = getState();
|
||||
|
||||
// skips the update request if user doesn't have write permissions
|
||||
if (!canUserWrite(newState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notifyError = (err) => {
|
||||
const statusCode = err.response && err.response.status;
|
||||
const notifyService = pluginServices.getServices().notify;
|
||||
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return notifyService.error(err.response, {
|
||||
title: strings.getSaveFailureTitle(),
|
||||
});
|
||||
case 413:
|
||||
return notifyService.error(strings.getTooLargeErrorMessage(), {
|
||||
title: strings.getSaveFailureTitle(),
|
||||
});
|
||||
default:
|
||||
return notifyService.error(err, {
|
||||
title: strings.getUpdateFailureTitle(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const changedWorkpad = workpadChanged(curState, newState);
|
||||
const changedAssets = assetsChanged(curState, newState);
|
||||
|
||||
if (changedWorkpad && changedAssets) {
|
||||
// if both the workpad and the assets changed, save it in its entirety to elasticsearch
|
||||
const persistedWorkpad = getFullWorkpadPersisted(getState());
|
||||
return update(persistedWorkpad.id, persistedWorkpad).catch(notifyError);
|
||||
} else if (changedWorkpad) {
|
||||
// if the workpad changed, save it to elasticsearch
|
||||
const persistedWorkpad = getWorkpadPersisted(getState());
|
||||
return updateWorkpad(persistedWorkpad.id, persistedWorkpad).catch(notifyError);
|
||||
} else if (changedAssets) {
|
||||
// if the assets changed, save it to elasticsearch
|
||||
const persistedWorkpad = getFullWorkpadPersisted(getState());
|
||||
return updateAssets(persistedWorkpad.id, persistedWorkpad.assets).catch(notifyError);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -8,21 +8,13 @@
|
|||
import { applyMiddleware, compose as reduxCompose } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { getWindow } from '../../lib/get_window';
|
||||
import { esPersistMiddleware } from './es_persist';
|
||||
import { inFlight } from './in_flight';
|
||||
import { workpadUpdate } from './workpad_update';
|
||||
import { elementStats } from './element_stats';
|
||||
import { resolvedArgs } from './resolved_args';
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(
|
||||
thunkMiddleware,
|
||||
elementStats,
|
||||
resolvedArgs,
|
||||
esPersistMiddleware,
|
||||
inFlight,
|
||||
workpadUpdate
|
||||
),
|
||||
applyMiddleware(thunkMiddleware, elementStats, resolvedArgs, inFlight, workpadUpdate),
|
||||
];
|
||||
|
||||
// compose with redux devtools, if extension is installed
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { get, omit } from 'lodash';
|
||||
import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/common';
|
||||
import { CanvasRenderedWorkpad } from '../../../shareable_runtime/types';
|
||||
import { append } from '../../lib/modify_path';
|
||||
import { getAssets } from './assets';
|
||||
import {
|
||||
|
@ -500,7 +501,7 @@ export function getRenderedWorkpad(state: State) {
|
|||
return {
|
||||
pages: renderedPages,
|
||||
...rest,
|
||||
};
|
||||
} as CanvasRenderedWorkpad;
|
||||
}
|
||||
|
||||
export function getRenderedWorkpadExpressions(state: State) {
|
||||
|
|
|
@ -24,15 +24,14 @@ export interface CanvasRenderedElement {
|
|||
* Represents a Page within a Canvas Workpad that is made up of ready-to-
|
||||
* render Elements.
|
||||
*/
|
||||
export interface CanvasRenderedPage extends Omit<Omit<CanvasPage, 'elements'>, 'groups'> {
|
||||
export interface CanvasRenderedPage extends Omit<CanvasPage, 'elements'> {
|
||||
elements: CanvasRenderedElement[];
|
||||
groups: CanvasRenderedElement[][];
|
||||
}
|
||||
|
||||
/**
|
||||
* A Canvas Workpad made up of ready-to-render Elements.
|
||||
*/
|
||||
export interface CanvasRenderedWorkpad extends Omit<CanvasWorkpad, 'pages'> {
|
||||
export interface CanvasRenderedWorkpad extends Omit<CanvasWorkpad, 'pages' | 'variables'> {
|
||||
pages: CanvasRenderedPage[];
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ interface PersistentState {
|
|||
|
||||
export interface State {
|
||||
app: StoreAppState;
|
||||
assets: { [assetKey: string]: AssetType | undefined };
|
||||
assets: { [assetKey: string]: AssetType };
|
||||
transient: TransientState;
|
||||
persistent: PersistentState;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue