Navigation: Display Starred dashboards in the Navbar (#51038)

* remove feature toggle, add empty state and fix bug with initial starring

* Extract empty message into lingui

* remove full stop

* add empty message in the backend

* remove unused import

* translate starred + empty starred states

* betterer changes
This commit is contained in:
Ashley Harrison 2022-06-27 15:41:00 +01:00 committed by GitHub
parent 4a9872d108
commit ee3f4f1709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 110 additions and 50 deletions

View File

@ -3233,9 +3233,9 @@ exports[`better eslint`] = {
[120, 15, 41, "Do not use any type assertions.", "1904113237"], [120, 15, 41, "Do not use any type assertions.", "1904113237"],
[137, 15, 41, "Do not use any type assertions.", "1904113237"] [137, 15, 41, "Do not use any type assertions.", "1904113237"]
], ],
"public/app/core/components/NavBar/NavBarMenu.tsx:2095049018": [ "public/app/core/components/NavBar/NavBarMenu.tsx:2778426907": [
[241, 54, 26, "Do not use any type assertions.", "1541386804"], [245, 54, 26, "Do not use any type assertions.", "1541386804"],
[445, 23, 21, "Do not use any type assertions.", "1121249950"] [463, 23, 21, "Do not use any type assertions.", "1121249950"]
], ],
"public/app/core/components/OptionsUI/DashboardPicker.tsx:3166151962": [ "public/app/core/components/OptionsUI/DashboardPicker.tsx:3166151962": [
[11, 85, 3, "Unexpected any. Specify a different type.", "193409811"] [11, 85, 3, "Unexpected any. Specify a different type.", "193409811"]

View File

@ -47,7 +47,6 @@ export interface FeatureToggles {
explore2Dashboard?: boolean; explore2Dashboard?: boolean;
tracing?: boolean; tracing?: boolean;
commandPalette?: boolean; commandPalette?: boolean;
savedItems?: boolean;
cloudWatchDynamicLabels?: boolean; cloudWatchDynamicLabels?: boolean;
datasourceQueryMultiStatus?: boolean; datasourceQueryMultiStatus?: boolean;
azureMonitorExperimentalUI?: boolean; azureMonitorExperimentalUI?: boolean;

View File

@ -16,6 +16,7 @@ export interface NavLinkDTO {
hideFromTabs?: boolean; hideFromTabs?: boolean;
children?: NavLinkDTO[]; children?: NavLinkDTO[];
highlightText?: string; highlightText?: string;
emptyMessageId?: string;
} }
export interface NavModelItem extends NavLinkDTO { export interface NavModelItem extends NavLinkDTO {

View File

@ -74,6 +74,7 @@ type NavLink struct {
Children []*NavLink `json:"children,omitempty"` Children []*NavLink `json:"children,omitempty"`
HighlightText string `json:"highlightText,omitempty"` HighlightText string `json:"highlightText,omitempty"`
HighlightID string `json:"highlightId,omitempty"` HighlightID string `json:"highlightId,omitempty"`
EmptyMessageId string `json:"emptyMessageId,omitempty"`
} }
// NavIDCfg is the id for org configuration navigation node // NavIDCfg is the id for org configuration navigation node

View File

@ -163,22 +163,21 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
hasAccess := ac.HasAccess(hs.AccessControl, c) hasAccess := ac.HasAccess(hs.AccessControl, c)
navTree := []*dtos.NavLink{} navTree := []*dtos.NavLink{}
if hs.Features.IsEnabled(featuremgmt.FlagSavedItems) { starredItemsLinks, err := hs.buildStarredItemsNavLinks(c, prefs)
starredItemsLinks, err := hs.buildStarredItemsNavLinks(c, prefs) if err != nil {
if err != nil { return nil, err
return nil, err
}
navTree = append(navTree, &dtos.NavLink{
Text: "Starred",
Id: "starred",
Icon: "star",
SortWeight: dtos.WeightSavedItems,
Section: dtos.NavSectionCore,
Children: starredItemsLinks,
})
} }
navTree = append(navTree, &dtos.NavLink{
Text: "Starred",
Id: "starred",
Icon: "star",
SortWeight: dtos.WeightSavedItems,
Section: dtos.NavSectionCore,
Children: starredItemsLinks,
EmptyMessageId: "starred-empty",
})
dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm) dashboardChildLinks := hs.buildDashboardNavLinks(c, hasEditPerm)
dashboardsUrl := "/dashboards" dashboardsUrl := "/dashboards"

View File

@ -178,11 +178,6 @@ var (
Description: "Enable command palette", Description: "Enable command palette",
State: FeatureStateAlpha, State: FeatureStateAlpha,
}, },
{
Name: "savedItems",
Description: "Enable Saved Items in the navbar.",
State: FeatureStateAlpha,
},
{ {
Name: "cloudWatchDynamicLabels", Name: "cloudWatchDynamicLabels",
Description: "Use dynamic labels instead of alias patterns in CloudWatch datasource", Description: "Use dynamic labels instead of alias patterns in CloudWatch datasource",

View File

@ -131,10 +131,6 @@ const (
// Enable command palette // Enable command palette
FlagCommandPalette = "commandPalette" FlagCommandPalette = "commandPalette"
// FlagSavedItems
// Enable Saved Items in the navbar.
FlagSavedItems = "savedItems"
// FlagCloudWatchDynamicLabels // FlagCloudWatchDynamicLabels
// Use dynamic labels instead of alias patterns in CloudWatch datasource // Use dynamic labels instead of alias patterns in CloudWatch datasource
FlagCloudWatchDynamicLabels = "cloudWatchDynamicLabels" FlagCloudWatchDynamicLabels = "cloudWatchDynamicLabels"

View File

@ -1,4 +1,5 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useLingui } from '@lingui/react';
import { useMenu } from '@react-aria/menu'; import { useMenu } from '@react-aria/menu';
import { mergeProps } from '@react-aria/utils'; import { mergeProps } from '@react-aria/utils';
import { useTreeState } from '@react-stately/tree'; import { useTreeState } from '@react-stately/tree';
@ -11,6 +12,7 @@ import { useTheme2 } from '@grafana/ui';
import { NavBarItemMenuItem } from './NavBarItemMenuItem'; import { NavBarItemMenuItem } from './NavBarItemMenuItem';
import { NavBarScrollContainer } from './NavBarScrollContainer'; import { NavBarScrollContainer } from './NavBarScrollContainer';
import { useNavBarItemMenuContext } from './context'; import { useNavBarItemMenuContext } from './context';
import menuItemTranslations from './navBarItem-translations';
import { getNavModelItemKey } from './utils'; import { getNavModelItemKey } from './utils';
export interface NavBarItemMenuProps extends SpectrumMenuProps<NavModelItem> { export interface NavBarItemMenuProps extends SpectrumMenuProps<NavModelItem> {
@ -21,6 +23,7 @@ export interface NavBarItemMenuProps extends SpectrumMenuProps<NavModelItem> {
export function NavBarItemMenu(props: NavBarItemMenuProps): ReactElement | null { export function NavBarItemMenu(props: NavBarItemMenuProps): ReactElement | null {
const { reverseMenuDirection, adjustHeightForBorder, disabledKeys, onNavigate, ...rest } = props; const { reverseMenuDirection, adjustHeightForBorder, disabledKeys, onNavigate, ...rest } = props;
const { i18n } = useLingui();
const contextProps = useNavBarItemMenuContext(); const contextProps = useNavBarItemMenuContext();
const completeProps = { const completeProps = {
...mergeProps(contextProps, rest), ...mergeProps(contextProps, rest),
@ -58,6 +61,15 @@ export function NavBarItemMenu(props: NavBarItemMenuProps): ReactElement | null
<NavBarItemMenuItem key={getNavModelItemKey(item.value)} item={item} state={state} onNavigate={onNavigate} /> <NavBarItemMenuItem key={getNavModelItemKey(item.value)} item={item} state={state} onNavigate={onNavigate} />
)); ));
if (itemComponents.length === 0 && section.value.emptyMessageId) {
const emptyMessageTranslated = i18n._(menuItemTranslations[section.value.emptyMessageId]);
itemComponents.push(
<div key="empty-message" className={styles.emptyMessage}>
{emptyMessageTranslated}
</div>
);
}
const subTitleComponent = menuSubTitle && ( const subTitleComponent = menuSubTitle && (
<li key={menuSubTitle} className={styles.subtitle}> <li key={menuSubTitle} className={styles.subtitle}>
{menuSubTitle} {menuSubTitle}
@ -105,5 +117,9 @@ function getStyles(theme: GrafanaTheme2, reverseDirection?: boolean) {
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
`, `,
emptyMessage: css`
font-style: italic;
padding: ${theme.spacing(0.5, 2)};
`,
}; };
} }

View File

@ -88,11 +88,5 @@ function getStyles(theme: GrafanaTheme2, isFocused: boolean, isSection: boolean)
transition: none; transition: none;
} }
`, `,
upgradeBoxContainer: css`
padding: ${theme.spacing(1)};
`,
upgradeBox: css`
width: 300px;
`,
}; };
} }

View File

@ -1,4 +1,5 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useLingui } from '@lingui/react';
import { useDialog } from '@react-aria/dialog'; import { useDialog } from '@react-aria/dialog';
import { FocusScope } from '@react-aria/focus'; import { FocusScope } from '@react-aria/focus';
import { OverlayContainer, useOverlay } from '@react-aria/overlays'; import { OverlayContainer, useOverlay } from '@react-aria/overlays';
@ -16,6 +17,7 @@ import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
import { NavBarMenuItem } from './NavBarMenuItem'; import { NavBarMenuItem } from './NavBarMenuItem';
import { NavBarToggle } from './NavBarToggle'; import { NavBarToggle } from './NavBarToggle';
import { NavFeatureHighlight } from './NavFeatureHighlight'; import { NavFeatureHighlight } from './NavFeatureHighlight';
import menuItemTranslations from './navBarItem-translations';
import { isMatchOrChildMatch } from './utils'; import { isMatchOrChildMatch } from './utils';
const MENU_WIDTH = '350px'; const MENU_WIDTH = '350px';
@ -35,6 +37,7 @@ export function NavBarMenu({ activeItem, isOpen, navItems, onClose, setMenuAnima
const animStyles = getAnimStyles(theme, ANIMATION_DURATION); const animStyles = getAnimStyles(theme, ANIMATION_DURATION);
const ref = useRef(null); const ref = useRef(null);
const { dialogProps } = useDialog({}, ref); const { dialogProps } = useDialog({}, ref);
const { overlayProps, underlayProps } = useOverlay( const { overlayProps, underlayProps } = useOverlay(
{ {
isDismissable: true, isDismissable: true,
@ -226,6 +229,7 @@ function NavItem({
activeItem?: NavModelItem; activeItem?: NavModelItem;
onClose: () => void; onClose: () => void;
}) { }) {
const { i18n } = useLingui();
const styles = useStyles2(getNavItemStyles); const styles = useStyles2(getNavItemStyles);
if (linkHasChildren(link)) { if (linkHasChildren(link)) {
@ -255,6 +259,15 @@ function NavItem({
</ul> </ul>
</CollapsibleNavItem> </CollapsibleNavItem>
); );
} else if (link.emptyMessageId) {
const emptyMessageTranslated = i18n._(menuItemTranslations[link.emptyMessageId]);
return (
<CollapsibleNavItem onClose={onClose} link={link} isActive={isMatchOrChildMatch(link, activeItem)}>
<ul className={styles.children}>
<div className={styles.emptyMessage}>{emptyMessageTranslated}</div>
</ul>
</CollapsibleNavItem>
);
} else { } else {
const FeatureHighlightWrapper = link.highlightText ? NavFeatureHighlight : React.Fragment; const FeatureHighlightWrapper = link.highlightText ? NavFeatureHighlight : React.Fragment;
return ( return (
@ -326,6 +339,11 @@ const getNavItemStyles = (theme: GrafanaTheme2) => ({
justifySelf: 'start', justifySelf: 'start',
padding: theme.spacing(0.5, 4.25, 0.5, 0.5), padding: theme.spacing(0.5, 4.25, 0.5, 0.5),
}), }),
emptyMessage: css({
color: theme.colors.text.secondary,
fontStyle: 'italic',
padding: theme.spacing(1, 1.5),
}),
}); });
function CollapsibleNavItem({ function CollapsibleNavItem({

View File

@ -15,6 +15,8 @@ const TRANSLATED_MENU_ITEMS: Record<string, MessageDescriptor> = {
import: defineMessage({ id: 'nav.create-import', message: 'Import' }), import: defineMessage({ id: 'nav.create-import', message: 'Import' }),
alert: defineMessage({ id: 'nav.create-alert', message: 'New alert rule' }), alert: defineMessage({ id: 'nav.create-alert', message: 'New alert rule' }),
starred: defineMessage({ id: 'nav.starred', message: 'Starred' }),
'starred-empty': defineMessage({ id: 'nav.starred-empty', message: 'Your starred dashboards will appear here' }),
dashboards: defineMessage({ id: 'nav.dashboards', message: 'Dashboards' }), dashboards: defineMessage({ id: 'nav.dashboards', message: 'Dashboards' }),
'manage-dashboards': defineMessage({ id: 'nav.manage-dashboards', message: 'Browse' }), 'manage-dashboards': defineMessage({ id: 'nav.manage-dashboards', message: 'Browse' }),
playlists: defineMessage({ id: 'nav.playlists', message: 'Playlists' }), playlists: defineMessage({ id: 'nav.playlists', message: 'Playlists' }),

View File

@ -12,29 +12,36 @@ const navTreeSlice = createSlice({
setStarred: (state, action: PayloadAction<{ id: string; title: string; url: string; isStarred: boolean }>) => { setStarred: (state, action: PayloadAction<{ id: string; title: string; url: string; isStarred: boolean }>) => {
const starredItems = state.find((navItem) => navItem.id === 'starred'); const starredItems = state.find((navItem) => navItem.id === 'starred');
const { id, title, url, isStarred } = action.payload; const { id, title, url, isStarred } = action.payload;
if (isStarred) { if (starredItems) {
const newStarredItem: NavModelItem = { if (isStarred) {
id, if (!starredItems.children) {
text: title, starredItems.children = [];
url, }
}; const newStarredItem: NavModelItem = {
starredItems?.children?.push(newStarredItem); id,
starredItems?.children?.sort((a, b) => a.text.localeCompare(b.text)); text: title,
} else { url,
const index = starredItems?.children?.findIndex((item) => item.id === id) ?? -1; };
if (index > -1) { starredItems.children.push(newStarredItem);
starredItems?.children?.splice(index, 1); starredItems.children.sort((a, b) => a.text.localeCompare(b.text));
} else {
const index = starredItems.children?.findIndex((item) => item.id === id) ?? -1;
if (index > -1) {
starredItems?.children?.splice(index, 1);
}
} }
} }
}, },
updateDashboardName: (state, action: PayloadAction<{ id: string; title: string; url: string }>) => { updateDashboardName: (state, action: PayloadAction<{ id: string; title: string; url: string }>) => {
const { id, title, url } = action.payload; const { id, title, url } = action.payload;
const starredItems = state.find((navItem) => navItem.id === 'starred'); const starredItems = state.find((navItem) => navItem.id === 'starred');
const navItem = starredItems?.children?.find((navItem) => navItem.id === id); if (starredItems) {
if (navItem) { const navItem = starredItems.children?.find((navItem) => navItem.id === id);
navItem.text = title; if (navItem) {
navItem.url = url; navItem.text = title;
starredItems?.children?.sort((a, b) => a.text.localeCompare(b.text)); navItem.url = url;
starredItems.children?.sort((a, b) => a.text.localeCompare(b.text));
}
} }
}, },
}, },

View File

@ -190,6 +190,14 @@ msgstr "Sign out"
msgid "nav.snapshots" msgid "nav.snapshots"
msgstr "Snapshots" msgstr "Snapshots"
#: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.starred"
msgstr "Starred"
#: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.starred-empty"
msgstr "Your starred dashboards will appear here"
#: public/app/core/components/NavBar/navBarItem-translations.ts #: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.teams" msgid "nav.teams"
msgstr "Teams" msgstr "Teams"

View File

@ -190,6 +190,14 @@ msgstr ""
msgid "nav.snapshots" msgid "nav.snapshots"
msgstr "" msgstr ""
#: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.starred"
msgstr ""
#: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.starred-empty"
msgstr ""
#: public/app/core/components/NavBar/navBarItem-translations.ts #: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.teams" msgid "nav.teams"
msgstr "" msgstr ""

View File

@ -190,6 +190,14 @@ msgstr ""
msgid "nav.snapshots" msgid "nav.snapshots"
msgstr "" msgstr ""
#: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.starred"
msgstr ""
#: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.starred-empty"
msgstr ""
#: public/app/core/components/NavBar/navBarItem-translations.ts #: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.teams" msgid "nav.teams"
msgstr "" msgstr ""

View File

@ -190,6 +190,14 @@ msgstr ""
msgid "nav.snapshots" msgid "nav.snapshots"
msgstr "" msgstr ""
#: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.starred"
msgstr ""
#: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.starred-empty"
msgstr ""
#: public/app/core/components/NavBar/navBarItem-translations.ts #: public/app/core/components/NavBar/navBarItem-translations.ts
msgid "nav.teams" msgid "nav.teams"
msgstr "" msgstr ""