[home] Sample Data Tab w/ callout UI (#136790)

* [home] Sample Data Tab w/ callout UI

* Fix tests

* Update packages/home/sample_data_tab_content/src/demo_env_panel.tsx

Co-authored-by: Kelly Murphy <kelly.murphy@elastic.co>

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* Fixes

* Fixes

* Update test/functional/page_objects/home_page.ts

* Fix tests

* Add telemetry

* Add docs, more telemetry

Co-authored-by: Kelly Murphy <kelly.murphy@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2022-07-25 20:34:32 -05:00 committed by GitHub
parent 7717e90b4e
commit b459ffa4c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 895 additions and 89 deletions

View file

@ -0,0 +1,145 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_DIRNAME = "sample_data_card"
PKG_REQUIRE_NAME = "@kbn/home-sample-data-card"
SOURCE_FILES = glob(
[
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.mdx",
"src/**/*.svg",
"src/**/*.png",
],
exclude = [
"**/*.test.*",
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
]
# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"@npm//@elastic/eui",
"@npm//@storybook/addon-actions",
"@npm//@storybook/react",
"@npm//enzyme",
"@npm//lodash",
"@npm//react",
"//packages/kbn-i18n",
]
# In this array place dependencies necessary to build the types, which will include the
# :npm_module_types target of other packages and packages from NPM, including @types/*
# packages.
#
# To reference the types for another package use:
# "//repo/relative/path/to/package:npm_module_types"
# eg. "//packages/kbn-utils:npm_module_types"
#
# References to NPM packages work the same as RUNTIME_DEPS
TYPES_DEPS = [
"@npm//@elastic/eui",
"@npm//@storybook/addon-actions",
"@npm//@storybook/react",
"@npm//@types/enzyme",
"@npm//@types/jest",
"@npm//@types/lodash",
"@npm//@types/node",
"@npm//@types/react",
"//packages/kbn-ambient-ui-types",
"//packages/kbn-i18n:npm_module_types",
"//packages/home/sample_data_types",
]
jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)
jsts_transpiler(
name = "target_web",
srcs = SRCS,
build_pkg_name = package_name(),
web = True,
additional_args = [
"--copy-files",
"--quiet"
],
)
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.bazel.json",
],
)
ts_project(
name = "tsc_types",
args = ['--pretty'],
srcs = SRCS,
deps = TYPES_DEPS,
declaration = True,
emit_declaration_only = True,
out_dir = "target_types",
root_dir = "src",
tsconfig = ":tsconfig",
)
js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node", ":target_web"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)
filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)
pkg_npm_types(
name = "npm_module_types",
srcs = SRCS,
deps = [":tsc_types"],
package_name = PKG_REQUIRE_NAME,
tsconfig = ":tsconfig",
visibility = ["//visibility:public"],
)
filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,23 @@
---
id: home/SampleData/Cards
slug: /home/sample-data/cards
title: Sample Data Cards
summary: A component that displays Sample Data Sets as cards and grid of cards.
tags: ['home', 'component', 'sample-data']
date: 2022-06-30
---
This package contains a pair of components. The first displays a Sample Data Set as a card which displays the Sample Data Set's name, description, and image as well as functions to install, uninstall and navigate to Saved Objects associated with the data set. The other component fetches a list of Sample Data Sets and displays them as a grid, which also responds to install and uninstall events.
## API
| Export | Description |
|---|---|
| `SampleDataCard` | Fetches and displays a grid of Sample Data Sets as `SampleDataCard` components. |
| `SampleDataCard` | A card component representing a Sample Data Set, which can install, uninstall and navigate relevant objects from a Sample Data Set. |
| `SampleDataCardProvider` | Provides contextual services to `KibanaNoDataPage`. |
| `SampleDataCardKibanaProvider` | Maps Kibana dependencies to provide contextual services to `KibanaNoDataPage`. |
## EUI Promotion Status
This component is not currently considered for promotion to EUI.

View file

@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/packages/home/sample_data_card'],
};

View file

