mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
(cherry picked from commit fa13a46dae
)
Co-authored-by: Sébastien Loix <sebastien.loix@elastic.co>
This commit is contained in:
parent
d9ad51dc8c
commit
f5df323f7b
20 changed files with 351 additions and 170 deletions
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -57,7 +57,12 @@ function mountWith({
|
|||
}}
|
||||
savedObjectsTagging={
|
||||
{
|
||||
ui: { ...savedObjectsTagging },
|
||||
ui: {
|
||||
...savedObjectsTagging,
|
||||
components: {
|
||||
TagList: () => null,
|
||||
},
|
||||
},
|
||||
} as unknown as TableListViewKibanaDependencies['savedObjectsTagging']
|
||||
}
|
||||
FormattedRelative={FormattedRelative}
|
||||
|
|
|
@ -16,4 +16,5 @@ export interface DashboardSavedObjectsTaggingService {
|
|||
getTableColumnDefinition?: SavedObjectsTaggingApi['ui']['getTableColumnDefinition'];
|
||||
hasTagDecoration?: SavedObjectsTaggingApi['ui']['hasTagDecoration'];
|
||||
parseSearchQuery?: SavedObjectsTaggingApi['ui']['parseSearchQuery'];
|
||||
getTagIdsFromReferences?: SavedObjectsTaggingApi['ui']['getTagIdsFromReferences'];
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -19,6 +19,8 @@ export interface TagAttributes {
|
|||
color: string;
|
||||
}
|
||||
|
||||
export type TagWithOptionalId = Omit<Tag, 'id'> & { id?: string };
|
||||
|
||||
export interface GetAllTagsOptions {
|
||||
asSystemRequest?: boolean;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -73,6 +73,7 @@ export const getCustomColumn = () => {
|
|||
defaultMessage: 'Type',
|
||||
}),
|
||||
sortable: true,
|
||||
width: '150px',
|
||||
render: (field: string, record: VisualizationListItem) =>
|
||||
!record.error ? (
|
||||
<span>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -21,6 +21,7 @@ export type TagWithRelations = Tag & {
|
|||
export type {
|
||||
Tag,
|
||||
TagAttributes,
|
||||
TagWithOptionalId,
|
||||
GetAllTagsOptions,
|
||||
ITagsClient,
|
||||
} from '@kbn/saved-objects-tagging-oss-plugin/common';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue