[TableListView] Improve help text of creator and view count (#202488)

## Summary

This PR brings back version mentions in help text for non-serverless
that we removed in https://github.com/elastic/kibana/pull/193024. In
that PR we decided that it is not worth adding complexity for checking
`isServerless` deep inside table list view components, but I want to
bring version mentions back now because I believe that it can be very
confusing without the version mentions for existing deployments

Two recent features: 1. created_by; 2. view counts are only working
since 8.14 and 8.16 respectively, so for older kibana with old
dashboards it might be confusing that the data for new features is
missing after the upgrade. In help text we can at least mention that the
reason that data is missing is because we only gather the data starting
from a specific version.

### Serverless (version mentions are missing as before) 


![Screenshot 2024-12-06 at 12 39
50](https://github.com/user-attachments/assets/4ad2cd23-3aa7-4400-a5bd-419407f2fad2)
![Screenshot 2024-12-03 at 11 59
09](https://github.com/user-attachments/assets/c56de5d3-1afb-411f-bdbd-419025ef9084)




### Statefull (version are shown again, just like before
https://github.com/elastic/kibana/pull/193024)


![Screenshot 2024-12-06 at 13 03
58](https://github.com/user-attachments/assets/24ea67a5-8a32-45b0-9a4f-2890aaf7ded5)
![Screenshot 2024-12-06 at 13 04
04](https://github.com/user-attachments/assets/8f91d32b-457f-4fd7-882a-d2dd9a3476f4)
![Screenshot 2024-12-03 at 14 11
09](https://github.com/user-attachments/assets/47ea1f8a-1a7b-4aa6-af81-206c6f2d087e)




# Release Notes

Improve help text of creator and view count features on dashboard
listing page
This commit is contained in:
Anton Dosov 2024-12-06 16:48:20 +01:00 committed by GitHub
parent efe06a3357
commit ea1c846e54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 160 additions and 48 deletions

View file

@ -22,12 +22,15 @@ import {
import { getUserDisplayName } from '@kbn/user-profile-components';
import { Item } from '../types';
import { useServices } from '../services';
export interface ActivityViewProps {
item: Pick<Partial<Item>, 'createdBy' | 'createdAt' | 'updatedBy' | 'updatedAt' | 'managed'>;
entityNamePlural?: string;
}
export const ActivityView = ({ item }: ActivityViewProps) => {
export const ActivityView = ({ item, entityNamePlural }: ActivityViewProps) => {
const isKibanaVersioningEnabled = useServices()?.isKibanaVersioningEnabled ?? false;
const showLastUpdated = Boolean(item.updatedAt && item.updatedAt !== item.createdAt);
const UnknownUserLabel = (
@ -62,7 +65,10 @@ export const ActivityView = ({ item }: ActivityViewProps) => {
) : (
<>
{UnknownUserLabel}
<NoCreatorTip />
<NoCreatorTip
includeVersionTip={isKibanaVersioningEnabled}
entityNamePlural={entityNamePlural}
/>
</>
)
}
@ -85,7 +91,10 @@ export const ActivityView = ({ item }: ActivityViewProps) => {
) : (
<>
{UnknownUserLabel}
<NoUpdaterTip />
<NoUpdaterTip
includeVersionTip={isKibanaVersioningEnabled}
entityNamePlural={entityNamePlural}
/>
</>
)
}

View file

@ -73,24 +73,39 @@ export const ViewsStats = ({ item }: { item: Item }) => {
);
};
const NoViewsTip = () => (
<EuiIconTip
aria-label={i18n.translate('contentManagement.contentEditor.viewsStats.noViewsTipAriaLabel', {
defaultMessage: 'Additional information',
})}
position="top"
color="inherit"
iconProps={{ style: { verticalAlign: 'text-bottom', marginLeft: 2 } }}
css={{ textWrap: 'balance' }}
type="questionInCircle"
content={
<FormattedMessage
id="contentManagement.contentEditor.viewsStats.noViewsTip"
defaultMessage="Views are counted every time someone opens a dashboard"
/>
}
/>
);
const NoViewsTip = () => {
const isKibanaVersioningEnabled = useServices()?.isKibanaVersioningEnabled ?? false;
return (
<EuiIconTip
aria-label={i18n.translate('contentManagement.contentEditor.viewsStats.noViewsTipAriaLabel', {
defaultMessage: 'Additional information',
})}
position="top"
color="inherit"
iconProps={{ style: { verticalAlign: 'text-bottom', marginLeft: 2 } }}
css={{ textWrap: 'balance' }}
type="questionInCircle"
content={
<>
<FormattedMessage
id="contentManagement.contentEditor.viewsStats.noViewsTip"
defaultMessage="Views are counted every time someone opens a dashboard"
/>
{isKibanaVersioningEnabled && (
<>
{' '}
<FormattedMessage
id="contentManagement.contentEditor.viewsStats.noViewsVersionTip"
defaultMessage="(after version {version})"
values={{ version: '8.16' }}
/>
</>
)}
</>
}
/>
);
};
export function getTotalDays(stats: ContentInsightsStats) {
return moment.utc().diff(moment.utc(stats.from), 'days');

View file

@ -17,6 +17,11 @@ import { ContentInsightsClientPublic } from './client';
*/
export interface ContentInsightsServices {
contentInsightsClient: ContentInsightsClientPublic;
/**
* Whether versioning is enabled for the current kibana instance. (aka is Serverless)
* This is used to determine if we should show the version mentions in the help text.
*/
isKibanaVersioningEnabled: boolean;
}
const ContentInsightsContext = React.createContext<ContentInsightsServices | null>(null);
@ -34,7 +39,10 @@ export const ContentInsightsProvider: FC<PropsWithChildren<Partial<ContentInsigh
return (
<ContentInsightsContext.Provider
value={{ contentInsightsClient: services.contentInsightsClient }}
value={{
contentInsightsClient: services.contentInsightsClient,
isKibanaVersioningEnabled: services.isKibanaVersioningEnabled ?? false,
}}
>
{children}
</ContentInsightsContext.Provider>

View file

@ -32,6 +32,7 @@ export const getMockServices = (overrides?: Partial<Services & UserProfilesServi
isFavoritesEnabled: () => false,
bulkGetUserProfiles: async () => [],
getUserProfile: async () => ({ uid: '', enabled: true, data: {}, user: { username: '' } }),
isKibanaVersioningEnabled: false,
...overrides,
};

View file

@ -16,7 +16,10 @@ import { ActivityView, ViewsStats } from '@kbn/content-management-content-insigh
/**
* This component is used as an extension for the ContentEditor to render the ActivityView and ViewsStats inside the flyout without depending on them directly
*/
export const ContentEditorActivityRow: FC<{ item: UserContentCommonSchema }> = ({ item }) => {
export const ContentEditorActivityRow: FC<{
item: UserContentCommonSchema;
entityNamePlural?: string;
}> = ({ item, entityNamePlural }) => {
return (
<EuiFormRow
fullWidth
@ -40,7 +43,7 @@ export const ContentEditorActivityRow: FC<{ item: UserContentCommonSchema }> = (
}
>
<>
<ActivityView item={item} />
<ActivityView item={item} entityNamePlural={entityNamePlural} />
<EuiSpacer size={'s'} />
<ViewsStats item={item} />
</>

View file

@ -113,7 +113,7 @@ export function Table<T extends UserContentCommonSchema>({
favoritesEnabled,
}: Props<T>) {
const euiTheme = useEuiTheme();
const { getTagList, isTaggingEnabled } = useServices();
const { getTagList, isTaggingEnabled, isKibanaVersioningEnabled } = useServices();
const renderToolsLeft = useCallback(() => {
if (!deleteItems || selectedIds.length === 0) {
@ -340,6 +340,8 @@ export function Table<T extends UserContentCommonSchema>({
}}
selectedUsers={tableFilter.createdBy}
showNoUserOption={showNoUserOption}
isKibanaVersioningEnabled={isKibanaVersioningEnabled}
entityNamePlural={entityNamePlural}
>
<TagFilterContextProvider
isPopoverOpen={isPopoverOpen}

View file

@ -21,6 +21,8 @@ interface Context {
selectedUsers: string[];
allUsers: string[];
showNoUserOption: boolean;
isKibanaVersioningEnabled: boolean;
entityNamePlural: string;
}
const UserFilterContext = React.createContext<Context | null>(null);
@ -44,7 +46,13 @@ export const UserFilterPanel: FC<{}> = () => {
if (!componentContext)
throw new Error('UserFilterPanel must be used within a UserFilterContextProvider');
const { onSelectedUsersChange, selectedUsers, showNoUserOption } = componentContext;
const {
onSelectedUsersChange,
selectedUsers,
showNoUserOption,
isKibanaVersioningEnabled,
entityNamePlural,
} = componentContext;
const [isPopoverOpen, setPopoverOpen] = React.useState(false);
const [searchTerm, setSearchTerm] = React.useState('');
@ -126,7 +134,12 @@ export const UserFilterPanel: FC<{}> = () => {
id="contentManagement.tableList.listing.userFilter.emptyMessage"
defaultMessage="None of the dashboards have creators"
/>
{<NoCreatorTip />}
{
<NoCreatorTip
includeVersionTip={isKibanaVersioningEnabled}
entityNamePlural={entityNamePlural}
/>
}
</p>
),
nullOptionLabel: i18n.translate(
@ -136,7 +149,12 @@ export const UserFilterPanel: FC<{}> = () => {
}
),
nullOptionProps: {
append: <NoCreatorTip />,
append: (
<NoCreatorTip
includeVersionTip={isKibanaVersioningEnabled}
entityNamePlural={entityNamePlural}
/>
),
},
clearButtonLabel: (
<FormattedMessage

View file

@ -76,6 +76,7 @@ export const getStoryServices = (params: Params, action: ActionFn = () => {}) =>
getTagIdsFromReferences: () => [],
isTaggingEnabled: () => true,
isFavoritesEnabled: () => false,
isKibanaVersioningEnabled: false,
...params,
};

View file

@ -79,6 +79,9 @@ export interface Services {
/** Handler to return the url to navigate to the kibana tags management */
getTagManagementUrl: () => string;
getTagIdsFromReferences: (references: SavedObjectsReference[]) => string[];
/** Whether versioning is enabled for the current kibana instance. (aka is Serverless)
This is used to determine if we should show the version mentions in the help text.*/
isKibanaVersioningEnabled: boolean;
}
const TableListViewContext = React.createContext<Services | null>(null);
@ -185,6 +188,12 @@ export interface TableListViewKibanaDependencies {
* Content insights client to enable content insights features.
*/
contentInsightsClient?: ContentInsightsClientPublic;
/**
* Flag to indicate if Kibana versioning is enabled. (aka not Serverless)
* Used to determine if we should show the version mentions in the help text.
*/
isKibanaVersioningEnabled?: boolean;
}
/**
@ -251,7 +260,10 @@ export const TableListViewKibanaProvider: FC<
<RedirectAppLinksKibanaProvider coreStart={core}>
<UserProfilesKibanaProvider core={core}>
<ContentEditorKibanaProvider core={core} savedObjectsTagging={savedObjectsTagging}>
<ContentInsightsProvider contentInsightsClient={services.contentInsightsClient}>
<ContentInsightsProvider
contentInsightsClient={services.contentInsightsClient}
isKibanaVersioningEnabled={services.isKibanaVersioningEnabled}
>
<FavoritesContextProvider
favoritesClient={services.favorites}
notifyError={(title, text) => {
@ -282,6 +294,7 @@ export const TableListViewKibanaProvider: FC<
itemHasTags={itemHasTags}
getTagIdsFromReferences={getTagIdsFromReferences}
getTagManagementUrl={() => core.http.basePath.prepend(TAG_MANAGEMENT_APP_URL)}
isKibanaVersioningEnabled={services.isKibanaVersioningEnabled ?? false}
>
{children}
</TableListViewProvider>

View file

@ -376,6 +376,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
DateFormatterComp,
getTagList,
isFavoritesEnabled,
isKibanaVersioningEnabled,
} = useServices();
const openContentEditor = useOpenContentEditor();
@ -578,7 +579,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
appendRows: contentInsightsServices && (
// have to "REWRAP" in the provider here because it will be rendered in a different context
<ContentInsightsProvider {...contentInsightsServices}>
<ContentEditorActivityRow item={item} />
<ContentEditorActivityRow item={item} entityNamePlural={entityNamePlural} />
</ContentInsightsProvider>
),
});
@ -591,6 +592,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
tableItemsRowActions,
fetchItems,
contentInsightsServices,
entityNamePlural,
]
);
@ -646,7 +648,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
) : record.managed ? (
<ManagedAvatarTip entityName={entityName} />
) : (
<NoCreatorTip iconType={'minus'} />
<NoCreatorTip iconType={'minus'} includeVersionTip={isKibanaVersioningEnabled} />
),
sortable:
false /* createdBy column is not sortable because it doesn't make sense to sort by id*/,
@ -753,6 +755,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
inspectItem,
entityName,
isFavoritesEnabled,
isKibanaVersioningEnabled,
]);
const itemsById = useMemo(() => {

View file

@ -7,29 +7,67 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiIconTip, IconType } from '@elastic/eui';
import React from 'react';
export const NoCreatorTip = (props: { iconType?: IconType }) => (
const fallbackEntityNamePlural = i18n.translate(
'contentManagement.userProfiles.fallbackEntityNamePlural',
{ defaultMessage: 'objects' }
);
export const NoCreatorTip = (props: {
iconType?: IconType;
includeVersionTip?: boolean;
entityNamePlural?: string;
}) => (
<NoUsersTip
content={
<FormattedMessage
id="contentManagement.userProfiles.noCreatorTip"
defaultMessage="Creators are assigned when objects are created"
/>
props.includeVersionTip ? (
<FormattedMessage
id="contentManagement.userProfiles.noCreatorTipWithVersion"
defaultMessage="Created by is set when {entityNamePlural} are created by users (not by API) starting from version {version}"
values={{
version: '8.14',
entityNamePlural: props.entityNamePlural ?? fallbackEntityNamePlural,
}}
/>
) : (
<FormattedMessage
id="contentManagement.userProfiles.noCreatorTip"
defaultMessage="Created by is set when {entityNamePlural} are created by users (not by API)"
values={{ entityNamePlural: props.entityNamePlural ?? fallbackEntityNamePlural }}
/>
)
}
{...props}
/>
);
export const NoUpdaterTip = (props: { iconType?: string }) => (
export const NoUpdaterTip = (props: {
iconType?: string;
includeVersionTip?: boolean;
entityNamePlural?: string;
}) => (
<NoUsersTip
content={
<FormattedMessage
id="contentManagement.userProfiles.noUpdaterTip"
defaultMessage="Updated by is set when objects are updated"
/>
props.includeVersionTip ? (
<FormattedMessage
id="contentManagement.userProfiles.noUpdaterTipWithVersion"
defaultMessage="Updated by is set when {entityNamePlural} are updated by users (not by API) starting from version {version}"
values={{
version: '8.15',
entityNamePlural: props.entityNamePlural ?? fallbackEntityNamePlural,
}}
/>
) : (
<FormattedMessage
id="contentManagement.userProfiles.noUpdaterTip"
defaultMessage="Updated by is set when {entityNamePlural} are created by users (not by API)"
values={{ entityNamePlural: props.entityNamePlural ?? fallbackEntityNamePlural }}
/>
)
}
{...props}
/>

View file

@ -19,6 +19,7 @@ import { DASHBOARD_APP_ID, DASHBOARD_CONTENT_ID } from '../dashboard_constants';
import {
coreServices,
savedObjectsTaggingService,
serverlessService,
usageCollectionService,
} from '../services/kibana_services';
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
@ -65,6 +66,7 @@ export const DashboardListing = ({
FormattedRelative,
favorites: dashboardFavoritesClient,
contentInsightsClient,
isKibanaVersioningEnabled: !serverlessService,
}}
>
<TableListView<DashboardSavedObjectUserContent> {...tableListViewTableProps}>

View file

@ -16,7 +16,11 @@ import {
import { FormattedRelative, I18nProvider } from '@kbn/i18n-react';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { coreServices, savedObjectsTaggingService } from '../services/kibana_services';
import {
coreServices,
savedObjectsTaggingService,
serverlessService,
} from '../services/kibana_services';
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
import { useDashboardListingTable } from './hooks/use_dashboard_listing_table';
import { DashboardListingProps, DashboardSavedObjectUserContent } from './types';
@ -57,6 +61,7 @@ export const DashboardListingTable = ({
savedObjectsTagging={savedObjectsTaggingService?.getTaggingApi()}
FormattedRelative={FormattedRelative}
contentInsightsClient={contentInsightsClient}
isKibanaVersioningEnabled={!serverlessService}
>
<>
<DashboardUnsavedListing

View file

@ -623,8 +623,6 @@
"contentManagement.userProfiles.managedAvatarTip.avatarLabel": "Géré",
"contentManagement.userProfiles.managedAvatarTip.avatarTooltip": "Cette entité {entityName} est créée et gérée par Elastic. Clonez-le pour effectuer des modifications.",
"contentManagement.userProfiles.managedAvatarTip.defaultEntityName": "objet",
"contentManagement.userProfiles.noCreatorTip": "Les créateurs sont assignés lors de la création des objets",
"contentManagement.userProfiles.noUpdaterTip": "Le champ Mis à jour par est défini lors de la mise à jour des objets",
"controls.blockingError": "Une erreur s'est produite lors du chargement de ce contrôle.",
"controls.controlFactoryRegistry.factoryAlreadyExistsError": "Une usine de contrôle pour le type : {key} est déjà enregistrée.",
"controls.controlFactoryRegistry.factoryNotFoundError": "Aucune usine de contrôle n'a été trouvée pour le type : {key}",

View file

@ -625,8 +625,6 @@
"contentManagement.userProfiles.managedAvatarTip.avatarLabel": "管理中",
"contentManagement.userProfiles.managedAvatarTip.avatarTooltip": "この{entityName}はElasticによって作成され、管理されています。変更するには、複製してください。",
"contentManagement.userProfiles.managedAvatarTip.defaultEntityName": "オブジェクト",
"contentManagement.userProfiles.noCreatorTip": "作成担当は、オブジェクトが作成されるときに割り当てられます",
"contentManagement.userProfiles.noUpdaterTip": "更新は、オブジェクトが更新されるときに割り当てられます",
"controls.blockingError": "このコントロールの読み込みエラーが発生しました。",
"controls.controlFactoryRegistry.factoryAlreadyExistsError": "タイプ\"{key}\"のコントロールファクトリはすでに登録されています。",
"controls.controlFactoryRegistry.factoryNotFoundError": "タイプ\"{key}\"のコントロールファクトリが見つかりません",

View file

@ -647,8 +647,6 @@
"contentManagement.userProfiles.managedAvatarTip.avatarLabel": "托管",
"contentManagement.userProfiles.managedAvatarTip.avatarTooltip": "此 {entityName} 由 Elastic 创建和管理。进行克隆以做出更改。",
"contentManagement.userProfiles.managedAvatarTip.defaultEntityName": "对象",
"contentManagement.userProfiles.noCreatorTip": "将在创建对象时分配创建者",
"contentManagement.userProfiles.noUpdaterTip": "将在更新对象时设置更新者",
"controls.blockingError": "加载此控件时出错。",
"controls.controlFactoryRegistry.factoryAlreadyExistsError": "已注册类型为 {key} 的控制工厂。",
"controls.controlFactoryRegistry.factoryNotFoundError": "未找到类型为 {key} 的控制工厂",