[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:
Catherine Liu 2023-06-30 20:45:39 -07:00 committed by GitHub
parent a25506c842
commit 938716e58a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 100 additions and 507 deletions

View file

@ -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,
@ -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

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { 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]);
});
});

View file

@ -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];
};

View file

@ -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>
`;

View file

@ -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');
});

View file

@ -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>
);
}
}

View file

@ -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);
}

View file

@ -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);
}
});
}

View file

@ -14,7 +14,7 @@ export interface DashboardDuplicateTitleCheckProps {
title: string;
copyOnSave: boolean;
lastSavedTitle: string;
onTitleDuplicate: () => void;
onTitleDuplicate?: () => void;
isTitleDuplicateConfirmed: boolean;
}

View file

@ -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();

View file

@ -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);
});
});
}

View file

@ -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 }

View file

@ -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",
@ -1221,11 +1217,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",
@ -39433,4 +39424,4 @@
"xpack.painlessLab.title": "Painless Lab",
"xpack.painlessLab.walkthroughButtonLabel": "Présentation"
}
}
}

View file

@ -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": "編集を続行",
@ -1221,11 +1217,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": "ダッシュボード",
@ -39407,4 +39398,4 @@
"xpack.painlessLab.title": "Painless Lab",
"xpack.painlessLab.walkthroughButtonLabel": "実地検証"
}
}
}

View file

@ -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": "继续编辑",
@ -1221,11 +1217,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": "仪表板",
@ -39401,4 +39392,4 @@
"xpack.painlessLab.title": "Painless 实验室",
"xpack.painlessLab.walkthroughButtonLabel": "指导"
}
}
}