mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Dashboard] Add Readonly State For Managed Dashboards (#166204)
Adds a new `managed` mode to Dashboards. When a user with write permissions opens a managed Dashboard they will be unable to hit the `edit` button - and will instead be prompted to clone the Dashboard first.
This commit is contained in:
parent
4f0a43d145
commit
8bd25152bb
24 changed files with 185 additions and 64 deletions
|
@ -36,7 +36,7 @@ export type TableListViewProps<T extends UserContentCommonSchema = UserContentCo
|
|||
| 'contentEditor'
|
||||
| 'titleColumnName'
|
||||
| 'withoutPageTemplateWrapper'
|
||||
| 'showEditActionForItem'
|
||||
| 'itemIsEditable'
|
||||
> & {
|
||||
title: string;
|
||||
description?: string;
|
||||
|
@ -73,6 +73,7 @@ export const TableListView = <T extends UserContentCommonSchema>({
|
|||
titleColumnName,
|
||||
additionalRightSideActions,
|
||||
withoutPageTemplateWrapper,
|
||||
itemIsEditable,
|
||||
}: TableListViewProps<T>) => {
|
||||
const PageTemplate = withoutPageTemplateWrapper
|
||||
? (React.Fragment as unknown as typeof KibanaPageTemplate)
|
||||
|
@ -118,6 +119,7 @@ export const TableListView = <T extends UserContentCommonSchema>({
|
|||
id={listingId}
|
||||
contentEditor={contentEditor}
|
||||
titleColumnName={titleColumnName}
|
||||
itemIsEditable={itemIsEditable}
|
||||
withoutPageTemplateWrapper={withoutPageTemplateWrapper}
|
||||
onFetchSuccess={onFetchSuccess}
|
||||
setPageDataTestSubject={setPageDataTestSubject}
|
||||
|
|
|
@ -90,10 +90,12 @@ export interface TableListViewTableProps<
|
|||
* Edit action onClick handler. Edit action not provided when property is not provided
|
||||
*/
|
||||
editItem?(item: T): void;
|
||||
|
||||
/**
|
||||
* Handler to set edit action visiblity per item.
|
||||
* Handler to set edit action visiblity, and content editor readonly state per item. If not provided all non-managed items are considered editable. Note: Items with the managed property set to true will always be non-editable.
|
||||
*/
|
||||
showEditActionForItem?(item: T): boolean;
|
||||
itemIsEditable?(item: T): boolean;
|
||||
|
||||
/**
|
||||
* Name for the column containing the "title" value.
|
||||
*/
|
||||
|
@ -144,6 +146,7 @@ export interface State<T extends UserContentCommonSchema = UserContentCommonSche
|
|||
export interface UserContentCommonSchema {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
managed?: boolean;
|
||||
references: SavedObjectsReference[];
|
||||
type: string;
|
||||
attributes: {
|
||||
|
@ -264,7 +267,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
findItems,
|
||||
createItem,
|
||||
editItem,
|
||||
showEditActionForItem,
|
||||
itemIsEditable,
|
||||
deleteItems,
|
||||
getDetailViewLink,
|
||||
onClickTitle,
|
||||
|
@ -451,6 +454,15 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
items,
|
||||
});
|
||||
|
||||
const isEditable = useCallback(
|
||||
(item: T) => {
|
||||
// If the So is `managed` it is never editable.
|
||||
if (item.managed) return false;
|
||||
return itemIsEditable?.(item) ?? true;
|
||||
},
|
||||
[itemIsEditable]
|
||||
);
|
||||
|
||||
const inspectItem = useCallback(
|
||||
(item: T) => {
|
||||
const tags = getTagIdsFromReferences(item.references).map((_id) => {
|
||||
|
@ -466,6 +478,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
},
|
||||
entityName,
|
||||
...contentEditor,
|
||||
isReadonly: contentEditor.isReadonly || !isEditable(item),
|
||||
onSave:
|
||||
contentEditor.onSave &&
|
||||
(async (args) => {
|
||||
|
@ -476,7 +489,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
}),
|
||||
});
|
||||
},
|
||||
[getTagIdsFromReferences, openContentEditor, entityName, contentEditor, fetchItems]
|
||||
[getTagIdsFromReferences, openContentEditor, entityName, contentEditor, isEditable, fetchItems]
|
||||
);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
|
@ -550,7 +563,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
),
|
||||
icon: 'pencil',
|
||||
type: 'icon',
|
||||
available: (v) => (showEditActionForItem ? showEditActionForItem(v) : true),
|
||||
available: (item) => isEditable(item),
|
||||
enabled: (v) => !(v as unknown as { error: string })?.error,
|
||||
onClick: editItem,
|
||||
'data-test-subj': `edit-action`,
|
||||
|
@ -598,16 +611,16 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
customTableColumn,
|
||||
hasUpdatedAtMetadata,
|
||||
editItem,
|
||||
contentEditor.enabled,
|
||||
listingId,
|
||||
getDetailViewLink,
|
||||
onClickTitle,
|
||||
searchQuery.text,
|
||||
addOrRemoveIncludeTagFilter,
|
||||
addOrRemoveExcludeTagFilter,
|
||||
addOrRemoveIncludeTagFilter,
|
||||
DateFormatterComp,
|
||||
contentEditor,
|
||||
isEditable,
|
||||
inspectItem,
|
||||
showEditActionForItem,
|
||||
]);
|
||||
|
||||
const itemsById = useMemo(() => {
|
||||
|
|
|
@ -43,11 +43,13 @@ function savedObjectToItem<Attributes extends object>(
|
|||
error,
|
||||
namespaces,
|
||||
version,
|
||||
managed,
|
||||
} = savedObject;
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
managed,
|
||||
updatedAt,
|
||||
createdAt,
|
||||
attributes: pick(attributes, allowedSavedObjectAttributes),
|
||||
|
|
|
@ -69,11 +69,13 @@ function savedObjectToItem<Attributes extends object>(
|
|||
error,
|
||||
namespaces,
|
||||
version,
|
||||
managed,
|
||||
} = savedObject;
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
managed,
|
||||
updatedAt,
|
||||
createdAt,
|
||||
attributes: pick(attributes, allowedSavedObjectAttributes),
|
||||
|
|
|
@ -200,6 +200,7 @@ export interface SOWithMetadata<Attributes extends object = object> {
|
|||
statusCode: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
managed?: boolean;
|
||||
attributes: Attributes;
|
||||
references: Reference[];
|
||||
namespaces?: string[];
|
||||
|
|
|
@ -25,6 +25,17 @@ 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.',
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* @param title {string} the current title of the dashboard
|
||||
* @param viewMode {DashboardViewMode} the current mode. If in editing state, prepends 'Editing ' to the title.
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
leaveConfirmStrings,
|
||||
getDashboardBreadcrumb,
|
||||
unsavedChangesBadgeStrings,
|
||||
dashboardManagedBadge,
|
||||
} from '../_dashboard_app_strings';
|
||||
import { UI_SETTINGS } from '../../../common';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
|
@ -67,7 +68,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
navigation: { TopNavMenu },
|
||||
embeddable: { getStateTransfer },
|
||||
initializerContext: { allowByValueEmbeddables },
|
||||
dashboardCapabilities: { saveQuery: showSaveQuery },
|
||||
dashboardCapabilities: { saveQuery: showSaveQuery, showWriteControls },
|
||||
} = pluginServices.getServices();
|
||||
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
|
||||
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
|
||||
|
@ -82,6 +83,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
const fullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
|
||||
const savedQueryId = dashboard.select((state) => state.componentState.savedQueryId);
|
||||
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||
const managed = dashboard.select((state) => state.componentState.managed);
|
||||
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
const query = dashboard.select((state) => state.explicitInput.query);
|
||||
|
@ -237,9 +239,8 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
});
|
||||
|
||||
const badges = useMemo(() => {
|
||||
if (viewMode !== ViewMode.EDIT) return;
|
||||
const allBadges: TopNavMenuProps['badges'] = [];
|
||||
if (hasUnsavedChanges) {
|
||||
if (hasUnsavedChanges && viewMode === ViewMode.EDIT) {
|
||||
allBadges.push({
|
||||
'data-test-subj': 'dashboardUnsavedChangesBadge',
|
||||
badgeText: unsavedChangesBadgeStrings.getUnsavedChangedBadgeText(),
|
||||
|
@ -251,7 +252,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
} as EuiToolTipProps,
|
||||
});
|
||||
}
|
||||
if (hasRunMigrations) {
|
||||
if (hasRunMigrations && viewMode === ViewMode.EDIT) {
|
||||
allBadges.push({
|
||||
'data-test-subj': 'dashboardSaveRecommendedBadge',
|
||||
badgeText: unsavedChangesBadgeStrings.getHasRunMigrationsText(),
|
||||
|
@ -264,8 +265,21 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
} as EuiToolTipProps,
|
||||
});
|
||||
}
|
||||
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,
|
||||
});
|
||||
}
|
||||
return allBadges;
|
||||
}, [hasRunMigrations, hasUnsavedChanges, viewMode]);
|
||||
}, [hasUnsavedChanges, viewMode, hasRunMigrations, showWriteControls, managed]);
|
||||
|
||||
return (
|
||||
<div className="dashboardTopNav">
|
||||
|
|
|
@ -56,6 +56,7 @@ export const useDashboardMenuItems = ({
|
|||
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
|
||||
const dashboardTitle = dashboard.select((state) => state.explicitInput.title);
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
const managed = dashboard.select((state) => state.componentState.managed);
|
||||
const disableTopNav = isSaveInProgress || hasOverlays;
|
||||
|
||||
/**
|
||||
|
@ -265,7 +266,7 @@ export const useDashboardMenuItems = ({
|
|||
const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
|
||||
const shareMenuItem = share ? [menuItems.share] : [];
|
||||
const cloneMenuItem = showWriteControls ? [menuItems.clone] : [];
|
||||
const editMenuItem = showWriteControls ? [menuItems.edit] : [];
|
||||
const editMenuItem = showWriteControls && !managed ? [menuItems.edit] : [];
|
||||
return [
|
||||
...labsMenuItem,
|
||||
menuItems.fullScreen,
|
||||
|
@ -274,7 +275,7 @@ export const useDashboardMenuItems = ({
|
|||
resetChangesMenuItem,
|
||||
...editMenuItem,
|
||||
];
|
||||
}, [menuItems, share, showWriteControls, resetChangesMenuItem, isLabsEnabled]);
|
||||
}, [isLabsEnabled, menuItems, share, showWriteControls, managed, resetChangesMenuItem]);
|
||||
|
||||
const editModeTopNavConfig = useMemo(() => {
|
||||
const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
|
||||
|
|
|
@ -33,10 +33,11 @@ export function runSaveAs(this: DashboardContainer) {
|
|||
|
||||
const {
|
||||
explicitInput: currentState,
|
||||
componentState: { lastSavedId },
|
||||
componentState: { lastSavedId, managed },
|
||||
} = this.getState();
|
||||
|
||||
return new Promise<SaveDashboardReturn | undefined>((resolve) => {
|
||||
if (managed) resolve(undefined);
|
||||
const onSave = async ({
|
||||
newTags,
|
||||
newTitle,
|
||||
|
@ -132,9 +133,11 @@ export async function runQuickSave(this: DashboardContainer) {
|
|||
|
||||
const {
|
||||
explicitInput: currentState,
|
||||
componentState: { lastSavedId },
|
||||
componentState: { lastSavedId, managed },
|
||||
} = this.getState();
|
||||
|
||||
if (managed) return;
|
||||
|
||||
const saveResult = await saveDashboardState({
|
||||
lastSavedId,
|
||||
currentState,
|
||||
|
|
|
@ -94,6 +94,21 @@ test('pulls state from dashboard saved object when given a saved object id', asy
|
|||
expect(dashboard!.getState().explicitInput.description).toBe(`wow would you look at that? Wow.`);
|
||||
});
|
||||
|
||||
test('passes managed state from the saved object into the Dashboard component state', async () => {
|
||||
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
dashboardInput: {
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
description: 'wow this description is okay',
|
||||
},
|
||||
managed: true,
|
||||
});
|
||||
const dashboard = await createDashboard({}, 0, 'what-an-id');
|
||||
expect(dashboard).toBeDefined();
|
||||
expect(dashboard!.getState().componentState.managed).toBe(true);
|
||||
});
|
||||
|
||||
test('pulls state from session storage which overrides state from saved object', async () => {
|
||||
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
|
||||
.fn()
|
||||
|
|
|
@ -29,6 +29,7 @@ import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_
|
|||
import { DEFAULT_DASHBOARD_INPUT, GLOBAL_STATE_STORAGE_KEY } from '../../../dashboard_constants';
|
||||
import { startSyncingDashboardControlGroup } from './controls/dashboard_control_group_integration';
|
||||
import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration';
|
||||
import { DashboardPublicState } from '../../types';
|
||||
|
||||
/**
|
||||
* Builds a new Dashboard from scratch.
|
||||
|
@ -86,16 +87,27 @@ export const createDashboard = async (
|
|||
// --------------------------------------------------------------------------------------
|
||||
// Build and return the dashboard container.
|
||||
// --------------------------------------------------------------------------------------
|
||||
const initialComponentState: DashboardPublicState = {
|
||||
lastSavedInput: savedObjectResult?.dashboardInput ?? {
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
id: input.id,
|
||||
},
|
||||
hasRunClientsideMigrations: savedObjectResult.anyMigrationRun,
|
||||
isEmbeddedExternally: creationOptions?.isEmbeddedExternally,
|
||||
animatePanelTransforms: false, // set panel transforms to false initially to avoid panels animating on initial render.
|
||||
hasUnsavedChanges: false, // if there is initial unsaved changes, the initial diff will catch them.
|
||||
managed: savedObjectResult.managed,
|
||||
lastSavedId: savedObjectId,
|
||||
};
|
||||
|
||||
const dashboardContainer = new DashboardContainer(
|
||||
input,
|
||||
reduxEmbeddablePackage,
|
||||
searchSessionId,
|
||||
savedObjectResult?.dashboardInput,
|
||||
savedObjectResult.anyMigrationRun,
|
||||
dashboardCreationStartTime,
|
||||
undefined,
|
||||
creationOptions,
|
||||
savedObjectId
|
||||
initialComponentState
|
||||
);
|
||||
dashboardContainerReady$.next(dashboardContainer);
|
||||
return dashboardContainer;
|
||||
|
|
|
@ -167,10 +167,15 @@ test('Container view mode change propagates to new children', async () => {
|
|||
|
||||
test('searchSessionId propagates to children', async () => {
|
||||
const searchSessionId1 = 'searchSessionId1';
|
||||
const sampleInput = getSampleDashboardInput();
|
||||
const container = new DashboardContainer(
|
||||
getSampleDashboardInput(),
|
||||
sampleInput,
|
||||
mockedReduxEmbeddablePackage,
|
||||
searchSessionId1
|
||||
searchSessionId1,
|
||||
0,
|
||||
undefined,
|
||||
undefined,
|
||||
{ lastSavedInput: sampleInput }
|
||||
);
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -11,7 +11,6 @@ import { batch } from 'react-redux';
|
|||
import { Subject, Subscription } from 'rxjs';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
||||
import {
|
||||
ViewMode,
|
||||
Container,
|
||||
|
@ -20,6 +19,10 @@ import {
|
|||
type EmbeddableOutput,
|
||||
type EmbeddableFactory,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
getDefaultControlGroupInput,
|
||||
persistableControlGroupInputIsEqual,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { RefreshInterval } from '@kbn/data-plugin/public';
|
||||
import type { Filter, TimeRange, Query } from '@kbn/es-query';
|
||||
|
@ -28,11 +31,8 @@ import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
|||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
|
||||
import {
|
||||
getDefaultControlGroupInput,
|
||||
persistableControlGroupInputIsEqual,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen';
|
||||
import { ReduxToolsPackage, ReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import {
|
||||
runClone,
|
||||
|
@ -44,19 +44,24 @@ import {
|
|||
addOrUpdateEmbeddable,
|
||||
} from './api';
|
||||
|
||||
import {
|
||||
DashboardPublicState,
|
||||
DashboardReduxState,
|
||||
DashboardRenderPerformanceStats,
|
||||
} from '../types';
|
||||
import { DASHBOARD_CONTAINER_TYPE } from '../..';
|
||||
import { createPanelState } from '../component/panel';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { initializeDashboard } from './create/create_dashboard';
|
||||
import { DASHBOARD_LOADED_EVENT } from '../../dashboard_constants';
|
||||
import { DashboardCreationOptions } from './dashboard_container_factory';
|
||||
import { DashboardAnalyticsService } from '../../services/analytics/types';
|
||||
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
|
||||
import { DashboardPanelState, DashboardContainerInput } from '../../../common';
|
||||
import { DashboardReduxState, DashboardRenderPerformanceStats } from '../types';
|
||||
import { dashboardContainerReducers } from '../state/dashboard_container_reducers';
|
||||
import { startDiffingDashboardState } from '../state/diffing/dashboard_diffing_integration';
|
||||
import { DASHBOARD_LOADED_EVENT, DEFAULT_DASHBOARD_INPUT } from '../../dashboard_constants';
|
||||
import { combineDashboardFiltersWithControlGroupFilters } from './create/controls/dashboard_control_group_integration';
|
||||
import { DashboardCapabilitiesService } from '../../services/dashboard_capabilities/types';
|
||||
|
||||
export interface InheritedChildInput {
|
||||
filters: Filter[];
|
||||
|
@ -118,6 +123,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
// Services that are used in the Dashboard container code
|
||||
private creationOptions?: DashboardCreationOptions;
|
||||
private analyticsService: DashboardAnalyticsService;
|
||||
private showWriteControls: DashboardCapabilitiesService['showWriteControls'];
|
||||
private theme$;
|
||||
private chrome;
|
||||
private customBranding;
|
||||
|
@ -126,12 +132,10 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
initialInput: DashboardContainerInput,
|
||||
reduxToolsPackage: ReduxToolsPackage,
|
||||
initialSessionId?: string,
|
||||
initialLastSavedInput?: DashboardContainerInput,
|
||||
anyMigrationRun?: boolean,
|
||||
dashboardCreationStartTime?: number,
|
||||
parent?: Container,
|
||||
creationOptions?: DashboardCreationOptions,
|
||||
savedObjectId?: string
|
||||
initialComponentState?: DashboardPublicState
|
||||
) {
|
||||
const {
|
||||
embeddable: { getEmbeddableFactory },
|
||||
|
@ -153,6 +157,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
},
|
||||
chrome: this.chrome,
|
||||
customBranding: this.customBranding,
|
||||
dashboardCapabilities: { showWriteControls: this.showWriteControls },
|
||||
} = pluginServices.getServices());
|
||||
|
||||
this.creationOptions = creationOptions;
|
||||
|
@ -170,17 +175,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
embeddable: this,
|
||||
reducers: dashboardContainerReducers,
|
||||
additionalMiddleware: [diffingMiddleware],
|
||||
initialComponentState: {
|
||||
lastSavedInput: initialLastSavedInput ?? {
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
id: initialInput.id,
|
||||
},
|
||||
hasRunClientsideMigrations: anyMigrationRun,
|
||||
isEmbeddedExternally: creationOptions?.isEmbeddedExternally,
|
||||
animatePanelTransforms: false, // set panel transforms to false initially to avoid panels animating on initial render.
|
||||
hasUnsavedChanges: false, // if there is initial unsaved changes, the initial diff will catch them.
|
||||
lastSavedId: savedObjectId,
|
||||
},
|
||||
initialComponentState,
|
||||
});
|
||||
|
||||
this.onStateChange = reduxTools.onStateChange;
|
||||
|
@ -248,6 +243,19 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
);
|
||||
}
|
||||
|
||||
public updateInput(changes: Partial<DashboardContainerInput>): void {
|
||||
// block the Dashboard from entering edit mode if this Dashboard is managed.
|
||||
if (
|
||||
(this.getState().componentState.managed || !this.showWriteControls) &&
|
||||
changes.viewMode?.toLowerCase() === ViewMode.EDIT?.toLowerCase()
|
||||
) {
|
||||
const { viewMode, ...rest } = changes;
|
||||
super.updateInput(rest);
|
||||
return;
|
||||
}
|
||||
super.updateInput(changes);
|
||||
}
|
||||
|
||||
protected getInheritedInput(id: string): InheritedChildInput {
|
||||
const {
|
||||
query,
|
||||
|
@ -394,6 +402,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
this.updateInput(newInput);
|
||||
batch(() => {
|
||||
this.dispatch.setLastSavedInput(loadDashboardReturn?.dashboardInput);
|
||||
this.dispatch.setManaged(loadDashboardReturn?.managed);
|
||||
this.dispatch.setAnimatePanelTransforms(false); // prevents panels from animating on navigate.
|
||||
this.dispatch.setLastSavedId(newSavedObjectId);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import {
|
||||
DashboardReduxState,
|
||||
|
@ -89,6 +90,11 @@ export const dashboardContainerReducers = {
|
|||
state: DashboardReduxState,
|
||||
action: PayloadAction<DashboardContainerInput['viewMode']>
|
||||
) => {
|
||||
// Managed Dashboards cannot be put into edit mode.
|
||||
if (state.componentState.managed) {
|
||||
state.explicitInput.viewMode = ViewMode.VIEW;
|
||||
return;
|
||||
}
|
||||
state.explicitInput.viewMode = action.payload;
|
||||
},
|
||||
|
||||
|
@ -103,6 +109,13 @@ export const dashboardContainerReducers = {
|
|||
state.explicitInput.title = action.payload;
|
||||
},
|
||||
|
||||
setManaged: (
|
||||
state: DashboardReduxState,
|
||||
action: PayloadAction<DashboardPublicState['managed']>
|
||||
) => {
|
||||
state.componentState.managed = action.payload;
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Unsaved Changes Reducers
|
||||
// ------------------------------------------------------------------------------
|
||||
|
|
|
@ -40,6 +40,7 @@ export interface DashboardPublicState {
|
|||
fullScreenMode?: boolean;
|
||||
savedQueryId?: string;
|
||||
lastSavedId?: string;
|
||||
managed?: boolean;
|
||||
scrollToPanelId?: string;
|
||||
highlightPanelId?: string;
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ test('when showWriteControls is true, table list view is passed editing function
|
|||
createItem: expect.any(Function),
|
||||
deleteItems: expect.any(Function),
|
||||
editItem: expect.any(Function),
|
||||
itemIsEditable: expect.any(Function),
|
||||
}),
|
||||
expect.any(Object) // react context
|
||||
);
|
||||
|
|
|
@ -149,6 +149,7 @@ describe('useDashboardListingTable', () => {
|
|||
initialPageSize: 5,
|
||||
listingLimit: 20,
|
||||
onFetchSuccess: expect.any(Function),
|
||||
itemIsEditable: expect.any(Function),
|
||||
setPageDataTestSubject: expect.any(Function),
|
||||
title: 'Dashboard List',
|
||||
urlStateEnabled: false,
|
||||
|
|
|
@ -7,27 +7,28 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import type { SavedObjectsFindOptionsReference } from '@kbn/core/public';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import { TableListViewTableProps } from '@kbn/content-management-table-list-view-table';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import type { SavedObjectsFindOptionsReference } from '@kbn/core/public';
|
||||
import { OpenContentEditorParams } from '@kbn/content-management-content-editor';
|
||||
import { DashboardContainerInput } from '../../../common';
|
||||
import { DashboardListingEmptyPrompt } from '../dashboard_listing_empty_prompt';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { TableListViewTableProps } from '@kbn/content-management-table-list-view-table';
|
||||
|
||||
import {
|
||||
DASHBOARD_CONTENT_ID,
|
||||
SAVED_OBJECT_DELETE_TIME,
|
||||
SAVED_OBJECT_LOADED_TIME,
|
||||
} from '../../dashboard_constants';
|
||||
import { DashboardItem } from '../../../common/content_management';
|
||||
import {
|
||||
dashboardListingErrorStrings,
|
||||
dashboardListingTableStrings,
|
||||
} from '../_dashboard_listing_strings';
|
||||
import { confirmCreateWithUnsaved } from '../confirm_overlays';
|
||||
import { DashboardContainerInput } from '../../../common';
|
||||
import { DashboardSavedObjectUserContent } from '../types';
|
||||
import { confirmCreateWithUnsaved } from '../confirm_overlays';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { DashboardItem } from '../../../common/content_management';
|
||||
import { DashboardListingEmptyPrompt } from '../dashboard_listing_empty_prompt';
|
||||
|
||||
type GetDetailViewLink =
|
||||
TableListViewTableProps<DashboardSavedObjectUserContent>['getDetailViewLink'];
|
||||
|
@ -42,6 +43,7 @@ const toTableListViewSavedObject = (hit: DashboardItem): DashboardSavedObjectUse
|
|||
id: hit.id,
|
||||
updatedAt: hit.updatedAt!,
|
||||
references: hit.references,
|
||||
managed: hit.managed,
|
||||
attributes: {
|
||||
title,
|
||||
description,
|
||||
|
@ -50,14 +52,16 @@ const toTableListViewSavedObject = (hit: DashboardItem): DashboardSavedObjectUse
|
|||
};
|
||||
};
|
||||
|
||||
type DashboardListingViewTableProps = Omit<
|
||||
TableListViewTableProps<DashboardSavedObjectUserContent>,
|
||||
'tableCaption'
|
||||
> & { title: string };
|
||||
|
||||
interface UseDashboardListingTableReturnType {
|
||||
hasInitialFetchReturned: boolean;
|
||||
pageDataTestSubject: string | undefined;
|
||||
refreshUnsavedDashboards: () => void;
|
||||
tableListViewTableProps: Omit<
|
||||
TableListViewTableProps<DashboardSavedObjectUserContent>,
|
||||
'tableCaption'
|
||||
> & { title: string };
|
||||
tableListViewTableProps: DashboardListingViewTableProps;
|
||||
unsavedDashboardIds: string[];
|
||||
}
|
||||
|
||||
|
@ -269,7 +273,7 @@ export const useDashboardListingTable = ({
|
|||
[getDashboardUrl]
|
||||
);
|
||||
|
||||
const tableListViewTableProps = useMemo(
|
||||
const tableListViewTableProps: DashboardListingViewTableProps = useMemo(
|
||||
() => ({
|
||||
contentEditor: {
|
||||
isReadonly: !showWriteControls,
|
||||
|
@ -279,6 +283,7 @@ export const useDashboardListingTable = ({
|
|||
createItem: !showWriteControls || !showCreateDashboardButton ? undefined : createItem,
|
||||
deleteItems: !showWriteControls ? undefined : deleteItems,
|
||||
editItem: !showWriteControls ? undefined : editItem,
|
||||
itemIsEditable: () => showWriteControls,
|
||||
emptyPrompt,
|
||||
entityName,
|
||||
entityNamePlural,
|
||||
|
|
|
@ -27,6 +27,7 @@ export type TableListViewApplicationService = DashboardApplicationService & {
|
|||
};
|
||||
|
||||
export interface DashboardSavedObjectUserContent extends UserContentCommonSchema {
|
||||
managed?: boolean;
|
||||
attributes: {
|
||||
title: string;
|
||||
description?: string;
|
||||
|
|
|
@ -68,7 +68,15 @@ export function setupIntersectionObserverMock({
|
|||
|
||||
export function buildMockDashboard(overrides?: Partial<DashboardContainerInput>) {
|
||||
const initialInput = getSampleDashboardInput(overrides);
|
||||
const dashboardContainer = new DashboardContainer(initialInput, mockedReduxEmbeddablePackage);
|
||||
const dashboardContainer = new DashboardContainer(
|
||||
initialInput,
|
||||
mockedReduxEmbeddablePackage,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ lastSavedInput: initialInput }
|
||||
);
|
||||
return dashboardContainer;
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ export const loadDashboardState = async ({
|
|||
/**
|
||||
* Inject saved object references back into the saved object attributes
|
||||
*/
|
||||
const { references, attributes: rawAttributes } = rawDashboardContent;
|
||||
const { references, attributes: rawAttributes, managed } = rawDashboardContent;
|
||||
const attributes = (() => {
|
||||
if (!references || references.length === 0) return rawAttributes;
|
||||
return injectReferences(
|
||||
|
@ -168,6 +168,7 @@ export const loadDashboardState = async ({
|
|||
);
|
||||
|
||||
return {
|
||||
managed,
|
||||
resolveMeta,
|
||||
dashboardInput,
|
||||
anyMigrationRun,
|
||||
|
|
|
@ -64,6 +64,7 @@ type DashboardResolveMeta = DashboardCrudTypes['GetOut']['meta'];
|
|||
export interface LoadDashboardReturn {
|
||||
dashboardFound: boolean;
|
||||
dashboardId?: string;
|
||||
managed?: boolean;
|
||||
resolveMeta?: DashboardResolveMeta;
|
||||
dashboardInput: DashboardContainerInput;
|
||||
anyMigrationRun?: boolean;
|
||||
|
|
|
@ -52,7 +52,7 @@ export function DashboardPicker(props: DashboardPickerProps) {
|
|||
if (objects) {
|
||||
setDashboardOptions(
|
||||
objects
|
||||
.filter((d) => !props.idsToOmit || !props.idsToOmit.includes(d.id))
|
||||
.filter((d) => !d.managed && !(props.idsToOmit ?? []).includes(d.id))
|
||||
.map((d) => ({
|
||||
value: d.id,
|
||||
label: d.attributes.title,
|
||||
|
|
|
@ -93,7 +93,7 @@ type CustomTableViewProps = Pick<
|
|||
| 'editItem'
|
||||
| 'contentEditor'
|
||||
| 'emptyPrompt'
|
||||
| 'showEditActionForItem'
|
||||
| 'itemIsEditable'
|
||||
>;
|
||||
|
||||
const useTableListViewProps = (
|
||||
|
@ -257,8 +257,7 @@ const useTableListViewProps = (
|
|||
editItem,
|
||||
emptyPrompt: noItemsFragment,
|
||||
createItem: createNewVis,
|
||||
showEditActionForItem: ({ attributes: { readOnly } }) =>
|
||||
visualizeCapabilities.save && !readOnly,
|
||||
itemIsEditable: ({ attributes: { readOnly } }) => visualizeCapabilities.save && !readOnly,
|
||||
};
|
||||
|
||||
return props;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue