Navigation: Show list of pinned items on MegaMenu (#90280)

* Navigation: Show list of pinned ites on the navigation

* Rename section to 'Bookmarks'

* Internationalization

* Rename everything to bookmarks

* Update public/app/core/reducers/navBarTree.ts

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Ignore empty message as well

* Dont update navigation if there is an error patching

---------

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Joao Silva 2024-07-22 11:43:40 +01:00 committed by GitHub
parent 38ac0f3506
commit 546f4aa700
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 127 additions and 27 deletions

View File

@ -53,7 +53,7 @@ Content-Type: application/json
"timezone": "utc",
"weekStart": "",
"navbar": {
"savedItemIds": null
"bookmarkIds": null
},
"queryHistory": {
"homeTab": ""
@ -142,7 +142,7 @@ Content-Type: application/json
"timezone": "",
"weekStart": "",
"navbar": {
"savedItemIds": null
"bookmarkIds": null
},
"queryHistory": {
"homeTab": ""

View File

@ -49,7 +49,7 @@ lineage: schemas: [{
} @cuetsy(kind="interface")
#NavbarPreference: {
savedItemIds: [...string]
bookmarkIds: [...string]
} @cuetsy(kind="interface")
}
}]

View File

@ -22,11 +22,11 @@ export interface CookiePreferences {
}
export interface NavbarPreference {
savedItemIds: Array<string>;
bookmarkIds: Array<string>;
}
export const defaultNavbarPreference: Partial<NavbarPreference> = {
savedItemIds: [],
bookmarkIds: [],
};
/**

View File

@ -18,7 +18,7 @@ type CookiePreferences struct {
// NavbarPreference defines model for NavbarPreference.
type NavbarPreference struct {
SavedItemIds []string `json:"savedItemIds"`
BookmarkIds []string `json:"bookmarkIds"`
}
// QueryHistoryPreference defines model for QueryHistoryPreference.

View File

@ -12,6 +12,7 @@ const (
// any items with default weight.
WeightHome = (iota - 20) * 100
WeightBookmarks
WeightSavedItems
WeightDashboard
WeightExplore

View File

@ -163,6 +163,20 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere
return nil, err
}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPinNavItems) {
bookmarks := s.buildBookmarksNavLinks(prefs, treeRoot)
treeRoot.AddSection(&navtree.NavLink{
Text: "Bookmarks",
Id: "bookmarks",
Icon: "bookmark",
SortWeight: navtree.WeightBookmarks,
Children: bookmarks,
EmptyMessageId: "bookmarks-empty",
Url: s.cfg.AppSubURL + "/bookmarks",
})
}
return treeRoot, nil
}
@ -316,6 +330,38 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *contextmodel.ReqContext) ([]*
return starredItemsChildNavs, nil
}
func (s *ServiceImpl) buildBookmarksNavLinks(prefs *pref.Preference, treeRoot *navtree.NavTreeRoot) []*navtree.NavLink {
bookmarksChildNavs := []*navtree.NavLink{}
bookmarkIds := prefs.JSONData.Navbar.BookmarkIds
if len(bookmarkIds) > 0 {
for _, id := range bookmarkIds {
item := treeRoot.FindById(id)
if item != nil {
bookmarksChildNavs = append(bookmarksChildNavs, &navtree.NavLink{
Id: item.Id,
Text: item.Text,
SubTitle: item.SubTitle,
Icon: item.Icon,
Img: item.Img,
Url: item.Url,
Target: item.Target,
HideFromTabs: item.HideFromTabs,
RoundIcon: item.RoundIcon,
IsSection: item.IsSection,
HighlightText: item.HighlightText,
HighlightID: item.HighlightID,
PluginID: item.PluginID,
IsCreateAction: item.IsCreateAction,
Keywords: item.Keywords,
})
}
}
}
return bookmarksChildNavs
}
func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink {
hasAccess := ac.HasAccess(s.accessControl, c)

View File

@ -98,7 +98,7 @@ type QueryHistoryPreference struct {
}
type NavbarPreference struct {
SavedItemIds []string `json:"savedItemIds"`
BookmarkIds []string `json:"bookmarkIds"`
}
func (j *PreferenceJSONData) FromDB(data []byte) error {

View File

@ -97,11 +97,11 @@ func GetPreferencesFor(ctx context.Context,
dto.Language = &preference.JSONData.Language
}
if preference.JSONData.Navbar.SavedItemIds != nil {
if preference.JSONData.Navbar.BookmarkIds != nil {
dto.Navbar = &preferences.NavbarPreference{
SavedItemIds: []string{},
BookmarkIds: []string{},
}
dto.Navbar.SavedItemIds = preference.JSONData.Navbar.SavedItemIds
dto.Navbar.BookmarkIds = preference.JSONData.Navbar.BookmarkIds
}
if preference.JSONData.QueryHistory.HomeTab != "" {

View File

@ -71,8 +71,8 @@ func (s *Service) GetWithDefaults(ctx context.Context, query *pref.GetPreference
res.JSONData.QueryHistory.HomeTab = p.JSONData.QueryHistory.HomeTab
}
if p.JSONData.Navbar.SavedItemIds != nil {
res.JSONData.Navbar.SavedItemIds = p.JSONData.Navbar.SavedItemIds
if p.JSONData.Navbar.BookmarkIds != nil {
res.JSONData.Navbar.BookmarkIds = p.JSONData.Navbar.BookmarkIds
}
if p.JSONData.CookiePreferences != nil {
@ -174,11 +174,11 @@ func (s *Service) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) e
preference.JSONData.Language = *cmd.Language
}
if cmd.Navbar != nil && cmd.Navbar.SavedItemIds != nil {
if cmd.Navbar != nil && cmd.Navbar.BookmarkIds != nil {
if preference.JSONData == nil {
preference.JSONData = &pref.PreferenceJSONData{}
}
preference.JSONData.Navbar.SavedItemIds = cmd.Navbar.SavedItemIds
preference.JSONData.Navbar.BookmarkIds = cmd.Navbar.BookmarkIds
}
if cmd.QueryHistory != nil {

View File

@ -17050,7 +17050,7 @@
"type": "object",
"title": "NavbarPreference defines model for NavbarPreference.",
"properties": {
"savedItemIds": {
"bookmarkIds": {
"type": "array",
"items": {
"type": "string"

View File

@ -3,14 +3,15 @@ import { DOMAttributes } from '@react-types/shared';
import { memo, forwardRef, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, reportInteraction } from '@grafana/runtime';
import { CustomScrollbar, Icon, IconButton, useStyles2, Stack } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { t } from 'app/core/internationalization';
import { setBookmark } from 'app/core/reducers/navBarTree';
import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index';
import { useSelector } from 'app/types';
import { useDispatch, useSelector } from 'app/types';
import { MegaMenuItem } from './MegaMenuItem';
import { usePinnedItems } from './hooks';
@ -28,6 +29,7 @@ export const MegaMenu = memo(
const styles = useStyles2(getStyles);
const location = useLocation();
const { chrome } = useGrafana();
const dispatch = useDispatch();
const state = chrome.useState();
const [patchPreferences] = usePatchUserPreferencesMutation();
const pinnedItems = usePinnedItems();
@ -61,7 +63,8 @@ export const MegaMenu = memo(
[pinnedItems]
);
const onPinItem = (id?: string) => {
const onPinItem = (item: NavModelItem) => {
const id = item.id;
if (id && config.featureToggles.pinNavItems) {
const navItem = navTree.find((item) => item.id === id);
const isSaved = isPinned(id);
@ -73,9 +76,13 @@ export const MegaMenu = memo(
patchPreferences({
patchPrefsCmd: {
navbar: {
savedItemIds: newItems,
bookmarkIds: newItems,
},
},
}).then((data) => {
if (!data.error) {
dispatch(setBookmark({ item: item, isSaved: !isSaved }));
}
});
}
};

View File

@ -19,7 +19,7 @@ interface Props {
activeItem?: NavModelItem;
onClick?: () => void;
level?: number;
onPin: (id?: string) => void;
onPin: (item: NavModelItem) => void;
isPinned: (id?: string) => boolean;
}
@ -105,7 +105,7 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPi
target={link.target}
url={link.url}
id={link.id}
onPin={onPin}
onPin={() => onPin(link)}
isPinned={isPinned(link.id)}
>
<div

View File

@ -32,7 +32,8 @@ export function MegaMenuItemText({ children, isActive, onClick, target, url, id,
}
{config.featureToggles.pinNavItems && (
<Icon
name={isPinned ? 'favorite' : 'star'}
name="bookmark"
type={isPinned ? 'solid' : 'default'}
className={'pin-icon'}
onClick={(e) => {
e.preventDefault();

View File

@ -5,7 +5,7 @@ import { useGetUserPreferencesQuery } from 'app/features/preferences/api';
export const usePinnedItems = () => {
const preferences = useGetUserPreferencesQuery();
const pinnedItems = useMemo(() => preferences.data?.navbar?.savedItemIds || [], [preferences]);
const pinnedItems = useMemo(() => preferences.data?.navbar?.bookmarkIds || [], [preferences]);
if (config.featureToggles.pinNavItems) {
return pinnedItems;

View File

@ -51,6 +51,31 @@ const navTreeSlice = createSlice({
}
}
},
setBookmark: (state, action: PayloadAction<{ item: NavModelItem; isSaved: boolean }>) => {
if (!config.featureToggles.pinNavItems) {
return;
}
const bookmarks = state.find((navItem) => navItem.id === 'bookmarks');
const { item, isSaved } = action.payload;
if (bookmarks) {
if (isSaved) {
if (!bookmarks.children) {
bookmarks.children = [];
}
const newBookmark: NavModelItem = {
...item,
// Clear the children, sortWeight and empty message of the item
children: [],
sortWeight: 0,
emptyMessageId: '',
emptyMessage: '',
};
bookmarks.children.push(newBookmark);
} else {
bookmarks.children = bookmarks.children?.filter((i) => i.id !== item.id) ?? [];
}
}
},
updateDashboardName: (state, action: PayloadAction<{ id: string; title: string; url: string }>) => {
const { id, title, url } = action.payload;
const starredItems = state.find((navItem) => navItem.id === 'starred');
@ -73,5 +98,5 @@ const navTreeSlice = createSlice({
},
});
export const { setStarred, removePluginFromNavTree, updateDashboardName } = navTreeSlice.actions;
export const { setStarred, removePluginFromNavTree, updateDashboardName, setBookmark } = navTreeSlice.actions;
export const navTreeReducer = navTreeSlice.reducer;

View File

@ -22,6 +22,10 @@ export function getNavTitle(navId: string | undefined) {
return t('nav.create-import.title', 'Import dashboard');
case 'alert':
return t('nav.create-alert.title', 'New alert rule');
case 'bookmarks':
return t('nav.bookmarks.title', 'Bookmarks');
case 'bookmarks-empty':
return t('nav.bookmarks-empty.title', 'Bookmark pages for them to appear here');
case 'starred':
return t('nav.starred.title', 'Starred');
case 'starred-empty':

View File

@ -38,7 +38,7 @@ export type CookiePreferencesDefinesModelForCookiePreferences = {
};
};
export type NavbarPreferenceDefinesModelForNavbarPreference = {
savedItemIds?: string[];
bookmarkIds?: string[];
};
export type QueryHistoryPreferenceDefinesModelForQueryHistoryPreference = {
/** HomeTab one of: '' | 'query' | 'starred'; */
@ -66,7 +66,7 @@ export type ErrorResponseBody = {
/** a human readable version of the error */
message: string;
/** Status An optional status to denote the cause of the error.
For example, a 412 Precondition Failed error may include additional information of why that error happened. */
status?: string;
};

View File

@ -514,6 +514,10 @@ export function getAppRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "DataTrailsPage"*/ 'app/features/trails/DataTrailsPage')
),
},
{
path: '/bookmarks',
component: () => <NavLandingPage navId="bookmarks" />,
},
...getPluginCatalogRoutes(),
...getSupportBundleRoutes(),
...getAlertingRoutes(),

View File

@ -1188,6 +1188,12 @@
"authentication": {
"title": "Authentication"
},
"bookmarks": {
"title": "Bookmarks"
},
"bookmarks-empty": {
"title": "Bookmark pages for them to appear here"
},
"collector": {
"title": "Collector"
},

View File

@ -1188,6 +1188,12 @@
"authentication": {
"title": "Åūŧĥęʼnŧįčäŧįőʼn"
},
"bookmarks": {
"title": "ßőőĸmäřĸş"
},
"bookmarks-empty": {
"title": "ßőőĸmäřĸ päģęş ƒőř ŧĥęm ŧő äppęäř ĥęřę"
},
"collector": {
"title": "Cőľľęčŧőř"
},

View File

@ -7146,7 +7146,7 @@
},
"NavbarPreference": {
"properties": {
"savedItemIds": {
"bookmarkIds": {
"items": {
"type": "string"
},