[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:
Drew Tate 2024-01-24 08:06:05 -07:00 committed by GitHub
parent 274c314236
commit 1d4b7df989
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 333 additions and 47 deletions

View file

@ -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
View file

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

View file

@ -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": []
}

View file

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

View file

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

View file

@ -0,0 +1,3 @@
# @kbn/managed-content-badge
Empty package generated by @kbn/generate

View 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,
});

View 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'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-browser",
"id": "@kbn/managed-content-badge",
"owner": "@elastic/kibana-visualizations"
}

View 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"
}

View 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",
]
}

View file

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

View file

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

View file

@ -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/**/*"]
}

View file

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

View file

@ -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."
/>
}
/>

View file

@ -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;
}
/**

View file

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

View file

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

View file

@ -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/**/*",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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> => {

View file

@ -142,6 +142,7 @@ export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>
export interface LensUnwrapMetaInfo {
sharingSavedObjectProps?: SharingSavedObjectProps;
managed?: boolean;
}
export interface LensUnwrapResult {

View file

@ -71,6 +71,7 @@ export function getLensAttributeService(
},
metaInfo: {
sharingSavedObjectProps,
managed: savedObject.managed,
},
};
},

View file

@ -24,6 +24,7 @@ Object {
"isLinkedToOriginatingApp": false,
"isLoading": false,
"isSaveable": true,
"managed": false,
"persistedDoc": Object {
"exactMatchDoc": Object {
"attributes": Object {

View file

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

View file

@ -68,6 +68,7 @@ export const initialState: LensAppState = {
indexPatterns: {},
},
annotationGroups: {},
managed: false,
};
export const getPreloadedState = ({

View file

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

View file

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

View file

@ -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/**/*"]

View file

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

View file

@ -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": "ダッシュボードを保存できません",

View file

@ -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": "无法保存仪表板",

View 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('.')],
};
}

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

View file

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

View file

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