-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow to "star" (favorite) a dashboard from the listing table (#189285)
## Summary close elastic/kibana-team#949 - Allows to "star" (favorite) a dashboard from the listing table ![Screenshot 2024-07-26 at 15 17 41](https://github.com/user-attachments/assets/18f8e3d6-3c83-4d62-8a70-811b05ecd99b) ![Screenshot 2024-07-26 at 15 17 45](https://github.com/user-attachments/assets/45462395-1db1-4858-a2d8-3f681bb2072b) - Favorites are isolated per user (user profile id) and per space ### Implementation Details Please refer to and comment on the README.md 🙏 https://github.com/elastic/kibana/pull/189285/files#diff-307fab4354532049891c828da893b4efcf0df9391b1f3018d8d016a2288c5d4c ### TODO - Telemetry: I will add telemetry in a separate PR
- Loading branch information
Showing
57 changed files
with
1,511 additions
and
39 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
--- | ||
id: sharedUX/Favorites | ||
slug: /shared-ux/favorites | ||
title: Favorites Service | ||
description: A service and a set of components and hooks for implementing content favorites | ||
tags: ['shared-ux', 'component'] | ||
date: 2024-07-26 | ||
--- | ||
|
||
## Description | ||
|
||
The Favorites service provides a way to add favorites feature to your content. It includes a service for managing the list of favorites and a set of components for displaying and interacting with the list. | ||
|
||
- The favorites are isolated per user, per space. | ||
- The service provides an API for adding, removing, and listing favorites. | ||
- The service provides a set of react-query hooks for interacting with the favorites list | ||
- The components include a button for toggling the favorite state of an object | ||
- The service relies on ambiguous object ids to identify the objects being favorite. This allows the service to be used with any type of content, not just saved objects. | ||
|
||
## API | ||
|
||
```tsx | ||
// client side | ||
import { | ||
FavoritesClient, | ||
FavoritesContextProvider, | ||
useFavorites, | ||
FavoriteButton, | ||
} from '@kbn/content-management-favorites-public'; | ||
|
||
const favoriteObjectType = 'dashboard'; | ||
const favoritesClient = new FavoritesClient('dashboard', { | ||
http: core.http, | ||
}); | ||
|
||
// wrap your content with the favorites context provider | ||
const myApp = () => { | ||
<FavoritesContextProvider favoritesClient={favoritesClient}> | ||
<App /> | ||
</FavoritesContextProvider>; | ||
}; | ||
|
||
const App = () => { | ||
// get the favorites list | ||
const favoritesQuery = useFavorites(); | ||
|
||
// display favorite state and toggle button for an object | ||
return <FavoriteButton id={'some-object-id'} />; | ||
}; | ||
``` | ||
|
||
## Implementation Details | ||
|
||
Internally the favorites list is backed by a saved object. A saved object of type "favorites" is created for each user (user profile id) and space (space id) and object type (e.g. dashboard) combination when a user for the first time favorites an object. The saved object contains a list of favorite objects of the type. | ||
|
||
``` | ||
{ | ||
"_index": ".kibana_8.16.0_001", | ||
"_id": "spaceid:favorites:object_type:u_profile_id", | ||
"_source": { | ||
"favorites": { | ||
"userId": "u_profile_id", | ||
"type: "dashboard", | ||
"favoriteIds": [ | ||
"dashboard_id_1", | ||
"dashboard_id_2", | ||
] | ||
}, | ||
"type": "favorites", | ||
"references": [], | ||
"namespace": "spaceid", | ||
} | ||
}, | ||
``` | ||
|
||
The service doesn't track the favorite object itself, only the object id. When the object is deleted, the favorite isn't removed from the list automatically. |
4 changes: 4 additions & 0 deletions
4
packages/content-management/favorites/favorites_public/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# @kbn/content-management-favorites-public | ||
|
||
Client-side code for the favorites feature | ||
Meant be used in conjunction with the `@kbn/content-management-favorites-server` package. |
19 changes: 19 additions & 0 deletions
19
packages/content-management/favorites/favorites_public/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { type FavoritesClientPublic, FavoritesClient } from './src/favorites_client'; | ||
export { FavoritesContextProvider } from './src/favorites_context'; | ||
export { useFavorites } from './src/favorites_query'; | ||
|
||
export { | ||
FavoriteButton, | ||
type FavoriteButtonProps, | ||
cssFavoriteHoverWithinEuiTableRow, | ||
} from './src/components/favorite_button'; | ||
|
||
export { FavoritesEmptyState } from './src/components/favorites_empty_state'; |
13 changes: 13 additions & 0 deletions
13
packages/content-management/favorites/favorites_public/jest.config.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/favorites/favorites_public'], | ||
}; |
5 changes: 5 additions & 0 deletions
5
packages/content-management/favorites/favorites_public/kibana.jsonc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"type": "shared-browser", | ||
"id": "@kbn/content-management-favorites-public", | ||
"owner": "@elastic/appex-sharedux" | ||
} |
6 changes: 6 additions & 0 deletions
6
packages/content-management/favorites/favorites_public/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"name": "@kbn/content-management-favorites-public", | ||
"private": true, | ||
"version": "1.0.0", | ||
"license": "SSPL-1.0 OR Elastic License 2.0" | ||
} |
1 change: 1 addition & 0 deletions
1
...t-management/favorites/favorites_public/src/components/empty_favorites_dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions
1
...-management/favorites/favorites_public/src/components/empty_favorites_light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 additions & 0 deletions
94
packages/content-management/favorites/favorites_public/src/components/favorite_button.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* | ||
* 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 { i18n } from '@kbn/i18n'; | ||
import classNames from 'classnames'; | ||
import { EuiButtonIcon, euiCanAnimate, EuiThemeComputed } from '@elastic/eui'; | ||
import { css } from '@emotion/react'; | ||
import { useFavorites, useRemoveFavorite, useAddFavorite } from '../favorites_query'; | ||
|
||
export interface FavoriteButtonProps { | ||
id: string; | ||
className?: string; | ||
} | ||
|
||
export const FavoriteButton = ({ id, className }: FavoriteButtonProps) => { | ||
const { data } = useFavorites(); | ||
|
||
const removeFavorite = useRemoveFavorite(); | ||
const addFavorite = useAddFavorite(); | ||
|
||
if (!data) return null; | ||
|
||
const isFavorite = data.favoriteIds.includes(id); | ||
|
||
if (isFavorite) { | ||
const title = i18n.translate('contentManagement.favorites.unfavoriteButtonLabel', { | ||
defaultMessage: 'Remove from Starred', | ||
}); | ||
|
||
return ( | ||
<EuiButtonIcon | ||
isLoading={removeFavorite.isLoading} | ||
title={title} | ||
aria-label={title} | ||
iconType={'starFilled'} | ||
onClick={() => { | ||
removeFavorite.mutate({ id }); | ||
}} | ||
className={classNames(className, 'cm-favorite-button', { | ||
'cm-favorite-button--active': !removeFavorite.isLoading, | ||
})} | ||
data-test-subj="unfavoriteButton" | ||
/> | ||
); | ||
} else { | ||
const title = i18n.translate('contentManagement.favorites.favoriteButtonLabel', { | ||
defaultMessage: 'Add to Starred', | ||
}); | ||
return ( | ||
<EuiButtonIcon | ||
isLoading={addFavorite.isLoading} | ||
title={title} | ||
aria-label={title} | ||
iconType={'starEmpty'} | ||
onClick={() => { | ||
addFavorite.mutate({ id }); | ||
}} | ||
className={classNames(className, 'cm-favorite-button', { | ||
'cm-favorite-button--empty': !addFavorite.isLoading, | ||
})} | ||
data-test-subj="favoriteButton" | ||
/> | ||
); | ||
} | ||
}; | ||
|
||
/** | ||
* CSS to apply to euiTable to show the favorite button on hover or when active | ||
* @param euiTheme | ||
*/ | ||
export const cssFavoriteHoverWithinEuiTableRow = (euiTheme: EuiThemeComputed) => css` | ||
@media (hover: hover) { | ||
.euiTableRow .cm-favorite-button--empty { | ||
visibility: hidden; | ||
opacity: 0; | ||
${euiCanAnimate} { | ||
transition: opacity ${euiTheme.animation.fast} ${euiTheme.animation.resistance}; | ||
} | ||
} | ||
.euiTableRow:hover, | ||
.euiTableRow:focus-within { | ||
.cm-favorite-button--empty { | ||
visibility: visible; | ||
opacity: 1; | ||
} | ||
} | ||
} | ||
`; |
88 changes: 88 additions & 0 deletions
88
...es/content-management/favorites/favorites_public/src/components/favorites_empty_state.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/* | ||
* 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 { i18n } from '@kbn/i18n'; | ||
import { EuiEmptyPrompt, useEuiTheme, EuiImage, EuiMarkdownFormat } from '@elastic/eui'; | ||
import { css } from '@emotion/react'; | ||
import React from 'react'; | ||
import emptyFavoritesDark from './empty_favorites_dark.svg'; | ||
import emptyFavoritesLight from './empty_favorites_light.svg'; | ||
|
||
export const FavoritesEmptyState = ({ | ||
emptyStateType = 'noItems', | ||
entityNamePlural = i18n.translate('contentManagement.favorites.defaultEntityNamePlural', { | ||
defaultMessage: 'items', | ||
}), | ||
entityName = i18n.translate('contentManagement.favorites.defaultEntityName', { | ||
defaultMessage: 'item', | ||
}), | ||
}: { | ||
emptyStateType: 'noItems' | 'noMatchingItems'; | ||
entityNamePlural?: string; | ||
entityName?: string; | ||
}) => { | ||
const title = | ||
emptyStateType === 'noItems' ? ( | ||
<FormattedMessage | ||
id="contentManagement.favorites.noFavoritesMessageHeading" | ||
defaultMessage="You haven’t starred any {entityNamePlural}" | ||
values={{ entityNamePlural }} | ||
/> | ||
) : ( | ||
<FormattedMessage | ||
id="contentManagement.favorites.noMatchingFavoritesMessageHeading" | ||
defaultMessage="No starred {entityNamePlural} match your search" | ||
values={{ entityNamePlural }} | ||
/> | ||
); | ||
|
||
return ( | ||
<EuiEmptyPrompt | ||
css={css` | ||
.euiEmptyPrompt__icon { | ||
min-inline-size: 25%; /* reduce the min size of the container to fit more title in a single line* / | ||
} | ||
`} | ||
layout="horizontal" | ||
color="transparent" | ||
icon={<NoFavoritesIllustration />} | ||
hasBorder={false} | ||
title={<h2>{title}</h2>} | ||
body={ | ||
<EuiMarkdownFormat> | ||
{i18n.translate('contentManagement.favorites.noFavoritesMessageBody', { | ||
defaultMessage: | ||
"Keep track of your most important {entityNamePlural} by adding them to your **Starred** list. Click the **{starIcon}** **star icon** next to a {entityName} name and it'll appear in this tab.", | ||
values: { entityNamePlural, entityName, starIcon: `✩` }, | ||
})} | ||
</EuiMarkdownFormat> | ||
} | ||
/> | ||
); | ||
}; | ||
|
||
const NoFavoritesIllustration = () => { | ||
const { colorMode } = useEuiTheme(); | ||
|
||
const src = colorMode === 'DARK' ? emptyFavoritesDark : emptyFavoritesLight; | ||
|
||
return ( | ||
<EuiImage | ||
style={{ | ||
width: 300, | ||
height: 220, | ||
objectFit: 'contain', | ||
}} /* we use fixed width to prevent layout shift */ | ||
src={src} | ||
alt={i18n.translate('contentManagement.favorites.noFavoritesIllustrationAlt', { | ||
defaultMessage: 'No starred items illustrations', | ||
})} | ||
/> | ||
); | ||
}; |
42 changes: 42 additions & 0 deletions
42
packages/content-management/favorites/favorites_public/src/favorites_client.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* 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 type { HttpStart } from '@kbn/core-http-browser'; | ||
import type { GetFavoritesResponse } from '@kbn/content-management-favorites-server'; | ||
|
||
export interface FavoritesClientPublic { | ||
getFavorites(): Promise<GetFavoritesResponse>; | ||
addFavorite({ id }: { id: string }): Promise<GetFavoritesResponse>; | ||
removeFavorite({ id }: { id: string }): Promise<GetFavoritesResponse>; | ||
|
||
getFavoriteType(): string; | ||
} | ||
|
||
export class FavoritesClient implements FavoritesClientPublic { | ||
constructor(private favoriteObjectType: string, private deps: { http: HttpStart }) {} | ||
|
||
public async getFavorites(): Promise<GetFavoritesResponse> { | ||
return this.deps.http.get(`/internal/content_management/favorites/${this.favoriteObjectType}`); | ||
} | ||
|
||
public async addFavorite({ id }: { id: string }): Promise<GetFavoritesResponse> { | ||
return this.deps.http.post( | ||
`/internal/content_management/favorites/${this.favoriteObjectType}/${id}/favorite` | ||
); | ||
} | ||
|
||
public async removeFavorite({ id }: { id: string }): Promise<GetFavoritesResponse> { | ||
return this.deps.http.post( | ||
`/internal/content_management/favorites/${this.favoriteObjectType}/${id}/unfavorite` | ||
); | ||
} | ||
|
||
public getFavoriteType() { | ||
return this.favoriteObjectType; | ||
} | ||
} |
Oops, something went wrong.