[canvas] New Home Page (#102446)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2021-06-22 14:11:15 -05:00 committed by GitHub
parent f422cbdcf1
commit 2b0f1256dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 2157 additions and 2289 deletions

View file

@ -1166,12 +1166,6 @@ export const ComponentStrings = {
description: 'This is referring to the dimensions of U.S. standard letter paper.',
}),
},
WorkpadCreate: {
getWorkpadCreateButtonLabel: () =>
i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', {
defaultMessage: 'Create workpad',
}),
},
WorkpadHeader: {
getAddElementButtonLabel: () =>
i18n.translate('xpack.canvas.workpadHeader.addElementButtonLabel', {
@ -1546,219 +1540,4 @@ export const ComponentStrings = {
defaultMessage: 'Reset',
}),
},
WorkpadLoader: {
getClonedWorkpadName: (workpadName: string) =>
i18n.translate('xpack.canvas.workpadLoader.clonedWorkpadName', {
defaultMessage: 'Copy of {workpadName}',
values: {
workpadName,
},
description:
'This suffix is added to the end of the name of a cloned workpad to indicate that this ' +
'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"',
}),
getCloneToolTip: () =>
i18n.translate('xpack.canvas.workpadLoader.cloneTooltip', {
defaultMessage: 'Clone workpad',
}),
getCreateWorkpadLoadingDescription: () =>
i18n.translate('xpack.canvas.workpadLoader.createWorkpadLoadingDescription', {
defaultMessage: 'Creating workpad...',
description:
'This message appears while the user is waiting for a new workpad to be created',
}),
getDeleteButtonAriaLabel: (numberOfWorkpads: number) =>
i18n.translate('xpack.canvas.workpadLoader.deleteButtonAriaLabel', {
defaultMessage: 'Delete {numberOfWorkpads} workpads',
values: {
numberOfWorkpads,
},
}),
getDeleteButtonLabel: (numberOfWorkpads: number) =>
i18n.translate('xpack.canvas.workpadLoader.deleteButtonLabel', {
defaultMessage: 'Delete ({numberOfWorkpads})',
values: {
numberOfWorkpads,
},
}),
getDeleteModalConfirmButtonLabel: () =>
i18n.translate('xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel', {
defaultMessage: 'Delete',
}),
getDeleteModalDescription: () =>
i18n.translate('xpack.canvas.workpadLoader.deleteModalDescription', {
defaultMessage: `You can't recover deleted workpads.`,
}),
getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) =>
i18n.translate('xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle', {
defaultMessage: 'Delete {numberOfWorkpads} workpads?',
values: {
numberOfWorkpads,
},
}),
getDeleteSingleWorkpadModalTitle: (workpadName: string) =>
i18n.translate('xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle', {
defaultMessage: `Delete workpad '{workpadName}'?`,
values: {
workpadName,
},
}),
getEmptyPromptGettingStartedDescription: () =>
i18n.translate('xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription', {
defaultMessage:
'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.',
values: {
JSON,
},
}),
getEmptyPromptNewUserDescription: () =>
i18n.translate('xpack.canvas.workpadLoader.emptyPromptNewUserDescription', {
defaultMessage: 'New to {CANVAS}?',
values: {
CANVAS,
},
}),
getEmptyPromptTitle: () =>
i18n.translate('xpack.canvas.workpadLoader.emptyPromptTitle', {
defaultMessage: 'Add your first workpad',
}),
getExportButtonAriaLabel: (numberOfWorkpads: number) =>
i18n.translate('xpack.canvas.workpadLoader.exportButtonAriaLabel', {
defaultMessage: 'Export {numberOfWorkpads} workpads',
values: {
numberOfWorkpads,
},
}),
getExportButtonLabel: (numberOfWorkpads: number) =>
i18n.translate('xpack.canvas.workpadLoader.exportButtonLabel', {
defaultMessage: 'Export ({numberOfWorkpads})',
values: {
numberOfWorkpads,
},
}),
getExportToolTip: () =>
i18n.translate('xpack.canvas.workpadLoader.exportTooltip', {
defaultMessage: 'Export workpad',
}),
getFetchLoadingDescription: () =>
i18n.translate('xpack.canvas.workpadLoader.fetchLoadingDescription', {
defaultMessage: 'Fetching workpads...',
description:
'This message appears while the user is waiting for their list of workpads to load',
}),
getFilePickerPlaceholder: () =>
i18n.translate('xpack.canvas.workpadLoader.filePickerPlaceholder', {
defaultMessage: 'Import workpad {JSON} file',
values: {
JSON,
},
}),
getLoadWorkpadArialLabel: (workpadName: string) =>
i18n.translate('xpack.canvas.workpadLoader.loadWorkpadArialLabel', {
defaultMessage: `Load workpad '{workpadName}'`,
values: {
workpadName,
},
}),
getNoPermissionToCloneToolTip: () =>
i18n.translate('xpack.canvas.workpadLoader.noPermissionToCloneToolTip', {
defaultMessage: `You don't have permission to clone workpads`,
}),
getNoPermissionToCreateToolTip: () =>
i18n.translate('xpack.canvas.workpadLoader.noPermissionToCreateToolTip', {
defaultMessage: `You don't have permission to create workpads`,
}),
getNoPermissionToDeleteToolTip: () =>
i18n.translate('xpack.canvas.workpadLoader.noPermissionToDeleteToolTip', {
defaultMessage: `You don't have permission to delete workpads`,
}),
getNoPermissionToUploadToolTip: () =>
i18n.translate('xpack.canvas.workpadLoader.noPermissionToUploadToolTip', {
defaultMessage: `You don't have permission to upload workpads`,
}),
getSampleDataLinkLabel: () =>
i18n.translate('xpack.canvas.workpadLoader.sampleDataLinkLabel', {
defaultMessage: 'Add your first workpad',
}),
getTableCreatedColumnTitle: () =>
i18n.translate('xpack.canvas.workpadLoader.table.createdColumnTitle', {
defaultMessage: 'Created',
description: 'This column in the table contains the date/time the workpad was created.',
}),
getTableNameColumnTitle: () =>
i18n.translate('xpack.canvas.workpadLoader.table.nameColumnTitle', {
defaultMessage: 'Workpad name',
}),
getTableUpdatedColumnTitle: () =>
i18n.translate('xpack.canvas.workpadLoader.table.updatedColumnTitle', {
defaultMessage: 'Updated',
description:
'This column in the table contains the date/time the workpad was last updated.',
}),
getTableActionsColumnTitle: () =>
i18n.translate('xpack.canvas.workpadLoader.table.actionsColumnTitle', {
defaultMessage: 'Actions',
description:
'This column in the table contains the actions that can be taken on a workpad.',
}),
},
WorkpadManager: {
getModalTitle: () =>
i18n.translate('xpack.canvas.workpadManager.modalTitle', {
defaultMessage: '{CANVAS} workpads',
values: {
CANVAS,
},
}),
getMyWorkpadsTabLabel: () =>
i18n.translate('xpack.canvas.workpadManager.myWorkpadsTabLabel', {
defaultMessage: 'My workpads',
}),
getWorkpadTemplatesTabLabel: () =>
i18n.translate('xpack.canvas.workpadManager.workpadTemplatesTabLabel', {
defaultMessage: 'Templates',
description: 'The label for the tab that displays a list of designed workpad templates.',
}),
},
WorkpadSearch: {
getWorkpadSearchPlaceholder: () =>
i18n.translate('xpack.canvas.workpadSearch.searchPlaceholder', {
defaultMessage: 'Find workpad',
}),
},
WorkpadTemplates: {
getCloneTemplateLinkAriaLabel: (templateName: string) =>
i18n.translate('xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel', {
defaultMessage: `Clone workpad template '{templateName}'`,
values: {
templateName,
},
}),
getTableDescriptionColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', {
defaultMessage: 'Description',
}),
getTableNameColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', {
defaultMessage: 'Template name',
}),
getTableTagsColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', {
defaultMessage: 'Tags',
description:
'This column contains relevant tags that indicate what type of template ' +
'is displayed. For example: "report", "presentation", etc.',
}),
getTemplateSearchPlaceholder: () =>
i18n.translate('xpack.canvas.workpadTemplate.searchPlaceholder', {
defaultMessage: 'Find template',
}),
getCreatingTemplateLabel: (templateName: string) =>
i18n.translate('xpack.canvas.workpadTemplate.creatingTemplateLabel', {
defaultMessage: `Creating from template '{templateName}'`,
values: {
templateName,
},
}),
},
};

View file

@ -6,7 +6,6 @@
*/
import { i18n } from '@kbn/i18n';
import { CANVAS, JSON } from './constants';
export const ErrorStrings = {
actionsElements: {
@ -93,54 +92,10 @@ export const ErrorStrings = {
},
}),
},
WorkpadFileUpload: {
getAcceptJSONOnlyErrorMessage: () =>
i18n.translate('xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage', {
defaultMessage: 'Only {JSON} files are accepted',
values: {
JSON,
},
}),
getFileUploadFailureWithFileNameErrorMessage: (fileName: string) =>
i18n.translate('xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage', {
defaultMessage: `Couldn't upload '{fileName}'`,
values: {
fileName,
},
}),
getFileUploadFailureWithoutFileNameErrorMessage: () =>
i18n.translate(
'xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage',
{
defaultMessage: `Couldn't upload file`,
}
),
getMissingPropertiesErrorMessage: () =>
i18n.translate('xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage', {
defaultMessage:
'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.',
values: {
CANVAS,
JSON,
},
}),
},
WorkpadLoader: {
getCloneFailureErrorMessage: () =>
i18n.translate('xpack.canvas.error.workpadLoader.cloneFailureErrorMessage', {
defaultMessage: `Couldn't clone workpad`,
}),
getDeleteFailureErrorMessage: () =>
i18n.translate('xpack.canvas.error.workpadLoader.deleteFailureErrorMessage', {
defaultMessage: `Couldn't delete all workpads`,
}),
getFindFailureErrorMessage: () =>
i18n.translate('xpack.canvas.error.workpadLoader.findFailureErrorMessage', {
defaultMessage: `Couldn't find workpad`,
}),
getUploadFailureErrorMessage: () =>
i18n.translate('xpack.canvas.error.workpadLoader.uploadFailureErrorMessage', {
defaultMessage: `Couldn't upload workpad`,
WorkpadDropzone: {
getTooManyFilesErrorMessage: () =>
i18n.translate('xpack.canvas.error.workpadDropzone.tooManyFilesErrorMessage', {
defaultMessage: 'One one file can be uploaded at a time',
}),
},
workpadRoutes: {

View file

@ -0,0 +1,67 @@
/*
* 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, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { KibanaPageTemplate } from '../../../../../../src/plugins/kibana_react/public';
import { withSuspense } from '../../../../../../src/plugins/presentation_util/public';
import { WorkpadCreate } from './workpad_create';
import { LazyWorkpadTemplates } from './workpad_templates';
import { LazyMyWorkpads } from './my_workpads';
export type HomePageTab = 'workpads' | 'templates';
export interface Props {
activeTab?: HomePageTab;
}
const WorkpadTemplates = withSuspense(LazyWorkpadTemplates);
const MyWorkpads = withSuspense(LazyMyWorkpads);
export const Home = ({ activeTab = 'workpads' }: Props) => {
const [tab, setTab] = useState(activeTab);
return (
<KibanaPageTemplate
pageHeader={{
pageTitle: 'Canvas',
rightSideItems: [<WorkpadCreate />],
bottomBorder: true,
tabs: [
{
label: strings.getMyWorkpadsTabLabel(),
id: 'myWorkpads',
isSelected: tab === 'workpads',
onClick: () => setTab('workpads'),
},
{
label: strings.getWorkpadTemplatesTabLabel(),
id: 'workpadTemplates',
'data-test-subj': 'workpadTemplates',
isSelected: tab === 'templates',
onClick: () => setTab('templates'),
},
],
}}
>
{tab === 'workpads' ? <MyWorkpads /> : <WorkpadTemplates />}
</KibanaPageTemplate>
);
};
const strings = {
getMyWorkpadsTabLabel: () =>
i18n.translate('xpack.canvas.home.myWorkpadsTabLabel', {
defaultMessage: 'My workpads',
}),
getWorkpadTemplatesTabLabel: () =>
i18n.translate('xpack.canvas.home.workpadTemplatesTabLabel', {
defaultMessage: 'Templates',
description: 'The label for the tab that displays a list of designed workpad templates.',
}),
};

View file

@ -0,0 +1,30 @@
/*
* 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 from 'react';
import {
reduxDecorator,
getAddonPanelParameters,
servicesContextDecorator,
getDisableStoryshotsParameter,
} from '../../../storybook';
import { Home } from './home.component';
export default {
title: 'Home/Home Page',
argTypes: {},
decorators: [reduxDecorator()],
parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
};
export const NoContent = () => <Home />;
export const HasContent = () => <Home />;
NoContent.decorators = [servicesContextDecorator()];
HasContent.decorators = [servicesContextDecorator({ findWorkpads: 5, findTemplates: true })];

View file

@ -0,0 +1,33 @@
/*
* 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, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { getBaseBreadcrumb } from '../../lib/breadcrumbs';
import { resetWorkpad } from '../../state/actions/workpad';
import { Home as Component } from './home.component';
import { usePlatformService } from '../../services';
export const Home = () => {
const { setBreadcrumbs } = usePlatformService();
const [isMounted, setIsMounted] = useState(false);
const dispatch = useDispatch();
useEffect(() => {
if (!isMounted) {
dispatch(resetWorkpad());
setIsMounted(true);
}
}, [dispatch, isMounted, setIsMounted]);
useEffect(() => {
setBreadcrumbs([getBaseBreadcrumb()]);
}, [setBreadcrumbs]);
return <Component />;
};

View file

@ -0,0 +1,15 @@
/*
* 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 { 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, useFindTemplatesOnMount } from './use_find_templates';
export { useFindWorkpads, useFindWorkpadsOnMount } from './use_find_workpad';
export { useImportWorkpad } from './use_upload_workpad';
export { useCreateFromTemplate } from './use_create_from_template';

View file

@ -0,0 +1,60 @@
/*
* 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 { useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { useNotifyService, useWorkpadService } from '../../../services';
import { getId } from '../../../lib/get_id';
export const useCloneWorkpad = () => {
const workpadService = useWorkpadService();
const notifyService = useNotifyService();
const history = useHistory();
return useCallback(
async (workpadId: string) => {
try {
let workpad = await workpadService.get(workpadId);
workpad = {
...workpad,
name: strings.getClonedWorkpadName(workpad.name),
id: getId('workpad'),
};
await workpadService.create(workpad);
history.push(`/workpad/${workpad.id}/page/1`);
} catch (err) {
notifyService.error(err, { title: errors.getCloneFailureErrorMessage() });
}
},
[notifyService, workpadService, history]
);
};
const strings = {
getClonedWorkpadName: (workpadName: string) =>
i18n.translate('xpack.canvas.useCloneWorkpad.clonedWorkpadName', {
defaultMessage: 'Copy of {workpadName}',
values: {
workpadName,
},
description:
'This suffix is added to the end of the name of a cloned workpad to indicate that this ' +
'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"',
}),
};
const errors = {
getCloneFailureErrorMessage: () =>
i18n.translate('xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage', {
defaultMessage: `Couldn't clone workpad`,
}),
};

View file

@ -0,0 +1,32 @@
/*
* 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 { useHistory } from 'react-router-dom';
import { CanvasTemplate } from '../../../../types';
import { useNotifyService, useWorkpadService } from '../../../services';
export const useCreateFromTemplate = () => {
const workpadService = useWorkpadService();
const notifyService = useNotifyService();
const history = useHistory();
return useCallback(
async (template: CanvasTemplate) => {
try {
const result = await workpadService.createFromTemplate(template.id);
history.push(`/workpad/${result.id}/page/1`);
} catch (e) {
notifyService.error(e, {
title: `Couldn't create workpad from template`,
});
}
},
[workpadService, notifyService, history]
);
};

View file

@ -0,0 +1,46 @@
/*
* 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 { useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
// @ts-expect-error
import { getDefaultWorkpad } from '../../../state/defaults';
import { useNotifyService, useWorkpadService } from '../../../services';
import type { CanvasWorkpad } from '../../../../types';
export const useCreateWorkpad = () => {
const workpadService = useWorkpadService();
const notifyService = useNotifyService();
const history = useHistory();
return useCallback(
async (_workpad?: CanvasWorkpad | null) => {
const workpad = _workpad || (getDefaultWorkpad() as CanvasWorkpad);
try {
await workpadService.create(workpad);
history.push(`/workpad/${workpad.id}/page/1`);
} catch (err) {
notifyService.error(err, {
title: errors.getUploadFailureErrorMessage(),
});
}
return;
},
[notifyService, history, workpadService]
);
};
const errors = {
getUploadFailureErrorMessage: () =>
i18n.translate('xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage', {
defaultMessage: `Couldn't upload workpad`,
}),
};

View file

@ -0,0 +1,63 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { useNotifyService, useWorkpadService } from '../../../services';
export const useDeleteWorkpads = () => {
const workpadService = useWorkpadService();
const notifyService = useNotifyService();
return useCallback(
async (workpadIds: string[]) => {
const removedWorkpads = workpadIds.map(async (id) => {
try {
await workpadService.remove(id);
return { id, err: null };
} catch (err) {
return { id, err };
}
});
return Promise.all(removedWorkpads).then((results) => {
const [passes, errored] = results.reduce<[string[], string[]]>(
([passesArr, errorsArr], result) => {
if (result.err) {
errorsArr.push(result.id);
} else {
passesArr.push(result.id);
}
return [passesArr, errorsArr];
},
[[], []]
);
const removedIds = workpadIds.filter((id) => passes.includes(id));
if (errored.length > 0) {
notifyService.error(errors.getDeleteFailureErrorMessage());
}
return {
removedIds,
errored,
};
});
},
[workpadService, notifyService]
);
};
const errors = {
getDeleteFailureErrorMessage: () =>
i18n.translate('xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage', {
defaultMessage: `Couldn't delete all workpads`,
}),
};

View file

@ -0,0 +1,12 @@
/*
* 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 { downloadWorkpad as downloadWorkpadFn } from '../../../lib/download_workpad';
export const useDownloadWorkpad = () =>
useCallback((workpadId: string) => downloadWorkpadFn(workpadId), []);

View file

@ -0,0 +1,38 @@
/*
* 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 { useState, useCallback } from 'react';
import useMount from 'react-use/lib/useMount';
import { useWorkpadService } from '../../../services';
import { TemplateFindResponse } from '../../../services/workpad';
const emptyResponse = { templates: [] };
export const useFindTemplates = () => {
const workpadService = useWorkpadService();
return useCallback(async () => await workpadService.findTemplates(), [workpadService]);
};
export const useFindTemplatesOnMount = (): [boolean, TemplateFindResponse] => {
const [isMounted, setIsMounted] = useState(false);
const findTemplates = useFindTemplates();
const [templateResponse, setTemplateResponse] = useState<TemplateFindResponse>(emptyResponse);
const fetchTemplates = useCallback(async () => {
const foundTemplates = await findTemplates();
setTemplateResponse(foundTemplates || emptyResponse);
setIsMounted(true);
}, [findTemplates]);
useMount(() => {
fetchTemplates();
return () => setIsMounted(false);
});
return [isMounted, templateResponse];
};

View file

@ -0,0 +1,57 @@
/*
* 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 { useState, useCallback } from 'react';
import useMount from 'react-use/lib/useMount';
import { i18n } from '@kbn/i18n';
import { WorkpadFindResponse } from '../../../services/workpad';
import { useNotifyService, useWorkpadService } from '../../../services';
const emptyResponse = { total: 0, workpads: [] };
export const useFindWorkpads = () => {
const workpadService = useWorkpadService();
const notifyService = useNotifyService();
return useCallback(
async (text = '') => {
try {
return await workpadService.find(text);
} catch (err) {
notifyService.error(err, { title: errors.getFindFailureErrorMessage() });
}
},
[notifyService, workpadService]
);
};
export const useFindWorkpadsOnMount = (): [boolean, WorkpadFindResponse] => {
const [isMounted, setIsMounted] = useState(false);
const findWorkpads = useFindWorkpads();
const [workpadResponse, setWorkpadResponse] = useState<WorkpadFindResponse>(emptyResponse);
const fetchWorkpads = useCallback(async () => {
const foundWorkpads = await findWorkpads();
setWorkpadResponse(foundWorkpads || emptyResponse);
setIsMounted(true);
}, [findWorkpads]);
useMount(() => {
fetchWorkpads();
return () => setIsMounted(false);
});
return [isMounted, workpadResponse];
};
const errors = {
getFindFailureErrorMessage: () =>
i18n.translate('xpack.canvas.error.useFindWorkpads.findFailureErrorMessage', {
defaultMessage: `Couldn't find workpad`,
}),
};

View file

@ -0,0 +1,100 @@
/*
* 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 { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { CANVAS, JSON as JSONString } from '../../../../i18n/constants';
import { useNotifyService } from '../../../services';
import { getId } from '../../../lib/get_id';
import type { CanvasWorkpad } from '../../../../types';
export const useImportWorkpad = () => {
const notifyService = useNotifyService();
return useCallback(
(file?: File, onComplete: (workpad?: CanvasWorkpad) => void = () => {}) => {
if (!file) {
onComplete();
return;
}
if (get(file, 'type') !== 'application/json') {
notifyService.warning(errors.getAcceptJSONOnlyErrorMessage(), {
title: file.name
? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
: errors.getFileUploadFailureWithoutFileNameErrorMessage(),
});
onComplete();
}
// TODO: Clean up this file, this loading stuff can, and should be, abstracted
const reader = new FileReader();
// handle reading the uploaded file
reader.onload = () => {
try {
const workpad = JSON.parse(reader.result as string); // Type-casting because we catch below.
workpad.id = getId('workpad');
// sanity check for workpad object
if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) {
onComplete();
throw new Error(errors.getMissingPropertiesErrorMessage());
}
onComplete(workpad);
} catch (e) {
notifyService.error(e, {
title: file.name
? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
: errors.getFileUploadFailureWithoutFileNameErrorMessage(),
});
onComplete();
}
};
// read the uploaded file
reader.readAsText(file);
},
[notifyService]
);
};
const errors = {
getFileUploadFailureWithoutFileNameErrorMessage: () =>
i18n.translate(
'xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage',
{
defaultMessage: `Couldn't upload file`,
}
),
getFileUploadFailureWithFileNameErrorMessage: (fileName: string) =>
i18n.translate('xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage', {
defaultMessage: `Couldn't upload '{fileName}'`,
values: {
fileName,
},
}),
getMissingPropertiesErrorMessage: () =>
i18n.translate('xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage', {
defaultMessage:
'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.',
values: {
CANVAS,
JSON: JSONString,
},
}),
getAcceptJSONOnlyErrorMessage: () =>
i18n.translate('xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage', {
defaultMessage: 'Only {JSON} files are accepted',
values: {
JSON: JSONString,
},
}),
};

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { WorkpadManager } from './workpad_manager';
export { Home } from './home';

View file

@ -0,0 +1,19 @@
/*
* 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 from 'react';
import { HomeEmptyPrompt } from './empty_prompt';
import { getDisableStoryshotsParameter } from '../../../../storybook';
export default {
title: 'Home/Empty Prompt',
argTypes: {},
parameters: { ...getDisableStoryshotsParameter() },
};
export const EmptyPrompt = () => <HomeEmptyPrompt />;

View file

@ -0,0 +1,65 @@
/*
* 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, { Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { CANVAS, JSON } from '../../../../i18n/constants';
export const HomeEmptyPrompt = () => (
<EuiFlexGroup justifyContent="spaceAround" alignItems="center" style={{ minHeight: 600 }}>
<EuiFlexItem grow={false}>
<EuiPanel color="subdued" borderRadius="none" hasShadow={false}>
<EuiEmptyPrompt
color="subdued"
iconType="importAction"
title={<h2>{strings.getEmptyPromptTitle()}</h2>}
titleSize="m"
body={
<Fragment>
<p>{strings.getEmptyPromptGettingStartedDescription()}</p>
<p>
{strings.getEmptyPromptNewUserDescription()}{' '}
<EuiLink href="home#/tutorial_directory/sampleData">
{strings.getSampleDataLinkLabel()}
</EuiLink>
.
</p>
</Fragment>
}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
const strings = {
getEmptyPromptGettingStartedDescription: () =>
i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription', {
defaultMessage:
'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.',
values: {
JSON,
},
}),
getEmptyPromptNewUserDescription: () =>
i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription', {
defaultMessage: 'New to {CANVAS}?',
values: {
CANVAS,
},
}),
getEmptyPromptTitle: () =>
i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptTitle', {
defaultMessage: 'Add your first workpad',
}),
getSampleDataLinkLabel: () =>
i18n.translate('xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel', {
defaultMessage: 'Add your first workpad',
}),
};

View file

@ -0,0 +1,10 @@
/*
* 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 from 'react';
export const LazyMyWorkpads = React.lazy(() => import('./my_workpads'));

View file

@ -0,0 +1,17 @@
/*
* 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 from 'react';
import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
export const Loading = () => (
<EuiFlexGroup justifyContent="spaceAround" alignItems="center" style={{ minHeight: 600 }}>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,38 @@
/*
* 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 from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FoundWorkpad } from '../../../services/workpad';
import { UploadDropzone } from './upload_dropzone';
import { HomeEmptyPrompt } from './empty_prompt';
import { WorkpadTable } from './workpad_table';
export interface Props {
workpads: FoundWorkpad[];
}
export const MyWorkpads = ({ workpads }: Props) => {
if (workpads.length === 0) {
return (
<UploadDropzone>
<EuiFlexGroup justifyContent="spaceAround" alignItems="center" style={{ minHeight: 600 }}>
<EuiFlexItem grow={false}>
<HomeEmptyPrompt />
</EuiFlexItem>
</EuiFlexGroup>
</UploadDropzone>
);
}
return (
<UploadDropzone>
<WorkpadTable />
</UploadDropzone>
);
};

View file

@ -0,0 +1,56 @@
/*
* 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, { useState } from 'react';
import { EuiPanel } from '@elastic/eui';
import {
reduxDecorator,
getAddonPanelParameters,
servicesContextDecorator,
getDisableStoryshotsParameter,
} from '../../../../storybook';
import { getSomeWorkpads } from '../../../services/stubs/workpad';
import { MyWorkpads, WorkpadsContext } from './my_workpads';
import { MyWorkpads as MyWorkpadsComponent } from './my_workpads.component';
export default {
title: 'Home/My Workpads',
argTypes: {},
decorators: [reduxDecorator()],
parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
};
export const NoWorkpads = () => {
return <MyWorkpads />;
};
export const HasWorkpads = () => {
return (
<EuiPanel>
<MyWorkpads />
</EuiPanel>
);
};
NoWorkpads.decorators = [servicesContextDecorator()];
HasWorkpads.decorators = [servicesContextDecorator({ findWorkpads: 5 })];
export const Component = ({ workpadCount }: { workpadCount: number }) => {
const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount));
return (
<WorkpadsContext.Provider value={{ workpads, setWorkpads }}>
<EuiPanel>
<MyWorkpadsComponent {...{ workpads }} />
</EuiPanel>
</WorkpadsContext.Provider>
);
};
Component.args = { workpadCount: 5 };

View file

@ -0,0 +1,42 @@
/*
* 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, { useState, useEffect, createContext, Dispatch, SetStateAction } from 'react';
import { useFindWorkpadsOnMount } from './../hooks';
import { FoundWorkpad } from '../../../services/workpad';
import { Loading } from './loading';
import { MyWorkpads as Component } from './my_workpads.component';
interface Context {
workpads: FoundWorkpad[];
setWorkpads: Dispatch<SetStateAction<FoundWorkpad[]>>;
}
export const WorkpadsContext = createContext<Context | null>(null);
export const MyWorkpads = () => {
const [isMounted, workpadResponse] = useFindWorkpadsOnMount();
const [workpads, setWorkpads] = useState(workpadResponse.workpads);
useEffect(() => {
setWorkpads(workpadResponse.workpads);
}, [workpadResponse]);
if (!isMounted) {
return <Loading />;
}
return (
<WorkpadsContext.Provider value={{ workpads, setWorkpads }}>
<Component {...{ workpads }} />
</WorkpadsContext.Provider>
);
};
// required for dynamic import using React.lazy()
// eslint-disable-next-line import/no-default-export
export default MyWorkpads;

View file

@ -0,0 +1,30 @@
/*
* 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 } from 'react';
// @ts-expect-error untyped library
import Dropzone from 'react-dropzone';
import './upload_dropzone.scss';
export interface Props {
disabled?: boolean;
onDrop?: (files: FileList) => void;
}
export const UploadDropzone: FC<Props> = ({ onDrop = () => {}, disabled, children }) => {
return (
<Dropzone
{...{ onDrop, disabled }}
disableClick
className="canvasWorkpad__dropzone"
activeClassName="canvasWorkpad__dropzone--active"
>
{children}
</Dropzone>
);
};

View file

@ -0,0 +1,8 @@
.canvasWorkpad__dropzone {
border: 2px dashed transparent;
}
.canvasWorkpad__dropzone--active {
background-color: $euiColorLightestShade;
border-color: $euiColorLightShade;
}

View file

@ -0,0 +1,55 @@
/*
* 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, useState } from 'react';
// @ts-expect-error untyped library
import Dropzone from 'react-dropzone';
import { useNotifyService } from '../../../services';
import { ErrorStrings } from '../../../../i18n';
import { useImportWorkpad, useCreateWorkpad } from '../hooks';
import { CanvasWorkpad } from '../../../../types';
import { UploadDropzone as Component } from './upload_dropzone.component';
const { WorkpadDropzone: errors } = ErrorStrings;
export const UploadDropzone: FC = ({ children }) => {
const notify = useNotifyService();
const uploadWorkpad = useImportWorkpad();
const createWorkpad = useCreateWorkpad();
const [isDisabled, setIsDisabled] = useState(false);
const onComplete = async (workpad?: CanvasWorkpad) => {
if (!workpad) {
setIsDisabled(false);
return;
}
await createWorkpad(workpad);
};
const onDrop = (files: FileList) => {
if (!files) {
return;
}
if (files.length > 1) {
notify.warning(errors.getTooManyFilesErrorMessage());
return;
}
setIsDisabled(true);
uploadWorkpad(files[0], onComplete);
};
return (
<Component disabled={isDisabled} {...{ onDrop }}>
{children}
</Component>
);
};

View file

@ -0,0 +1,40 @@
/*
* 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 from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFilePicker, EuiFilePickerProps } from '@elastic/eui';
import { JSON } from '../../../../i18n/constants';
export interface Props {
canUserWrite: boolean;
onImportWorkpad?: EuiFilePickerProps['onChange'];
uniqueKey?: string | number;
}
export const WorkpadImport = ({ uniqueKey, canUserWrite, onImportWorkpad = () => {} }: Props) => (
<EuiFilePicker
display="default"
className="canvasWorkpad__upload--compressed"
aria-label={strings.getFilePickerPlaceholder()}
initialPromptText={strings.getFilePickerPlaceholder()}
onChange={onImportWorkpad}
key={uniqueKey}
accept="application/json"
disabled={!canUserWrite}
/>
);
const strings = {
getFilePickerPlaceholder: () =>
i18n.translate('xpack.canvas.workpadImport.filePickerPlaceholder', {
defaultMessage: 'Import workpad {JSON} file',
values: {
JSON,
},
}),
};

View file

@ -0,0 +1,35 @@
/*
* 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, { useState } from 'react';
import { useSelector } from 'react-redux';
import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
import type { State } from '../../../../types';
import { useImportWorkpad } from '../hooks';
import { WorkpadImport as Component, Props as ComponentProps } from './workpad_import.component';
type Props = Omit<ComponentProps, 'canUserWrite' | 'onImportWorkpad'>;
export const WorkpadImport = (props: Props) => {
const importWorkpad = useImportWorkpad();
const [uniqueKey, setUniqueKey] = useState(Date.now());
const { canUserWrite } = useSelector((state: State) => ({
canUserWrite: canUserWriteSelector(state),
}));
const onImportWorkpad: ComponentProps['onImportWorkpad'] = (files) => {
if (files) {
importWorkpad(files[0]);
}
setUniqueKey(Date.now());
};
return <Component {...{ ...props, uniqueKey, onImportWorkpad, canUserWrite }} />;
};

View file

@ -0,0 +1,203 @@
/*
* 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, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiInMemoryTable,
EuiInMemoryTableProps,
EuiTableActionsColumnType,
EuiBasicTableColumn,
EuiToolTip,
EuiButtonIcon,
EuiTableSelectionType,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import moment from 'moment';
import { RoutingLink } from '../../routing';
import { FoundWorkpad } from '../../../services/workpad';
import { WorkpadTableTools } from './workpad_table_tools';
import { WorkpadImport } from './workpad_import';
export interface Props {
workpads: FoundWorkpad[];
canUserWrite: boolean;
dateFormat: string;
onExportWorkpad: (ids: string) => void;
onCloneWorkpad: (id: string) => void;
}
const getDisplayName = (name: string, workpadId: string, loadedWorkpadId?: string) => {
const workpadName = name.length ? <span>{name}</span> : <em>{workpadId}</em>;
return workpadId === loadedWorkpadId ? <strong>{workpadName}</strong> : workpadName;
};
export const WorkpadTable = ({
workpads,
canUserWrite,
dateFormat,
onExportWorkpad: onExport,
onCloneWorkpad,
}: Props) => {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const formatDate = (date: string) => date && moment(date).format(dateFormat);
const selection: EuiTableSelectionType<FoundWorkpad> = {
onSelectionChange: (selectedWorkpads) => {
setSelectedIds(selectedWorkpads.map((workpad) => workpad.id).filter((id) => !!id));
},
};
const actions: EuiTableActionsColumnType<any>['actions'] = [
{
render: (workpad: FoundWorkpad) => (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiToolTip content={strings.getExportToolTip()}>
<EuiButtonIcon
iconType="exportAction"
onClick={() => onExport(workpad.id)}
aria-label={strings.getExportToolTip()}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={
canUserWrite ? strings.getCloneToolTip() : strings.getNoPermissionToCloneToolTip()
}
>
<EuiButtonIcon
iconType="copy"
onClick={() => onCloneWorkpad(workpad.id)}
aria-label={strings.getCloneToolTip()}
disabled={!canUserWrite}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
),
},
];
const search: EuiInMemoryTableProps<FoundWorkpad>['search'] = {
toolsLeft:
selectedIds.length > 0 ? <WorkpadTableTools selectedWorkpadIds={selectedIds} /> : undefined,
toolsRight: <WorkpadImport />,
box: {
schema: true,
incremental: true,
placeholder: strings.getWorkpadSearchPlaceholder(),
'data-test-subj': 'tableListSearchBox',
},
};
const columns: Array<EuiBasicTableColumn<FoundWorkpad>> = [
{
field: 'name',
name: strings.getTableNameColumnTitle(),
sortable: true,
dataType: 'string',
render: (name, workpad) => (
<RoutingLink
data-test-subj="canvasWorkpadTableWorkpad"
to={`/workpad/${workpad.id}`}
aria-label={strings.getLoadWorkpadArialLabel(name.length ? name : workpad.id)}
>
{getDisplayName(name, workpad.id)}
</RoutingLink>
),
},
{
field: '@created',
name: strings.getTableCreatedColumnTitle(),
sortable: true,
dataType: 'date',
width: '20%',
render: (date: string) => formatDate(date),
},
{
field: '@timestamp',
name: strings.getTableUpdatedColumnTitle(),
sortable: true,
dataType: 'date',
width: '20%',
render: (date: string) => formatDate(date),
},
{ name: strings.getTableActionsColumnTitle(), actions, width: '100px' },
];
return (
<EuiInMemoryTable
itemId="id"
items={workpads}
columns={columns}
message={strings.getNoWorkpadsFoundMessage()}
search={search}
sorting={{
sort: {
field: '@timestamp',
direction: 'desc',
},
}}
pagination={true}
selection={selection}
data-test-subj="canvasWorkpadTable"
/>
);
};
const strings = {
getCloneToolTip: () =>
i18n.translate('xpack.canvas.workpadTable.cloneTooltip', {
defaultMessage: 'Clone workpad',
}),
getExportToolTip: () =>
i18n.translate('xpack.canvas.workpadTable.exportTooltip', {
defaultMessage: 'Export workpad',
}),
getLoadWorkpadArialLabel: (workpadName: string) =>
i18n.translate('xpack.canvas.workpadTable.loadWorkpadArialLabel', {
defaultMessage: `Load workpad '{workpadName}'`,
values: {
workpadName,
},
}),
getNoPermissionToCloneToolTip: () =>
i18n.translate('xpack.canvas.workpadTable.noPermissionToCloneToolTip', {
defaultMessage: `You don't have permission to clone workpads`,
}),
getNoWorkpadsFoundMessage: () =>
i18n.translate('xpack.canvas.workpadTable.noWorkpadsFoundMessage', {
defaultMessage: 'No workpads matched your search.',
}),
getWorkpadSearchPlaceholder: () =>
i18n.translate('xpack.canvas.workpadTable.searchPlaceholder', {
defaultMessage: 'Find workpad',
}),
getTableCreatedColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTable.table.createdColumnTitle', {
defaultMessage: 'Created',
description: 'This column in the table contains the date/time the workpad was created.',
}),
getTableNameColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTable.table.nameColumnTitle', {
defaultMessage: 'Workpad name',
}),
getTableUpdatedColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTable.table.updatedColumnTitle', {
defaultMessage: 'Updated',
description: 'This column in the table contains the date/time the workpad was last updated.',
}),
getTableActionsColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTable.table.actionsColumnTitle', {
defaultMessage: 'Actions',
description: 'This column in the table contains the actions that can be taken on a workpad.',
}),
};

View file

@ -0,0 +1,83 @@
/*
* 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, { useState, useEffect } from 'react';
import { EuiPanel } from '@elastic/eui';
import { action } from '@storybook/addon-actions';
import {
reduxDecorator,
getAddonPanelParameters,
getDisableStoryshotsParameter,
} from '../../../../storybook';
import { getSomeWorkpads } from '../../../services/stubs/workpad';
import { WorkpadTable } from './workpad_table';
import { WorkpadTable as WorkpadTableComponent } from './workpad_table.component';
import { WorkpadsContext } from './my_workpads';
export default {
title: 'Home/Workpad Table',
argTypes: {},
decorators: [reduxDecorator()],
parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
};
export const NoWorkpads = () => {
const [workpads, setWorkpads] = useState(getSomeWorkpads(0));
return (
<WorkpadsContext.Provider value={{ workpads, setWorkpads }}>
<EuiPanel>
<WorkpadTable />
</EuiPanel>
</WorkpadsContext.Provider>
);
};
export const HasWorkpads = () => {
const [workpads, setWorkpads] = useState(getSomeWorkpads(5));
return (
<WorkpadsContext.Provider value={{ workpads, setWorkpads }}>
<EuiPanel>
<WorkpadTable />
</EuiPanel>
</WorkpadsContext.Provider>
);
};
export const Component = ({
workpadCount,
canUserWrite,
dateFormat,
}: {
workpadCount: number;
canUserWrite: boolean;
dateFormat: string;
}) => {
const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount));
useEffect(() => {
setWorkpads(getSomeWorkpads(workpadCount));
}, [workpadCount]);
return (
<WorkpadsContext.Provider value={{ workpads, setWorkpads }}>
<EuiPanel>
<WorkpadTableComponent
{...{ workpads, canUserWrite, dateFormat }}
onCloneWorkpad={action('onCloneWorkpad')}
onExportWorkpad={action('onExportWorkpad')}
/>
</EuiPanel>
</WorkpadsContext.Provider>
);
};
Component.args = { workpadCount: 5, canUserWrite: true, dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS' };
Component.argTypes = {};

View file

@ -0,0 +1,38 @@
/*
* 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, { useContext } from 'react';
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 { WorkpadTable as Component } from './workpad_table.component';
import { WorkpadsContext } from './my_workpads';
export const WorkpadTable = () => {
const platformService = usePlatformService();
const onCloneWorkpad = useCloneWorkpad();
const onExportWorkpad = useDownloadWorkpad();
const context = useContext(WorkpadsContext);
const { canUserWrite } = useSelector((state: State) => ({
canUserWrite: canUserWriteSelector(state),
}));
if (!context) {
return null;
}
const { workpads } = context;
const dateFormat = platformService.getUISetting('dateFormat');
return <Component {...{ workpads, dateFormat, canUserWrite, onCloneWorkpad, onExportWorkpad }} />;
};

View file

@ -0,0 +1,160 @@
/*
* 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, { useState, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiToolTip, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { ConfirmModal } from '../../confirm_modal';
import { FoundWorkpad } from '../../../services/workpad';
export interface Props {
workpads: FoundWorkpad[];
canUserWrite: boolean;
selectedWorkpadIds: string[];
onDeleteWorkpads: (ids: string[]) => void;
onExportWorkpads: (ids: string[]) => void;
}
export const WorkpadTableTools = ({
workpads,
canUserWrite,
selectedWorkpadIds,
onDeleteWorkpads,
onExportWorkpads,
}: Props) => {
const [isDeletePending, setIsDeletePending] = useState(false);
const openRemoveConfirm = () => setIsDeletePending(true);
const closeRemoveConfirm = () => setIsDeletePending(false);
let deleteButton = (
<EuiButton
color="danger"
iconType="trash"
onClick={openRemoveConfirm}
disabled={!canUserWrite}
aria-label={strings.getDeleteButtonAriaLabel(selectedWorkpadIds.length)}
data-test-subj="deleteWorkpadButton"
>
{strings.getDeleteButtonLabel(selectedWorkpadIds.length)}
</EuiButton>
);
const downloadButton = (
<EuiButton
color="secondary"
onClick={() => onExportWorkpads(selectedWorkpadIds)}
iconType="exportAction"
aria-label={strings.getExportButtonAriaLabel(selectedWorkpadIds.length)}
>
{strings.getExportButtonLabel(selectedWorkpadIds.length)}
</EuiButton>
);
if (!canUserWrite) {
deleteButton = (
<EuiToolTip content={strings.getNoPermissionToDeleteToolTip()}>{deleteButton}</EuiToolTip>
);
}
const modalTitle =
selectedWorkpadIds.length === 1
? strings.getDeleteSingleWorkpadModalTitle(
workpads.find((workpad) => workpad.id === selectedWorkpadIds[0])?.name || ''
)
: strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpadIds.length + '');
const confirmModal = (
<ConfirmModal
isOpen={isDeletePending}
title={modalTitle}
message={strings.getDeleteModalDescription()}
confirmButtonText={strings.getDeleteModalConfirmButtonLabel()}
onConfirm={() => {
onDeleteWorkpads(selectedWorkpadIds);
closeRemoveConfirm();
}}
onCancel={closeRemoveConfirm}
/>
);
return (
<Fragment>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>{downloadButton}</EuiFlexItem>
<EuiFlexItem grow={false}>{deleteButton}</EuiFlexItem>
</EuiFlexGroup>
{confirmModal}
</Fragment>
);
};
const strings = {
getDeleteButtonAriaLabel: (numberOfWorkpads: number) =>
i18n.translate('xpack.canvas.workpadTableTools.deleteButtonAriaLabel', {
defaultMessage: 'Delete {numberOfWorkpads} workpads',
values: {
numberOfWorkpads,
},
}),
getDeleteButtonLabel: (numberOfWorkpads: number) =>
i18n.translate('xpack.canvas.workpadTableTools.deleteButtonLabel', {
defaultMessage: 'Delete ({numberOfWorkpads})',
values: {
numberOfWorkpads,
},
}),
getDeleteModalConfirmButtonLabel: () =>
i18n.translate('xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel', {
defaultMessage: 'Delete',
}),
getDeleteModalDescription: () =>
i18n.translate('xpack.canvas.workpadTableTools.deleteModalDescription', {
defaultMessage: `You can't recover deleted workpads.`,
}),
getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) =>
i18n.translate('xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle', {
defaultMessage: 'Delete {numberOfWorkpads} workpads?',
values: {
numberOfWorkpads,
},
}),
getDeleteSingleWorkpadModalTitle: (workpadName: string) =>
i18n.translate('xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle', {
defaultMessage: `Delete workpad '{workpadName}'?`,
values: {
workpadName,
},
}),
getExportButtonAriaLabel: (numberOfWorkpads: number) =>
i18n.translate('xpack.canvas.workpadTableTools.exportButtonAriaLabel', {
defaultMessage: 'Export {numberOfWorkpads} workpads',
values: {
numberOfWorkpads,
},
}),
getExportButtonLabel: (numberOfWorkpads: number) =>
i18n.translate('xpack.canvas.workpadTableTools.exportButtonLabel', {
defaultMessage: 'Export ({numberOfWorkpads})',
values: {
numberOfWorkpads,
},
}),
getNoPermissionToCreateToolTip: () =>
i18n.translate('xpack.canvas.workpadTableTools.noPermissionToCreateToolTip', {
defaultMessage: `You don't have permission to create workpads`,
}),
getNoPermissionToDeleteToolTip: () =>
i18n.translate('xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip', {
defaultMessage: `You don't have permission to delete workpads`,
}),
getNoPermissionToUploadToolTip: () =>
i18n.translate('xpack.canvas.workpadTableTools.noPermissionToUploadToolTip', {
defaultMessage: `You don't have permission to upload workpads`,
}),
};

View file

@ -0,0 +1,51 @@
/*
* 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, { useContext } from 'react';
import { useSelector } from 'react-redux';
import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app';
import type { State } from '../../../../types';
import { useDeleteWorkpads, useDownloadWorkpad } from '../hooks';
import {
WorkpadTableTools as Component,
Props as ComponentProps,
} from './workpad_table_tools.component';
import { WorkpadsContext } from './my_workpads';
export type Props = Pick<ComponentProps, 'selectedWorkpadIds'>;
export const WorkpadTableTools = ({ selectedWorkpadIds }: Props) => {
const deleteWorkpads = useDeleteWorkpads();
const downloadWorkpad = useDownloadWorkpad();
const context = useContext(WorkpadsContext);
const { canUserWrite } = useSelector((state: State) => ({
canUserWrite: canUserWriteSelector(state),
}));
if (context === null || selectedWorkpadIds.length <= 0) {
return null;
}
const { workpads, setWorkpads } = context;
const onExport = () => selectedWorkpadIds.map((id) => downloadWorkpad(id));
const onDelete = async () => {
const { removedIds } = await deleteWorkpads(selectedWorkpadIds);
setWorkpads(workpads.filter((workpad) => !removedIds.includes(workpad.id)));
};
return (
<Component
{...{ workpads, selectedWorkpadIds, canUserWrite }}
onDeleteWorkpads={onDelete}
onExportWorkpads={onExport}
/>
);
};

View file

@ -0,0 +1,37 @@
/*
* 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 from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton } from '@elastic/eui';
import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button';
export interface Props
extends Omit<EuiButtonPropsForButton, 'iconType' | 'fill' | 'data-test-subj' | 'children'> {
canUserWrite: boolean;
}
export const WorkpadCreate = ({ canUserWrite, disabled, ...rest }: Props) => {
return (
<EuiButton
{...{ ...rest }}
iconType="plusInCircleFilled"
fill
disabled={!canUserWrite && !disabled}
data-test-subj="create-workpad-button"
>
{strings.getWorkpadCreateButtonLabel()}
</EuiButton>
);
};
const strings = {
getWorkpadCreateButtonLabel: () =>
i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', {
defaultMessage: 'Create workpad',
}),
};

View file

@ -0,0 +1,31 @@
/*
* 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 from 'react';
import { useSelector } from 'react-redux';
import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app';
import type { State } from '../../../types';
import { useCreateWorkpad } from './hooks';
import { WorkpadCreate as Component, Props as ComponentProps } from './workpad_create.component';
type Props = Omit<ComponentProps, 'canUserWrite' | 'onClick'>;
export const WorkpadCreate = (props: Props) => {
const createWorkpad = useCreateWorkpad();
const { canUserWrite } = useSelector((state: State) => ({
canUserWrite: canUserWriteSelector(state),
}));
const onClick: ComponentProps['onClick'] = async () => {
await createWorkpad();
};
return <Component {...{ ...props, onClick, canUserWrite }} />;
};

View file

@ -0,0 +1,10 @@
/*
* 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 from 'react';
export const LazyWorkpadTemplates = React.lazy(() => import('./workpad_templates'));

View file

@ -0,0 +1,157 @@
/*
* 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 from 'react';
import { uniq } from 'lodash';
import { i18n } from '@kbn/i18n';
import {
EuiInMemoryTable,
EuiBasicTableColumn,
EuiButtonEmpty,
EuiSearchBarProps,
SearchFilterConfig,
} from '@elastic/eui';
import { CanvasTemplate } from '../../../../types';
import { tagsRegistry } from '../../../lib/tags_registry';
import { TagList } from '../../tag_list';
export interface Props {
templates: CanvasTemplate[];
onCreateWorkpad: (template: CanvasTemplate) => void;
}
export const WorkpadTemplates = ({ templates, onCreateWorkpad }: Props) => {
const columns: Array<EuiBasicTableColumn<CanvasTemplate>> = [
{
field: 'name',
name: strings.getTableNameColumnTitle(),
sortable: true,
width: '30%',
dataType: 'string',
render: (name: string, template) => {
const templateName = name.length ? name : 'Unnamed Template';
return (
<EuiButtonEmpty
onClick={() => onCreateWorkpad(template)}
aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)}
type="button"
>
{templateName}
</EuiButtonEmpty>
);
},
},
{
field: 'help',
name: strings.getTableDescriptionColumnTitle(),
sortable: false,
dataType: 'string',
width: '30%',
},
{
field: 'tags',
name: strings.getTableTagsColumnTitle(),
sortable: false,
dataType: 'string',
width: '30%',
render: (tags: string[]) => <TagList tags={tags} tagType="health" />,
},
];
let uniqueTagNames: string[] = [];
templates.forEach((template) => {
const { tags } = template;
tags.forEach((tag) => uniqueTagNames.push(tag));
uniqueTagNames = uniq(uniqueTagNames);
});
const uniqueTags = uniqueTagNames.map(
(name) =>
tagsRegistry.get(name) || {
color: undefined,
name,
}
);
const filters: SearchFilterConfig[] = [
{
type: 'field_value_selection',
field: 'tags',
name: 'Tags',
multiSelect: true,
options: uniqueTags.map((tag) => ({
value: tag.name,
name: tag.name,
view: <TagList tags={[tag.name]} tagType="health" />,
})),
},
];
const search: EuiSearchBarProps = {
box: {
incremental: true,
schema: true,
},
filters,
};
return (
<EuiInMemoryTable
itemId="id"
items={templates}
columns={columns}
search={search}
sorting={{
sort: {
field: 'name',
direction: 'asc',
},
}}
pagination={true}
data-test-subj="canvasTemplatesTable"
/>
);
};
const strings = {
getCloneTemplateLinkAriaLabel: (templateName: string) =>
i18n.translate('xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel', {
defaultMessage: `Clone workpad template '{templateName}'`,
values: {
templateName,
},
}),
getTableDescriptionColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', {
defaultMessage: 'Description',
}),
getTableNameColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', {
defaultMessage: 'Template name',
}),
getTableTagsColumnTitle: () =>
i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', {
defaultMessage: 'Tags',
description:
'This column contains relevant tags that indicate what type of template ' +
'is displayed. For example: "report", "presentation", etc.',
}),
getTemplateSearchPlaceholder: () =>
i18n.translate('xpack.canvas.workpadTemplates.searchPlaceholder', {
defaultMessage: 'Find template',
}),
getCreatingTemplateLabel: (templateName: string) =>
i18n.translate('xpack.canvas.workpadTemplates.creatingTemplateLabel', {
defaultMessage: `Creating from template '{templateName}'`,
values: {
templateName,
},
}),
};

View file

@ -0,0 +1,62 @@
/*
* 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 { EuiPanel } from '@elastic/eui';
import { action } from '@storybook/addon-actions';
import React from 'react';
import {
reduxDecorator,
getAddonPanelParameters,
servicesContextDecorator,
getDisableStoryshotsParameter,
} from '../../../../storybook';
import { getSomeTemplates } from '../../../services/stubs/workpad';
import { WorkpadTemplates } from './workpad_templates';
import { WorkpadTemplates as WorkpadTemplatesComponent } from './workpad_templates.component';
export default {
title: 'Home/Workpad Templates',
argTypes: {},
decorators: [reduxDecorator()],
parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() },
};
export const NoTemplates = () => {
return (
<EuiPanel>
<WorkpadTemplates />
</EuiPanel>
);
};
export const HasTemplates = () => {
return (
<EuiPanel>
<WorkpadTemplates />
</EuiPanel>
);
};
NoTemplates.decorators = [servicesContextDecorator()];
HasTemplates.decorators = [servicesContextDecorator({ findTemplates: true })];
export const Component = ({ hasTemplates }: { hasTemplates: boolean }) => {
return (
<EuiPanel>
<WorkpadTemplatesComponent
onCreateWorkpad={action('onCreateWorkpad')}
templates={hasTemplates ? getSomeTemplates().templates : []}
/>
</EuiPanel>
);
};
Component.args = {
hasTemplates: true,
};

View file

@ -0,0 +1,35 @@
/*
* 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 from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { useCreateFromTemplate, useFindTemplatesOnMount } from '../hooks';
import { WorkpadTemplates as Component } from './workpad_templates.component';
export const WorkpadTemplates = () => {
const [isMounted, templateResponse] = useFindTemplatesOnMount();
const onCreateWorkpad = useCreateFromTemplate();
if (!isMounted) {
return (
<EuiFlexGroup justifyContent="spaceAround" alignItems="center" style={{ minHeight: 600 }}>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
);
}
const { templates } = templateResponse;
return <Component {...{ templates, onCreateWorkpad }} />;
};
// required for dynamic import using React.lazy()
// eslint-disable-next-line import/no-default-export
export default WorkpadTemplates;

View file

@ -6,9 +6,7 @@
*/
import React, { FC } from 'react';
import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui';
// @ts-expect-error untyped local
import { WorkpadManager } from '../workpad_manager';
import { Home } from '../home';
// @ts-expect-error untyped local
import { setDocTitle } from '../../lib/doc_title';
@ -19,17 +17,5 @@ export interface Props {
export const HomeApp: FC<Props> = ({ onLoad = () => {} }) => {
onLoad();
setDocTitle('Canvas');
return (
<EuiPage className="canvasHomeApp" restrictWidth>
<EuiPageBody>
<EuiPageContent
className="canvasHomeApp__content"
panelPaddingSize="none"
horizontalPosition="center"
>
<WorkpadManager onClose={() => {}} />
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
return <Home />;
};

View file

@ -18,7 +18,6 @@ storiesOf('components/Toolbar', module)
isWriteable={true}
selectedPageNumber={1}
totalPages={1}
workpadId={'abc'}
workpadName={'My Canvas Workpad'}
/>
))
@ -28,7 +27,6 @@ storiesOf('components/Toolbar', module)
selectedElement={getDefaultElement()}
selectedPageNumber={1}
totalPages={1}
workpadId={'abc'}
workpadName={'My Canvas Workpad'}
/>
));

View file

@ -7,17 +7,8 @@
import React, { FC, useState, useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiModal,
EuiModalFooter,
EuiButton,
} from '@elastic/eui';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
// @ts-expect-error untyped local
import { WorkpadManager } from '../workpad_manager';
import { PageManager } from '../page_manager';
import { Expression } from '../expression';
import { Tray } from './tray';
@ -37,7 +28,6 @@ export interface Props {
selectedElement?: CanvasElement;
selectedPageNumber: number;
totalPages: number;
workpadId: string;
workpadName: string;
}
@ -46,11 +36,9 @@ export const Toolbar: FC<Props> = ({
selectedElement,
selectedPageNumber,
totalPages,
workpadId,
workpadName,
}) => {
const [activeTray, setActiveTray] = useState<TrayType | null>(null);
const [showWorkpadManager, setShowWorkpadManager] = useState(false);
const { getUrl, previousPage } = useContext(WorkpadRoutingContext);
// While the tray doesn't get activated if the workpad isn't writeable,
@ -75,20 +63,6 @@ export const Toolbar: FC<Props> = ({
}
};
const closeWorkpadManager = () => setShowWorkpadManager(false);
const openWorkpadManager = () => setShowWorkpadManager(true);
const workpadManager = (
<EuiModal onClose={closeWorkpadManager} className="canvasModal--fixedSize" maxWidth="1000px">
<WorkpadManager onClose={closeWorkpadManager} />
<EuiModalFooter>
<EuiButton size="s" onClick={closeWorkpadManager}>
{strings.getWorkpadManagerCloseButtonLabel()}
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
const trays = {
pageManager: <PageManager onPreviousPage={previousPage} />,
expression: !elementIsSelected ? null : <Expression done={() => setActiveTray(null)} />,
@ -99,12 +73,6 @@ export const Toolbar: FC<Props> = ({
{activeTray !== null && <Tray done={() => setActiveTray(null)}>{trays[activeTray]}</Tray>}
<div className="canvasToolbar__container">
<EuiFlexGroup alignItems="center" gutterSize="none" className="canvasToolbar__controls">
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="text" iconType="grid" onClick={() => openWorkpadManager()}>
{workpadName}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false} />
<EuiFlexItem grow={false}>
<RoutingButtonIcon
color="text"
@ -143,7 +111,6 @@ export const Toolbar: FC<Props> = ({
)}
</EuiFlexGroup>
</div>
{showWorkpadManager && workpadManager}
</div>
);
};
@ -153,6 +120,5 @@ Toolbar.propTypes = {
selectedElement: PropTypes.object,
selectedPageNumber: PropTypes.number.isRequired,
totalPages: PropTypes.number.isRequired,
workpadId: PropTypes.string.isRequired,
workpadName: PropTypes.string.isRequired,
};

View file

@ -1,173 +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 React, { FC, useState, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
import moment from 'moment';
// @ts-expect-error
import { getDefaultWorkpad } from '../../state/defaults';
import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app';
import { getWorkpad } from '../../state/selectors/workpad';
import { getId } from '../../lib/get_id';
import { downloadWorkpad } from '../../lib/download_workpad';
import { ComponentStrings, ErrorStrings } from '../../../i18n';
import { State, CanvasWorkpad } from '../../../types';
import { useNotifyService, useWorkpadService, usePlatformService } from '../../services';
// @ts-expect-error
import { WorkpadLoader as Component } from './workpad_loader';
const { WorkpadLoader: strings } = ComponentStrings;
const { WorkpadLoader: errors } = ErrorStrings;
type WorkpadStatePromise = ReturnType<ReturnType<typeof useWorkpadService>['find']>;
type WorkpadState = WorkpadStatePromise extends PromiseLike<infer U> ? U : never;
export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => {
const fromState = useSelector((state: State) => ({
workpadId: getWorkpad(state).id,
canUserWrite: canUserWriteSelector(state),
}));
const [workpadsState, setWorkpadsState] = useState<WorkpadState | null>(null);
const workpadService = useWorkpadService();
const notifyService = useNotifyService();
const platformService = usePlatformService();
const history = useHistory();
const createWorkpad = useCallback(
async (_workpad: CanvasWorkpad | null | undefined) => {
const workpad = _workpad || getDefaultWorkpad();
if (workpad != null) {
try {
await workpadService.create(workpad);
history.push(`/workpad/${workpad.id}/page/1`);
} catch (err) {
notifyService.error(err, {
title: errors.getUploadFailureErrorMessage(),
});
}
return;
}
},
[workpadService, notifyService, history]
);
const findWorkpads = useCallback(
async (text) => {
try {
const fetchedWorkpads = await workpadService.find(text);
setWorkpadsState(fetchedWorkpads);
} catch (err) {
notifyService.error(err, { title: errors.getFindFailureErrorMessage() });
}
},
[notifyService, workpadService]
);
const onDownloadWorkpad = useCallback((workpadId: string) => downloadWorkpad(workpadId), []);
const cloneWorkpad = useCallback(
async (workpadId: string) => {
try {
const workpad = await workpadService.get(workpadId);
workpad.name = strings.getClonedWorkpadName(workpad.name);
workpad.id = getId('workpad');
await workpadService.create(workpad);
history.push(`/workpad/${workpad.id}/page/1`);
} catch (err) {
notifyService.error(err, { title: errors.getCloneFailureErrorMessage() });
}
},
[notifyService, workpadService, history]
);
const removeWorkpads = useCallback(
(workpadIds: string[]) => {
if (workpadsState === null) {
return;
}
const removedWorkpads = workpadIds.map(async (id) => {
try {
await workpadService.remove(id);
return { id, err: null };
} catch (err) {
return { id, err };
}
});
return Promise.all(removedWorkpads).then((results) => {
let redirectHome = false;
const [passes, errored] = results.reduce<[string[], string[]]>(
([passesArr, errorsArr], result) => {
if (result.id === fromState.workpadId && !result.err) {
redirectHome = true;
}
if (result.err) {
errorsArr.push(result.id);
} else {
passesArr.push(result.id);
}
return [passesArr, errorsArr];
},
[[], []]
);
const remainingWorkpads = workpadsState.workpads.filter(({ id }) => !passes.includes(id));
const workpadState = {
total: remainingWorkpads.length,
workpads: remainingWorkpads,
};
if (errored.length > 0) {
notifyService.error(errors.getDeleteFailureErrorMessage());
}
setWorkpadsState(workpadState);
if (redirectHome) {
history.push('/');
}
return errored;
});
},
[history, workpadService, fromState.workpadId, workpadsState, notifyService]
);
const formatDate = useCallback(
(date: any) => {
const dateFormat = platformService.getUISetting('dateFormat');
return date && moment(date).format(dateFormat);
},
[platformService]
);
const { workpadId, canUserWrite } = fromState;
return (
<Component
{...{
downloadWorkpad: onDownloadWorkpad,
workpads: workpadsState,
workpadId,
canUserWrite,
cloneWorkpad,
createWorkpad,
findWorkpads,
removeWorkpads,
formatDate,
onClose,
}}
/>
);
};

View file

@ -1,52 +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 { get } from 'lodash';
import { getId } from '../../lib/get_id';
import { ErrorStrings } from '../../../i18n';
const { WorkpadFileUpload: errors } = ErrorStrings;
export const uploadWorkpad = (file, onUpload, notify) => {
if (!file) {
return;
}
if (get(file, 'type') !== 'application/json') {
return notify.warning(errors.getAcceptJSONOnlyErrorMessage(), {
title: file.name
? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
: errors.getFileUploadFailureWithoutFileNameErrorMessage(),
});
}
// TODO: Clean up this file, this loading stuff can, and should be, abstracted
const reader = new FileReader();
// handle reading the uploaded file
reader.onload = () => {
try {
const workpad = JSON.parse(reader.result);
workpad.id = getId('workpad');
// sanity check for workpad object
if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) {
throw new Error(errors.getMissingPropertiesErrorMessage());
}
onUpload(workpad);
} catch (e) {
notify.error(e, {
title: file.name
? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
: errors.getFileUploadFailureWithoutFileNameErrorMessage(),
});
}
};
// read the uploaded file
reader.readAsText(file);
};

View file

@ -1,31 +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 React from 'react';
import PropTypes from 'prop-types';
import { EuiButton } from '@elastic/eui';
import { ComponentStrings } from '../../../i18n';
const { WorkpadCreate: strings } = ComponentStrings;
export const WorkpadCreate = ({ createPending, onCreate, ...rest }) => (
<EuiButton
{...rest}
iconType="plusInCircle"
fill
onClick={onCreate}
isLoading={createPending}
data-test-subj="create-workpad-button"
>
{strings.getWorkpadCreateButtonLabel()}
</EuiButton>
);
WorkpadCreate.propTypes = {
onCreate: PropTypes.func.isRequired,
createPending: PropTypes.bool,
};

View file

@ -1,31 +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 PropTypes from 'prop-types';
import { compose, withHandlers } from 'recompose';
import { uploadWorkpad } from '../upload_workpad';
import { ErrorStrings } from '../../../../i18n';
import { WorkpadDropzone as Component } from './workpad_dropzone';
const { WorkpadFileUpload: errors } = ErrorStrings;
export const WorkpadDropzone = compose(
withHandlers(({ notify }) => ({
onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload),
onDropRejected: () => ([file]) => {
notify.warning(errors.getAcceptJSONOnlyErrorMessage(), {
title: file.name
? errors.getFileUploadFailureWithFileNameErrorMessage(file.name)
: errors.getFileUploadFailureWithoutFileNameErrorMessage(),
});
},
}))
)(Component);
WorkpadDropzone.propTypes = {
onUpload: PropTypes.func.isRequired,
};

View file

@ -1,31 +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 React from 'react';
import PropTypes from 'prop-types';
import Dropzone from 'react-dropzone';
export const WorkpadDropzone = ({ onDropAccepted, onDropRejected, disabled, children }) => (
<Dropzone
accept="application/json"
onDropAccepted={onDropAccepted}
onDropRejected={onDropRejected}
disableClick
disabled={disabled}
className="canvasWorkpad__dropzone"
activeClassName="canvasWorkpad__dropzone--active"
>
{children}
</Dropzone>
);
WorkpadDropzone.propTypes = {
onDropAccepted: PropTypes.func.isRequired,
onDropRejected: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
};

View file

@ -1,22 +0,0 @@
.canvasWorkpad__dropzone {
border: 2px dashed transparent;
}
.canvasWorkpad__dropzone--active {
background-color: $euiColorLightestShade;
border-color: $euiColorLightShade;
}
.canvasWorkpad__dropzoneTable .euiTable {
background-color: transparent;
}
.canvasWorkpad__dropzoneTable--tags {
.euiTableCellContent {
flex-wrap: wrap;
}
.euiHealth {
width: 100%;
}
}

View file

@ -1,426 +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 React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
EuiFlexGroup,
EuiFlexItem,
EuiBasicTable,
EuiButtonIcon,
EuiPagination,
EuiSpacer,
EuiButton,
EuiToolTip,
EuiEmptyPrompt,
EuiFilePicker,
EuiLink,
} from '@elastic/eui';
import { orderBy } from 'lodash';
import { ConfirmModal } from '../confirm_modal';
import { RoutingLink } from '../routing';
import { Paginate } from '../paginate';
import { ComponentStrings } from '../../../i18n';
import { WorkpadDropzone } from './workpad_dropzone';
import { WorkpadCreate } from './workpad_create';
import { WorkpadSearch } from './workpad_search';
import { uploadWorkpad } from './upload_workpad';
const { WorkpadLoader: strings } = ComponentStrings;
const getDisplayName = (name, workpad, loadedWorkpad) => {
const workpadName = name.length ? name : <em>{workpad.id}</em>;
return workpad.id === loadedWorkpad ? <strong>{workpadName}</strong> : workpadName;
};
export class WorkpadLoader extends React.PureComponent {
static propTypes = {
workpadId: PropTypes.string.isRequired,
canUserWrite: PropTypes.bool.isRequired,
createWorkpad: PropTypes.func.isRequired,
findWorkpads: PropTypes.func.isRequired,
downloadWorkpad: PropTypes.func.isRequired,
cloneWorkpad: PropTypes.func.isRequired,
removeWorkpads: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
workpads: PropTypes.object,
formatDate: PropTypes.func.isRequired,
};
state = {
createPending: false,
deletingWorkpad: false,
sortField: '@timestamp',
sortDirection: 'desc',
selectedWorkpads: [],
pageSize: 10,
};
async componentDidMount() {
// on component load, kick off the workpad search
this.props.findWorkpads();
// keep track of whether or not the component is mounted, to prevent rogue setState calls
this._isMounted = true;
}
UNSAFE_componentWillReceiveProps(newProps) {
// the workpadId prop will change when a is created or loaded, close the toolbar when it does
const { workpadId, onClose } = this.props;
if (workpadId !== newProps.workpadId) {
onClose();
}
}
componentWillUnmount() {
this._isMounted = false;
}
// create new empty workpad
createWorkpad = async () => {
this.setState({ createPending: true });
await this.props.createWorkpad();
this._isMounted && this.setState({ createPending: false });
};
// create new workpad from uploaded JSON
onUpload = async (workpad) => {
this.setState({ createPending: true });
await this.props.createWorkpad(workpad);
this._isMounted && this.setState({ createPending: false });
};
// clone existing workpad
cloneWorkpad = async (workpad) => {
this.setState({ createPending: true });
await this.props.cloneWorkpad(workpad.id);
this._isMounted && this.setState({ createPending: false });
};
// Workpad remove methods
openRemoveConfirm = () => this.setState({ deletingWorkpad: true });
closeRemoveConfirm = () => this.setState({ deletingWorkpad: false });
removeWorkpads = () => {
const { selectedWorkpads } = this.state;
this.props.removeWorkpads(selectedWorkpads.map(({ id }) => id)).then((remainingIds) => {
const remainingWorkpads =
remainingIds.length > 0
? selectedWorkpads.filter(({ id }) => remainingIds.includes(id))
: [];
this._isMounted &&
this.setState({
deletingWorkpad: false,
selectedWorkpads: remainingWorkpads,
});
});
};
// downloads selected workpads as JSON files
downloadWorkpads = () => {
this.state.selectedWorkpads.forEach(({ id }) => this.props.downloadWorkpad(id));
};
onSelectionChange = (selectedWorkpads) => {
this.setState({ selectedWorkpads });
};
onTableChange = ({ sort = {} }) => {
const { field: sortField, direction: sortDirection } = sort;
this.setState({
sortField,
sortDirection,
});
};
renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }) => {
const { sortField, sortDirection } = this.state;
const { canUserWrite, createPending, workpadId: loadedWorkpad } = this.props;
const actions = [
{
render: (workpad) => (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiToolTip content={strings.getExportToolTip()}>
<EuiButtonIcon
iconType="exportAction"
onClick={() => this.props.downloadWorkpad(workpad.id)}
aria-label={strings.getExportToolTip()}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={
canUserWrite ? strings.getCloneToolTip() : strings.getNoPermissionToCloneToolTip()
}
>
<EuiButtonIcon
iconType="copy"
onClick={() => this.cloneWorkpad(workpad)}
aria-label={strings.getCloneToolTip()}
disabled={!canUserWrite}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
),
},
];
const columns = [
{
field: 'name',
name: strings.getTableNameColumnTitle(),
sortable: true,
dataType: 'string',
render: (name, workpad) => {
const workpadName = getDisplayName(name, workpad, loadedWorkpad);
return (
<RoutingLink
data-test-subj="canvasWorkpadLoaderWorkpad"
to={`/workpad/${workpad.id}`}
aria-label={strings.getLoadWorkpadArialLabel()}
>
{workpadName}
</RoutingLink>
);
},
},
{
field: '@created',
name: strings.getTableCreatedColumnTitle(),
sortable: true,
dataType: 'date',
width: '20%',
render: (date) => this.props.formatDate(date),
},
{
field: '@timestamp',
name: strings.getTableUpdatedColumnTitle(),
sortable: true,
dataType: 'date',
width: '20%',
render: (date) => this.props.formatDate(date),
},
{ name: strings.getTableActionsColumnTitle(), actions, width: '100px' },
];
const sorting = {
sort: {
field: sortField,
direction: sortDirection,
},
};
const selection = {
itemId: 'id',
onSelectionChange: this.onSelectionChange,
};
const emptyTable = (
<EuiEmptyPrompt
iconType="importAction"
title={<h2>{strings.getEmptyPromptTitle()}</h2>}
titleSize="s"
body={
<Fragment>
<p>{strings.getEmptyPromptGettingStartedDescription()}</p>
<p>
{strings.getEmptyPromptNewUserDescription()}{' '}
<EuiLink href="home#/tutorial_directory/sampleData">
{strings.getSampleDataLinkLabel()}
</EuiLink>
.
</p>
</Fragment>
}
/>
);
return (
<Fragment>
<WorkpadDropzone
onUpload={this.onUpload}
disabled={createPending || !canUserWrite}
notify={this.props.notify}
>
<EuiBasicTable
items={rows}
itemId="id"
columns={columns}
sorting={sorting}
noItemsMessage={emptyTable}
onChange={this.onTableChange}
isSelectable
selection={selection}
className="canvasWorkpad__dropzoneTable"
data-test-subj="canvasWorkpadLoaderTable"
/>
<EuiSpacer />
{rows.length > 0 && (
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiPagination
activePage={pageNumber}
onPageClick={setPage}
pageCount={totalPages}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</WorkpadDropzone>
</Fragment>
);
};
render() {
const {
deletingWorkpad,
createPending,
selectedWorkpads,
sortField,
sortDirection,
} = this.state;
const { canUserWrite } = this.props;
const isLoading = this.props.workpads == null;
let createButton = (
<WorkpadCreate
createPending={createPending}
onCreate={this.createWorkpad}
disabled={!canUserWrite}
/>
);
let deleteButton = (
<EuiButton
color="danger"
iconType="trash"
onClick={this.openRemoveConfirm}
disabled={!canUserWrite}
aria-label={strings.getDeleteButtonAriaLabel(selectedWorkpads.length)}
data-test-subj="deleteWorkpadButton"
>
{strings.getDeleteButtonLabel(selectedWorkpads.length)}
</EuiButton>
);
const downloadButton = (
<EuiButton
color="secondary"
onClick={this.downloadWorkpads}
iconType="exportAction"
aria-label={strings.getExportButtonAriaLabel(selectedWorkpads.length)}
>
{strings.getExportButtonLabel(selectedWorkpads.length)}
</EuiButton>
);
let uploadButton = (
<EuiFilePicker
display="default"
compressed
className="canvasWorkpad__upload--compressed"
aria-label={strings.getFilePickerPlaceholder()}
initialPromptText={strings.getFilePickerPlaceholder()}
onChange={([file]) => uploadWorkpad(file, this.onUpload, this.props.notify)}
accept="application/json"
disabled={createPending || !canUserWrite}
/>
);
if (!canUserWrite) {
createButton = (
<EuiToolTip content={strings.getNoPermissionToCreateToolTip()}>{createButton}</EuiToolTip>
);
deleteButton = (
<EuiToolTip content={strings.getNoPermissionToDeleteToolTip()}>{deleteButton}</EuiToolTip>
);
uploadButton = (
<EuiToolTip content={strings.getNoPermissionToUploadToolTip()}>{uploadButton}</EuiToolTip>
);
}
const modalTitle =
selectedWorkpads.length === 1
? strings.getDeleteSingleWorkpadModalTitle(selectedWorkpads[0].name)
: strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpads.length);
const confirmModal = (
<ConfirmModal
isOpen={deletingWorkpad}
title={modalTitle}
message={strings.getDeleteModalDescription()}
confirmButtonText={strings.getDeleteModalConfirmButtonLabel()}
onConfirm={this.removeWorkpads}
onCancel={this.closeRemoveConfirm}
/>
);
let sortedWorkpads = [];
if (!createPending && !isLoading) {
const { workpads } = this.props.workpads;
sortedWorkpads = orderBy(workpads, [sortField, '@timestamp'], [sortDirection, 'desc']);
}
return (
<Paginate rows={sortedWorkpads}>
{(pagination) => (
<Fragment>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={2}>
<EuiFlexGroup gutterSize="s">
{selectedWorkpads.length > 0 && (
<Fragment>
<EuiFlexItem grow={false}>{downloadButton}</EuiFlexItem>
<EuiFlexItem grow={false}>{deleteButton}</EuiFlexItem>
</Fragment>
)}
<EuiFlexItem grow={1}>
<WorkpadSearch
onChange={(text) => {
pagination.setPage(0);
this.props.findWorkpads(text);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd" wrap>
<EuiFlexItem grow={false}>{uploadButton}</EuiFlexItem>
<EuiFlexItem grow={false}>{createButton}</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{createPending && (
<div style={{ width: '100%' }}>{strings.getCreateWorkpadLoadingDescription()}</div>
)}
{!createPending && isLoading && (
<div style={{ width: '100%' }}>{strings.getFetchLoadingDescription()}</div>
)}
{!createPending && !isLoading && this.renderWorkpadTable(pagination)}
{confirmModal}
</Fragment>
)}
</Paginate>
);
}
}

View file

@ -1,25 +0,0 @@
.canvasWorkpad__upload--compressed {
&.euiFilePicker--compressed.euiFilePicker {
.euiFilePicker__prompt {
height: $euiSizeXXL;
padding: $euiSizeM;
padding-left: $euiSizeXXL;
}
.euiFilePicker__icon {
top: $euiSizeM;
}
}
// The file picker input is being used moreso as a button, outside of a form,
// and thus the need to override the default max-width of form inputs.
// An issue has been opened in EUI to consider creating a button
// version of the file picker - https://github.com/elastic/eui/issues/1987
.euiFilePicker__wrap {
@include euiBreakpoint('xs', 's') {
max-width: none;
}
}
}

View file

@ -1,44 +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 React from 'react';
import PropTypes from 'prop-types';
import { EuiFieldSearch } from '@elastic/eui';
import { debounce } from 'lodash';
import { ComponentStrings } from '../../../i18n';
const { WorkpadSearch: strings } = ComponentStrings;
export class WorkpadSearch extends React.PureComponent {
static propTypes = {
onChange: PropTypes.func.isRequired,
initialText: PropTypes.string,
};
state = {
searchText: this.props.initialText || '',
};
triggerChange = debounce(this.props.onChange, 150);
setSearchText = (ev) => {
const text = ev.target.value;
this.setState({ searchText: text });
this.triggerChange(text);
};
render() {
return (
<EuiFieldSearch
placeholder={strings.getWorkpadSearchPlaceholder()}
value={this.state.searchText}
onChange={this.setSearchText}
fullWidth
incremental
/>
);
}
}

View file

@ -1,69 +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 React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
EuiTabbedContent,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { WorkpadLoader } from '../workpad_loader';
import { WorkpadTemplates } from '../workpad_templates';
import { ComponentStrings } from '../../../i18n';
const { WorkpadManager: strings } = ComponentStrings;
export const WorkpadManager = ({ onClose }) => {
const tabs = [
{
id: 'workpadLoader',
name: strings.getMyWorkpadsTabLabel(),
content: (
<Fragment>
<EuiSpacer />
<WorkpadLoader onClose={onClose} />
</Fragment>
),
},
{
id: 'workpadTemplates',
name: strings.getWorkpadTemplatesTabLabel(),
'data-test-subj': 'workpadTemplates',
content: (
<Fragment>
<EuiSpacer />
<WorkpadTemplates onClose={onClose} />
</Fragment>
),
},
];
return (
<Fragment>
<EuiModalHeader className="canvasHomeApp__modalHeader">
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiModalHeaderTitle>
<h1>{strings.getModalTitle()}</h1>
</EuiModalHeaderTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeader>
<EuiModalBody className="canvasHomeApp__modalBody">
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} />
</EuiModalBody>
</Fragment>
);
};
WorkpadManager.propTypes = {
onClose: PropTypes.func,
};

View file

@ -1,564 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/WorkpadTemplates default 1`] = `
<div
style={
Object {
"width": "500px",
}
}
>
<div
className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap"
>
<div
className="euiFlexItem euiSearchBar__searchHolder"
>
<div
className="euiFormControlLayout euiFormControlLayout--fullWidth"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
aria-label="This is a search bar. As you type, the results lower in the page will automatically filter."
className="euiFieldSearch euiFieldSearch--fullWidth"
defaultValue=""
onKeyUp={[Function]}
placeholder="Find template"
type="search"
/>
<div
className="euiFormControlLayoutIcons"
>
<span
className="euiFormControlLayoutCustomIcon"
>
<span
aria-hidden="true"
className="euiFormControlLayoutCustomIcon__icon"
data-euiicon-type="search"
size="m"
/>
</span>
</div>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero euiSearchBar__filtersHolder"
>
<div
className="euiFilterGroup"
>
<div
className="euiPopover euiPopover--anchorDownCenter"
id="field_value_selection_0"
>
<div
className="euiPopover__anchor"
>
<button
className="euiButtonEmpty euiButtonEmpty--text euiFilterButton euiFilterButton--hasIcon"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content"
>
<span
className="euiButtonContent__icon"
color="inherit"
data-euiicon-type="arrowDown"
size="m"
/>
<span
className="euiButtonEmpty__text"
>
<span
className="euiFilterButton__textShift"
data-text="Tags"
title="Tags"
>
Tags
</span>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="euiSpacer euiSpacer--l"
/>
<div
className="euiBasicTable canvasWorkpad__dropzoneTable canvasWorkpad__dropzoneTable--tags"
data-test-subj="canvasTemplatesTable"
>
<div>
<div
className="euiTableHeaderMobile"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
/>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiTableSortMobile"
>
<div
className="euiPopover euiPopover--anchorDownRight"
>
<div
className="euiPopover__anchor"
>
<button
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushRight"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content"
>
<span
className="euiButtonContent__icon"
color="inherit"
data-euiicon-type="arrowDown"
size="s"
/>
<span
className="euiButtonEmpty__text"
>
Sorting
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<table
className="euiTable euiTable--compressed euiTable--responsive"
id="generated-id"
tabIndex={-1}
>
<caption
className="euiScreenReaderOnly euiTableCaption"
/>
<thead>
<tr>
<th
aria-live="polite"
aria-sort="ascending"
className="euiTableHeaderCell"
data-test-subj="tableHeaderCell_name_0"
role="columnheader"
scope="col"
style={
Object {
"width": "30%",
}
}
>
<button
className="euiTableHeaderButton euiTableHeaderButton-isSorted"
data-test-subj="tableHeaderSortButton"
onClick={[Function]}
type="button"
>
<span
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
>
Template name
</span>
<span
className="euiTableSortIcon"
data-euiicon-type="sortUp"
size="m"
/>
</span>
</button>
</th>
<th
className="euiTableHeaderCell"
data-test-subj="tableHeaderCell_help_1"
role="columnheader"
scope="col"
style={
Object {
"width": "30%",
}
}
>
<span
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
>
Description
</span>
</span>
</th>
<th
className="euiTableHeaderCell"
data-test-subj="tableHeaderCell_tags_2"
role="columnheader"
scope="col"
style={
Object {
"width": "30%",
}
}
>
<span
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
>
Tags
</span>
</span>
</th>
</tr>
</thead>
<tbody>
<tr
className="euiTableRow"
>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Template name
</div>
<div
className="euiTableCellContent euiTableCellContent--overflowingContent"
>
<button
aria-label="Clone workpad template 'test1'"
className="euiButtonEmpty euiButtonEmpty--primary"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="euiButtonContent euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
test1
</span>
</span>
</button>
</div>
</td>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Description
</div>
<div
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
>
This is a test template
</span>
</div>
</td>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Tags
</div>
<div
className="euiTableCellContent euiTableCellContent--overflowingContent"
>
<div
className="euiHealth euiHealth--textSizeS"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
color="#666666"
data-euiicon-type="dot"
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag1
</div>
</div>
</div>
<div
className="euiHealth euiHealth--textSizeS"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
color="#666666"
data-euiicon-type="dot"
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag2
</div>
</div>
</div>
</div>
</td>
</tr>
<tr
className="euiTableRow"
>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Template name
</div>
<div
className="euiTableCellContent euiTableCellContent--overflowingContent"
>
<button
aria-label="Clone workpad template 'test2'"
className="euiButtonEmpty euiButtonEmpty--primary"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="euiButtonContent euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
test2
</span>
</span>
</button>
</div>
</td>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Description
</div>
<div
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
>
This is a second test template
</span>
</div>
</td>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Tags
</div>
<div
className="euiTableCellContent euiTableCellContent--overflowingContent"
>
<div
className="euiHealth euiHealth--textSizeS"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
color="#666666"
data-euiicon-type="dot"
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag2
</div>
</div>
</div>
<div
className="euiHealth euiHealth--textSizeS"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
color="#666666"
data-euiicon-type="dot"
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag3
</div>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
className="euiSpacer euiSpacer--l"
/>
<div
className="euiFlexGroup euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<nav
className="euiPagination"
>
<button
aria-label="Previous page"
className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="pagination-button-previous"
disabled={true}
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
className="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="arrowLeft"
size="m"
/>
</button>
<ul
className="euiPagination__list"
>
<li
className="euiPagination__item"
>
<button
aria-current={true}
aria-label="Page 1 of 1"
className="euiButtonEmpty euiButtonEmpty--text euiButtonEmpty--small euiButtonEmpty-isDisabled euiPaginationButton euiPaginationButton-isActive euiPaginationButton--hideOnMobile"
data-test-subj="pagination-button-0"
disabled={true}
onClick={[Function]}
type="button"
>
<span
className="euiButtonContent euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
1
</span>
</span>
</button>
</li>
</ul>
<button
aria-label="Next page"
className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall"
data-test-subj="pagination-button-next"
disabled={true}
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
className="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="arrowRight"
size="m"
/>
</button>
</nav>
</div>
</div>
</div>
`;

View file

@ -1,45 +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 React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { WorkpadTemplates } from '../workpad_templates';
import { CanvasTemplate } from '../../../../types';
const templates: Record<string, CanvasTemplate> = {
test1: {
id: 'test1-id',
name: 'test1',
help: 'This is a test template',
tags: ['tag1', 'tag2'],
template_key: 'test1-key',
},
test2: {
id: 'test2-id',
name: 'test2',
help: 'This is a second test template',
tags: ['tag2', 'tag3'],
template_key: 'test2-key',
},
};
storiesOf('components/WorkpadTemplates', module)
.addDecorator((story) => <div style={{ width: '500px' }}>{story()}</div>)
.add('default', () => {
const onCreateFromTemplateAction = action('onCreateFromTemplate');
return (
<WorkpadTemplates
templates={templates}
onClose={action('onClose')}
onCreateFromTemplate={(template) => {
onCreateFromTemplateAction(template);
return Promise.resolve();
}}
/>
);
});

View file

@ -1,86 +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 React, { useCallback, useState, useEffect, FunctionComponent } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { ComponentStrings } from '../../../i18n/components';
// @ts-expect-error
import * as workpadService from '../../lib/workpad_service';
import { WorkpadTemplates as Component } from './workpad_templates';
import { CanvasTemplate } from '../../../types';
import { list } from '../../lib/template_service';
import { applyTemplateStrings } from '../../../i18n/templates/apply_strings';
import { useNotifyService, useServices } from '../../services';
interface WorkpadTemplatesProps {
onClose: () => void;
}
const Creating: FunctionComponent<{ name: string }> = ({ name }) => (
<div>
<EuiLoadingSpinner size="l" />{' '}
{ComponentStrings.WorkpadTemplates.getCreatingTemplateLabel(name)}
</div>
);
export const WorkpadTemplates: FunctionComponent<WorkpadTemplatesProps> = ({ onClose }) => {
const history = useHistory();
const services = useServices();
const [templates, setTemplates] = useState<CanvasTemplate[] | undefined>(undefined);
const [creatingFromTemplateName, setCreatingFromTemplateName] = useState<string | undefined>(
undefined
);
const { error } = useNotifyService();
useEffect(() => {
if (!templates) {
(async () => {
const fetchedTemplates = await list();
setTemplates(applyTemplateStrings(fetchedTemplates));
})();
}
}, [templates]);
let templateProp: Record<string, CanvasTemplate> = {};
if (templates) {
templateProp = templates.reduce<Record<string, any>>((reduction, template) => {
reduction[template.name] = template;
return reduction;
}, {});
}
const createFromTemplate = useCallback(
async (template: CanvasTemplate) => {
setCreatingFromTemplateName(template.name);
try {
const result = await services.workpad.createFromTemplate(template.id);
history.push(`/workpad/${result.id}/page/1`);
} catch (e) {
setCreatingFromTemplateName(undefined);
error(e, {
title: `Couldn't create workpad from template`,
});
}
},
[services.workpad, error, history]
);
if (creatingFromTemplateName) {
return <Creating name={creatingFromTemplateName} />;
}
return (
<Component
onClose={onClose}
templates={templateProp}
onCreateFromTemplate={createFromTemplate}
/>
);
};

