mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Time to Visualize] Allow By Value Flow Without Visualize Save Permissions (#95951)
* Made sure users can use by value workflow without visualize save permissions
This commit is contained in:
parent
b531d28364
commit
fe17879ae3
26 changed files with 484 additions and 69 deletions
|
@ -41,8 +41,15 @@ const start = doStart();
|
|||
let container: DashboardContainer;
|
||||
let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable;
|
||||
let coreStart: CoreStart;
|
||||
let capabilities: CoreStart['application']['capabilities'];
|
||||
|
||||
beforeEach(async () => {
|
||||
coreStart = coreMock.createStart();
|
||||
capabilities = {
|
||||
...coreStart.application.capabilities,
|
||||
visualize: { save: true },
|
||||
maps: { save: true },
|
||||
};
|
||||
|
||||
const containerOptions = {
|
||||
ExitFullScreenButton: () => null,
|
||||
|
@ -83,7 +90,10 @@ beforeEach(async () => {
|
|||
});
|
||||
|
||||
test('Add to library is incompatible with Error Embeddables', async () => {
|
||||
const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts });
|
||||
const action = new AddToLibraryAction({
|
||||
toasts: coreStart.notifications.toasts,
|
||||
capabilities,
|
||||
});
|
||||
const errorEmbeddable = new ErrorEmbeddable(
|
||||
'Wow what an awful error',
|
||||
{ id: ' 404' },
|
||||
|
@ -92,20 +102,37 @@ test('Add to library is incompatible with Error Embeddables', async () => {
|
|||
expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false);
|
||||
});
|
||||
|
||||
test('Add to library is incompatible on visualize embeddable without visualize save permissions', async () => {
|
||||
const action = new AddToLibraryAction({
|
||||
toasts: coreStart.notifications.toasts,
|
||||
capabilities: { ...capabilities, visualize: { save: false } },
|
||||
});
|
||||
expect(await action.isCompatible({ embeddable })).toBe(false);
|
||||
});
|
||||
|
||||
test('Add to library is compatible when embeddable on dashboard has value type input', async () => {
|
||||
const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts });
|
||||
const action = new AddToLibraryAction({
|
||||
toasts: coreStart.notifications.toasts,
|
||||
capabilities,
|
||||
});
|
||||
embeddable.updateInput(await embeddable.getInputAsValueType());
|
||||
expect(await action.isCompatible({ embeddable })).toBe(true);
|
||||
});
|
||||
|
||||
test('Add to library is not compatible when embeddable input is by reference', async () => {
|
||||
const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts });
|
||||
const action = new AddToLibraryAction({
|
||||
toasts: coreStart.notifications.toasts,
|
||||
capabilities,
|
||||
});
|
||||
embeddable.updateInput(await embeddable.getInputAsRefType());
|
||||
expect(await action.isCompatible({ embeddable })).toBe(false);
|
||||
});
|
||||
|
||||
test('Add to library is not compatible when view mode is set to view', async () => {
|
||||
const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts });
|
||||
const action = new AddToLibraryAction({
|
||||
toasts: coreStart.notifications.toasts,
|
||||
capabilities,
|
||||
});
|
||||
embeddable.updateInput(await embeddable.getInputAsRefType());
|
||||
embeddable.updateInput({ viewMode: ViewMode.VIEW });
|
||||
expect(await action.isCompatible({ embeddable })).toBe(false);
|
||||
|
@ -126,7 +153,10 @@ test('Add to library is not compatible when embeddable is not in a dashboard con
|
|||
mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id },
|
||||
mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id },
|
||||
});
|
||||
const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts });
|
||||
const action = new AddToLibraryAction({
|
||||
toasts: coreStart.notifications.toasts,
|
||||
capabilities,
|
||||
});
|
||||
expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -135,7 +165,10 @@ test('Add to library replaces embeddableId and retains panel count', async () =>
|
|||
const originalPanelCount = Object.keys(dashboard.getInput().panels).length;
|
||||
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
|
||||
|
||||
const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts });
|
||||
const action = new AddToLibraryAction({
|
||||
toasts: coreStart.notifications.toasts,
|
||||
capabilities,
|
||||
});
|
||||
await action.execute({ embeddable });
|
||||
expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount);
|
||||
|
||||
|
@ -161,7 +194,10 @@ test('Add to library returns reference type input', async () => {
|
|||
});
|
||||
const dashboard = embeddable.getRoot() as IContainer;
|
||||
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
|
||||
const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts });
|
||||
const action = new AddToLibraryAction({
|
||||
toasts: coreStart.notifications.toasts,
|
||||
capabilities,
|
||||
});
|
||||
await action.execute({ embeddable });
|
||||
const newPanelId = Object.keys(container.getInput().panels).find(
|
||||
(key) => !originalPanelKeySet.has(key)
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
isReferenceOrValueEmbeddable,
|
||||
isErrorEmbeddable,
|
||||
} from '../../services/embeddable';
|
||||
import { NotificationsStart } from '../../services/core';
|
||||
import { ApplicationStart, NotificationsStart } from '../../services/core';
|
||||
import { dashboardAddToLibraryAction } from '../../dashboard_strings';
|
||||
import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..';
|
||||
|
||||
|
@ -33,7 +33,12 @@ export class AddToLibraryAction implements Action<AddToLibraryActionContext> {
|
|||
public readonly id = ACTION_ADD_TO_LIBRARY;
|
||||
public order = 15;
|
||||
|
||||
constructor(private deps: { toasts: NotificationsStart['toasts'] }) {}
|
||||
constructor(
|
||||
private deps: {
|
||||
toasts: NotificationsStart['toasts'];
|
||||
capabilities: ApplicationStart['capabilities'];
|
||||
}
|
||||
) {}
|
||||
|
||||
public getDisplayName({ embeddable }: AddToLibraryActionContext) {
|
||||
if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
|
||||
|
@ -50,8 +55,15 @@ export class AddToLibraryAction implements Action<AddToLibraryActionContext> {
|
|||
}
|
||||
|
||||
public async isCompatible({ embeddable }: AddToLibraryActionContext) {
|
||||
// TODO: Fix this, potentially by adding a 'canSave' function to embeddable interface
|
||||
const canSave =
|
||||
embeddable.type === 'map'
|
||||
? this.deps.capabilities.maps?.save
|
||||
: this.deps.capabilities.visualize.save;
|
||||
|
||||
return Boolean(
|
||||
!isErrorEmbeddable(embeddable) &&
|
||||
canSave &&
|
||||
!isErrorEmbeddable(embeddable) &&
|
||||
embeddable.getInput()?.viewMode !== ViewMode.VIEW &&
|
||||
embeddable.getRoot() &&
|
||||
embeddable.getRoot().isContainer &&
|
||||
|
|
|
@ -61,7 +61,8 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
|
|||
embeddable.getInput()?.viewMode !== ViewMode.VIEW &&
|
||||
embeddable.getRoot() &&
|
||||
embeddable.getRoot().isContainer &&
|
||||
embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE
|
||||
embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE &&
|
||||
embeddable.getOutput().editable
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -342,7 +342,7 @@ export class DashboardPlugin
|
|||
}
|
||||
|
||||
public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart {
|
||||
const { notifications, overlays } = core;
|
||||
const { notifications, overlays, application } = core;
|
||||
const { uiActions, data, share, presentationUtil, embeddable } = plugins;
|
||||
|
||||
const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings);
|
||||
|
@ -370,7 +370,10 @@ export class DashboardPlugin
|
|||
}
|
||||
|
||||
if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) {
|
||||
const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts });
|
||||
const addToLibraryAction = new AddToLibraryAction({
|
||||
toasts: notifications.toasts,
|
||||
capabilities: application.capabilities,
|
||||
});
|
||||
uiActions.registerAction(addToLibraryAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id);
|
||||
|
||||
|
@ -386,8 +389,8 @@ export class DashboardPlugin
|
|||
overlays,
|
||||
embeddable.getStateTransfer(),
|
||||
{
|
||||
canCreateNew: Boolean(core.application.capabilities.dashboard.createNew),
|
||||
canEditExisting: !Boolean(core.application.capabilities.dashboard.hideWriteControls),
|
||||
canCreateNew: Boolean(application.capabilities.dashboard.createNew),
|
||||
canEditExisting: !Boolean(application.capabilities.dashboard.hideWriteControls),
|
||||
},
|
||||
presentationUtil.ContextProvider
|
||||
);
|
||||
|
|
|
@ -12,4 +12,5 @@ export {
|
|||
PluginInitializerContext,
|
||||
ScopedHistory,
|
||||
NotificationsStart,
|
||||
ApplicationStart,
|
||||
} from '../../../../core/public';
|
||||
|
|
|
@ -28,6 +28,7 @@ interface SaveModalDocumentInfo {
|
|||
|
||||
export interface SaveModalDashboardProps {
|
||||
documentInfo: SaveModalDocumentInfo;
|
||||
canSaveByReference: boolean;
|
||||
objectType: string;
|
||||
onClose: () => void;
|
||||
onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void;
|
||||
|
@ -35,7 +36,7 @@ export interface SaveModalDashboardProps {
|
|||
}
|
||||
|
||||
export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) {
|
||||
const { documentInfo, tagOptions, objectType, onClose } = props;
|
||||
const { documentInfo, tagOptions, objectType, onClose, canSaveByReference } = props;
|
||||
const { id: documentId } = documentInfo;
|
||||
const initialCopyOnSave = !Boolean(documentId);
|
||||
|
||||
|
@ -49,7 +50,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) {
|
|||
documentId || disableDashboardOptions ? null : 'existing'
|
||||
);
|
||||
const [isAddToLibrarySelected, setAddToLibrary] = useState<boolean>(
|
||||
!initialCopyOnSave || disableDashboardOptions
|
||||
canSaveByReference && (!initialCopyOnSave || disableDashboardOptions)
|
||||
);
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>(
|
||||
null
|
||||
|
@ -65,13 +66,16 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) {
|
|||
onChange={(option) => {
|
||||
setDashboardOption(option);
|
||||
}}
|
||||
canSaveByReference={canSaveByReference}
|
||||
{...{ copyOnSave, documentId, dashboardOption, setAddToLibrary, isAddToLibrarySelected }}
|
||||
/>
|
||||
)
|
||||
: null;
|
||||
|
||||
const onCopyOnSaveChange = (newCopyOnSave: boolean) => {
|
||||
setAddToLibrary(true);
|
||||
if (canSaveByReference) {
|
||||
setAddToLibrary(true);
|
||||
}
|
||||
setDashboardOption(null);
|
||||
setCopyOnSave(newCopyOnSave);
|
||||
};
|
||||
|
|
|
@ -33,15 +33,21 @@ export default {
|
|||
control: 'boolean',
|
||||
defaultValue: true,
|
||||
},
|
||||
canSaveVisualizations: {
|
||||
control: 'boolean',
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function Example({
|
||||
copyOnSave,
|
||||
hasDocumentId,
|
||||
canSaveVisualizations,
|
||||
}: {
|
||||
copyOnSave: boolean;
|
||||
hasDocumentId: boolean;
|
||||
canSaveVisualizations: boolean;
|
||||
} & StorybookParams) {
|
||||
const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>('existing');
|
||||
const [isAddToLibrarySelected, setAddToLibrary] = useState(false);
|
||||
|
@ -52,6 +58,7 @@ export function Example({
|
|||
onChange={setDashboardOption}
|
||||
dashboardOption={dashboardOption}
|
||||
copyOnSave={copyOnSave}
|
||||
canSaveByReference={canSaveVisualizations}
|
||||
documentId={hasDocumentId ? 'abc' : undefined}
|
||||
isAddToLibrarySelected={isAddToLibrarySelected}
|
||||
setAddToLibrary={setAddToLibrary}
|
||||
|
|
|
@ -30,6 +30,7 @@ export interface SaveModalDashboardSelectorProps {
|
|||
copyOnSave: boolean;
|
||||
documentId?: string;
|
||||
onSelectDashboard: DashboardPickerProps['onChange'];
|
||||
canSaveByReference: boolean;
|
||||
setAddToLibrary: (selected: boolean) => void;
|
||||
isAddToLibrarySelected: boolean;
|
||||
dashboardOption: 'new' | 'existing' | null;
|
||||
|
@ -40,6 +41,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp
|
|||
const {
|
||||
documentId,
|
||||
onSelectDashboard,
|
||||
canSaveByReference,
|
||||
setAddToLibrary,
|
||||
isAddToLibrarySelected,
|
||||
dashboardOption,
|
||||
|
@ -114,7 +116,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp
|
|||
setAddToLibrary(true);
|
||||
onChange(null);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
disabled={isDisabled || !canSaveByReference}
|
||||
/>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
|
@ -127,7 +129,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp
|
|||
defaultMessage: 'Add to library',
|
||||
})}
|
||||
checked={isAddToLibrarySelected}
|
||||
disabled={dashboardOption === null || isDisabled}
|
||||
disabled={dashboardOption === null || isDisabled || !canSaveByReference}
|
||||
onChange={(event) => setAddToLibrary(event.target.checked)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface PresentationDashboardsService {
|
|||
export interface PresentationCapabilitiesService {
|
||||
canAccessDashboards: () => boolean;
|
||||
canCreateNewDashboards: () => boolean;
|
||||
canSaveVisualizations: () => boolean;
|
||||
}
|
||||
|
||||
export interface PresentationUtilServices {
|
||||
|
|
|
@ -16,10 +16,11 @@ export type CapabilitiesServiceFactory = KibanaPluginServiceFactory<
|
|||
>;
|
||||
|
||||
export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreStart }) => {
|
||||
const { dashboard } = coreStart.application.capabilities;
|
||||
const { dashboard, visualize } = coreStart.application.capabilities;
|
||||
|
||||
return {
|
||||
canAccessDashboards: () => Boolean(dashboard.show),
|
||||
canCreateNewDashboards: () => Boolean(dashboard.createNew),
|
||||
canSaveVisualizations: () => Boolean(visualize.save),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -19,11 +19,13 @@ export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({
|
|||
canAccessDashboards,
|
||||
canCreateNewDashboards,
|
||||
canEditDashboards,
|
||||
canSaveVisualizations,
|
||||
}) => {
|
||||
const check = (value: boolean = true) => value;
|
||||
return {
|
||||
canAccessDashboards: () => check(canAccessDashboards),
|
||||
canCreateNewDashboards: () => check(canCreateNewDashboards),
|
||||
canEditDashboards: () => check(canEditDashboards),
|
||||
canSaveVisualizations: () => check(canSaveVisualizations),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface StorybookParams {
|
|||
canAccessDashboards?: boolean;
|
||||
canCreateNewDashboards?: boolean;
|
||||
canEditDashboards?: boolean;
|
||||
canSaveVisualizations?: boolean;
|
||||
}
|
||||
|
||||
export const providers: PluginServiceProviders<PresentationUtilServices, StorybookParams> = {
|
||||
|
|
|
@ -15,4 +15,5 @@ export const capabilitiesServiceFactory: CapabilitiesServiceFactory = () => ({
|
|||
canAccessDashboards: () => true,
|
||||
canCreateNewDashboards: () => true,
|
||||
canEditDashboards: () => true,
|
||||
canSaveVisualizations: () => true,
|
||||
});
|
||||
|
|
|
@ -67,7 +67,10 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe
|
|||
indexPatterns = [vis.data.indexPattern];
|
||||
}
|
||||
|
||||
const editable = getCapabilities().visualize.save as boolean;
|
||||
const capabilities = {
|
||||
visualizeSave: Boolean(getCapabilities().visualize.save),
|
||||
dashboardSave: Boolean(getCapabilities().dashboard?.showWriteControls),
|
||||
};
|
||||
|
||||
return new VisualizeEmbeddable(
|
||||
getTimeFilter(),
|
||||
|
@ -76,8 +79,8 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe
|
|||
indexPatterns,
|
||||
editPath,
|
||||
editUrl,
|
||||
editable,
|
||||
deps,
|
||||
capabilities,
|
||||
},
|
||||
input,
|
||||
attributeService,
|
||||
|
|
|
@ -50,7 +50,7 @@ export interface VisualizeEmbeddableConfiguration {
|
|||
indexPatterns?: IIndexPattern[];
|
||||
editPath: string;
|
||||
editUrl: string;
|
||||
editable: boolean;
|
||||
capabilities: { visualizeSave: boolean; dashboardSave: boolean };
|
||||
deps: VisualizeEmbeddableFactoryDeps;
|
||||
}
|
||||
|
||||
|
@ -111,7 +111,7 @@ export class VisualizeEmbeddable
|
|||
|
||||
constructor(
|
||||
timefilter: TimefilterContract,
|
||||
{ vis, editPath, editUrl, indexPatterns, editable, deps }: VisualizeEmbeddableConfiguration,
|
||||
{ vis, editPath, editUrl, indexPatterns, deps, capabilities }: VisualizeEmbeddableConfiguration,
|
||||
initialInput: VisualizeInput,
|
||||
attributeService?: AttributeService<
|
||||
VisualizeSavedObjectAttributes,
|
||||
|
@ -129,7 +129,6 @@ export class VisualizeEmbeddable
|
|||
editApp: 'visualize',
|
||||
editUrl,
|
||||
indexPatterns,
|
||||
editable,
|
||||
visTypeName: vis.type.name,
|
||||
},
|
||||
parent
|
||||
|
@ -143,6 +142,12 @@ export class VisualizeEmbeddable
|
|||
this.attributeService = attributeService;
|
||||
this.savedVisualizationsLoader = savedVisualizationsLoader;
|
||||
|
||||
if (this.attributeService) {
|
||||
const isByValue = !this.inputIsRefType(initialInput);
|
||||
const editable = capabilities.visualizeSave || (isByValue && capabilities.dashboardSave);
|
||||
this.updateOutput({ ...this.getOutput(), editable });
|
||||
}
|
||||
|
||||
this.subscriptions.push(
|
||||
this.getUpdated$().subscribe(() => {
|
||||
const isDirty = this.handleChanges();
|
||||
|
|
|
@ -82,6 +82,7 @@ export const getTopNavConfig = (
|
|||
setActiveUrl,
|
||||
toastNotifications,
|
||||
visualizeCapabilities,
|
||||
dashboardCapabilities,
|
||||
i18n: { Context: I18nContext },
|
||||
dashboard,
|
||||
savedObjectsTagging,
|
||||
|
@ -205,9 +206,9 @@ export const getTopNavConfig = (
|
|||
}
|
||||
};
|
||||
|
||||
const allowByValue = dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables;
|
||||
const saveButtonLabel =
|
||||
embeddableId ||
|
||||
(!savedVis.id && dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && originatingApp)
|
||||
embeddableId || (!savedVis.id && allowByValue && originatingApp)
|
||||
? i18n.translate('visualize.topNavMenu.saveVisualizationToLibraryButtonLabel', {
|
||||
defaultMessage: 'Save to library',
|
||||
})
|
||||
|
@ -219,9 +220,11 @@ export const getTopNavConfig = (
|
|||
defaultMessage: 'Save',
|
||||
});
|
||||
|
||||
const showSaveAndReturn =
|
||||
originatingApp &&
|
||||
(savedVis?.id || dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables);
|
||||
const showSaveAndReturn = originatingApp && (savedVis?.id || allowByValue);
|
||||
|
||||
const showSaveButton =
|
||||
visualizeCapabilities.save ||
|
||||
(allowByValue && !showSaveAndReturn && dashboardCapabilities.showWriteControls);
|
||||
|
||||
const topNavMenu: TopNavMenuData[] = [
|
||||
{
|
||||
|
@ -300,7 +303,7 @@ export const getTopNavConfig = (
|
|||
},
|
||||
]
|
||||
: []),
|
||||
...(visualizeCapabilities.save
|
||||
...(showSaveButton
|
||||
? [
|
||||
{
|
||||
id: 'save',
|
||||
|
@ -439,7 +442,12 @@ export const getTopNavConfig = (
|
|||
/>
|
||||
) : (
|
||||
<SavedObjectSaveModalDashboard
|
||||
documentInfo={savedVis || { title: '' }}
|
||||
documentInfo={{
|
||||
id: visualizeCapabilities.save ? savedVis?.id : undefined,
|
||||
title: savedVis?.title || '',
|
||||
description: savedVis?.description || '',
|
||||
}}
|
||||
canSaveByReference={Boolean(visualizeCapabilities.save)}
|
||||
onSave={onSave}
|
||||
tagOptions={tagOptions}
|
||||
objectType={'visualization'}
|
||||
|
@ -455,7 +463,7 @@ export const getTopNavConfig = (
|
|||
},
|
||||
]
|
||||
: []),
|
||||
...(visualizeCapabilities.save && showSaveAndReturn
|
||||
...(showSaveAndReturn
|
||||
? [
|
||||
{
|
||||
id: 'saveAndReturn',
|
||||
|
@ -471,7 +479,7 @@ export const getTopNavConfig = (
|
|||
}
|
||||
),
|
||||
testId: 'visualizesaveAndReturnButton',
|
||||
disableButton: hasUnappliedChanges,
|
||||
disableButton: hasUnappliedChanges || !dashboardCapabilities.showWriteControls,
|
||||
tooltip() {
|
||||
if (hasUnappliedChanges) {
|
||||
return i18n.translate(
|
||||
|
|
|
@ -32,7 +32,7 @@ export const addBadgeToAppChrome = (chrome: ChromeStart) => {
|
|||
defaultMessage: 'Read only',
|
||||
}),
|
||||
tooltip: i18n.translate('visualize.badge.readOnly.tooltip', {
|
||||
defaultMessage: 'Unable to save visualizations',
|
||||
defaultMessage: 'Unable to save visualizations to the library',
|
||||
}),
|
||||
iconType: 'glasses',
|
||||
});
|
||||
|
|
|
@ -531,7 +531,13 @@ export function App({
|
|||
|
||||
const { TopNavMenu } = navigation.ui;
|
||||
|
||||
const savingPermitted = Boolean(state.isSaveable && application.capabilities.visualize.save);
|
||||
const savingToLibraryPermitted = Boolean(
|
||||
state.isSaveable && application.capabilities.visualize.save
|
||||
);
|
||||
const savingToDashboardPermitted = Boolean(
|
||||
state.isSaveable && application.capabilities.dashboard?.showWriteControls
|
||||
);
|
||||
|
||||
const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', {
|
||||
defaultMessage: 'unsaved',
|
||||
});
|
||||
|
@ -545,8 +551,10 @@ export function App({
|
|||
state.isSaveable && state.activeData && Object.keys(state.activeData).length
|
||||
),
|
||||
isByValueMode: getIsByValueMode(),
|
||||
allowByValue: dashboardFeatureFlag.allowByValueEmbeddables,
|
||||
showCancel: Boolean(state.isLinkedToOriginatingApp),
|
||||
savingPermitted,
|
||||
savingToLibraryPermitted,
|
||||
savingToDashboardPermitted,
|
||||
actions: {
|
||||
exportToCSV: () => {
|
||||
if (!state.activeData) {
|
||||
|
@ -577,7 +585,7 @@ export function App({
|
|||
}
|
||||
},
|
||||
saveAndReturn: () => {
|
||||
if (savingPermitted && lastKnownDoc) {
|
||||
if (savingToDashboardPermitted && lastKnownDoc) {
|
||||
// disabling the validation on app leave because the document has been saved.
|
||||
onAppLeave((actions) => {
|
||||
return actions.default();
|
||||
|
@ -597,7 +605,7 @@ export function App({
|
|||
}
|
||||
},
|
||||
showSaveModal: () => {
|
||||
if (savingPermitted) {
|
||||
if (savingToDashboardPermitted || savingToLibraryPermitted) {
|
||||
setState((s) => ({ ...s, isSaveModalVisible: true }));
|
||||
}
|
||||
},
|
||||
|
@ -697,6 +705,7 @@ export function App({
|
|||
<SaveModal
|
||||
isVisible={state.isSaveModalVisible}
|
||||
originatingApp={state.isLinkedToOriginatingApp ? incomingState?.originatingApp : undefined}
|
||||
savingToLibraryPermitted={savingToLibraryPermitted}
|
||||
allowByValueEmbeddables={dashboardFeatureFlag.allowByValueEmbeddables}
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
tagsIds={tagsIds}
|
||||
|
|
|
@ -14,12 +14,29 @@ export function getLensTopNavConfig(options: {
|
|||
enableExportToCSV: boolean;
|
||||
showCancel: boolean;
|
||||
isByValueMode: boolean;
|
||||
allowByValue: boolean;
|
||||
actions: LensTopNavActions;
|
||||
savingPermitted: boolean;
|
||||
savingToLibraryPermitted: boolean;
|
||||
savingToDashboardPermitted: boolean;
|
||||
}): TopNavMenuData[] {
|
||||
const { showSaveAndReturn, showCancel, actions, savingPermitted, enableExportToCSV } = options;
|
||||
const {
|
||||
actions,
|
||||
showCancel,
|
||||
allowByValue,
|
||||
enableExportToCSV,
|
||||
showSaveAndReturn,
|
||||
savingToLibraryPermitted,
|
||||
savingToDashboardPermitted,
|
||||
} = options;
|
||||
const topNavMenu: TopNavMenuData[] = [];
|
||||
|
||||
const enableSaveButton =
|
||||
savingToLibraryPermitted ||
|
||||
(allowByValue &&
|
||||
savingToDashboardPermitted &&
|
||||
!options.isByValueMode &&
|
||||
!options.showSaveAndReturn);
|
||||
|
||||
const saveButtonLabel = options.isByValueMode
|
||||
? i18n.translate('xpack.lens.app.addToLibrary', {
|
||||
defaultMessage: 'Save to library',
|
||||
|
@ -66,7 +83,7 @@ export function getLensTopNavConfig(options: {
|
|||
description: i18n.translate('xpack.lens.app.saveButtonAriaLabel', {
|
||||
defaultMessage: 'Save the current lens visualization',
|
||||
}),
|
||||
disableButton: !savingPermitted,
|
||||
disableButton: !enableSaveButton,
|
||||
});
|
||||
|
||||
if (showSaveAndReturn) {
|
||||
|
@ -78,7 +95,7 @@ export function getLensTopNavConfig(options: {
|
|||
iconType: 'checkInCircleFilled',
|
||||
run: actions.saveAndReturn,
|
||||
testId: 'lnsApp_saveAndReturnButton',
|
||||
disableButton: !savingPermitted,
|
||||
disableButton: !savingToDashboardPermitted,
|
||||
description: i18n.translate('xpack.lens.app.saveAndReturnButtonAriaLabel', {
|
||||
defaultMessage: 'Save the current lens visualization and return to the last app',
|
||||
}),
|
||||
|
|
|
@ -24,6 +24,7 @@ export type SaveProps = OriginSaveProps | DashboardSaveProps;
|
|||
|
||||
export interface Props {
|
||||
isVisible: boolean;
|
||||
savingToLibraryPermitted?: boolean;
|
||||
|
||||
originatingApp?: string;
|
||||
allowByValueEmbeddables: boolean;
|
||||
|
@ -47,6 +48,7 @@ export const SaveModal = (props: Props) => {
|
|||
|
||||
const {
|
||||
originatingApp,
|
||||
savingToLibraryPermitted,
|
||||
savedObjectsTagging,
|
||||
tagsIds,
|
||||
lastKnownDoc,
|
||||
|
@ -85,13 +87,15 @@ export const SaveModal = (props: Props) => {
|
|||
<TagEnhancedSavedObjectSaveModalDashboard
|
||||
savedObjectsTagging={savedObjectsTagging}
|
||||
initialTags={tagsIds}
|
||||
canSaveByReference={Boolean(savingToLibraryPermitted)}
|
||||
onSave={(saveProps) => {
|
||||
const saveToLibrary = Boolean(saveProps.addToLibrary);
|
||||
onSave(saveProps, { saveToLibrary });
|
||||
}}
|
||||
onClose={onClose}
|
||||
documentInfo={{
|
||||
id: lastKnownDoc.savedObjectId,
|
||||
// if the user cannot save to the library - treat this as a new document.
|
||||
id: savingToLibraryPermitted ? lastKnownDoc.savedObjectId : undefined,
|
||||
title: lastKnownDoc.title || '',
|
||||
description: lastKnownDoc.description || '',
|
||||
}}
|
||||
|
|
|
@ -112,7 +112,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -151,7 +154,7 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: { canSaveDashboards: true, canSaveVisualizations: true },
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -191,7 +194,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -231,7 +237,10 @@ describe('embeddable', () => {
|
|||
indexPatternService: ({
|
||||
get: (id: string) => Promise.resolve({ id }),
|
||||
} as unknown) as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -266,7 +275,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -307,7 +319,7 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: { canSaveDashboards: true, canSaveVisualizations: true },
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -352,7 +364,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -395,7 +410,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -445,7 +463,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -495,7 +516,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -544,7 +568,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: ({ get: jest.fn() } as unknown) as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -582,7 +609,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -620,7 +650,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -658,7 +691,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -711,7 +747,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -780,7 +819,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -824,7 +866,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -868,7 +913,10 @@ describe('embeddable', () => {
|
|||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
editable: true,
|
||||
capabilities: {
|
||||
canSaveDashboards: true,
|
||||
canSaveVisualizations: true,
|
||||
},
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
|
|
@ -88,13 +88,13 @@ export interface LensEmbeddableDeps {
|
|||
documentToExpression: (
|
||||
doc: Document
|
||||
) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>;
|
||||
editable: boolean;
|
||||
indexPatternService: IndexPatternsContract;
|
||||
expressionRenderer: ReactExpressionRendererType;
|
||||
timefilter: TimefilterContract;
|
||||
basePath: IBasePath;
|
||||
getTrigger?: UiActionsStart['getTrigger'] | undefined;
|
||||
getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions'];
|
||||
capabilities: { canSaveVisualizations: boolean; canSaveDashboards: boolean };
|
||||
}
|
||||
|
||||
export class Embeddable
|
||||
|
@ -129,7 +129,6 @@ export class Embeddable
|
|||
initialInput,
|
||||
{
|
||||
editApp: 'lens',
|
||||
editable: deps.editable,
|
||||
},
|
||||
parent
|
||||
);
|
||||
|
@ -326,7 +325,7 @@ export class Embeddable
|
|||
hasCompatibleActions={this.hasCompatibleActions}
|
||||
className={input.className}
|
||||
style={input.style}
|
||||
canEdit={this.deps.editable && input.viewMode === 'edit'}
|
||||
canEdit={this.getIsEditable() && input.viewMode === 'edit'}
|
||||
/>,
|
||||
domNode
|
||||
);
|
||||
|
@ -451,6 +450,7 @@ export class Embeddable
|
|||
this.updateOutput({
|
||||
...this.getOutput(),
|
||||
defaultTitle: this.savedVis.title,
|
||||
editable: this.getIsEditable(),
|
||||
title,
|
||||
editPath: getEditPath(savedObjectId),
|
||||
editUrl: this.deps.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`),
|
||||
|
@ -458,6 +458,13 @@ export class Embeddable
|
|||
});
|
||||
}
|
||||
|
||||
private getIsEditable() {
|
||||
return (
|
||||
this.deps.capabilities.canSaveVisualizations ||
|
||||
(!this.inputIsRefType(this.getInput()) && this.deps.capabilities.canSaveDashboards)
|
||||
);
|
||||
}
|
||||
|
||||
public inputIsRefType = (
|
||||
input: LensByValueInput | LensByReferenceInput
|
||||
): input is LensByReferenceInput => {
|
||||
|
|
|
@ -53,7 +53,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
|
||||
public isEditable = async () => {
|
||||
const { capabilities } = await this.getStartServices();
|
||||
return capabilities.visualize.save as boolean;
|
||||
return Boolean(capabilities.visualize.save || capabilities.dashboard?.showWriteControls);
|
||||
};
|
||||
|
||||
canCreateNew() {
|
||||
|
@ -86,6 +86,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
coreHttp,
|
||||
attributeService,
|
||||
indexPatternService,
|
||||
capabilities,
|
||||
} = await this.getStartServices();
|
||||
|
||||
const { Embeddable } = await import('../../async_services');
|
||||
|
@ -96,11 +97,14 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
indexPatternService,
|
||||
timefilter,
|
||||
expressionRenderer,
|
||||
editable: await this.isEditable(),
|
||||
basePath: coreHttp.basePath,
|
||||
getTrigger: uiActions?.getTrigger,
|
||||
getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions,
|
||||
documentToExpression,
|
||||
capabilities: {
|
||||
canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls),
|
||||
canSaveVisualizations: Boolean(capabilities.visualize.save),
|
||||
},
|
||||
},
|
||||
input,
|
||||
parent
|
||||
|
|
|
@ -201,7 +201,11 @@ export function getTopNavConfig({
|
|||
options={tagSelector}
|
||||
/>
|
||||
) : (
|
||||
<SavedObjectSaveModalDashboard {...saveModalProps} tagOptions={tagSelector} />
|
||||
<SavedObjectSaveModalDashboard
|
||||
{...saveModalProps}
|
||||
canSaveByReference={true} // we know here that we have save capabilities.
|
||||
tagOptions={tagSelector}
|
||||
/>
|
||||
);
|
||||
|
||||
showSaveModal(saveModal, getCoreI18n().Context, PresentationUtilContext);
|
||||
|
|
|
@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
describe('feature controls', function () {
|
||||
this.tags(['skipFirefox']);
|
||||
loadTestFile(require.resolve('./dashboard_security'));
|
||||
loadTestFile(require.resolve('./time_to_visualize_security'));
|
||||
loadTestFile(require.resolve('./dashboard_spaces'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* 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([
|
||||
'timeToVisualize',
|
||||
'timePicker',
|
||||
'dashboard',
|
||||
'visEditor',
|
||||
'visualize',
|
||||
'security',
|
||||
'common',
|
||||
'header',
|
||||
'lens',
|
||||
]);
|
||||
|
||||
const dashboardVisualizations = getService('dashboardVisualizations');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const dashboardExpect = getService('dashboardExpect');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const security = getService('security');
|
||||
const find = getService('find');
|
||||
|
||||
describe('dashboard time to visualize security', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('dashboard/feature_controls/security');
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
|
||||
// ensure we're logged out so we can login as the appropriate users
|
||||
await PageObjects.security.forceLogout();
|
||||
|
||||
await security.role.create('dashboard_write_vis_read', {
|
||||
elasticsearch: {
|
||||
indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
dashboard: ['all'],
|
||||
visualize: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await security.user.create('dashboard_write_vis_read_user', {
|
||||
password: 'dashboard_write_vis_read_user-password',
|
||||
roles: ['dashboard_write_vis_read'],
|
||||
full_name: 'test user',
|
||||
});
|
||||
|
||||
await PageObjects.security.login(
|
||||
'dashboard_write_vis_read_user',
|
||||
'dashboard_write_vis_read_user-password',
|
||||
{
|
||||
expectSpaceSelector: false,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await security.role.delete('dashboard_write_vis_read');
|
||||
await security.user.delete('dashboard_write_vis_read_user');
|
||||
|
||||
await esArchiver.unload('dashboard/feature_controls/security');
|
||||
|
||||
// logout, so the other tests don't accidentally run as the custom users we're testing below
|
||||
await PageObjects.security.forceLogout();
|
||||
});
|
||||
|
||||
describe('lens by value works without library save permissions', () => {
|
||||
before(async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.preserveCrossAppState();
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
});
|
||||
|
||||
it('can add a lens panel by value', async () => {
|
||||
await dashboardVisualizations.ensureNewVisualizationDialogIsShowing();
|
||||
await PageObjects.lens.createAndAddLensFromDashboard({});
|
||||
const newPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(newPanelCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('edits to a by value lens panel are properly applied', async () => {
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.lens.switchToVisualization('donut');
|
||||
await PageObjects.lens.saveAndReturn();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
|
||||
const pieExists = await find.existsByCssSelector('.lnsPieExpression__container');
|
||||
expect(pieExists).to.be(true);
|
||||
});
|
||||
|
||||
it('disables save to library button without visualize save permissions', async () => {
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
const saveButton = await testSubjects.find('lnsApp_saveButton');
|
||||
expect(await saveButton.getAttribute('disabled')).to.equal('true');
|
||||
await PageObjects.lens.saveAndReturn();
|
||||
await PageObjects.timeToVisualize.resetNewDashboard();
|
||||
});
|
||||
|
||||
it('should allow new lens to be added by value, but not by reference', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
|
||||
await PageObjects.lens.switchToVisualization('lnsMetric');
|
||||
|
||||
await PageObjects.lens.waitForVisualization();
|
||||
await PageObjects.lens.assertMetric('Average of bytes', '5,727.322');
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.click('lnsApp_saveButton');
|
||||
|
||||
const libraryCheckbox = await find.byCssSelector('#add-to-library-checkbox');
|
||||
expect(await libraryCheckbox.getAttribute('disabled')).to.equal('true');
|
||||
|
||||
await PageObjects.timeToVisualize.saveFromModal('New Lens from Modal', {
|
||||
addToDashboard: 'new',
|
||||
saveAsNew: true,
|
||||
saveToLibrary: false,
|
||||
});
|
||||
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
|
||||
await PageObjects.lens.assertMetric('Average of bytes', '5,727.322');
|
||||
const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists(
|
||||
'New Lens from Modal'
|
||||
);
|
||||
expect(isLinked).to.be(false);
|
||||
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(panelCount).to.eql(1);
|
||||
|
||||
await PageObjects.timeToVisualize.resetNewDashboard();
|
||||
});
|
||||
});
|
||||
|
||||
describe('visualize by value works without library save permissions', () => {
|
||||
const originalMarkdownText = 'Original markdown text';
|
||||
const modifiedMarkdownText = 'Modified markdown text';
|
||||
|
||||
before(async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.preserveCrossAppState();
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
});
|
||||
|
||||
it('can add a markdown panel by value', async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
|
||||
await testSubjects.click('dashboardAddNewPanelButton');
|
||||
await dashboardVisualizations.ensureNewVisualizationDialogIsShowing();
|
||||
await PageObjects.visualize.clickMarkdownWidget();
|
||||
await PageObjects.visEditor.setMarkdownTxt(originalMarkdownText);
|
||||
await PageObjects.visEditor.clickGo();
|
||||
|
||||
await PageObjects.visualize.saveVisualizationAndReturn();
|
||||
const newPanelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(newPanelCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('edits to a by value visualize panel are properly applied', async () => {
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.visEditor.setMarkdownTxt(modifiedMarkdownText);
|
||||
await PageObjects.visEditor.clickGo();
|
||||
await PageObjects.visualize.saveVisualizationAndReturn();
|
||||
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const markdownText = await testSubjects.find('markdownBody');
|
||||
expect(await markdownText.getVisibleText()).to.eql(modifiedMarkdownText);
|
||||
|
||||
const newPanelCount = PageObjects.dashboard.getPanelCount();
|
||||
expect(newPanelCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('disables save to library button without visualize save permissions', async () => {
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.missingOrFail('visualizeSaveButton');
|
||||
await PageObjects.visualize.saveVisualizationAndReturn();
|
||||
await PageObjects.timeToVisualize.resetNewDashboard();
|
||||
});
|
||||
|
||||
it('should allow new visualization to be added by value, but not by reference', async function () {
|
||||
await PageObjects.visualize.navigateToNewAggBasedVisualization();
|
||||
await PageObjects.visualize.clickMetric();
|
||||
await PageObjects.visualize.clickNewSearch();
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
|
||||
await testSubjects.click('visualizeSaveButton');
|
||||
|
||||
await PageObjects.visualize.ensureSavePanelOpen();
|
||||
const libraryCheckbox = await find.byCssSelector('#add-to-library-checkbox');
|
||||
expect(await libraryCheckbox.getAttribute('disabled')).to.equal('true');
|
||||
|
||||
await PageObjects.timeToVisualize.saveFromModal('My New Vis 1', {
|
||||
addToDashboard: 'new',
|
||||
});
|
||||
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await dashboardExpect.metricValuesExist(['14,005']);
|
||||
const panelCount = await PageObjects.dashboard.getPanelCount();
|
||||
expect(panelCount).to.eql(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue