New visualization type selection (#23833) (#26085)

* First version of new visualization selection

* Extract some components

* Remove visualization category

* Remove old wizard code

* Fix i18n ids

* Fix tests

* Fix tag cloud tests

* Fix broken test method

* Fix wrong method call

* Fix TSVB navigation in tests

* Restructure components

* Fix for lab removal

* Add tests

* Timroes/eui vis type selection (#4)

* Added background graphic from welcome screen to modal

* Fixed up responsiveness

* Change wording

* Fix test snapshot

* Create VisTypeIcon

* Implement suggestions

* Change experimental wording

* Use regular quotes for i18n engine
This commit is contained in:
Tim Roes 2018-11-22 13:51:12 +01:00 committed by GitHub
parent fa213f36b1
commit d9b1c0b626
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1620 additions and 440 deletions

View file

@ -31,7 +31,6 @@ You should also register the visualization with `VisTypesRegistryProvider`.
["source","js"]
-----------
import { CATEGORY } from 'ui/vis/vis_category';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
@ -43,7 +42,6 @@ const MyNewVisType = (Private) => {
title: 'My New Vis',
icon: 'my_icon',
description: 'Cool new chart',
category: CATEGORY.OTHER
...
});
}
@ -59,7 +57,7 @@ The list of common parameters:
- *image*: instead of an icon you can provide a SVG image (imported)
- *legacyIcon*: (DEPRECATED) <string> provide a class name (e.g. for a font awesome icon)
- *description*: description of your visualization as shown in kibana
- *category*: the category your visualization falls into (one of `ui/vis/vis_category` values)
- *hidden*: <bool> if set to true, will hide the type from showing up in the visualization wizard
- *visConfig*: object holding visualization parameters
- *visConfig.defaults*: object holding default visualization configuration
- *visualization*: A constructor function for a Visualization.

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { CATEGORY } from 'ui/vis/vis_category';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { VisController } from './vis_controller';
@ -40,7 +39,6 @@ function InputControlVisProvider(Private) {
description: i18n.translate('inputControl.register.controlsDescription', {
defaultMessage: 'Create interactive controls for easy dashboard manipulation.'
}),
category: CATEGORY.OTHER,
stage: 'experimental',
requiresUpdateStatus: [Status.PARAMS, Status.TIME],
feedbackMessage: defaultFeedbackMessage,

View file

@ -19,7 +19,6 @@
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { CATEGORY } from 'ui/vis/vis_category';
import pointSeriesTemplate from './editors/point_series.html';
export default function PointSeriesVisType(Private, i18n) {
@ -30,7 +29,6 @@ export default function PointSeriesVisType(Private, i18n) {
title: i18n('kbnVislibVisTypes.area.areaTitle', { defaultMessage: 'Area' }),
icon: 'visArea',
description: i18n('kbnVislibVisTypes.area.areaDescription', { defaultMessage: 'Emphasize the quantity beneath a line chart' }),
category: CATEGORY.BASIC,
visConfig: {
defaults: {
type: 'area',

View file

@ -19,7 +19,6 @@
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { CATEGORY } from 'ui/vis/vis_category';
import gaugeTemplate from './editors/gauge.html';
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
@ -33,7 +32,6 @@ export default function GaugeVisType(Private, i18n) {
description: i18n('kbnVislibVisTypes.gauge.gaugeDescription', {
defaultMessage: 'Gauges indicate the status of a metric. Use it to show how a metric\'s value relates to reference threshold values.'
}),
category: CATEGORY.DATA,
visConfig: {
defaults: {
type: 'gauge',

View file

@ -19,7 +19,6 @@
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { CATEGORY } from 'ui/vis/vis_category';
import gaugeTemplate from './editors/gauge.html';
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
@ -33,7 +32,6 @@ export default function GoalVisType(Private, i18n) {
description: i18n('kbnVislibVisTypes.goal.goalDescription', {
defaultMessage: 'A goal chart indicates how close you are to your final goal.'
}),
category: CATEGORY.DATA,
visConfig: {
defaults: {
addTooltip: true,

View file

@ -19,7 +19,6 @@
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { CATEGORY } from 'ui/vis/vis_category';
import heatmapTemplate from './editors/heatmap.html';
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
@ -31,7 +30,6 @@ export default function HeatmapVisType(Private, i18n) {
title: i18n('kbnVislibVisTypes.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }),
icon: 'visHeatmap',
description: i18n('kbnVislibVisTypes.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix' }),
category: CATEGORY.BASIC,
visConfig: {
defaults: {
type: 'heatmap',

View file

@ -19,7 +19,6 @@
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { CATEGORY } from 'ui/vis/vis_category';
import pointSeriesTemplate from './editors/point_series.html';
export default function PointSeriesVisType(Private, i18n) {
@ -32,7 +31,6 @@ export default function PointSeriesVisType(Private, i18n) {
description: i18n('kbnVislibVisTypes.histogram.histogramDescription',
{ defaultMessage: 'Assign a continuous variable to each axis' }
),
category: CATEGORY.BASIC,
visConfig: {
defaults: {
type: 'histogram',

View file

@ -19,7 +19,6 @@
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { CATEGORY } from 'ui/vis/vis_category';
import pointSeriesTemplate from './editors/point_series.html';
export default function PointSeriesVisType(Private, i18n) {
@ -32,7 +31,6 @@ export default function PointSeriesVisType(Private, i18n) {
description: i18n('kbnVislibVisTypes.horizontalBar.horizontalBarDescription',
{ defaultMessage: 'Assign a continuous variable to each axis' }
),
category: CATEGORY.BASIC,
visConfig: {
defaults: {
type: 'histogram',

View file

@ -19,7 +19,6 @@
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { CATEGORY } from 'ui/vis/vis_category';
import pointSeriesTemplate from './editors/point_series.html';
export default function PointSeriesVisType(Private, i18n) {
@ -30,7 +29,6 @@ export default function PointSeriesVisType(Private, i18n) {
title: i18n('kbnVislibVisTypes.line.lineTitle', { defaultMessage: 'Line' }),
icon: 'visLine',
description: i18n('kbnVislibVisTypes.line.lineDescription', { defaultMessage: 'Emphasize trends' }),
category: CATEGORY.BASIC,
visConfig: {
defaults: {
type: 'line',

View file

@ -19,7 +19,6 @@
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { CATEGORY } from 'ui/vis/vis_category';
import pieTemplate from './editors/pie.html';
export default function HistogramVisType(Private, i18n) {
@ -30,7 +29,6 @@ export default function HistogramVisType(Private, i18n) {
title: i18n('kbnVislibVisTypes.pie.pieTitle', { defaultMessage: 'Pie' }),
icon: 'visPie',
description: i18n('kbnVislibVisTypes.pie.pieDescription', { defaultMessage: 'Compare parts of a whole' }),
category: CATEGORY.BASIC,
visConfig: {
defaults: {
type: 'pie',

View file

@ -37,7 +37,6 @@ import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
import { DocTitleProvider } from 'ui/doc_title';
import { getTopNavConfig } from './top_nav/get_top_nav_config';
import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
import { VisualizeConstants } from '../visualize/visualize_constants';
import { DashboardStateManager } from './dashboard_state_manager';
import { saveDashboard } from './lib';
import { showCloneModal } from './top_nav/show_clone_modal';
@ -45,6 +44,7 @@ import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
import { DashboardSaveModal } from './top_nav/save_modal';
import { showAddPanel } from './top_nav/show_add_panel';
import { showOptionsPopover } from './top_nav/show_options_popover';
import { showNewVisModal } from '../visualize/wizard';
import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share';
import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
import * as filterActions from 'ui/doc_table/actions/filter';
@ -417,10 +417,7 @@ app.directive('dashboardApp', function ($injector) {
};
navActions[TopNavIds.ADD] = () => {
const addNewVis = () => {
kbnUrl.change(
`${VisualizeConstants.WIZARD_STEP_1_PAGE_PATH}?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`);
// Function is called outside of angular. Must apply digest cycle to trigger URL update
$scope.$apply();
showNewVisModal(visTypes, { editorParams: [DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM] });
};
showAddPanel(dashboardStateManager.addNewPanel, addNewVis, visTypes);

View file

@ -40,6 +40,21 @@ uiRoutes
template: visualizeListingTemplate,
controller: VisualizeListingController,
controllerAs: 'listingController',
resolve: {
createNewVis: () => false,
},
})
.when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, {
template: visualizeListingTemplate,
controller: VisualizeListingController,
controllerAs: 'listingController',
resolve: {
createNewVis: () => true,
},
})
// Old path, will be removed in 7.0
.when('/visualize/step/1', {
redirectTo: VisualizeConstants.WIZARD_STEP_1_PAGE_PATH,
});
FeatureCatalogueRegistryProvider.register(() => {

View file

@ -33,6 +33,13 @@
<visualize-listing-table
fetch-items="listingController.fetchItems"
delete-selected-items="listingController.deleteSelectedItems"
on-create-vis="listingController.createNewVis"
></visualize-listing-table>
<new-vis-modal
is-open="listingController.showNewVisModal"
on-close="listingController.closeNewVisModal"
vis-types-registry="listingController.visTypeRegistry"
></new-vis-modal>
</div>

View file

@ -22,24 +22,44 @@ import 'ui/pager_control';
import 'ui/pager';
import { uiModules } from 'ui/modules';
import { timefilter } from 'ui/timefilter';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
import { VisualizeListingTable } from './visualize_listing_table';
import { NewVisModal } from '../wizard/new_vis_modal';
import { injectI18nProvider } from '@kbn/i18n/react';
const app = uiModules.get('app/visualize', ['ngRoute', 'react']);
app.directive('visualizeListingTable', function (reactDirective) {
return reactDirective(VisualizeListingTable);
});
app.directive('visualizeListingTable', reactDirective => reactDirective(VisualizeListingTable));
app.directive('newVisModal', reactDirective => reactDirective(injectI18nProvider(NewVisModal)));
export function VisualizeListingController($injector) {
export function VisualizeListingController($injector, createNewVis) {
const Notifier = $injector.get('Notifier');
const Private = $injector.get('Private');
const config = $injector.get('config');
this.visTypeRegistry = Private(VisTypesRegistryProvider);
timefilter.disableAutoRefreshSelector();
timefilter.disableTimeRangeSelector();
this.showNewVisModal = false;
this.createNewVis = () => {
this.showNewVisModal = true;
};
this.closeNewVisModal = () => {
this.showNewVisModal = false;
};
if (createNewVis) {
// In case the user navigated to the page via the /visualize/new URL we start the dialog immediately
this.createNewVis();
}
// TODO: Extract this into an external service.
const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName;
const visualizationService = services.visualizations;

View file

@ -260,14 +260,14 @@ export class VisualizeListingTable extends Component {
this.setState({ selectedRowIds: newSelectedIds });
};
onCreate() {
window.location = '#/visualize/new';
onCreate = () => {
this.props.onCreateVis();
}
renderToolBarActions() {
return this.state.selectedRowIds.length > 0 ?
<KuiListingTableDeleteButton onDelete={this.onDelete} aria-label="Delete selected visualizations"/> :
<KuiListingTableCreateButton onCreate={this.onCreate} aria-label="Create new visualization"/>;
<KuiListingTableCreateButton onCreate={this.onCreate} data-test-subj="createNewVis" aria-label="Create new visualization"/>;
}
renderPager() {
@ -323,4 +323,5 @@ export class VisualizeListingTable extends Component {
VisualizeListingTable.propTypes = {
deleteSelectedItems: PropTypes.func,
fetchItems: PropTypes.func,
onCreateVis: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,768 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NewVisModal should render as expected 1`] = `
<I18nProvider
intl={
Object {
"defaultFormats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
>
<IntlProvider
defaultFormats={
Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
}
}
defaultLocale="en"
locale="en"
messages={Object {}}
textComponent={Symbol(react.fragment)}
>
<NewVisModal
editorParams={Array []}
isOpen={true}
onClose={[Function]}
visTypesRegistry={
Array [
Object {
"hidden": false,
"name": "vis",
"requestHandler": "none",
"requiresSearch": false,
"responseHandler": "none",
"stage": "production",
"title": "Vis Type 1",
"visualization": [Function],
},
Object {
"hidden": false,
"name": "visExp",
"requestHandler": "none",
"requiresSearch": false,
"responseHandler": "none",
"stage": "experimental",
"title": "Experimental Vis",
"visualization": [Function],
},
Object {
"hidden": false,
"name": "visWithSearch",
"requestHandler": "none",
"requiresSearch": false,
"responseHandler": "none",
"stage": "production",
"title": "Vis with search",
"visualization": [Function],
},
]
}
>
<EuiOverlayMask>
<EuiModal
className="visNewVisDialog"
maxWidth="100vw"
onClose={[Function]}
>
<FocusTrap
_createFocusTrap={[Function]}
active={true}
focusTrapOptions={
Object {
"fallbackFocus": [Function],
"initialFocus": undefined,
}
}
paused={false}
tag="div"
>
<div>
<div
className="euiModal visNewVisDialog"
onKeyDown={[Function]}
style={
Object {
"maxWidth": "100vw",
}
}
tabIndex={0}
>
<EuiButtonIcon
aria-label="Closes this modal window"
className="euiModal__closeIcon"
color="text"
iconSize="m"
iconType="cross"
onClick={[Function]}
type="button"
>
<button
aria-label="Closes this modal window"
className="euiButtonIcon euiButtonIcon--text euiModal__closeIcon"
onClick={[Function]}
type="button"
>
<EuiIcon
aria-hidden="true"
className="euiButtonIcon__icon"
size="m"
type="cross"
>
<cross
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={
Object {
"fill": undefined,
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiButtonIcon__icon"
focusable="false"
height="16"
style={
Object {
"fill": undefined,
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M7.293 8l-4.147 4.146a.5.5 0 0 0 .708.708L8 8.707l4.146 4.147a.5.5 0 0 0 .708-.708L8.707 8l4.147-4.146a.5.5 0 0 0-.708-.708L8 7.293 3.854 3.146a.5.5 0 1 0-.708.708L7.293 8z"
id="cross-a"
/>
</defs>
<use
fillRule="nonzero"
xlinkHref="#cross-a"
/>
</svg>
</cross>
</EuiIcon>
</button>
</EuiButtonIcon>
<div
className="euiModal__flex"
>
<TypeSelection
onVisTypeSelected={[Function]}
visTypesRegistry={
Array [
Object {
"hidden": false,
"name": "vis",
"requestHandler": "none",
"requiresSearch": false,
"responseHandler": "none",
"stage": "production",
"title": "Vis Type 1",
"visualization": [Function],
},
Object {
"hidden": false,
"name": "visExp",
"requestHandler": "none",
"requiresSearch": false,
"responseHandler": "none",
"stage": "experimental",
"title": "Experimental Vis",
"visualization": [Function],
},
Object {
"hidden": false,
"name": "visWithSearch",
"requestHandler": "none",
"requiresSearch": false,
"responseHandler": "none",
"stage": "production",
"title": "Vis with search",
"visualization": [Function],
},
]
}
>
<EuiModalHeader>
<div
className="euiModalHeader"
>
<EuiModalHeaderTitle>
<div
className="euiModalHeader__title"
>
<FormattedMessage
defaultMessage="New Visualization"
id="kbn.visualize.newVisWizard.title"
values={Object {}}
>
New Visualization
</FormattedMessage>
</div>
</EuiModalHeaderTitle>
</div>
</EuiModalHeader>
<div
className="visNewVisDialog__body"
>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="xl"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<EuiFlexItem
component="div"
grow={true}
>
<div
className="euiFlexItem"
>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="column"
gutterSize="none"
justifyContent="flexStart"
responsive={false}
wrap={false}
>
<div
className="euiFlexGroup euiFlexGroup--directionColumn"
>
<EuiFlexItem
className="visNewVisDialog__searchWrapper"
component="div"
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero visNewVisDialog__searchWrapper"
>
<EuiFieldSearch
compressed={false}
fullWidth={true}
incremental={false}
isLoading={false}
onChange={[Function]}
placeholder="Filter"
value=""
>
<EuiFormControlLayout
compressed={false}
fullWidth={true}
icon="search"
isLoading={false}
>
<div
className="euiFormControlLayout euiFormControlLayout--fullWidth"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<EuiValidatableControl>
<input
className="euiFieldSearch euiFieldSearch--fullWidth"
onChange={[Function]}
onKeyUp={[Function]}
placeholder="Filter"
type="search"
value=""
/>
</EuiValidatableControl>
<EuiFormControlLayoutIcons
icon="search"
isLoading={false}
>
<div
className="euiFormControlLayoutIcons"
>
<EuiFormControlLayoutCustomIcon
type="search"
>
<span
className="euiFormControlLayoutCustomIcon"
>
<EuiIcon
aria-hidden="true"
className="euiFormControlLayoutCustomIcon__icon"
size="m"
type="search"
>
<search
aria-hidden="true"
className="euiIcon euiIcon--medium euiFormControlLayoutCustomIcon__icon"
focusable="false"
height="16"
style={
Object {
"fill": undefined,
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiFormControlLayoutCustomIcon__icon"
focusable="false"
height="16"
style={
Object {
"fill": undefined,
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M11.271 11.978l3.872 3.873a.502.502 0 0 0 .708 0 .502.502 0 0 0 0-.708l-3.565-3.564c2.38-2.747 2.267-6.923-.342-9.532-2.73-2.73-7.17-2.73-9.898 0-2.728 2.729-2.728 7.17 0 9.9a6.955 6.955 0 0 0 4.949 2.05.5.5 0 0 0 0-1 5.96 5.96 0 0 1-4.242-1.757 6.01 6.01 0 0 1 0-8.486c2.337-2.34 6.143-2.34 8.484 0a6.01 6.01 0 0 1 0 8.486.5.5 0 0 0 .034.738z"
id="search-a"
/>
</defs>
<use
xlinkHref="#search-a"
/>
</svg>
</search>
</EuiIcon>
</span>
</EuiFormControlLayoutCustomIcon>
</div>
</EuiFormControlLayoutIcons>
</div>
</div>
</EuiFormControlLayout>
</EuiFieldSearch>
</div>
</EuiFlexItem>
<EuiFlexItem
className="visNewVisDialog__typesWrapper"
component="div"
grow={1}
>
<div
className="euiFlexItem euiFlexItem--flexGrow1 visNewVisDialog__typesWrapper"
>
<EuiKeyPadMenu
className="visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
<div
className="euiKeyPadMenu visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
role="menu"
>
<EuiKeyPadMenuItemButton
className="visNewVisDialog__type"
data-test-subj="visType-vis"
data-vis-stage="production"
disabled={false}
key="vis"
label={
<span
data-test-subj="visTypeTitle"
>
Vis Type 1
</span>
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-vis"
data-vis-stage="production"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
type="button"
>
<div
className="euiKeyPadMenuItem__inner"
>
<div
className="euiKeyPadMenuItem__icon"
>
<Component
visType={
Object {
"hidden": false,
"highlighted": false,
"name": "vis",
"requestHandler": "none",
"requiresSearch": false,
"responseHandler": "none",
"stage": "production",
"title": "Vis Type 1",
"visualization": [Function],
}
}
>
<EuiIcon
aria-hidden="true"
color="secondary"
size="l"
type="empty"
>
<empty
aria-hidden="true"
className="euiIcon euiIcon--large euiIcon--secondary"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--large euiIcon--secondary"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
</empty>
</EuiIcon>
</Component>
</div>
<p
className="euiKeyPadMenuItem__label"
>
<span
data-test-subj="visTypeTitle"
>
Vis Type 1
</span>
</p>
</div>
</button>
</EuiKeyPadMenuItemButton>
<EuiKeyPadMenuItemButton
className="visNewVisDialog__type"
data-test-subj="visType-visWithSearch"
data-vis-stage="production"
disabled={false}
key="visWithSearch"
label={
<span
data-test-subj="visTypeTitle"
>
Vis with search
</span>
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-visWithSearch"
data-vis-stage="production"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
type="button"
>
<div
className="euiKeyPadMenuItem__inner"
>
<div
className="euiKeyPadMenuItem__icon"
>
<Component
visType={
Object {
"hidden": false,
"highlighted": false,
"name": "visWithSearch",
"requestHandler": "none",
"requiresSearch": false,
"responseHandler": "none",
"stage": "production",
"title": "Vis with search",
"visualization": [Function],
}
}
>
<EuiIcon
aria-hidden="true"
color="secondary"
size="l"
type="empty"
>
<empty
aria-hidden="true"
className="euiIcon euiIcon--large euiIcon--secondary"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--large euiIcon--secondary"
focusable="false"
height="16"
style={null}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
</empty>
</EuiIcon>
</Component>
</div>
<p
className="euiKeyPadMenuItem__label"
>
<span
data-test-subj="visTypeTitle"
>
Vis with search
</span>
</p>
</div>
</button>
</EuiKeyPadMenuItemButton>
</div>
</EuiKeyPadMenu>
</div>
</EuiFlexItem>
</div>
</EuiFlexGroup>
</div>
</EuiFlexItem>
<EuiFlexItem
className="visNewVisDialog__description"
component="div"
grow={false}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero visNewVisDialog__description"
>
<EuiTitle
size="s"
textTransform="none"
>
<h2
className="euiTitle euiTitle--small"
>
<FormattedMessage
defaultMessage="Select a visualization type"
id="kbn.visualize.newVisWizard.selectVisType"
values={Object {}}
>
Select a visualization type
</FormattedMessage>
</h2>
</EuiTitle>
<EuiSpacer
size="m"
>
<div
className="euiSpacer euiSpacer--m"
/>
</EuiSpacer>
<Component>
<EuiText
grow={true}
size="m"
>
<div
className="euiText euiText--medium"
>
<p>
<FormattedMessage
defaultMessage="Start creating your visualization by selecting a type for that visualization."
id="kbn.visualize.newVisWizard.helpText"
values={Object {}}
>
Start creating your visualization by selecting a type for that visualization.
</FormattedMessage>
</p>
</div>
</EuiText>
</Component>
</div>
</EuiFlexItem>
</div>
</EuiFlexGroup>
</div>
</TypeSelection>
</div>
</div>
</div>
</FocusTrap>
</EuiModal>
</EuiOverlayMask>
</NewVisModal>
</IntlProvider>
</I18nProvider>
`;

View file

@ -0,0 +1,99 @@
.visNewVisDialog {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='166' height='157' viewBox='0 0 166 157'%3E%3Cdefs%3E%3ClinearGradient id='untitled-2-a' x1='0%25' y1='0%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23FFF' stop-opacity='.2'/%3E%3Cstop offset='100%25' stop-opacity='0'/%3E%3C/linearGradient%3E%3CradialGradient id='untitled-2-b' cx='0%25' cy='0%25' r='127.787%25' fx='0%25' fy='0%25' gradientTransform='matrix(.681 .68098 -.63326 .7323 0 0)'%3E%3Cstop offset='0%25' stop-color='%23BBB' stop-opacity='.1'/%3E%3Cstop offset='100%25' stop-opacity='.5'/%3E%3C/radialGradient%3E%3ClinearGradient id='untitled-2-c' x1='0%25' y1='0%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23FFF' stop-opacity='.4'/%3E%3Cstop offset='100%25' stop-opacity='0'/%3E%3C/linearGradient%3E%3CradialGradient id='untitled-2-d' cx='0%25' cy='0%25' r='148.851%25' fx='0%25' fy='0%25' gradientTransform='matrix(.6718 .67182 -.74072 .60932 0 0)'%3E%3Cstop offset='0%25' stop-color='%23FFF' stop-opacity='.101'/%3E%3Cstop offset='100%25' stop-opacity='.15'/%3E%3C/radialGradient%3E%3CradialGradient id='untitled-2-e' cx='0%25' cy='0%25' r='127.349%25' fx='0%25' fy='0%25' gradientTransform='matrix(.68331 .68332 -.73013 .63951 0 0)'%3E%3Cstop offset='0%25' stop-color='%23BBB' stop-opacity='.1'/%3E%3Cstop offset='100%25' stop-opacity='.5'/%3E%3C/radialGradient%3E%3C/defs%3E%3Cg opacity='.5' fill='none' fill-rule='evenodd' transform='matrix(-1 0 0 1 166 0)'%3E%3Cg opacity='.65' transform='matrix(0 -1 -1 0 146 157)'%3E%3Cpolygon fill='%23DD0A73' points='0 0 157 146 0 146' opacity='.418'/%3E%3Cpolygon fill='url(%23untitled-2-a)' points='0 0 157 146 0 146' style='mix-blend-mode:overlay'/%3E%3Cpolygon fill='url(%23untitled-2-b)' points='0 0 157 146 0 146' opacity='.618' style='mix-blend-mode:overlay'/%3E%3C/g%3E%3Cg opacity='.65' transform='translate(88 71)'%3E%3Cpath fill='%23017F75' d='M0,86 L78,86 C74.2038079,48.730962 43.6293886,16.7871605 0,0 L0,86 Z' opacity='.409'/%3E%3Cpath fill='url(%23untitled-2-c)' d='M0,86 L78,86 C74.2038079,48.730962 43.6293886,16.7871605 0,0 L0,86 Z' style='mix-blend-mode:overlay'/%3E%3Cpath fill='url(%23untitled-2-d)' d='M0,86 L78,86 C74.2038079,48.730962 43.6293886,16.7871605 0,0 L0,86 Z' opacity='.663' style='mix-blend-mode:overlay'/%3E%3C/g%3E%3Cg opacity='.15' transform='translate(73 79)'%3E%3Cpolygon fill='%23353535' points='0 0 73 78 0 78' opacity='.38'/%3E%3Cpolygon fill='url(%23untitled-2-a)' points='0 0 73 78 0 78' style='mix-blend-mode:overlay'/%3E%3Cpolygon fill='url(%23untitled-2-e)' points='0 0 73 78 0 78' style='mix-blend-mode:overlay'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A");
background-repeat: no-repeat;
background-position: calc(100% + 1px) calc(100% + 1px);
background-size: 37%;
}
.visNewVisDialog__body {
display: flex;
padding: $euiSizeM $euiSizeL 0;
}
.visNewVisDialog__searchWrapper {
flex-shrink: 0;
}
.visNewVisDialog__typesWrapper {
max-width: $euiSizeXXL * 10;
padding-top: 2px; // Account for search field dropshadow
padding-bottom: $euiSize;
// Add overflow shadows via pseudo elements
position: relative;
&::before,
&::after {
content: "";
display: block;
position: absolute;
height: $euiSizeXXL;
left: 0;
right: 0;
pointer-events: none;
}
&::before {
top: -$euiSizeXXL + 2px; // Account for search field dropshadow
@include euiOverflowShadowBottom;
}
&::after {
bottom: -$euiSizeL;
@include euiOverflowShadowTop;
}
}
.visNewVisDialog__types {
@include euiScrollBar;
// EUITODO: allow for more (calculated) widths of `EuiKeyPadMenu`
width: auto;
overflow-y: auto;
padding-top: $euiSize;
justify-content: center;
padding-bottom: $euiSize;
}
.visNewVisDialog__description {
width: $euiSizeXL * 10;
}
.visNewVisDialog__type:disabled {
opacity: 0.2;
pointer-events: none;
}
.visNewVisDialog__typeLegacyIcon {
font-size: $euiSizeL;
color: $euiColorSecondary;
}
.visNewVisDialog__typeImage {
@include size($euiSizeL);
}
@include euiBreakpoint('xs', 's') {
.visNewVisDialog {
background-image: none;
}
.visNewVisDialog__typesWrapper {
max-width: none;
}
.visNewVisDialog__types {
justify-content: flex-start;
}
.visNewVisDialog__description {
display: none;
}
}
@include internetExplorerOnly {
.visNewVisDialog {
width: 820px;
}
.visNewVisDialog__body {
flex-basis: 800px;
}
}

View file

@ -1 +1,2 @@
@import './wizard';
@import './dialog';

View file

@ -30,19 +30,3 @@
.visWizard__savedObjectFinder {
padding: $euiSizeS; /* 1 */
}
/**
* This preserves backwards-compatibility with any plugins that are still specifying an `icon`
* class.
*
* 1. Size icon correctly to match xxLarge EuiIcon
*/
.visWizard__visTypeIcon {
color: $euiColorFullShade;
font-size: $euiSizeXXL; /* 1 */
}
// SASSTODO: Remove img element selector when `.kuiGalleryItem__image img` selector is removed
img.visWizard__visTypeImage { // Use element selector to override base .kuiGalleryItem__image styles
@include size($euiSizeXXL); /* 1 */
}

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { NewVisModal } from './new_vis_modal';
export { showNewVisModal } from './show_new_vis';

View file

@ -0,0 +1,128 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { I18nProvider } from '@kbn/i18n/react';
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
const settingsGet = jest.fn();
jest.mock('ui/chrome', () => ({
getUiSettingsClient: () => ({
get: settingsGet,
}),
}));
import { NewVisModal } from './new_vis_modal';
import { VisType } from 'ui/vis';
describe('NewVisModal', () => {
const defaultVisTypeParams = {
hidden: false,
visualization: class Controller {
public render = jest.fn();
public destroy = jest.fn();
},
requiresSearch: false,
requestHandler: 'none',
responseHandler: 'none',
};
const visTypes: VisType[] = [
{ name: 'vis', title: 'Vis Type 1', stage: 'production', ...defaultVisTypeParams },
{ name: 'visExp', title: 'Experimental Vis', stage: 'experimental', ...defaultVisTypeParams },
{
name: 'visWithSearch',
title: 'Vis with search',
stage: 'production',
...defaultVisTypeParams,
},
];
it('should render as expected', () => {
const wrapper = mountWithIntl(
<I18nProvider>
<NewVisModal isOpen={true} onClose={() => null} visTypesRegistry={visTypes} />
</I18nProvider>
);
expect(wrapper).toMatchSnapshot();
});
it('should show a button for regular visualizations', () => {
const wrapper = mountWithIntl(
<I18nProvider>
<NewVisModal isOpen={true} onClose={() => null} visTypesRegistry={visTypes} />
</I18nProvider>
);
expect(wrapper.find('[data-test-subj="visType-vis"]').exists()).toBe(true);
});
describe('open editor', () => {
it('should open the editor for visualizations without search', () => {
window.location.assign = jest.fn();
const wrapper = mountWithIntl(
<I18nProvider>
<NewVisModal isOpen={true} onClose={() => null} visTypesRegistry={visTypes} />
</I18nProvider>
);
const visButton = wrapper.find('button[data-test-subj="visType-vis"]');
visButton.simulate('click');
expect(window.location.assign).toBeCalledWith('#/visualize/create?type=vis');
});
it('passes through editor params to the editor URL', () => {
window.location.assign = jest.fn();
const wrapper = mountWithIntl(
<I18nProvider>
<NewVisModal
isOpen={true}
onClose={() => null}
visTypesRegistry={visTypes}
editorParams={['foo=true', 'bar=42']}
/>
</I18nProvider>
);
const visButton = wrapper.find('button[data-test-subj="visType-vis"]');
visButton.simulate('click');
expect(window.location.assign).toBeCalledWith('#/visualize/create?type=vis&foo=true&bar=42');
});
});
describe('experimental visualizations', () => {
it('should not show experimental visualizations if visualize:enableLabs is false', () => {
settingsGet.mockReturnValue(false);
const wrapper = mountWithIntl(
<I18nProvider>
<NewVisModal isOpen={true} onClose={() => null} visTypesRegistry={visTypes} />
</I18nProvider>
);
expect(wrapper.find('[data-test-subj="visType-visExp"]').exists()).toBe(false);
});
it('should show experimental visualizations if visualize:enableLabs is true', () => {
settingsGet.mockReturnValue(true);
const wrapper = mountWithIntl(
<I18nProvider>
<NewVisModal isOpen={true} onClose={() => null} visTypesRegistry={visTypes} />
</I18nProvider>
);
expect(wrapper.find('[data-test-subj="visType-visExp"]').exists()).toBe(true);
});
});
});

View file

@ -0,0 +1,79 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
import { VisualizeConstants } from '../visualize_constants';
import { TypeSelection } from './type_selection';
import chrome from 'ui/chrome';
import { VisType } from 'ui/vis';
interface TypeSelectionProps {
isOpen: boolean;
onClose: () => void;
visTypesRegistry: VisType[];
editorParams?: string[];
}
class NewVisModal extends React.Component<TypeSelectionProps> {
public static defaultProps = {
editorParams: [],
};
private readonly isLabsEnabled: boolean;
constructor(props: TypeSelectionProps) {
super(props);
this.isLabsEnabled = chrome.getUiSettingsClient().get('visualize:enableLabs');
}
public render() {
if (!this.props.isOpen) {
return null;
}
return (
<EuiOverlayMask>
<EuiModal onClose={this.props.onClose} maxWidth={'100vw'} className="visNewVisDialog">
<TypeSelection
showExperimental={this.isLabsEnabled}
onVisTypeSelected={this.onVisTypeSelected}
visTypesRegistry={this.props.visTypesRegistry}
/>
</EuiModal>
</EuiOverlayMask>
);
}
private onVisTypeSelected = (visType: VisType) => {
const baseUrl =
visType.requiresSearch && visType.options.showIndexSelection
? `#${VisualizeConstants.WIZARD_STEP_2_PAGE_PATH}?`
: `#${VisualizeConstants.CREATE_PATH}?`;
const params = [`type=${encodeURIComponent(visType.name)}`, ...this.props.editorParams!];
this.props.onClose();
location.assign(`${baseUrl}${params.join('&')}`);
};
}
export { NewVisModal };

View file

@ -0,0 +1,53 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { VisType } from 'ui/vis';
import { NewVisModal } from './new_vis_modal';
interface ShowNewVisModalParams {
editorParams?: string[];
}
export function showNewVisModal(
visTypeRegistry: VisType[],
{ editorParams = [] }: ShowNewVisModalParams = {}
) {
const container = document.createElement('div');
const onClose = () => {
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
};
document.body.appendChild(container);
const element = (
<I18nProvider>
<NewVisModal
isOpen={true}
onClose={onClose}
visTypesRegistry={visTypeRegistry}
editorParams={editorParams}
/>
</I18nProvider>
);
ReactDOM.render(element, container);
}

View file

@ -1,102 +0,0 @@
<!-- Local nav. -->
<kbn-top-nav name="visualize">
<!-- Transcluded elements. -->
<div data-transclude-slots>
<!-- Breadcrumbs. -->
<bread-crumbs
data-transclude-slot="topLeftCorner"
use-links="true"
omit-current-page="true"
page-title="'New'"
></bread-crumbs>
</div>
</kbn-top-nav>
<div class="kuiViewContent kuiViewContent--constrainedWidth kuiViewContentItem" data-test-subj="visualizeSelectTypePage">
<div class="kuiViewContentItem">
<!-- Header -->
<div class="visualizeViewContentHeader kuiVerticalRhythm kuiVerticalRhythm--medium">
<h1 class="kuiTitle">
Select visualization type
</h1>
</div>
<!-- Search -->
<div class="kuiSearchInput kuiVerticalRhythm kuiVerticalRhythm--medium fullWidth" role="search">
<icon class="kuiSearchInput__icon kuiIcon fa-search"></icon>
<input
class="kuiSearchInput__input"
type="text"
placeholder="Search visualization types..."
ng-model="searchTerm"
>
</div>
<div
class="kuiVerticalRhythm kuiVerticalRhythm--medium"
ng-repeat="category in filteredVisTypeCategories"
>
<!-- Title for each category -->
<h2 class="kuiSubTitle kuiVerticalRhythm">
{{ category.label }}
</h2>
<!-- Gallery of buttons for each vis type -->
<div class="kuiGallery kuiVerticalRhythm">
<a
class="kuiGalleryItem"
ng-repeat="type in category.list"
ng-href="{{ getVisTypeUrl(type) }}"
tooltip="{{ getVisTypeTooltip(type) }}"
tooltip-placement="{{ getVisTypeTooltipPosition($parent.$index) }}"
aria-describedby="visDescription_{{ ::getVisTypeId(type) }}"
data-test-subj="visType-{{::type.name}}"
>
<div class="kuiGalleryItem__image">
<img
class="visWizard__visTypeImage"
ng-if="type.image"
aria-hidden="true"
ng-src="{{ type.image }}"
/>
<!-- Allowing legacyIcon to hold a CSS name, will be removed in 7.0 -->
<span
ng-if="!type.image && !type.icon"
aria-hidden="true"
class="kuiIcon visWizard__visTypeIcon"
ng-class="type.legacyIcon"
></span>
<!-- If there's no image, default to an icon, for BWC. -->
<icon
ng-if="!type.image && type.icon"
aria-hidden="true"
class="kuiIcon visWizard__visTypeIcon"
type="type.icon"
size="'xxl'"
></icon>
</div>
<div
class="kuiGalleryItem__label"
data-test-subj="visualizeWizardChartTypeTitle"
>
{{ type.title }}
</div>
<div
class="kuiGalleryItem__icon kuiIcon fa-flask"
ng-if="type.shouldMarkAsExperimentalInUI()"
></div>
<span
class="euiScreenReaderOnly"
aria-hidden="true"
id="visDescription_{{ ::getVisTypeId(type) }}"
>{{::getVisTypeTooltip(type)}}</span>
</a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { TypeSelection } from './type_selection';

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { EuiText } from '@elastic/eui';
export const NewVisHelp = () => (
<EuiText>
<p>
<FormattedMessage
id="kbn.visualize.newVisWizard.helpText"
defaultMessage="Start creating your visualization by selecting a type for that visualization."
/>
</p>
</EuiText>
);

View file

@ -0,0 +1,203 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { sortByOrder } from 'lodash';
import React, { ChangeEvent } from 'react';
import {
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiKeyPadMenu,
EuiKeyPadMenuItemButton,
EuiModalHeader,
EuiModalHeaderTitle,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { NewVisHelp } from './new_vis_help';
import { VisHelpText } from './vis_help_text';
import { VisTypeIcon } from './vis_type_icon';
import { memoizeLast } from 'ui/utils/memoize';
import { VisType } from 'ui/vis';
interface VisTypeListEntry extends VisType {
highlighted: boolean;
}
interface TypeSelectionProps {
onVisTypeSelected: (visType: VisType) => void;
visTypesRegistry: VisType[];
showExperimental: boolean;
}
interface TypeSelectionState {
highlightedType: VisType | null;
query: string;
}
class TypeSelection extends React.Component<TypeSelectionProps, TypeSelectionState> {
public state = {
highlightedType: null,
query: '',
};
private readonly getFilteredVisTypes = memoizeLast(this.filteredVisTypes);
public render() {
const { query, highlightedType } = this.state;
const visTypes = this.getFilteredVisTypes(this.props.visTypesRegistry, query);
return (
<React.Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="kbn.visualize.newVisWizard.title"
defaultMessage="New Visualization"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<div className="visNewVisDialog__body">
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false} className="visNewVisDialog__searchWrapper">
<EuiFieldSearch
placeholder="Filter"
value={query}
onChange={this.onQueryChange}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem grow={1} className="visNewVisDialog__typesWrapper">
<EuiKeyPadMenu
className="visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
{visTypes.map(this.renderVisType)}
</EuiKeyPadMenu>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem className="visNewVisDialog__description" grow={false}>
{highlightedType ? (
<VisHelpText visType={highlightedType} />
) : (
<React.Fragment>
<EuiTitle size="s">
<h2>
<FormattedMessage
id="kbn.visualize.newVisWizard.selectVisType"
defaultMessage="Select a visualization type"
/>
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<NewVisHelp />
</React.Fragment>
)}
</EuiFlexItem>
</EuiFlexGroup>
</div>
</React.Fragment>
);
}
private filteredVisTypes(visTypes: VisType[], query: string): VisTypeListEntry[] {
const types = visTypes.filter(type => {
// Filter out all lab visualizations if lab mode is not enabled
if (!this.props.showExperimental && type.stage === 'experimental') {
return false;
}
// Filter out hidden visualizations
if (type.hidden) {
return false;
}
return true;
});
let entries: VisTypeListEntry[];
if (!query) {
entries = types.map(type => ({ ...type, highlighted: false }));
} else {
const q = query.toLowerCase();
entries = types.map(type => {
const matchesQuery =
type.name.toLowerCase().includes(q) ||
type.title.toLowerCase().includes(q) ||
(typeof type.description === 'string' && type.description.toLowerCase().includes(q));
return { ...type, highlighted: matchesQuery };
});
}
return sortByOrder(entries, ['highlighted', 'title'], ['desc', 'asc']);
}
private renderVisType = (visType: VisTypeListEntry) => {
let stage = {};
if (visType.stage === 'experimental') {
stage = {
betaBadgeLabel: i18n.translate('kbn.visualize.newVisWizard.experimentalTitle', {
defaultMessage: 'Experimental',
}),
betaBadgeTooltipContent: i18n.translate('kbn.visualize.newVisWizard.experimentalTooltip', {
defaultMessage: 'This visualization is experimental.',
}),
};
}
const isDisabled = this.state.query !== '' && !visType.highlighted;
return (
<EuiKeyPadMenuItemButton
key={visType.name}
label={<span data-test-subj="visTypeTitle">{visType.title}</span>}
onClick={() => this.props.onVisTypeSelected(visType)}
onFocus={() => this.highlightType(visType)}
onMouseEnter={() => this.highlightType(visType)}
onMouseLeave={() => this.highlightType(null)}
onBlur={() => this.highlightType(null)}
className="visNewVisDialog__type"
data-test-subj={`visType-${visType.name}`}
data-vis-stage={visType.stage}
disabled={isDisabled}
{...stage}
>
<VisTypeIcon visType={visType} />
</EuiKeyPadMenuItemButton>
);
};
private highlightType(visType: VisType | null) {
this.setState({
highlightedType: visType,
});
}
private onQueryChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({
query: ev.target.value,
});
};
}
export { TypeSelection };

View file

@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { VisType } from 'ui/vis';
interface VisHelpTextProps {
visType: VisType;
}
export const VisHelpText = ({ visType }: VisHelpTextProps) => {
return (
<React.Fragment>
<EuiTitle size="s">
<h2>{visType.title}</h2>
</EuiTitle>
<EuiSpacer size="s" />
{visType.stage === 'experimental' && (
<React.Fragment>
<EuiText>
<em>
<FormattedMessage
id="kbn.visualize.newVisWizard.experimentalDescription"
defaultMessage="This visualization is experimental. The design and implementation
are less mature than stable visualizations and might be subject to change."
/>
</em>
</EuiText>
<EuiSpacer size="s" />
</React.Fragment>
)}
<EuiText>{visType.description}</EuiText>
</React.Fragment>
);
};

View file

@ -0,0 +1,54 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { EuiIcon } from '@elastic/eui';
import classnames from 'classnames';
import React from 'react';
import { VisType } from 'ui/vis';
interface VisTypeIconProps {
visType: VisType;
}
/**
* This renders the icon for a specific visualization type.
* This currently checks the following:
* - If visType.image is set, use that as the `src` of an image
* - If legacyIcon is set, use that as a classname for a span with kuiIcon (to be removed in 7.0)
* - Otherwise use the visType.icon as an EuiIcon or the 'empty' icon if that's not set
*/
export const VisTypeIcon = ({ visType }: VisTypeIconProps) => {
const legacyIconClass = classnames(
'kuiIcon',
'visNewVisDialog__typeLegacyIcon',
visType.legacyIcon
);
return (
<React.Fragment>
{visType.image && (
<img src={visType.image} aria-hidden="true" className="visNewVisDialog__typeImage" />
)}
{!visType.image && visType.legacyIcon && <span className={legacyIconClass} />}
{!visType.image &&
!visType.legacyIcon && (
<EuiIcon type={visType.icon || 'empty'} size="l" color="secondary" aria-hidden="true" />
)}
</React.Fragment>
);
};

View file

@ -22,175 +22,16 @@ import 'ui/directives/saved_object_finder';
import 'ui/directives/paginated_selectable_list';
import '../../discover/saved_searches/saved_searches';
import _ from 'lodash';
import { CATEGORY, CATEGORY_DISPLAY_NAMES } from 'ui/vis/vis_category';
import { DashboardConstants } from '../../dashboard/dashboard_constants';
import { VisualizeConstants } from '../visualize_constants';
import routes from 'ui/routes';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { uiModules } from 'ui/modules';
import visualizeWizardStep1Template from './step_1.html';
import visualizeWizardStep2Template from './step_2.html';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { timefilter } from 'ui/timefilter';
const module = uiModules.get('app/visualize', ['kibana/courier']);
/********
/** Wizard Step 1
/********/
// Redirect old route to new route.
routes.when('/visualize/step/1', {
redirectTo: VisualizeConstants.WIZARD_STEP_1_PAGE_PATH,
});
routes.when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, {
template: visualizeWizardStep1Template,
controller: 'VisualizeWizardStep1',
});
module.controller('VisualizeWizardStep1', function ($scope, $route, kbnUrl, Private, config) {
timefilter.disableAutoRefreshSelector();
timefilter.disableTimeRangeSelector();
const addToDashMode = $route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM];
kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
const visTypes = Private(VisTypesRegistryProvider);
const isLabsEnabled = config.get('visualize:enableLabs');
$scope.toggleLabView = () => {
$route.current.params.lab = !$route.current.params.lab;
$route.updateParams($route.current.params);
$route.reload();
};
const categoryToVisTypesMap = {};
visTypes.forEach(visType => {
let categoryName = visType.category;
if (categoryName === CATEGORY.HIDDEN) {
return;
}
if (!isLabsEnabled && visType.stage === 'experimental') {
return;
}
// If the specified category doesn't have a value in our display names
// mapping (most likely because the vis specified a random category, not using
// CATEGORY values), just move it to the OTHER category.
if (!CATEGORY_DISPLAY_NAMES[categoryName]) {
categoryName = CATEGORY.OTHER;
}
// Create category object if it doesn't exist yet.
if (!categoryToVisTypesMap[categoryName]) {
categoryToVisTypesMap[categoryName] = {
label: CATEGORY_DISPLAY_NAMES[categoryName],
list: [],
};
}
const categoryVisTypes = categoryToVisTypesMap[categoryName];
// Add the visType to the list and sort them by their title.
categoryVisTypes.list = _.sortBy(
categoryVisTypes.list.concat(visType),
type => type.title
);
});
// Sort the categories alphabetically.
const sortedVisTypeCategories = Object.values(categoryToVisTypesMap).sort((a, b) => {
const other = CATEGORY.OTHER.toLowerCase();
// Put "other" category at the end of the list.
const labelA = a.label.toLowerCase();
if (labelA === other) return 1;
const labelB = b.label.toLowerCase();
if (labelB === other) return -1;
if (labelA < labelB) return -1;
if (labelA > labelB) return 1;
return 0;
});
$scope.searchTerm = '';
$scope.filteredVisTypeCategories = [];
$scope.$watch('searchTerm', () => {
function getVisTypeCategories() {
const normalizedSearchTerm = $scope.searchTerm.toLowerCase().trim();
const filteredVisTypeCategories = sortedVisTypeCategories.map(category => {
// Include entire category if the category matches the search term.
if (category.label.toLowerCase().includes(normalizedSearchTerm)) {
return category;
}
// Otherwise, return just the vis types in the category which match.
const filteredVisTypes = category.list.filter(visType => {
return visType.title.toLowerCase().includes(normalizedSearchTerm);
});
return {
label: category.label,
list: filteredVisTypes,
};
});
return filteredVisTypeCategories.filter(category => category.list.length);
}
$scope.filteredVisTypeCategories = getVisTypeCategories();
});
$scope.getVisTypeId = type => {
return _.camelCase(type.name);
};
$scope.getVisTypeTooltip = type => {
//to not clutter the tooltip, just only notify if labs or experimental.
//labs is more important in this regard.
let prefix = '';
if (type.stage === 'experimental') {
prefix = '(Experimental)';
}
return `${prefix} ${type.description}`;
};
$scope.getVisTypeTooltipPosition = index => {
// Tooltips should appear on the bottom by default, unless they're on the last row. This is a
// cheap workaround to automatically positioning the tooltip so that it won't disappear off
// the edge of the screen.
if (index === $scope.filteredVisTypeCategories.length - 1) {
return 'top';
}
return 'bottom';
};
$scope.getVisTypeUrl = function (visType) {
const baseUrl =
visType.requiresSearch && visType.options.showIndexSelection
? `#${VisualizeConstants.WIZARD_STEP_2_PAGE_PATH}?`
: `#${VisualizeConstants.CREATE_PATH}?`;
const params = [`type=${encodeURIComponent(visType.name)}`];
if (addToDashMode) {
params.push(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
}
return baseUrl + params.join('&');
};
});
/********
/** Wizard Step 2
/********/

View file

@ -19,7 +19,6 @@
import { MarkdownVisWrapper } from './markdown_vis_controller';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CATEGORY } from 'ui/vis/vis_category';
import markdownVisParamsTemplate from './markdown_vis_params.html';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { DefaultEditorSize } from 'ui/vis/editor_size';
@ -41,7 +40,6 @@ function MarkdownVisProvider(Private, i18n) {
isAccessible: true,
icon: 'visText',
description: i18n('markdownVis.markdownDescription', { defaultMessage: 'Create a document using markdown syntax' }),
category: CATEGORY.OTHER,
visConfig: {
component: MarkdownVisWrapper,
defaults: {

View file

@ -19,7 +19,6 @@
import './metric_vis_params';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CATEGORY } from 'ui/vis/vis_category';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
@ -41,7 +40,6 @@ function MetricVisProvider(Private, i18n) {
title: i18n('metricVis.metricTitle', { defaultMessage: 'Metric' }),
icon: 'visMetric',
description: i18n('metricVis.metricDescription', { defaultMessage: 'Display a calculation as a single number' }),
category: CATEGORY.DATA,
visConfig: {
component: MetricVisComponent,
defaults: {

View file

@ -20,7 +20,6 @@
import { MetricsRequestHandlerProvider } from './request_handler';
import { ReactEditorControllerProvider } from './editor_controller';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CATEGORY } from 'ui/vis/vis_category';
import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message';
// register the provider with the visTypes registry so that other know it exists
@ -36,7 +35,6 @@ export default function MetricsVisProvider(Private) {
name: 'metrics',
title: 'Visual Builder',
description: 'Build time-series using a visual pipeline interface',
category: CATEGORY.TIME,
icon: 'visVisualBuilder',
feedbackMessage: defaultFeedbackMessage,
visConfig: {

View file

@ -19,7 +19,6 @@
import './region_map_vis_params';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CATEGORY } from 'ui/vis/vis_category';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colormaps';
@ -40,7 +39,6 @@ VisTypesRegistryProvider.register(function RegionMapProvider(Private, regionmaps
title: i18n('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }),
description: i18n('regionMap.mapVis.regionMapDescription', { defaultMessage: 'Show metrics on a thematic map. Use one of the \
provided base maps, or add your own. Darker colors represent higher values.' }),
category: CATEGORY.MAP,
icon: 'visMapRegion',
visConfig: {
defaults: {

View file

@ -23,7 +23,6 @@ import './table_vis_params';
import 'ui/agg_table';
import 'ui/agg_table/agg_table_group';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CATEGORY } from 'ui/vis/vis_category';
import { Schemas } from 'ui/vis/editors/default/schemas';
import tableVisTemplate from './table_vis.html';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
@ -59,7 +58,6 @@ function TableVisTypeProvider(Private) {
description: i18n.translate('tableVis.tableVisDescription', {
defaultMessage: 'Display values in a table',
}),
category: CATEGORY.DATA,
visConfig: {
defaults: {
perPage: 10,

View file

@ -19,7 +19,6 @@
import './tag_cloud_vis_params';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CATEGORY } from 'ui/vis/vis_category';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { TagCloudVisualization } from './tag_cloud_visualization';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
@ -36,7 +35,6 @@ VisTypesRegistryProvider.register(function (Private, i18n) {
description: i18n('tagCloud.vis.tagCloudDescription', {
defaultMessage: 'A group of words, sized according to their importance'
}),
category: CATEGORY.OTHER,
visConfig: {
defaults: {
scale: 'linear',

View file

@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n';
import 'plugins/kbn_vislib_vis_types/controls/vislib_basic_options';
import './editors/tile_map_vis_params';
import { supports } from 'ui/utils/supports';
import { CATEGORY } from 'ui/vis/vis_category';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CoordinateMapsVisualizationProvider } from './coordinate_maps_visualization';
import { Schemas } from 'ui/vis/editors/default/schemas';
@ -44,7 +43,6 @@ VisTypesRegistryProvider.register(function TileMapVisType(Private, getAppState,
description: i18n.translate('tileMap.vis.mapDescription', {
defaultMessage: 'Plot latitude and longitude coordinates on a map',
}),
category: CATEGORY.MAP,
visConfig: {
canDesaturate: !!supports.cssFilters,
defaults: {

View file

@ -18,7 +18,6 @@
*/
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CATEGORY } from 'ui/vis/vis_category';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { TimelionRequestHandlerProvider } from './timelion_request_handler';
import { DefaultEditorSize } from 'ui/vis/editor_size';
@ -46,7 +45,6 @@ export default function TimelionVisProvider(Private, i18n) {
description: i18n('timelion.timelionDescription', {
defaultMessage: 'Build time-series using functional expressions',
}),
category: CATEGORY.TIME,
visConfig: {
defaults: {
expression: '.es(*)',

View file

@ -20,7 +20,6 @@
import { i18n } from '@kbn/i18n';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { CATEGORY } from 'ui/vis/vis_category';
import { DefaultEditorSize } from 'ui/vis/editor_size';
import { Status } from 'ui/vis/update_status';
import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message';
@ -49,7 +48,6 @@ VisTypesRegistryProvider.register((Private) => {
defaultMessage: 'Create custom visualizations using Vega and Vega-Lite',
}),
icon: 'visVega',
category: CATEGORY.OTHER,
visConfig: { defaults: { spec: defaultSpec } },
editorConfig: {
optionsTemplate: vegaEditorTemplate,

View file

@ -1,48 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* You should always make sure that every CATEGORY on top have a corresponding
* display name in the below object, otherwise they won't be shown properly
* in the vis creation wizard.
*/
import { i18n } from '@kbn/i18n';
const CATEGORY = {
BASIC: 'basic',
DATA: 'data',
GRAPHIC: 'graphic',
MAP: 'map',
OTHER: 'other',
TIME: 'time',
// Hidden is a specific category and doesn't need a display name below
HIDDEN: 'hidden'
};
const CATEGORY_DISPLAY_NAMES = {
[CATEGORY.BASIC]: i18n.translate('common.ui.vis.visCategory.basicChartsLabel', { defaultMessage: 'Basic Charts' }),
[CATEGORY.DATA]: i18n.translate('common.ui.vis.visCategory.dataLabel', { defaultMessage: 'Data' }),
[CATEGORY.GRAPHIC]: i18n.translate('common.ui.vis.visCategory.graphicLabel', { defaultMessage: 'Graphic' }),
[CATEGORY.MAP]: i18n.translate('common.ui.vis.visCategory.mapsLabel', { defaultMessage: 'Maps' }),
[CATEGORY.OTHER]: i18n.translate('common.ui.vis.visCategory.otherLabel', { defaultMessage: 'Other' }),
[CATEGORY.TIME]: i18n.translate('common.ui.vis.visCategory.timeSeriesLabel', { defaultMessage: 'Time Series' })
};
export { CATEGORY, CATEGORY_DISPLAY_NAMES };

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { CATEGORY } from '../vis_category';
import _ from 'lodash';
import { VisFiltersProvider } from '../vis_filters';
@ -45,7 +44,6 @@ export function BaseVisTypeProvider(Private) {
const _defaults = {
// name, title, description, icon, image
category: CATEGORY.OTHER,
visualization: null, // must be a class with render/resize/destroy methods
visConfig: {
defaults: {}, // default configuration
@ -69,7 +67,8 @@ export function BaseVisTypeProvider(Private) {
}
},
stage: 'production',
feedbackMessage: ''
feedbackMessage: '',
hidden: false,
};
_.defaultsDeep(this, opts, _defaults);

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { IconType } from '@elastic/eui';
import { RequestHandler, ResponseHandler, Vis } from '..';
import { Status } from '../update_status';
@ -28,11 +29,18 @@ export class VisualizationController {
}
export interface VisType {
name: string;
title: string;
description?: string;
visualization: typeof VisualizationController;
isAccessible?: boolean;
requestHandler: string | RequestHandler;
responseHandler: string | ResponseHandler;
icon?: IconType;
image?: string;
stage: 'experimental' | 'production';
requiresSearch: boolean;
hidden: boolean;
// Since we haven't typed everything here yet, we basically "any" the rest
// of that interface. This should be removed as soon as this type definition

View file

@ -32,23 +32,23 @@ export default function ({ getService, getPageObjects }) {
it('should show the correct chart types', async function () {
const expectedChartTypes = [
'Area',
'Heat Map',
'Horizontal Bar',
'Line',
'Pie',
'Vertical Bar',
'Controls',
'Coordinate Map',
'Data Table',
'Gauge',
'Goal',
'Metric',
'Coordinate Map',
'Region Map',
'Timelion',
'Visual Builder',
'Controls',
'Heat Map',
'Horizontal Bar',
'Line',
'Markdown',
'Metric',
'Pie',
'Region Map',
'Tag Cloud',
'Timelion',
'Vega',
'Vertical Bar',
'Visual Builder',
];
// find all the chart types and make sure there all there

View file

@ -142,8 +142,8 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.openControlsByName(termsField);
await PageObjects.settings.setFieldFormat('bytes');
await PageObjects.settings.controlChangeSave();
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.loadSavedVisualization(vizName1);
await PageObjects.common.navigateToApp('visualize');
await PageObjects.visualize.loadSavedVisualization(vizName1, { navigateToVisualize: false });
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
await PageObjects.visualize.waitForVisualization();

View file

@ -34,9 +34,8 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) {
const toTime = '2015-09-22 18:31:44.000';
log.debug('navigateToApp visualize');
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.header.waitUntilLoadingHasFinished();
log.debug('clickVisualBuilderChart');
await find.clickByPartialLinkText('Visual Builder');
await PageObjects.visualize.clickVisualBuilder();
log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
await PageObjects.header.waitUntilLoadingHasFinished();

View file

@ -45,15 +45,15 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
}
async navigateToNewVisualization() {
log.debug('navigateToApp visualize new');
await PageObjects.common.navigateToUrl('visualize', 'new');
log.debug('navigateToApp visualize');
await PageObjects.common.navigateToApp('visualize');
await testSubjects.click('createNewVis');
await this.waitForVisualizationSelectPage();
await PageObjects.header.waitUntilLoadingHasFinished();
}
async waitForVisualizationSelectPage() {
await retry.try(async () => {
const visualizeSelectTypePage = await testSubjects.find('visualizeSelectTypePage');
const visualizeSelectTypePage = await testSubjects.find('visNewDialogTypes');
if (!visualizeSelectTypePage.isDisplayed()) {
throw new Error('wait for visualization select page');
}
@ -66,28 +66,23 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
}
async clickAreaChart() {
await find.clickByPartialLinkText('Area');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('area');
}
async clickDataTable() {
await find.clickByPartialLinkText('Data Table');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('table');
}
async clickLineChart() {
await find.clickByPartialLinkText('Line');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('line');
}
async clickRegionMap() {
await find.clickByPartialLinkText('Region Map');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('region_map');
}
async clickMarkdownWidget() {
await find.clickByPartialLinkText('Markdown');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('markdown');
}
async clickAddMetric() {
@ -99,38 +94,31 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
}
async clickMetric() {
await find.clickByPartialLinkText('Metric');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('metric');
}
async clickGauge() {
await find.clickByPartialLinkText('Gauge');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('gauge');
}
async clickPieChart() {
await find.clickByPartialLinkText('Pie');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('pie');
}
async clickTileMap() {
await find.clickByPartialLinkText('Coordinate Map');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('tile_map');
}
async clickTagCloud() {
await find.clickByPartialLinkText('Tag Cloud');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('tagcloud');
}
async clickVega() {
await find.clickByPartialLinkText('Vega');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('vega');
}
async clickVisualBuilder() {
await find.clickByPartialLinkText('Visual Builder');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('metrics');
}
async clickEditorSidebarCollapse() {
@ -157,29 +145,23 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
}
async clickVerticalBarChart() {
await find.clickByPartialLinkText('Vertical Bar');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('histogram');
}
async clickHeatmapChart() {
await find.clickByPartialLinkText('Heat Map');
await PageObjects.header.waitUntilLoadingHasFinished();
await this.clickVisType('heatmap');
}
async clickInputControlVis() {
await find.clickByPartialLinkText('Controls');
await PageObjects.header.waitUntilLoadingHasFinished();
}
async getChartTypeCount() {
const tags = await find.allByCssSelector('a.wizard-vis-type');
return tags.length;
await this.clickVisType('input_control_vis');
}
async getChartTypes() {
const chartTypes = await testSubjects.findAll('visualizeWizardChartTypeTitle');
const chartTypeField = await testSubjects.find('visNewDialogTypes');
const chartTypes = await chartTypeField.findAllByTagName('button');
async function getChartType(chart) {
return await chart.getVisibleText();
const label = await testSubjects.findDescendant('visTypeTitle', chart);
return await label.getVisibleText();
}
const getChartTypesPromises = chartTypes.map(getChartType);
return await Promise.all(getChartTypesPromises);
@ -195,7 +177,7 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
}
async getExperimentalTypeLinks() {
return await remote.findAllByPartialLinkText('(Experimental)');
return await remote.findAllByCssSelector('[data-vis-stage="experimental"]');
}
async isExperimentalInfoShown() {
@ -782,10 +764,10 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
});
}
// this starts by clicking the Load Saved Viz button, not from the
// bottom half of the "Create a new visualization Step 1" page
async loadSavedVisualization(vizName) {
await this.clickLoadSavedVisButton();
async loadSavedVisualization(vizName, { navigateToVisualize = true } = {}) {
if (navigateToVisualize) {
await this.clickLoadSavedVisButton();
}
await this.openSavedVisualization(vizName);
}