@ -0,0 +1,8 @@
{
"name": "@kbn/home-sample-data-card",
"private": true,
"version": "1.0.0",
"main": "./target_node/index.js",
"browser": "./target_web/index.js",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,275 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SampleDataCard installed renders with app links 1`] = `
<div
class="euiPanel euiPanel--plain euiPanel--paddingMedium euiCard euiCard--leftAligned euiCard--hasBetaBadge emotion-euiPanel-grow-m-m-plain-hasShadow"
data-test-subj="sampleDataSetCardsample-data-set"
>
<div
class="euiCard__top"
>
<div
class="euiCard__image"
>
<img
alt=""
src="test-file-stub"
/>
</div>
</div>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title emotion-euiTitle-s"
id="generated-idTitle"
>
Sample Data Set
</span>
<div
class="euiText euiCard__description emotion-euiText-s"
id="generated-idDescription"
>
<p>
This is a sample data set you can use.
</p>
</div>
</div>
<span
class="euiCard__betaBadgeWrapper"
>
<span
class="euiBetaBadge euiBetaBadge--hollow euiCard__betaBadge"
id="generated-idBetaBadge"
title="installed"
>
installed
</span>
</span>
<div
class="euiCard__footer"
>
<div
class="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Remove Sample Data Set"
class="euiButtonEmpty euiButtonEmpty--danger euiButtonEmpty--flushLeft"
data-test-subj="removeSampleDataSetsample-data-set"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Remove
</span>
</span>
</button>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiPopover euiPopover--anchorDownCenter"
data-test-subj="launchSampleDataSetsample-data-set"
id="sampleDataLinkssample-data-set"
>
<div
class="euiPopover__anchor"
>
<button
aria-label="View Sample Data Set"
class="euiButton euiButton--primary"
type="button"
>
<span
class="euiButtonContent euiButtonContent--iconRight euiButton__content"
>
<span
class="euiButtonContent__icon"
color="inherit"
data-euiicon-type="arrowDown"
/>
<span
class="euiButton__text"
>
View data
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`SampleDataCard installed renders without app links 1`] = `
<div
class="euiPanel euiPanel--plain euiPanel--paddingMedium euiCard euiCard--leftAligned euiCard--hasBetaBadge emotion-euiPanel-grow-m-m-plain-hasShadow"
data-test-subj="sampleDataSetCardsample-data-set"
>
<div
class="euiCard__top"
>
<div
class="euiCard__image"
>
<img
alt=""
src="test-file-stub"
/>
</div>
</div>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title emotion-euiTitle-s"
id="generated-idTitle"
>
Sample Data Set
</span>
<div
class="euiText euiCard__description emotion-euiText-s"
id="generated-idDescription"
>
<p>
This is a sample data set you can use.
</p>
</div>
</div>
<span
class="euiCard__betaBadgeWrapper"
>
<span
class="euiBetaBadge euiBetaBadge--hollow euiCard__betaBadge"
id="generated-idBetaBadge"
title="installed"
>
installed
</span>
</span>
<div
class="euiCard__footer"
>
<div
class="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Remove Sample Data Set"
class="euiButtonEmpty euiButtonEmpty--danger euiButtonEmpty--flushLeft"
data-test-subj="removeSampleDataSetsample-data-set"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Remove
</span>
</span>
</button>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="View Sample Data Set"
class="euiButton euiButton--primary"
data-test-subj="launchSampleDataSetsample-data-set"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
View data
</span>
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`SampleDataCard not installed renders 1`] = `
<div
class="euiPanel euiPanel--plain euiPanel--paddingMedium euiCard euiCard--leftAligned emotion-euiPanel-grow-m-m-plain-hasShadow"
data-test-subj="sampleDataSetCardsample-data-set"
>
<div
class="euiCard__top"
>
<div
class="euiCard__image"
>
<img
alt=""
src="test-file-stub"
/>
</div>
</div>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title emotion-euiTitle-s"
id="generated-idTitle"
>
Sample Data Set
</span>
<div
class="euiText euiCard__description emotion-euiText-s"
id="generated-idDescription"
>
<p>
This is a sample data set you can use.
</p>
</div>
</div>
<div
class="euiCard__footer"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Sample Data Set"
class="euiButton euiButton--primary"
data-test-subj="addSampleDataSetsample-data-set"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Add data
</span>
</span>
</button>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,22 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* DataSetStatusType for an installed data set.
* @see src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types
*/
export const INSTALLED_STATUS = 'installed';
/**
* DataSetStatusType for a data set that is not installed yet.
* @see src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types
*/
export const UNINSTALLED_STATUS = 'not_installed';
// Corresponds to src/plugins/home/server/services/sample_data/routes
export const SAMPLE_DATA_API = '/api/sample_data';

View file

@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`install footer should render 1`] = `
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
class="euiToolTipAnchor"
>
<button
aria-label="Add Data Set Name"
class="euiButton euiButton--primary euiButton-isDisabled"
data-test-subj="addSampleDataSetdata-set-id"
disabled=""
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Add data
</span>
</span>
</button>
</span>
</div>
</div>
`;

View file

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`install footer should render 1`] = `
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Add Data Set Name"
class="euiButton euiButton--primary"
data-test-subj="addSampleDataSetdata-set-id"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Add data
</span>
</span>
</button>
</div>
</div>
`;

View file

@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`install footer should render 1`] = `
<div
class="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Remove Data Set Name"
class="euiButtonEmpty euiButtonEmpty--danger euiButtonEmpty--flushLeft"
data-test-subj="removeSampleDataSetdata-set-id"
type="button"
>
<span
class="euiButtonContent euiButtonEmpty__content"
>
<span
class="euiButtonEmpty__text"
>
Remove
</span>
</span>
</button>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="View Data Set Name"
class="euiButton euiButton--primary"
data-test-subj="launchSampleDataSetdata-set-id"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
View data
</span>
</span>
</button>
</div>
</div>
`;

View file

