Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dashboard][Content Editor] Edit title, description, and tags from dashboard listing page #161399

Merged
merged 22 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b6dd4b3
Add content editor to dashboard listing
cqliu1 Jul 6, 2023
c66ff63
Added content editor validator
cqliu1 Jul 6, 2023
2acffbb
Remove unused prop
cqliu1 Jul 7, 2023
2c40659
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jul 6, 2023
c1b63c9
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jul 6, 2023
24af52e
Fix references
cqliu1 Jul 12, 2023
7d85ade
Added content editor functional test
cqliu1 Jul 18, 2023
551d805
Change to iInCircle icon
cqliu1 Jul 20, 2023
02fef98
Add tooltip in read only mode
cqliu1 Jul 20, 2023
37fb3e2
Add dashboard listing test
cqliu1 Jul 20, 2023
6ff19a6
Fix ts errors
cqliu1 Jul 24, 2023
37a6f59
Fix jest test
cqliu1 Jul 25, 2023
a7482ee
Move string to strings file
cqliu1 Jul 26, 2023
d5fb094
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Jul 26, 2023
45c9328
Fix warning string
cqliu1 Jul 27, 2023
77fbc67
Merge branch 'dashboard/save-from-listing-page' of https://github.com…
cqliu1 Jul 27, 2023
07dbbc8
Merge branch 'main' of https://github.com/elastic/kibana into dashboa…
cqliu1 Jul 27, 2023
2aa567e
Merge branch 'main' into dashboard/save-from-listing-page
cqliu1 Jul 31, 2023
8676fcd
Merge branch 'main' into dashboard/save-from-listing-page
cqliu1 Aug 2, 2023
211058a
Merge branch 'main' into dashboard/save-from-listing-page
cqliu1 Aug 7, 2023
a8a24a3
Fix dashboard_listing test
cqliu1 Aug 7, 2023
50df486
Remove exclusive suite
cqliu1 Aug 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const ContentEditorFlyoutContent: FC<Props> = ({
<EuiFlyoutHeader>
<EuiTitle data-test-subj="flyoutTitle">
<h2>
<EuiIcon type="inspect" css={iconCSS} size="l" />
<EuiIcon type="iInCircle" css={iconCSS} size="l" />
<span>{title}</span>
</h2>
</EuiTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
import React from 'react';
import type { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiForm, EuiFormRow, EuiFieldText, EuiTextArea, EuiSpacer } from '@elastic/eui';
import {
EuiForm,
EuiFormRow,
EuiFieldText,
EuiTextArea,
EuiSpacer,
EuiToolTip,
} from '@elastic/eui';

import { ContentEditorFlyoutWarningsCallOut } from './editor_flyout_warnings';
import type { MetadataFormState, Field } from './use_metadata_form';
Expand Down Expand Up @@ -47,6 +54,11 @@ export const MetadataForm: FC<Props> = ({
getWarnings,
} = form;

const readOnlyToolTip = i18n.translate(
'contentManagement.contentEditor.metadataForm.readOnlyToolTip',
{ defaultMessage: 'To edit these details, contact your administrator for access.' }
);

return (
<EuiForm isInvalid={isSubmitted && !isValid} error={getErrors()} data-test-subj="metadataForm">
<ContentEditorFlyoutWarningsCallOut warningMessages={getWarnings()} />
Expand All @@ -59,16 +71,22 @@ export const MetadataForm: FC<Props> = ({
isInvalid={!isFormFieldValid(title)}
fullWidth
>
<EuiFieldText
isInvalid={!isFormFieldValid(title)}
value={title.value}
onChange={(e) => {
setTitle(e.target.value);
}}
fullWidth
data-test-subj="nameInput"
readOnly={isReadonly}
/>
<EuiToolTip
position="top"
content={isReadonly ? readOnlyToolTip : undefined}
display="block"
>
<EuiFieldText
isInvalid={!isFormFieldValid(title)}
value={title.value}
onChange={(e) => {
setTitle(e.target.value);
}}
fullWidth
data-test-subj="nameInput"
readOnly={isReadonly}
/>
</EuiToolTip>
</EuiFormRow>

<EuiSpacer />
Expand All @@ -84,19 +102,25 @@ export const MetadataForm: FC<Props> = ({
isInvalid={!isFormFieldValid(description)}
fullWidth
>
<EuiTextArea
isInvalid={!isFormFieldValid(description)}
value={description.value}
onChange={(e) => {
setDescription(e.target.value);
}}
fullWidth
data-test-subj="descriptionInput"
readOnly={isReadonly}
/>
<EuiToolTip
position="top"
content={isReadonly ? readOnlyToolTip : undefined}
display="block"
>
<EuiTextArea
isInvalid={!isFormFieldValid(description)}
value={description.value}
onChange={(e) => {
setDescription(e.target.value);
}}
fullWidth
data-test-subj="descriptionInput"
readOnly={isReadonly}
/>
</EuiToolTip>
</EuiFormRow>

{TagList && isReadonly && (
{TagList && isReadonly && tagsReferences.length > 0 && (
<>
<EuiSpacer />
<EuiFormRow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
available: (v) => (showEditActionForItem ? showEditActionForItem(v) : true),
enabled: (v) => !(v as unknown as { error: string })?.error,
onClick: editItem,
'data-test-subj': `edit-action`,
});
}

Expand All @@ -575,9 +576,10 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
defaultMessage: 'View details',
}
),
icon: 'inspect',
icon: 'iInCircle',
type: 'icon',
onClick: inspectItem,
'data-test-subj': `inspect-action`,
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-content-management-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ export interface ContentManagementCrudTypes<
/**
* Update item params
*/
UpdateIn: UpdateIn<ContentType, Attributes, UpdateOptions>;
UpdateIn: UpdateIn<ContentType, Partial<Attributes>, UpdateOptions>;
Copy link
Contributor Author

@cqliu1 cqliu1 Jul 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elastic/kibana-data-discovery I believe this is the only line under your ownership that needs review. I made this a Partial type because updates allow for partial saved object attributes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM, but @mattkime can you do the final approval since you're most familiar with this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look in a bit. At first glance this should be fine but I need to refresh my memory. I started out overly strict with the idea we could loosen things if needed.

/**
* Update item result
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function DashboardEditingToolbar() {
<AddFromLibraryButton
onClick={() => dashboard.addFromLibrary()}
size="s"
data-test-subj="dashboardAddPanelButton"
data-test-subj="dashboardAddFromLibraryButton"
/>,
];
if (dashboard.controlGroup) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
import { FormattedRelative, I18nProvider } from '@kbn/i18n-react';
import React, { useMemo } from 'react';

import { TableListView } from '@kbn/content-management-table-list-view';
import {
type TableListViewKibanaDependencies,
TableListViewKibanaProvider,
} from '@kbn/content-management-table-list-view-table';
import { TableListView } from '@kbn/content-management-table-list-view';

import { toMountPoint, useExecutionContext } from '@kbn/kibana-react-plugin/public';

Expand Down Expand Up @@ -41,7 +41,6 @@ export const DashboardListing = ({
http,
chrome: { theme },
savedObjectsTagging,

coreContext: { executionContext },
} = pluginServices.getServices();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ describe('useDashboardListingTable', () => {
setPageDataTestSubject: expect.any(Function),
title: 'Dashboard List',
urlStateEnabled: false,
contentEditor: {
onSave: expect.any(Function),
isReadonly: false,
customValidators: expect.any(Object),
},
};

expect(tableListViewTableProps).toEqual(expectedProps);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { TableListViewTableProps } from '@kbn/content-management-table-list-view-table';
import { ViewMode } from '@kbn/embeddable-plugin/public';

import { OpenContentEditorParams } from '@kbn/content-management-content-editor';
import { i18n } from '@kbn/i18n';
import { DashboardContainerInput } from '../../../common';
import { DashboardListingEmptyPrompt } from '../dashboard_listing_empty_prompt';
import { pluginServices } from '../../services/plugin_services';
import {
Expand Down Expand Up @@ -81,8 +84,13 @@ export const useDashboardListingTable = ({
const {
dashboardSessionStorage,
dashboardCapabilities: { showWriteControls },
dashboardContentManagement: { findDashboards, deleteDashboards },
settings: { uiSettings },
dashboardContentManagement: {
findDashboards,
deleteDashboards,
updateDashboardMeta,
checkForDuplicateDashboardTitle,
},
notifications: { toasts },
} = pluginServices.getServices();

Expand Down Expand Up @@ -110,6 +118,56 @@ export const useDashboardListingTable = ({
goToDashboard();
}, [dashboardSessionStorage, goToDashboard, useSessionStorageIntegration]);

const updateItemMeta = useCallback(
async (props: Pick<DashboardContainerInput, 'id' | 'title' | 'description' | 'tags'>) => {
await updateDashboardMeta(props);

setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges());
},
[dashboardSessionStorage, updateDashboardMeta]
);

const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo(
() => ({
title: [
{
type: 'warning',
fn: async (value: string, id: string) => {
if (id) {
try {
const [dashboard] = await findDashboards.findByIds([id]);
if (dashboard.status === 'error') {
return;
}

const validTitle = await checkForDuplicateDashboardTitle({
title: value,
copyOnSave: false,
lastSavedTitle: dashboard.attributes.title,
isTitleDuplicateConfirmed: false,
});

if (!validTitle) {
throw new Error(
i18n.translate('dashboard.dashboardListingEditErrorTitle.duplicateWarning', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we add this to the strings file src/plugins/dashboard/public/dashboard_listing/_dashboard_listing_strings.ts just to be consistent?

defaultMessage: 'Saving "{value}" creates a duplicate title',
values: {
value,
},
})
);
}
} catch (e) {
return e.message;
}
}
},
},
],
}),
[checkForDuplicateDashboardTitle, findDashboards]
);

const emptyPrompt = useMemo(
() => (
<DashboardListingEmptyPrompt
Expand Down Expand Up @@ -219,6 +277,11 @@ export const useDashboardListingTable = ({

const tableListViewTableProps = useMemo(
() => ({
contentEditor: {
isReadonly: !showWriteControls,
onSave: updateItemMeta,
customValidators: contentEditorValidators,
},
createItem: !showWriteControls ? undefined : createItem,
deleteItems: !showWriteControls ? undefined : deleteItems,
editItem: !showWriteControls ? undefined : editItem,
Expand All @@ -238,6 +301,7 @@ export const useDashboardListingTable = ({
urlStateEnabled,
}),
[
contentEditorValidators,
createItem,
dashboardListingId,
deleteItems,
Expand All @@ -254,6 +318,7 @@ export const useDashboardListingTable = ({
onFetchSuccess,
showWriteControls,
title,
updateItemMeta,
urlStateEnabled,
]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,6 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
},
deleteDashboards: jest.fn(),
checkForDuplicateDashboardTitle: jest.fn(),
updateDashboardMeta: jest.fn(),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
} from './types';
import { loadDashboardState } from './lib/load_dashboard_state';
import { deleteDashboards } from './lib/delete_dashboards';
import { updateDashboardMeta } from './lib/update_dashboard_meta';

export type DashboardContentManagementServiceFactory = KibanaPluginServiceFactory<
DashboardContentManagementService,
Expand Down Expand Up @@ -79,5 +80,7 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
checkForDuplicateDashboardTitle: (props) =>
checkForDuplicateDashboardTitle(props, contentManagement),
deleteDashboards: (ids) => deleteDashboards(ids, contentManagement),
updateDashboardMeta: (props) =>
updateDashboardMeta(props, { contentManagement, savedObjectsTagging, embeddable }),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { SavedObjectError, SavedObjectsFindOptionsReference } from '@kbn/core/public';

import { Reference } from '@kbn/content-management-utils';
import {
DashboardItem,
DashboardCrudTypes,
Expand Down Expand Up @@ -60,7 +61,7 @@ export async function searchDashboards({
}

export type FindDashboardsByIdResponse = { id: string } & (
| { status: 'success'; attributes: DashboardAttributes }
| { status: 'success'; attributes: DashboardAttributes; references: Reference[] }
| { status: 'error'; error: SavedObjectError }
);

Expand All @@ -78,8 +79,8 @@ export async function findDashboardsByIds(

return results.map((result) => {
if (result.item.error) return { status: 'error', error: result.item.error, id: result.item.id };
const { attributes, id } = result.item;
return { id, status: 'success', attributes };
const { attributes, id, references } = result.item;
return { id, status: 'success', attributes, references };
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { DashboardContainerInput } from '../../../../common';
import { DashboardStartDependencies } from '../../../plugin';
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
import { DashboardCrudTypes } from '../../../../common/content_management';
import { findDashboardsByIds } from './find_dashboards';
import { DashboardContentManagementRequiredServices } from '../types';

type UpdateDashboardMetaProps = Pick<
DashboardContainerInput,
'id' | 'title' | 'description' | 'tags'
>;
interface UpdateDashboardMetaDependencies {
contentManagement: DashboardStartDependencies['contentManagement'];
savedObjectsTagging: DashboardContentManagementRequiredServices['savedObjectsTagging'];
embeddable: DashboardContentManagementRequiredServices['embeddable'];
}

export const updateDashboardMeta = async (
{ id, title, description = '', tags }: UpdateDashboardMetaProps,
{ contentManagement, savedObjectsTagging, embeddable }: UpdateDashboardMetaDependencies
) => {
const [dashboard] = await findDashboardsByIds(contentManagement, [id]);
if (dashboard.status === 'error') {
return;
}

const references =
savedObjectsTagging.updateTagsReferences && tags.length
? savedObjectsTagging.updateTagsReferences(dashboard.references, tags)
: dashboard.references;

await contentManagement.client.update<
DashboardCrudTypes['UpdateIn'],
DashboardCrudTypes['UpdateOut']
>({
contentTypeId: DASHBOARD_CONTENT_ID,
id,
data: { title, description },
options: { references },
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export interface DashboardContentManagementService {
loadDashboardState: (props: { id?: string }) => Promise<LoadDashboardReturn>;
saveDashboardState: (props: SaveDashboardProps) => Promise<SaveDashboardReturn>;
checkForDuplicateDashboardTitle: (meta: DashboardDuplicateTitleCheckProps) => Promise<boolean>;
updateDashboardMeta: (
props: Pick<DashboardContainerInput, 'id' | 'title' | 'description' | 'tags'>
) => Promise<void>;
}

/**
Expand Down
Loading