mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -06:00
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:
parent
4a9872d108
commit
ee3f4f1709
@ -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"]
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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)};
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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' }),
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
|
@ -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 ""
|
||||||
|
@ -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 ""
|
||||||
|
@ -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 ""
|
||||||
|
Loading…
Reference in New Issue
Block a user