@ -0,0 +1,86 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render popover when appLinks is not empty 1`] = `
<div
class="euiPopover euiPopover--anchorDownCenter"
data-test-subj="launchSampleDataSetecommerce"
id="sampleDataLinksecommerce"
>
<div
class="euiPopover__anchor"
>
<button
aria-label="View Sample eCommerce orders"
class="euiButton euiButton--primary"
type="button"
>
<span
class="euiButtonContent euiButtonContent--iconRight euiButton__content"
>
<span
class="euiButtonContent__icon"
color="inherit"
data-euiicon-type="arrowDown"
/>
<span
class="euiButton__text"
>
View data
</span>
</span>
</button>
</div>
</div>
`;
exports[`should render popover with ordered appLinks 1`] = `
<div
class="euiPopover euiPopover--anchorDownCenter"
data-test-subj="launchSampleDataSetecommerce"
id="sampleDataLinksecommerce"
>
<div
class="euiPopover__anchor"
>
<button
aria-label="View Sample eCommerce orders"
class="euiButton euiButton--primary"
type="button"
>
<span
class="euiButtonContent euiButtonContent--iconRight euiButton__content"
>
<span
class="euiButtonContent__icon"
color="inherit"
data-euiicon-type="arrowDown"
/>
<span
class="euiButton__text"
>
View data
</span>
</span>
</button>
</div>
</div>
`;
exports[`should render simple button when appLinks is empty 1`] = `
<button
aria-label="View Sample eCommerce orders"
class="euiButton euiButton--primary"
data-test-subj="launchSampleDataSetecommerce"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
View data
</span>
</span>
</button>
`;

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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { renderWithIntl } from '@kbn/test-jest-helpers';
import { DisabledFooter, Props } from './disabled_footer';
import { SampleDataCardProvider } from '../services';
import { getMockServices } from '../mocks';
describe('install footer', () => {
const props: Props = {
id: 'data-set-id',
name: 'Data Set Name',
statusMsg: 'Data Set Status Message',
};
const render = (element: React.ReactElement) =>
renderWithIntl(
<SampleDataCardProvider {...getMockServices()}>{element}</SampleDataCardProvider>
);
test('should render', () => {
const component = render(<DisabledFooter {...props} />);
expect(component).toMatchSnapshot();
});
});

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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SampleDataSet } from '@kbn/home-sample-data-types';
/**
* Props for the `DisabledFooter` component.
*/
export type Props = Pick<SampleDataSet, 'id' | 'name' | 'statusMsg'>;
const addDataLabel = i18n.translate('homePackages.sampleDataCard.default.addButtonLabel', {
defaultMessage: 'Add data',
});
/**
* A footer for the `SampleDataCard` displayed when an unknown error or status prevents a person
* from installing the Sample Data Set.
*/
export const DisabledFooter = ({ id, name, statusMsg }: Props) => {
const errorMessage = i18n.translate(
'homePackages.sampleDataCard.default.unableToVerifyErrorMessage',
{ defaultMessage: 'Unable to verify dataset status, error: {statusMsg}', values: { statusMsg } }
);
const addButtonAriaLabel = i18n.translate(
'homePackages.sampleDataCard.default.addButtonAriaLabel',
{
defaultMessage: 'Add {datasetName}',
values: {
datasetName: name,
},
}
);
return (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiToolTip position="top" content={<p>{errorMessage}</p>}>
<EuiButton
isDisabled
data-test-subj={`addSampleDataSet${id}`}
aria-label={addButtonAriaLabel}
>
{addDataLabel}
</EuiButton>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,49 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import type { SampleDataSet } from '@kbn/home-sample-data-types';
import { Params, getStoryArgTypes, getStoryServices, mockDataSet } from '../mocks';
import { SampleDataCardProvider } from '../services';
import { Footer as Component } from '.';
import mdx from '../../README.mdx';
export default {
title: 'Sample Data/Card Footer',
description: '',
parameters: {
docs: {
page: mdx,
},
},
decorators: [(Story) => <div style={{ width: '433px', padding: '25px' }}>{Story()}</div>],
} as ComponentMeta<typeof Component>;
const { description, ...argTypes } = getStoryArgTypes();
export const CardFooter = (params: Params) => {
const { includeAppLinks, status, ...rest } = params;
const sampleDataSet: SampleDataSet = {
...mockDataSet,
...rest,
status,
appLinks: includeAppLinks ? mockDataSet.appLinks : [],
};
return (
<SampleDataCardProvider {...getStoryServices(params)}>
<Component sampleDataSet={sampleDataSet} onAction={action('onAction')} />
</SampleDataCardProvider>
);
};
CardFooter.argTypes = argTypes;

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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { SampleDataSet, InstalledStatus } from '@kbn/home-sample-data-types';
import { INSTALLED_STATUS, UNINSTALLED_STATUS } from '../constants';
import { DisabledFooter } from './disabled_footer';
import { InstallFooter } from './install_footer';
import { RemoveFooter } from './remove_footer';
/**
* Props for the `Footer` component.
*/
export interface Props {
/** The Sample Data Set and its status. */
sampleDataSet: SampleDataSet;
/** The handler to invoke when an action is performed upon the Sample Data Set. */
onAction: (id: string, status: InstalledStatus) => void;
}
/**
* Displays the appropriate Footer component based on the status of the Sample Data Set.
*/
export const Footer = ({ sampleDataSet, onAction }: Props) => {
if (sampleDataSet.status === INSTALLED_STATUS) {
return <RemoveFooter onRemove={(id) => onAction(id, UNINSTALLED_STATUS)} {...sampleDataSet} />;
}
if (sampleDataSet.status === UNINSTALLED_STATUS) {
return <InstallFooter onInstall={(id) => onAction(id, INSTALLED_STATUS)} {...sampleDataSet} />;
}
return <DisabledFooter {...sampleDataSet} />;
};

View file

@ -0,0 +1,78 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { renderWithIntl, mountWithIntl } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import { InstallFooter, Props } from './install_footer';
import { SampleDataCardProvider, Services } from '../services';
import { getMockServices } from '../mocks';
describe('install footer', () => {
beforeEach(() => {
jest.resetAllMocks();
});
const id = 'data-set-id';
const onInstall = jest.fn();
const notifyError = jest.fn();
const notifySuccess = jest.fn();
const props: Props = {
id,
onInstall,
name: 'Data Set Name',
defaultIndex: 'default-index',
};
const render = (element: React.ReactElement) =>
renderWithIntl(
<SampleDataCardProvider {...getMockServices()}>{element}</SampleDataCardProvider>
);
const mount = (element: React.ReactElement, params?: Partial<Services>) =>
mountWithIntl(
<SampleDataCardProvider {...getMockServices({ notifyError, notifySuccess, ...params })}>
{element}
</SampleDataCardProvider>
);
test('should render', () => {
const component = render(<InstallFooter {...props} />);
expect(component).toMatchSnapshot();
});
test('should invoke onInstall when install button is clicked', async () => {
const component = mount(<InstallFooter {...props} />);
await act(async () => {
component.find(`button[data-test-subj="addSampleDataSet${id}"]`).simulate('click');
});
expect(onInstall).toHaveBeenCalledTimes(1);
expect(notifySuccess).toHaveBeenCalledTimes(1);
expect(notifyError).toHaveBeenCalledTimes(0);
});
test('should not invoke onInstall when install button is clicked and an error is thrown', async () => {
const component = mount(<InstallFooter {...props} />, {
installSampleDataSet: () => {
throw new Error('error');
},
});
await act(async () => {
component.find(`button[data-test-subj="addSampleDataSet${id}"]`).simulate('click');
});
expect(onInstall).toHaveBeenCalledTimes(0);
expect(notifySuccess).toHaveBeenCalledTimes(0);
expect(notifyError).toHaveBeenCalledTimes(1);
});
});

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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { SampleDataSet } from '@kbn/home-sample-data-types';
import { useInstall } from '../hooks';
import type { UseInstallParams } from '../hooks';
/**
* Props for the `InstallFooter` component.
*/
export type Props = Pick<SampleDataSet, 'id' | 'name'> & UseInstallParams;
const addingLabel = i18n.translate('homePackages.sampleDataCard.addingButtonLabel', {
defaultMessage: 'Adding',
});
const addLabel = i18n.translate('homePackages.sampleDataCard.addButtonLabel', {
defaultMessage: 'Add data',
});
/**
* A footer displayed when a Sample Data Set is not installed, allowing a person to install it.
*/
export const InstallFooter = (params: Props) => {
const [install, isInstalling] = useInstall(params);
const { id, name } = params;
const addingAriaLabel = i18n.translate('homePackages.sampleDataCard.addingButtonAriaLabel', {
defaultMessage: 'Adding {datasetName}',
values: {
datasetName: name,
},
});
const addAriaLabel = i18n.translate('homePackages.sampleDataCard.addButtonAriaLabel', {
defaultMessage: 'Add {datasetName}',
values: {
datasetName: name,
},
});
return (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
isLoading={isInstalling}
onClick={install}
data-test-subj={`addSampleDataSet${id}`}
aria-label={isInstalling ? addingAriaLabel : addAriaLabel}
>
{isInstalling ? addingLabel : addLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,80 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { renderWithIntl, mountWithIntl } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import { RemoveFooter, Props } from './remove_footer';
import { SampleDataCardProvider, Services } from '../services';
import { getMockServices } from '../mocks';
describe('install footer', () => {
beforeEach(() => {
jest.resetAllMocks();
});
const id = 'data-set-id';
const onRemove = jest.fn();
const notifyError = jest.fn();
const notifySuccess = jest.fn();
const props: Props = {
id,
onRemove,
name: 'Data Set Name',
defaultIndex: 'default-index',
overviewDashboard: 'path/to/overview',
appLinks: [],
};
const render = (element: React.ReactElement) =>
renderWithIntl(
<SampleDataCardProvider {...getMockServices()}>{element}</SampleDataCardProvider>
);
const mount = (element: React.ReactElement, params?: Partial<Services>) =>
mountWithIntl(
<SampleDataCardProvider {...getMockServices({ notifyError, notifySuccess, ...params })}>
{element}
</SampleDataCardProvider>
);
test('should render', () => {
const component = render(<RemoveFooter {...props} />);
expect(component).toMatchSnapshot();
});
test('should invoke onRemove when remove button is clicked', async () => {
const component = mount(<RemoveFooter {...props} />);
await act(async () => {
component.find(`button[data-test-subj="removeSampleDataSet${id}"]`).simulate('click');
});
expect(onRemove).toHaveBeenCalledTimes(1);
expect(notifySuccess).toHaveBeenCalledTimes(1);
expect(notifyError).toHaveBeenCalledTimes(0);
});
test('should not invoke onRemove when remove button is clicked and an error is thrown', async () => {
const component = mount(<RemoveFooter {...props} />, {
removeSampleDataSet: () => {
throw new Error('error');
},
});
await act(async () => {
component.find(`button[data-test-subj="removeSampleDataSet${id}"]`).simulate('click');
});
expect(onRemove).toHaveBeenCalledTimes(0);
expect(notifySuccess).toHaveBeenCalledTimes(0);
expect(notifyError).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,72 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { SampleDataSet } from '@kbn/home-sample-data-types';
import { useRemove } from '../hooks';
import { ViewButton } from './view_button';
import type { UseRemoveParams } from '../hooks';
import type { Props as ViewButtonProps } from './view_button';
/**
* Props for the `RemoveFooter` component.
*/
export type Props = Pick<SampleDataSet, 'id' | 'name'> & UseRemoveParams & ViewButtonProps;
const removeLabel = i18n.translate('homePackages.sampleDataCard.removeButtonLabel', {
defaultMessage: 'Remove',
});
const removingLabel = i18n.translate('homePackages.sampleDataCard.removingButtonLabel', {
defaultMessage: 'Removing',
});
/**
* A footer displayed when a Sample Data Set is installed, allowing a person to remove it or view
* saved objects associated with it in their related solutions.
*/
export const RemoveFooter = (props: Props) => {
const [remove, isRemoving] = useRemove(props);
const { id, name } = props;
const removeAriaLabel = i18n.translate('homePackages.sampleDataCard.removeButtonAriaLabel', {
defaultMessage: 'Remove {datasetName}',
values: {
datasetName: name,
},
});
const removingAriaLabel = i18n.translate('homePackages.sampleDataCard.removingButtonAriaLabel', {
defaultMessage: 'Removing {datasetName}',
values: {
datasetName: name,
},
});
return (
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
isLoading={isRemoving}
onClick={remove}
color="danger"
data-test-subj={`removeSampleDataSet${id}`}
flush="left"
aria-label={isRemoving ? removingAriaLabel : removeAriaLabel}
>
{isRemoving ? removingLabel : removeLabel}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ViewButton {...props} />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,87 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { renderWithIntl } from '@kbn/test-jest-helpers';
import { ViewButton } from './view_button';
import { SampleDataCardProvider } from '../services';
import { getMockServices } from '../mocks';
const render = (element: React.ReactElement) =>
renderWithIntl(<SampleDataCardProvider {...getMockServices()}>{element}</SampleDataCardProvider>);
test('should render simple button when appLinks is empty', () => {
const component = render(
<ViewButton
id="ecommerce"
name="Sample eCommerce orders"
overviewDashboard="722b74f0-b882-11e8-a6d9-e546fe2bba5f"
appLinks={[]}
/>
);
expect(component).toMatchSnapshot();
});
test('should render popover when appLinks is not empty', () => {
const appLinks = [
{
path: 'app/myAppPath',
label: 'myAppLabel',
icon: 'logoKibana',
},
];
const component = render(
<ViewButton
id="ecommerce"
name="Sample eCommerce orders"
overviewDashboard="722b74f0-b882-11e8-a6d9-e546fe2bba5f"
appLinks={appLinks}
/>
);
expect(component).toMatchSnapshot();
});
test('should render popover with ordered appLinks', () => {
const appLinks = [
{
path: 'app/myAppPath',
label: 'myAppLabel[-1]',
icon: 'logoKibana',
order: -1, // to position it above Dashboard link
},
{
path: 'app/myAppPath',
label: 'myAppLabel',
icon: 'logoKibana',
},
{
path: 'app/myAppPath',
label: 'myAppLabel[5]',
icon: 'logoKibana',
order: 5,
},
{
path: 'app/myAppPath',
label: 'myAppLabel[3]',
icon: 'logoKibana',
order: 3,
},
];
const component = render(
<ViewButton
id="ecommerce"
name="Sample eCommerce orders"
overviewDashboard="722b74f0-b882-11e8-a6d9-e546fe2bba5f"
appLinks={appLinks}
/>
);
expect(component).toMatchSnapshot();
});

View file

@ -0,0 +1,122 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { sortBy } from 'lodash';
import {
EuiButton,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiIcon,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { SampleDataSet } from '@kbn/home-sample-data-types';
import { useServices } from '../services';
/**
* Props for the `ViewButton` component.
*/
export type Props = Pick<SampleDataSet, 'id' | 'name' | 'overviewDashboard' | 'appLinks'>;
const viewDataButtonLabel = i18n.translate('homePackages.sampleDataCard.viewDataButtonLabel', {
defaultMessage: 'View data',
});
const dashboardLabel = i18n.translate('homePackages.sampleDataCard.dashboardLinkLabel', {
defaultMessage: 'Dashboard',
});
/**
* A button displayed when a Sample Data Set is installed, allowing a person to view the overview dashboard,
* and, if included, a number of actions to navigate to other solutions.
*/
export const ViewButton = ({ id, name, overviewDashboard, appLinks }: Props) => {
const { addBasePath, getAppNavigationHandler } = useServices();
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const viewDataButtonAriaLabel = i18n.translate(
'homePackages.sampleDataCard.viewDataButtonAriaLabel',
{
defaultMessage: 'View {datasetName}',
values: {
datasetName: name,
},
}
);
const dashboardPath = `/app/dashboards#/view/${overviewDashboard}`;
if (appLinks.length === 0) {
return (
<EuiButton
onClick={getAppNavigationHandler(dashboardPath)}
data-test-subj={`launchSampleDataSet${id}`}
aria-label={viewDataButtonAriaLabel}
>
{viewDataButtonLabel}
</EuiButton>
);
}
const togglePopover = () => {
setIsPopoverOpen(!isPopoverOpen);
};
const dashboardAppLink = {
path: dashboardPath,
label: dashboardLabel,
icon: 'dashboardApp',
order: 0,
'data-test-subj': `viewSampleDataSet${id}-dashboard`,
};
const sortedItems = sortBy([dashboardAppLink, ...appLinks], 'order');
const items = sortedItems.map(({ path, label, icon, ...rest }) => {
return {
name: label,
icon: <EuiIcon type={icon} size="m" />,
href: addBasePath(path),
onClick: getAppNavigationHandler(path),
...(rest['data-test-subj'] ? { 'data-test-subj': rest['data-test-subj'] } : {}),
};
});
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
items,
},
];
const popoverButton = (
<EuiButton
aria-label={viewDataButtonAriaLabel}
onClick={togglePopover}
iconType="arrowDown"
iconSide="right"
>
{viewDataButtonLabel}
</EuiButton>
);
return (
<EuiPopover
id={`sampleDataLinks${id}`}
button={popoverButton}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
panelPaddingSize="none"
anchorPosition="downCenter"
data-test-subj={`launchSampleDataSet${id}`}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
};

View file

@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { useInstall } from './use_install';
export type { Params as UseInstallParams } from './use_install';
export { useRemove } from './use_remove';
export type { Params as UseRemoveParams } from './use_remove';

View file

@ -0,0 +1,64 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import type { SampleDataSet } from '@kbn/home-sample-data-types';
import { useServices } from '../services';
/**
* Parameters for the `useInstall` React hook.
*/
export type Params = Pick<SampleDataSet, 'id' | 'defaultIndex' | 'name'> & {
/** Handler to invoke when the Sample Data Set is successfully installed. */
onInstall: (id: string) => void;
};
/**
* A React hook that allows a component to install a sample data set, handling success and
* failure in the Kibana UI. It also provides a boolean that indicates if the data set is
* in the process of being installed.
*/
export const useInstall = ({
id,
defaultIndex,
name,
onInstall,
}: Params): [() => void, boolean] => {
const { installSampleDataSet, notifyError, notifySuccess } = useServices();
const [isInstalling, setIsInstalling] = React.useState(false);
const install = useCallback(async () => {
try {
setIsInstalling(true);
await installSampleDataSet(id, defaultIndex);
setIsInstalling(false);
notifySuccess({
title: i18n.translate('homePackages.sampleDataSet.installedLabel', {
defaultMessage: '{name} installed',
values: { name },
}),
['data-test-subj']: 'sampleDataSetInstallToast',
});
onInstall(id);
} catch (e) {
setIsInstalling(false);
notifyError({
title: i18n.translate('homePackages.sampleDataSet.unableToInstallErrorMessage', {
defaultMessage: 'Unable to install sample data set: {name}',
values: { name },
}),
text: `${e.message}`,
});
}
}, [installSampleDataSet, notifyError, notifySuccess, id, defaultIndex, name, onInstall]);
return [install, isInstalling];
};

View file

@ -0,0 +1,61 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import type { SampleDataSet } from '@kbn/home-sample-data-types';
import { useServices } from '../services';
/**
* Parameters for the `useRemove` React hook.
*/
export type Params = Pick<SampleDataSet, 'id' | 'defaultIndex' | 'name'> & {
/** Handler to invoke when the Sample Data Set is successfully removed. */
onRemove: (id: string) => void;
};
/**
* A React hook that allows a component to remove a sample data set, handling success and
* failure in the Kibana UI. It also provides a boolean that indicates if the data set is
* in the process of being removed.
*/
export const useRemove = ({ id, defaultIndex, name, onRemove }: Params): [() => void, boolean] => {
const { removeSampleDataSet, notifyError, notifySuccess } = useServices();
const [isRemoving, setIsRemoving] = React.useState(false);
const remove = useCallback(async () => {
try {
setIsRemoving(true);
await removeSampleDataSet(id, defaultIndex);
setIsRemoving(false);
notifySuccess({
title: i18n.translate('homePackages.sampleDataSet.uninstalledLabel', {
defaultMessage: '{name} uninstalled',
values: { name },
}),
['data-test-subj']: 'sampleDataSetUninstallToast',
});
onRemove(id);
} catch (e) {
setIsRemoving(false);
notifyError({
title: i18n.translate('homePackages.sampleDataSet.unableToUninstallErrorMessage', {
defaultMessage: 'Unable to uninstall sample data set: {name}',
values: { name },
}),
text: `${e.message}`,
});
}
}, [removeSampleDataSet, notifyError, notifySuccess, id, defaultIndex, name, onRemove]);
return [remove, isRemoving];
};

View file

@ -0,0 +1,24 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { SampleDataCard } from './sample_data_card';
export type { Props as SampleDataCardProps } from './sample_data_card';
export { SampleDataCardKibanaProvider, SampleDataCardProvider } from './services';
export type {
Services as SampleDataCardServices,
KibanaDependencies as SampleDataCardKibanaDependencies,
} from './services';
// TODO: clintandrewhall - convert to new Storybook mock when published.
export {
getStoryArgTypes as getSampleDataCardStoryArgTypes,
getStoryServices as getSampleDataCardStoryServices,
getMockDataSet as getSampleDataCardMockDataSet,
} from './mocks';
export type { Params as SampleDataCardStorybookParams } from './mocks';

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View file

@ -0,0 +1,6 @@
<svg width="34" height="32" viewBox="0 0 34 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.00024 22C2.47508 22.2862 3.49851 22.5776 4.42785 22.8421L4.64995 22.9054C7.06615 23.5957 10.4115 24 14 24C14.7424 24 15.4744 23.9827 16.1899 23.9491C16.3251 24.6338 16.5303 25.2934 16.7976 25.9201C15.8936 25.9725 14.9581 26 14 26C6.26801 26 0 24.2091 0 22V4C0 1.79086 6.26801 0 14 0C21.732 0 28 1.79086 28 4V12.2C27.3538 12.0689 26.6849 12 26 12C24.0582 12 22.2456 12.5535 20.7115 13.5113C18.7188 13.8229 16.4318 14 14 14C8.90735 14 4.44979 13.2231 2 12.0615V16C2.47483 16.2862 3.49851 16.5776 4.42785 16.8421L4.64995 16.9054C7.06615 17.5957 10.4115 18 14 18C14.9798 18 15.9415 17.9699 16.871 17.912C16.5813 18.558 16.3581 19.2404 16.2102 19.9504C15.4903 19.9831 14.7521 20 14 20C8.90735 20 4.44979 19.2231 2 18.0615L2.00024 22ZM2 6.06148V10C2.47483 10.2862 3.49853 10.5776 4.42787 10.8421L4.64995 10.9054C7.06615 11.5957 10.4115 12 14 12C17.5885 12 20.9338 11.5957 23.3501 10.9054L23.5863 10.8381C24.5124 10.5746 25.5311 10.2848 26.0035 10H26V6.06148C23.5502 7.2231 19.0927 8 14 8C8.90735 8 4.44979 7.2231 2 6.06148ZM23.3501 3.09462C20.9338 2.40428 17.5885 2 14 2C10.4115 2 7.06615 2.40428 4.64995 3.09462C3.6667 3.37555 2.89023 3.69073 2.37721 4C2.89023 4.30927 3.6667 4.62445 4.64995 4.90538C7.06615 5.59572 10.4115 6 14 6C17.5885 6 20.9338 5.59572 23.3501 4.90538C24.3333 4.62445 25.1098 4.30927 25.6228 4C25.1098 3.69073 24.3333 3.37555 23.3501 3.09462Z" fill="#343741"/>
<path d="M22 28.5C22 27.675 22.675 27 23.5 27C24.325 27 25 27.675 25 28.5C25 29.325 24.325 30 23.5 30C22.675 30 22 29.325 22 28.5Z" fill="#343741"/>
<path d="M19 16.5V15H21.475L22.15 16.5H33.25C33.7 16.5 34 16.8 34 17.25C34 17.4 34 17.475 33.85 17.625L31.15 22.5C30.925 22.95 30.475 23.25 29.875 23.25H24.325L23.65 24.525V24.6C23.65 24.675 23.725 24.75 23.8 24.75H32.5V26.25H23.5C22.675 26.25 22 25.575 22 24.75C22 24.525 22.075 24.225 22.15 24L23.2 22.2L20.5 16.5H19Z" fill="#343741"/>
<path d="M29.5 28.5C29.5 27.675 30.175 27 31 27C31.825 27 32.5 27.675 32.5 28.5C32.5 29.325 31.825 30 31 30C30.175 30 29.5 29.325 29.5 28.5Z" fill="#343741"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,137 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { action } from '@storybook/addon-actions';
import { SampleDataSet } from '@kbn/home-sample-data-types';
import previewImagePath from './dashboard.png';
import darkPreviewImagePath from './dashboard_dark.png';
import iconPath from './icon.svg';
import { Services } from '../services';
/**
* A set of e-commerce images for use in Storybook stories.
*/
export const ecommerceImages = { previewImagePath, darkPreviewImagePath, iconPath };
/**
* A mocked sample data set for use in Storybook stories.
*/
export const mockDataSet: SampleDataSet = {
darkPreviewImagePath,
defaultIndex: 'default-index',
iconPath,
id: 'sample-data-set',
overviewDashboard: 'overview-dashboard',
previewImagePath,
appLinks: [
{
icon: 'visLine',
label: 'View in App',
path: 'path-to-app',
},
],
name: 'Sample Data Set',
description: 'This is a sample data set you can use.',
status: 'not_installed',
statusMsg: 'optional status message',
};
/**
* Customize the Sample Data Set mock.
*/
export const getMockDataSet = (params: Partial<SampleDataSet> = {}) => ({
...mockDataSet,
...params,
});
/**
* Parameters drawn from the Storybook arguments collection that customize a component story.
*/
export type Params = Record<keyof ReturnType<typeof getStoryArgTypes>, any>;
/**
* Returns Storybook-compatible service abstractions for the `SampleDataCard` Provider.
*/
export const getStoryServices = (params: Params) => {
const { simulateErrors } = params;
const services: Services = {
...params,
addBasePath: (path) => {
action('addBasePath')(path);
return path;
},
getAppNavigationHandler: (path) => () => action('getAppNavigationHandler')(path),
installSampleDataSet: async (id, defaultIndex) => {
if (simulateErrors) {
throw new Error('Error on install');
}
action('installSampleDataSet')(id, defaultIndex);
},
notifyError: action('notifyError'),
notifySuccess: action('notifySuccess'),
removeSampleDataSet: async (id) => {
if (simulateErrors) {
throw new Error('Error on uninstall');
}
action('removeSampleDataSet')(id);
},
};
return services;
};
/**
* Returns the Storybook arguments for `SampleDataCard`, for its stories and for
* consuming component stories.
*/
export const getStoryArgTypes = () => ({
name: {
control: {
type: 'text',
},
defaultValue: mockDataSet.name,
},
description: {
control: {
type: 'text',
},
defaultValue: mockDataSet.description,
},
status: {
options: ['not_installed', 'installed', undefined],
control: { type: 'radio' },
defaultValue: mockDataSet.status,
},
includeAppLinks: {
control: 'boolean',
defaultValue: true,
},
simulateErrors: {
control: 'boolean',
defaultValue: false,
},
});
/**
* Returns the Jest-compatible service abstractions for the `NoDataCard` Provider.
*/
export const getMockServices = (params: Partial<Services> = {}) => {
const services: Services = {
addBasePath: (path) => path,
getAppNavigationHandler: jest.fn(),
installSampleDataSet: jest.fn(),
notifyError: jest.fn(),
notifySuccess: jest.fn(),
removeSampleDataSet: jest.fn(),
...params,
};
return services;
};

View file

@ -0,0 +1,52 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiCard } from '@elastic/eui';
import type { SampleDataSet, InstalledStatus } from '@kbn/home-sample-data-types';
import { INSTALLED_STATUS } from './constants';
import { Footer } from './footer';
export interface Props {
/** A Sample Data Set to display. */
sampleDataSet: SampleDataSet;
/** A resolved, themed image to display in the card. */
imagePath: string;
/** A handler to invoke when the status of a Sample Data Set is changed. */
onStatusChange: (id: string, status: InstalledStatus) => void;
}
/**
* A pure implementation of the `SampleDataCard` component that itself
* does not depend on any Kibana services. Still requires a
* `SampleDataCardProvider` for its dependencies to render and function.
*/
export const SampleDataCard = ({
sampleDataSet,
imagePath: image,
onStatusChange: onAction,
}: Props) => {
const { name: title, description, id } = sampleDataSet;
const betaBadgeProps = {
label: sampleDataSet.status === INSTALLED_STATUS ? INSTALLED_STATUS : null,
};
const footer = <Footer {...{ sampleDataSet, onAction }} />;
return (
<EuiCard
textAlign="left"
paddingSize="m"
data-test-subj={`sampleDataSetCard${id}`}
{...{ image, title, description, betaBadgeProps, footer }}
/>
);
};

View file

@ -0,0 +1,49 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import type { SampleDataSet } from '@kbn/home-sample-data-types';
import { Params, getStoryArgTypes, getStoryServices, mockDataSet } from './mocks';
import { SampleDataCardProvider } from './services';
import { SampleDataCard } from './sample_data_card';
import mdx from '../README.mdx';
export default {
title: 'Sample Data/Card',
description:
'A card describing a Sample Data Set, with options to install it, remove it, or see its saved objects.',
parameters: {
docs: {
page: mdx,
},
},
decorators: [(Story) => <div style={{ width: '433px', padding: '25px' }}>{Story()}</div>],
} as ComponentMeta<typeof SampleDataCard>;
const argTypes = getStoryArgTypes();
export const Card = (params: Params) => {
const { includeAppLinks, ...rest } = params;
const sampleDataSet: SampleDataSet = {
...mockDataSet,
...rest,
appLinks: includeAppLinks ? mockDataSet.appLinks : [],
};
return (
<SampleDataCardProvider {...getStoryServices(params)}>
<SampleDataCard sampleDataSet={sampleDataSet} onStatusChange={action('onStatusChange')} />
</SampleDataCardProvider>
);
};
Card.argTypes = argTypes;

View file

@ -0,0 +1,88 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { renderWithIntl, mountWithIntl } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import { SampleDataCard } from './sample_data_card';
import { SampleDataCardProvider } from './services';
import { getMockServices, getMockDataSet } from './mocks';
import { Services } from './services';
import { INSTALLED_STATUS, UNINSTALLED_STATUS } from './constants';
describe('SampleDataCard', () => {
const onStatusChange = jest.fn();
const sampleDataSet = getMockDataSet();
beforeAll(() => jest.resetAllMocks());
const render = (element: React.ReactElement, services: Partial<Services> = {}) =>
renderWithIntl(
<SampleDataCardProvider {...getMockServices(services)}>{element}</SampleDataCardProvider>
);
const mount = (element: React.ReactElement, services: Partial<Services> = {}) =>
mountWithIntl(
<SampleDataCardProvider {...getMockServices(services)}>{element}</SampleDataCardProvider>
);
describe('not installed', () => {
test('renders', () => {
const component = render(<SampleDataCard {...{ sampleDataSet, onStatusChange }} />);
expect(component).toMatchSnapshot();
});
test('installs', async () => {
const component = mount(<SampleDataCard {...{ sampleDataSet, onStatusChange }} />);
await act(async () => {
component
.find(`button[data-test-subj="addSampleDataSet${sampleDataSet.id}"]`)
.simulate('click');
});
expect(onStatusChange).toHaveBeenCalledWith(sampleDataSet.id, INSTALLED_STATUS);
});
});
describe('installed', () => {
test('renders with app links', () => {
const component = render(
<SampleDataCard
sampleDataSet={getMockDataSet({ status: 'installed' })}
onStatusChange={onStatusChange}
/>
);
expect(component).toMatchSnapshot();
});
test('renders without app links', () => {
const component = render(
<SampleDataCard
sampleDataSet={getMockDataSet({ status: 'installed', appLinks: [] })}
onStatusChange={onStatusChange}
/>
);
expect(component).toMatchSnapshot();
});
test('removes', async () => {
const component = mount(
<SampleDataCard
sampleDataSet={getMockDataSet({ status: 'installed', appLinks: [] })}
onStatusChange={onStatusChange}
/>
);
await act(async () => {
component
.find(`button[data-test-subj="removeSampleDataSet${sampleDataSet.id}"]`)
.simulate('click');
});
expect(onStatusChange).toHaveBeenCalledWith(sampleDataSet.id, UNINSTALLED_STATUS);
});
});
});

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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useMemo } from 'react';
import { useEuiTheme } from '@elastic/eui';
import type { SampleDataSet } from '@kbn/home-sample-data-types';
import { useServices } from './services';
import { SampleDataCard as Component, Props as ComponentProps } from './sample_data_card.component';
/**
* Props for the `SampleDataCard` component.
*/
export interface Props extends Pick<ComponentProps, 'onStatusChange'> {
/** A Sample Data Set to display. */
sampleDataSet: SampleDataSet;
}
/**
* A card representing a Sample Data Set that can be installed. Uses Kibana services to
* display and install the data set. Requires a `SampleDataCardProvider` to render and
* function.
*/
export const SampleDataCard = ({ sampleDataSet, onStatusChange }: Props) => {
const { addBasePath } = useServices();
const { colorMode } = useEuiTheme();
const { darkPreviewImagePath, previewImagePath } = sampleDataSet;
const path =
colorMode === 'DARK' && darkPreviewImagePath ? darkPreviewImagePath : previewImagePath;
const imagePath = useMemo(() => addBasePath(path), [addBasePath, path]);
return <Component {...{ sampleDataSet, imagePath, onStatusChange }} />;
};

View file

@ -0,0 +1,159 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC, MouseEventHandler, useContext } from 'react';
import { EuiGlobalToastListToast as EuiToast } from '@elastic/eui';
import { SAMPLE_DATA_API } from './constants';
type NavigateToUrl = (url: string) => Promise<void> | void;
type UnmountCallback = () => void;
type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
type ValidNotifyString = string | MountPoint<HTMLElement>;
type NotifyInputFields = Pick<EuiToast, Exclude<keyof EuiToast, 'id' | 'text' | 'title'>> & {
title?: ValidNotifyString;
text?: ValidNotifyString;
};
type NotifyInput = string | NotifyInputFields;
type NotifyFn = (notification: NotifyInput) => void;
/**
* A list of services that are consumed by this component.
*/
export interface Services {
addBasePath: (path: string) => string;
getAppNavigationHandler: (path: string) => MouseEventHandler;
installSampleDataSet: (id: string, defaultIndex: string) => Promise<void>;
notifyError: NotifyFn;
notifySuccess: NotifyFn;
removeSampleDataSet: (id: string, defaultIndex: string) => Promise<void>;
}
const Context = React.createContext<Services | null>(null);
/**
* A Context Provider that provides services to the component and its dependencies.
*/
export const SampleDataCardProvider: FC<Services> = ({ children, ...services }) => {
const {
addBasePath,
getAppNavigationHandler,
installSampleDataSet,
notifyError,
notifySuccess,
removeSampleDataSet,
} = services;
return (
<Context.Provider
value={{
addBasePath,
getAppNavigationHandler,
installSampleDataSet,
notifyError,
notifySuccess,
removeSampleDataSet,
}}
>
{children}
</Context.Provider>
);
};
export interface KibanaDependencies {
coreStart: {
application: {
navigateToUrl: NavigateToUrl;
};
http: {
basePath: {
prepend: (path: string) => string;
};
delete: (path: string) => Promise<unknown>;
post: (path: string) => Promise<unknown>;
};
notifications: {
toasts: {
addDanger: NotifyFn;
addSuccess: NotifyFn;
};
};
uiSettings: {
get: (key: string, defaultOverride?: any) => any;
isDefault: (key: string) => boolean;
set: (key: string, value: any) => Promise<boolean>;
};
};
dataViews: {
clearCache: () => void;
};
}
/**
* Kibana-specific Provider that maps dependencies to services.
*/
export const SampleDataCardKibanaProvider: FC<KibanaDependencies> = ({
children,
...dependencies
}) => {
const { application, http, notifications, uiSettings } = dependencies.coreStart;
const clearDataViewsCache = dependencies.dataViews.clearCache;
const value: Services = {
addBasePath: http.basePath.prepend,
getAppNavigationHandler: (targetUrl) => (event) => {
if (event.altKey || event.metaKey || event.ctrlKey) {
return;
}
event.preventDefault();
application.navigateToUrl(http.basePath.prepend(targetUrl));
},
installSampleDataSet: async (id, defaultIndex) => {
await http.post(`${SAMPLE_DATA_API}/${id}`);
if (uiSettings.isDefault('defaultIndex')) {
uiSettings.set('defaultIndex', defaultIndex);
}
clearDataViewsCache();
},
removeSampleDataSet: async (id, defaultIndex) => {
await http.delete(`${SAMPLE_DATA_API}/${id}`);
if (
!uiSettings.isDefault('defaultIndex') &&
uiSettings.get('defaultIndex') === defaultIndex
) {
uiSettings.set('defaultIndex', null);
}
clearDataViewsCache();
},
notifyError: (input) => notifications.toasts.addDanger(input),
notifySuccess: (input) => notifications.toasts.addSuccess(input),
};
return <Context.Provider {...{ value }}>{children}</Context.Provider>;
};
/**
* React hook for accessing pre-wired services.
*/
export function useServices() {
const context = useContext(Context);
if (!context) {
throw new Error(
'SampleDataCard Context is missing. Ensure your component or React root is wrapped with SampleDataCardContext.'
);
}
return context;
}

View file

@ -0,0 +1,19 @@
{
"extends": "../../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"rootDir": "src",
"stripInternal": false,
"types": [
"jest",
"node",
"react",
"@kbn/ambient-ui-types"
]
},
"include": [
"src/**/*"
]
}