mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[Dashboard] Redesign clone experience (#159752)
## Summary Closes #154500. Closes https://github.com/elastic/kibana/issues/114206. This updates the UX around cloning dashboards. I removed the clone confirm model, so cloning a dashboard is just a one click action now. This aligns with how other apps like ML handle cloning. I've also made the dashboard title breadcrumb a button in edit mode that opens the dashboard settings flyout for better discoverability.4f5ea117
-a5e4-4ec5-9113-8b09fd8c84a1 I also changed the pattern for cloned dashboard title from `Dashboard Title Copy` to `Dashboard Title (#)`. <img width="1226" alt="Screenshot 2023-06-30 at 1 03 35 PM" src="b50ba5c6
-dc95-4aab-a320-b1a78b74c1b6">
This commit is contained in:
parent
a25506c842
commit
938716e58a
15 changed files with 100 additions and 507 deletions
|
@ -17,7 +17,7 @@ import {
|
||||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||||
import type { DataView } from '@kbn/data-views-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 {
|
import {
|
||||||
getDashboardTitle,
|
getDashboardTitle,
|
||||||
leaveConfirmStrings,
|
leaveConfirmStrings,
|
||||||
|
@ -145,10 +145,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
|
* 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 { DashboardSaveOptions, DashboardStateFromSaveModal } from '../../types';
|
||||||
import { DashboardSaveModal } from './overlays/save_modal';
|
import { DashboardSaveModal } from './overlays/save_modal';
|
||||||
import { DashboardContainer } from '../dashboard_container';
|
import { DashboardContainer } from '../dashboard_container';
|
||||||
import { showCloneModal } from './overlays/show_clone_modal';
|
|
||||||
import { pluginServices } from '../../../services/plugin_services';
|
import { pluginServices } from '../../../services/plugin_services';
|
||||||
import { DashboardContainerInput } from '../../../../common';
|
import { DashboardContainerInput } from '../../../../common';
|
||||||
import { SaveDashboardReturn } from '../../../services/dashboard_content_management/types';
|
import { SaveDashboardReturn } from '../../../services/dashboard_content_management/types';
|
||||||
|
import { extractTitleAndCount } from './lib/extract_title_and_count';
|
||||||
|
|
||||||
export function runSaveAs(this: DashboardContainer) {
|
export function runSaveAs(this: DashboardContainer) {
|
||||||
const {
|
const {
|
||||||
|
@ -152,31 +152,41 @@ export async function runClone(this: DashboardContainer) {
|
||||||
|
|
||||||
const { explicitInput: currentState } = this.getState();
|
const { explicitInput: currentState } = this.getState();
|
||||||
|
|
||||||
return new Promise<SaveDashboardReturn | undefined>((resolve) => {
|
return new Promise<SaveDashboardReturn | undefined>(async (resolve, reject) => {
|
||||||
const onClone = async (
|
try {
|
||||||
newTitle: string,
|
const [baseTitle, baseCount] = extractTitleAndCount(currentState.title);
|
||||||
isTitleDuplicateConfirmed: boolean,
|
let copyCount = baseCount;
|
||||||
onTitleDuplicate: () => void
|
let newTitle = `${baseTitle} (${copyCount})`;
|
||||||
) => {
|
while (
|
||||||
if (
|
|
||||||
!(await checkForDuplicateDashboardTitle({
|
!(await checkForDuplicateDashboardTitle({
|
||||||
title: newTitle,
|
title: newTitle,
|
||||||
onTitleDuplicate,
|
|
||||||
lastSavedTitle: currentState.title,
|
lastSavedTitle: currentState.title,
|
||||||
copyOnSave: true,
|
copyOnSave: true,
|
||||||
isTitleDuplicateConfirmed,
|
isTitleDuplicateConfirmed: false,
|
||||||
}))
|
}))
|
||||||
) {
|
) {
|
||||||
// do not clone if title is duplicate and is unconfirmed
|
copyCount++;
|
||||||
return {};
|
newTitle = `${baseTitle} (${copyCount})`;
|
||||||
}
|
}
|
||||||
const saveResult = await saveDashboardState({
|
const saveResult = await saveDashboardState({
|
||||||
saveOptions: { saveAsCopy: true },
|
saveOptions: {
|
||||||
currentState: { ...currentState, title: newTitle },
|
saveAsCopy: true,
|
||||||
|
},
|
||||||
|
currentState: {
|
||||||
|
...currentState,
|
||||||
|
title: newTitle,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
resolve(saveResult);
|
resolve(saveResult);
|
||||||
return saveResult.id ? { id: saveResult.id } : { error: saveResult.error };
|
return saveResult.id
|
||||||
|
? {
|
||||||
|
id: saveResult.id,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
error: saveResult.error,
|
||||||
};
|
};
|
||||||
showCloneModal({ onClone, title: currentState.title, onClose: () => resolve(undefined) });
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export interface DashboardDuplicateTitleCheckProps {
|
||||||
title: string;
|
title: string;
|
||||||
copyOnSave: boolean;
|
copyOnSave: boolean;
|
||||||
lastSavedTitle: string;
|
lastSavedTitle: string;
|
||||||
onTitleDuplicate: () => void;
|
onTitleDuplicate?: () => void;
|
||||||
isTitleDuplicateConfirmed: boolean;
|
isTitleDuplicateConfirmed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
|
|
||||||
describe('Dashboard', () => {
|
describe('Dashboard', () => {
|
||||||
const dashboardName = 'Dashboard Listing A11y';
|
const dashboardName = 'Dashboard Listing A11y';
|
||||||
const clonedDashboardName = 'Dashboard Listing A11y Copy';
|
const clonedDashboardName = 'Dashboard Listing A11y (1)';
|
||||||
|
|
||||||
it('dashboard', async () => {
|
it('dashboard', async () => {
|
||||||
await PageObjects.common.navigateToApp('dashboard');
|
await PageObjects.common.navigateToApp('dashboard');
|
||||||
|
@ -132,11 +132,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
await a11y.testAppSnapshot();
|
await a11y.testAppSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Confirm clone with *copy* appended', async () => {
|
|
||||||
await PageObjects.dashboard.confirmClone();
|
|
||||||
await a11y.testAppSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Dashboard listing table', async () => {
|
it('Dashboard listing table', async () => {
|
||||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||||
await a11y.testAppSnapshot();
|
await a11y.testAppSnapshot();
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
|
|
||||||
describe('dashboard clone', function describeIndexTests() {
|
describe('dashboard clone', function describeIndexTests() {
|
||||||
const dashboardName = 'Dashboard Clone Test';
|
const dashboardName = 'Dashboard Clone Test';
|
||||||
const clonedDashboardName = dashboardName + ' Copy';
|
const clonedDashboardName = dashboardName + ' (1)';
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
return PageObjects.dashboard.initTests();
|
return PageObjects.dashboard.initTests();
|
||||||
|
@ -31,7 +31,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
await PageObjects.dashboard.saveDashboard(dashboardName);
|
await PageObjects.dashboard.saveDashboard(dashboardName);
|
||||||
|
|
||||||
await PageObjects.dashboard.clickClone();
|
await PageObjects.dashboard.clickClone();
|
||||||
await PageObjects.dashboard.confirmClone();
|
|
||||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||||
await listingTable.searchAndExpectItemsCount('dashboard', clonedDashboardName, 1);
|
await listingTable.searchAndExpectItemsCount('dashboard', clonedDashboardName, 1);
|
||||||
});
|
});
|
||||||
|
@ -43,38 +42,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||||
expect(panelTitles).to.eql(PageObjects.dashboard.getTestVisualizationNames());
|
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');
|
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.
|
* Asserts that the duplicate title warning is either displayed or not displayed.
|
||||||
* @param { displayed: boolean }
|
* @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.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.share.defaultDashboardTitle": "Tableau de bord [{date}]",
|
||||||
"dashboard.strings.dashboardEditTitle": "Modification de {title}",
|
"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.DownloadCreateDrilldownAction.displayName": "Télécharger au format CSV",
|
||||||
"dashboard.actions.downloadOptionsUnsavedFilename": "sans titre",
|
"dashboard.actions.downloadOptionsUnsavedFilename": "sans titre",
|
||||||
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "Minimiser",
|
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "Minimiser",
|
||||||
|
@ -1128,7 +1125,6 @@
|
||||||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "Modifications non enregistrées",
|
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "Modifications non enregistrées",
|
||||||
"dashboard.badge.readOnly.text": "Lecture seule",
|
"dashboard.badge.readOnly.text": "Lecture seule",
|
||||||
"dashboard.badge.readOnly.tooltip": "Impossible d'enregistrer les tableaux de bord",
|
"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.cancelButtonLabel": "Annuler",
|
||||||
"dashboard.createConfirmModal.confirmButtonLabel": "Redémarrer",
|
"dashboard.createConfirmModal.confirmButtonLabel": "Redémarrer",
|
||||||
"dashboard.createConfirmModal.continueButtonLabel": "Poursuivre les modifications",
|
"dashboard.createConfirmModal.continueButtonLabel": "Poursuivre les modifications",
|
||||||
|
@ -1221,11 +1217,6 @@
|
||||||
"dashboard.solutionToolbar.addPanelButtonLabel": "Créer une visualisation",
|
"dashboard.solutionToolbar.addPanelButtonLabel": "Créer une visualisation",
|
||||||
"dashboard.solutionToolbar.editorMenuButtonLabel": "Sélectionner un type",
|
"dashboard.solutionToolbar.editorMenuButtonLabel": "Sélectionner un type",
|
||||||
"dashboard.solutionToolbar.quickCreateButtonGroupLegend": "Raccourcis vers les types de visualisation populaires",
|
"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.labsButtonAriaLabel": "ateliers",
|
||||||
"dashboard.topNav.labsConfigDescription": "Ateliers",
|
"dashboard.topNav.labsConfigDescription": "Ateliers",
|
||||||
"dashboard.topNav.saveModal.objectType": "tableau de bord",
|
"dashboard.topNav.saveModal.objectType": "tableau de bord",
|
||||||
|
|
|
@ -1114,9 +1114,6 @@
|
||||||
"dashboard.panelStorageError.setError": "保存されていない変更の設定中にエラーが発生しました:{message}",
|
"dashboard.panelStorageError.setError": "保存されていない変更の設定中にエラーが発生しました:{message}",
|
||||||
"dashboard.share.defaultDashboardTitle": "ダッシュボード[{date}]",
|
"dashboard.share.defaultDashboardTitle": "ダッシュボード[{date}]",
|
||||||
"dashboard.strings.dashboardEditTitle": "{title}の編集中",
|
"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.DownloadCreateDrilldownAction.displayName": "CSV をダウンロード",
|
||||||
"dashboard.actions.downloadOptionsUnsavedFilename": "無題",
|
"dashboard.actions.downloadOptionsUnsavedFilename": "無題",
|
||||||
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化",
|
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化",
|
||||||
|
@ -1128,7 +1125,6 @@
|
||||||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "保存されていない変更",
|
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "保存されていない変更",
|
||||||
"dashboard.badge.readOnly.text": "読み取り専用",
|
"dashboard.badge.readOnly.text": "読み取り専用",
|
||||||
"dashboard.badge.readOnly.tooltip": "ダッシュボードを保存できません",
|
"dashboard.badge.readOnly.tooltip": "ダッシュボードを保存できません",
|
||||||
"dashboard.cloneModal.cloneDashboardTitleAriaLabel": "クローンダッシュボードタイトル",
|
|
||||||
"dashboard.createConfirmModal.cancelButtonLabel": "キャンセル",
|
"dashboard.createConfirmModal.cancelButtonLabel": "キャンセル",
|
||||||
"dashboard.createConfirmModal.confirmButtonLabel": "やり直す",
|
"dashboard.createConfirmModal.confirmButtonLabel": "やり直す",
|
||||||
"dashboard.createConfirmModal.continueButtonLabel": "編集を続行",
|
"dashboard.createConfirmModal.continueButtonLabel": "編集を続行",
|
||||||
|
@ -1221,11 +1217,6 @@
|
||||||
"dashboard.solutionToolbar.addPanelButtonLabel": "ビジュアライゼーションを作成",
|
"dashboard.solutionToolbar.addPanelButtonLabel": "ビジュアライゼーションを作成",
|
||||||
"dashboard.solutionToolbar.editorMenuButtonLabel": "タイプを選択してください",
|
"dashboard.solutionToolbar.editorMenuButtonLabel": "タイプを選択してください",
|
||||||
"dashboard.solutionToolbar.quickCreateButtonGroupLegend": "よく使用されるビジュアライゼーションタイプへのショートカット",
|
"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.labsButtonAriaLabel": "ラボ",
|
||||||
"dashboard.topNav.labsConfigDescription": "ラボ",
|
"dashboard.topNav.labsConfigDescription": "ラボ",
|
||||||
"dashboard.topNav.saveModal.objectType": "ダッシュボード",
|
"dashboard.topNav.saveModal.objectType": "ダッシュボード",
|
||||||
|
|
|
@ -1114,9 +1114,6 @@
|
||||||
"dashboard.panelStorageError.setError": "设置未保存更改时遇到错误:{message}",
|
"dashboard.panelStorageError.setError": "设置未保存更改时遇到错误:{message}",
|
||||||
"dashboard.share.defaultDashboardTitle": "仪表板 [{date}]",
|
"dashboard.share.defaultDashboardTitle": "仪表板 [{date}]",
|
||||||
"dashboard.strings.dashboardEditTitle": "正在编辑 {title}",
|
"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.DownloadCreateDrilldownAction.displayName": "下载为 CSV",
|
||||||
"dashboard.actions.downloadOptionsUnsavedFilename": "未命名",
|
"dashboard.actions.downloadOptionsUnsavedFilename": "未命名",
|
||||||
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化",
|
"dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化",
|
||||||
|
@ -1128,7 +1125,6 @@
|
||||||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "未保存的更改",
|
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "未保存的更改",
|
||||||
"dashboard.badge.readOnly.text": "只读",
|
"dashboard.badge.readOnly.text": "只读",
|
||||||
"dashboard.badge.readOnly.tooltip": "无法保存仪表板",
|
"dashboard.badge.readOnly.tooltip": "无法保存仪表板",
|
||||||
"dashboard.cloneModal.cloneDashboardTitleAriaLabel": "克隆仪表板标题",
|
|
||||||
"dashboard.createConfirmModal.cancelButtonLabel": "取消",
|
"dashboard.createConfirmModal.cancelButtonLabel": "取消",
|
||||||
"dashboard.createConfirmModal.confirmButtonLabel": "重头开始",
|
"dashboard.createConfirmModal.confirmButtonLabel": "重头开始",
|
||||||
"dashboard.createConfirmModal.continueButtonLabel": "继续编辑",
|
"dashboard.createConfirmModal.continueButtonLabel": "继续编辑",
|
||||||
|
@ -1221,11 +1217,6 @@
|
||||||
"dashboard.solutionToolbar.addPanelButtonLabel": "创建可视化",
|
"dashboard.solutionToolbar.addPanelButtonLabel": "创建可视化",
|
||||||
"dashboard.solutionToolbar.editorMenuButtonLabel": "选择类型",
|
"dashboard.solutionToolbar.editorMenuButtonLabel": "选择类型",
|
||||||
"dashboard.solutionToolbar.quickCreateButtonGroupLegend": "常用可视化类型的快捷键",
|
"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.labsButtonAriaLabel": "实验",
|
||||||
"dashboard.topNav.labsConfigDescription": "实验",
|
"dashboard.topNav.labsConfigDescription": "实验",
|
||||||
"dashboard.topNav.saveModal.objectType": "仪表板",
|
"dashboard.topNav.saveModal.objectType": "仪表板",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue