[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:
Devon Thomson 2023-09-21 11:04:23 -04:00 committed by GitHub
parent 4f0a43d145
commit 8bd25152bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 185 additions and 64 deletions

View file

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

View file

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

View file

@ -43,11 +43,13 @@ function savedObjectToItem<Attributes extends object>(
error,
namespaces,
version,
managed,
} = savedObject;
return {
id,
type,
managed,
updatedAt,
createdAt,
attributes: pick(attributes, allowedSavedObjectAttributes),

View file

@ -69,11 +69,13 @@ function savedObjectToItem<Attributes extends object>(
error,
namespaces,
version,
managed,
} = savedObject;
return {
id,
type,
managed,
updatedAt,
createdAt,
attributes: pick(attributes, allowedSavedObjectAttributes),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,6 +40,7 @@ export interface DashboardPublicState {
fullScreenMode?: boolean;
savedQueryId?: string;
lastSavedId?: string;
managed?: boolean;
scrollToPanelId?: string;
highlightPanelId?: string;
}

View file

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

View file

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

View file

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

View file

@ -27,6 +27,7 @@ export type TableListViewApplicationService = DashboardApplicationService & {
};
export interface DashboardSavedObjectUserContent extends UserContentCommonSchema {
managed?: boolean;
attributes: {
title: string;
description?: string;

View file

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

View file

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

View file

@ -64,6 +64,7 @@ type DashboardResolveMeta = DashboardCrudTypes['GetOut']['meta'];
export interface LoadDashboardReturn {
dashboardFound: boolean;
dashboardId?: string;
managed?: boolean;
resolveMeta?: DashboardResolveMeta;
dashboardInput: DashboardContainerInput;
anyMigrationRun?: boolean;

View file

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

View file

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