From 7625083c775c71f6ca9b1dae2f08dd271903aa12 Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Tue, 30 Sep 2025 06:47:48 +0100 Subject: [PATCH 1/2] Experiment with help menu opening Signed-off-by: Jack Baldry --- src/components/App/App.tsx | 13 +++-- src/components/HelpFooter/HelpFooter.tsx | 25 ++++++++-- src/components/docs-panel/context-panel.tsx | 11 +++-- src/module.tsx | 55 +++++++++++++++++++-- src/plugin.json | 17 +++++++ src/styles/content-html.styles.ts | 8 +-- src/styles/context-panel.styles.ts | 4 +- 7 files changed, 113 insertions(+), 20 deletions(-) diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 588e7faa..06463775 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -1,10 +1,11 @@ -import { AppRootProps } from '@grafana/data'; +import { AppRootProps, NavModelItem } from '@grafana/data'; import React, { useMemo, useEffect } from 'react'; import { SceneApp } from '@grafana/scenes'; import { docsPage } from '../../pages/docsPage'; -import { ContextPanelComponent } from '../../utils/docs.utils'; import { PluginPropsContext } from '../../utils/utils.plugin'; import { getConfigWithDefaults } from '../../constants'; +import { CombinedLearningJourneyPanel } from '../docs-panel/docs-panel'; +import { usePluginContext } from '@grafana/data'; function getSceneApp() { return new SceneApp({ @@ -12,8 +13,12 @@ function getSceneApp() { }); } -export function MemoizedContextPanel() { - return ; +export function MemoizedContextPanel({ helpNode }: { helpNode?: NavModelItem }) { + const pluginContext = usePluginContext(); + const config = getConfigWithDefaults(pluginContext?.meta?.jsonData || {}); + const panel = useMemo(() => new CombinedLearningJourneyPanel(config, helpNode), [config, helpNode]); + + return ; } function App(props: AppRootProps) { diff --git a/src/components/HelpFooter/HelpFooter.tsx b/src/components/HelpFooter/HelpFooter.tsx index b6b4fc58..c8afca6b 100644 --- a/src/components/HelpFooter/HelpFooter.tsx +++ b/src/components/HelpFooter/HelpFooter.tsx @@ -2,13 +2,15 @@ import React, { useState } from 'react'; import { Icon, useTheme2, Modal } from '@grafana/ui'; import { config } from '@grafana/runtime'; import { t } from '@grafana/i18n'; +import { NavModelItem } from '@grafana/data'; import { getHelpFooterStyles } from '../../styles/help-footer.styles'; interface HelpFooterProps { className?: string; + helpNode?: NavModelItem; } -export const HelpFooter: React.FC = ({ className }) => { +export const HelpFooter: React.FC = ({ className, helpNode }) => { const theme = useTheme2(); const styles = getHelpFooterStyles(theme); const [isHelpModalOpen, setIsHelpModalOpen] = useState(false); @@ -21,7 +23,8 @@ export const HelpFooter: React.FC = ({ className }) => { setIsHelpModalOpen(false); }; - const helpButtons = [ + // Used when helpNode isn't provided. + const defaultHelpButtons = [ { key: 'documentation', label: t('helpFooter.buttons.documentation', 'Documentation'), @@ -60,6 +63,22 @@ export const HelpFooter: React.FC = ({ className }) => { }, ]; + const helpButtons = React.useMemo(() => { + if (helpNode?.children && helpNode.children.length > 0) { + const nodeButtons = helpNode.children + .filter((child) => child.text && (child.url || child.onClick)) + .map((child, index) => ({ + key: child.id || `help-${index}`, + label: child.text || '', + icon: (child.icon || 'question-circle') as any, + href: child.url, + target: child.target, + onClick: child.onClick, + })); + } + return defaultHelpButtons; + }, [helpNode]); + return (
@@ -68,7 +87,7 @@ export const HelpFooter: React.FC = ({ className }) => { const buttonProps = button.href ? { href: button.href, - target: '_blank', + target: button.target || '_blank', rel: 'noopener noreferrer', } : { diff --git a/src/components/docs-panel/context-panel.tsx b/src/components/docs-panel/context-panel.tsx index 69ef6005..1c19c50d 100644 --- a/src/components/docs-panel/context-panel.tsx +++ b/src/components/docs-panel/context-panel.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { Icon, useStyles2, Card } from '@grafana/ui'; -import { usePluginContext } from '@grafana/data'; +import { usePluginContext, NavModelItem } from '@grafana/data'; import { t } from '@grafana/i18n'; import logoSvg from '../../img/logo.svg'; import { SkeletonLoader } from '../SkeletonLoader'; @@ -20,6 +20,7 @@ import { getConfigWithDefaults } from '../../constants'; interface ContextPanelState extends SceneObjectState { onOpenLearningJourney?: (url: string, title: string) => void; onOpenDocsPage?: (url: string, title: string) => void; + helpNode?: NavModelItem; } export class ContextPanel extends SceneObjectBase { @@ -31,11 +32,13 @@ export class ContextPanel extends SceneObjectBase { public constructor( onOpenLearningJourney?: (url: string, title: string) => void, - onOpenDocsPage?: (url: string, title: string) => void + onOpenDocsPage?: (url: string, title: string) => void, + helpNode?: NavModelItem ) { super({ onOpenLearningJourney, onOpenDocsPage, + helpNode, }); } @@ -410,8 +413,8 @@ function ContextPanelRenderer({ model }: SceneComponentProps) {
)} - {/* Help Footer */} - + {/* Help Footer - now receives helpNode */} +
); diff --git a/src/module.tsx b/src/module.tsx index 77284e38..bd9080b0 100644 --- a/src/module.tsx +++ b/src/module.tsx @@ -1,4 +1,4 @@ -import { AppPlugin, type AppRootProps, PluginExtensionPoints, BusEventWithPayload } from '@grafana/data'; +import { AppPlugin, type AppRootProps, PluginExtensionPoints, BusEventWithPayload, NavModelItem } from '@grafana/data'; import { LoadingPlaceholder } from '@grafana/ui'; import { getAppEvents } from '@grafana/runtime'; import React, { Suspense, lazy } from 'react'; @@ -56,11 +56,12 @@ const plugin = new AppPlugin<{}>() export { plugin }; +// Expose the main component for the sidebar plugin.addComponent({ targets: `grafana/extension-sidebar/v0-alpha`, title: 'Grafana Pathfinder', description: 'Opens Grafana Pathfinder', - component: function ContextSidebar() { + component: function ContextSidebar(props: { helpNode?: NavModelItem }) { React.useEffect(() => { reportAppInteraction(UserInteraction.DocsPanelInteraction, { action: 'open', @@ -79,7 +80,55 @@ plugin.addComponent({ return ( }> - + + + ); + }, +}); + +plugin.addLink({ + title: 'Grafana Pathfinder', + description: 'Open Grafana Pathfinder documentation assistant', + targets: ['grafana/topbar/help-button/v1'], + // Should be: targets: [PluginExtensionPoints.TopBarHelpButtonV1], + onClick: (event, helpers) => { + reportAppInteraction(UserInteraction.DocsPanelInteraction, { + action: 'open', + source: 'help_button', + timestamp: Date.now(), + }); + + helpers.openInSidebar('grafana-grafanadocsplugin-app/pathfinder-help/v1', { + helpNode: helpers.context?.helpNode, + }); + }, +}); + +plugin.addComponent({ + id: 'grafana-grafanadocsplugin-app/pathfinder-help/v1', + title: 'Grafana Pathfinder', + description: 'Grafana Pathfinder documentation assistant', + targets: PluginExtensionPoints.ExtensionSidebar, + component: function PathfinderHelpSidebar(props: { helpNode?: NavModelItem }) { + React.useEffect(() => { + reportAppInteraction(UserInteraction.DocsPanelInteraction, { + action: 'open', + source: 'help_button_sidebar', + timestamp: Date.now(), + }); + + return () => { + reportAppInteraction(UserInteraction.DocsPanelInteraction, { + action: 'close', + source: 'help_button_sidebar_unmount', + timestamp: Date.now(), + }); + }; + }, []); + + return ( + }> + ); }, diff --git a/src/plugin.json b/src/plugin.json index d3cda2d7..fd4c45c3 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -38,6 +38,18 @@ "targets": ["grafana/extension-sidebar/v0-alpha"], "description": "Opens Grafana Pathfinder", "title": "Grafana Pathfinder" + }, + { + "targets": ["grafana/extension-sidebar/v0-alpha"], + "description": "Grafana Pathfinder documentation assistant", + "title": "Grafana Pathfinder" + } + ], + "exposedComponents": [ + { + "id": "grafana-grafanadocsplugin-app/pathfinder-help/v1", + "title": "Grafana Pathfinder", + "description": "Grafana Pathfinder documentation assistant" } ], "addedLinks": [ @@ -46,6 +58,11 @@ "description": "Opens Grafana Pathfinder", "title": "Documentation-Link" }, + { + "targets": ["grafana/topbar/help-button/v1"], + "description": "Open Grafana Pathfinder documentation assistant", + "title": "Grafana Pathfinder" + }, { "targets": ["grafana/commandpalette/action"], "description": "Open Grafana Pathfinder", diff --git a/src/styles/content-html.styles.ts b/src/styles/content-html.styles.ts index 490e4b8c..d1fd6720 100644 --- a/src/styles/content-html.styles.ts +++ b/src/styles/content-html.styles.ts @@ -1268,8 +1268,8 @@ const getSharedUtilityStyles = (theme: GrafanaTheme2) => ({ margin: 0, marginBottom: theme.spacing(0.75), display: '-webkit-box', - '-webkit-line-clamp': '2', - '-webkit-box-orient': 'vertical', + WebkitLineClamp: '2', + WebkitBoxOrient: 'vertical', overflow: 'hidden', width: '100%', }, @@ -1281,8 +1281,8 @@ const getSharedUtilityStyles = (theme: GrafanaTheme2) => ({ margin: 0, flex: 1, display: '-webkit-box', - '-webkit-line-clamp': '3', - '-webkit-box-orient': 'vertical', + WebkitLineClamp: '3', + WebkitBoxOrient: 'vertical', overflow: 'hidden', width: '100%', }, diff --git a/src/styles/context-panel.styles.ts b/src/styles/context-panel.styles.ts index 36974e02..3a94cd47 100644 --- a/src/styles/context-panel.styles.ts +++ b/src/styles/context-panel.styles.ts @@ -161,8 +161,8 @@ export const getRecommendationCardStyles = (theme: GrafanaTheme2) => ({ minWidth: 0, maxWidth: 'calc(100% - 100px)', display: '-webkit-box', - '-webkit-line-clamp': '2', - '-webkit-box-orient': 'vertical', + WebkitLineClamp: '2', + WebkitBoxOrient: 'vertical', overflow: 'hidden', }), cardActions: css({ From a618d437240c14c74d008248dd0c7640f2d5c45e Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Tue, 30 Sep 2025 19:01:23 +0200 Subject: [PATCH 2/2] Got it working. --- src/components/App/App.tsx | 12 +-------- src/components/App/ContextPanel.tsx | 14 +++++++++++ src/components/HelpFooter/HelpFooter.tsx | 10 ++++++-- src/components/docs-panel/docs-panel.tsx | 7 +++--- src/module.tsx | 32 ++++++++++-------------- src/plugin.json | 7 +----- 6 files changed, 41 insertions(+), 41 deletions(-) create mode 100644 src/components/App/ContextPanel.tsx diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 06463775..f74f081d 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -1,11 +1,9 @@ -import { AppRootProps, NavModelItem } from '@grafana/data'; +import { AppRootProps } from '@grafana/data'; import React, { useMemo, useEffect } from 'react'; import { SceneApp } from '@grafana/scenes'; import { docsPage } from '../../pages/docsPage'; import { PluginPropsContext } from '../../utils/utils.plugin'; import { getConfigWithDefaults } from '../../constants'; -import { CombinedLearningJourneyPanel } from '../docs-panel/docs-panel'; -import { usePluginContext } from '@grafana/data'; function getSceneApp() { return new SceneApp({ @@ -13,14 +11,6 @@ function getSceneApp() { }); } -export function MemoizedContextPanel({ helpNode }: { helpNode?: NavModelItem }) { - const pluginContext = usePluginContext(); - const config = getConfigWithDefaults(pluginContext?.meta?.jsonData || {}); - const panel = useMemo(() => new CombinedLearningJourneyPanel(config, helpNode), [config, helpNode]); - - return ; -} - function App(props: AppRootProps) { const scene = useMemo(() => getSceneApp(), []); diff --git a/src/components/App/ContextPanel.tsx b/src/components/App/ContextPanel.tsx new file mode 100644 index 00000000..91e98f36 --- /dev/null +++ b/src/components/App/ContextPanel.tsx @@ -0,0 +1,14 @@ +import React, { useMemo } from "react"; +import { NavModelItem, usePluginContext } from "@grafana/data"; +import { CombinedLearningJourneyPanel } from "components/docs-panel/docs-panel"; +import { getConfigWithDefaults } from '../../constants'; + +export default function MemoizedContextPanel({ helpNode }: { helpNode?: NavModelItem }) { + const pluginContext = usePluginContext(); + const config = getConfigWithDefaults(pluginContext?.meta?.jsonData || {}); + const panel = useMemo(() => new CombinedLearningJourneyPanel(config, helpNode), [config, helpNode]); + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/HelpFooter/HelpFooter.tsx b/src/components/HelpFooter/HelpFooter.tsx index c8afca6b..bf8f8bc1 100644 --- a/src/components/HelpFooter/HelpFooter.tsx +++ b/src/components/HelpFooter/HelpFooter.tsx @@ -30,42 +30,48 @@ export const HelpFooter: React.FC = ({ className, helpNode }) = label: t('helpFooter.buttons.documentation', 'Documentation'), icon: 'file-alt' as const, href: 'https://grafana.com/docs/grafana/latest/?utm_source=grafana_footer', + target: '_blank', }, { key: 'support', label: t('helpFooter.buttons.support', 'Support'), icon: 'question-circle' as const, href: 'https://grafana.com/support/?utm_source=grafana_footer', + target: '_blank', }, { key: 'community', label: t('helpFooter.buttons.community', 'Community'), icon: 'comments-alt' as const, href: 'https://community.grafana.com/?utm_source=grafana_footer', + target: '_blank', }, { key: 'enterprise', label: t('helpFooter.buttons.enterprise', 'Enterprise'), icon: 'external-link-alt' as const, href: 'https://grafana.com/products/enterprise/?utm_source=grafana_footer', + target: '_blank', }, { key: 'download', label: t('helpFooter.buttons.download', 'Download'), icon: 'download-alt' as const, href: 'https://grafana.com/grafana/download?utm_source=grafana_footer', - }, + target: '_blank', + }, { key: 'shortcuts', label: t('helpFooter.buttons.shortcuts', 'Shortcuts'), icon: 'keyboard' as const, onClick: handleKeyboardShortcuts, + target: '_blank', }, ]; const helpButtons = React.useMemo(() => { if (helpNode?.children && helpNode.children.length > 0) { - const nodeButtons = helpNode.children + return helpNode.children .filter((child) => child.text && (child.url || child.onClick)) .map((child, index) => ({ key: child.id || `help-${index}`, diff --git a/src/components/docs-panel/docs-panel.tsx b/src/components/docs-panel/docs-panel.tsx index 8e2cca69..066dfa10 100644 --- a/src/components/docs-panel/docs-panel.tsx +++ b/src/components/docs-panel/docs-panel.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { SceneObjectBase, SceneObjectState, SceneComponentProps } from '@grafana/scenes'; import { IconButton, Alert, Icon, useStyles2 } from '@grafana/ui'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getConfigWithDefaults, DocsPluginConfig } from '../../constants'; @@ -77,11 +77,12 @@ class CombinedLearningJourneyPanel extends SceneObjectBase { return true; } - public constructor(pluginConfig: DocsPluginConfig = {}) { + public constructor(pluginConfig: DocsPluginConfig = {}, helpNode?: NavModelItem) { const restoredTabs = CombinedLearningJourneyPanel.restoreTabsFromStorage(); const contextPanel = new ContextPanel( (url: string, title: string) => this.openLearningJourney(url, title), - (url: string, title: string) => this.openDocsPage(url, title) + (url: string, title: string) => this.openDocsPage(url, title), + helpNode ); const activeTabId = CombinedLearningJourneyPanel.restoreActiveTabFromStorage(restoredTabs); diff --git a/src/module.tsx b/src/module.tsx index bd9080b0..6bf77411 100644 --- a/src/module.tsx +++ b/src/module.tsx @@ -1,4 +1,4 @@ -import { AppPlugin, type AppRootProps, PluginExtensionPoints, BusEventWithPayload, NavModelItem } from '@grafana/data'; +import { AppPlugin, type AppRootProps, BusEventWithPayload, NavModelItem } from '@grafana/data'; import { LoadingPlaceholder } from '@grafana/ui'; import { getAppEvents } from '@grafana/runtime'; import React, { Suspense, lazy } from 'react'; @@ -29,9 +29,7 @@ function openExtensionSidebar(pluginId: string, componentTitle: string, props?: } const LazyApp = lazy(() => import('./components/App/App')); -const LazyMemoizedContextPanel = lazy(() => - import('./components/App/App').then((module) => ({ default: module.MemoizedContextPanel })) -); +const LazyMemoizedContextPanel = lazy(() => import('./components/App/ContextPanel')); const LazyAppConfig = lazy(() => import('./components/AppConfig/AppConfig')); const LazyTermsAndConditions = lazy(() => import('./components/AppConfig/TermsAndConditions')); @@ -41,7 +39,7 @@ const App = (props: AppRootProps) => ( ); -const plugin = new AppPlugin<{}>() +export const plugin = new AppPlugin<{}>() .setRootPage(App) .addConfigPage({ title: 'Configuration', @@ -54,8 +52,6 @@ const plugin = new AppPlugin<{}>() id: 'recommendations-config', }); -export { plugin }; - // Expose the main component for the sidebar plugin.addComponent({ targets: `grafana/extension-sidebar/v0-alpha`, @@ -86,29 +82,27 @@ plugin.addComponent({ }, }); -plugin.addLink({ +plugin.addLink<{ helpNode?: NavModelItem }>({ title: 'Grafana Pathfinder', description: 'Open Grafana Pathfinder documentation assistant', - targets: ['grafana/topbar/help-button/v1'], - // Should be: targets: [PluginExtensionPoints.TopBarHelpButtonV1], - onClick: (event, helpers) => { + targets: ['grafana/app/topbar/help/v1'], + onClick: (_, {openSidebar, context}) => { reportAppInteraction(UserInteraction.DocsPanelInteraction, { action: 'open', source: 'help_button', timestamp: Date.now(), }); - helpers.openInSidebar('grafana-grafanadocsplugin-app/pathfinder-help/v1', { - helpNode: helpers.context?.helpNode, + openSidebar('Grafana Pathfinder', { + helpNode: context?.helpNode, }); }, }); -plugin.addComponent({ +plugin.exposeComponent({ id: 'grafana-grafanadocsplugin-app/pathfinder-help/v1', title: 'Grafana Pathfinder', description: 'Grafana Pathfinder documentation assistant', - targets: PluginExtensionPoints.ExtensionSidebar, component: function PathfinderHelpSidebar(props: { helpNode?: NavModelItem }) { React.useEffect(() => { reportAppInteraction(UserInteraction.DocsPanelInteraction, { @@ -137,7 +131,7 @@ plugin.addComponent({ plugin.addLink({ title: 'Open Grafana Pathfinder', description: 'Open Grafana Pathfinder', - targets: [PluginExtensionPoints.CommandPalette], + targets: ['grafana/commandpalette/action'], onClick: () => { reportAppInteraction(UserInteraction.DocsPanelInteraction, { action: 'open', @@ -155,7 +149,7 @@ plugin.addLink({ plugin.addLink({ title: 'Need help?', description: 'Get help with Grafana', - targets: [PluginExtensionPoints.CommandPalette], + targets: ['grafana/commandpalette/action'], onClick: () => { reportAppInteraction(UserInteraction.DocsPanelInteraction, { action: 'open', @@ -173,7 +167,7 @@ plugin.addLink({ plugin.addLink({ title: 'Learn Grafana', description: 'Learn how to use Grafana', - targets: [PluginExtensionPoints.CommandPalette], + targets: ['grafana/commandpalette/action'], onClick: () => { reportAppInteraction(UserInteraction.DocsPanelInteraction, { action: 'open', @@ -200,4 +194,4 @@ plugin.addLink({ }; }, onClick: () => {}, -}); +}); \ No newline at end of file diff --git a/src/plugin.json b/src/plugin.json index fd4c45c3..1f6e11c9 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -38,11 +38,6 @@ "targets": ["grafana/extension-sidebar/v0-alpha"], "description": "Opens Grafana Pathfinder", "title": "Grafana Pathfinder" - }, - { - "targets": ["grafana/extension-sidebar/v0-alpha"], - "description": "Grafana Pathfinder documentation assistant", - "title": "Grafana Pathfinder" } ], "exposedComponents": [ @@ -59,7 +54,7 @@ "title": "Documentation-Link" }, { - "targets": ["grafana/topbar/help-button/v1"], + "targets": ["grafana/app/topbar/help/v1"], "description": "Open Grafana Pathfinder documentation assistant", "title": "Grafana Pathfinder" },