From 546f4aa70080824cb1624b719c6c0ac959bf4420 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:43:40 +0100 Subject: [PATCH] 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 * Ignore empty message as well * Dont update navigation if there is an error patching --------- Co-authored-by: Alex Khomenko --- .../developers/http_api/preferences.md | 4 +- kinds/preferences/preferences_kind.cue | 2 +- .../preferences/x/preferences_types.gen.ts | 4 +- pkg/kinds/preferences/preferences_spec_gen.go | 2 +- pkg/services/navtree/models.go | 1 + pkg/services/navtree/navtreeimpl/navtree.go | 46 +++++++++++++++++++ pkg/services/preference/model.go | 2 +- pkg/services/preference/prefapi/api.go | 6 +-- pkg/services/preference/prefimpl/pref.go | 8 ++-- public/api-merged.json | 2 +- .../AppChrome/MegaMenu/MegaMenu.tsx | 15 ++++-- .../AppChrome/MegaMenu/MegaMenuItem.tsx | 4 +- .../AppChrome/MegaMenu/MegaMenuItemText.tsx | 3 +- .../components/AppChrome/MegaMenu/hooks.ts | 2 +- public/app/core/reducers/navBarTree.ts | 27 ++++++++++- .../app/core/utils/navBarItem-translations.ts | 4 ++ .../preferences/api/user/endpoints.gen.ts | 4 +- public/app/routes/routes.tsx | 4 ++ public/locales/en-US/grafana.json | 6 +++ public/locales/pseudo-LOCALE/grafana.json | 6 +++ public/openapi3.json | 2 +- 21 files changed, 127 insertions(+), 27 deletions(-) diff --git a/docs/sources/developers/http_api/preferences.md b/docs/sources/developers/http_api/preferences.md index 0d869d3c353..9841b26569a 100644 --- a/docs/sources/developers/http_api/preferences.md +++ b/docs/sources/developers/http_api/preferences.md @@ -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": "" diff --git a/kinds/preferences/preferences_kind.cue b/kinds/preferences/preferences_kind.cue index dd97beb4b0c..87ca5b76864 100644 --- a/kinds/preferences/preferences_kind.cue +++ b/kinds/preferences/preferences_kind.cue @@ -49,7 +49,7 @@ lineage: schemas: [{ } @cuetsy(kind="interface") #NavbarPreference: { - savedItemIds: [...string] + bookmarkIds: [...string] } @cuetsy(kind="interface") } }] diff --git a/packages/grafana-schema/src/raw/preferences/x/preferences_types.gen.ts b/packages/grafana-schema/src/raw/preferences/x/preferences_types.gen.ts index 9d9953e10d9..eb0b2b11c7e 100644 --- a/packages/grafana-schema/src/raw/preferences/x/preferences_types.gen.ts +++ b/packages/grafana-schema/src/raw/preferences/x/preferences_types.gen.ts @@ -22,11 +22,11 @@ export interface CookiePreferences { } export interface NavbarPreference { - savedItemIds: Array; + bookmarkIds: Array; } export const defaultNavbarPreference: Partial = { - savedItemIds: [], + bookmarkIds: [], }; /** diff --git a/pkg/kinds/preferences/preferences_spec_gen.go b/pkg/kinds/preferences/preferences_spec_gen.go index 7d0dd39cfc1..96506431def 100644 --- a/pkg/kinds/preferences/preferences_spec_gen.go +++ b/pkg/kinds/preferences/preferences_spec_gen.go @@ -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. diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go index 59bd2fff3f4..963f5364d9e 100644 --- a/pkg/services/navtree/models.go +++ b/pkg/services/navtree/models.go @@ -12,6 +12,7 @@ const ( // any items with default weight. WeightHome = (iota - 20) * 100 + WeightBookmarks WeightSavedItems WeightDashboard WeightExplore diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 57962943e20..e3805fffb83 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -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) diff --git a/pkg/services/preference/model.go b/pkg/services/preference/model.go index 674f5895472..ac74f7878af 100644 --- a/pkg/services/preference/model.go +++ b/pkg/services/preference/model.go @@ -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 { diff --git a/pkg/services/preference/prefapi/api.go b/pkg/services/preference/prefapi/api.go index 51d2718f419..191a35c546a 100644 --- a/pkg/services/preference/prefapi/api.go +++ b/pkg/services/preference/prefapi/api.go @@ -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 != "" { diff --git a/pkg/services/preference/prefimpl/pref.go b/pkg/services/preference/prefimpl/pref.go index 0de2069efed..48c2932b78c 100644 --- a/pkg/services/preference/prefimpl/pref.go +++ b/pkg/services/preference/prefimpl/pref.go @@ -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 { diff --git a/public/api-merged.json b/public/api-merged.json index 81ca8dfc0fe..f49a88d8a44 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17050,7 +17050,7 @@ "type": "object", "title": "NavbarPreference defines model for NavbarPreference.", "properties": { - "savedItemIds": { + "bookmarkIds": { "type": "array", "items": { "type": "string" diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index 6de0eb2d84f..1c783f55e15 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -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 })); + } }); } }; diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx index ee5d85cc97c..3cd54142d75 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx @@ -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)} >
{ e.preventDefault(); diff --git a/public/app/core/components/AppChrome/MegaMenu/hooks.ts b/public/app/core/components/AppChrome/MegaMenu/hooks.ts index 0334df81fd0..ebb6f1437b1 100644 --- a/public/app/core/components/AppChrome/MegaMenu/hooks.ts +++ b/public/app/core/components/AppChrome/MegaMenu/hooks.ts @@ -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; diff --git a/public/app/core/reducers/navBarTree.ts b/public/app/core/reducers/navBarTree.ts index e4e81bb3c8b..88d70dba538 100644 --- a/public/app/core/reducers/navBarTree.ts +++ b/public/app/core/reducers/navBarTree.ts @@ -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; diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index 47339ea0dab..5589056e0cf 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -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': diff --git a/public/app/features/preferences/api/user/endpoints.gen.ts b/public/app/features/preferences/api/user/endpoints.gen.ts index e5aff73929c..99165c810bd 100644 --- a/public/app/features/preferences/api/user/endpoints.gen.ts +++ b/public/app/features/preferences/api/user/endpoints.gen.ts @@ -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; }; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index e1eef8c4b29..ae471afb03a 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -514,6 +514,10 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "DataTrailsPage"*/ 'app/features/trails/DataTrailsPage') ), }, + { + path: '/bookmarks', + component: () => , + }, ...getPluginCatalogRoutes(), ...getSupportBundleRoutes(), ...getAlertingRoutes(), diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d554908eb79..42936e7047f 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1188,6 +1188,12 @@ "authentication": { "title": "Authentication" }, + "bookmarks": { + "title": "Bookmarks" + }, + "bookmarks-empty": { + "title": "Bookmark pages for them to appear here" + }, "collector": { "title": "Collector" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index cf684d502de..9ad38614679 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1188,6 +1188,12 @@ "authentication": { "title": "Åūŧĥęʼnŧįčäŧįőʼn" }, + "bookmarks": { + "title": "ßőőĸmäřĸş" + }, + "bookmarks-empty": { + "title": "ßőőĸmäřĸ päģęş ƒőř ŧĥęm ŧő äppęäř ĥęřę" + }, "collector": { "title": "Cőľľęčŧőř" }, diff --git a/public/openapi3.json b/public/openapi3.json index ac6f215551d..569ca3882ae 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7146,7 +7146,7 @@ }, "NavbarPreference": { "properties": { - "savedItemIds": { + "bookmarkIds": { "items": { "type": "string" },