Return to dashboard after editing embeddable (#62865)
Changed the process for saving visualizations and lens visualizations when the user has been redirected there from another application.
This commit is contained in:
parent
932d6ad214
commit
5bad855e4b
|
@ -34,6 +34,7 @@ import {
|
|||
import { KibanaContextProvider } from '../../../../../kibana_react/public';
|
||||
// eslint-disable-next-line
|
||||
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
|
||||
import { applicationServiceMock } from '../../../../../../core/public/mocks';
|
||||
|
||||
let dashboardContainer: DashboardContainer | undefined;
|
||||
|
||||
|
@ -50,7 +51,7 @@ function getProps(
|
|||
|
||||
const start = doStart();
|
||||
const options: DashboardContainerOptions = {
|
||||
application: {} as any,
|
||||
application: applicationServiceMock.createStartContract(),
|
||||
embeddable: {
|
||||
getTriggerCompatibleActions: (() => []) as any,
|
||||
getEmbeddableFactories: start.getEmbeddableFactories,
|
||||
|
|
|
@ -38,6 +38,7 @@ import { embeddablePluginMock } from '../../../../embeddable/public/mocks';
|
|||
import { inspectorPluginMock } from '../../../../inspector/public/mocks';
|
||||
import { KibanaContextProvider } from '../../../../kibana_react/public';
|
||||
import { uiActionsPluginMock } from '../../../../ui_actions/public/mocks';
|
||||
import { applicationServiceMock } from '../../../../../core/public/mocks';
|
||||
|
||||
test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
||||
const inspector = inspectorPluginMock.createStartContract();
|
||||
|
@ -56,7 +57,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
|||
|
||||
const initialInput = getSampleDashboardInput({ viewMode: ViewMode.VIEW });
|
||||
const options: DashboardContainerOptions = {
|
||||
application: {} as any,
|
||||
application: applicationServiceMock.createStartContract(),
|
||||
embeddable: start,
|
||||
notifications: {} as any,
|
||||
overlays: {} as any,
|
||||
|
@ -84,7 +85,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
|
|||
getAllEmbeddableFactories={(() => []) as any}
|
||||
getEmbeddableFactory={(() => null) as any}
|
||||
notifications={{} as any}
|
||||
application={{} as any}
|
||||
application={options.application}
|
||||
overlays={{} as any}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
export const DashboardConstants = {
|
||||
ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM: 'addToDashboard',
|
||||
LANDING_PAGE_PATH: '/dashboards',
|
||||
CREATE_NEW_DASHBOARD_URL: '/dashboard',
|
||||
ADD_EMBEDDABLE_ID: 'addEmbeddableId',
|
||||
|
|
|
@ -22,6 +22,7 @@ import './index.scss';
|
|||
import { PluginInitializerContext } from 'src/core/public';
|
||||
import { EmbeddablePublicPlugin } from './plugin';
|
||||
|
||||
export { EMBEDDABLE_ORIGINATING_APP_PARAM } from './types';
|
||||
export {
|
||||
ACTION_ADD_PANEL,
|
||||
ACTION_APPLY_FILTER,
|
||||
|
|
|
@ -22,10 +22,12 @@ import { Embeddable, EmbeddableInput } from '../embeddables';
|
|||
import { ViewMode } from '../types';
|
||||
import { ContactCardEmbeddable } from '../test_samples';
|
||||
import { embeddablePluginMock } from '../../mocks';
|
||||
import { applicationServiceMock } from '../../../../../core/public/mocks';
|
||||
|
||||
const { doStart } = embeddablePluginMock.createInstance();
|
||||
const start = doStart();
|
||||
const getFactory = start.getEmbeddableFactory;
|
||||
const applicationMock = applicationServiceMock.createStartContract();
|
||||
|
||||
class EditableEmbeddable extends Embeddable {
|
||||
public readonly type = 'EDITABLE_EMBEDDABLE';
|
||||
|
@ -41,7 +43,7 @@ class EditableEmbeddable extends Embeddable {
|
|||
}
|
||||
|
||||
test('is compatible when edit url is available, in edit mode and editable', async () => {
|
||||
const action = new EditPanelAction(getFactory, {} as any);
|
||||
const action = new EditPanelAction(getFactory, applicationMock);
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
embeddable: new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true),
|
||||
|
@ -50,7 +52,7 @@ test('is compatible when edit url is available, in edit mode and editable', asyn
|
|||
});
|
||||
|
||||
test('getHref returns the edit urls', async () => {
|
||||
const action = new EditPanelAction(getFactory, {} as any);
|
||||
const action = new EditPanelAction(getFactory, applicationMock);
|
||||
expect(action.getHref).toBeDefined();
|
||||
|
||||
if (action.getHref) {
|
||||
|
@ -64,7 +66,7 @@ test('getHref returns the edit urls', async () => {
|
|||
});
|
||||
|
||||
test('is not compatible when edit url is not available', async () => {
|
||||
const action = new EditPanelAction(getFactory, {} as any);
|
||||
const action = new EditPanelAction(getFactory, applicationMock);
|
||||
const embeddable = new ContactCardEmbeddable(
|
||||
{
|
||||
id: '123',
|
||||
|
@ -83,7 +85,7 @@ test('is not compatible when edit url is not available', async () => {
|
|||
});
|
||||
|
||||
test('is not visible when edit url is available but in view mode', async () => {
|
||||
const action = new EditPanelAction(getFactory, {} as any);
|
||||
const action = new EditPanelAction(getFactory, applicationMock);
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
embeddable: new EditableEmbeddable(
|
||||
|
@ -98,7 +100,7 @@ test('is not visible when edit url is available but in view mode', async () => {
|
|||
});
|
||||
|
||||
test('is not compatible when edit url is available, in edit mode, but not editable', async () => {
|
||||
const action = new EditPanelAction(getFactory, {} as any);
|
||||
const action = new EditPanelAction(getFactory, applicationMock);
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
embeddable: new EditableEmbeddable(
|
||||
|
|
|
@ -20,10 +20,11 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { Action } from 'src/plugins/ui_actions/public';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { ViewMode } from '../types';
|
||||
import { EmbeddableFactoryNotFoundError } from '../errors';
|
||||
import { IEmbeddable } from '../embeddables';
|
||||
import { EmbeddableStart } from '../../plugin';
|
||||
import { EMBEDDABLE_ORIGINATING_APP_PARAM, IEmbeddable } from '../..';
|
||||
|
||||
export const ACTION_EDIT_PANEL = 'editPanel';
|
||||
|
||||
|
@ -35,11 +36,18 @@ export class EditPanelAction implements Action<ActionContext> {
|
|||
public readonly type = ACTION_EDIT_PANEL;
|
||||
public readonly id = ACTION_EDIT_PANEL;
|
||||
public order = 50;
|
||||
public currentAppId: string | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'],
|
||||
private readonly application: ApplicationStart
|
||||
) {}
|
||||
) {
|
||||
if (this.application?.currentAppId$) {
|
||||
this.application.currentAppId$
|
||||
.pipe(take(1))
|
||||
.subscribe((appId: string | undefined) => (this.currentAppId = appId));
|
||||
}
|
||||
}
|
||||
|
||||
public getDisplayName({ embeddable }: ActionContext) {
|
||||
const factory = this.getEmbeddableFactory(embeddable.type);
|
||||
|
@ -93,7 +101,15 @@ export class EditPanelAction implements Action<ActionContext> {
|
|||
}
|
||||
|
||||
public async getHref({ embeddable }: ActionContext): Promise<string> {
|
||||
const editUrl = embeddable ? embeddable.getOutput().editUrl : undefined;
|
||||
let editUrl = embeddable ? embeddable.getOutput().editUrl : undefined;
|
||||
if (editUrl && this.currentAppId) {
|
||||
editUrl += `?${EMBEDDABLE_ORIGINATING_APP_PARAM}=${this.currentAppId}`;
|
||||
|
||||
// TODO: Remove this after https://github.com/elastic/kibana/pull/63443
|
||||
if (this.currentAppId === 'kibana') {
|
||||
editUrl += `:${window.location.hash.split(/[\/\?]/)[1]}`;
|
||||
}
|
||||
}
|
||||
return editUrl ? editUrl : '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
import { inspectorPluginMock } from '../../../../inspector/public/mocks';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import { embeddablePluginMock } from '../../mocks';
|
||||
import { applicationServiceMock } from '../../../../../core/public/mocks';
|
||||
|
||||
const actionRegistry = new Map<string, Action>();
|
||||
const triggerRegistry = new Map<string, Trigger>();
|
||||
|
@ -55,6 +56,7 @@ const trigger: Trigger = {
|
|||
id: CONTEXT_MENU_TRIGGER,
|
||||
};
|
||||
const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
|
||||
const applicationMock = applicationServiceMock.createStartContract();
|
||||
|
||||
actionRegistry.set(editModeAction.id, editModeAction);
|
||||
triggerRegistry.set(trigger.id, trigger);
|
||||
|
@ -159,7 +161,7 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => {
|
|||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
application={{} as any}
|
||||
application={applicationMock}
|
||||
overlays={{} as any}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
|
@ -199,7 +201,7 @@ const renderInEditModeAndOpenContextMenu = async (
|
|||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
/>
|
||||
|
@ -306,7 +308,7 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => {
|
|||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
/>
|
||||
|
@ -369,7 +371,7 @@ test('Updates when hidePanelTitles is toggled', async () => {
|
|||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
/>
|
||||
|
@ -422,7 +424,7 @@ test('Check when hide header option is false', async () => {
|
|||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
hideHeader={false}
|
||||
|
|
|
@ -26,6 +26,8 @@ import {
|
|||
EmbeddableFactoryDefinition,
|
||||
} from './lib/embeddables';
|
||||
|
||||
export const EMBEDDABLE_ORIGINATING_APP_PARAM = 'embeddableOriginatingApp';
|
||||
|
||||
export type EmbeddableFactoryRegistry = Map<string, EmbeddableFactory>;
|
||||
|
||||
export type EmbeddableFactoryProvider = <
|
||||
|
|
|
@ -19,7 +19,14 @@
|
|||
|
||||
import { SavedObjectsPublicPlugin } from './plugin';
|
||||
|
||||
export { OnSaveProps, SavedObjectSaveModal, SaveResult, showSaveModal } from './save_modal';
|
||||
export {
|
||||
OnSaveProps,
|
||||
SavedObjectSaveModal,
|
||||
SavedObjectSaveModalOrigin,
|
||||
SaveModalState,
|
||||
SaveResult,
|
||||
showSaveModal,
|
||||
} from './save_modal';
|
||||
export { getSavedObjectFinder, SavedObjectFinderUi, SavedObjectMetaData } from './finder';
|
||||
export {
|
||||
SavedObjectLoader,
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { SavedObjectSaveModal, OnSaveProps } from './saved_object_save_modal';
|
||||
export { SavedObjectSaveModal, OnSaveProps, SaveModalState } from './saved_object_save_modal';
|
||||
export { SavedObjectSaveModalOrigin } from './saved_object_save_modal_origin';
|
||||
export { showSaveModal, SaveResult } from './show_saved_object_save_modal';
|
||||
|
|
|
@ -53,14 +53,15 @@ interface Props {
|
|||
onClose: () => void;
|
||||
title: string;
|
||||
showCopyOnSave: boolean;
|
||||
initialCopyOnSave?: boolean;
|
||||
objectType: string;
|
||||
confirmButtonLabel?: React.ReactNode;
|
||||
options?: React.ReactNode;
|
||||
options?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
|
||||
description?: string;
|
||||
showDescription: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
export interface SaveModalState {
|
||||
title: string;
|
||||
copyOnSave: boolean;
|
||||
isTitleDuplicateConfirmed: boolean;
|
||||
|
@ -71,11 +72,11 @@ interface State {
|
|||
|
||||
const generateId = htmlIdGenerator();
|
||||
|
||||
export class SavedObjectSaveModal extends React.Component<Props, State> {
|
||||
export class SavedObjectSaveModal extends React.Component<Props, SaveModalState> {
|
||||
private warning = React.createRef<HTMLDivElement>();
|
||||
public readonly state = {
|
||||
title: this.props.title,
|
||||
copyOnSave: false,
|
||||
copyOnSave: Boolean(this.props.initialCopyOnSave),
|
||||
isTitleDuplicateConfirmed: false,
|
||||
hasTitleDuplicate: false,
|
||||
isLoading: false,
|
||||
|
@ -139,7 +140,9 @@ export class SavedObjectSaveModal extends React.Component<Props, State> {
|
|||
|
||||
{this.renderViewDescription()}
|
||||
|
||||
{this.props.options}
|
||||
{typeof this.props.options === 'function'
|
||||
? this.props.options(this.state)
|
||||
: this.props.options}
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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, { Fragment, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { OnSaveProps, SaveModalState, SavedObjectSaveModal } from '.';
|
||||
|
||||
interface SaveModalDocumentInfo {
|
||||
id?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface OriginSaveModalProps {
|
||||
originatingApp?: string;
|
||||
documentInfo: SaveModalDocumentInfo;
|
||||
objectType: string;
|
||||
onClose: () => void;
|
||||
onSave: (props: OnSaveProps & { returnToOrigin: boolean }) => void;
|
||||
}
|
||||
|
||||
export function SavedObjectSaveModalOrigin(props: OriginSaveModalProps) {
|
||||
const [returnToOriginMode, setReturnToOriginMode] = useState(Boolean(props.originatingApp));
|
||||
const { documentInfo } = props;
|
||||
|
||||
const returnLabel = i18n.translate('savedObjects.saveModalOrigin.returnToOriginLabel', {
|
||||
defaultMessage: 'Return',
|
||||
});
|
||||
const addLabel = i18n.translate('savedObjects.saveModalOrigin.addToOriginLabel', {
|
||||
defaultMessage: 'Add',
|
||||
});
|
||||
|
||||
const getReturnToOriginSwitch = (state: SaveModalState) => {
|
||||
if (!props.originatingApp) {
|
||||
return;
|
||||
}
|
||||
let origin = props.originatingApp!;
|
||||
|
||||
// TODO: Remove this after https://github.com/elastic/kibana/pull/63443
|
||||
if (origin.startsWith('kibana:')) {
|
||||
origin = origin.split(':')[1];
|
||||
}
|
||||
|
||||
if (
|
||||
!state.copyOnSave ||
|
||||
origin === 'dashboard' // dashboard supports adding a copied panel on save...
|
||||
) {
|
||||
const originVerb = !documentInfo.id || state.copyOnSave ? addLabel : returnLabel;
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
data-test-subj="returnToOriginModeSwitch"
|
||||
checked={returnToOriginMode}
|
||||
onChange={event => {
|
||||
setReturnToOriginMode(event.target.checked);
|
||||
}}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModalOrigin.originAfterSavingSwitchLabel"
|
||||
defaultMessage="{originVerb} to {origin} after saving"
|
||||
values={{ originVerb, origin }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
setReturnToOriginMode(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onModalSave = (onSaveProps: OnSaveProps) => {
|
||||
props.onSave({ ...onSaveProps, returnToOrigin: returnToOriginMode });
|
||||
};
|
||||
|
||||
const confirmButtonLabel = returnToOriginMode
|
||||
? i18n.translate('savedObjects.saveModalOrigin.saveAndReturnLabel', {
|
||||
defaultMessage: 'Save and return',
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<SavedObjectSaveModal
|
||||
onSave={onModalSave}
|
||||
onClose={props.onClose}
|
||||
title={documentInfo.title}
|
||||
showCopyOnSave={documentInfo.id ? true : false}
|
||||
initialCopyOnSave={Boolean(documentInfo.id) && returnToOriginMode}
|
||||
confirmButtonLabel={confirmButtonLabel}
|
||||
objectType={props.objectType}
|
||||
options={getReturnToOriginSwitch}
|
||||
description={documentInfo.description}
|
||||
showDescription={true}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -19,12 +19,14 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SavedObjectMetaData } from 'src/plugins/saved_objects/public';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { SavedObjectAttributes } from '../../../../core/public';
|
||||
import {
|
||||
EmbeddableFactoryDefinition,
|
||||
EmbeddableOutput,
|
||||
ErrorEmbeddable,
|
||||
IContainer,
|
||||
EMBEDDABLE_ORIGINATING_APP_PARAM,
|
||||
} from '../../../embeddable/public';
|
||||
import { DisabledLabEmbeddable } from './disabled_lab_embeddable';
|
||||
import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable';
|
||||
|
@ -59,6 +61,7 @@ export class VisualizeEmbeddableFactory
|
|||
VisualizationAttributes
|
||||
> {
|
||||
public readonly type = VISUALIZE_EMBEDDABLE_TYPE;
|
||||
|
||||
public readonly savedObjectMetaData: SavedObjectMetaData<VisualizationAttributes> = {
|
||||
name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }),
|
||||
includeFields: ['visState'],
|
||||
|
@ -98,6 +101,18 @@ export class VisualizeEmbeddableFactory
|
|||
});
|
||||
}
|
||||
|
||||
public async getCurrentAppId() {
|
||||
let currentAppId = await this.deps
|
||||
.start()
|
||||
.core.application.currentAppId$.pipe(first())
|
||||
.toPromise();
|
||||
// TODO: Remove this after https://github.com/elastic/kibana/pull/63443
|
||||
if (currentAppId === 'kibana') {
|
||||
currentAppId += `:${window.location.hash.split(/[\/\?]/)[1]}`;
|
||||
}
|
||||
return currentAppId;
|
||||
}
|
||||
|
||||
public async createFromSavedObject(
|
||||
savedObjectId: string,
|
||||
input: Partial<VisualizeInput> & { id: string },
|
||||
|
@ -118,8 +133,9 @@ export class VisualizeEmbeddableFactory
|
|||
public async create() {
|
||||
// TODO: This is a bit of a hack to preserve the original functionality. Ideally we will clean this up
|
||||
// to allow for in place creation of visualizations without having to navigate away to a new URL.
|
||||
const originatingAppParam = await this.getCurrentAppId();
|
||||
showNewVisModal({
|
||||
editorParams: ['addToDashboard'],
|
||||
editorParams: [`${EMBEDDABLE_ORIGINATING_APP_PARAM}=${originatingAppParam}`],
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import { PluginInitializerContext } from '../../../core/public';
|
||||
import { VisualizationsSetup, VisualizationsStart } from './';
|
||||
import { VisualizationsPlugin } from './plugin';
|
||||
import { coreMock } from '../../../core/public/mocks';
|
||||
import { coreMock, applicationServiceMock } from '../../../core/public/mocks';
|
||||
import { embeddablePluginMock } from '../../../plugins/embeddable/public/mocks';
|
||||
import { expressionsPluginMock } from '../../../plugins/expressions/public/mocks';
|
||||
import { dataPluginMock } from '../../../plugins/data/public/mocks';
|
||||
|
@ -65,6 +65,7 @@ const createInstance = async () => {
|
|||
expressions: expressionsPluginMock.createStartContract(),
|
||||
inspector: inspectorPluginMock.createStartContract(),
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
application: applicationServiceMock.createStartContract(),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -17,7 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
|
||||
import {
|
||||
PluginInitializerContext,
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Plugin,
|
||||
ApplicationStart,
|
||||
} from '../../../core/public';
|
||||
import { TypesService, TypesSetup, TypesStart } from './vis_types';
|
||||
import {
|
||||
setUISettings,
|
||||
|
@ -95,6 +101,7 @@ export interface VisualizationsStartDeps {
|
|||
expressions: ExpressionsStart;
|
||||
inspector: InspectorStart;
|
||||
uiActions: UiActionsStart;
|
||||
application: ApplicationStart;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -131,7 +138,6 @@ export class VisualizationsPlugin
|
|||
expressions.registerRenderer(visualizationRenderer);
|
||||
expressions.registerFunction(rangeExpressionFunction);
|
||||
expressions.registerFunction(visDimensionExpressionFunction);
|
||||
|
||||
const embeddableFactory = new VisualizeEmbeddableFactory({ start });
|
||||
embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory);
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ describe('NewVisModal', () => {
|
|||
isOpen={true}
|
||||
onClose={onClose}
|
||||
visTypesRegistry={visTypes}
|
||||
editorParams={['foo=true', 'bar=42', 'addToDashboard']}
|
||||
editorParams={['foo=true', 'bar=42', 'embeddableOriginatingApp=notAnApp']}
|
||||
addBasePath={addBasePath}
|
||||
uiSettings={uiSettings}
|
||||
savedObjects={{} as SavedObjectsStart}
|
||||
|
@ -152,7 +152,9 @@ describe('NewVisModal', () => {
|
|||
);
|
||||
const visButton = wrapper.find('button[data-test-subj="visType-visWithAliasUrl"]');
|
||||
visButton.simulate('click');
|
||||
expect(window.location.assign).toBeCalledWith('testbasepath/aliasUrl?addToDashboard');
|
||||
expect(window.location.assign).toBeCalledWith(
|
||||
'testbasepath/aliasUrl?embeddableOriginatingApp=notAnApp'
|
||||
);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import { SearchSelection } from './search_selection';
|
|||
import { TypeSelection } from './type_selection';
|
||||
import { TypesStart, VisType, VisTypeAlias } from '../vis_types';
|
||||
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public';
|
||||
import { EMBEDDABLE_ORIGINATING_APP_PARAM } from '../../../embeddable/public';
|
||||
|
||||
interface TypeSelectionProps {
|
||||
isOpen: boolean;
|
||||
|
@ -143,8 +144,11 @@ class NewVisModal extends React.Component<TypeSelectionProps, TypeSelectionState
|
|||
let params;
|
||||
if ('aliasUrl' in visType) {
|
||||
params = this.props.addBasePath(visType.aliasUrl);
|
||||
if (this.props.editorParams && this.props.editorParams.includes('addToDashboard')) {
|
||||
params = `${params}?addToDashboard`;
|
||||
if (this.props.editorParams) {
|
||||
const originatingAppParam = this.props.editorParams?.find((param: string) =>
|
||||
param.startsWith(EMBEDDABLE_ORIGINATING_APP_PARAM)
|
||||
);
|
||||
params = originatingAppParam ? `${params}?${originatingAppParam}` : params;
|
||||
}
|
||||
this.props.onClose();
|
||||
window.location.assign(params);
|
||||
|
|
|
@ -25,11 +25,12 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EventEmitter } from 'events';
|
||||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { makeStateful, useVisualizeAppState, addEmbeddableToDashboardUrl } from './lib';
|
||||
import { VisualizeConstants } from '../visualize_constants';
|
||||
import { getEditBreadcrumbs } from '../breadcrumbs';
|
||||
|
||||
import { EMBEDDABLE_ORIGINATING_APP_PARAM } from '../../../../embeddable/public';
|
||||
|
||||
import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util';
|
||||
import { unhashUrl, removeQueryParam } from '../../../../kibana_utils/public';
|
||||
import { MarkdownSimple, toMountPoint } from '../../../../kibana_react/public';
|
||||
|
@ -38,9 +39,8 @@ import {
|
|||
subscribeWithScope,
|
||||
migrateLegacyQuery,
|
||||
} from '../../../../kibana_legacy/public';
|
||||
import { SavedObjectSaveModal, showSaveModal } from '../../../../saved_objects/public';
|
||||
import { showSaveModal, SavedObjectSaveModalOrigin } from '../../../../saved_objects/public';
|
||||
import { esFilters, connectToQueryState, syncQueryStateWithUrl } from '../../../../data/public';
|
||||
import { DashboardConstants } from '../../../../dashboard/public';
|
||||
|
||||
import { initVisEditorDirective } from './visualization_editor';
|
||||
import { initVisualizationDirective } from './visualization';
|
||||
|
@ -110,6 +110,11 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
|
|||
localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'),
|
||||
};
|
||||
|
||||
const originatingApp = $route.current.params[EMBEDDABLE_ORIGINATING_APP_PARAM];
|
||||
removeQueryParam(history, EMBEDDABLE_ORIGINATING_APP_PARAM);
|
||||
|
||||
$scope.getOriginatingApp = () => originatingApp;
|
||||
|
||||
const visStateToEditorState = () => {
|
||||
const savedVisState = visualizations.convertFromSerializedVis(vis.serialize());
|
||||
return {
|
||||
|
@ -144,13 +149,58 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
|
|||
$scope.embeddableHandler = embeddableHandler;
|
||||
|
||||
$scope.topNavMenu = [
|
||||
...($scope.getOriginatingApp() && savedVis.id
|
||||
? [
|
||||
{
|
||||
id: 'saveAndReturn',
|
||||
label: i18n.translate('visualize.topNavMenu.saveAndReturnVisualizationButtonLabel', {
|
||||
defaultMessage: 'Save and return',
|
||||
}),
|
||||
emphasize: true,
|
||||
iconType: 'check',
|
||||
description: i18n.translate(
|
||||
'visualize.topNavMenu.saveAndReturnVisualizationButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Finish editing visualization and return to the last app',
|
||||
}
|
||||
),
|
||||
testId: 'visualizesaveAndReturnButton',
|
||||
disableButton() {
|
||||
return Boolean($scope.dirty);
|
||||
},
|
||||
tooltip() {
|
||||
if ($scope.dirty) {
|
||||
return i18n.translate(
|
||||
'visualize.topNavMenu.saveAndReturnVisualizationDisabledButtonTooltip',
|
||||
{
|
||||
defaultMessage: 'Apply or Discard your changes before finishing',
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
run: async () => {
|
||||
const saveOptions = {
|
||||
confirmOverwrite: false,
|
||||
returnToOrigin: true,
|
||||
};
|
||||
return doSave(saveOptions);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(visualizeCapabilities.save
|
||||
? [
|
||||
{
|
||||
id: 'save',
|
||||
label: i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', {
|
||||
defaultMessage: 'save',
|
||||
}),
|
||||
label:
|
||||
savedVis.id && $scope.getOriginatingApp()
|
||||
? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', {
|
||||
defaultMessage: 'save as',
|
||||
})
|
||||
: i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', {
|
||||
defaultMessage: 'save',
|
||||
}),
|
||||
emphasize: !savedVis.id || !$scope.getOriginatingApp(),
|
||||
description: i18n.translate('visualize.topNavMenu.saveVisualizationButtonAriaLabel', {
|
||||
defaultMessage: 'Save Visualization',
|
||||
}),
|
||||
|
@ -175,6 +225,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
|
|||
isTitleDuplicateConfirmed,
|
||||
onTitleDuplicate,
|
||||
newDescription,
|
||||
returnToOrigin,
|
||||
}) => {
|
||||
const currentTitle = savedVis.title;
|
||||
savedVis.title = newTitle;
|
||||
|
@ -184,6 +235,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
|
|||
confirmOverwrite: false,
|
||||
isTitleDuplicateConfirmed,
|
||||
onTitleDuplicate,
|
||||
returnToOrigin,
|
||||
};
|
||||
return doSave(saveOptions).then(response => {
|
||||
// If the save wasn't successful, put the original values back.
|
||||
|
@ -194,23 +246,13 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
|
|||
});
|
||||
};
|
||||
|
||||
const confirmButtonLabel = $scope.isAddToDashMode() ? (
|
||||
<FormattedMessage
|
||||
id="visualize.saveDialog.saveAndAddToDashboardButtonLabel"
|
||||
defaultMessage="Save and add to dashboard"
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const saveModal = (
|
||||
<SavedObjectSaveModal
|
||||
<SavedObjectSaveModalOrigin
|
||||
documentInfo={savedVis}
|
||||
onSave={onSave}
|
||||
objectType={'visualization'}
|
||||
onClose={() => {}}
|
||||
title={savedVis.title}
|
||||
showCopyOnSave={savedVis.id ? true : false}
|
||||
objectType="visualization"
|
||||
confirmButtonLabel={confirmButtonLabel}
|
||||
description={savedVis.description}
|
||||
showDescription={true}
|
||||
originatingApp={$scope.getOriginatingApp()}
|
||||
/>
|
||||
);
|
||||
showSaveModal(saveModal, I18nContext);
|
||||
|
@ -398,12 +440,6 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
|
|||
$scope.refreshInterval = timefilter.getRefreshInterval();
|
||||
handleLinkedSearch(initialState.linked);
|
||||
|
||||
const addToDashMode =
|
||||
$route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM];
|
||||
removeQueryParam(history, DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
|
||||
|
||||
$scope.isAddToDashMode = () => addToDashMode;
|
||||
|
||||
$scope.showFilterBar = () => {
|
||||
return vis.type.options.showFilterBar;
|
||||
};
|
||||
|
@ -604,6 +640,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
|
|||
*/
|
||||
function doSave(saveOptions) {
|
||||
// vis.title was not bound and it's needed to reflect title into visState
|
||||
const firstSave = !Boolean(savedVis.id);
|
||||
stateContainer.transitions.setVis({
|
||||
title: savedVis.title,
|
||||
type: savedVis.type || stateContainer.getState().vis.type,
|
||||
|
@ -631,15 +668,23 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
|
|||
'data-test-subj': 'saveVisualizationSuccess',
|
||||
});
|
||||
|
||||
if ($scope.isAddToDashMode()) {
|
||||
if ($scope.getOriginatingApp() && saveOptions.returnToOrigin) {
|
||||
const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(savedVis.id)}`;
|
||||
|
||||
// Manually insert a new url so the back button will open the saved visualization.
|
||||
history.replace(appPath);
|
||||
setActiveUrl(appPath);
|
||||
const lastAppType = $scope.getOriginatingApp();
|
||||
let href = chrome.navLinks.get(lastAppType).url;
|
||||
|
||||
const lastDashboardUrl = chrome.navLinks.get('kibana:dashboard').url;
|
||||
const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardUrl, savedVis.id);
|
||||
history.push(dashboardUrl);
|
||||
// TODO: Remove this and use application.redirectTo after https://github.com/elastic/kibana/pull/63443
|
||||
if (lastAppType === 'kibana:dashboard') {
|
||||
const savedVisId = firstSave || savedVis.copyOnSave ? savedVis.id : '';
|
||||
href = addEmbeddableToDashboardUrl(href, savedVisId);
|
||||
history.push(href);
|
||||
} else {
|
||||
window.location.href = href;
|
||||
}
|
||||
} else if (savedVis.id === $route.current.params.id) {
|
||||
chrome.docTitle.change(savedVis.lastSavedTitle);
|
||||
chrome.setBreadcrumbs($injector.invoke(getEditBreadcrumbs));
|
||||
|
|
|
@ -33,8 +33,10 @@ export function addEmbeddableToDashboardUrl(dashboardUrl: string, embeddableId:
|
|||
const { url, query } = parseUrl(dashboardUrl);
|
||||
const [, dashboardId] = url.split(DashboardConstants.CREATE_NEW_DASHBOARD_URL);
|
||||
|
||||
query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = VISUALIZE_EMBEDDABLE_TYPE;
|
||||
query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId;
|
||||
if (embeddableId) {
|
||||
query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = VISUALIZE_EMBEDDABLE_TYPE;
|
||||
query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId;
|
||||
}
|
||||
|
||||
return `${DashboardConstants.CREATE_NEW_DASHBOARD_URL}${dashboardId}?${stringify(query)}`;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,8 @@ export default function({ getService, getPageObjects }) {
|
|||
await PageObjects.visualize.clickAreaChart();
|
||||
await PageObjects.visualize.clickNewSearch();
|
||||
await PageObjects.visualize.saveVisualizationExpectSuccess(
|
||||
'visualization from top nav add new panel'
|
||||
'visualization from top nav add new panel',
|
||||
{ redirectToOrigin: true }
|
||||
);
|
||||
await retry.try(async () => {
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
|
@ -64,7 +65,8 @@ export default function({ getService, getPageObjects }) {
|
|||
await PageObjects.visualize.clickAreaChart();
|
||||
await PageObjects.visualize.clickNewSearch();
|
||||
await PageObjects.visualize.saveVisualizationExpectSuccess(
|
||||
'visualization from add new link'
|
||||
'visualization from add new link',
|
||||
{ redirectToOrigin: true }
|
||||
);
|
||||
|
||||
await retry.try(async () => {
|
||||
|
|
79
test/functional/apps/dashboard/edit_embeddable_redirects.js
Normal file
79
test/functional/apps/dashboard/edit_embeddable_redirects.js
Normal 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 expect from '@kbn/expect';
|
||||
|
||||
export default function({ getService, getPageObjects }) {
|
||||
const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']);
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
|
||||
describe('edit embeddable redirects', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('dashboard/current/kibana');
|
||||
await kibanaServer.uiSettings.replace({
|
||||
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
|
||||
});
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.preserveCrossAppState();
|
||||
await PageObjects.dashboard.loadSavedDashboard('few panels');
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
});
|
||||
|
||||
it('redirects via save and return button after edit', async () => {
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.visualize.saveVisualizationAndReturn();
|
||||
});
|
||||
|
||||
it('redirects via save as button after edit, renaming itself', async () => {
|
||||
const newTitle = 'wowee, looks like I have a new title';
|
||||
const originalPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.visualize.saveVisualizationExpectSuccess(newTitle, {
|
||||
saveAsNew: false,
|
||||
redirectToOrigin: true,
|
||||
});
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const newPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(newPanelCount).to.eql(originalPanelCount);
|
||||
const titles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(titles.indexOf(newTitle)).to.not.be(-1);
|
||||
});
|
||||
|
||||
it('redirects via save as button after edit, adding a new panel', async () => {
|
||||
const newTitle = 'wowee, my title just got cooler';
|
||||
const originalPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.visualize.saveVisualizationExpectSuccess(newTitle, {
|
||||
saveAsNew: true,
|
||||
redirectToOrigin: true,
|
||||
});
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
const newPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(newPanelCount).to.eql(originalPanelCount + 1);
|
||||
const titles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(titles.indexOf(newTitle)).to.not.be(-1);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -51,6 +51,7 @@ export default function({ getService, loadTestFile }) {
|
|||
loadTestFile(require.resolve('./empty_dashboard'));
|
||||
loadTestFile(require.resolve('./embeddable_rendering'));
|
||||
loadTestFile(require.resolve('./create_and_add_embeddables'));
|
||||
loadTestFile(require.resolve('./edit_embeddable_redirects'));
|
||||
loadTestFile(require.resolve('./time_zones'));
|
||||
loadTestFile(require.resolve('./dashboard_options'));
|
||||
loadTestFile(require.resolve('./data_shared_attributes'));
|
||||
|
|
|
@ -136,7 +136,10 @@ export default function({ getService, getPageObjects }) {
|
|||
await dashboardAddPanel.clickAddNewEmbeddableLink('visualization');
|
||||
await PageObjects.visualize.clickAreaChart();
|
||||
await PageObjects.visualize.clickNewSearch();
|
||||
await PageObjects.visualize.saveVisualizationExpectSuccess('new viz panel');
|
||||
await PageObjects.visualize.saveVisualizationExpectSuccess('new viz panel', {
|
||||
saveAsNew: false,
|
||||
redirectToOrigin: true,
|
||||
});
|
||||
|
||||
await PageObjects.dashboard.clickCancelOutOfEditMode();
|
||||
// for this sleep see https://github.com/elastic/kibana/issues/22299
|
||||
|
|
|
@ -300,12 +300,25 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide
|
|||
}
|
||||
}
|
||||
|
||||
public async saveVisualization(vizName: string, { saveAsNew = false } = {}) {
|
||||
public async saveVisualization(
|
||||
vizName: string,
|
||||
{ saveAsNew = false, redirectToOrigin = false } = {}
|
||||
) {
|
||||
await this.ensureSavePanelOpen();
|
||||
await testSubjects.setValue('savedObjectTitle', vizName);
|
||||
if (saveAsNew) {
|
||||
log.debug('Check save as new visualization');
|
||||
await testSubjects.click('saveAsNewCheckbox');
|
||||
|
||||
const saveAsNewCheckboxExists = await testSubjects.exists('saveAsNewCheckbox');
|
||||
if (saveAsNewCheckboxExists) {
|
||||
const state = saveAsNew ? 'check' : 'uncheck';
|
||||
log.debug('save as new checkbox exists. Setting its state to', state);
|
||||
await testSubjects.setEuiSwitch('saveAsNewCheckbox', state);
|
||||
}
|
||||
|
||||
const redirectToOriginCheckboxExists = await testSubjects.exists('returnToOriginModeSwitch');
|
||||
if (redirectToOriginCheckboxExists) {
|
||||
const state = redirectToOrigin ? 'check' : 'uncheck';
|
||||
log.debug('redirect to origin checkbox exists. Setting its state to', state);
|
||||
await testSubjects.setEuiSwitch('returnToOriginModeSwitch', state);
|
||||
}
|
||||
log.debug('Click Save Visualization button');
|
||||
|
||||
|
@ -320,8 +333,11 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide
|
|||
return message;
|
||||
}
|
||||
|
||||
public async saveVisualizationExpectSuccess(vizName: string, { saveAsNew = false } = {}) {
|
||||
const saveMessage = await this.saveVisualization(vizName, { saveAsNew });
|
||||
public async saveVisualizationExpectSuccess(
|
||||
vizName: string,
|
||||
{ saveAsNew = false, redirectToOrigin = false } = {}
|
||||
) {
|
||||
const saveMessage = await this.saveVisualization(vizName, { saveAsNew, redirectToOrigin });
|
||||
if (!saveMessage) {
|
||||
throw new Error(
|
||||
`Expected saveVisualization to respond with the saveMessage from the toast, got ${saveMessage}`
|
||||
|
@ -331,14 +347,20 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide
|
|||
|
||||
public async saveVisualizationExpectSuccessAndBreadcrumb(
|
||||
vizName: string,
|
||||
{ saveAsNew = false } = {}
|
||||
{ saveAsNew = false, redirectToOrigin = false } = {}
|
||||
) {
|
||||
await this.saveVisualizationExpectSuccess(vizName, { saveAsNew });
|
||||
await this.saveVisualizationExpectSuccess(vizName, { saveAsNew, redirectToOrigin });
|
||||
await retry.waitFor(
|
||||
'last breadcrumb to have new vis name',
|
||||
async () => (await globalNav.getLastBreadcrumb()) === vizName
|
||||
);
|
||||
}
|
||||
|
||||
public async saveVisualizationAndReturn() {
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.existOrFail('visualizesaveAndReturnButton');
|
||||
await testSubjects.click('visualizesaveAndReturnButton');
|
||||
}
|
||||
}
|
||||
|
||||
return new VisualizePage();
|
||||
|
|
|
@ -116,7 +116,10 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) {
|
|||
await PageObjects.visualize.clickMarkdownWidget();
|
||||
await PageObjects.visEditor.setMarkdownTxt(markdown);
|
||||
await PageObjects.visEditor.clickGo();
|
||||
await PageObjects.visualize.saveVisualizationExpectSuccess(name);
|
||||
await PageObjects.visualize.saveVisualizationExpectSuccess(name, {
|
||||
saveAsNew: false,
|
||||
redirectToOrigin: true,
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -307,6 +307,7 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) {
|
|||
await element.scrollIntoViewIfNecessary();
|
||||
}
|
||||
|
||||
// isChecked always returns false when run on an euiSwitch, because they use the aria-checked attribute
|
||||
public async isChecked(selector: string) {
|
||||
const checkbox = await this.find(selector);
|
||||
return await checkbox.isSelected();
|
||||
|
@ -316,7 +317,22 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) {
|
|||
const isChecked = await this.isChecked(selector);
|
||||
const states = { check: true, uncheck: false };
|
||||
if (isChecked !== states[state]) {
|
||||
log.debug(`updating checkbox ${selector}`);
|
||||
log.debug(`updating checkbox ${selector} from ${isChecked} to ${states[state]}`);
|
||||
await this.click(selector);
|
||||
}
|
||||
}
|
||||
|
||||
public async isEuiSwitchChecked(selector: string) {
|
||||
const euiSwitch = await this.find(selector);
|
||||
const isChecked = await euiSwitch.getAttribute('aria-checked');
|
||||
return isChecked === 'true';
|
||||
}
|
||||
|
||||
public async setEuiSwitch(selector: string, state: 'check' | 'uncheck') {
|
||||
const isChecked = await this.isEuiSwitchChecked(selector);
|
||||
const states = { check: true, uncheck: false };
|
||||
if (isChecked !== states[state]) {
|
||||
log.debug(`updating checkbox ${selector} from ${isChecked} to ${states[state]}`);
|
||||
await this.click(selector);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,8 +104,8 @@ describe('Lens App', () => {
|
|||
storage: Storage;
|
||||
docId?: string;
|
||||
docStorage: SavedObjectStore;
|
||||
redirectTo: (id?: string) => void;
|
||||
addToDashboardMode?: boolean;
|
||||
redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void;
|
||||
originatingApp: string | undefined;
|
||||
}> {
|
||||
return ({
|
||||
navigation: navigationStartMock,
|
||||
|
@ -140,7 +140,7 @@ describe('Lens App', () => {
|
|||
load: jest.fn(),
|
||||
save: jest.fn(),
|
||||
},
|
||||
redirectTo: jest.fn(id => {}),
|
||||
redirectTo: jest.fn((id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => {}),
|
||||
} as unknown) as jest.Mocked<{
|
||||
navigation: typeof navigationStartMock;
|
||||
editorFrame: EditorFrameInstance;
|
||||
|
@ -149,8 +149,8 @@ describe('Lens App', () => {
|
|||
storage: Storage;
|
||||
docId?: string;
|
||||
docStorage: SavedObjectStore;
|
||||
redirectTo: (id?: string) => void;
|
||||
addToDashboardMode?: boolean;
|
||||
redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void;
|
||||
originatingApp: string | undefined;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
@ -336,6 +336,7 @@ describe('Lens App', () => {
|
|||
describe('save button', () => {
|
||||
interface SaveProps {
|
||||
newCopyOnSave: boolean;
|
||||
returnToOrigin?: boolean;
|
||||
newTitle: string;
|
||||
}
|
||||
|
||||
|
@ -347,8 +348,8 @@ describe('Lens App', () => {
|
|||
storage: Storage;
|
||||
docId?: string;
|
||||
docStorage: SavedObjectStore;
|
||||
redirectTo: (id?: string) => void;
|
||||
addToDashboardMode?: boolean;
|
||||
redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void;
|
||||
originatingApp: string | undefined;
|
||||
}>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -374,32 +375,25 @@ describe('Lens App', () => {
|
|||
|
||||
async function testSave(inst: ReactWrapper, saveProps: SaveProps) {
|
||||
await getButton(inst).run(inst.getDOMNode());
|
||||
|
||||
inst.update();
|
||||
|
||||
const handler = inst.findWhere(el => el.prop('onSave')).prop('onSave') as (
|
||||
const handler = inst.find('[data-test-subj="lnsApp_saveModalOrigin"]').prop('onSave') as (
|
||||
p: unknown
|
||||
) => void;
|
||||
handler(saveProps);
|
||||
}
|
||||
|
||||
async function save({
|
||||
initialDocId,
|
||||
addToDashboardMode,
|
||||
lastKnownDoc = { expression: 'kibana 3' },
|
||||
initialDocId,
|
||||
...saveProps
|
||||
}: SaveProps & {
|
||||
lastKnownDoc?: object;
|
||||
initialDocId?: string;
|
||||
addToDashboardMode?: boolean;
|
||||
}) {
|
||||
const args = {
|
||||
...defaultArgs,
|
||||
docId: initialDocId,
|
||||
};
|
||||
if (addToDashboardMode) {
|
||||
args.addToDashboardMode = addToDashboardMode;
|
||||
}
|
||||
args.editorFrame = frame;
|
||||
(args.docStorage.load as jest.Mock).mockResolvedValue({
|
||||
id: '1234',
|
||||
|
@ -438,7 +432,7 @@ describe('Lens App', () => {
|
|||
expect(getButton(instance).disableButton).toEqual(false);
|
||||
|
||||
await act(async () => {
|
||||
testSave(instance, saveProps);
|
||||
testSave(instance, { ...saveProps });
|
||||
});
|
||||
|
||||
return { args, instance };
|
||||
|
@ -527,7 +521,7 @@ describe('Lens App', () => {
|
|||
expression: 'kibana 3',
|
||||
});
|
||||
|
||||
expect(args.redirectTo).toHaveBeenCalledWith('aaa');
|
||||
expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true);
|
||||
|
||||
inst.setProps({ docId: 'aaa' });
|
||||
|
||||
|
@ -547,7 +541,7 @@ describe('Lens App', () => {
|
|||
expression: 'kibana 3',
|
||||
});
|
||||
|
||||
expect(args.redirectTo).toHaveBeenCalledWith('aaa');
|
||||
expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true);
|
||||
|
||||
inst.setProps({ docId: 'aaa' });
|
||||
|
||||
|
@ -601,10 +595,10 @@ describe('Lens App', () => {
|
|||
expect(getButton(instance).disableButton).toEqual(false);
|
||||
});
|
||||
|
||||
it('saves new doc and redirects to dashboard', async () => {
|
||||
it('saves new doc and redirects to originating app', async () => {
|
||||
const { args } = await save({
|
||||
initialDocId: undefined,
|
||||
addToDashboardMode: true,
|
||||
returnToOrigin: true,
|
||||
newCopyOnSave: false,
|
||||
newTitle: 'hello there',
|
||||
});
|
||||
|
@ -615,7 +609,7 @@ describe('Lens App', () => {
|
|||
title: 'hello there',
|
||||
});
|
||||
|
||||
expect(args.redirectTo).toHaveBeenCalledWith('aaa');
|
||||
expect(args.redirectTo).toHaveBeenCalledWith('aaa', true, true);
|
||||
});
|
||||
|
||||
it('saves app filters and does not save pinned filters', async () => {
|
||||
|
@ -666,7 +660,6 @@ describe('Lens App', () => {
|
|||
})
|
||||
);
|
||||
instance.update();
|
||||
|
||||
await act(async () => getButton(instance).run(instance.getDOMNode()));
|
||||
instance.update();
|
||||
|
||||
|
@ -684,7 +677,7 @@ describe('Lens App', () => {
|
|||
storage: Storage;
|
||||
docId?: string;
|
||||
docStorage: SavedObjectStore;
|
||||
redirectTo: (id?: string) => void;
|
||||
redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void;
|
||||
}>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -12,9 +12,11 @@ import { Query, DataPublicPluginStart } from 'src/plugins/data/public';
|
|||
import { NavigationPublicPluginStart } from 'src/plugins/navigation/public';
|
||||
import { AppMountContext, NotificationsStart } from 'kibana/public';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { SavedObjectSaveModal } from '../../../../../src/plugins/saved_objects/public';
|
||||
import {
|
||||
SavedObjectSaveModalOrigin,
|
||||
OnSaveProps,
|
||||
} from '../../../../../src/plugins/saved_objects/public';
|
||||
import { Document, SavedObjectStore } from '../persistence';
|
||||
import { EditorFrameInstance } from '../types';
|
||||
import { NativeRenderer } from '../native_renderer';
|
||||
|
@ -52,7 +54,7 @@ export function App({
|
|||
docId,
|
||||
docStorage,
|
||||
redirectTo,
|
||||
addToDashboardMode,
|
||||
originatingApp,
|
||||
navigation,
|
||||
}: {
|
||||
editorFrame: EditorFrameInstance;
|
||||
|
@ -62,8 +64,8 @@ export function App({
|
|||
storage: IStorageWrapper;
|
||||
docId?: string;
|
||||
docStorage: SavedObjectStore;
|
||||
redirectTo: (id?: string) => void;
|
||||
addToDashboardMode?: boolean;
|
||||
redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void;
|
||||
originatingApp?: string | undefined;
|
||||
}) {
|
||||
const language =
|
||||
storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage');
|
||||
|
@ -182,6 +184,63 @@ export function App({
|
|||
lastKnownDoc.expression.length > 0 &&
|
||||
core.application.capabilities.visualize.save;
|
||||
|
||||
const runSave = (
|
||||
saveProps: Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
}
|
||||
) => {
|
||||
if (!lastKnownDoc) {
|
||||
return;
|
||||
}
|
||||
const [pinnedFilters, appFilters] = _.partition(
|
||||
lastKnownDoc.state?.filters,
|
||||
esFilters.isFilterPinned
|
||||
);
|
||||
const lastDocWithoutPinned = pinnedFilters?.length
|
||||
? {
|
||||
...lastKnownDoc,
|
||||
state: {
|
||||
...lastKnownDoc.state,
|
||||
filters: appFilters,
|
||||
},
|
||||
}
|
||||
: lastKnownDoc;
|
||||
|
||||
const doc = {
|
||||
...lastDocWithoutPinned,
|
||||
id: saveProps.newCopyOnSave ? undefined : lastKnownDoc.id,
|
||||
title: saveProps.newTitle,
|
||||
};
|
||||
|
||||
const newlyCreated: boolean = saveProps.newCopyOnSave || !lastKnownDoc?.id;
|
||||
docStorage
|
||||
.save(doc)
|
||||
.then(({ id }) => {
|
||||
// Prevents unnecessary network request and disables save button
|
||||
const newDoc = { ...doc, id };
|
||||
setState(s => ({
|
||||
...s,
|
||||
isSaveModalVisible: false,
|
||||
persistedDoc: newDoc,
|
||||
lastKnownDoc: newDoc,
|
||||
}));
|
||||
if (docId !== id || saveProps.returnToOrigin) {
|
||||
redirectTo(id, saveProps.returnToOrigin, newlyCreated);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.dir(e);
|
||||
trackUiEvent('save_failed');
|
||||
core.notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.lens.app.docSavingError', {
|
||||
defaultMessage: 'Error saving document',
|
||||
})
|
||||
);
|
||||
setState(s => ({ ...s, isSaveModalVisible: false }));
|
||||
});
|
||||
};
|
||||
|
||||
const onError = useCallback(
|
||||
(e: { message: string }) =>
|
||||
core.notifications.toasts.addDanger({
|
||||
|
@ -192,13 +251,6 @@ export function App({
|
|||
|
||||
const { TopNavMenu } = navigation.ui;
|
||||
|
||||
const confirmButton = addToDashboardMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.lens.app.saveAddToDashboard"
|
||||
defaultMessage="Save and add to dashboard"
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider
|
||||
|
@ -213,10 +265,39 @@ export function App({
|
|||
<div className="lnsApp__header">
|
||||
<TopNavMenu
|
||||
config={[
|
||||
...(!!originatingApp && lastKnownDoc?.id
|
||||
? [
|
||||
{
|
||||
label: i18n.translate('xpack.lens.app.saveAndReturn', {
|
||||
defaultMessage: 'Save and return',
|
||||
}),
|
||||
emphasize: true,
|
||||
iconType: 'check',
|
||||
run: () => {
|
||||
if (isSaveable && lastKnownDoc) {
|
||||
runSave({
|
||||
newTitle: lastKnownDoc.title,
|
||||
newCopyOnSave: false,
|
||||
isTitleDuplicateConfirmed: false,
|
||||
returnToOrigin: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
testId: 'lnsApp_saveAndReturnButton',
|
||||
disableButton: !isSaveable,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: i18n.translate('xpack.lens.app.save', {
|
||||
defaultMessage: 'Save',
|
||||
}),
|
||||
label:
|
||||
lastKnownDoc?.id && !!originatingApp
|
||||
? i18n.translate('xpack.lens.app.saveAs', {
|
||||
defaultMessage: 'Save as',
|
||||
})
|
||||
: i18n.translate('xpack.lens.app.save', {
|
||||
defaultMessage: 'Save',
|
||||
}),
|
||||
emphasize: !originatingApp || !lastKnownDoc?.id,
|
||||
run: () => {
|
||||
if (isSaveable && lastKnownDoc) {
|
||||
setState(s => ({ ...s, isSaveModalVisible: true }));
|
||||
|
@ -336,63 +417,18 @@ export function App({
|
|||
)}
|
||||
</div>
|
||||
{lastKnownDoc && state.isSaveModalVisible && (
|
||||
<SavedObjectSaveModal
|
||||
onSave={props => {
|
||||
const [pinnedFilters, appFilters] = _.partition(
|
||||
lastKnownDoc.state?.filters,
|
||||
esFilters.isFilterPinned
|
||||
);
|
||||
const lastDocWithoutPinned = pinnedFilters?.length
|
||||
? {
|
||||
...lastKnownDoc,
|
||||
state: {
|
||||
...lastKnownDoc.state,
|
||||
filters: appFilters,
|
||||
},
|
||||
}
|
||||
: lastKnownDoc;
|
||||
|
||||
const doc = {
|
||||
...lastDocWithoutPinned,
|
||||
id: props.newCopyOnSave ? undefined : lastKnownDoc.id,
|
||||
title: props.newTitle,
|
||||
};
|
||||
|
||||
docStorage
|
||||
.save(doc)
|
||||
.then(({ id }) => {
|
||||
// Prevents unnecessary network request and disables save button
|
||||
const newDoc = { ...doc, id };
|
||||
setState(s => ({
|
||||
...s,
|
||||
isSaveModalVisible: false,
|
||||
persistedDoc: newDoc,
|
||||
lastKnownDoc: newDoc,
|
||||
}));
|
||||
if (docId !== id) {
|
||||
redirectTo(id);
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.dir(e);
|
||||
trackUiEvent('save_failed');
|
||||
core.notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.lens.app.docSavingError', {
|
||||
defaultMessage: 'Error saving document',
|
||||
})
|
||||
);
|
||||
setState(s => ({ ...s, isSaveModalVisible: false }));
|
||||
});
|
||||
}}
|
||||
<SavedObjectSaveModalOrigin
|
||||
originatingApp={originatingApp}
|
||||
onSave={props => runSave(props)}
|
||||
onClose={() => setState(s => ({ ...s, isSaveModalVisible: false }))}
|
||||
title={lastKnownDoc.title || ''}
|
||||
showCopyOnSave={!!lastKnownDoc.id && !addToDashboardMode}
|
||||
documentInfo={{
|
||||
id: lastKnownDoc.id,
|
||||
title: lastKnownDoc.title || '',
|
||||
}}
|
||||
objectType={i18n.translate('xpack.lens.app.saveModalType', {
|
||||
defaultMessage: 'Lens visualization',
|
||||
})}
|
||||
showDescription={false}
|
||||
confirmButtonLabel={confirmButton}
|
||||
data-test-subj="lnsApp_saveModalOrigin"
|
||||
/>
|
||||
)}
|
||||
</KibanaContextProvider>
|
||||
|
|
|
@ -11,8 +11,8 @@ import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom
|
|||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import rison from 'rison-node';
|
||||
import { DashboardConstants } from '../../../../../src/plugins/dashboard/public';
|
||||
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { parse } from 'query-string';
|
||||
import { Storage, removeQueryParam } from '../../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry';
|
||||
|
||||
|
@ -52,33 +52,48 @@ export async function mountApp(
|
|||
};
|
||||
const redirectTo = (
|
||||
routeProps: RouteComponentProps<{ id?: string }>,
|
||||
addToDashboardMode: boolean,
|
||||
id?: string
|
||||
originatingApp: string,
|
||||
id?: string,
|
||||
returnToOrigin?: boolean,
|
||||
newlyCreated?: boolean
|
||||
) => {
|
||||
if (!!originatingApp && !returnToOrigin) {
|
||||
removeQueryParam(routeProps.history, 'embeddableOriginatingApp');
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
routeProps.history.push('/lens');
|
||||
} else if (!addToDashboardMode) {
|
||||
} else if (!originatingApp) {
|
||||
routeProps.history.push(`/lens/edit/${id}`);
|
||||
} else if (addToDashboardMode && id) {
|
||||
} else if (!!originatingApp && id && returnToOrigin) {
|
||||
routeProps.history.push(`/lens/edit/${id}`);
|
||||
const lastDashboardLink = coreStart.chrome.navLinks.get('kibana:dashboard');
|
||||
if (!lastDashboardLink || !lastDashboardLink.url) {
|
||||
throw new Error('Cannot get last dashboard url');
|
||||
const originatingAppLink = coreStart.chrome.navLinks.get(originatingApp);
|
||||
if (!originatingAppLink || !originatingAppLink.url) {
|
||||
throw new Error('Cannot get originating app url');
|
||||
}
|
||||
|
||||
// TODO: Remove this and use application.redirectTo after https://github.com/elastic/kibana/pull/63443
|
||||
if (originatingApp === 'kibana:dashboard') {
|
||||
const addLensId = newlyCreated ? id : '';
|
||||
const urlVars = getUrlVars(originatingAppLink.url);
|
||||
updateUrlTime(urlVars); // we need to pass in timerange in query params directly
|
||||
const dashboardUrl = addEmbeddableToDashboardUrl(
|
||||
originatingAppLink.url,
|
||||
addLensId,
|
||||
urlVars
|
||||
);
|
||||
window.history.pushState({}, '', dashboardUrl);
|
||||
} else {
|
||||
window.location.href = originatingAppLink.url;
|
||||
}
|
||||
const urlVars = getUrlVars(lastDashboardLink.url);
|
||||
updateUrlTime(urlVars); // we need to pass in timerange in query params directly
|
||||
const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardLink.url, id, urlVars);
|
||||
window.history.pushState({}, '', dashboardUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => {
|
||||
trackUiEvent('loaded');
|
||||
const addToDashboardMode =
|
||||
!!routeProps.location.search &&
|
||||
routeProps.location.search.includes(
|
||||
DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM
|
||||
);
|
||||
const urlParams = parse(routeProps.location.search) as Record<string, string>;
|
||||
const originatingApp = urlParams.embeddableOriginatingApp;
|
||||
|
||||
return (
|
||||
<App
|
||||
core={coreStart}
|
||||
|
@ -88,8 +103,10 @@ export async function mountApp(
|
|||
storage={new Storage(localStorage)}
|
||||
docId={routeProps.match.params.id}
|
||||
docStorage={new SavedObjectIndexStore(savedObjectsClient)}
|
||||
redirectTo={id => redirectTo(routeProps, addToDashboardMode, id)}
|
||||
addToDashboardMode={addToDashboardMode}
|
||||
redirectTo={(id, returnToOrigin, newlyCreated) =>
|
||||
redirectTo(routeProps, originatingApp, id, returnToOrigin, newlyCreated)
|
||||
}
|
||||
originatingApp={originatingApp}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -37,8 +37,10 @@ export function addEmbeddableToDashboardUrl(url: string, embeddableId: string, u
|
|||
keys.forEach(key => {
|
||||
dashboardParsedUrl.query[key] = urlVars[key];
|
||||
});
|
||||
dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = 'lens';
|
||||
dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId;
|
||||
if (embeddableId) {
|
||||
dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = 'lens';
|
||||
dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId;
|
||||
}
|
||||
const query = stringify(dashboardParsedUrl.query);
|
||||
|
||||
return `${dashboardParsedUrl.url}?${query}`;
|
||||
|
|
|
@ -3969,7 +3969,6 @@
|
|||
"visualize.listing.table.titleColumnName": "タイトル",
|
||||
"visualize.listing.table.typeColumnName": "タイプ",
|
||||
"visualize.pageHeading": "{chartName} {chartType} ビジュアライゼーション",
|
||||
"visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存してダッシュボードに追加",
|
||||
"visualize.topNavMenu.openInspectorButtonAriaLabel": "ビジュアライゼーションのインスペクターを開く",
|
||||
"visualize.topNavMenu.openInspectorDisabledButtonTooltip": "このビジュアライゼーションはインスペクターをサポートしていません。",
|
||||
"visualize.topNavMenu.saveVisualization.failureNotificationText": "「{visTitle}」の保存中にエラーが発生しました",
|
||||
|
@ -8363,7 +8362,6 @@
|
|||
"xpack.lens.app.docSavingError": "ドキュメントの保存中にエラーが発生",
|
||||
"xpack.lens.app.indexPatternLoadingError": "インデックスパターンの読み込み中にエラーが発生",
|
||||
"xpack.lens.app.save": "保存",
|
||||
"xpack.lens.app.saveAddToDashboard": "保存してダッシュボードに追加",
|
||||
"xpack.lens.app.saveModalType": "レンズビジュアライゼーション",
|
||||
"xpack.lens.app404": "404 Not Found",
|
||||
"xpack.lens.breadcrumbsCreate": "作成",
|
||||
|
|
|
@ -3970,7 +3970,6 @@
|
|||
"visualize.listing.table.titleColumnName": "标题",
|
||||
"visualize.listing.table.typeColumnName": "类型",
|
||||
"visualize.pageHeading": "{chartName} {chartType}可视化",
|
||||
"visualize.saveDialog.saveAndAddToDashboardButtonLabel": "保存并添加到仪表板",
|
||||
"visualize.topNavMenu.openInspectorButtonAriaLabel": "打开检查器查看可视化",
|
||||
"visualize.topNavMenu.openInspectorDisabledButtonTooltip": "此可视化不支持任何检查器。",
|
||||
"visualize.topNavMenu.saveVisualization.failureNotificationText": "保存 “{visTitle}” 时出错",
|
||||
|
@ -8369,7 +8368,6 @@
|
|||
"xpack.lens.app.docSavingError": "保存文档时出错",
|
||||
"xpack.lens.app.indexPatternLoadingError": "加载索引模式时出错",
|
||||
"xpack.lens.app.save": "保存",
|
||||
"xpack.lens.app.saveAddToDashboard": "保存并添加到仪表板",
|
||||
"xpack.lens.app.saveModalType": "Lens 可视化",
|
||||
"xpack.lens.app404": "404 找不到",
|
||||
"xpack.lens.breadcrumbsCreate": "创建",
|
||||
|
|
|
@ -4,11 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
export default function({ getPageObjects, getService }) {
|
||||
const log = getService('log');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const dashboardVisualizations = getService('dashboardVisualizations');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens']);
|
||||
|
||||
describe('empty dashboard', function() {
|
||||
|
@ -52,7 +55,7 @@ export default function({ getPageObjects, getService }) {
|
|||
operation: 'terms',
|
||||
field: 'ip',
|
||||
});
|
||||
await PageObjects.lens.save(title);
|
||||
await PageObjects.lens.save(title, false, true);
|
||||
}
|
||||
|
||||
it('adds Lens visualization to empty dashboard', async () => {
|
||||
|
@ -64,5 +67,39 @@ export default function({ getPageObjects, getService }) {
|
|||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await testSubjects.exists(`embeddablePanelHeading-${title}`);
|
||||
});
|
||||
|
||||
it('redirects via save and return button after edit', async () => {
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.lens.saveAndReturn();
|
||||
});
|
||||
|
||||
it('redirects via save as button after edit, renaming itself', async () => {
|
||||
const newTitle = 'wowee, looks like I have a new title';
|
||||
const originalPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.lens.save(newTitle, false, true);
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const newPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(newPanelCount).to.eql(originalPanelCount);
|
||||
const titles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(titles.indexOf(newTitle)).to.not.be(-1);
|
||||
});
|
||||
|
||||
it('redirects via save as button after edit, adding a new panel', async () => {
|
||||
const newTitle = 'wowee, my title just got cooler';
|
||||
const originalPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.lens.save(newTitle, true, true);
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const newPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(newPanelCount).to.eql(originalPanelCount + 1);
|
||||
const titles = await PageObjects.dashboard.getPanelTitles();
|
||||
expect(titles.indexOf(newTitle)).to.not.be(-1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -133,9 +133,22 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
/**
|
||||
* Save the current Lens visualization.
|
||||
*/
|
||||
async save(title: string) {
|
||||
async save(title: string, saveAsNew?: boolean, redirectToOrigin?: boolean) {
|
||||
await testSubjects.click('lnsApp_saveButton');
|
||||
await testSubjects.setValue('savedObjectTitle', title);
|
||||
|
||||
const saveAsNewCheckboxExists = await testSubjects.exists('saveAsNewCheckbox');
|
||||
if (saveAsNewCheckboxExists) {
|
||||
const state = saveAsNew ? 'check' : 'uncheck';
|
||||
await testSubjects.setEuiSwitch('saveAsNewCheckbox', state);
|
||||
}
|
||||
|
||||
const redirectToOriginCheckboxExists = await testSubjects.exists('returnToOriginModeSwitch');
|
||||
if (redirectToOriginCheckboxExists) {
|
||||
const state = redirectToOrigin ? 'check' : 'uncheck';
|
||||
await testSubjects.setEuiSwitch('returnToOriginModeSwitch', state);
|
||||
}
|
||||
|
||||
await testSubjects.click('confirmSaveSavedObjectButton');
|
||||
retry.waitForWithTimeout('Save modal to disappear', 1000, () =>
|
||||
testSubjects
|
||||
|
@ -145,6 +158,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
);
|
||||
},
|
||||
|
||||
async saveAndReturn() {
|
||||
await testSubjects.click('lnsApp_saveAndReturnButton');
|
||||
},
|
||||
|
||||
getTitle() {
|
||||
return testSubjects.getVisibleText('lns_ChartTitle');
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue