mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# Backport This will backport the following commits from `main` to `8.9`: - [[Dashboard] Redesign clone experience (#159752)](https://github.com/elastic/kibana/pull/159752) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Catherine Liu","email":"catherine.liu@elastic.co"},"sourceCommit":{"committedDate":"2023-07-01T03:45:39Z","message":"[Dashboard] Redesign clone experience (#159752)\n\n## Summary\r\n\r\nCloses #154500. \r\nCloses https://github.com/elastic/kibana/issues/114206.\r\n\r\nThis updates the UX around cloning dashboards. I removed the clone\r\nconfirm model, so cloning a dashboard is just a one click action now.\r\nThis aligns with how other apps like ML handle cloning. I've also made\r\nthe dashboard title breadcrumb a button in edit mode that opens the\r\ndashboard settings flyout for better discoverability.\r\n\r\n\r\n4f5ea117
-a5e4-4ec5-9113-8b09fd8c84a1\r\n\r\nI also changed the pattern for cloned dashboard title from `Dashboard\r\nTitle Copy` to `Dashboard Title (#)`.\r\n<img width=\"1226\" alt=\"Screenshot 2023-06-30 at 1 03 35 PM\"\r\nsrc=\"b50ba5c6
-dc95-4aab-a320-b1a78b74c1b6\">","sha":"938716e58aa7b4ee37233529510ac0956cfe888c","branchLabelMapping":{"^v8.10.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Feature:Dashboard","Team:Presentation","loe:hours","impact:medium","backport:skip","ui-copy","v8.10.0"],"number":159752,"url":"https://github.com/elastic/kibana/pull/159752","mergeCommit":{"message":"[Dashboard] Redesign clone experience (#159752)\n\n## Summary\r\n\r\nCloses #154500. \r\nCloses https://github.com/elastic/kibana/issues/114206.\r\n\r\nThis updates the UX around cloning dashboards. I removed the clone\r\nconfirm model, so cloning a dashboard is just a one click action now.\r\nThis aligns with how other apps like ML handle cloning. I've also made\r\nthe dashboard title breadcrumb a button in edit mode that opens the\r\ndashboard settings flyout for better discoverability.\r\n\r\n\r\n4f5ea117
-a5e4-4ec5-9113-8b09fd8c84a1\r\n\r\nI also changed the pattern for cloned dashboard title from `Dashboard\r\nTitle Copy` to `Dashboard Title (#)`.\r\n<img width=\"1226\" alt=\"Screenshot 2023-06-30 at 1 03 35 PM\"\r\nsrc=\"b50ba5c6
-dc95-4aab-a320-b1a78b74c1b6\">","sha":"938716e58aa7b4ee37233529510ac0956cfe888c"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.10.0","labelRegex":"^v8.10.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/159752","number":159752,"mergeCommit":{"message":"[Dashboard] Redesign clone experience (#159752)\n\n## Summary\r\n\r\nCloses #154500. \r\nCloses https://github.com/elastic/kibana/issues/114206.\r\n\r\nThis updates the UX around cloning dashboards. I removed the clone\r\nconfirm model, so cloning a dashboard is just a one click action now.\r\nThis aligns with how other apps like ML handle cloning. I've also made\r\nthe dashboard title breadcrumb a button in edit mode that opens the\r\ndashboard settings flyout for better discoverability.\r\n\r\n\r\n4f5ea117
-a5e4-4ec5-9113-8b09fd8c84a1\r\n\r\nI also changed the pattern for cloned dashboard title from `Dashboard\r\nTitle Copy` to `Dashboard Title (#)`.\r\n<img width=\"1226\" alt=\"Screenshot 2023-06-30 at 1 03 35 PM\"\r\nsrc=\"b50ba5c6
-dc95-4aab-a320-b1a78b74c1b6\">","sha":"938716e58aa7b4ee37233529510ac0956cfe888c"}}]}] BACKPORT--> Co-authored-by: Catherine Liu <catherine.liu@elastic.co>
This commit is contained in:
parent
7a27f80ab5
commit
b9e0b75b3c
15 changed files with 97 additions and 504 deletions
|
@ -17,7 +17,7 @@ import {
|
|||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
||||
import { EuiHorizontalRule, EuiToolTipProps } from '@elastic/eui';
|
||||
import { EuiHorizontalRule, EuiIcon, EuiToolTipProps } from '@elastic/eui';
|
||||
import {
|
||||
getDashboardTitle,
|
||||
leaveConfirmStrings,
|
||||
|
@ -144,10 +144,23 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
},
|
||||
},
|
||||
{
|
||||
text: dashboardTitle,
|
||||
text:
|
||||
viewMode === ViewMode.EDIT ? (
|
||||
<>
|
||||
{dashboardTitle} <EuiIcon size="s" type="pencil" />
|
||||
</>
|
||||
) : (
|
||||
dashboardTitle
|
||||
),
|
||||
onClick:
|
||||
viewMode === ViewMode.EDIT
|
||||
? () => {
|
||||
dashboard.showSettings();
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
]);
|
||||
}, [setBreadcrumbs, redirectTo, dashboardTitle]);
|
||||
}, [setBreadcrumbs, redirectTo, dashboardTitle, dashboard, viewMode]);
|
||||
|
||||
/**
|
||||
* Build app leave handler whenever hasUnsavedChanges changes
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { extractTitleAndCount } from './extract_title_and_count';
|
||||
|
||||
describe('extractTitleAndCount', () => {
|
||||
it('extracts base title and copy count from a cloned dashboard title', () => {
|
||||
expect(extractTitleAndCount('Test dashboard (1)')).toEqual(['Test dashboard', 1]);
|
||||
expect(extractTitleAndCount('Test dashboard (2)')).toEqual(['Test dashboard', 2]);
|
||||
expect(extractTitleAndCount('Test dashboard (200)')).toEqual(['Test dashboard', 200]);
|
||||
expect(extractTitleAndCount('Test dashboard (1) (2) (3) (4) (5)')).toEqual([
|
||||
'Test dashboard (1) (2) (3) (4)',
|
||||
5,
|
||||
]);
|
||||
});
|
||||
|
||||
it('defaults to the count to 1 and returns the original title when the provided title does not contain a valid count', () => {
|
||||
expect(extractTitleAndCount('Test dashboard')).toEqual(['Test dashboard', 1]);
|
||||
expect(extractTitleAndCount('Test dashboard 2')).toEqual(['Test dashboard 2', 1]);
|
||||
expect(extractTitleAndCount('Test dashboard (-1)')).toEqual(['Test dashboard (-1)', 1]);
|
||||
expect(extractTitleAndCount('Test dashboard (0)')).toEqual(['Test dashboard (0)', 1]);
|
||||
expect(extractTitleAndCount('Test dashboard (3.0)')).toEqual(['Test dashboard (3.0)', 1]);
|
||||
expect(extractTitleAndCount('Test dashboard (8.4)')).toEqual(['Test dashboard (8.4)', 1]);
|
||||
expect(extractTitleAndCount('Test dashboard (foo3.0)')).toEqual(['Test dashboard (foo3.0)', 1]);
|
||||
expect(extractTitleAndCount('Test dashboard (bar7)')).toEqual(['Test dashboard (bar7)', 1]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const extractTitleAndCount = (title: string): [string, number] => {
|
||||
if (title.slice(-1) === ')') {
|
||||
const startIndex = title.lastIndexOf(' (');
|
||||
const count = title.substring(startIndex + 2, title.lastIndexOf(')'));
|
||||
if (!count.includes('.') && Number.isInteger(Number(count)) && Number(count) >= 1) {
|
||||
const baseTitle = title.substring(0, startIndex);
|
||||
return [baseTitle, Number(count)];
|
||||
}
|
||||
}
|
||||
return [title, 1];
|
||||
};
|
|
@ -1,65 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders DashboardCloneModal 1`] = `
|
||||
<EuiModal
|
||||
className="dshCloneModal"
|
||||
data-test-subj="dashboardCloneModal"
|
||||
onClose={[Function]}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Clone dashboard"
|
||||
id="dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Please enter a new name for your dashboard."
|
||||
id="dashboard.topNav.cloneModal.enterNewNameForDashboardDescription"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiFieldText
|
||||
aria-label="Cloned Dashboard Title"
|
||||
autoFocus={true}
|
||||
data-test-subj="clonedDashboardTitle"
|
||||
isInvalid={false}
|
||||
onChange={[Function]}
|
||||
value="dash title"
|
||||
/>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="cloneCancelButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="dashboard.topNav.cloneModal.cancelButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="cloneConfirmButton"
|
||||
fill={true}
|
||||
isLoading={false}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Confirm Clone"
|
||||
id="dashboard.topNav.cloneModal.confirmButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
`;
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { shallowWithI18nProvider, mountWithI18nProvider } from '@kbn/test-jest-helpers';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
import { DashboardCloneModal } from './clone_modal';
|
||||
|
||||
let onClone;
|
||||
let onClose;
|
||||
|
||||
beforeEach(() => {
|
||||
onClone = sinon.spy();
|
||||
onClose = sinon.spy();
|
||||
});
|
||||
|
||||
test('renders DashboardCloneModal', () => {
|
||||
const component = shallowWithI18nProvider(
|
||||
<DashboardCloneModal title="dash title" onClose={onClose} onClone={onClone} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('onClone', () => {
|
||||
const component = mountWithI18nProvider(
|
||||
<DashboardCloneModal title="dash title" onClose={onClose} onClone={onClone} />
|
||||
);
|
||||
findTestSubject(component, 'cloneConfirmButton').simulate('click');
|
||||
sinon.assert.calledWith(onClone, 'dash title');
|
||||
sinon.assert.notCalled(onClose);
|
||||
});
|
||||
|
||||
test('onClose', () => {
|
||||
const component = mountWithI18nProvider(
|
||||
<DashboardCloneModal title="dash title" onClose={onClose} onClone={onClone} />
|
||||
);
|
||||
findTestSubject(component, 'cloneCancelButton').simulate('click');
|
||||
sinon.assert.calledOnce(onClose);
|
||||
sinon.assert.notCalled(onClone);
|
||||
});
|
||||
|
||||
test('title', () => {
|
||||
const component = mountWithI18nProvider(
|
||||
<DashboardCloneModal title="dash title" onClose={onClose} onClone={onClone} />
|
||||
);
|
||||
const event = { target: { value: 'a' } };
|
||||
component.find('input').simulate('change', event);
|
||||
findTestSubject(component, 'cloneConfirmButton').simulate('click');
|
||||
sinon.assert.calledWith(onClone, 'a');
|
||||
});
|
|
@ -1,203 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFieldText,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
onClone: (
|
||||
newTitle: string,
|
||||
isTitleDuplicateConfirmed: boolean,
|
||||
onTitleDuplicate: () => void
|
||||
) => Promise<void>;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
newDashboardName: string;
|
||||
isTitleDuplicateConfirmed: boolean;
|
||||
hasTitleDuplicate: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export class DashboardCloneModal extends React.Component<Props, State> {
|
||||
private isMounted = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
newDashboardName: props.title,
|
||||
isTitleDuplicateConfirmed: false,
|
||||
hasTitleDuplicate: false,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
onTitleDuplicate = () => {
|
||||
this.setState({
|
||||
isTitleDuplicateConfirmed: true,
|
||||
hasTitleDuplicate: true,
|
||||
});
|
||||
};
|
||||
|
||||
cloneDashboard = async () => {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
await this.props.onClone(
|
||||
this.state.newDashboardName,
|
||||
this.state.isTitleDuplicateConfirmed,
|
||||
this.onTitleDuplicate
|
||||
);
|
||||
|
||||
if (this.isMounted) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onInputChange = (event: any) => {
|
||||
this.setState({
|
||||
newDashboardName: event.target.value,
|
||||
isTitleDuplicateConfirmed: false,
|
||||
hasTitleDuplicate: false,
|
||||
});
|
||||
};
|
||||
|
||||
renderDuplicateTitleCallout = () => {
|
||||
if (!this.state.hasTitleDuplicate) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer />
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={i18n.translate('dashboard.topNav.cloneModal.dashboardExistsTitle', {
|
||||
defaultMessage: 'A dashboard with the title {newDashboardName} already exists.',
|
||||
values: {
|
||||
newDashboardName: `'${this.state.newDashboardName}'`,
|
||||
},
|
||||
})}
|
||||
color="warning"
|
||||
data-test-subj="titleDupicateWarnMsg"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="dashboard.topNav.cloneModal.dashboardExistsDescription"
|
||||
defaultMessage="Click {confirmClone} to clone the dashboard with the duplicate title."
|
||||
values={{
|
||||
confirmClone: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="dashboard.topNav.cloneModal.confirmCloneDescription"
|
||||
defaultMessage="Confirm Clone"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EuiModal
|
||||
data-test-subj="dashboardCloneModal"
|
||||
className="dshCloneModal"
|
||||
onClose={this.props.onClose}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle"
|
||||
defaultMessage="Clone dashboard"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="dashboard.topNav.cloneModal.enterNewNameForDashboardDescription"
|
||||
defaultMessage="Please enter a new name for your dashboard."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiFieldText
|
||||
autoFocus
|
||||
aria-label={i18n.translate('dashboard.cloneModal.cloneDashboardTitleAriaLabel', {
|
||||
defaultMessage: 'Cloned Dashboard Title',
|
||||
})}
|
||||
data-test-subj="clonedDashboardTitle"
|
||||
value={this.state.newDashboardName}
|
||||
onChange={this.onInputChange}
|
||||
isInvalid={this.state.hasTitleDuplicate}
|
||||
/>
|
||||
|
||||
{this.renderDuplicateTitleCallout()}
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty data-test-subj="cloneCancelButton" onClick={this.props.onClose}>
|
||||
<FormattedMessage
|
||||
id="dashboard.topNav.cloneModal.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
fill
|
||||
data-test-subj="cloneConfirmButton"
|
||||
onClick={this.cloneDashboard}
|
||||
isLoading={this.state.isLoading}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="dashboard.topNav.cloneModal.confirmButtonLabel"
|
||||
defaultMessage="Confirm Clone"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { DashboardCloneModal } from './clone_modal';
|
||||
import { pluginServices } from '../../../../services/plugin_services';
|
||||
|
||||
export interface ShowCloneModalProps {
|
||||
onClose: () => void;
|
||||
onClone: (
|
||||
newTitle: string,
|
||||
isTitleDuplicateConfirmed: boolean,
|
||||
onTitleDuplicate: () => void
|
||||
) => Promise<{ id?: string } | { error: Error }>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function showCloneModal({ onClone, title, onClose }: ShowCloneModalProps) {
|
||||
const {
|
||||
settings: { theme },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const container = document.createElement('div');
|
||||
const closeModal = () => {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
document.body.removeChild(container);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCloneConfirmed = async (
|
||||
newTitle: string,
|
||||
isTitleDuplicateConfirmed: boolean,
|
||||
onTitleDuplicate: () => void
|
||||
) => {
|
||||
onClone(newTitle, isTitleDuplicateConfirmed, onTitleDuplicate).then(
|
||||
(response: { id?: string } | { error: Error }) => {
|
||||
// The only time you don't want to close the modal is if it's asking you
|
||||
// to confirm a duplicate title, in which case there will be no error and no id.
|
||||
if ((response as { error: Error }).error || (response as { id?: string }).id) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
document.body.appendChild(container);
|
||||
const element = (
|
||||
<I18nProvider>
|
||||
<KibanaThemeProvider theme$={theme.theme$}>
|
||||
<DashboardCloneModal
|
||||
onClone={onCloneConfirmed}
|
||||
onClose={closeModal}
|
||||
title={i18n.translate('dashboard.topNav.showCloneModal.dashboardCopyTitle', {
|
||||
defaultMessage: '{title} Copy',
|
||||
values: { title },
|
||||
})}
|
||||
/>
|
||||
</KibanaThemeProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
ReactDOM.render(element, container);
|
||||
}
|
|
@ -15,10 +15,10 @@ import { DASHBOARD_CONTENT_ID, SAVED_OBJECT_POST_TIME } from '../../../dashboard
|
|||
import { DashboardSaveOptions, DashboardStateFromSaveModal } from '../../types';
|
||||
import { DashboardSaveModal } from './overlays/save_modal';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
import { showCloneModal } from './overlays/show_clone_modal';
|
||||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { DashboardContainerInput } from '../../../../common';
|
||||
import { SaveDashboardReturn } from '../../../services/dashboard_content_management/types';
|
||||
import { extractTitleAndCount } from './lib/extract_title_and_count';
|
||||
|
||||
export function runSaveAs(this: DashboardContainer) {
|
||||
const {
|
||||
|
@ -152,31 +152,41 @@ export async function runClone(this: DashboardContainer) {
|
|||
|
||||
const { explicitInput: currentState } = this.getState();
|
||||
|
||||
return new Promise<SaveDashboardReturn | undefined>((resolve) => {
|
||||
const onClone = async (
|
||||
newTitle: string,
|
||||
isTitleDuplicateConfirmed: boolean,
|
||||
onTitleDuplicate: () => void
|
||||
) => {
|
||||
if (
|
||||
return new Promise<SaveDashboardReturn | undefined>(async (resolve, reject) => {
|
||||
try {
|
||||
const [baseTitle, baseCount] = extractTitleAndCount(currentState.title);
|
||||
let copyCount = baseCount;
|
||||
let newTitle = `${baseTitle} (${copyCount})`;
|
||||
while (
|
||||
!(await checkForDuplicateDashboardTitle({
|
||||
title: newTitle,
|
||||
onTitleDuplicate,
|
||||
lastSavedTitle: currentState.title,
|
||||
copyOnSave: true,
|
||||
isTitleDuplicateConfirmed,
|
||||
isTitleDuplicateConfirmed: false,
|
||||
}))
|
||||
) {
|
||||
// do not clone if title is duplicate and is unconfirmed
|
||||
return {};
|
||||
copyCount++;
|
||||
newTitle = `${baseTitle} (${copyCount})`;
|
||||
}
|
||||
const saveResult = await saveDashboardState({
|
||||
saveOptions: { saveAsCopy: true },
|
||||
currentState: { ...currentState, title: newTitle },
|
||||
saveOptions: {
|
||||
saveAsCopy: true,
|
||||
},
|
||||
currentState: {
|
||||
...currentState,
|
||||
title: newTitle,
|
||||
},
|
||||
});
|
||||
resolve(saveResult);
|
||||
return saveResult.id ? { id: saveResult.id } : { error: saveResult.error };
|
||||
};
|
||||
showCloneModal({ onClone, title: currentState.title, onClose: () => resolve(undefined) });
|
||||
return saveResult.id
|
||||
? {
|
||||
id: saveResult.id,
|
||||
}
|
||||
: {
|
||||
error: saveResult.error,
|
||||
};
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export interface DashboardDuplicateTitleCheckProps {
|
|||
title: string;
|
||||
copyOnSave: boolean;
|
||||
lastSavedTitle: string;
|
||||
onTitleDuplicate: () => void;
|
||||
onTitleDuplicate?: () => void;
|
||||
isTitleDuplicateConfirmed: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
describe('Dashboard', () => {
|
||||
const dashboardName = 'Dashboard Listing A11y';
|
||||
const clonedDashboardName = 'Dashboard Listing A11y Copy';
|
||||
const clonedDashboardName = 'Dashboard Listing A11y (1)';
|
||||
|
||||
it('dashboard', async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
|
@ -132,11 +132,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await a11y.testAppSnapshot();
|
||||
});
|
||||
|
||||
it('Confirm clone with *copy* appended', async () => {
|
||||
await PageObjects.dashboard.confirmClone();
|
||||
await a11y.testAppSnapshot();
|
||||
});
|
||||
|
||||
it('Dashboard listing table', async () => {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await a11y.testAppSnapshot();
|
||||
|
|
|
@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
describe('dashboard clone', function describeIndexTests() {
|
||||
const dashboardName = 'Dashboard Clone Test';
|
||||
const clonedDashboardName = dashboardName + ' Copy';
|
||||
const clonedDashboardName = dashboardName + ' (1)';
|
||||
|
||||
before(async function () {
|
||||
return PageObjects.dashboard.initTests();
|
||||
|
@ -31,7 +31,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.dashboard.saveDashboard(dashboardName);
|
||||
|
||||
await PageObjects.dashboard.clickClone();
|
||||
await PageObjects.dashboard.confirmClone();
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await listingTable.searchAndExpectItemsCount('dashboard', clonedDashboardName, 1);
|
||||
});
|
||||
|
@ -43,38 +42,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(panelTitles).to.eql(PageObjects.dashboard.getTestVisualizationNames());
|
||||
});
|
||||
});
|
||||
|
||||
it('clone appends Copy to the dashboard title name', async () => {
|
||||
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
await PageObjects.dashboard.clickClone();
|
||||
|
||||
const title = await PageObjects.dashboard.getCloneTitle();
|
||||
expect(title).to.be(clonedDashboardName);
|
||||
});
|
||||
|
||||
it('and warns on duplicate name', async function () {
|
||||
await PageObjects.dashboard.confirmClone();
|
||||
await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true });
|
||||
});
|
||||
|
||||
it("and doesn't save", async () => {
|
||||
await PageObjects.dashboard.cancelClone();
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
|
||||
await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 1);
|
||||
});
|
||||
|
||||
it('Clones on confirm duplicate title warning', async function () {
|
||||
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
await PageObjects.dashboard.clickClone();
|
||||
|
||||
await PageObjects.dashboard.confirmClone();
|
||||
await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true });
|
||||
await PageObjects.dashboard.confirmClone();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
|
||||
await listingTable.searchAndExpectItemsCount('dashboard', dashboardName + ' Copy', 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -212,24 +212,6 @@ export class DashboardPageObject extends FtrService {
|
|||
await this.testSubjects.click('dashboardClone');
|
||||
}
|
||||
|
||||
public async getCloneTitle() {
|
||||
return await this.testSubjects.getAttribute('clonedDashboardTitle', 'value');
|
||||
}
|
||||
|
||||
public async confirmClone() {
|
||||
this.log.debug('Confirming clone');
|
||||
await this.testSubjects.click('cloneConfirmButton');
|
||||
}
|
||||
|
||||
public async cancelClone() {
|
||||
this.log.debug('Canceling clone');
|
||||
await this.testSubjects.click('cloneCancelButton');
|
||||
}
|
||||
|
||||
public async setClonedDashboardTitle(title: string) {
|
||||
await this.testSubjects.setValue('clonedDashboardTitle', title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the duplicate title warning is either displayed or not displayed.
|
||||
* @param { displayed: boolean }
|
||||
|
|
|
@ -1114,9 +1114,6 @@
|
|||
"dashboard.panelStorageError.setError": "Une erreur s'est produite lors de la définition des modifications non enregistrées : {message}",
|
||||
"dashboard.share.defaultDashboardTitle": "Tableau de bord [{date}]",
|
||||
"dashboard.strings.dashboardEditTitle": "Modification de {title}",
|
||||
"dashboard.topNav.cloneModal.dashboardExistsDescription": "Cliquez sur {confirmClone} pour cloner le tableau de bord avec le titre dupliqué.",
|
||||
"dashboard.topNav.cloneModal.dashboardExistsTitle": "Il existe déjà un tableau de bord avec le titre {newDashboardName}.",
|
||||
"dashboard.topNav.showCloneModal.dashboardCopyTitle": "Copie de {title}",
|
||||
"dashboard.actions.DownloadCreateDrilldownAction.displayName": "Télécharger au format CSV",
|
||||
"dashboard.actions.downloadOptionsUnsavedFilename": "sans titre",
|
||||
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "Minimiser",
|
||||
|
@ -1128,7 +1125,6 @@
|
|||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "Modifications non enregistrées",
|
||||
"dashboard.badge.readOnly.text": "Lecture seule",
|
||||
"dashboard.badge.readOnly.tooltip": "Impossible d'enregistrer les tableaux de bord",
|
||||
"dashboard.cloneModal.cloneDashboardTitleAriaLabel": "Titre du tableau de bord cloné",
|
||||
"dashboard.createConfirmModal.cancelButtonLabel": "Annuler",
|
||||
"dashboard.createConfirmModal.confirmButtonLabel": "Redémarrer",
|
||||
"dashboard.createConfirmModal.continueButtonLabel": "Poursuivre les modifications",
|
||||
|
@ -1222,11 +1218,6 @@
|
|||
"dashboard.solutionToolbar.addPanelButtonLabel": "Créer une visualisation",
|
||||
"dashboard.solutionToolbar.editorMenuButtonLabel": "Sélectionner un type",
|
||||
"dashboard.solutionToolbar.quickCreateButtonGroupLegend": "Raccourcis vers les types de visualisation populaires",
|
||||
"dashboard.topNav.cloneModal.cancelButtonLabel": "Annuler",
|
||||
"dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "Cloner le tableau de bord",
|
||||
"dashboard.topNav.cloneModal.confirmButtonLabel": "Confirmer le clonage",
|
||||
"dashboard.topNav.cloneModal.confirmCloneDescription": "Confirmer le clonage",
|
||||
"dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "Veuillez saisir un autre nom pour votre tableau de bord.",
|
||||
"dashboard.topNav.labsButtonAriaLabel": "ateliers",
|
||||
"dashboard.topNav.labsConfigDescription": "Ateliers",
|
||||
"dashboard.topNav.saveModal.objectType": "tableau de bord",
|
||||
|
|
|
@ -1114,9 +1114,6 @@
|
|||
"dashboard.panelStorageError.setError": "保存されていない変更の設定中にエラーが発生しました:{message}",
|
||||
"dashboard.share.defaultDashboardTitle": "ダッシュボード[{date}]",
|
||||
"dashboard.strings.dashboardEditTitle": "{title}の編集中",
|
||||
"dashboard.topNav.cloneModal.dashboardExistsDescription": "{confirmClone}をクリックして重複タイトルでダッシュボードのクローンを作成します。",
|
||||
"dashboard.topNav.cloneModal.dashboardExistsTitle": "「{newDashboardName}」というタイトルのダッシュボードがすでに存在します。",
|
||||
"dashboard.topNav.showCloneModal.dashboardCopyTitle": "{title}コピー",
|
||||
"dashboard.actions.DownloadCreateDrilldownAction.displayName": "CSV をダウンロード",
|
||||
"dashboard.actions.downloadOptionsUnsavedFilename": "無題",
|
||||
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化",
|
||||
|
@ -1128,7 +1125,6 @@
|
|||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "保存されていない変更",
|
||||
"dashboard.badge.readOnly.text": "読み取り専用",
|
||||
"dashboard.badge.readOnly.tooltip": "ダッシュボードを保存できません",
|
||||
"dashboard.cloneModal.cloneDashboardTitleAriaLabel": "クローンダッシュボードタイトル",
|
||||
"dashboard.createConfirmModal.cancelButtonLabel": "キャンセル",
|
||||
"dashboard.createConfirmModal.confirmButtonLabel": "やり直す",
|
||||
"dashboard.createConfirmModal.continueButtonLabel": "編集を続行",
|
||||
|
@ -1222,11 +1218,6 @@
|
|||
"dashboard.solutionToolbar.addPanelButtonLabel": "ビジュアライゼーションを作成",
|
||||
"dashboard.solutionToolbar.editorMenuButtonLabel": "タイプを選択してください",
|
||||
"dashboard.solutionToolbar.quickCreateButtonGroupLegend": "よく使用されるビジュアライゼーションタイプへのショートカット",
|
||||
"dashboard.topNav.cloneModal.cancelButtonLabel": "キャンセル",
|
||||
"dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "ダッシュボードのクローンを作成",
|
||||
"dashboard.topNav.cloneModal.confirmButtonLabel": "クローンの確認",
|
||||
"dashboard.topNav.cloneModal.confirmCloneDescription": "クローンの確認",
|
||||
"dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "ダッシュボードの新しい名前を入力してください。",
|
||||
"dashboard.topNav.labsButtonAriaLabel": "ラボ",
|
||||
"dashboard.topNav.labsConfigDescription": "ラボ",
|
||||
"dashboard.topNav.saveModal.objectType": "ダッシュボード",
|
||||
|
|
|
@ -1114,9 +1114,6 @@
|
|||
"dashboard.panelStorageError.setError": "设置未保存更改时遇到错误:{message}",
|
||||
"dashboard.share.defaultDashboardTitle": "仪表板 [{date}]",
|
||||
"dashboard.strings.dashboardEditTitle": "正在编辑 {title}",
|
||||
"dashboard.topNav.cloneModal.dashboardExistsDescription": "单击“{confirmClone}”以克隆具有重复标题的仪表板。",
|
||||
"dashboard.topNav.cloneModal.dashboardExistsTitle": "具有标题 {newDashboardName} 的仪表板已存在。",
|
||||
"dashboard.topNav.showCloneModal.dashboardCopyTitle": "{title} 副本",
|
||||
"dashboard.actions.DownloadCreateDrilldownAction.displayName": "下载为 CSV",
|
||||
"dashboard.actions.downloadOptionsUnsavedFilename": "未命名",
|
||||
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化",
|
||||
|
@ -1128,7 +1125,6 @@
|
|||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "未保存的更改",
|
||||
"dashboard.badge.readOnly.text": "只读",
|
||||
"dashboard.badge.readOnly.tooltip": "无法保存仪表板",
|
||||
"dashboard.cloneModal.cloneDashboardTitleAriaLabel": "克隆仪表板标题",
|
||||
"dashboard.createConfirmModal.cancelButtonLabel": "取消",
|
||||
"dashboard.createConfirmModal.confirmButtonLabel": "重头开始",
|
||||
"dashboard.createConfirmModal.continueButtonLabel": "继续编辑",
|
||||
|
@ -1222,11 +1218,6 @@
|
|||
"dashboard.solutionToolbar.addPanelButtonLabel": "创建可视化",
|
||||
"dashboard.solutionToolbar.editorMenuButtonLabel": "选择类型",
|
||||
"dashboard.solutionToolbar.quickCreateButtonGroupLegend": "常用可视化类型的快捷键",
|
||||
"dashboard.topNav.cloneModal.cancelButtonLabel": "取消",
|
||||
"dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "克隆仪表板",
|
||||
"dashboard.topNav.cloneModal.confirmButtonLabel": "确认克隆",
|
||||
"dashboard.topNav.cloneModal.confirmCloneDescription": "确认克隆",
|
||||
"dashboard.topNav.cloneModal.enterNewNameForDashboardDescription": "请为您的仪表板输入新的名称。",
|
||||
"dashboard.topNav.labsButtonAriaLabel": "实验",
|
||||
"dashboard.topNav.labsConfigDescription": "实验",
|
||||
"dashboard.topNav.saveModal.objectType": "仪表板",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue