mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Add dashboard metadata to the info flyout (#185941)
## Summary Close https://github.com/elastic/kibana-team/issues/898 - Show createdAt, createdBy, updatedAt, updatedBy in info flyout. Add a bit of special handling for managed objects and when info is not available. - I had to extract some components into a separate package to use them in contentEditor package - tiny tweaks to column width and "no creator" state  **Unknown creator:**  **For managed objects:**  **Just created, no updates yet** 
This commit is contained in:
parent
dde8ef93bb
commit
e2a98cf965
39 changed files with 715 additions and 141 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -101,6 +101,7 @@ packages/content-management/tabbed_table_list_view @elastic/appex-sharedux
|
|||
packages/content-management/table_list_view @elastic/appex-sharedux
|
||||
packages/content-management/table_list_view_common @elastic/appex-sharedux
|
||||
packages/content-management/table_list_view_table @elastic/appex-sharedux
|
||||
packages/content-management/user_profiles @elastic/appex-sharedux
|
||||
packages/kbn-content-management-utils @elastic/kibana-data-discovery
|
||||
examples/controls_example @elastic/kibana-presentation
|
||||
src/plugins/controls @elastic/kibana-presentation
|
||||
|
|
|
@ -217,6 +217,7 @@
|
|||
"@kbn/content-management-table-list-view": "link:packages/content-management/table_list_view",
|
||||
"@kbn/content-management-table-list-view-common": "link:packages/content-management/table_list_view_common",
|
||||
"@kbn/content-management-table-list-view-table": "link:packages/content-management/table_list_view_table",
|
||||
"@kbn/content-management-user-profiles": "link:packages/content-management/user_profiles",
|
||||
"@kbn/content-management-utils": "link:packages/kbn-content-management-utils",
|
||||
"@kbn/controls-example-plugin": "link:examples/controls_example",
|
||||
"@kbn/controls-plugin": "link:src/plugins/controls",
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { UserProfilesProvider } from '@kbn/content-management-user-profiles';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
import { ActivityView as ActivityViewComponent, ActivityViewProps } from './activity_view';
|
||||
|
||||
const mockGetUserProfile = jest.fn(async (uid: string) => ({
|
||||
uid,
|
||||
enabled: true,
|
||||
data: {},
|
||||
user: { username: uid, full_name: uid.toLocaleUpperCase() },
|
||||
}));
|
||||
|
||||
const ActivityView = (props: ActivityViewProps) => {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<UserProfilesProvider bulkGetUserProfiles={jest.fn()} getUserProfile={mockGetUserProfile}>
|
||||
<ActivityViewComponent {...props} />
|
||||
</UserProfilesProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
test('should render activity view', () => {
|
||||
render(<ActivityView item={{}} />);
|
||||
|
||||
expect(screen.getByTestId('activityView')).toBeVisible();
|
||||
|
||||
expect(screen.getByTestId('createdByCard')).toHaveTextContent(/Unknown/);
|
||||
expect(() => screen.getByTestId('updateByCard')).toThrow();
|
||||
});
|
||||
|
||||
test('should render creator card', async () => {
|
||||
render(<ActivityView item={{ createdBy: 'john', createdAt: '2024-06-13T12:55:46.825Z' }} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const createdByCard = screen.getByTestId('createdByCard');
|
||||
expect(createdByCard).toHaveTextContent(/JOHN/);
|
||||
expect(createdByCard).toHaveTextContent(/June 13/);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not render updater card when updatedAt matches createdAt', async () => {
|
||||
render(
|
||||
<ActivityView
|
||||
item={{
|
||||
createdBy: 'john',
|
||||
updatedBy: 'john',
|
||||
createdAt: '2024-06-13T12:55:46.825Z',
|
||||
updatedAt: '2024-06-13T12:55:46.825Z',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('createdByCard')).toBeVisible();
|
||||
expect(() => screen.getByTestId('updateByCard')).toThrow();
|
||||
});
|
||||
|
||||
test('should render updater card', async () => {
|
||||
render(
|
||||
<ActivityView
|
||||
item={{
|
||||
createdBy: 'john',
|
||||
updatedBy: 'pete',
|
||||
createdAt: '2024-06-13T12:55:46.825Z',
|
||||
updatedAt: '2024-06-14T12:55:46.825Z',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const createdByCard = screen.getByTestId('createdByCard');
|
||||
expect(createdByCard).toHaveTextContent(/JOHN/);
|
||||
expect(createdByCard).toHaveTextContent(/June 13/);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const updatedByCard = screen.getByTestId('updatedByCard');
|
||||
expect(updatedByCard).toHaveTextContent(/PETE/);
|
||||
expect(updatedByCard).toHaveTextContent(/June 14/);
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle managed objects', async () => {
|
||||
render(
|
||||
<ActivityView
|
||||
item={{
|
||||
managed: true,
|
||||
createdAt: '2024-06-13T12:55:46.825Z',
|
||||
updatedAt: '2024-06-14T12:55:46.825Z',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const createdByCard = screen.getByTestId('createdByCard');
|
||||
expect(createdByCard).toHaveTextContent(/System/);
|
||||
expect(createdByCard).toHaveTextContent(/June 13/);
|
||||
});
|
||||
|
||||
const updatedByCard = screen.getByTestId('updatedByCard');
|
||||
expect(updatedByCard).toHaveTextContent(/System/);
|
||||
expect(updatedByCard).toHaveTextContent(/June 14/);
|
||||
});
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import {
|
||||
UserAvatarTip,
|
||||
useUserProfile,
|
||||
NoUpdaterTip,
|
||||
NoCreatorTip,
|
||||
ManagedAvatarTip,
|
||||
} from '@kbn/content-management-user-profiles';
|
||||
import { getUserDisplayName } from '@kbn/user-profile-components';
|
||||
|
||||
import { Item } from '../types';
|
||||
|
||||
export interface ActivityViewProps {
|
||||
item: Pick<Item, 'createdBy' | 'createdAt' | 'updatedBy' | 'updatedAt' | 'managed'>;
|
||||
}
|
||||
|
||||
export const ActivityView = ({ item }: ActivityViewProps) => {
|
||||
const showLastUpdated = Boolean(item.updatedAt && item.updatedAt !== item.createdAt);
|
||||
|
||||
const UnknownUserLabel = (
|
||||
<FormattedMessage
|
||||
id="contentManagement.contentEditor.activity.unkownUserLabel"
|
||||
defaultMessage="Unknown"
|
||||
/>
|
||||
);
|
||||
|
||||
const ManagedUserLabel = (
|
||||
<>
|
||||
<ManagedAvatarTip />{' '}
|
||||
<FormattedMessage
|
||||
id="contentManagement.contentEditor.activity.managedUserLabel"
|
||||
defaultMessage="System"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="contentManagement.contentEditor.metadataForm.activityLabel"
|
||||
defaultMessage="Activity"
|
||||
/>{' '}
|
||||
<EuiIconTip
|
||||
type={'iInCircle'}
|
||||
iconProps={{ style: { verticalAlign: 'bottom' } }}
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="contentManagement.contentEditor.activity.activityLabelHelpText"
|
||||
defaultMessage="Activity data is auto-generated and cannot be updated."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
fullWidth
|
||||
data-test-subj={'activityView'}
|
||||
>
|
||||
<>
|
||||
<EuiFlexGroup gutterSize={'s'}>
|
||||
<EuiFlexItem grow={1} css={{ flexBasis: '50%', minWidth: 0 }}>
|
||||
<ActivityCard
|
||||
what={i18n.translate('contentManagement.contentEditor.activity.createdByLabelText', {
|
||||
defaultMessage: 'Created by',
|
||||
})}
|
||||
who={
|
||||
item.createdBy ? (
|
||||
<UserLabel uid={item.createdBy} />
|
||||
) : item.managed ? (
|
||||
<>{ManagedUserLabel}</>
|
||||
) : (
|
||||
<>
|
||||
{UnknownUserLabel}
|
||||
<NoCreatorTip />
|
||||
</>
|
||||
)
|
||||
}
|
||||
when={item.createdAt}
|
||||
data-test-subj={'createdByCard'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1} css={{ flexBasis: '50%', minWidth: 0 }}>
|
||||
{showLastUpdated && (
|
||||
<ActivityCard
|
||||
what={i18n.translate(
|
||||
'contentManagement.contentEditor.activity.lastUpdatedByLabelText',
|
||||
{ defaultMessage: 'Last updated by' }
|
||||
)}
|
||||
who={
|
||||
item.updatedBy ? (
|
||||
<UserLabel uid={item.updatedBy} />
|
||||
) : item.managed ? (
|
||||
<>{ManagedUserLabel}</>
|
||||
) : (
|
||||
<>
|
||||
{UnknownUserLabel}
|
||||
<NoUpdaterTip />
|
||||
</>
|
||||
)
|
||||
}
|
||||
when={item.updatedAt}
|
||||
data-test-subj={'updatedByCard'}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat(i18n.getLocale(), {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
|
||||
const ActivityCard = ({
|
||||
what,
|
||||
when,
|
||||
who,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}: {
|
||||
what: string;
|
||||
who: React.ReactNode;
|
||||
when?: string;
|
||||
'data-test-subj'?: string;
|
||||
}) => {
|
||||
return (
|
||||
<EuiPanel hasBorder paddingSize={'s'} data-test-subj={dataTestSubj}>
|
||||
<EuiText size={'s'}>
|
||||
<b>{what}</b>
|
||||
</EuiText>
|
||||
<EuiSpacer size={'xs'} />
|
||||
<EuiText size={'s'} className={'eui-textTruncate'}>
|
||||
{who}
|
||||
</EuiText>
|
||||
{when && (
|
||||
<>
|
||||
<EuiSpacer size={'xs'} />
|
||||
<EuiText title={when} color={'subdued'} size={'s'}>
|
||||
<FormattedMessage
|
||||
id="contentManagement.contentEditor.activity.lastUpdatedByDateTime"
|
||||
defaultMessage="on {dateTime}"
|
||||
values={{
|
||||
dateTime: dateFormatter.format(new Date(when)),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
const UserLabel = ({ uid }: { uid: string }) => {
|
||||
const userQuery = useUserProfile(uid);
|
||||
|
||||
if (!userQuery.data) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserAvatarTip uid={uid} /> {getUserDisplayName(userQuery.data.user)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -28,6 +28,7 @@ import type { Item } from '../types';
|
|||
import { MetadataForm } from './metadata_form';
|
||||
import { useMetadataForm } from './use_metadata_form';
|
||||
import type { CustomValidators } from './use_metadata_form';
|
||||
import { ActivityView } from './activity_view';
|
||||
|
||||
const getI18nTexts = ({ entityName }: { entityName: string }) => ({
|
||||
saveButtonLabel: i18n.translate('contentManagement.contentEditor.saveButtonLabel', {
|
||||
|
@ -55,6 +56,7 @@ export interface Props {
|
|||
}) => Promise<void>;
|
||||
customValidators?: CustomValidators;
|
||||
onCancel: () => void;
|
||||
showActivityView?: boolean;
|
||||
}
|
||||
|
||||
const capitalize = (str: string) => `${str.charAt(0).toLocaleUpperCase()}${str.substring(1)}`;
|
||||
|
@ -68,6 +70,7 @@ export const ContentEditorFlyoutContent: FC<Props> = ({
|
|||
onSave,
|
||||
onCancel,
|
||||
customValidators,
|
||||
showActivityView,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
@ -147,7 +150,9 @@ export const ContentEditorFlyoutContent: FC<Props> = ({
|
|||
tagsReferences={item.tags}
|
||||
TagList={TagList}
|
||||
TagSelector={TagSelector}
|
||||
/>
|
||||
>
|
||||
{showActivityView && <ActivityView item={item} />}
|
||||
</MetadataForm>
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
|
|
|
@ -21,6 +21,7 @@ type CommonProps = Pick<
|
|||
| 'onCancel'
|
||||
| 'entityName'
|
||||
| 'customValidators'
|
||||
| 'showActivityView'
|
||||
>;
|
||||
|
||||
export type Props = CommonProps;
|
||||
|
|
|
@ -262,5 +262,22 @@ describe('<ContentEditorFlyoutContent />', () => {
|
|||
tags: ['id-3', 'id-4'], // New selection
|
||||
});
|
||||
});
|
||||
|
||||
test('should render activity view', async () => {
|
||||
await act(async () => {
|
||||
testBed = await setup({ showActivityView: true });
|
||||
});
|
||||
const { find, component } = testBed!;
|
||||
|
||||
expect(find('activityView').exists()).toBe(true);
|
||||
expect(find('activityView.createdByCard').exists()).toBe(true);
|
||||
expect(find('activityView.updatedByCard').exists()).toBe(false);
|
||||
|
||||
testBed.setProps({
|
||||
item: { ...savedObjectItem, updatedAt: '2021-01-01T00:00:00Z' },
|
||||
});
|
||||
component.update();
|
||||
expect(find('activityView.updatedByCard').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,20 +6,20 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiTextArea,
|
||||
EuiSpacer,
|
||||
EuiTextArea,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ContentEditorFlyoutWarningsCallOut } from './editor_flyout_warnings';
|
||||
import type { MetadataFormState, Field } from './use_metadata_form';
|
||||
import type { Field, MetadataFormState } from './use_metadata_form';
|
||||
import type { SavedObjectsReference, Services } from '../services';
|
||||
|
||||
interface Props {
|
||||
|
@ -42,6 +42,7 @@ export const MetadataForm: FC<Props> = ({
|
|||
TagSelector,
|
||||
isReadonly,
|
||||
readonlyReason,
|
||||
children,
|
||||
}) => {
|
||||
const {
|
||||
title,
|
||||
|
@ -137,6 +138,13 @@ export const MetadataForm: FC<Props> = ({
|
|||
<TagSelector initialSelection={tags.value} onTagsSelected={setTags} fullWidth />
|
||||
</>
|
||||
)}
|
||||
|
||||
{children && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,7 +15,13 @@ import type { ContentEditorFlyoutContentContainerProps } from './components';
|
|||
|
||||
export type OpenContentEditorParams = Pick<
|
||||
ContentEditorFlyoutContentContainerProps,
|
||||
'item' | 'onSave' | 'isReadonly' | 'readonlyReason' | 'entityName' | 'customValidators'
|
||||
| 'item'
|
||||
| 'onSave'
|
||||
| 'isReadonly'
|
||||
| 'readonlyReason'
|
||||
| 'entityName'
|
||||
| 'customValidators'
|
||||
| 'showActivityView'
|
||||
>;
|
||||
|
||||
export function useOpenContentEditor() {
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
|
||||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import {
|
||||
UserProfilesProvider,
|
||||
useUserProfilesServices,
|
||||
} from '@kbn/content-management-user-profiles';
|
||||
|
||||
import type { EuiComboBoxProps } from '@elastic/eui';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
|
@ -130,11 +134,19 @@ export const ContentEditorKibanaProvider: FC<
|
|||
return Comp;
|
||||
}, [savedObjectsTagging?.ui.components.TagList]);
|
||||
|
||||
const userProfilesServices = useUserProfilesServices();
|
||||
|
||||
const openFlyout = useCallback(
|
||||
(node: ReactNode, options: OverlayFlyoutOpenOptions) => {
|
||||
return coreOpenFlyout(toMountPoint(node, startServices), options);
|
||||
return coreOpenFlyout(
|
||||
toMountPoint(
|
||||
<UserProfilesProvider {...userProfilesServices}>{node}</UserProfilesProvider>,
|
||||
startServices
|
||||
),
|
||||
options
|
||||
);
|
||||
},
|
||||
[coreOpenFlyout, startServices]
|
||||
[coreOpenFlyout, startServices, userProfilesServices]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -13,4 +13,11 @@ export interface Item {
|
|||
title: string;
|
||||
description?: string;
|
||||
tags: SavedObjectsReference[];
|
||||
|
||||
createdAt?: string;
|
||||
createdBy?: string;
|
||||
updatedAt?: string;
|
||||
updatedBy?: string;
|
||||
|
||||
managed?: boolean;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
"react",
|
||||
"@kbn/ambient-ui-types",
|
||||
"@kbn/ambient-storybook-types",
|
||||
"@emotion/react/types/css-prop"
|
||||
"@emotion/react/types/css-prop",
|
||||
"@testing-library/jest-dom",
|
||||
"@testing-library/react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
@ -26,7 +28,9 @@
|
|||
"@kbn/core-i18n-browser",
|
||||
"@kbn/core-theme-browser",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/react-kibana-mount"
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/content-management-user-profiles",
|
||||
"@kbn/user-profile-components"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
|
@ -11,6 +11,8 @@ import type { SavedObjectsReference } from '@kbn/content-management-content-edit
|
|||
export interface UserContentCommonSchema {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
updatedBy?: string;
|
||||
createdAt?: string;
|
||||
createdBy?: string;
|
||||
managed?: boolean;
|
||||
references: SavedObjectsReference[];
|
||||
|
|
|
@ -147,7 +147,7 @@ describe('created_by filter', () => {
|
|||
// wait until first render
|
||||
expect(await screen.findByTestId('itemsInMemTable')).toBeVisible();
|
||||
|
||||
// 5 items in the list
|
||||
// 4 items in the list
|
||||
expect(screen.getAllByTestId(/userContentListingTitleLink/)).toHaveLength(4);
|
||||
|
||||
userEvent.click(screen.getByTestId('userFilterPopoverButton'));
|
||||
|
|
|
@ -9,12 +9,13 @@ import React from 'react';
|
|||
import type { ComponentType } from 'react';
|
||||
import { from } from 'rxjs';
|
||||
import { ContentEditorProvider } from '@kbn/content-management-content-editor';
|
||||
import { UserProfilesProvider, UserProfilesServices } from '@kbn/content-management-user-profiles';
|
||||
|
||||
import { TagList } from '../mocks';
|
||||
import { TableListViewProvider, Services } from '../services';
|
||||
|
||||
export const getMockServices = (overrides?: Partial<Services>) => {
|
||||
const services: Services = {
|
||||
export const getMockServices = (overrides?: Partial<Services & UserProfilesServices>) => {
|
||||
const services: Services & UserProfilesServices = {
|
||||
canEditAdvancedSettings: true,
|
||||
getListingLimitSettingsUrl: () => 'http://elastic.co',
|
||||
notifyError: () => undefined,
|
||||
|
@ -25,24 +26,29 @@ export const getMockServices = (overrides?: Partial<Services>) => {
|
|||
itemHasTags: () => true,
|
||||
getTagManagementUrl: () => '',
|
||||
getTagIdsFromReferences: () => [],
|
||||
bulkGetUserProfiles: jest.fn(() => Promise.resolve([])),
|
||||
getUserProfile: jest.fn(),
|
||||
isTaggingEnabled: () => true,
|
||||
bulkGetUserProfiles: async () => [],
|
||||
getUserProfile: async () => ({ uid: '', enabled: true, data: {}, user: { username: '' } }),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return services;
|
||||
};
|
||||
|
||||
export function WithServices<P>(Comp: ComponentType<P>, overrides: Partial<Services> = {}) {
|
||||
export function WithServices<P>(
|
||||
Comp: ComponentType<P>,
|
||||
overrides: Partial<Services & UserProfilesServices> = {}
|
||||
) {
|
||||
return (props: P) => {
|
||||
const services = getMockServices(overrides);
|
||||
return (
|
||||
<ContentEditorProvider openFlyout={jest.fn()} notifyError={() => undefined}>
|
||||
<TableListViewProvider {...services}>
|
||||
<Comp {...(props as any)} />
|
||||
</TableListViewProvider>
|
||||
</ContentEditorProvider>
|
||||
<UserProfilesProvider {...services}>
|
||||
<ContentEditorProvider openFlyout={jest.fn()} notifyError={() => undefined}>
|
||||
<TableListViewProvider {...services}>
|
||||
<Comp {...(props as any)} />
|
||||
</TableListViewProvider>
|
||||
</ContentEditorProvider>
|
||||
</UserProfilesProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,10 +11,8 @@ import React from 'react';
|
|||
import { EuiFilterButton, useEuiTheme } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { UserProfile, UserProfilesPopover } from '@kbn/user-profile-components';
|
||||
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useServices } from '../services';
|
||||
import { NoUsersTip } from './user_missing_tip';
|
||||
import { useUserProfiles, NoCreatorTip } from '@kbn/content-management-user-profiles';
|
||||
|
||||
interface Context {
|
||||
enabled: boolean;
|
||||
|
@ -25,43 +23,29 @@ interface Context {
|
|||
}
|
||||
|
||||
const UserFilterContext = React.createContext<Context | null>(null);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, staleTime: 30 * 60 * 1000 } },
|
||||
});
|
||||
|
||||
export const UserFilterContextProvider: FC<Context> = ({ children, ...props }) => {
|
||||
if (!props.enabled) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<UserFilterContext.Provider value={props}>{children}</UserFilterContext.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
return <UserFilterContext.Provider value={props}>{children}</UserFilterContext.Provider>;
|
||||
};
|
||||
|
||||
export const NULL_USER = 'no-user';
|
||||
|
||||
export const UserFilterPanel: FC<{}> = () => {
|
||||
const { bulkGetUserProfiles } = useServices();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const componentContext = React.useContext(UserFilterContext);
|
||||
if (!componentContext)
|
||||
throw new Error('UserFilterPanel must be used within a UserFilterContextProvider');
|
||||
if (!bulkGetUserProfiles)
|
||||
throw new Error('UserFilterPanel must be used with a bulkGetUserProfiles function');
|
||||
|
||||
const { onSelectedUsersChange, selectedUsers, showNoUserOption } = componentContext;
|
||||
|
||||
const [isPopoverOpen, setPopoverOpen] = React.useState(false);
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['user-filter-suggestions', componentContext.allUsers],
|
||||
queryFn: () => bulkGetUserProfiles(componentContext.allUsers),
|
||||
enabled: isPopoverOpen,
|
||||
});
|
||||
const query = useUserProfiles(componentContext.allUsers, { enabled: isPopoverOpen });
|
||||
|
||||
const usersMap = React.useMemo(() => {
|
||||
if (!query.data) return {};
|
||||
|
@ -138,7 +122,7 @@ export const UserFilterPanel: FC<{}> = () => {
|
|||
id="contentManagement.tableList.listing.userFilter.emptyMessage"
|
||||
defaultMessage="None of the dashboards have creators"
|
||||
/>
|
||||
{<NoUsersTip />}
|
||||
{<NoCreatorTip />}
|
||||
</p>
|
||||
),
|
||||
nullOptionLabel: i18n.translate(
|
||||
|
@ -148,7 +132,7 @@ export const UserFilterPanel: FC<{}> = () => {
|
|||
}
|
||||
),
|
||||
nullOptionProps: {
|
||||
append: <NoUsersTip />,
|
||||
append: <NoCreatorTip />,
|
||||
},
|
||||
clearButtonLabel: (
|
||||
<FormattedMessage
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiIconTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export const NoUsersTip = () => (
|
||||
<EuiIconTip
|
||||
aria-label="Additional information"
|
||||
position="bottom"
|
||||
type="questionInCircle"
|
||||
color="inherit"
|
||||
iconProps={{ style: { verticalAlign: 'text-bottom', marginLeft: 2 } }}
|
||||
css={{ textWrap: 'balance' }}
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="contentManagement.tableList.listing.noUsersTip"
|
||||
defaultMessage="Creators are assigned when dashboards are created (after version 8.14)"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
|
@ -71,8 +71,6 @@ export const getStoryServices = (params: Params, action: ActionFn = () => {}) =>
|
|||
itemHasTags: () => true,
|
||||
getTagManagementUrl: () => '',
|
||||
getTagIdsFromReferences: () => [],
|
||||
bulkGetUserProfiles: () => Promise.resolve([]),
|
||||
getUserProfile: jest.fn(),
|
||||
isTaggingEnabled: () => true,
|
||||
...params,
|
||||
};
|
||||
|
|
|
@ -20,11 +20,10 @@ import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser';
|
|||
import type { OverlayFlyoutOpenOptions } from '@kbn/core-overlays-browser';
|
||||
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import type { UserProfileServiceStart } from '@kbn/core-user-profile-browser';
|
||||
import type { UserProfile } from '@kbn/user-profile-components';
|
||||
import type { FormattedRelative } from '@kbn/i18n-react';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { RedirectAppLinksKibanaProvider } from '@kbn/shared-ux-link-redirect-app';
|
||||
import { createBatcher } from './utils/batcher';
|
||||
import { UserProfilesKibanaProvider } from '@kbn/content-management-user-profiles';
|
||||
|
||||
import { TAG_MANAGEMENT_APP_URL } from './constants';
|
||||
import type { Tag } from './types';
|
||||
|
@ -69,9 +68,6 @@ export interface Services {
|
|||
/** Handler to return the url to navigate to the kibana tags management */
|
||||
getTagManagementUrl: () => string;
|
||||
getTagIdsFromReferences: (references: SavedObjectsReference[]) => string[];
|
||||
/** resolve user profiles for the user filter and creator functionality */
|
||||
bulkGetUserProfiles: (uids: string[]) => Promise<UserProfile[]>;
|
||||
getUserProfile: (uid: string) => Promise<UserProfile>;
|
||||
}
|
||||
|
||||
const TableListViewContext = React.createContext<Services | null>(null);
|
||||
|
@ -229,51 +225,35 @@ export const TableListViewKibanaProvider: FC<
|
|||
[getTagIdsFromReferences]
|
||||
);
|
||||
|
||||
const bulkGetUserProfiles = useCallback<(userProfileIds: string[]) => Promise<UserProfile[]>>(
|
||||
async (uids: string[]) => {
|
||||
if (uids.length === 0) return [];
|
||||
|
||||
return core.userProfile.bulkGet({ uids: new Set(uids), dataPath: 'avatar' });
|
||||
},
|
||||
[core.userProfile]
|
||||
);
|
||||
|
||||
const getUserProfile = useMemo(() => {
|
||||
return createBatcher({
|
||||
fetcher: bulkGetUserProfiles,
|
||||
resolver: (users, id) => users.find((u) => u.uid === id)!,
|
||||
}).fetch;
|
||||
}, [bulkGetUserProfiles]);
|
||||
|
||||
return (
|
||||
<RedirectAppLinksKibanaProvider coreStart={core}>
|
||||
<ContentEditorKibanaProvider core={core} savedObjectsTagging={savedObjectsTagging}>
|
||||
<TableListViewProvider
|
||||
canEditAdvancedSettings={Boolean(application.capabilities.advancedSettings?.save)}
|
||||
getListingLimitSettingsUrl={() =>
|
||||
application.getUrlForApp('management', {
|
||||
path: `/kibana/settings?query=savedObjects:listingLimit`,
|
||||
})
|
||||
}
|
||||
notifyError={(title, text) => {
|
||||
notifications.toasts.addDanger({ title: toMountPoint(title, startServices), text });
|
||||
}}
|
||||
searchQueryParser={searchQueryParser}
|
||||
DateFormatterComp={(props) => <FormattedRelative {...props} />}
|
||||
currentAppId$={application.currentAppId$}
|
||||
navigateToUrl={application.navigateToUrl}
|
||||
isTaggingEnabled={() => Boolean(savedObjectsTagging)}
|
||||
getTagList={getTagList}
|
||||
TagList={TagList}
|
||||
itemHasTags={itemHasTags}
|
||||
getTagIdsFromReferences={getTagIdsFromReferences}
|
||||
getTagManagementUrl={() => core.http.basePath.prepend(TAG_MANAGEMENT_APP_URL)}
|
||||
bulkGetUserProfiles={bulkGetUserProfiles}
|
||||
getUserProfile={getUserProfile}
|
||||
>
|
||||
{children}
|
||||
</TableListViewProvider>
|
||||
</ContentEditorKibanaProvider>
|
||||
<UserProfilesKibanaProvider core={core}>
|
||||
<ContentEditorKibanaProvider core={core} savedObjectsTagging={savedObjectsTagging}>
|
||||
<TableListViewProvider
|
||||
canEditAdvancedSettings={Boolean(application.capabilities.advancedSettings?.save)}
|
||||
getListingLimitSettingsUrl={() =>
|
||||
application.getUrlForApp('management', {
|
||||
path: `/kibana/settings?query=savedObjects:listingLimit`,
|
||||
})
|
||||
}
|
||||
notifyError={(title, text) => {
|
||||
notifications.toasts.addDanger({ title: toMountPoint(title, startServices), text });
|
||||
}}
|
||||
searchQueryParser={searchQueryParser}
|
||||
DateFormatterComp={(props) => <FormattedRelative {...props} />}
|
||||
currentAppId$={application.currentAppId$}
|
||||
navigateToUrl={application.navigateToUrl}
|
||||
isTaggingEnabled={() => Boolean(savedObjectsTagging)}
|
||||
getTagList={getTagList}
|
||||
TagList={TagList}
|
||||
itemHasTags={itemHasTags}
|
||||
getTagIdsFromReferences={getTagIdsFromReferences}
|
||||
getTagManagementUrl={() => core.http.basePath.prepend(TAG_MANAGEMENT_APP_URL)}
|
||||
>
|
||||
{children}
|
||||
</TableListViewProvider>
|
||||
</ContentEditorKibanaProvider>
|
||||
</UserProfilesKibanaProvider>
|
||||
</RedirectAppLinksKibanaProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -27,6 +27,11 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { useOpenContentEditor } from '@kbn/content-management-content-editor';
|
||||
import {
|
||||
UserAvatarTip,
|
||||
ManagedAvatarTip,
|
||||
NoCreatorTip,
|
||||
} from '@kbn/content-management-user-profiles';
|
||||
import type {
|
||||
OpenContentEditorParams,
|
||||
SavedObjectsReference,
|
||||
|
@ -47,12 +52,12 @@ import { type SortColumnField, getInitialSorting, saveSorting } from './componen
|
|||
import { useTags } from './use_tags';
|
||||
import { useInRouterContext, useUrlState } from './use_url_state';
|
||||
import { RowActions, TableItemsRowActions } from './types';
|
||||
import { UserAvatarTip } from './components/user_avatar_tip';
|
||||
import { NoUsersTip } from './components/user_missing_tip';
|
||||
import { ManagedAvatarTip } from './components/managed_avatar_tip';
|
||||
|
||||
interface ContentEditorConfig
|
||||
extends Pick<OpenContentEditorParams, 'isReadonly' | 'onSave' | 'customValidators'> {
|
||||
extends Pick<
|
||||
OpenContentEditorParams,
|
||||
'isReadonly' | 'onSave' | 'customValidators' | 'showActivityView'
|
||||
> {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
|
@ -506,6 +511,11 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
title: item.attributes.title,
|
||||
description: item.attributes.description,
|
||||
tags,
|
||||
createdAt: item.createdAt,
|
||||
createdBy: item.createdBy,
|
||||
updatedAt: item.updatedAt,
|
||||
updatedBy: item.updatedBy,
|
||||
managed: item.managed,
|
||||
},
|
||||
entityName,
|
||||
...contentEditor,
|
||||
|
@ -575,7 +585,6 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
{i18n.translate('contentManagement.tableList.createdByColumnTitle', {
|
||||
defaultMessage: 'Creator',
|
||||
})}
|
||||
<NoUsersTip />
|
||||
</>
|
||||
),
|
||||
render: (field: string, record: { createdBy?: string; managed?: boolean }) =>
|
||||
|
@ -583,7 +592,9 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
<UserAvatarTip uid={record.createdBy} />
|
||||
) : record.managed ? (
|
||||
<ManagedAvatarTip entityName={entityName} />
|
||||
) : null,
|
||||
) : (
|
||||
<NoCreatorTip iconType={'minus'} />
|
||||
),
|
||||
sortable:
|
||||
false /* createdBy column is not sortable because it doesn't make sense to sort by id*/,
|
||||
width: '100px',
|
||||
|
@ -601,7 +612,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
<UpdatedAtField dateTime={record.updatedAt} DateFormatterComp={DateFormatterComp} />
|
||||
),
|
||||
sortable: true,
|
||||
width: '120px',
|
||||
width: '130px',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,8 @@
|
|||
"@kbn/content-management-table-list-view-common",
|
||||
"@kbn/user-profile-components",
|
||||
"@kbn/core-user-profile-browser",
|
||||
"@kbn/react-kibana-mount"
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/content-management-user-profiles"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
3
packages/content-management/user_profiles/README.md
Normal file
3
packages/content-management/user_profiles/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/content-management-user-profiles
|
||||
|
||||
Shared user profile components for content management components.
|
19
packages/content-management/user_profiles/index.ts
Normal file
19
packages/content-management/user_profiles/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { UserAvatarTip, NoUpdaterTip, NoCreatorTip, ManagedAvatarTip } from './src/components';
|
||||
|
||||
export { useUserProfile, useUserProfiles } from './src/queries';
|
||||
|
||||
export {
|
||||
UserProfilesKibanaProvider,
|
||||
type UserProfilesKibanaDependencies,
|
||||
UserProfilesProvider,
|
||||
type UserProfilesServices,
|
||||
useUserProfilesServices,
|
||||
} from './src/services';
|
13
packages/content-management/user_profiles/jest.config.js
Normal file
13
packages/content-management/user_profiles/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/packages/content-management/user_profiles'],
|
||||
};
|
5
packages/content-management/user_profiles/kibana.jsonc
Normal file
5
packages/content-management/user_profiles/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/content-management-user-profiles",
|
||||
"owner": "@elastic/appex-sharedux"
|
||||
}
|
6
packages/content-management/user_profiles/package.json
Normal file
6
packages/content-management/user_profiles/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/content-management-user-profiles",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { UserAvatarTip } from './user_avatar_tip';
|
||||
export { NoUpdaterTip, NoCreatorTip } from './user_missing_tip';
|
||||
export { ManagedAvatarTip } from './managed_avatar_tip';
|
|
@ -10,10 +10,16 @@ import React from 'react';
|
|||
import { EuiAvatar, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function ManagedAvatarTip({ entityName }: { entityName: string }) {
|
||||
export function ManagedAvatarTip({
|
||||
entityName = i18n.translate('contentManagement.userProfiles.managedAvatarTip.defaultEntityName', {
|
||||
defaultMessage: 'object',
|
||||
}),
|
||||
}: {
|
||||
entityName?: string;
|
||||
}) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate('contentManagement.tableList.managedAvatarTip.avatarTooltip', {
|
||||
content={i18n.translate('contentManagement.userProfiles.managedAvatarTip.avatarTooltip', {
|
||||
defaultMessage:
|
||||
'This {entityName} is created and managed by Elastic. Clone it to make changes.',
|
||||
values: {
|
||||
|
@ -22,7 +28,7 @@ export function ManagedAvatarTip({ entityName }: { entityName: string }) {
|
|||
})}
|
||||
>
|
||||
<EuiAvatar
|
||||
name={i18n.translate('contentManagement.tableList.managedAvatarTip.avatarLabel', {
|
||||
name={i18n.translate('contentManagement.userProfiles.managedAvatarTip.avatarLabel', {
|
||||
defaultMessage: 'Managed',
|
||||
})}
|
||||
iconType={'logoElastic'}
|
|
@ -8,18 +8,10 @@
|
|||
|
||||
import React from 'react';
|
||||
import { UserAvatarTip as UserAvatarTipComponent } from '@kbn/user-profile-components';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useServices } from '../services';
|
||||
import { useUserProfile } from '../queries';
|
||||
|
||||
export function UserAvatarTip(props: { uid: string }) {
|
||||
const { getUserProfile } = useServices();
|
||||
const query = useQuery(
|
||||
['user-profile', props.uid],
|
||||
async () => {
|
||||
return getUserProfile(props.uid);
|
||||
},
|
||||
{ staleTime: Infinity }
|
||||
);
|
||||
const query = useUserProfile(props.uid);
|
||||
|
||||
if (query.data) {
|
||||
return (
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiIconTip, IconType } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export const NoCreatorTip = (props: { iconType?: IconType }) => (
|
||||
<NoUsersTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="contentManagement.userProfiles.noCreatorTip"
|
||||
defaultMessage="Creators are assigned when objects are created (after version 8.14)"
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const NoUpdaterTip = (props: { iconType?: string }) => (
|
||||
<NoUsersTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="contentManagement.userProfiles.noUpdaterTip"
|
||||
defaultMessage="Updated by is set when objects are updated (after version 8.14)"
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const NoUsersTip = ({
|
||||
iconType: type = 'questionInCircle',
|
||||
...props
|
||||
}: {
|
||||
content: React.ReactNode;
|
||||
iconType?: IconType;
|
||||
}) => (
|
||||
<EuiIconTip
|
||||
aria-label="Additional information"
|
||||
position="top"
|
||||
color="inherit"
|
||||
iconProps={{ style: { verticalAlign: 'text-bottom', marginLeft: 2 } }}
|
||||
css={{ textWrap: 'balance' }}
|
||||
type={type}
|
||||
{...props}
|
||||
/>
|
||||
);
|
36
packages/content-management/user_profiles/src/queries.ts
Normal file
36
packages/content-management/user_profiles/src/queries.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { useUserProfilesServices } from './services';
|
||||
|
||||
export const userProfileKeys = {
|
||||
get: (uid: string) => ['user-profile', uid],
|
||||
bulkGet: (uids: string[]) => ['user-profile', { uids }],
|
||||
};
|
||||
|
||||
export const useUserProfile = (uid: string) => {
|
||||
const { getUserProfile } = useUserProfilesServices();
|
||||
const query = useQuery(
|
||||
userProfileKeys.get(uid),
|
||||
async () => {
|
||||
return getUserProfile(uid);
|
||||
},
|
||||
{ staleTime: Infinity }
|
||||
);
|
||||
return query;
|
||||
};
|
||||
|
||||
export const useUserProfiles = (uids: string[], opts?: { enabled?: boolean }) => {
|
||||
const { bulkGetUserProfiles } = useUserProfilesServices();
|
||||
const query = useQuery({
|
||||
queryKey: userProfileKeys.bulkGet(uids),
|
||||
queryFn: () => bulkGetUserProfiles(uids),
|
||||
enabled: opts?.enabled ?? true,
|
||||
});
|
||||
return query;
|
||||
};
|
82
packages/content-management/user_profiles/src/services.tsx
Normal file
82
packages/content-management/user_profiles/src/services.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { UserProfileServiceStart } from '@kbn/core-user-profile-browser';
|
||||
import React, { FC, PropsWithChildren, useCallback, useContext, useMemo } from 'react';
|
||||
import type { UserProfile } from '@kbn/user-profile-components';
|
||||
import { createBatcher } from './utils/batcher';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, staleTime: 30 * 60 * 1000 } },
|
||||
});
|
||||
|
||||
export interface UserProfilesKibanaDependencies {
|
||||
core: {
|
||||
userProfile: {
|
||||
bulkGet: UserProfileServiceStart['bulkGet'];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserProfilesServices {
|
||||
bulkGetUserProfiles: (uids: string[]) => Promise<UserProfile[]>;
|
||||
getUserProfile: (uid: string) => Promise<UserProfile>;
|
||||
}
|
||||
|
||||
const UserProfilesContext = React.createContext<UserProfilesServices | null>(null);
|
||||
|
||||
export const UserProfilesProvider: FC<PropsWithChildren<UserProfilesServices>> = ({
|
||||
children,
|
||||
...services
|
||||
}) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<UserProfilesContext.Provider value={services}>{children}</UserProfilesContext.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserProfilesKibanaProvider: FC<PropsWithChildren<UserProfilesKibanaDependencies>> = ({
|
||||
children,
|
||||
core,
|
||||
}) => {
|
||||
const bulkGetUserProfiles = useCallback<(userProfileIds: string[]) => Promise<UserProfile[]>>(
|
||||
async (uids: string[]) => {
|
||||
if (uids.length === 0) return [];
|
||||
|
||||
return core.userProfile.bulkGet({ uids: new Set(uids), dataPath: 'avatar' });
|
||||
},
|
||||
[core.userProfile]
|
||||
);
|
||||
|
||||
const getUserProfile = useMemo(() => {
|
||||
return createBatcher({
|
||||
fetcher: bulkGetUserProfiles,
|
||||
resolver: (users, id) => users.find((u) => u.uid === id)!,
|
||||
}).fetch;
|
||||
}, [bulkGetUserProfiles]);
|
||||
|
||||
return (
|
||||
<UserProfilesProvider getUserProfile={getUserProfile} bulkGetUserProfiles={bulkGetUserProfiles}>
|
||||
{children}
|
||||
</UserProfilesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useUserProfilesServices() {
|
||||
const context = useContext(UserProfilesContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'UserProfilesContext is missing. Ensure your component or React root is wrapped with <UserProfilesProvider />'
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
24
packages/content-management/user_profiles/tsconfig.json
Normal file
24
packages/content-management/user_profiles/tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/user-profile-components",
|
||||
"@kbn/core-user-profile-browser",
|
||||
"@kbn/i18n",
|
||||
"@kbn/i18n-react",
|
||||
]
|
||||
}
|
|
@ -154,6 +154,7 @@ describe('useDashboardListingTable', () => {
|
|||
onSave: expect.any(Function),
|
||||
isReadonly: false,
|
||||
customValidators: expect.any(Object),
|
||||
showActivityView: true,
|
||||
},
|
||||
createdByEnabled: true,
|
||||
};
|
||||
|
|
|
@ -42,7 +42,9 @@ const toTableListViewSavedObject = (hit: DashboardItem): DashboardSavedObjectUse
|
|||
type: 'dashboard',
|
||||
id: hit.id,
|
||||
updatedAt: hit.updatedAt!,
|
||||
createdAt: hit.createdAt,
|
||||
createdBy: hit.createdBy,
|
||||
updatedBy: hit.updatedBy,
|
||||
references: hit.references,
|
||||
managed: hit.managed,
|
||||
attributes: {
|
||||
|
@ -280,6 +282,7 @@ export const useDashboardListingTable = ({
|
|||
isReadonly: !showWriteControls,
|
||||
onSave: updateItemMeta,
|
||||
customValidators: contentEditorValidators,
|
||||
showActivityView: true,
|
||||
},
|
||||
createItem: !showWriteControls || !showCreateDashboardButton ? undefined : createItem,
|
||||
deleteItems: !showWriteControls ? undefined : deleteItems,
|
||||
|
|
|
@ -196,6 +196,8 @@
|
|||
"@kbn/content-management-table-list-view-common/*": ["packages/content-management/table_list_view_common/*"],
|
||||
"@kbn/content-management-table-list-view-table": ["packages/content-management/table_list_view_table"],
|
||||
"@kbn/content-management-table-list-view-table/*": ["packages/content-management/table_list_view_table/*"],
|
||||
"@kbn/content-management-user-profiles": ["packages/content-management/user_profiles"],
|
||||
"@kbn/content-management-user-profiles/*": ["packages/content-management/user_profiles/*"],
|
||||
"@kbn/content-management-utils": ["packages/kbn-content-management-utils"],
|
||||
"@kbn/content-management-utils/*": ["packages/kbn-content-management-utils/*"],
|
||||
"@kbn/controls-example-plugin": ["examples/controls_example"],
|
||||
|
|
|
@ -3545,6 +3545,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/content-management-user-profiles@link:packages/content-management/user_profiles":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/content-management-utils@link:packages/kbn-content-management-utils":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue