mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Lens] Prevent overwriting managed content from editor (#175062)
## Summary Close https://github.com/elastic/kibana/issues/166720 I marked this a breaking change since it is preventing users from doing something they have been able to do before. They can no longer save changes to managed Lens visualizations. Instead, they have to save changes to a new visualization. To test, import this `ndjson` file which includes both a managed and an unmanaged visualization: --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
274c314236
commit
1d4b7df989
41 changed files with 333 additions and 47 deletions
|
@ -291,6 +291,7 @@ enabled:
|
|||
- x-pack/test/functional/apps/lens/open_in_lens/dashboard/config.ts
|
||||
- x-pack/test/functional/apps/license_management/config.ts
|
||||
- x-pack/test/functional/apps/logstash/config.ts
|
||||
- x-pack/test/functional/apps/managed_content/config.ts
|
||||
- x-pack/test/functional/apps/management/config.ts
|
||||
- x-pack/test/functional/apps/maps/group1/config.ts
|
||||
- x-pack/test/functional/apps/maps/group2/config.ts
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -503,6 +503,7 @@ packages/kbn-logging @elastic/kibana-core
|
|||
packages/kbn-logging-mocks @elastic/kibana-core
|
||||
x-pack/plugins/logs_shared @elastic/obs-ux-logs-team
|
||||
x-pack/plugins/logstash @elastic/logstash
|
||||
packages/kbn-managed-content-badge @elastic/kibana-visualizations
|
||||
packages/kbn-managed-vscode-config @elastic/kibana-operations
|
||||
packages/kbn-managed-vscode-config-cli @elastic/kibana-operations
|
||||
packages/kbn-management/cards_navigation @elastic/platform-deployment-management
|
||||
|
|
|
@ -141,7 +141,8 @@
|
|||
"unifiedFieldList": "packages/kbn-unified-field-list",
|
||||
"unifiedHistogram": "src/plugins/unified_histogram",
|
||||
"unifiedDataTable": "packages/kbn-unified-data-table",
|
||||
"unsavedChangesBadge": "packages/kbn-unsaved-changes-badge"
|
||||
"unsavedChangesBadge": "packages/kbn-unsaved-changes-badge",
|
||||
"managedContentBadge": "packages/kbn-managed-content-badge"
|
||||
},
|
||||
"translations": []
|
||||
}
|
||||
|
|
|
@ -524,6 +524,7 @@
|
|||
"@kbn/logging-mocks": "link:packages/kbn-logging-mocks",
|
||||
"@kbn/logs-shared-plugin": "link:x-pack/plugins/logs_shared",
|
||||
"@kbn/logstash-plugin": "link:x-pack/plugins/logstash",
|
||||
"@kbn/managed-content-badge": "link:packages/kbn-managed-content-badge",
|
||||
"@kbn/management-cards-navigation": "link:packages/kbn-management/cards_navigation",
|
||||
"@kbn/management-plugin": "link:src/plugins/management",
|
||||
"@kbn/management-settings-application": "link:packages/kbn-management/settings/application",
|
||||
|
|
|
@ -402,8 +402,13 @@ export class TestSubjects extends FtrService {
|
|||
}
|
||||
}
|
||||
|
||||
public async isEuiSwitchChecked(selector: string) {
|
||||
const euiSwitch = await this.find(selector);
|
||||
public async isEuiSwitchChecked(selector: string | WebElementWrapper) {
|
||||
let euiSwitch: WebElementWrapper;
|
||||
if (typeof selector === 'string') {
|
||||
euiSwitch = await this.find(selector);
|
||||
} else {
|
||||
euiSwitch = selector;
|
||||
}
|
||||
const isChecked = await euiSwitch.getAttribute('aria-checked');
|
||||
return isChecked === 'true';
|
||||
}
|
||||
|
|
3
packages/kbn-managed-content-badge/README.md
Normal file
3
packages/kbn-managed-content-badge/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/managed-content-badge
|
||||
|
||||
Empty package generated by @kbn/generate
|
28
packages/kbn-managed-content-badge/index.ts
Normal file
28
packages/kbn-managed-content-badge/index.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import type { EuiToolTipProps } from '@elastic/eui';
|
||||
import type { TopNavMenuBadgeProps } from '@kbn/navigation-plugin/public';
|
||||
|
||||
export const getManagedContentBadge: (tooltipText: string) => TopNavMenuBadgeProps = (
|
||||
tooltipText
|
||||
) => ({
|
||||
'data-test-subj': 'managedContentBadge',
|
||||
badgeText: i18n.translate('managedContentBadge.text', {
|
||||
defaultMessage: 'Managed',
|
||||
}),
|
||||
title: i18n.translate('managedContentBadge.text', {
|
||||
defaultMessage: 'Managed',
|
||||
}),
|
||||
color: 'primary',
|
||||
iconType: 'glasses',
|
||||
toolTipProps: {
|
||||
content: tooltipText,
|
||||
position: 'bottom',
|
||||
} as EuiToolTipProps,
|
||||
});
|
13
packages/kbn-managed-content-badge/jest.config.js
Normal file
13
packages/kbn-managed-content-badge/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-managed-content-badge'],
|
||||
};
|
5
packages/kbn-managed-content-badge/kibana.jsonc
Normal file
5
packages/kbn-managed-content-badge/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/managed-content-badge",
|
||||
"owner": "@elastic/kibana-visualizations"
|
||||
}
|
6
packages/kbn-managed-content-badge/package.json
Normal file
6
packages/kbn-managed-content-badge/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/managed-content-badge",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
20
packages/kbn-managed-content-badge/tsconfig.json
Normal file
20
packages/kbn-managed-content-badge/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/i18n",
|
||||
"@kbn/navigation-plugin",
|
||||
]
|
||||
}
|
|
@ -26,10 +26,6 @@ export const dashboardReadonlyBadge = {
|
|||
};
|
||||
|
||||
export const dashboardManagedBadge = {
|
||||
getText: () =>
|
||||
i18n.translate('dashboard.badge.managed.text', {
|
||||
defaultMessage: 'Managed',
|
||||
}),
|
||||
getTooltip: () =>
|
||||
i18n.translate('dashboard.badge.managed.tooltip', {
|
||||
defaultMessage: 'This dashboard is system managed. Clone this dashboard to make changes.',
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
LazyLabsFlyout,
|
||||
getContextProvider as getPresentationUtilContextProvider,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { getManagedContentBadge } from '@kbn/managed-content-badge';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
||||
|
@ -305,17 +306,7 @@ export function InternalDashboardTopNav({
|
|||
});
|
||||
}
|
||||
if (showWriteControls && managed) {
|
||||
allBadges.push({
|
||||
'data-test-subj': 'dashboardSaveRecommendedBadge',
|
||||
badgeText: dashboardManagedBadge.getText(),
|
||||
title: '',
|
||||
color: 'primary',
|
||||
iconType: 'glasses',
|
||||
toolTipProps: {
|
||||
content: dashboardManagedBadge.getTooltip(),
|
||||
position: 'bottom',
|
||||
} as EuiToolTipProps,
|
||||
});
|
||||
allBadges.push(getManagedContentBadge(dashboardManagedBadge.getTooltip()));
|
||||
}
|
||||
return allBadges;
|
||||
}, [hasUnsavedChanges, viewMode, hasRunMigrations, showWriteControls, managed]);
|
||||
|
|
|
@ -74,7 +74,8 @@
|
|||
"@kbn/presentation-containers",
|
||||
"@kbn/presentation-panel-plugin",
|
||||
"@kbn/content-management-table-list-view-common",
|
||||
"@kbn/shared-ux-utility"
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/managed-content-badge"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -117,6 +117,7 @@ function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) {
|
|||
options={isAddToLibrarySelected ? tagOptions : undefined} // Show tags when not adding to dashboard
|
||||
description={documentInfo.description}
|
||||
showDescription={true}
|
||||
mustCopyOnSaveMessage={props.mustCopyOnSaveMessage}
|
||||
{...{
|
||||
confirmButtonLabel,
|
||||
initialCopyOnSave,
|
||||
|
|
|
@ -151,7 +151,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp
|
|||
content={
|
||||
<FormattedMessage
|
||||
id="presentationUtil.saveModalDashboard.dashboardInfoTooltip"
|
||||
defaultMessage="items added to the Visualize Library are available to all dashboards. Edits to a library item appear everywhere it is used."
|
||||
defaultMessage="Items added to the Visualize Library are available to all dashboards. Edits to a library item appear everywhere it is used."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -27,6 +27,8 @@ export interface SaveModalDashboardProps {
|
|||
onClose: () => void;
|
||||
onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void;
|
||||
tagOptions?: React.ReactNode | ((state: SaveModalState) => React.ReactNode);
|
||||
// include a message if the user has to copy on save
|
||||
mustCopyOnSaveMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,7 +10,8 @@ import { shallow } from 'enzyme';
|
|||
import React from 'react';
|
||||
import { SavedObjectSaveModal } from './saved_object_save_modal';
|
||||
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('SavedObjectSaveModal', () => {
|
||||
it('should render matching snapshot', () => {
|
||||
|
@ -72,7 +73,9 @@ describe('SavedObjectSaveModal', () => {
|
|||
});
|
||||
|
||||
it('allows specifying custom save button label', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
const confirmButtonLabel = 'Save and done';
|
||||
|
||||
render(
|
||||
<SavedObjectSaveModal
|
||||
onSave={() => void 0}
|
||||
onClose={() => void 0}
|
||||
|
@ -80,11 +83,36 @@ describe('SavedObjectSaveModal', () => {
|
|||
showCopyOnSave={false}
|
||||
objectType="visualization"
|
||||
showDescription={true}
|
||||
confirmButtonLabel="Save and done"
|
||||
confirmButtonLabel={confirmButtonLabel}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('button[data-test-subj="confirmSaveSavedObjectButton"]').text()).toBe(
|
||||
'Save and done'
|
||||
|
||||
expect(screen.queryByText(confirmButtonLabel)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('enforces copy on save', async () => {
|
||||
const onSave = jest.fn();
|
||||
|
||||
render(
|
||||
<SavedObjectSaveModal
|
||||
onSave={onSave}
|
||||
onClose={() => void 0}
|
||||
title={'Saved Object title'}
|
||||
objectType="visualization"
|
||||
showDescription={true}
|
||||
showCopyOnSave={true}
|
||||
mustCopyOnSaveMessage="You must save a copy of the object."
|
||||
/>
|
||||
);
|
||||
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
|
||||
expect(screen.getByTestId('saveAsNewCheckbox')).toBeDisabled();
|
||||
userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(onSave.mock.calls[0][0].newCopyOnSave).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,11 +25,13 @@ import {
|
|||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
EuiTextArea,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
||||
export interface OnSaveProps {
|
||||
newTitle: string;
|
||||
|
@ -44,6 +46,7 @@ interface Props {
|
|||
onClose: () => void;
|
||||
title: string;
|
||||
showCopyOnSave: boolean;
|
||||
mustCopyOnSaveMessage?: string;
|
||||
onCopyOnSaveChange?: (copyOnChange: boolean) => void;
|
||||
initialCopyOnSave?: boolean;
|
||||
objectType: string;
|
||||
|
@ -173,7 +176,7 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
|
||||
<EuiModalFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
|
||||
{this.props.showCopyOnSave && <EuiFlexItem grow>{this.renderCopyOnSave()}</EuiFlexItem>}
|
||||
{this.props.showCopyOnSave && this.renderCopyOnSave()}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="saveCancelButton" onClick={this.props.onClose}>
|
||||
<FormattedMessage
|
||||
|
@ -249,7 +252,7 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
|
||||
await this.props.onSave({
|
||||
newTitle: this.state.title,
|
||||
newCopyOnSave: this.state.copyOnSave,
|
||||
newCopyOnSave: Boolean(this.props.mustCopyOnSaveMessage) || this.state.copyOnSave,
|
||||
isTitleDuplicateConfirmed: this.state.isTitleDuplicateConfirmed,
|
||||
onTitleDuplicate: this.onTitleDuplicate,
|
||||
newDescription: this.state.visualizationDescription,
|
||||
|
@ -355,18 +358,29 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
|
|||
|
||||
private renderCopyOnSave = () => {
|
||||
return (
|
||||
<EuiSwitch
|
||||
data-test-subj="saveAsNewCheckbox"
|
||||
checked={this.state.copyOnSave}
|
||||
onChange={this.onCopyOnSaveChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModal.saveAsNewLabel"
|
||||
defaultMessage="Save as new {objectType}"
|
||||
values={{ objectType: this.props.objectType }}
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
data-test-subj="saveAsNewCheckbox"
|
||||
checked={Boolean(this.props.mustCopyOnSaveMessage) || this.state.copyOnSave}
|
||||
disabled={Boolean(this.props.mustCopyOnSaveMessage)}
|
||||
onChange={this.onCopyOnSaveChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="savedObjects.saveModal.saveAsNewLabel"
|
||||
defaultMessage="Save as new {objectType}"
|
||||
values={{ objectType: this.props.objectType }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{this.props.mustCopyOnSaveMessage && (
|
||||
<EuiFlexItem css={{ marginLeft: `-${euiThemeVars.euiSize}` }} grow={false}>
|
||||
<EuiIconTip type="iInCircle" content={this.props.mustCopyOnSaveMessage} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={true} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
"@kbn/i18n",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/ui-theme",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{"attributes":{"allowHidden":false,"fieldAttrs":"{}","fieldFormatMap":"{}","fields":"[]","name":"logstash-*","runtimeFieldMap":"{}","sourceFilters":"[]","timeFieldName":"@timestamp","title":"logstash-*"},"coreMigrationVersion":"8.8.0","created_at":"2024-01-18T17:35:58.606Z","id":"5f863f70-4728-4e8d-b441-db08f8c33b28","managed":false,"references":[],"type":"index-pattern","typeMigrationVersion":"8.0.0","updated_at":"2024-01-18T17:35:58.606Z","version":"WzI4LDFd"}
|
||||
|
||||
{"attributes":{"description":"","state":{"adHocDataViews":{},"datasourceStates":{"formBased":{"layers":{"e633b1af-3ab4-4bf5-8faa-fefde06c4a4a":{"columnOrder":["f2555a1a-6f93-43fd-bc63-acdfadd47729","d229daf9-9658-4579-99af-01d8adb2f25f"],"columns":{"d229daf9-9658-4579-99af-01d8adb2f25f":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","params":{"emptyAsNull":true},"scale":"ratio","sourceField":"bytes"},"f2555a1a-6f93-43fd-bc63-acdfadd47729":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"dropPartials":false,"includeEmptyRows":true,"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{},"sampling":1}}},"indexpattern":{"layers":{}},"textBased":{"layers":{}}},"filters":[],"internalReferences":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"labelsOrientation":{"x":0,"yLeft":0,"yRight":0},"layers":[{"accessors":["d229daf9-9658-4579-99af-01d8adb2f25f"],"layerId":"e633b1af-3ab4-4bf5-8faa-fefde06c4a4a","layerType":"data","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"f2555a1a-6f93-43fd-bc63-acdfadd47729"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"Lens vis (managed)","visualizationType":"lnsXY"},"coreMigrationVersion":"8.8.0","created_at":"2024-01-18T17:42:12.920Z","id":"managed-36db-4a3b-a4ba-7a64ab8f130b","managed":true,"references":[{"id":"5f863f70-4728-4e8d-b441-db08f8c33b28","name":"indexpattern-datasource-layer-e633b1af-3ab4-4bf5-8faa-fefde06c4a4a","type":"index-pattern"}],"type":"lens","typeMigrationVersion":"8.9.0","updated_at":"2024-01-18T17:42:12.920Z","version":"WzQ1LDFd"}
|
||||
|
||||
{"attributes":{"description":"","state":{"adHocDataViews":{},"datasourceStates":{"formBased":{"layers":{"e633b1af-3ab4-4bf5-8faa-fefde06c4a4a":{"columnOrder":["f2555a1a-6f93-43fd-bc63-acdfadd47729","d229daf9-9658-4579-99af-01d8adb2f25f"],"columns":{"d229daf9-9658-4579-99af-01d8adb2f25f":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","params":{"emptyAsNull":true},"scale":"ratio","sourceField":"bytes"},"f2555a1a-6f93-43fd-bc63-acdfadd47729":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"dropPartials":false,"includeEmptyRows":true,"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{},"sampling":1}}},"indexpattern":{"layers":{}},"textBased":{"layers":{}}},"filters":[],"internalReferences":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"labelsOrientation":{"x":0,"yLeft":0,"yRight":0},"layers":[{"accessors":["d229daf9-9658-4579-99af-01d8adb2f25f"],"layerId":"e633b1af-3ab4-4bf5-8faa-fefde06c4a4a","layerType":"data","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"f2555a1a-6f93-43fd-bc63-acdfadd47729"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"Lens vis (unmanaged)","visualizationType":"lnsXY"},"coreMigrationVersion":"8.8.0","created_at":"2024-01-18T17:42:12.920Z","id":"unmanaged-36db-4a3b-a4ba-7a64ab8f130b","managed":false,"references":[{"id":"5f863f70-4728-4e8d-b441-db08f8c33b28","name":"indexpattern-datasource-layer-e633b1af-3ab4-4bf5-8faa-fefde06c4a4a","type":"index-pattern"}],"type":"lens","typeMigrationVersion":"8.9.0","updated_at":"2024-01-18T17:42:12.920Z","version":"WzQ1LDFd"}
|
|
@ -1000,6 +1000,8 @@
|
|||
"@kbn/logs-shared-plugin/*": ["x-pack/plugins/logs_shared/*"],
|
||||
"@kbn/logstash-plugin": ["x-pack/plugins/logstash"],
|
||||
"@kbn/logstash-plugin/*": ["x-pack/plugins/logstash/*"],
|
||||
"@kbn/managed-content-badge": ["packages/kbn-managed-content-badge"],
|
||||
"@kbn/managed-content-badge/*": ["packages/kbn-managed-content-badge/*"],
|
||||
"@kbn/managed-vscode-config": ["packages/kbn-managed-vscode-config"],
|
||||
"@kbn/managed-vscode-config/*": ["packages/kbn-managed-vscode-config/*"],
|
||||
"@kbn/managed-vscode-config-cli": ["packages/kbn-managed-vscode-config-cli"],
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
updateIndexPatterns,
|
||||
selectActiveDatasourceId,
|
||||
selectFramePublicAPI,
|
||||
selectIsManaged,
|
||||
} from '../state_management';
|
||||
import { SaveModalContainer, runSaveLensVisualization } from './save_modal_container';
|
||||
import { LensInspector } from '../lens_inspector_service';
|
||||
|
@ -509,6 +510,8 @@ export function App({
|
|||
[locator, shortUrls]
|
||||
);
|
||||
|
||||
const isManaged = useLensSelector(selectIsManaged);
|
||||
|
||||
const returnToOriginSwitchLabelForContext =
|
||||
initialContext &&
|
||||
'isEmbeddable' in initialContext &&
|
||||
|
@ -614,6 +617,7 @@ export function App({
|
|||
initialInput={initialInput}
|
||||
redirectTo={redirectTo}
|
||||
redirectToOrigin={redirectToOrigin}
|
||||
managed={isManaged}
|
||||
initialContext={initialContext}
|
||||
returnToOriginSwitchLabel={
|
||||
returnToOriginSwitchLabelForContext ??
|
||||
|
|
|
@ -15,6 +15,7 @@ import { getEsQueryConfig } from '@kbn/data-plugin/public';
|
|||
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
|
||||
import { getManagedContentBadge } from '@kbn/managed-content-badge';
|
||||
import moment from 'moment';
|
||||
import { LENS_APP_LOCATOR } from '../../common/locator/locator';
|
||||
import { LENS_APP_NAME } from '../../common/constants';
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
useLensDispatch,
|
||||
LensAppState,
|
||||
switchAndCleanDatasource,
|
||||
selectIsManaged,
|
||||
} from '../state_management';
|
||||
import {
|
||||
getIndexPatternsObjects,
|
||||
|
@ -1056,6 +1058,8 @@ export const LensTopNavMenu = ({
|
|||
severity: 'error',
|
||||
}).map(({ shortMessage }) => new Error(shortMessage));
|
||||
|
||||
const managed = useLensSelector(selectIsManaged);
|
||||
|
||||
return (
|
||||
<AggregateQueryTopNavMenu
|
||||
setMenuMountPoint={setHeaderActionMenu}
|
||||
|
@ -1065,6 +1069,18 @@ export const LensTopNavMenu = ({
|
|||
? 'allowed_by_app_privilege'
|
||||
: 'globally_managed'
|
||||
}
|
||||
badges={
|
||||
managed
|
||||
? [
|
||||
getManagedContentBadge(
|
||||
i18n.translate('xpack.lens.managedBadgeTooltip', {
|
||||
defaultMessage:
|
||||
'This visualization is managed by Elastic. Changes made here must be saved in a new visualization.',
|
||||
})
|
||||
),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
savedQuery={savedQuery}
|
||||
onQuerySubmit={onQuerySubmitWrapped}
|
||||
onSaved={onSavedWrapped}
|
||||
|
|
|
@ -39,6 +39,8 @@ export interface Props {
|
|||
returnToOrigin?: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (props: SaveProps, options: { saveToLibrary: boolean }) => void;
|
||||
|
||||
managed: boolean;
|
||||
}
|
||||
|
||||
export const SaveModal = (props: Props) => {
|
||||
|
@ -57,6 +59,7 @@ export const SaveModal = (props: Props) => {
|
|||
onClose,
|
||||
onSave,
|
||||
returnToOrigin,
|
||||
managed,
|
||||
} = 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
|
||||
|
@ -104,6 +107,14 @@ export const SaveModal = (props: Props) => {
|
|||
})}
|
||||
data-test-subj="lnsApp_saveModalDashboard"
|
||||
getOriginatingPath={getOriginatingPath}
|
||||
mustCopyOnSaveMessage={
|
||||
managed
|
||||
? i18n.translate('xpack.lens.app.mustCopyOnSave', {
|
||||
defaultMessage:
|
||||
'This visualization is managed by Elastic. Changes here must be saved to a new visualization.',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -36,8 +36,22 @@ export type SaveModalContainerProps = {
|
|||
runSave?: (saveProps: SaveProps, options: { saveToLibrary: boolean }) => void;
|
||||
isSaveable?: boolean;
|
||||
getAppNameFromId?: () => string | undefined;
|
||||
lensServices: LensAppServices;
|
||||
lensServices: Pick<
|
||||
LensAppServices,
|
||||
| 'attributeService'
|
||||
| 'savedObjectsTagging'
|
||||
| 'application'
|
||||
| 'dashboardFeatureFlag'
|
||||
| 'notifications'
|
||||
| 'http'
|
||||
| 'chrome'
|
||||
| 'overlays'
|
||||
| 'stateTransfer'
|
||||
| 'savedObjectStore'
|
||||
>;
|
||||
initialContext?: VisualizeFieldContext | VisualizeEditorContext;
|
||||
// is this visualization managed by the system?
|
||||
managed?: boolean;
|
||||
} & ExtraProps;
|
||||
|
||||
export function SaveModalContainer({
|
||||
|
@ -56,6 +70,7 @@ export function SaveModalContainer({
|
|||
lastKnownDoc: initLastKnownDoc,
|
||||
lensServices,
|
||||
initialContext,
|
||||
managed,
|
||||
}: SaveModalContainerProps) {
|
||||
let title = '';
|
||||
let description;
|
||||
|
@ -168,6 +183,7 @@ export function SaveModalContainer({
|
|||
savedObjectId={savedObjectId}
|
||||
returnToOriginSwitchLabel={returnToOriginSwitchLabel}
|
||||
returnToOrigin={redirectToOrigin != null}
|
||||
managed={Boolean(managed)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -204,7 +220,17 @@ export const runSaveLensVisualization = async (
|
|||
switchDatasource?: () => void;
|
||||
savedObjectStore: SavedObjectIndexStore;
|
||||
} & ExtraProps &
|
||||
LensAppServices,
|
||||
Pick<
|
||||
LensAppServices,
|
||||
| 'application'
|
||||
| 'chrome'
|
||||
| 'overlays'
|
||||
| 'notifications'
|
||||
| 'stateTransfer'
|
||||
| 'dashboardFeatureFlag'
|
||||
| 'attributeService'
|
||||
| 'savedObjectsTagging'
|
||||
>,
|
||||
saveProps: SaveProps,
|
||||
options: { saveToLibrary: boolean }
|
||||
): Promise<Partial<LensAppState> | undefined> => {
|
||||
|
|
|
@ -142,6 +142,7 @@ export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>
|
|||
|
||||
export interface LensUnwrapMetaInfo {
|
||||
sharingSavedObjectProps?: SharingSavedObjectProps;
|
||||
managed?: boolean;
|
||||
}
|
||||
|
||||
export interface LensUnwrapResult {
|
||||
|
|
|
@ -71,6 +71,7 @@ export function getLensAttributeService(
|
|||
},
|
||||
metaInfo: {
|
||||
sharingSavedObjectProps,
|
||||
managed: savedObject.managed,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@ Object {
|
|||
"isLinkedToOriginatingApp": false,
|
||||
"isLoading": false,
|
||||
"isSaveable": true,
|
||||
"managed": false,
|
||||
"persistedDoc": Object {
|
||||
"exactMatchDoc": Object {
|
||||
"attributes": Object {
|
||||
|
|
|
@ -25,10 +25,15 @@ export const getPersisted = async ({
|
|||
history,
|
||||
}: {
|
||||
initialInput: LensEmbeddableInput;
|
||||
lensServices: LensAppServices;
|
||||
lensServices: Pick<LensAppServices, 'attributeService' | 'notifications' | 'spaces' | 'http'>;
|
||||
history?: History<unknown>;
|
||||
}): Promise<
|
||||
{ doc: Document; sharingSavedObjectProps: Omit<SharingSavedObjectProps, 'sourceId'> } | undefined
|
||||
| {
|
||||
doc: Document;
|
||||
sharingSavedObjectProps: Omit<SharingSavedObjectProps, 'sourceId'>;
|
||||
managed: boolean;
|
||||
}
|
||||
| undefined
|
||||
> => {
|
||||
const { notifications, spaces, attributeService } = lensServices;
|
||||
let doc: Document;
|
||||
|
@ -44,6 +49,7 @@ export const getPersisted = async ({
|
|||
sharingSavedObjectProps: {
|
||||
outcome: 'exactMatch',
|
||||
},
|
||||
managed: false,
|
||||
};
|
||||
}
|
||||
const { metaInfo, attributes } = result;
|
||||
|
@ -74,6 +80,7 @@ export const getPersisted = async ({
|
|||
aliasTargetId: sharingSavedObjectProps?.aliasTargetId,
|
||||
outcome: sharingSavedObjectProps?.outcome,
|
||||
},
|
||||
managed: Boolean(metaInfo?.managed),
|
||||
};
|
||||
} catch (e) {
|
||||
notifications.toasts.addDanger(
|
||||
|
@ -273,7 +280,7 @@ export function loadInitial(
|
|||
.then(
|
||||
(persisted) => {
|
||||
if (persisted) {
|
||||
const { doc, sharingSavedObjectProps } = persisted;
|
||||
const { doc, sharingSavedObjectProps, managed } = persisted;
|
||||
if (attributeService.inputIsRefType(initialInput)) {
|
||||
lensServices.chrome.recentlyAccessed.add(
|
||||
getFullPath(initialInput.savedObjectId),
|
||||
|
@ -361,6 +368,7 @@ export function loadInitial(
|
|||
),
|
||||
isLoading: false,
|
||||
annotationGroups,
|
||||
managed,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ export const initialState: LensAppState = {
|
|||
indexPatterns: {},
|
||||
},
|
||||
annotationGroups: {},
|
||||
managed: false,
|
||||
};
|
||||
|
||||
export const getPreloadedState = ({
|
||||
|
|
|
@ -36,6 +36,7 @@ export const selectVisualizationState = (state: LensState) => state.lens.visuali
|
|||
export const selectActiveDatasourceId = (state: LensState) => state.lens.activeDatasourceId;
|
||||
export const selectActiveData = (state: LensState) => state.lens.activeData;
|
||||
export const selectDataViews = (state: LensState) => state.lens.dataViews;
|
||||
export const selectIsManaged = (state: LensState) => state.lens.managed;
|
||||
export const selectIsFullscreenDatasource = (state: LensState) =>
|
||||
Boolean(state.lens.isFullscreenDatasource);
|
||||
|
||||
|
|
|
@ -70,6 +70,9 @@ export interface LensAppState extends EditorFrameState {
|
|||
// Dataview/Indexpattern management has moved in here from datasource
|
||||
dataViews: DataViewsState;
|
||||
annotationGroups: AnnotationGroups;
|
||||
|
||||
// Whether the current visualization is managed by the system
|
||||
managed: boolean;
|
||||
}
|
||||
|
||||
export interface LensState {
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
"@kbn/test-eui-helpers",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/text-based-editor",
|
||||
"@kbn/managed-content-badge",
|
||||
"@kbn/sort-predicates"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
|
|
|
@ -1162,7 +1162,6 @@
|
|||
"dashboard.appLeaveConfirmModal.cancelButtonLabel": "Annuler",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "Quitter le tableau de bord sans enregistrer ?",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "Modifications non enregistrées",
|
||||
"dashboard.badge.managed.text": "Géré",
|
||||
"dashboard.badge.managed.tooltip": "Ce tableau de bord est géré par le système. Cloner ce tableau de bord pour effectuer des modifications.",
|
||||
"dashboard.badge.readOnly.text": "Lecture seule",
|
||||
"dashboard.badge.readOnly.tooltip": "Impossible d'enregistrer les tableaux de bord",
|
||||
|
|
|
@ -1176,7 +1176,6 @@
|
|||
"dashboard.appLeaveConfirmModal.cancelButtonLabel": "キャンセル",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "作業を保存せずにダッシュボードから移動しますか?",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "保存されていない変更",
|
||||
"dashboard.badge.managed.text": "管理中",
|
||||
"dashboard.badge.managed.tooltip": "このダッシュボードはシステムで管理されています。変更するには、このダッシュボードを複製してください。",
|
||||
"dashboard.badge.readOnly.text": "読み取り専用",
|
||||
"dashboard.badge.readOnly.tooltip": "ダッシュボードを保存できません",
|
||||
|
|
|
@ -1176,7 +1176,6 @@
|
|||
"dashboard.appLeaveConfirmModal.cancelButtonLabel": "取消",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesSubtitle": "离开有未保存工作的仪表板?",
|
||||
"dashboard.appLeaveConfirmModal.unsavedChangesTitle": "未保存的更改",
|
||||
"dashboard.badge.managed.text": "托管",
|
||||
"dashboard.badge.managed.tooltip": "此仪表板由系统管理。克隆此仪表板以做出更改。",
|
||||
"dashboard.badge.readOnly.text": "只读",
|
||||
"dashboard.badge.readOnly.tooltip": "无法保存仪表板",
|
||||
|
|
17
x-pack/test/functional/apps/managed_content/config.ts
Normal file
17
x-pack/test/functional/apps/managed_content/config.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrConfigProviderContext } from '@kbn/test';
|
||||
|
||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const functionalConfig = await readConfigFile(require.resolve('../../config.base.js'));
|
||||
|
||||
return {
|
||||
...functionalConfig.getAll(),
|
||||
testFiles: [require.resolve('.')],
|
||||
};
|
||||
}
|
12
x-pack/test/functional/apps/managed_content/index.js
Normal file
12
x-pack/test/functional/apps/managed_content/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('managed content', function () {
|
||||
loadTestFile(require.resolve('./managed_content'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['timePicker', 'lens', 'common']);
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
describe('Managed Content', () => {
|
||||
before(async () => {
|
||||
esArchiver.load('x-pack/test/functional/es_archives/logstash_functional');
|
||||
kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/managed_content');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional');
|
||||
kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/managed_content');
|
||||
});
|
||||
|
||||
describe('preventing the user from overwriting managed content', () => {
|
||||
it('lens', async () => {
|
||||
await PageObjects.common.navigateToActualUrl(
|
||||
'lens',
|
||||
'edit/managed-36db-4a3b-a4ba-7a64ab8f130b'
|
||||
);
|
||||
|
||||
await PageObjects.lens.waitForVisualization('xyVisChart');
|
||||
|
||||
await testSubjects.existOrFail('managedContentBadge');
|
||||
await testSubjects.click('lnsApp_saveButton');
|
||||
const saveAsNewCheckbox = await testSubjects.find('saveAsNewCheckbox');
|
||||
expect(await testSubjects.isEuiSwitchChecked(saveAsNewCheckbox)).to.be(true);
|
||||
expect(await saveAsNewCheckbox.getAttribute('disabled')).to.be('true');
|
||||
|
||||
await PageObjects.common.navigateToActualUrl(
|
||||
'lens',
|
||||
'edit/unmanaged-36db-4a3b-a4ba-7a64ab8f130b'
|
||||
);
|
||||
|
||||
await PageObjects.lens.waitForVisualization('xyVisChart');
|
||||
|
||||
await testSubjects.missingOrFail('managedContentBadge');
|
||||
await testSubjects.click('lnsApp_saveButton');
|
||||
await testSubjects.existOrFail('saveAsNewCheckbox');
|
||||
expect(await testSubjects.isEuiSwitchChecked('saveAsNewCheckbox')).to.be(false);
|
||||
expect(await saveAsNewCheckbox.getAttribute('disabled')).to.be(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -5065,6 +5065,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/managed-content-badge@link:packages/kbn-managed-content-badge":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/managed-vscode-config-cli@link:packages/kbn-managed-vscode-config-cli":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue