[TableListView] Group title description and tags (#140947) (#141618)

(cherry picked from commit fa13a46dae)

Co-authored-by: Sébastien Loix <sebastien.loix@elastic.co>
This commit is contained in:
Kibana Machine 2022-09-23 07:26:43 -06:00 committed by GitHub
parent d9ad51dc8c
commit f5df323f7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 351 additions and 170 deletions

View file

@ -9,6 +9,7 @@ import React from 'react';
import type { ComponentType } from 'react';
import { from } from 'rxjs';
import { TagList } from '../mocks';
import { TableListViewProvider, Services } from '../services';
export const getMockServices = (overrides?: Partial<Services>) => {
@ -18,6 +19,8 @@ export const getMockServices = (overrides?: Partial<Services>) => {
notifyError: () => undefined,
currentAppId$: from('mockedApp'),
navigateToUrl: () => undefined,
TagList,
itemHasTags: () => true,
...overrides,
};

View file

@ -10,3 +10,4 @@ export { Table } from './table';
export { UpdatedAtField } from './updated_at_field';
export { ConfirmDeleteModal } from './confirm_delete_modal';
export { ListingLimitWarning } from './listing_limit_warning';
export { ItemDetails } from './item_details';

View file

@ -0,0 +1,114 @@
/*
* 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 React, { useCallback, useMemo } from 'react';
import { EuiText, EuiLink, EuiTitle, EuiSpacer } from '@elastic/eui';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { useServices } from '../services';
import type { UserContentCommonSchema, Props as TableListViewProps } from '../table_list_view';
type InheritedProps<T extends UserContentCommonSchema> = Pick<
TableListViewProps<T>,
'onClickTitle' | 'getDetailViewLink' | 'id'
>;
interface Props<T extends UserContentCommonSchema> extends InheritedProps<T> {
item: T;
searchTerm?: string;
}
/**
* Copied from https://stackoverflow.com/a/9310752
*/
// const escapeRegExp = (text: string) => {
// return text.replace(/[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
// };
export function ItemDetails<T extends UserContentCommonSchema>({
id,
item,
searchTerm = '',
getDetailViewLink,
onClickTitle,
}: Props<T>) {
const {
references,
attributes: { title, description },
} = item;
const { navigateToUrl, currentAppId$, TagList, itemHasTags } = useServices();
const redirectAppLinksCoreStart = useMemo(
() => ({
application: {
navigateToUrl,
currentAppId$,
},
}),
[currentAppId$, navigateToUrl]
);
const onClickTitleHandler = useMemo(() => {
if (!onClickTitle) {
return undefined;
}
return ((e) => {
e.preventDefault();
onClickTitle(item);
}) as React.MouseEventHandler<HTMLAnchorElement>;
}, [item, onClickTitle]);
const renderTitle = useCallback(() => {
const href = getDetailViewLink ? getDetailViewLink(item) : undefined;
if (!href && !onClickTitle) {
// This item is not clickable
return <span>{title}</span>;
}
return (
<RedirectAppLinks coreStart={redirectAppLinksCoreStart}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiLink
href={getDetailViewLink ? getDetailViewLink(item) : undefined}
onClick={onClickTitleHandler}
data-test-subj={`${id}ListingTitleLink-${item.attributes.title.split(' ').join('-')}`}
>
{title}
</EuiLink>
</RedirectAppLinks>
);
}, [
getDetailViewLink,
id,
item,
onClickTitle,
onClickTitleHandler,
redirectAppLinksCoreStart,
title,
]);
const hasTags = itemHasTags(references);
return (
<div>
<EuiTitle size="xs">{renderTitle()}</EuiTitle>
{Boolean(description) && (
<EuiText size="s">
<p>{description!}</p>
</EuiText>
)}
{hasTags && (
<>
<EuiSpacer size="s" />
<TagList references={references} />
</>
)}
</div>
);
}

View file

@ -5,7 +5,9 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { from } from 'rxjs';
import { EuiBadgeGroup, EuiBadge } from '@elastic/eui';
import { Services } from './services';
@ -15,6 +17,43 @@ import { Services } from './services';
export type Params = Record<keyof ReturnType<typeof getStoryArgTypes>, any>;
type ActionFn = (name: string) => any;
const tags = [
{
name: 'elastic',
color: '#8dc4de',
description: 'elastic tag',
},
{
name: 'cloud',
color: '#f5ed14',
description: 'cloud tag',
},
];
export const TagList: Services['TagList'] = ({ onClick }) => {
return (
<EuiBadgeGroup>
{tags.map((tag) => (
<EuiBadge
key={tag.name}
onClick={() => {
if (onClick) {
onClick(tag);
}
}}
onClickAriaLabel="tag button"
iconOnClick={() => undefined}
iconOnClickAriaLabel=""
color={tag.color}
title={tag.description}
>
{tag.name}
</EuiBadge>
))}
</EuiBadgeGroup>
);
};
/**
* Returns Storybook-compatible service abstractions for the `NoDataCard` Provider.
*/
@ -27,6 +66,8 @@ export const getStoryServices = (params: Params, action: ActionFn = () => {}) =>
},
currentAppId$: from('mockedApp'),
navigateToUrl: () => undefined,
TagList,
itemHasTags: () => true,
...params,
};

View file

@ -5,54 +5,12 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { sortBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { UpdatedAtField } from './components';
import type { State, UserContentCommonSchema } from './table_list_view';
import type { Action } from './actions';
import type { Services } from './services';
interface Dependencies {
DateFormatterComp: Services['DateFormatterComp'];
}
function onInitialItemsFetch<T extends UserContentCommonSchema>(
items: T[],
{ DateFormatterComp }: Dependencies
) {
// We check if the saved object have the "updatedAt" metadata
// to render or not that column in the table
const hasUpdatedAtMetadata = Boolean(items.find((item) => Boolean(item.updatedAt)));
if (hasUpdatedAtMetadata) {
// Add "Last update" column and sort by that column initially
return {
tableSort: {
field: 'updatedAt' as keyof T,
direction: 'desc' as const,
},
tableColumns: [
{
field: 'updatedAt',
name: i18n.translate('contentManagement.tableList.lastUpdatedColumnTitle', {
defaultMessage: 'Last updated',
}),
render: (field: string, record: { updatedAt?: string }) => (
<UpdatedAtField dateTime={record.updatedAt} DateFormatterComp={DateFormatterComp} />
),
sortable: true,
width: '150px',
},
],
};
}
return {};
}
export function getReducer<T extends UserContentCommonSchema>({ DateFormatterComp }: Dependencies) {
export function getReducer<T extends UserContentCommonSchema>() {
return (state: State<T>, action: Action<T>): State<T> => {
switch (action.type) {
case 'onFetchItems': {
@ -63,11 +21,20 @@ export function getReducer<T extends UserContentCommonSchema>({ DateFormatterCom
}
case 'onFetchItemsSuccess': {
const items = action.data.response.hits;
// We only get the state on the initial fetch of items
// After that we don't want to reset the columns or change the sort after fetching
const { tableColumns, tableSort } = state.hasInitialFetchReturned
? { tableColumns: undefined, tableSort: undefined }
: onInitialItemsFetch(items, { DateFormatterComp });
let tableSort;
let hasUpdatedAtMetadata = state.hasUpdatedAtMetadata;
if (!state.hasInitialFetchReturned) {
// We only get the state on the initial fetch of items
// After that we don't want to reset the columns or change the sort after fetching
hasUpdatedAtMetadata = Boolean(items.find((item) => Boolean(item.updatedAt)));
if (hasUpdatedAtMetadata) {
tableSort = {
field: 'updatedAt' as keyof T,
direction: 'desc' as const,
};
}
}
return {
...state,
@ -75,9 +42,7 @@ export function getReducer<T extends UserContentCommonSchema>({ DateFormatterCom
isFetchingItems: false,
items: !state.searchQuery ? sortBy<T>(items, 'title') : items,
totalItems: action.data.response.total,
tableColumns: tableColumns
? [...state.tableColumns, ...tableColumns]
: state.tableColumns,
hasUpdatedAtMetadata,
tableSort: tableSort ?? state.tableSort,
pagination: {
...state.pagination,

View file

@ -6,14 +6,12 @@
* Side Public License, v 1.
*/
import React, { FC, useContext, useMemo } from 'react';
import type { EuiTableFieldDataColumnType, SearchFilterConfig } from '@elastic/eui';
import React, { FC, useContext, useMemo, useCallback } from 'react';
import type { SearchFilterConfig } from '@elastic/eui';
import type { Observable } from 'rxjs';
import type { FormattedRelative } from '@kbn/i18n-react';
import { RedirectAppLinksKibanaProvider } from '@kbn/shared-ux-link-redirect-app';
import { UserContentCommonSchema } from './table_list_view';
type UnmountCallback = () => void;
type MountPoint = (element: HTMLElement) => UnmountCallback;
type NotifyFn = (title: JSX.Element, text?: string) => void;
@ -44,9 +42,11 @@ export interface Services {
searchQuery: string;
references?: SavedObjectsFindOptionsReference[];
};
getTagsColumnDefinition?: () => EuiTableFieldDataColumnType<UserContentCommonSchema> | undefined;
getSearchBarFilters?: () => SearchFilterConfig[];
DateFormatterComp?: DateFormatter;
TagList: FC<{ references: SavedObjectsReference[]; onClick?: (tag: { name: string }) => void }>;
/** Predicate function to indicate if the saved object references include tags */
itemHasTags: (references: SavedObjectsReference[]) => boolean;
}
const TableListViewContext = React.createContext<Services | null>(null);
@ -101,7 +101,14 @@ export interface TableListViewKibanaDependencies {
*/
savedObjectsTagging?: {
ui: {
getTableColumnDefinition: () => EuiTableFieldDataColumnType<UserContentCommonSchema>;
components: {
TagList: React.FC<{
object: {
references: SavedObjectsReference[];
};
onClick?: (tag: { name: string; description: string; color: string }) => void;
}>;
};
parseSearchQuery: (
query: string,
options?: {
@ -117,6 +124,7 @@ export interface TableListViewKibanaDependencies {
useName?: boolean;
tagField?: string;
}) => SearchFilterConfig;
getTagIdsFromReferences: (references: SavedObjectsReference[]) => string[];
};
};
/** The <FormattedRelative /> component from the @kbn/i18n-react package */
@ -150,6 +158,29 @@ export const TableListViewKibanaProvider: FC<TableListViewKibanaDependencies> =
}
}, [savedObjectsTagging]);
const TagList = useMemo(() => {
const Comp: Services['TagList'] = ({ references, onClick }) => {
if (!savedObjectsTagging?.ui.components.TagList) {
return null;
}
const PluginTagList = savedObjectsTagging.ui.components.TagList;
return <PluginTagList object={{ references }} onClick={onClick} />;
};
return Comp;
}, [savedObjectsTagging?.ui.components.TagList]);
const itemHasTags = useCallback(
(references: SavedObjectsReference[]) => {
if (!savedObjectsTagging?.ui.getTagIdsFromReferences) {
return false;
}
return savedObjectsTagging.ui.getTagIdsFromReferences(references).length > 0;
},
[savedObjectsTagging?.ui]
);
return (
<RedirectAppLinksKibanaProvider coreStart={core}>
<TableListViewProvider
@ -162,12 +193,13 @@ export const TableListViewKibanaProvider: FC<TableListViewKibanaDependencies> =
notifyError={(title, text) => {
core.notifications.toasts.addDanger({ title: toMountPoint(title), text });
}}
getTagsColumnDefinition={savedObjectsTagging?.ui.getTableColumnDefinition}
getSearchBarFilters={getSearchBarFilters}
searchQueryParser={searchQueryParser}
DateFormatterComp={(props) => <FormattedRelative {...props} />}
currentAppId$={core.application.currentAppId$}
navigateToUrl={core.application.navigateToUrl}
TagList={TagList}
itemHasTags={itemHasTags}
>
{children}
</TableListViewProvider>

View file

@ -148,8 +148,8 @@ describe('TableListView', () => {
const { tableCellsValues } = table.getMetaData('itemsInMemTable');
expect(tableCellsValues).toEqual([
['Item 2', 'Item 2 description', yesterdayToString], // Comes first as it is the latest updated
['Item 1', 'Item 1 description', twoDaysAgoToString],
['Item 2Item 2 descriptionelasticcloud', yesterdayToString], // Comes first as it is the latest updated
['Item 1Item 1 descriptionelasticcloud', twoDaysAgoToString],
]);
});
@ -185,8 +185,8 @@ describe('TableListView', () => {
expect(tableCellsValues).toEqual([
// Renders the datetime with this format: "July 28, 2022"
['Item 1', 'Item 1 description', updatedAtValues[0].format('LL')],
['Item 2', 'Item 2 description', updatedAtValues[1].format('LL')],
['Item 1Item 1 descriptionelasticcloud', updatedAtValues[0].format('LL')],
['Item 2Item 2 descriptionelasticcloud', updatedAtValues[1].format('LL')],
]);
});
@ -209,8 +209,8 @@ describe('TableListView', () => {
const { tableCellsValues } = table.getMetaData('itemsInMemTable');
expect(tableCellsValues).toEqual([
['Item 1', 'Item 1 description'], // Sorted by title
['Item 2', 'Item 2 description'],
['Item 1Item 1 descriptionelasticcloud'], // Sorted by title
['Item 2Item 2 descriptionelasticcloud'],
]);
});
@ -235,9 +235,9 @@ describe('TableListView', () => {
const { tableCellsValues } = table.getMetaData('itemsInMemTable');
expect(tableCellsValues).toEqual([
['Item 2', 'Item 2 description', yesterdayToString],
['Item 1', 'Item 1 description', twoDaysAgoToString],
['Item 3', 'Item 3 description', '-'], // Empty column as no updatedAt provided
['Item 2Item 2 descriptionelasticcloud', yesterdayToString],
['Item 1Item 1 descriptionelasticcloud', twoDaysAgoToString],
['Item 3Item 3 descriptionelasticcloud', '-'], // Empty column as no updatedAt provided
]);
});
});
@ -273,8 +273,8 @@ describe('TableListView', () => {
const [[firstRowTitle]] = tableCellsValues;
const [lastRowTitle] = tableCellsValues[tableCellsValues.length - 1];
expect(firstRowTitle).toBe('Item 00');
expect(lastRowTitle).toBe('Item 19');
expect(firstRowTitle).toBe('Item 00elasticcloud');
expect(lastRowTitle).toBe('Item 19elasticcloud');
});
test('should navigate to page 2', async () => {
@ -302,8 +302,8 @@ describe('TableListView', () => {
const [[firstRowTitle]] = tableCellsValues;
const [lastRowTitle] = tableCellsValues[tableCellsValues.length - 1];
expect(firstRowTitle).toBe('Item 20');
expect(lastRowTitle).toBe('Item 29');
expect(firstRowTitle).toBe('Item 20elasticcloud');
expect(lastRowTitle).toBe('Item 29elasticcloud');
});
});
});

View file

@ -6,15 +6,7 @@
* Side Public License, v 1.
*/
import React, {
useReducer,
useCallback,
useEffect,
useRef,
useMemo,
ReactNode,
MouseEvent,
} from 'react';
import React, { useReducer, useCallback, useEffect, useRef, useMemo, ReactNode } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import {
EuiBasicTableColumn,
@ -25,16 +17,20 @@ import {
Direction,
EuiSpacer,
EuiTableActionsColumnType,
EuiLink,
} from '@elastic/eui';
import { keyBy, uniq, get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { IHttpFetchError } from '@kbn/core-http-browser';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { Table, ConfirmDeleteModal, ListingLimitWarning } from './components';
import {
Table,
ConfirmDeleteModal,
ListingLimitWarning,
ItemDetails,
UpdatedAtField,
} from './components';
import { useServices } from './services';
import type { SavedObjectsReference, SavedObjectsFindOptionsReference } from './services';
import type { Action } from './actions';
@ -81,7 +77,7 @@ export interface State<T extends UserContentCommonSchema = UserContentCommonSche
searchQuery: string;
selectedIds: string[];
totalItems: number;
tableColumns: Array<EuiBasicTableColumn<T>>;
hasUpdatedAtMetadata: boolean;
pagination: Pagination;
tableSort?: {
field: keyof T;
@ -137,27 +133,14 @@ function TableListViewComp<T extends UserContentCommonSchema>({
const {
canEditAdvancedSettings,
getListingLimitSettingsUrl,
getTagsColumnDefinition,
searchQueryParser,
notifyError,
DateFormatterComp,
navigateToUrl,
currentAppId$,
} = useServices();
const reducer = useMemo(() => {
return getReducer<T>({ DateFormatterComp });
}, [DateFormatterComp]);
const redirectAppLinksCoreStart = useMemo(
() => ({
application: {
navigateToUrl,
currentAppId$,
},
}),
[navigateToUrl, currentAppId$]
);
return getReducer<T>();
}, []);
const [state, dispatch] = useReducer<(state: State<T>, action: Action<T>) => State<T>>(reducer, {
items: [],
@ -166,53 +149,8 @@ function TableListViewComp<T extends UserContentCommonSchema>({
isFetchingItems: false,
isDeletingItems: false,
showDeleteModal: false,
hasUpdatedAtMetadata: false,
selectedIds: [],
tableColumns: [
{
field: 'attributes.title',
name: i18n.translate('contentManagement.tableList.titleColumnName', {
defaultMessage: 'Title',
}),
sortable: true,
render: (field: keyof T, record: T) => {
// The validation is handled at the top of the component
const href = getDetailViewLink ? getDetailViewLink(record) : undefined;
if (!href && !onClickTitle) {
// This item is not clickable
return <span>{record.attributes.title}</span>;
}
return (
<RedirectAppLinks coreStart={redirectAppLinksCoreStart}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiLink
href={getDetailViewLink ? getDetailViewLink(record) : undefined}
onClick={
onClickTitle
? (e: MouseEvent) => {
e.preventDefault();
onClickTitle(record);
}
: undefined
}
data-test-subj={`${id}ListingTitleLink-${record.attributes.title
.split(' ')
.join('-')}`}
>
{record.attributes.title}
</EuiLink>
</RedirectAppLinks>
);
},
},
{
field: 'attributes.description',
name: i18n.translate('contentManagement.tableList.descriptionColumnName', {
defaultMessage: 'Description',
}),
},
],
searchQuery: initialQuery,
pagination: {
pageIndex: 0,
@ -232,7 +170,7 @@ function TableListViewComp<T extends UserContentCommonSchema>({
isDeletingItems,
selectedIds,
totalItems,
tableColumns: stateTableColumns,
hasUpdatedAtMetadata,
pagination,
tableSort,
} = state;
@ -242,15 +180,43 @@ function TableListViewComp<T extends UserContentCommonSchema>({
const showLimitError = !showFetchError && totalItems > listingLimit;
const tableColumns = useMemo(() => {
const columns = stateTableColumns.slice();
const columns: Array<EuiBasicTableColumn<T>> = [
{
field: 'attributes.title',
name: i18n.translate('contentManagement.tableList.mainColumnName', {
defaultMessage: 'Name, description, tags',
}),
sortable: true,
render: (field: keyof T, record: T) => {
return (
<ItemDetails<T>
id={id}
item={record}
getDetailViewLink={getDetailViewLink}
onClickTitle={onClickTitle}
searchTerm={searchQuery}
/>
);
},
},
];
if (customTableColumn) {
columns.push(customTableColumn);
}
const tagsColumnDef = getTagsColumnDefinition ? getTagsColumnDefinition() : undefined;
if (tagsColumnDef) {
columns.push(tagsColumnDef);
if (hasUpdatedAtMetadata) {
columns.push({
field: 'updatedAt',
name: i18n.translate('contentManagement.tableList.lastUpdatedColumnTitle', {
defaultMessage: 'Last updated',
}),
render: (field: string, record: { updatedAt?: string }) => (
<UpdatedAtField dateTime={record.updatedAt} DateFormatterComp={DateFormatterComp} />
),
sortable: true,
width: '150px',
});
}
// Add "Actions" column
@ -288,7 +254,16 @@ function TableListViewComp<T extends UserContentCommonSchema>({
}
return columns;
}, [stateTableColumns, customTableColumn, getTagsColumnDefinition, editItem]);
}, [
customTableColumn,
hasUpdatedAtMetadata,
editItem,
id,
getDetailViewLink,
onClickTitle,
searchQuery,
DateFormatterComp,
]);
const itemsById = useMemo(() => {
return keyBy(items, 'id');

View file

@ -57,7 +57,12 @@ function mountWith({
}}
savedObjectsTagging={
{
ui: { ...savedObjectsTagging },
ui: {
...savedObjectsTagging,
components: {
TagList: () => null,
},
},
} as unknown as TableListViewKibanaDependencies['savedObjectsTagging']
}
FormattedRelative={FormattedRelative}

View file

@ -16,4 +16,5 @@ export interface DashboardSavedObjectsTaggingService {
getTableColumnDefinition?: SavedObjectsTaggingApi['ui']['getTableColumnDefinition'];
hasTagDecoration?: SavedObjectsTaggingApi['ui']['hasTagDecoration'];
parseSearchQuery?: SavedObjectsTaggingApi['ui']['parseSearchQuery'];
getTagIdsFromReferences?: SavedObjectsTaggingApi['ui']['getTagIdsFromReferences'];
}

View file

@ -6,4 +6,10 @@
* Side Public License, v 1.
*/
export type { Tag, TagAttributes, GetAllTagsOptions, ITagsClient } from './types';
export type {
Tag,
TagAttributes,
GetAllTagsOptions,
ITagsClient,
TagWithOptionalId,
} from './types';

View file

@ -19,6 +19,8 @@ export interface TagAttributes {
color: string;
}
export type TagWithOptionalId = Omit<Tag, 'id'> & { id?: string };
export interface GetAllTagsOptions {
asSystemRequest?: boolean;
}

View file

@ -13,7 +13,7 @@ import { SavedObject, SavedObjectReference } from '@kbn/core/types';
import { SavedObjectsFindOptionsReference } from '@kbn/core/public';
import { SavedObject as SavedObjectClass } from '@kbn/saved-objects-plugin/public';
import { TagDecoratedSavedObject } from './decorator';
import { ITagsClient, Tag } from '../common';
import { ITagsClient, Tag, TagWithOptionalId } from '../common';
/**
* @public
@ -217,7 +217,11 @@ export interface TagListComponentProps {
/**
* The object to display tags for.
*/
object: SavedObject;
object: { references: SavedObject['references'] };
/**
* Handler to execute when clicking on a tag
*/
onClick?: (tag: TagWithOptionalId) => void;
}
/**

View file

@ -73,6 +73,7 @@ export const getCustomColumn = () => {
defaultMessage: 'Type',
}),
sortable: true,
width: '150px',
render: (field: string, record: VisualizationListItem) =>
!record.error ? (
<span>

View file

@ -8,7 +8,14 @@
export type { TagsCapabilities } from './capabilities';
export { getTagsCapabilities } from './capabilities';
export { tagFeatureId, tagSavedObjectTypeName, tagManagementSectionId } from './constants';
export type { TagWithRelations, TagAttributes, Tag, ITagsClient, TagSavedObject } from './types';
export type {
TagWithRelations,
TagAttributes,
Tag,
ITagsClient,
TagSavedObject,
TagWithOptionalId,
} from './types';
export type { TagValidation } from './validation';
export {
validateTagColor,

View file

@ -21,6 +21,7 @@ export type TagWithRelations = Tag & {
export type {
Tag,
TagAttributes,
TagWithOptionalId,
GetAllTagsOptions,
ITagsClient,
} from '@kbn/saved-objects-tagging-oss-plugin/common';

View file

@ -7,18 +7,36 @@
import React, { FC } from 'react';
import { EuiBadge } from '@elastic/eui';
import { Tag, TagAttributes } from '../../../common/types';
import { i18n } from '@kbn/i18n';
import { TagWithOptionalId } from '../../../common/types';
export interface TagBadgeProps {
tag: Tag | TagAttributes;
tag: TagWithOptionalId;
onClick?: (tag: TagWithOptionalId) => void;
}
/**
* The badge representation of a Tag, which is the default display to be used for them.
*/
export const TagBadge: FC<TagBadgeProps> = ({ tag }) => {
export const TagBadge: FC<TagBadgeProps> = ({ tag, onClick }) => {
const onClickProps = onClick
? {
onClick: () => {
onClick!(tag);
},
onClickAriaLabel: i18n.translate('xpack.savedObjectsTagging.tagList.tagBadge.buttonLabel', {
defaultMessage: '{tagName} tag button.',
values: {
tagName: tag.name,
},
}),
iconOnClick: () => undefined,
iconOnClickAriaLabel: '',
}
: {};
return (
<EuiBadge color={tag.color} title={tag.description}>
<EuiBadge color={tag.color} title={tag.description} {...onClickProps}>
{tag.name}
</EuiBadge>
);

View file

@ -7,21 +7,22 @@
import React, { FC } from 'react';
import { EuiBadgeGroup } from '@elastic/eui';
import { Tag, TagAttributes } from '../../../common/types';
import { TagWithOptionalId } from '../../../common/types';
import { TagBadge } from './tag_badge';
export interface TagListProps {
tags: Array<Tag | TagAttributes>;
tags: TagWithOptionalId[];
onClick?: (tag: TagWithOptionalId) => void;
}
/**
* Displays a list of tag
*/
export const TagList: FC<TagListProps> = ({ tags }) => {
export const TagList: FC<TagListProps> = ({ tags, onClick }) => {
return (
<EuiBadgeGroup>
{tags.map((tag) => (
<TagBadge key={tag.name} tag={tag} />
<TagBadge key={tag.name} tag={tag} onClick={onClick} />
))}
</EuiBadgeGroup>
);

View file

@ -7,27 +7,28 @@
import React, { FC, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { SavedObject } from '@kbn/core/types';
import { SavedObjectReference } from '@kbn/core/types';
import { TagListComponentProps } from '@kbn/saved-objects-tagging-oss-plugin/public';
import { Tag } from '../../../common/types';
import { Tag, TagWithOptionalId } from '../../../common/types';
import { getObjectTags } from '../../utils';
import { TagList } from '../base';
import { ITagsCache } from '../../services';
import { byNameTagSorter } from '../../utils';
interface SavedObjectTagListProps {
object: SavedObject;
object: { references: SavedObjectReference[] };
tags: Tag[];
onClick?: (tag: TagWithOptionalId) => void;
}
const SavedObjectTagList: FC<SavedObjectTagListProps> = ({ object, tags: allTags }) => {
const SavedObjectTagList: FC<SavedObjectTagListProps> = ({ object, tags: allTags, onClick }) => {
const objectTags = useMemo(() => {
const { tags } = getObjectTags(object, allTags);
tags.sort(byNameTagSorter);
return tags;
}, [object, allTags]);
return <TagList tags={objectTags} />;
return <TagList tags={objectTags} onClick={onClick} />;
};
interface GetConnectedTagListOptions {

View file

@ -16,7 +16,10 @@ export {
replaceTagReferences as updateTagsReferences,
} from '../common/references';
export const getObjectTags = (object: SavedObject, allTags: Tag[]) => {
export const getObjectTags = (
object: { references: SavedObject['references'] },
allTags: Tag[]
) => {
return getTagsFromReferences(object.references, allTags);
};