View file

@ -1,215 +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 React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
EuiFlexGroup,
EuiFlexItem,
EuiBasicTable,
EuiPagination,
EuiSpacer,
EuiButtonEmpty,
EuiSearchBar,
EuiTableSortingType,
Direction,
SortDirection,
} from '@elastic/eui';
import { orderBy } from 'lodash';
// @ts-ignore untyped local
import { EuiBasicTableColumn } from '@elastic/eui';
import { Paginate, PaginateChildProps } from '../paginate';
import { TagList } from '../tag_list';
import { getTagsFilter } from '../../lib/get_tags_filter';
// @ts-expect-error
import { extractSearch } from '../../lib/extract_search';
import { ComponentStrings } from '../../../i18n';
import { CanvasTemplate } from '../../../types';
interface TableChange<T> {
page?: {
index: number;
size: number;
};
sort?: {
field: keyof T;
direction: Direction;
};
}
const { WorkpadTemplates: strings } = ComponentStrings;
interface WorkpadTemplatesProps {
onCreateFromTemplate: (template: CanvasTemplate) => Promise<void>;
onClose: () => void;
templates: Record<string, CanvasTemplate>;
}
interface WorkpadTemplatesState {
sortField: string;
sortDirection: Direction;
pageSize: number;
searchTerm: string;
filterTags: string[];
}
export class WorkpadTemplates extends React.PureComponent<
WorkpadTemplatesProps,
WorkpadTemplatesState
> {
static propTypes = {
onCreateFromTemplate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
templates: PropTypes.object,
};
state = {
sortField: 'name',
sortDirection: SortDirection.ASC,
pageSize: 10,
searchTerm: '',
filterTags: [],
};
tagType: 'health' = 'health';
onTableChange = (tableChange: TableChange<CanvasTemplate>) => {
if (tableChange.sort) {
const { field: sortField, direction: sortDirection } = tableChange.sort;
this.setState({
sortField,
sortDirection,
});
}
};
onSearch = ({ queryText = '' }) => this.setState(extractSearch(queryText));
cloneTemplate = (template: CanvasTemplate) =>
this.props.onCreateFromTemplate(template).then(() => this.props.onClose());
renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }: PaginateChildProps) => {
const { sortField, sortDirection } = this.state;
const columns: Array<EuiBasicTableColumn<CanvasTemplate>> = [
{
field: 'name',
name: strings.getTableNameColumnTitle(),
sortable: true,
width: '30%',
dataType: 'string',
render: (name: string, template) => {
const templateName = name.length ? name : 'Unnamed Template';
return (
<EuiButtonEmpty
onClick={() => this.cloneTemplate(template)}
aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)}
type="button"
>
{templateName}
</EuiButtonEmpty>
);
},
},
{
field: 'help',
name: strings.getTableDescriptionColumnTitle(),
sortable: false,
dataType: 'string',
width: '30%',
},
{
field: 'tags',
name: strings.getTableTagsColumnTitle(),
sortable: false,
dataType: 'string',
width: '30%',
render: (tags: string[]) => <TagList tags={tags} tagType={this.tagType} />,
},
];
const sorting: EuiTableSortingType<any> = {
sort: {
field: sortField,
direction: sortDirection,
},
};
return (
<Fragment>
<EuiBasicTable
compressed
items={rows}
itemId="id"
columns={columns}
sorting={sorting}
onChange={this.onTableChange}
className="canvasWorkpad__dropzoneTable canvasWorkpad__dropzoneTable--tags"
data-test-subj="canvasTemplatesTable"
/>
<EuiSpacer />
{rows.length > 0 && (
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiPagination activePage={pageNumber} onPageClick={setPage} pageCount={totalPages} />
</EuiFlexItem>
</EuiFlexGroup>
)}
</Fragment>
);
};
renderSearch = () => {
const { searchTerm } = this.state;
const filters = [getTagsFilter(this.tagType)];
return (
<EuiSearchBar
defaultQuery={searchTerm}
box={{
placeholder: strings.getTemplateSearchPlaceholder(),
incremental: true,
}}
filters={filters}
onChange={this.onSearch}
/>
);
};
render() {
const { templates } = this.props;
const { sortField, sortDirection, searchTerm, filterTags } = this.state;
const sortedTemplates = orderBy(templates, [sortField, 'name'], [sortDirection, 'asc']);
const filteredTemplates = sortedTemplates.filter(({ name = '', help = '', tags = [] }) => {
const tagMatch = filterTags.length
? filterTags.every((filterTag) => tags.indexOf(filterTag) > -1)
: true;
const lowercaseSearch = searchTerm.toLowerCase();
const textMatch = lowercaseSearch
? name.toLowerCase().indexOf(lowercaseSearch) > -1 ||
help.toLowerCase().indexOf(lowercaseSearch) > -1
: true;
return tagMatch && textMatch;
});
return (
<Paginate rows={filteredTemplates}>
{(pagination: PaginateChildProps) => (
<Fragment>
{this.renderSearch()}
<EuiSpacer />
{this.renderWorkpadTable(pagination)}
</Fragment>
)}
</Paginate>
);
}
}

View file

@ -1,39 +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 React from 'react';
import { sortBy } from 'lodash';
import { SearchFilterConfig } from '@elastic/eui';
import { Tag } from '../components/tag';
import { getId } from './get_id';
import { tagsRegistry } from './tags_registry';
import { ComponentStrings } from '../../i18n';
const { WorkpadTemplates: strings } = ComponentStrings;
// EUI helper function
// generates the FieldValueSelectionFilter object for EuiSearchBar for tag filtering
export const getTagsFilter = (type: 'health' | 'badge'): SearchFilterConfig => {
const uniqueTags = sortBy(Object.values(tagsRegistry.toJS()), 'name');
const filterType = 'field_value_selection';
return {
type: filterType,
field: 'tag',
name: strings.getTableTagsColumnTitle(),
multiSelect: true,
options: uniqueTags.map(({ name, color }) => ({
value: name,
name,
view: (
<div>
<Tag key={getId('tag')} color={color} name={name} type={type} />
</div>
),
})),
};
};

View file

@ -34,7 +34,7 @@ export type CanvasServiceFactory<Service> = (
appUpdater: BehaviorSubject<AppUpdater>
) => Service | Promise<Service>;
class CanvasServiceProvider<Service> {
export class CanvasServiceProvider<Service> {
private factory: CanvasServiceFactory<Service>;
private service: Service | undefined;

View file

@ -9,13 +9,19 @@ import { PlatformService } from '../platform';
const noop = (..._args: any[]): any => {};
const uiSettings: Record<string, any> = {
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
};
const getUISetting = (setting: string) => uiSettings[setting];
export const platformService: PlatformService = {
getBasePath: () => '/base/path',
getBasePathInterface: noop,
getDocLinkVersion: () => 'dockLinkVersion',
getElasticWebsiteUrl: () => 'https://elastic.co',
getHasWriteAccess: () => true,
getUISetting: noop,
getUISetting,
setBreadcrumbs: noop,
setRecentlyAccessed: noop,
getSavedObjects: noop,

View file

@ -5,17 +5,95 @@
* 2.0.
*/
import { WorkpadService } from '../workpad';
import { CanvasWorkpad } from '../../../types';
import moment from 'moment';
export const workpadService: WorkpadService = {
get: (id: string) => Promise.resolve({} as CanvasWorkpad),
create: (workpad) => Promise.resolve({} as CanvasWorkpad),
createFromTemplate: (templateId: string) => Promise.resolve({} as CanvasWorkpad),
find: (term: string) =>
Promise.resolve({
// @ts-expect-error
import { getDefaultWorkpad } from '../../state/defaults';
import { WorkpadService } from '../workpad';
import { getId } from '../../lib/get_id';
import { CanvasTemplate } from '../../../types';
const TIMEOUT = 500;
const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time));
const getName = () => {
const lorem = 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split(
' '
);
return [1, 2, 3].map(() => lorem[Math.floor(Math.random() * lorem.length)]).join(' ');
};
const randomDate = (
start: Date = moment().toDate(),
end: Date = moment().subtract(7, 'days').toDate()
) => new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString();
const templates: CanvasTemplate[] = [
{
id: 'test1-id',
name: 'test1',
help: 'This is a test template',
tags: ['tag1', 'tag2'],
template_key: 'test1-key',
},
{
id: 'test2-id',
name: 'test2',
help: 'This is a second test template',
tags: ['tag2', 'tag3'],
template_key: 'test2-key',
},
];
export const getSomeWorkpads = (count = 3) =>
Array.from({ length: count }, () => ({
'@created': randomDate(
moment().subtract(3, 'days').toDate(),
moment().subtract(10, 'days').toDate()
),
'@timestamp': randomDate(),
id: getId('workpad'),
name: getName(),
}));
export const findSomeWorkpads = (count = 3, timeout = TIMEOUT) => (_term: string) => {
return Promise.resolve()
.then(promiseTimeout(timeout))
.then(() => ({
total: count,
workpads: getSomeWorkpads(count),
}));
};
export const findNoWorkpads = (timeout = TIMEOUT) => (_term: string) => {
return Promise.resolve()
.then(promiseTimeout(timeout))
.then(() => ({
total: 0,
workpads: [],
}),
remove: (id: string) => Promise.resolve(undefined),
}));
};
export const findSomeTemplates = (timeout = TIMEOUT) => () => {
return Promise.resolve()
.then(promiseTimeout(timeout))
.then(() => getSomeTemplates());
};
export const findNoTemplates = (timeout = TIMEOUT) => () => {
return Promise.resolve()
.then(promiseTimeout(timeout))
.then(() => getNoTemplates());
};
export const getNoTemplates = () => ({ templates: [] });
export const getSomeTemplates = () => ({ templates });
export const workpadService: WorkpadService = {
get: (id: string) => Promise.resolve({ ...getDefaultWorkpad(), id }),
findTemplates: findNoTemplates(),
create: (workpad) => Promise.resolve(workpad),
createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()),
find: findNoWorkpads(),
remove: (id: string) => Promise.resolve(),
};

View file

@ -5,8 +5,12 @@
* 2.0.
*/
import { API_ROUTE_WORKPAD, DEFAULT_WORKPAD_CSS } from '../../common/lib/constants';
import { CanvasWorkpad } from '../../types';
import {
API_ROUTE_WORKPAD,
DEFAULT_WORKPAD_CSS,
API_ROUTE_TEMPLATES,
} from '../../common/lib/constants';
import { CanvasWorkpad, CanvasTemplate } from '../../types';
import { CanvasServiceFactory } from './';
/*
@ -40,9 +44,15 @@ const sanitizeWorkpad = function (workpad: CanvasWorkpad) {
return workpad;
};
interface WorkpadFindResponse {
export type FoundWorkpads = Array<Pick<CanvasWorkpad, 'name' | 'id' | '@timestamp' | '@created'>>;
export type FoundWorkpad = FoundWorkpads[number];
export interface WorkpadFindResponse {
total: number;
workpads: Array<Pick<CanvasWorkpad, 'name' | 'id' | '@timestamp' | '@created'>>;
workpads: FoundWorkpads;
}
export interface TemplateFindResponse {
templates: CanvasTemplate[];
}
export interface WorkpadService {
@ -51,6 +61,7 @@ export interface WorkpadService {
createFromTemplate: (templateId: string) => Promise<CanvasWorkpad>;
find: (term: string) => Promise<WorkpadFindResponse>;
remove: (id: string) => Promise<void>;
findTemplates: () => Promise<TemplateFindResponse>;
}
export const workpadServiceFactory: CanvasServiceFactory<WorkpadService> = (
@ -82,7 +93,9 @@ export const workpadServiceFactory: CanvasServiceFactory<WorkpadService> = (
body: JSON.stringify({ templateId }),
});
},
findTemplates: async () => coreStart.http.get(API_ROUTE_TEMPLATES),
find: (searchTerm: string) => {
// TODO: this shouldn't be necessary. Check for usage.
const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0;
return coreStart.http.get(`${getApiPath()}/find`, {

View file

@ -40,8 +40,6 @@
@import '../components/workpad_header/element_menu/element_menu';
@import '../components/workpad_header/share_menu/share_menu';
@import '../components/workpad_header/view_menu/view_menu';
@import '../components/workpad_loader/workpad_loader';
@import '../components/workpad_loader/workpad_dropzone/workpad_dropzone';
@import '../components/workpad_page/workpad_page';
@import '../components/workpad_page/workpad_interactive_page/workpad_interactive_page';
@import '../components/workpad_page/workpad_static_page/workpad_static_page';

View file

@ -11,6 +11,7 @@ import { kibanaContextDecorator } from './kibana_decorator';
import { servicesContextDecorator } from './services_decorator';
export { reduxDecorator } from './redux_decorator';
export { servicesContextDecorator } from './services_decorator';
export const addDecorators = () => {
if (process.env.NODE_ENV === 'test') {
@ -20,5 +21,5 @@ export const addDecorators = () => {
addDecorator(kibanaContextDecorator);
addDecorator(routerContextDecorator);
addDecorator(servicesContextDecorator);
addDecorator(servicesContextDecorator());
};

View file

@ -25,7 +25,7 @@ elementsRegistry.register(image);
import { getInitialState, getReducer, getMiddleware, patchDispatch } from '../addon/src/state';
export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants';
interface Params {
export interface Params {
workpad?: CanvasWorkpad;
elements?: CanvasElement[];
assets?: CanvasAsset[];

View file

@ -7,8 +7,40 @@
import React from 'react';
import { ServicesProvider } from '../../public/services';
import {
CanvasServiceFactory,
CanvasServiceProvider,
ServicesProvider,
} from '../../public/services';
import {
findNoWorkpads,
findSomeWorkpads,
workpadService,
findSomeTemplates,
findNoTemplates,
} from '../../public/services/stubs/workpad';
import { WorkpadService } from '../../public/services/workpad';
export const servicesContextDecorator = (story: Function) => (
<ServicesProvider>{story()}</ServicesProvider>
);
interface Params {
findWorkpads?: number;
findTemplates?: boolean;
}
export const servicesContextDecorator = ({
findWorkpads = 0,
findTemplates: findTemplatesOption = false,
}: Params = {}) => {
const workpadServiceFactory: CanvasServiceFactory<WorkpadService> = (): WorkpadService => ({
...workpadService,
find: findWorkpads > 0 ? findSomeWorkpads(findWorkpads) : findNoWorkpads(),
findTemplates: findTemplatesOption ? findSomeTemplates() : findNoTemplates(),
});
const workpad = new CanvasServiceProvider(workpadServiceFactory);
// @ts-expect-error This is a hack at the moment, until we can get Canvas moved over to the new services architecture.
workpad.start();
return (story: Function) => (
<ServicesProvider providers={{ workpad }}>{story()}</ServicesProvider>
);
};

View file

@ -10,3 +10,8 @@ import { ACTIONS_PANEL_ID } from './addon/src/constants';
export * from './decorators';
export { ACTIONS_PANEL_ID } from './addon/src/constants';
export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } });
export const getDisableStoryshotsParameter = () => ({
storyshots: {
disable: true,
},
});

View file

@ -53,6 +53,11 @@ const canvasWebpack = {
},
],
},
resolve: {
alias: {
'src/plugins': resolve(KIBANA_ROOT, 'src/plugins'),
},
},
};
module.exports = {

View file

@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Home/Empty Prompt Empty Prompt 1`] = `
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceAround euiFlexGroup--directionRow euiFlexGroup--responsive"
style={
Object {
"minHeight": 600,
}
}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusNone euiPanel--subdued euiPanel--noShadow euiPanel--noBorder"
>
<div
className="euiEmptyPrompt"
color="subdued"
>
<span
color="subdued"
data-euiicon-type="importAction"
size="xxl"
/>
<div
className="euiSpacer euiSpacer--s"
/>
<span
className="euiTextColor euiTextColor--subdued"
>
<h2
className="euiTitle euiTitle--medium"
>
Add your first workpad
</h2>
<div
className="euiSpacer euiSpacer--m"
/>
<div
className="euiText euiText--medium"
>
<p>
Create a new workpad, start from a template, or import a workpad JSON file by dropping it here.
</p>
<p>
New to Canvas?
<a
className="euiLink euiLink--primary"
href="home#/tutorial_directory/sampleData"
rel="noreferrer"
>
Add your first workpad
</a>
.
</p>
</div>
</span>
</div>
</div>
</div>
</div>
`;

View file

@ -90,6 +90,11 @@ import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'
jest.mock('@elastic/eui/test-env/components/observer/observer');
EuiObserver.mockImplementation(() => 'EuiObserver');
// @ts-expect-error untyped library
import Dropzone from 'react-dropzone';
jest.mock('react-dropzone');
Dropzone.mockImplementation(() => 'Dropzone');
// This element uses a `ref` and cannot be rendered by Jest snapshots.
import { RenderedElement } from '../shareable_runtime/components/rendered_element';
jest.mock('../shareable_runtime/components/rendered_element');
@ -111,7 +116,7 @@ addSerializer(styleSheetSerializer);
// Initialize Storyshots and build the Jest Snapshots
initStoryshots({
configPath: path.resolve(__dirname, './../storybook'),
configPath: path.resolve(__dirname),
framework: 'react',
test: multiSnapshotWithOptions({}),
// Don't snapshot tests that start with 'redux'

View file

@ -6107,18 +6107,18 @@
"xpack.canvas.error.esService.indicesFetchErrorMessage": "Elasticsearch インデックスを取得できませんでした",
"xpack.canvas.error.RenderWithFn.renderErrorMessage": "「{functionName}」のレンダリングが失敗しました",
"xpack.canvas.error.repeatImage.missingMaxArgument": "{emptyImageArgument} を指定する場合は、{maxArgument} を設定する必要があります",
"xpack.canvas.error.workpadLoader.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした",
"xpack.canvas.error.workpadLoader.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした",
"xpack.canvas.error.workpadLoader.findFailureErrorMessage": "ワークパッドが見つかりませんでした",
"xpack.canvas.error.workpadLoader.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした",
"xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした",
"xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした",
"xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした",
"xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "ワークパッドが見つかりませんでした",
"xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした",
"xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage": "ファイルをアップロードできませんでした",
"xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。",
"xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "ワークパッドを作成できませんでした",
"xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "ID でワークパッドを読み込めませんでした",
"xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした",
"xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage": "ファイルをアップロードできませんでした",
"xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。",
"xpack.canvas.errorComponent.description": "表現が失敗し次のメッセージが返されました:",
"xpack.canvas.errorComponent.title": "おっと!表現が失敗しました",
"xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした",
"xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした",
"xpack.canvas.expression.cancelButtonLabel": "キャンセル",
"xpack.canvas.expression.closeButtonLabel": "閉じる",
"xpack.canvas.expression.learnLinkText": "表現構文の詳細",
@ -6452,6 +6452,12 @@
"xpack.canvas.helpMenu.description": "{CANVAS} に関する情報",
"xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} ドキュメント",
"xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "キーボードショートカット",
"xpack.canvas.home.myWorkpadsTabLabel": "マイワークパッド",
"xpack.canvas.home.workpadTemplatesTabLabel": "テンプレート",
"xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription": "新規ワークパッドを作成、テンプレートで開始、またはワークパッド {JSON} ファイルをここにドロップしてインポートします。",
"xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription": "{CANVAS} を初めて使用する場合",
"xpack.canvas.homeEmptyPrompt.emptyPromptTitle": "初の’ワークパッドを追加しましょう",
"xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel": "初の’ワークパッドを追加しましょう",
"xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText": "前に移動",
"xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText": "表面に移動",
"xpack.canvas.keyboardShortcuts.cloneShortcutHelpText": "クローンを作成",
@ -6898,6 +6904,7 @@
"xpack.canvas.units.quickRange.last90Days": "過去90日間",
"xpack.canvas.units.quickRange.today": "今日",
"xpack.canvas.units.quickRange.yesterday": "昨日",
"xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} のコピー",
"xpack.canvas.varConfig.addButtonLabel": "変数の追加",
"xpack.canvas.varConfig.addTooltipLabel": "変数の追加",
"xpack.canvas.varConfig.copyActionButtonLabel": "スニペットをコピー",
@ -7024,40 +7031,30 @@
"xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "ズーム",
"xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "リセット",
"xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%",
"xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} のコピー",
"xpack.canvas.workpadLoader.cloneTooltip": "ワークパッドのクローンを作成します",
"xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "ワークパッドを作成中...",
"xpack.canvas.workpadLoader.deleteButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドを削除",
"xpack.canvas.workpadLoader.deleteButtonLabel": " ({numberOfWorkpads}) ワークパッドを削除",
"xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel": "削除",
"xpack.canvas.workpadLoader.deleteModalDescription": "削除されたワークパッドは復元できません。",
"xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle": "{numberOfWorkpads} 個のワークパッドを削除しますか?",
"xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle": "ワークパッド「{workpadName}」削除しますか?",
"xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription": "新規ワークパッドを作成、テンプレートで開始、またはワークパッド {JSON} ファイルをここにドロップしてインポートします。",
"xpack.canvas.workpadLoader.emptyPromptNewUserDescription": "{CANVAS} を初めて使用する場合",
"xpack.canvas.workpadLoader.emptyPromptTitle": "初の’ワークパッドを追加しましょう",
"xpack.canvas.workpadLoader.exportButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドをエクスポート",
"xpack.canvas.workpadLoader.exportButtonLabel": "エクスポート ({numberOfWorkpads}) ",
"xpack.canvas.workpadLoader.exportTooltip": "ワークパッドをエクスポート",
"xpack.canvas.workpadLoader.fetchLoadingDescription": "ワークパッドを取得中...",
"xpack.canvas.workpadLoader.filePickerPlaceholder": "ワークパッド {JSON} ファイルをインポート",
"xpack.canvas.workpadLoader.loadWorkpadArialLabel": "ワークパッド「{workpadName}」を読み込む",
"xpack.canvas.workpadLoader.noPermissionToCloneToolTip": "ワークパッドのクローンを作成するパーミッションがありません",
"xpack.canvas.workpadLoader.noPermissionToCreateToolTip": "ワークパッドを作成するパーミッションがありません",
"xpack.canvas.workpadLoader.noPermissionToDeleteToolTip": "ワークパッドを削除するパーミッションがありません",
"xpack.canvas.workpadLoader.noPermissionToUploadToolTip": "ワークパッドを更新するパーミッションがありません",
"xpack.canvas.workpadLoader.sampleDataLinkLabel": "初の’ワークパッドを追加しましょう",
"xpack.canvas.workpadLoader.table.actionsColumnTitle": "アクション",
"xpack.canvas.workpadLoader.table.createdColumnTitle": "作成済み",
"xpack.canvas.workpadLoader.table.nameColumnTitle": "ワークパッド名",
"xpack.canvas.workpadLoader.table.updatedColumnTitle": "更新しました",
"xpack.canvas.workpadManager.modalTitle": "{CANVAS} ワークパッド",
"xpack.canvas.workpadManager.myWorkpadsTabLabel": "マイワークパッド",
"xpack.canvas.workpadManager.workpadTemplatesTabLabel": "テンプレート",
"xpack.canvas.workpadSearch.searchPlaceholder": "ワークパッドを検索",
"xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel": "ワークパッドテンプレート「{templateName}」のクローンを作成",
"xpack.canvas.workpadTemplate.creatingTemplateLabel": "テンプレート「{templateName}」から作成しています",
"xpack.canvas.workpadTemplate.searchPlaceholder": "テンプレートを検索",
"xpack.canvas.workpadImport.filePickerPlaceholder": "ワークパッド {JSON} ファイルをインポート",
"xpack.canvas.workpadTable.cloneTooltip": "ワークパッドのクローンを作成します",
"xpack.canvas.workpadTable.exportTooltip": "ワークパッドをエクスポート",
"xpack.canvas.workpadTable.loadWorkpadArialLabel": "ワークパッド「{workpadName}」を読み込む",
"xpack.canvas.workpadTable.noPermissionToCloneToolTip": "ワークパッドのクローンを作成するパーミッションがありません",
"xpack.canvas.workpadTable.searchPlaceholder": "ワークパッドを検索",
"xpack.canvas.workpadTable.table.actionsColumnTitle": "アクション",
"xpack.canvas.workpadTable.table.createdColumnTitle": "作成済み",
"xpack.canvas.workpadTable.table.nameColumnTitle": "ワークパッド名",
"xpack.canvas.workpadTable.table.updatedColumnTitle": "更新しました",
"xpack.canvas.workpadTableTools.deleteButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドを削除",
"xpack.canvas.workpadTableTools.deleteButtonLabel": " ({numberOfWorkpads}) ワークパッドを削除",
"xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel": "削除",
"xpack.canvas.workpadTableTools.deleteModalDescription": "削除されたワークパッドは復元できません。",
"xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle": "{numberOfWorkpads} 個のワークパッドを削除しますか?",
"xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle": "ワークパッド「{workpadName}」削除しますか?",
"xpack.canvas.workpadTableTools.exportButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドをエクスポート",
"xpack.canvas.workpadTableTools.exportButtonLabel": "エクスポート ({numberOfWorkpads}) ",
"xpack.canvas.workpadTableTools.noPermissionToCreateToolTip": "ワークパッドを作成するパーミッションがありません",
"xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip": "ワークパッドを削除するパーミッションがありません",
"xpack.canvas.workpadTableTools.noPermissionToUploadToolTip": "ワークパッドを更新するパーミッションがありません",
"xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel": "ワークパッドテンプレート「{templateName}」のクローンを作成",
"xpack.canvas.workpadTemplates.creatingTemplateLabel": "テンプレート「{templateName}」から作成しています",
"xpack.canvas.workpadTemplates.searchPlaceholder": "テンプレートを検索",
"xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "説明",
"xpack.canvas.workpadTemplates.table.nameColumnTitle": "テンプレート名",
"xpack.canvas.workpadTemplates.table.tagsColumnTitle": "タグ",

View file

@ -6146,18 +6146,18 @@
"xpack.canvas.error.esService.indicesFetchErrorMessage": "无法提取 Elasticsearch 索引",
"xpack.canvas.error.RenderWithFn.renderErrorMessage": "呈现“{functionName}”失败。",
"xpack.canvas.error.repeatImage.missingMaxArgument": "如果提供 {emptyImageArgument},则必须设置 {maxArgument}",
"xpack.canvas.error.workpadLoader.cloneFailureErrorMessage": "无法克隆 Workpad",
"xpack.canvas.error.workpadLoader.deleteFailureErrorMessage": "无法删除所有 Workpad",
"xpack.canvas.error.workpadLoader.findFailureErrorMessage": "无法查找 Workpad",
"xpack.canvas.error.workpadLoader.uploadFailureErrorMessage": "无法上传 Workpad",
"xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "无法克隆 Workpad",
"xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "无法上传 Workpad",
"xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "无法删除所有 Workpad",
"xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "无法查找 Workpad",
"xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件",
"xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage": "无法上传文件",
"xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。",
"xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "无法创建 Workpad",
"xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "无法加载具有以下 ID 的 Workpad",
"xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件",
"xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage": "无法上传文件",
"xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。",
"xpack.canvas.errorComponent.description": "表达式失败,并显示消息:",
"xpack.canvas.errorComponent.title": "哎哟!表达式失败",
"xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage": "无法上传“{fileName}”",
"xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "无法上传“{fileName}”",
"xpack.canvas.expression.cancelButtonLabel": "取消",
"xpack.canvas.expression.closeButtonLabel": "关闭",
"xpack.canvas.expression.learnLinkText": "学习表达式语法",
@ -6492,6 +6492,12 @@
"xpack.canvas.helpMenu.description": "有关 {CANVAS} 特定信息",
"xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} 文档",
"xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "快捷键",
"xpack.canvas.home.myWorkpadsTabLabel": "我的 Workpad",
"xpack.canvas.home.workpadTemplatesTabLabel": "模板",
"xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription": "创建新的 Workpad、从模板入手或通过将 Workpad {JSON} 文件拖放到此处来导入。",
"xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription": "{CANVAS} 新手?",
"xpack.canvas.homeEmptyPrompt.emptyPromptTitle": "添加您的首个 Workpad",
"xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel": "添加您的首个 Workpad",
"xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText": "前移",
"xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText": "置前",
"xpack.canvas.keyboardShortcuts.cloneShortcutHelpText": "克隆",
@ -6942,6 +6948,7 @@
"xpack.canvas.units.time.hours": "{hours, plural, other {# 小时}}",
"xpack.canvas.units.time.minutes": "{minutes, plural, other {# 分钟}}",
"xpack.canvas.units.time.seconds": "{seconds, plural, other {# 秒}}",
"xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} 副本",
"xpack.canvas.varConfig.addButtonLabel": "添加变量",
"xpack.canvas.varConfig.addTooltipLabel": "添加变量",
"xpack.canvas.varConfig.copyActionButtonLabel": "复制代码片段",
@ -7072,40 +7079,30 @@
"xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "缩放",
"xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "重置",
"xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%",
"xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} 副本",
"xpack.canvas.workpadLoader.cloneTooltip": "克隆 Workpad",
"xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "正在创建 Workpad......",
"xpack.canvas.workpadLoader.deleteButtonAriaLabel": "删除 {numberOfWorkpads} 个 Workpad",
"xpack.canvas.workpadLoader.deleteButtonLabel": "删除 ({numberOfWorkpads})",
"xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel": "删除",
"xpack.canvas.workpadLoader.deleteModalDescription": "您无法恢复删除的 Workpad。",
"xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle": "删除 {numberOfWorkpads} 个 Workpad",
"xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle": "删除 Workpad“{workpadName}”?",
"xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription": "创建新的 Workpad、从模板入手或通过将 Workpad {JSON} 文件拖放到此处来导入。",
"xpack.canvas.workpadLoader.emptyPromptNewUserDescription": "{CANVAS} 新手?",
"xpack.canvas.workpadLoader.emptyPromptTitle": "添加您的首个 Workpad",
"xpack.canvas.workpadLoader.exportButtonAriaLabel": "导出 {numberOfWorkpads} 个 Workpad",
"xpack.canvas.workpadLoader.exportButtonLabel": "导出 ({numberOfWorkpads})",
"xpack.canvas.workpadLoader.exportTooltip": "导出 Workpad",
"xpack.canvas.workpadLoader.fetchLoadingDescription": "正在获取 Workpad......",
"xpack.canvas.workpadLoader.filePickerPlaceholder": "导入 Workpad {JSON} 文件",
"xpack.canvas.workpadLoader.loadWorkpadArialLabel": "加载 Workpad“{workpadName}”",
"xpack.canvas.workpadLoader.noPermissionToCloneToolTip": "您无权克隆 Workpad",
"xpack.canvas.workpadLoader.noPermissionToCreateToolTip": "您无权创建 Workpad",
"xpack.canvas.workpadLoader.noPermissionToDeleteToolTip": "您无权删除 Workpad",
"xpack.canvas.workpadLoader.noPermissionToUploadToolTip": "您无权上传 Workpad",
"xpack.canvas.workpadLoader.sampleDataLinkLabel": "添加您的首个 Workpad",
"xpack.canvas.workpadLoader.table.actionsColumnTitle": "操作",
"xpack.canvas.workpadLoader.table.createdColumnTitle": "创建时间",
"xpack.canvas.workpadLoader.table.nameColumnTitle": "Workpad 名称",
"xpack.canvas.workpadLoader.table.updatedColumnTitle": "更新时间",
"xpack.canvas.workpadManager.modalTitle": "{CANVAS} Workpad",
"xpack.canvas.workpadManager.myWorkpadsTabLabel": "我的 Workpad",
"xpack.canvas.workpadManager.workpadTemplatesTabLabel": "模板",
"xpack.canvas.workpadSearch.searchPlaceholder": "查找 Workpad",
"xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel": "克隆 Workpad 模板“{templateName}”",
"xpack.canvas.workpadTemplate.creatingTemplateLabel": "正在从模板“{templateName}”创建",
"xpack.canvas.workpadTemplate.searchPlaceholder": "查找模板",
"xpack.canvas.workpadImport.filePickerPlaceholder": "导入 Workpad {JSON} 文件",
"xpack.canvas.workpadTable.searchPlaceholder": "查找 Workpad",
"xpack.canvas.workpadTable.cloneTooltip": "克隆 Workpad",
"xpack.canvas.workpadTable.exportTooltip": "导出 Workpad",
"xpack.canvas.workpadTable.loadWorkpadArialLabel": "加载 Workpad“{workpadName}”",
"xpack.canvas.workpadTable.noPermissionToCloneToolTip": "您无权克隆 Workpad",
"xpack.canvas.workpadTable.table.actionsColumnTitle": "操作",
"xpack.canvas.workpadTable.table.createdColumnTitle": "创建时间",
"xpack.canvas.workpadTable.table.nameColumnTitle": "Workpad 名称",
"xpack.canvas.workpadTable.table.updatedColumnTitle": "更新时间",
"xpack.canvas.workpadTableTools.deleteButtonAriaLabel": "删除 {numberOfWorkpads} 个 Workpad",
"xpack.canvas.workpadTableTools.deleteButtonLabel": "删除 ({numberOfWorkpads})",
"xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel": "删除",
"xpack.canvas.workpadTableTools.deleteModalDescription": "您无法恢复删除的 Workpad。",
"xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle": "删除 {numberOfWorkpads} 个 Workpad",
"xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle": "删除 Workpad“{workpadName}”?",
"xpack.canvas.workpadTableTools.exportButtonAriaLabel": "导出 {numberOfWorkpads} 个 Workpad",
"xpack.canvas.workpadTableTools.exportButtonLabel": "导出 ({numberOfWorkpads})",
"xpack.canvas.workpadTableTools.noPermissionToCreateToolTip": "您无权创建 Workpad",
"xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip": "您无权删除 Workpad",
"xpack.canvas.workpadTableTools.noPermissionToUploadToolTip": "您无权上传 Workpad",
"xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel": "克隆 Workpad 模板“{templateName}”",
"xpack.canvas.workpadTemplates.creatingTemplateLabel": "正在从模板“{templateName}”创建",
"xpack.canvas.workpadTemplates.searchPlaceholder": "查找模板",
"xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "描述",
"xpack.canvas.workpadTemplates.table.nameColumnTitle": "模板名称",
"xpack.canvas.workpadTemplates.table.tagsColumnTitle": "标签",

View file

@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('loads workpads', async function () {
await retry.waitFor(
'canvas workpads visible',
async () => await testSubjects.exists('canvasWorkpadLoaderTable')
async () => await testSubjects.exists('canvasWorkpadTable')
);
await a11y.testAppSnapshot();
});

View file

@ -17,7 +17,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) {
describe('smoke test', function () {
this.tags('includeFirefox');
const workpadListSelector = 'canvasWorkpadLoaderTable > canvasWorkpadLoaderWorkpad';
const workpadListSelector = 'canvasWorkpadTable > canvasWorkpadTableWorkpad';
const testWorkpadId = 'workpad-1705f884-6224-47de-ba49-ca224fe6ec31';
before(async () => {

View file

@ -39,7 +39,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo
* to load the workpad. Resolves once the workpad is in the DOM
*/
async loadFirstWorkpad(workpadName: string) {
const elem = await testSubjects.find('canvasWorkpadLoaderWorkpad');
const elem = await testSubjects.find('canvasWorkpadTableWorkpad');
const text = await elem.getVisibleText();
expect(text).to.be(workpadName);
await elem.click();