mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Discover] Create data view from sidebar (#123391)
* [Discover] Create data view from sidebar * Fix failing unit test * Fix invalid import * Addressing PR comments * Add horizontal separator * Design tweaks * Update unit test * Remove double declaration Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ryan Keairns <contactryank@gmail.com>
This commit is contained in:
parent
6a707516f6
commit
9b20c4f035
15 changed files with 194 additions and 13 deletions
|
@ -13,7 +13,8 @@
|
|||
"navigation",
|
||||
"uiActions",
|
||||
"savedObjects",
|
||||
"dataViewFieldEditor"
|
||||
"dataViewFieldEditor",
|
||||
"dataViewEditor"
|
||||
],
|
||||
"optionalPlugins": ["home", "share", "usageCollection", "spaces"],
|
||||
"requiredBundles": ["kibanaUtils", "home", "kibanaReact", "dataViews"],
|
||||
|
|
|
@ -48,7 +48,7 @@ import {
|
|||
import { FieldStatisticsTable } from '../field_stats_table';
|
||||
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
|
||||
import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants';
|
||||
import { DataViewType } from '../../../../../../data_views/common';
|
||||
import { DataViewType, DataView } from '../../../../../../data_views/common';
|
||||
|
||||
/**
|
||||
* Local storage key for sidebar persistence state
|
||||
|
@ -204,6 +204,14 @@ export function DiscoverLayout({
|
|||
}, [isSidebarClosed, storage]);
|
||||
|
||||
const contentCentered = resultState === 'uninitialized' || resultState === 'none';
|
||||
const onDataViewCreated = useCallback(
|
||||
(dataView: DataView) => {
|
||||
if (dataView.id) {
|
||||
onChangeIndexPattern(dataView.id);
|
||||
}
|
||||
},
|
||||
[onChangeIndexPattern]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPage className="dscPage" data-fetch-counter={fetchCounter.current}>
|
||||
|
@ -245,6 +253,7 @@ export function DiscoverLayout({
|
|||
useNewFieldsApi={useNewFieldsApi}
|
||||
onEditRuntimeField={onEditRuntimeField}
|
||||
viewMode={viewMode}
|
||||
onDataViewCreated={onDataViewCreated}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
|
|
|
@ -133,6 +133,7 @@ exports[`Discover DataView Management renders correctly 1`] = `
|
|||
}
|
||||
>
|
||||
<DiscoverIndexPatternManagement
|
||||
createNewDataView={[MockFunction]}
|
||||
editField={[MockFunction]}
|
||||
selectedIndexPattern={
|
||||
DataView {
|
||||
|
@ -685,7 +686,7 @@ exports[`Discover DataView Management renders correctly 1`] = `
|
|||
hasArrow={true}
|
||||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="s"
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<div
|
||||
className="euiPopover euiPopover--anchorDownCenter"
|
||||
|
|
|
@ -54,6 +54,7 @@ describe('Discover DataView Management', () => {
|
|||
const indexPattern = stubLogstashIndexPattern;
|
||||
|
||||
const editField = jest.fn();
|
||||
const createNewDataView = jest.fn();
|
||||
|
||||
const mountComponent = () => {
|
||||
return mountWithIntl(
|
||||
|
@ -62,6 +63,7 @@ describe('Discover DataView Management', () => {
|
|||
editField={editField}
|
||||
selectedIndexPattern={indexPattern}
|
||||
useNewFieldsApi={true}
|
||||
createNewDataView={createNewDataView}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
@ -81,7 +83,7 @@ describe('Discover DataView Management', () => {
|
|||
button.simulate('click');
|
||||
|
||||
expect(component.find(EuiContextMenuPanel).length).toBe(1);
|
||||
expect(component.find(EuiContextMenuItem).length).toBe(2);
|
||||
expect(component.find(EuiContextMenuItem).length).toBe(3);
|
||||
});
|
||||
|
||||
test('click on an add button executes editField callback', () => {
|
||||
|
@ -103,4 +105,14 @@ describe('Discover DataView Management', () => {
|
|||
manageButton.simulate('click');
|
||||
expect(mockServices.core.application.navigateToApp).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('click on add dataView button executes createNewDataView callback', () => {
|
||||
const component = mountComponent();
|
||||
const button = findTestSubject(component, 'discoverIndexPatternActions');
|
||||
button.simulate('click');
|
||||
|
||||
const manageButton = findTestSubject(component, 'dataview-create-new');
|
||||
manageButton.simulate('click');
|
||||
expect(createNewDataView).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,13 @@
|
|||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiHorizontalRule,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDiscoverServices } from '../../../../utils/use_discover_services';
|
||||
import { DataView } from '../../../../../../data/common';
|
||||
|
@ -26,11 +32,16 @@ export interface DiscoverIndexPatternManagementProps {
|
|||
* @param fieldName
|
||||
*/
|
||||
editField: (fieldName?: string) => void;
|
||||
|
||||
/**
|
||||
* Callback to execute on create new data action
|
||||
*/
|
||||
createNewDataView: () => void;
|
||||
}
|
||||
|
||||
export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManagementProps) {
|
||||
const { dataViewFieldEditor, core } = useDiscoverServices();
|
||||
const { useNewFieldsApi, selectedIndexPattern, editField } = props;
|
||||
const { useNewFieldsApi, selectedIndexPattern, editField, createNewDataView } = props;
|
||||
const dataViewEditPermission = dataViewFieldEditor?.userPermissions.editIndexPattern();
|
||||
const canEditDataViewField = !!dataViewEditPermission && useNewFieldsApi;
|
||||
const [isAddIndexPatternFieldPopoverOpen, setIsAddIndexPatternFieldPopoverOpen] = useState(false);
|
||||
|
@ -45,7 +56,7 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage
|
|||
|
||||
return (
|
||||
<EuiPopover
|
||||
panelPaddingSize="s"
|
||||
panelPaddingSize="none"
|
||||
isOpen={isAddIndexPatternFieldPopoverOpen}
|
||||
closePopover={() => {
|
||||
setIsAddIndexPatternFieldPopoverOpen(false);
|
||||
|
@ -67,7 +78,8 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage
|
|||
}
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
size="s"
|
||||
size="m"
|
||||
title="Data view"
|
||||
items={[
|
||||
<EuiContextMenuItem
|
||||
key="add"
|
||||
|
@ -79,7 +91,7 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage
|
|||
}}
|
||||
>
|
||||
{i18n.translate('discover.fieldChooser.indexPatterns.addFieldButton', {
|
||||
defaultMessage: 'Add field to data view',
|
||||
defaultMessage: 'Add field',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
|
@ -94,7 +106,21 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage
|
|||
}}
|
||||
>
|
||||
{i18n.translate('discover.fieldChooser.indexPatterns.manageFieldButton', {
|
||||
defaultMessage: 'Manage data view fields',
|
||||
defaultMessage: 'Manage settings',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiHorizontalRule style={{ margin: '0px' }} />,
|
||||
<EuiContextMenuItem
|
||||
key="new"
|
||||
icon="plusInCircleFilled"
|
||||
data-test-subj="dataview-create-new"
|
||||
onClick={() => {
|
||||
setIsAddIndexPatternFieldPopoverOpen(false);
|
||||
createNewDataView();
|
||||
}}
|
||||
>
|
||||
{i18n.translate('discover.fieldChooser.dataViews.createNewDataView', {
|
||||
defaultMessage: 'Create new data view',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
|
|
|
@ -63,6 +63,8 @@ function getCompProps(): DiscoverSidebarProps {
|
|||
onEditRuntimeField: jest.fn(),
|
||||
editField: jest.fn(),
|
||||
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
|
||||
createNewDataView: jest.fn(),
|
||||
onDataViewCreated: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,8 @@ export interface DiscoverSidebarProps extends Omit<DiscoverSidebarResponsiveProp
|
|||
|
||||
editField: (fieldName?: string) => void;
|
||||
|
||||
createNewDataView: () => void;
|
||||
|
||||
/**
|
||||
* a statistics of the distribution of fields in the given hits
|
||||
*/
|
||||
|
@ -104,6 +106,7 @@ export function DiscoverSidebarComponent({
|
|||
closeFlyout,
|
||||
editField,
|
||||
viewMode,
|
||||
createNewDataView,
|
||||
}: DiscoverSidebarProps) {
|
||||
const { uiSettings, dataViewFieldEditor } = useDiscoverServices();
|
||||
const [fields, setFields] = useState<DataViewField[] | null>(null);
|
||||
|
@ -299,6 +302,7 @@ export function DiscoverSidebarComponent({
|
|||
selectedIndexPattern={selectedIndexPattern}
|
||||
editField={editField}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
createNewDataView={createNewDataView}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -336,6 +340,7 @@ export function DiscoverSidebarComponent({
|
|||
selectedIndexPattern={selectedIndexPattern}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
editField={editField}
|
||||
createNewDataView={createNewDataView}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -104,6 +104,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps {
|
|||
trackUiMetric: jest.fn(),
|
||||
onEditRuntimeField: jest.fn(),
|
||||
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
|
||||
onDataViewCreated: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -102,6 +102,10 @@ export interface DiscoverSidebarResponsiveProps {
|
|||
* callback to execute on edit runtime field
|
||||
*/
|
||||
onEditRuntimeField: () => void;
|
||||
/**
|
||||
* callback to execute on create dataview
|
||||
*/
|
||||
onDataViewCreated: (dataView: DataView) => void;
|
||||
/**
|
||||
* Discover view mode
|
||||
*/
|
||||
|
@ -115,7 +119,13 @@ export interface DiscoverSidebarResponsiveProps {
|
|||
*/
|
||||
export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) {
|
||||
const services = useDiscoverServices();
|
||||
const { selectedIndexPattern, onEditRuntimeField, useNewFieldsApi, onChangeIndexPattern } = props;
|
||||
const {
|
||||
selectedIndexPattern,
|
||||
onEditRuntimeField,
|
||||
useNewFieldsApi,
|
||||
onChangeIndexPattern,
|
||||
onDataViewCreated,
|
||||
} = props;
|
||||
const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter());
|
||||
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
|
||||
/**
|
||||
|
@ -146,12 +156,16 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
|||
}, [selectedIndexPattern]);
|
||||
|
||||
const closeFieldEditor = useRef<() => void | undefined>();
|
||||
const closeDataViewEditor = useRef<() => void | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const cleanup = () => {
|
||||
if (closeFieldEditor?.current) {
|
||||
closeFieldEditor?.current();
|
||||
}
|
||||
if (closeDataViewEditor?.current) {
|
||||
closeDataViewEditor?.current();
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
// Make sure to close the editor when unmounting
|
||||
|
@ -163,11 +177,15 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
|||
closeFieldEditor.current = ref;
|
||||
}, []);
|
||||
|
||||
const setDataViewEditorRef = useCallback((ref: () => void | undefined) => {
|
||||
closeDataViewEditor.current = ref;
|
||||
}, []);
|
||||
|
||||
const closeFlyout = useCallback(() => {
|
||||
setIsFlyoutVisible(false);
|
||||
}, []);
|
||||
|
||||
const { dataViewFieldEditor } = services;
|
||||
const { dataViewFieldEditor, dataViewEditor } = services;
|
||||
|
||||
const editField = useCallback(
|
||||
(fieldName?: string) => {
|
||||
|
@ -203,6 +221,24 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
|||
]
|
||||
);
|
||||
|
||||
const createNewDataView = useCallback(() => {
|
||||
const indexPatternFieldEditPermission = dataViewEditor.userPermissions.editDataView;
|
||||
if (!indexPatternFieldEditPermission) {
|
||||
return;
|
||||
}
|
||||
const ref = dataViewEditor.openEditor({
|
||||
onSave: async (dataView) => {
|
||||
onDataViewCreated(dataView);
|
||||
},
|
||||
});
|
||||
if (setDataViewEditorRef) {
|
||||
setDataViewEditorRef(ref);
|
||||
}
|
||||
if (closeFlyout) {
|
||||
closeFlyout();
|
||||
}
|
||||
}, [dataViewEditor, setDataViewEditorRef, closeFlyout, onDataViewCreated]);
|
||||
|
||||
if (!selectedIndexPattern) {
|
||||
return null;
|
||||
}
|
||||
|
@ -218,6 +254,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
|||
fieldCounts={fieldCounts.current}
|
||||
setFieldFilter={setFieldFilter}
|
||||
editField={editField}
|
||||
createNewDataView={createNewDataView}
|
||||
/>
|
||||
</EuiHideFor>
|
||||
)}
|
||||
|
@ -244,6 +281,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
|||
selectedIndexPattern={selectedIndexPattern}
|
||||
editField={editField}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
createNewDataView={createNewDataView}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -307,6 +345,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
|
|||
setFieldEditorRef={setFieldEditorRef}
|
||||
closeFlyout={closeFlyout}
|
||||
editField={editField}
|
||||
createNewDataView={createNewDataView}
|
||||
/>
|
||||
</div>
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -40,6 +40,7 @@ import { FieldFormatsStart } from '../../field_formats/public';
|
|||
import { EmbeddableStart } from '../../embeddable/public';
|
||||
|
||||
import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public';
|
||||
import { DataViewEditorStart } from '../../../plugins/data_view_editor/public';
|
||||
|
||||
export interface HistoryLocationState {
|
||||
referrer: string;
|
||||
|
@ -68,6 +69,7 @@ export interface DiscoverServices {
|
|||
uiSettings: IUiSettingsClient;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
dataViewFieldEditor: IndexPatternFieldEditorStart;
|
||||
dataViewEditor: DataViewEditorStart;
|
||||
http: HttpStart;
|
||||
storage: Storage;
|
||||
spaces?: SpacesApi;
|
||||
|
@ -109,5 +111,6 @@ export const buildServices = memoize(function (
|
|||
dataViewFieldEditor: plugins.dataViewFieldEditor,
|
||||
http: core.http,
|
||||
spaces: plugins.spaces,
|
||||
dataViewEditor: plugins.dataViewEditor,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -62,6 +62,7 @@ import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public
|
|||
import { FieldFormatsStart } from '../../field_formats/public';
|
||||
import { injectTruncateStyles } from './utils/truncate_styles';
|
||||
import { DOC_TABLE_LEGACY, TRUNCATE_MAX_HEIGHT } from '../common';
|
||||
import { DataViewEditorStart } from '../../../plugins/data_view_editor/public';
|
||||
import { useDiscoverServices } from './utils/use_discover_services';
|
||||
|
||||
declare module '../../share/public' {
|
||||
|
@ -176,6 +177,7 @@ export interface DiscoverSetupPlugins {
|
|||
* @internal
|
||||
*/
|
||||
export interface DiscoverStartPlugins {
|
||||
dataViewEditor: DataViewEditorStart;
|
||||
uiActions: UiActionsStart;
|
||||
embeddable: EmbeddableStart;
|
||||
navigation: NavigationStart;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
{ "path": "../data_view_field_editor/tsconfig.json"},
|
||||
{ "path": "../field_formats/tsconfig.json" },
|
||||
{ "path": "../data_views/tsconfig.json" },
|
||||
{ "path": "../../../x-pack/plugins/spaces/tsconfig.json" }
|
||||
{ "path": "../../../x-pack/plugins/spaces/tsconfig.json" },
|
||||
{ "path": "../data_view_editor/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
|
65
test/functional/apps/discover/_data_view_editor.ts
Normal file
65
test/functional/apps/discover/_data_view_editor.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from './ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const retry = getService('retry');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const security = getService('security');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
|
||||
const defaultSettings = {
|
||||
defaultIndex: 'logstash-*',
|
||||
};
|
||||
|
||||
const createDataView = async (dataViewName: string) => {
|
||||
await PageObjects.discover.clickIndexPatternActions();
|
||||
await PageObjects.discover.clickCreateNewDataView();
|
||||
await testSubjects.setValue('createIndexPatternNameInput', dataViewName, {
|
||||
clearWithKeyboard: true,
|
||||
typeCharByChar: true,
|
||||
});
|
||||
await testSubjects.click('saveIndexPatternButton');
|
||||
};
|
||||
|
||||
describe('discover integration with data view editor', function describeIndexTests() {
|
||||
before(async function () {
|
||||
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
|
||||
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
|
||||
await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] });
|
||||
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
|
||||
await kibanaServer.uiSettings.replace(defaultSettings);
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await security.testUser.restoreDefaults();
|
||||
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
|
||||
await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] });
|
||||
});
|
||||
|
||||
it('allows creating a new data view', async function () {
|
||||
const dataViewToCreate = 'logstash';
|
||||
await createDataView(dataViewToCreate);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await retry.waitForWithTimeout(
|
||||
'data view selector to include a newly created dataview',
|
||||
5000,
|
||||
async () => {
|
||||
const dataViewTitle = await PageObjects.discover.getCurrentlySelectedDataView();
|
||||
// data view editor will add wildcard symbol by default
|
||||
// so we need to include it in our original title when comparing
|
||||
return dataViewTitle === `${dataViewToCreate}*`;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -54,6 +54,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./_search_on_page_load'));
|
||||
loadTestFile(require.resolve('./_chart_hidden'));
|
||||
loadTestFile(require.resolve('./_context_encoded_url_param'));
|
||||
loadTestFile(require.resolve('./_data_view_editor'));
|
||||
loadTestFile(require.resolve('./_empty_state'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -363,6 +363,13 @@ export class DiscoverPageObject extends FtrService {
|
|||
});
|
||||
}
|
||||
|
||||
public async clickCreateNewDataView() {
|
||||
await this.retry.try(async () => {
|
||||
await this.testSubjects.click('dataview-create-new');
|
||||
await this.find.byClassName('indexPatternEditor__form');
|
||||
});
|
||||
}
|
||||
|
||||
public async hasNoResults() {
|
||||
return await this.testSubjects.exists('discoverNoResults');
|
||||
}
|
||||
|
@ -598,4 +605,10 @@ export class DiscoverPageObject extends FtrService {
|
|||
await this.testSubjects.existOrFail('dscFieldStatsEmbeddedContent');
|
||||
});
|
||||
}
|
||||
|
||||
public async getCurrentlySelectedDataView() {
|
||||
await this.testSubjects.existOrFail('discover-sidebar');
|
||||
const button = await this.testSubjects.find('indexPattern-switch-link');
|
||||
return button.getAttribute('title');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue