mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Co-authored-by: Ryan Keairns <contactryank@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> # Conflicts: # packages/kbn-optimizer/limits.yml
This commit is contained in:
parent
1094b751fd
commit
a6e71edbf0
30 changed files with 1237 additions and 116 deletions
|
@ -28,6 +28,7 @@
|
|||
],
|
||||
"maps_legacy": "src/plugins/maps_legacy",
|
||||
"monaco": "packages/kbn-monaco/src",
|
||||
"presentationUtil": "src/plugins/presentation_util",
|
||||
"indexPatternManagement": "src/plugins/index_pattern_management",
|
||||
"advancedSettings": "src/plugins/advanced_settings",
|
||||
"kibana_legacy": "src/plugins/kibana_legacy",
|
||||
|
|
|
@ -156,6 +156,10 @@ It also provides a stateful version of it on the start contract.
|
|||
Content is fetched from the remote (https://feeds.elastic.co and https://feeds-staging.elastic.co in dev mode) once a day, with periodic checks if the content needs to be refreshed. All newsfeed content is hosted remotely.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/presentation_util/README.md[presentationUtil]
|
||||
|Utilities and components used by the presentation-related plugins
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/src/plugins/region_map/README.md[regionMap]
|
||||
|Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states.
|
||||
|
||||
|
|
|
@ -104,3 +104,4 @@ pageLoadAssetSize:
|
|||
watcher: 43742
|
||||
stackAlerts: 29684
|
||||
runtimeFields: 41752
|
||||
presentationUtil: 28545
|
||||
|
|
|
@ -549,6 +549,7 @@ export class DashboardAppController {
|
|||
incomingEmbeddable.type,
|
||||
incomingEmbeddable.input
|
||||
);
|
||||
updateViewMode(ViewMode.EDIT);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
3
src/plugins/presentation_util/README.md
Executable file
3
src/plugins/presentation_util/README.md
Executable file
|
@ -0,0 +1,3 @@
|
|||
# presentationUtil
|
||||
|
||||
Utilities and components used by the presentation-related plugins
|
21
src/plugins/presentation_util/common/index.ts
Normal file
21
src/plugins/presentation_util/common/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const PLUGIN_ID = 'presentationUtil';
|
||||
export const PLUGIN_NAME = 'presentationUtil';
|
9
src/plugins/presentation_util/kibana.json
Normal file
9
src/plugins/presentation_util/kibana.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "presentationUtil",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"server": false,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["dashboard", "savedObjects"],
|
||||
"optionalPlugins": []
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { SavedObjectsClientContract } from '../../../../core/public';
|
||||
import { SavedObjectDashboard } from '../../../../plugins/dashboard/public';
|
||||
|
||||
export interface DashboardPickerProps {
|
||||
onChange: (dashboard: { name: string; id: string } | null) => void;
|
||||
isDisabled: boolean;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
||||
interface DashboardOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function DashboardPicker(props: DashboardPickerProps) {
|
||||
const [dashboards, setDashboards] = useState<DashboardOption[]>([]);
|
||||
const [isLoadingDashboards, setIsLoadingDashboards] = useState(true);
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<DashboardOption | null>(null);
|
||||
|
||||
const { savedObjectsClient, isDisabled, onChange } = props;
|
||||
|
||||
const fetchDashboards = useCallback(
|
||||
async (query) => {
|
||||
setIsLoadingDashboards(true);
|
||||
setDashboards([]);
|
||||
|
||||
const { savedObjects } = await savedObjectsClient.find<SavedObjectDashboard>({
|
||||
type: 'dashboard',
|
||||
search: query ? `${query}*` : '',
|
||||
searchFields: ['title'],
|
||||
});
|
||||
if (savedObjects) {
|
||||
setDashboards(savedObjects.map((d) => ({ value: d.id, label: d.attributes.title })));
|
||||
}
|
||||
setIsLoadingDashboards(false);
|
||||
},
|
||||
[savedObjectsClient]
|
||||
);
|
||||
|
||||
// Initial dashboard load
|
||||
useEffect(() => {
|
||||
fetchDashboards('');
|
||||
}, [fetchDashboards]);
|
||||
|
||||
return (
|
||||
<EuiComboBox
|
||||
placeholder={i18n.translate('presentationUtil.dashboardPicker.searchDashboardPlaceholder', {
|
||||
defaultMessage: 'Search dashboards...',
|
||||
})}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={dashboards || []}
|
||||
selectedOptions={!!selectedDashboard ? [selectedDashboard] : undefined}
|
||||
onChange={(e) => {
|
||||
if (e.length) {
|
||||
setSelectedDashboard({ value: e[0].value || '', label: e[0].label });
|
||||
onChange({ name: e[0].label, id: e[0].value || '' });
|
||||
} else {
|
||||
setSelectedDashboard(null);
|
||||
onChange(null);
|
||||
}
|
||||
}}
|
||||
onSearchChange={fetchDashboards}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoadingDashboards}
|
||||
compressed={true}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.savAddDashboard__searchDashboards {
|
||||
margin-left: $euiSizeL;
|
||||
margin-top: $euiSizeXS;
|
||||
width: 300px;
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiRadio,
|
||||
EuiIconTip,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { SavedObjectsClientContract } from '../../../../core/public';
|
||||
|
||||
import {
|
||||
OnSaveProps,
|
||||
SaveModalState,
|
||||
SavedObjectSaveModal,
|
||||
} from '../../../../plugins/saved_objects/public';
|
||||
|
||||
import { DashboardPicker } from './dashboard_picker';
|
||||
|
||||
import './saved_object_save_modal_dashboard.scss';
|
||||
|
||||
interface SaveModalDocumentInfo {
|
||||
id?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface DashboardSaveModalProps {
|
||||
documentInfo: SaveModalDocumentInfo;
|
||||
objectType: string;
|
||||
onClose: () => void;
|
||||
onSave: (props: OnSaveProps & { dashboardId: string | null }) => void;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
|
||||
}
|
||||
|
||||
export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) {
|
||||
const { documentInfo, savedObjectsClient, tagOptions } = props;
|
||||
const initialCopyOnSave = !Boolean(documentInfo.id);
|
||||
|
||||
const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>(
|
||||
documentInfo.id ? null : 'existing'
|
||||
);
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>(
|
||||
null
|
||||
);
|
||||
const [copyOnSave, setCopyOnSave] = useState<boolean>(initialCopyOnSave);
|
||||
|
||||
const renderDashboardSelect = (state: SaveModalState) => {
|
||||
const isDisabled = Boolean(!state.copyOnSave && documentInfo.id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="presentationUtil.saveModalDashboard.addToDashboardLabel"
|
||||
defaultMessage="Add to dashboard"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
type="iInCircle"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="presentationUtil.saveModalDashboard.dashboardInfoTooltip"
|
||||
defaultMessage="Items added to a dashboard will not appear in the library and must be edited from the dashboard."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
hasChildLabel={false}
|
||||
>
|
||||
<EuiPanel color="subdued" hasShadow={false}>
|
||||
<div>
|
||||
<EuiRadio
|
||||
checked={dashboardOption === 'existing'}
|
||||
id="existing"
|
||||
name="dashboard-option"
|
||||
label={i18n.translate(
|
||||
'presentationUtil.saveModalDashboard.existingDashboardOptionLabel',
|
||||
{
|
||||
defaultMessage: 'Existing',
|
||||
}
|
||||
)}
|
||||
onChange={() => setDashboardOption('existing')}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
|
||||
<div className="savAddDashboard__searchDashboards">
|
||||
<DashboardPicker
|
||||
savedObjectsClient={savedObjectsClient}
|
||||
isDisabled={dashboardOption !== 'existing'}
|
||||
onChange={(dash) => {
|
||||
setSelectedDashboard(dash);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiRadio
|
||||
checked={dashboardOption === 'new'}
|
||||
id="new"
|
||||
name="dashboard-option"
|
||||
label={i18n.translate(
|
||||
'presentationUtil.saveModalDashboard.newDashboardOptionLabel',
|
||||
{
|
||||
defaultMessage: 'New',
|
||||
}
|
||||
)}
|
||||
onChange={() => setDashboardOption('new')}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiRadio
|
||||
checked={dashboardOption === null}
|
||||
id="library"
|
||||
name="dashboard-option"
|
||||
label={i18n.translate('presentationUtil.saveModalDashboard.libraryOptionLabel', {
|
||||
defaultMessage: 'No dashboard, but add to library',
|
||||
})}
|
||||
onChange={() => setDashboardOption(null)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const onCopyOnSaveChange = (newCopyOnSave: boolean) => {
|
||||
setDashboardOption(null);
|
||||
setCopyOnSave(newCopyOnSave);
|
||||
};
|
||||
|
||||
const onModalSave = (onSaveProps: OnSaveProps) => {
|
||||
let dashboardId = null;
|
||||
|
||||
// Don't save with a dashboard ID if we're
|
||||
// just updating an existing visualization
|
||||
if (!(!onSaveProps.newCopyOnSave && documentInfo.id)) {
|
||||
if (dashboardOption === 'existing') {
|
||||
dashboardId = selectedDashboard?.id || null;
|
||||
} else {
|
||||
dashboardId = dashboardOption;
|
||||
}
|
||||
}
|
||||
|
||||
props.onSave({ ...onSaveProps, dashboardId });
|
||||
};
|
||||
|
||||
const saveLibraryLabel =
|
||||
!copyOnSave && documentInfo.id
|
||||
? i18n.translate('presentationUtil.saveModalDashboard.saveLabel', {
|
||||
defaultMessage: 'Save',
|
||||
})
|
||||
: i18n.translate('presentationUtil.saveModalDashboard.saveToLibraryLabel', {
|
||||
defaultMessage: 'Save and add to library',
|
||||
});
|
||||
const saveDashboardLabel = i18n.translate(
|
||||
'presentationUtil.saveModalDashboard.saveAndGoToDashboardLabel',
|
||||
{
|
||||
defaultMessage: 'Save and go to Dashboard',
|
||||
}
|
||||
);
|
||||
|
||||
const confirmButtonLabel = dashboardOption === null ? saveLibraryLabel : saveDashboardLabel;
|
||||
|
||||
const isValid = !(dashboardOption === 'existing' && selectedDashboard === null);
|
||||
|
||||
return (
|
||||
<SavedObjectSaveModal
|
||||
onSave={onModalSave}
|
||||
onClose={props.onClose}
|
||||
title={documentInfo.title}
|
||||
showCopyOnSave={documentInfo.id ? true : false}
|
||||
initialCopyOnSave={initialCopyOnSave}
|
||||
confirmButtonLabel={confirmButtonLabel}
|
||||
objectType={props.objectType}
|
||||
options={dashboardOption === null ? tagOptions : undefined} // Show tags when not adding to dashboard
|
||||
rightOptions={renderDashboardSelect}
|
||||
description={documentInfo.description}
|
||||
showDescription={true}
|
||||
isValid={isValid}
|
||||
onCopyOnSaveChange={onCopyOnSaveChange}
|
||||
/>
|
||||
);
|
||||
}
|
30
src/plugins/presentation_util/public/index.ts
Normal file
30
src/plugins/presentation_util/public/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { PresentationUtilPlugin } from './plugin';
|
||||
|
||||
export {
|
||||
SavedObjectSaveModalDashboard,
|
||||
DashboardSaveModalProps,
|
||||
} from './components/saved_object_save_modal_dashboard';
|
||||
|
||||
export function plugin() {
|
||||
return new PresentationUtilPlugin();
|
||||
}
|
||||
export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types';
|
34
src/plugins/presentation_util/public/plugin.ts
Normal file
34
src/plugins/presentation_util/public/plugin.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { CoreSetup, CoreStart, Plugin } from '../../../core/public';
|
||||
import { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types';
|
||||
|
||||
export class PresentationUtilPlugin
|
||||
implements Plugin<PresentationUtilPluginSetup, PresentationUtilPluginStart> {
|
||||
public setup(core: CoreSetup): PresentationUtilPluginSetup {
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(core: CoreStart): PresentationUtilPluginStart {
|
||||
return {};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
23
src/plugins/presentation_util/public/types.ts
Normal file
23
src/plugins/presentation_util/public/types.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface PresentationUtilPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface PresentationUtilPluginStart {}
|
|
@ -25,7 +25,6 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = `
|
|||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiForm>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
|
@ -98,3 +97,311 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = `
|
|||
</form>
|
||||
</EuiOverlayMask>
|
||||
`;
|
||||
|
||||
exports[`SavedObjectSaveModal should render matching snapshot when custom isValid is set 1`] = `
|
||||
<EuiOverlayMask>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<EuiModal
|
||||
className="kbnSavedObjectSaveModal"
|
||||
data-test-subj="savedObjectSaveModal"
|
||||
onClose={[Function]}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save {objectType}"
|
||||
id="savedObjects.saveModal.saveTitle"
|
||||
values={
|
||||
Object {
|
||||
"objectType": "visualization",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Title"
|
||||
id="savedObjects.saveModal.titleLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
autoFocus={true}
|
||||
data-test-subj="savedObjectTitle"
|
||||
fullWidth={true}
|
||||
isInvalid={false}
|
||||
onChange={[Function]}
|
||||
value="Saved Object title"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Description"
|
||||
id="savedObjects.saveModal.descriptionLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="viewDescription"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="saveCancelButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="savedObjects.saveModal.cancelButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
data-test-subj="confirmSaveSavedObjectButton"
|
||||
fill={true}
|
||||
isDisabled={true}
|
||||
isLoading={false}
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</form>
|
||||
</EuiOverlayMask>
|
||||
`;
|
||||
|
||||
exports[`SavedObjectSaveModal should render matching snapshot when custom isValid is set 2`] = `
|
||||
<EuiOverlayMask>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<EuiModal
|
||||
className="kbnSavedObjectSaveModal"
|
||||
data-test-subj="savedObjectSaveModal"
|
||||
onClose={[Function]}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save {objectType}"
|
||||
id="savedObjects.saveModal.saveTitle"
|
||||
values={
|
||||
Object {
|
||||
"objectType": "visualization",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Title"
|
||||
id="savedObjects.saveModal.titleLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
autoFocus={true}
|
||||
data-test-subj="savedObjectTitle"
|
||||
fullWidth={true}
|
||||
isInvalid={false}
|
||||
onChange={[Function]}
|
||||
value="Saved Object title"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Description"
|
||||
id="savedObjects.saveModal.descriptionLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="viewDescription"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="saveCancelButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="savedObjects.saveModal.cancelButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
data-test-subj="confirmSaveSavedObjectButton"
|
||||
fill={true}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</form>
|
||||
</EuiOverlayMask>
|
||||
`;
|
||||
|
||||
exports[`SavedObjectSaveModal should render matching snapshot when given options 1`] = `
|
||||
<EuiOverlayMask>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<EuiModal
|
||||
className="kbnSavedObjectSaveModal kbnSavedObjectsSaveModal--wide"
|
||||
data-test-subj="savedObjectSaveModal"
|
||||
onClose={[Function]}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save {objectType}"
|
||||
id="savedObjects.saveModal.saveTitle"
|
||||
values={
|
||||
Object {
|
||||
"objectType": "visualization",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiForm>
|
||||
<EuiFlexGroup
|
||||
gutterSize="m"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Title"
|
||||
id="savedObjects.saveModal.titleLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
autoFocus={true}
|
||||
data-test-subj="savedObjectTitle"
|
||||
fullWidth={true}
|
||||
isInvalid={false}
|
||||
onChange={[Function]}
|
||||
value="Saved Object title"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Description"
|
||||
id="savedObjects.saveModal.descriptionLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="viewDescription"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<div>
|
||||
Hello! Main options
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<div>
|
||||
Hey there! Options on the right
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="saveCancelButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="savedObjects.saveModal.cancelButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
data-test-subj="confirmSaveSavedObjectButton"
|
||||
fill={true}
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</form>
|
||||
</EuiOverlayMask>
|
||||
`;
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.kbnSavedObjectSaveModal {
|
||||
width: $euiSizeXXL * 10;
|
||||
}
|
||||
}
|
||||
|
||||
.kbnSavedObjectsSaveModal--wide {
|
||||
width: 800px;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,50 @@ describe('SavedObjectSaveModal', () => {
|
|||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render matching snapshot when given options', () => {
|
||||
const wrapper = shallow(
|
||||
<SavedObjectSaveModal
|
||||
onSave={() => void 0}
|
||||
onClose={() => void 0}
|
||||
title={'Saved Object title'}
|
||||
showCopyOnSave={false}
|
||||
objectType="visualization"
|
||||
showDescription={true}
|
||||
options={<div>Hello! Main options</div>}
|
||||
rightOptions={<div>Hey there! Options on the right</div>}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render matching snapshot when custom isValid is set', () => {
|
||||
const falseWrapper = shallow(
|
||||
<SavedObjectSaveModal
|
||||
onSave={() => void 0}
|
||||
onClose={() => void 0}
|
||||
title={'Saved Object title'}
|
||||
showCopyOnSave={false}
|
||||
objectType="visualization"
|
||||
showDescription={true}
|
||||
isValid={false}
|
||||
/>
|
||||
);
|
||||
expect(falseWrapper).toMatchSnapshot();
|
||||
|
||||
const trueWrapper = shallow(
|
||||
<SavedObjectSaveModal
|
||||
onSave={() => void 0}
|
||||
onClose={() => void 0}
|
||||
title={'Saved Object title'}
|
||||
showCopyOnSave={false}
|
||||
objectType="visualization"
|
||||
showDescription={true}
|
||||
isValid={true}
|
||||
/>
|
||||
);
|
||||
expect(trueWrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('allows specifying custom save button label', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SavedObjectSaveModal
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiModal,
|
||||
|
@ -53,12 +55,15 @@ interface Props {
|
|||
onClose: () => void;
|
||||
title: string;
|
||||
showCopyOnSave: boolean;
|
||||
onCopyOnSaveChange?: (copyOnChange: boolean) => void;
|
||||
initialCopyOnSave?: boolean;
|
||||
objectType: string;
|
||||
confirmButtonLabel?: React.ReactNode;
|
||||
options?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
|
||||
rightOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
|
||||
description?: string;
|
||||
showDescription: boolean;
|
||||
isValid?: boolean;
|
||||
}
|
||||
|
||||
export interface SaveModalState {
|
||||
|
@ -87,12 +92,54 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
const { isTitleDuplicateConfirmed, hasTitleDuplicate, title } = this.state;
|
||||
const duplicateWarningId = generateId();
|
||||
|
||||
const hasColumns = !!this.props.rightOptions;
|
||||
|
||||
const formBodyContent = (
|
||||
<>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={<FormattedMessage id="savedObjects.saveModal.titleLabel" defaultMessage="Title" />}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
autoFocus
|
||||
data-test-subj="savedObjectTitle"
|
||||
value={title}
|
||||
onChange={this.onTitleChange}
|
||||
isInvalid={(!isTitleDuplicateConfirmed && hasTitleDuplicate) || title.length === 0}
|
||||
aria-describedby={this.state.hasTitleDuplicate ? duplicateWarningId : undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{this.renderViewDescription()}
|
||||
|
||||
{typeof this.props.options === 'function'
|
||||
? this.props.options(this.state)
|
||||
: this.props.options}
|
||||
</>
|
||||
);
|
||||
|
||||
const formBody = hasColumns ? (
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem>{formBodyContent}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{typeof this.props.rightOptions === 'function'
|
||||
? this.props.rightOptions(this.state)
|
||||
: this.props.rightOptions}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
formBodyContent
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<EuiModal
|
||||
data-test-subj="savedObjectSaveModal"
|
||||
className="kbnSavedObjectSaveModal"
|
||||
className={`kbnSavedObjectSaveModal${
|
||||
hasColumns ? ' kbnSavedObjectsSaveModal--wide' : ''
|
||||
}`}
|
||||
onClose={this.props.onClose}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
|
@ -114,38 +161,8 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
{this.props.description}
|
||||
</EuiText>
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{formBody}
|
||||
{this.renderCopyOnSave()}
|
||||
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModal.titleLabel"
|
||||
defaultMessage="Title"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
autoFocus
|
||||
data-test-subj="savedObjectTitle"
|
||||
value={title}
|
||||
onChange={this.onTitleChange}
|
||||
isInvalid={
|
||||
(!isTitleDuplicateConfirmed && hasTitleDuplicate) || title.length === 0
|
||||
}
|
||||
aria-describedby={this.state.hasTitleDuplicate ? duplicateWarningId : undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{this.renderViewDescription()}
|
||||
|
||||
{typeof this.props.options === 'function'
|
||||
? this.props.options(this.state)
|
||||
: this.props.options}
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
|
||||
|
@ -238,6 +255,10 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
this.setState({
|
||||
copyOnSave: event.target.checked,
|
||||
});
|
||||
|
||||
if (this.props.onCopyOnSaveChange) {
|
||||
this.props.onCopyOnSaveChange(event.target.checked);
|
||||
}
|
||||
};
|
||||
|
||||
private onFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
|
@ -259,12 +280,14 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
confirmLabel = this.props.confirmButtonLabel;
|
||||
}
|
||||
|
||||
const isValid = this.props.isValid !== undefined ? this.props.isValid : true;
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
fill
|
||||
data-test-subj="confirmSaveSavedObjectButton"
|
||||
isLoading={isLoading}
|
||||
isDisabled={title.length === 0}
|
||||
isDisabled={title.length === 0 || !isValid}
|
||||
type="submit"
|
||||
>
|
||||
{confirmLabel}
|
||||
|
@ -315,6 +338,7 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiSwitch
|
||||
data-test-subj="saveAsNewCheckbox"
|
||||
checked={this.state.copyOnSave}
|
||||
|
@ -327,7 +351,6 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
/>
|
||||
}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"kibanaReact",
|
||||
"home",
|
||||
"discover",
|
||||
"visDefaultEditor"
|
||||
"visDefaultEditor",
|
||||
"presentationUtil"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -14,4 +14,18 @@
|
|||
vertical-align: baseline;
|
||||
padding: 0 $euiSizeS;
|
||||
margin-left: $euiSizeS;
|
||||
}
|
||||
}
|
||||
|
||||
.visListingCallout {
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
padding: $euiSize $euiSize 0 $euiSize;
|
||||
}
|
||||
|
||||
.visListingCallout__link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
|
||||
import './visualize_listing.scss';
|
||||
|
||||
import React, { useCallback, useRef, useMemo, useEffect } from 'react';
|
||||
import React, { useCallback, useRef, useMemo, useEffect, MouseEvent } from 'react';
|
||||
import { EuiCallOut, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import useUnmount from 'react-use/lib/useUnmount';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
|
||||
|
@ -150,35 +152,65 @@ export const VisualizeListing = () => {
|
|||
: [];
|
||||
}, [savedObjectsTagging]);
|
||||
|
||||
const calloutMessage = (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="visualize.visualizeListingDashboardFlowDescription"
|
||||
defaultMessage="Building a dashboard? Create content directly from the {dashboardApp} using a new integrated workflow."
|
||||
values={{
|
||||
dashboardApp: (
|
||||
<EuiLink
|
||||
className="visListingCallout__link"
|
||||
onClick={(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
application.navigateToUrl(application.getUrlForApp('dashboards'));
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="visualize.visualizeListingDashboardAppName"
|
||||
defaultMessage="Dashboard application"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<TableListView
|
||||
headingId="visualizeListingHeading"
|
||||
// we allow users to create visualizations even if they can't save them
|
||||
// for data exploration purposes
|
||||
createItem={createNewVis}
|
||||
tableCaption={i18n.translate('visualize.listing.table.listTitle', {
|
||||
defaultMessage: 'Visualizations',
|
||||
})}
|
||||
findItems={fetchItems}
|
||||
deleteItems={visualizeCapabilities.delete ? deleteItems : undefined}
|
||||
editItem={visualizeCapabilities.save ? editItem : undefined}
|
||||
tableColumns={tableColumns}
|
||||
listingLimit={listingLimit}
|
||||
initialPageSize={savedObjectsPublic.settings.getPerPage()}
|
||||
initialFilter={''}
|
||||
rowHeader="title"
|
||||
noItemsFragment={noItemsFragment}
|
||||
entityName={i18n.translate('visualize.listing.table.entityName', {
|
||||
defaultMessage: 'visualization',
|
||||
})}
|
||||
entityNamePlural={i18n.translate('visualize.listing.table.entityNamePlural', {
|
||||
defaultMessage: 'visualizations',
|
||||
})}
|
||||
tableListTitle={i18n.translate('visualize.listing.table.listTitle', {
|
||||
defaultMessage: 'Visualizations',
|
||||
})}
|
||||
toastNotifications={toastNotifications}
|
||||
searchFilters={searchFilters}
|
||||
/>
|
||||
<>
|
||||
<div className="visListingCallout">
|
||||
<EuiCallOut size="s" title={calloutMessage} iconType="iInCircle" />
|
||||
</div>
|
||||
<TableListView
|
||||
headingId="visualizeListingHeading"
|
||||
// we allow users to create visualizations even if they can't save them
|
||||
// for data exploration purposes
|
||||
createItem={createNewVis}
|
||||
tableCaption={i18n.translate('visualize.listing.table.listTitle', {
|
||||
defaultMessage: 'Visualizations',
|
||||
})}
|
||||
findItems={fetchItems}
|
||||
deleteItems={visualizeCapabilities.delete ? deleteItems : undefined}
|
||||
editItem={visualizeCapabilities.save ? editItem : undefined}
|
||||
tableColumns={tableColumns}
|
||||
listingLimit={listingLimit}
|
||||
initialPageSize={savedObjectsPublic.settings.getPerPage()}
|
||||
initialFilter={''}
|
||||
rowHeader="title"
|
||||
noItemsFragment={noItemsFragment}
|
||||
entityName={i18n.translate('visualize.listing.table.entityName', {
|
||||
defaultMessage: 'visualization',
|
||||
})}
|
||||
entityNamePlural={i18n.translate('visualize.listing.table.entityNamePlural', {
|
||||
defaultMessage: 'visualizations',
|
||||
})}
|
||||
tableListTitle={i18n.translate('visualize.listing.table.listTitle', {
|
||||
defaultMessage: 'Visualizations',
|
||||
})}
|
||||
toastNotifications={toastNotifications}
|
||||
searchFilters={searchFilters}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -81,6 +81,7 @@ const TopNav = ({
|
|||
[visInstance.embeddableHandler]
|
||||
);
|
||||
const stateTransfer = services.embeddable.getStateTransfer();
|
||||
const savedObjectsClient = services.savedObjects.client;
|
||||
|
||||
const config = useMemo(() => {
|
||||
if (isEmbeddableRendered) {
|
||||
|
@ -96,6 +97,7 @@ const TopNav = ({
|
|||
stateContainer,
|
||||
visualizationIdFromUrl,
|
||||
stateTransfer,
|
||||
savedObjectsClient,
|
||||
embeddableId,
|
||||
onAppLeave,
|
||||
},
|
||||
|
@ -116,6 +118,7 @@ const TopNav = ({
|
|||
services,
|
||||
embeddableId,
|
||||
stateTransfer,
|
||||
savedObjectsClient,
|
||||
onAppLeave,
|
||||
]);
|
||||
const [indexPatterns, setIndexPatterns] = useState<IndexPattern[]>(
|
||||
|
|
|
@ -29,7 +29,9 @@ import {
|
|||
SavedObjectSaveOpts,
|
||||
OnSaveProps,
|
||||
} from '../../../../saved_objects/public';
|
||||
import { SavedObjectSaveModalDashboard } from '../../../../presentation_util/public';
|
||||
import { unhashUrl } from '../../../../kibana_utils/public';
|
||||
import { SavedObjectsClientContract } from '../../../../../core/public';
|
||||
|
||||
import {
|
||||
VisualizeServices,
|
||||
|
@ -51,6 +53,7 @@ interface TopNavConfigParams {
|
|||
stateContainer: VisualizeAppStateContainer;
|
||||
visualizationIdFromUrl?: string;
|
||||
stateTransfer: EmbeddableStateTransfer;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
embeddableId?: string;
|
||||
onAppLeave: AppMountParameters['onAppLeave'];
|
||||
}
|
||||
|
@ -65,6 +68,7 @@ export const getTopNavConfig = (
|
|||
hasUnappliedChanges,
|
||||
visInstance,
|
||||
stateContainer,
|
||||
savedObjectsClient,
|
||||
visualizationIdFromUrl,
|
||||
stateTransfer,
|
||||
embeddableId,
|
||||
|
@ -168,6 +172,7 @@ export const getTopNavConfig = (
|
|||
if (!originatingApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = {
|
||||
input: {
|
||||
savedVis: vis.serialize(),
|
||||
|
@ -298,10 +303,12 @@ export const getTopNavConfig = (
|
|||
onTitleDuplicate,
|
||||
newDescription,
|
||||
returnToOrigin,
|
||||
}: OnSaveProps & { returnToOrigin: boolean }) => {
|
||||
dashboardId,
|
||||
}: OnSaveProps & { returnToOrigin?: boolean } & { dashboardId?: string | null }) => {
|
||||
if (!savedVis) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTitle = savedVis.title;
|
||||
savedVis.title = newTitle;
|
||||
embeddableHandler.updateInput({ title: newTitle });
|
||||
|
@ -318,16 +325,48 @@ export const getTopNavConfig = (
|
|||
onTitleDuplicate,
|
||||
returnToOrigin,
|
||||
};
|
||||
|
||||
if (dashboardId) {
|
||||
const appPath = `${VisualizeConstants.LANDING_PAGE_PATH}`;
|
||||
|
||||
// Manually insert a new url so the back button will open the saved visualization.
|
||||
history.replace(appPath);
|
||||
setActiveUrl(appPath);
|
||||
|
||||
const state = {
|
||||
input: {
|
||||
savedVis: {
|
||||
...vis.serialize(),
|
||||
title: newTitle,
|
||||
description: newDescription,
|
||||
},
|
||||
} as VisualizeInput,
|
||||
embeddableId,
|
||||
type: VISUALIZE_EMBEDDABLE_TYPE,
|
||||
};
|
||||
|
||||
const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
|
||||
|
||||
stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
|
||||
state,
|
||||
path,
|
||||
});
|
||||
|
||||
// TODO: Saved Object Modal requires `id` to be defined so this is a workaround
|
||||
return { id: true };
|
||||
}
|
||||
|
||||
const response = await doSave(saveOptions);
|
||||
// If the save wasn't successful, put the original values back.
|
||||
if (!response.id || response.error) {
|
||||
savedVis.title = currentTitle;
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
let selectedTags: string[] = [];
|
||||
let options: React.ReactNode | undefined;
|
||||
let tagOptions: React.ReactNode | undefined;
|
||||
|
||||
if (
|
||||
savedVis &&
|
||||
|
@ -335,7 +374,7 @@ export const getTopNavConfig = (
|
|||
savedObjectsTagging.ui.hasTagDecoration(savedVis)
|
||||
) {
|
||||
selectedTags = savedVis.getTags();
|
||||
options = (
|
||||
tagOptions = (
|
||||
<savedObjectsTagging.ui.components.SavedObjectSaveModalTagSelector
|
||||
initialSelection={selectedTags}
|
||||
onTagsSelected={(newSelection) => {
|
||||
|
@ -345,17 +384,29 @@ export const getTopNavConfig = (
|
|||
);
|
||||
}
|
||||
|
||||
const saveModal = (
|
||||
<SavedObjectSaveModalOrigin
|
||||
documentInfo={savedVis || { title: '' }}
|
||||
onSave={onSave}
|
||||
options={options}
|
||||
getAppNameFromId={stateTransfer.getAppNameFromId}
|
||||
objectType={'visualization'}
|
||||
onClose={() => {}}
|
||||
originatingApp={originatingApp}
|
||||
/>
|
||||
);
|
||||
const saveModal =
|
||||
!!originatingApp ||
|
||||
!dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables ? (
|
||||
<SavedObjectSaveModalOrigin
|
||||
documentInfo={savedVis || { title: '' }}
|
||||
onSave={onSave}
|
||||
options={tagOptions}
|
||||
getAppNameFromId={stateTransfer.getAppNameFromId}
|
||||
objectType={'visualization'}
|
||||
onClose={() => {}}
|
||||
originatingApp={originatingApp}
|
||||
/>
|
||||
) : (
|
||||
<SavedObjectSaveModalDashboard
|
||||
documentInfo={savedVis || { title: '' }}
|
||||
onSave={onSave}
|
||||
tagOptions={tagOptions}
|
||||
objectType={'visualization'}
|
||||
onClose={() => {}}
|
||||
savedObjectsClient={savedObjectsClient}
|
||||
/>
|
||||
);
|
||||
|
||||
const isSaveAsButton = anchorElement.classList.contains('saveAsButton');
|
||||
onAppLeave((actions) => {
|
||||
return actions.default();
|
||||
|
|
|
@ -20,5 +20,5 @@
|
|||
"optionalPlugins": ["usageCollection", "taskManager", "globalSearch", "savedObjectsTagging"],
|
||||
"configPath": ["xpack", "lens"],
|
||||
"extraPublicDirs": ["common/constants"],
|
||||
"requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable", "lensOss"]
|
||||
"requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable", "lensOss", "presentationUtil"]
|
||||
}
|
||||
|
|
|
@ -630,7 +630,7 @@ describe('Lens App', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Shows Save and Return and Save As buttons in create by value mode', async () => {
|
||||
it('Shows Save and Return and Save As buttons in create by value mode with originating app', async () => {
|
||||
const props = makeDefaultProps();
|
||||
const services = makeDefaultServices();
|
||||
services.dashboardFeatureFlag = { allowByValueEmbeddables: true };
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
import { LENS_EMBEDDABLE_TYPE, getFullPath } from '../../common';
|
||||
import { LensAppProps, LensAppServices, LensAppState } from './types';
|
||||
import { getLensTopNavConfig } from './lens_top_nav';
|
||||
import { TagEnhancedSavedObjectSaveModalOrigin } from './tags_saved_object_save_modal_origin_wrapper';
|
||||
import { SaveModal } from './save_modal';
|
||||
import {
|
||||
LensByReferenceInput,
|
||||
LensEmbeddableInput,
|
||||
|
@ -48,6 +48,7 @@ export function App({
|
|||
initialInput,
|
||||
incomingState,
|
||||
redirectToOrigin,
|
||||
redirectToDashboard,
|
||||
setHeaderActionMenu,
|
||||
initialContext,
|
||||
}: LensAppProps) {
|
||||
|
@ -355,6 +356,7 @@ export function App({
|
|||
const runSave = async (
|
||||
saveProps: Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
dashboardId?: string | null;
|
||||
onTitleDuplicate?: OnSaveProps['onTitleDuplicate'];
|
||||
newDescription?: string;
|
||||
newTags?: string[];
|
||||
|
@ -429,6 +431,13 @@ export function App({
|
|||
});
|
||||
redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave });
|
||||
return;
|
||||
} else if (saveProps.dashboardId && redirectToDashboard) {
|
||||
// disabling the validation on app leave because the document has been saved.
|
||||
onAppLeave((actions) => {
|
||||
return actions.default();
|
||||
});
|
||||
redirectToDashboard(newInput, saveProps.dashboardId);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.toasts.addSuccess(
|
||||
|
@ -679,35 +688,28 @@ export function App({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{lastKnownDoc && state.isSaveModalVisible && (
|
||||
<TagEnhancedSavedObjectSaveModalOrigin
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
initialTags={tagsIds}
|
||||
originatingApp={incomingState?.originatingApp}
|
||||
onSave={(props) => runSave(props, { saveToLibrary: true })}
|
||||
onClose={() => {
|
||||
setState((s) => ({ ...s, isSaveModalVisible: false }));
|
||||
}}
|
||||
getAppNameFromId={() => getOriginatingAppName()}
|
||||
documentInfo={{
|
||||
id: lastKnownDoc.savedObjectId,
|
||||
title: lastKnownDoc.title || '',
|
||||
description: lastKnownDoc.description || '',
|
||||
}}
|
||||
returnToOriginSwitchLabel={
|
||||
getIsByValueMode() && initialInput
|
||||
? i18n.translate('xpack.lens.app.updatePanel', {
|
||||
defaultMessage: 'Update panel on {originatingAppName}',
|
||||
values: { originatingAppName: getOriginatingAppName() },
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
objectType={i18n.translate('xpack.lens.app.saveModalType', {
|
||||
defaultMessage: 'Lens visualization',
|
||||
})}
|
||||
data-test-subj="lnsApp_saveModalOrigin"
|
||||
/>
|
||||
)}
|
||||
<SaveModal
|
||||
isVisible={state.isSaveModalVisible}
|
||||
originatingApp={incomingState?.originatingApp}
|
||||
allowByValueEmbeddables={dashboardFeatureFlag.allowByValueEmbeddables}
|
||||
savedObjectsClient={savedObjectsClient}
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
tagsIds={tagsIds}
|
||||
onSave={runSave}
|
||||
onClose={() => {
|
||||
setState((s) => ({ ...s, isSaveModalVisible: false }));
|
||||
}}
|
||||
getAppNameFromId={() => getOriginatingAppName()}
|
||||
lastKnownDoc={lastKnownDoc}
|
||||
returnToOriginSwitchLabel={
|
||||
getIsByValueMode() && initialInput
|
||||
? i18n.translate('xpack.lens.app.updatePanel', {
|
||||
defaultMessage: 'Update panel on {originatingAppName}',
|
||||
values: { originatingAppName: getOriginatingAppName() },
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -107,6 +107,23 @@ export async function mountApp(
|
|||
}
|
||||
};
|
||||
|
||||
const redirectToDashboard = (embeddableInput: LensEmbeddableInput, dashboardId: string) => {
|
||||
if (!lensServices.dashboardFeatureFlag.allowByValueEmbeddables) {
|
||||
throw new Error('redirectToDashboard called with by-value embeddables disabled');
|
||||
}
|
||||
|
||||
const state = {
|
||||
input: embeddableInput,
|
||||
type: LENS_EMBEDDABLE_TYPE,
|
||||
};
|
||||
|
||||
const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
|
||||
stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
|
||||
state,
|
||||
path,
|
||||
});
|
||||
};
|
||||
|
||||
const redirectToOrigin = (props?: RedirectToOriginProps) => {
|
||||
if (!embeddableEditorIncomingState?.originatingApp) {
|
||||
throw new Error('redirectToOrigin called without an originating app');
|
||||
|
@ -135,6 +152,7 @@ export async function mountApp(
|
|||
initialInput={getInitialInput(routeProps)}
|
||||
redirectTo={(savedObjectId?: string) => redirectTo(routeProps, savedObjectId)}
|
||||
redirectToOrigin={redirectToOrigin}
|
||||
redirectToDashboard={redirectToDashboard}
|
||||
onAppLeave={params.onAppLeave}
|
||||
setHeaderActionMenu={params.setHeaderActionMenu}
|
||||
history={routeProps.history}
|
||||
|
|
106
x-pack/plugins/lens/public/app_plugin/save_modal.tsx
Normal file
106
x-pack/plugins/lens/public/app_plugin/save_modal.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { SavedObjectsStart } from '../../../../../src/core/public';
|
||||
|
||||
import { Document } from '../persistence';
|
||||
import type { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public';
|
||||
|
||||
import {
|
||||
TagEnhancedSavedObjectSaveModalOrigin,
|
||||
OriginSaveProps,
|
||||
} from './tags_saved_object_save_modal_origin_wrapper';
|
||||
import {
|
||||
TagEnhancedSavedObjectSaveModalDashboard,
|
||||
DashboardSaveProps,
|
||||
} from './tags_saved_object_save_modal_dashboard_wrapper';
|
||||
|
||||
export type SaveProps = OriginSaveProps | DashboardSaveProps;
|
||||
|
||||
export interface Props {
|
||||
isVisible: boolean;
|
||||
|
||||
originatingApp?: string;
|
||||
allowByValueEmbeddables: boolean;
|
||||
|
||||
savedObjectsClient: SavedObjectsStart['client'];
|
||||
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
tagsIds: string[];
|
||||
|
||||
lastKnownDoc?: Document;
|
||||
|
||||
getAppNameFromId: () => string | undefined;
|
||||
returnToOriginSwitchLabel?: string;
|
||||
|
||||
onClose: () => void;
|
||||
onSave: (props: SaveProps, options: { saveToLibrary: boolean }) => void;
|
||||
}
|
||||
|
||||
export const SaveModal = (props: Props) => {
|
||||
if (!props.isVisible || !props.lastKnownDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
originatingApp,
|
||||
savedObjectsTagging,
|
||||
savedObjectsClient,
|
||||
tagsIds,
|
||||
lastKnownDoc,
|
||||
allowByValueEmbeddables,
|
||||
returnToOriginSwitchLabel,
|
||||
getAppNameFromId,
|
||||
onClose,
|
||||
onSave,
|
||||
} = props;
|
||||
|
||||
// Use the modal with return-to-origin features if we're in an app's edit flow or if by-value embeddables are disabled
|
||||
if (originatingApp || !allowByValueEmbeddables) {
|
||||
return (
|
||||
<TagEnhancedSavedObjectSaveModalOrigin
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
initialTags={tagsIds}
|
||||
originatingApp={originatingApp}
|
||||
onClose={onClose}
|
||||
onSave={(saveProps) => onSave(saveProps, { saveToLibrary: true })}
|
||||
getAppNameFromId={getAppNameFromId}
|
||||
documentInfo={{
|
||||
id: lastKnownDoc.savedObjectId,
|
||||
title: lastKnownDoc.title || '',
|
||||
description: lastKnownDoc.description || '',
|
||||
}}
|
||||
returnToOriginSwitchLabel={returnToOriginSwitchLabel}
|
||||
objectType={i18n.translate('xpack.lens.app.saveModalType', {
|
||||
defaultMessage: 'Lens visualization',
|
||||
})}
|
||||
data-test-subj="lnsApp_saveModalOrigin"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TagEnhancedSavedObjectSaveModalDashboard
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
savedObjectsClient={savedObjectsClient}
|
||||
initialTags={tagsIds}
|
||||
onSave={(saveProps) => onSave(saveProps, { saveToLibrary: false })}
|
||||
onClose={onClose}
|
||||
documentInfo={{
|
||||
id: lastKnownDoc.savedObjectId,
|
||||
title: lastKnownDoc.title || '',
|
||||
description: lastKnownDoc.description || '',
|
||||
}}
|
||||
objectType={i18n.translate('xpack.lens.app.saveModalType', {
|
||||
defaultMessage: 'Lens visualization',
|
||||
})}
|
||||
data-test-subj="lnsApp_saveModalDashboard"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useMemo, useCallback } from 'react';
|
||||
import { OnSaveProps } from '../../../../../src/plugins/saved_objects/public';
|
||||
import {
|
||||
DashboardSaveModalProps,
|
||||
SavedObjectSaveModalDashboard,
|
||||
} from '../../../../../src/plugins/presentation_util/public';
|
||||
import { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public';
|
||||
|
||||
export type DashboardSaveProps = OnSaveProps & {
|
||||
returnToOrigin: boolean;
|
||||
dashboardId?: string | null;
|
||||
newTags?: string[];
|
||||
};
|
||||
|
||||
export type TagEnhancedSavedObjectSaveModalDashboardProps = Omit<
|
||||
DashboardSaveModalProps,
|
||||
'onSave'
|
||||
> & {
|
||||
initialTags: string[];
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
onSave: (props: DashboardSaveProps) => void;
|
||||
};
|
||||
|
||||
export const TagEnhancedSavedObjectSaveModalDashboard: FC<TagEnhancedSavedObjectSaveModalDashboardProps> = ({
|
||||
initialTags,
|
||||
onSave,
|
||||
savedObjectsTagging,
|
||||
...otherProps
|
||||
}) => {
|
||||
const [selectedTags, setSelectedTags] = useState(initialTags);
|
||||
|
||||
const tagSelectorOption = useMemo(
|
||||
() =>
|
||||
savedObjectsTagging ? (
|
||||
<savedObjectsTagging.ui.components.SavedObjectSaveModalTagSelector
|
||||
initialSelection={initialTags}
|
||||
onTagsSelected={setSelectedTags}
|
||||
/>
|
||||
) : undefined,
|
||||
[savedObjectsTagging, initialTags]
|
||||
);
|
||||
|
||||
const tagEnhancedOptions = <>{tagSelectorOption}</>;
|
||||
|
||||
const tagEnhancedOnSave: DashboardSaveModalProps['onSave'] = useCallback(
|
||||
(saveOptions) => {
|
||||
onSave({
|
||||
...saveOptions,
|
||||
returnToOrigin: false,
|
||||
newTags: selectedTags,
|
||||
});
|
||||
},
|
||||
[onSave, selectedTags]
|
||||
);
|
||||
|
||||
return (
|
||||
<SavedObjectSaveModalDashboard
|
||||
{...otherProps}
|
||||
onSave={tagEnhancedOnSave}
|
||||
tagOptions={tagEnhancedOptions}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -13,10 +13,12 @@ import {
|
|||
} from '../../../../../src/plugins/saved_objects/public';
|
||||
import { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public';
|
||||
|
||||
type TagEnhancedSavedObjectSaveModalOriginProps = Omit<OriginSaveModalProps, 'onSave'> & {
|
||||
export type OriginSaveProps = OnSaveProps & { returnToOrigin: boolean; newTags?: string[] };
|
||||
|
||||
export type TagEnhancedSavedObjectSaveModalOriginProps = Omit<OriginSaveModalProps, 'onSave'> & {
|
||||
initialTags: string[];
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
onSave: (props: OnSaveProps & { returnToOrigin: boolean; newTags?: string[] }) => void;
|
||||
onSave: (props: OriginSaveProps) => void;
|
||||
};
|
||||
|
||||
export const TagEnhancedSavedObjectSaveModalOrigin: FC<TagEnhancedSavedObjectSaveModalOriginProps> = ({
|
||||
|
|
|
@ -76,6 +76,7 @@ export interface LensAppProps {
|
|||
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
||||
redirectTo: (savedObjectId?: string) => void;
|
||||
redirectToOrigin?: (props?: RedirectToOriginProps) => void;
|
||||
redirectToDashboard?: (input: LensEmbeddableInput, dashboardId: string) => void;
|
||||
|
||||
// The initial input passed in by the container when editing. Can be either by reference or by value.
|
||||
initialInput?: LensEmbeddableInput;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue