Skip to content

Commit 9e06c6d

Browse files
feat(dev-mode): Wiziwig fullscreen (#372)
* added full screen mode edits * fixed step checker * removed json from index.json
1 parent 1e61279 commit 9e06c6d

26 files changed

+3064
-41
lines changed

src/bundled-interactives/first-dashboard.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
},
3737
{
3838
"action": "highlight",
39-
"reftarget": "button:contains(\"gdev-testdata TestData\")",
39+
"reftarget": "button:contains(\"TestData\")",
4040
"requirements": ["exists-reftarget"],
4141
"tooltip": "In real-world scenarios, you'd connect to data sources like Prometheus for metrics, Loki for logs, or databases like PostgreSQL. For learning purposes, Grafana includes TestData - a built-in data source that generates realistic sample data without requiring any external setup. This lets you practice building dashboards right away."
4242
}

src/bundled-interactives/index.json

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,6 @@
88
"url": ["/"],
99
"targetPlatform": "oss"
1010
},
11-
{
12-
"id": "json-guide-demo",
13-
"title": "JSON Guide Demo - All Block Types",
14-
"summary": "Comprehensive demo showcasing all JSON guide block types: markdown, html, images, videos, sections, interactive steps, multistep, and guided blocks.",
15-
"filename": "json-guide-demo.json",
16-
"url": ["/"],
17-
"targetPlatform": "oss"
18-
},
1911
{
2012
"id": "welcome-to-grafana-cloud",
2113
"title": "Welcome to Grafana Cloud",

src/components/DomPathTooltip/DomPathTooltip.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import React from 'react';
77
import { useStyles2 } from '@grafana/ui';
88
import { getDomPathTooltipStyles } from './dom-path-tooltip.styles';
9+
import { testIds } from '../testIds';
910

1011
export interface DomPathTooltipProps {
1112
/** The DOM path to display */
@@ -98,6 +99,7 @@ export function DomPathTooltip({ domPath, position, visible }: DomPathTooltipPro
9899
<div
99100
className={styles.tooltip}
100101
data-inspector-tooltip="true"
102+
data-testid={testIds.wysiwygEditor.fullScreen.domPathTooltip}
101103
style={{
102104
left: `${position.x + OFFSET_X}px`,
103105
top: `${position.y + OFFSET_Y}px`,
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/**
2+
* Minimized Sidebar Icon
3+
*
4+
* A floating icon that appears when the sidebar is minimized during full screen mode.
5+
* Shows the Pathfinder logo with a step count badge and pulsing animation.
6+
* Clicking expands the sidebar back to full size.
7+
*/
8+
9+
import React, { useState, useEffect, useCallback } from 'react';
10+
import { Portal, Badge, useStyles2 } from '@grafana/ui';
11+
import { GrafanaTheme2 } from '@grafana/data';
12+
import { css, keyframes } from '@emotion/css';
13+
import logoSvg from '../../img/logo.svg';
14+
import { testIds } from '../testIds';
15+
16+
const pulseAnimation = keyframes`
17+
0%, 100% {
18+
box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.7);
19+
}
20+
50% {
21+
box-shadow: 0 0 0 8px rgba(255, 107, 107, 0);
22+
}
23+
`;
24+
25+
const getStyles = (theme: GrafanaTheme2) => ({
26+
container: css({
27+
position: 'fixed',
28+
right: theme.spacing(2),
29+
top: '50%',
30+
transform: 'translateY(-50%)',
31+
zIndex: theme.zIndex.portal,
32+
display: 'flex',
33+
flexDirection: 'column',
34+
alignItems: 'center',
35+
gap: theme.spacing(1),
36+
}),
37+
iconButton: css({
38+
width: '56px',
39+
height: '56px',
40+
borderRadius: '50%',
41+
backgroundColor: theme.colors.background.primary,
42+
border: `2px solid ${theme.colors.primary.main}`,
43+
boxShadow: theme.shadows.z3,
44+
display: 'flex',
45+
alignItems: 'center',
46+
justifyContent: 'center',
47+
cursor: 'pointer',
48+
transition: 'all 0.2s ease-in-out',
49+
50+
'&:hover': {
51+
transform: 'scale(1.1)',
52+
boxShadow: theme.shadows.z3,
53+
borderColor: theme.colors.primary.shade,
54+
},
55+
56+
'&:active': {
57+
transform: 'scale(0.95)',
58+
},
59+
}),
60+
iconButtonRecording: css({
61+
animation: `${pulseAnimation} 2s ease-in-out infinite`,
62+
borderColor: theme.colors.error.main,
63+
}),
64+
logo: css({
65+
width: '32px',
66+
height: '32px',
67+
}),
68+
badge: css({
69+
position: 'absolute',
70+
top: '-4px',
71+
right: '-4px',
72+
}),
73+
statusText: css({
74+
fontSize: theme.typography.bodySmall.fontSize,
75+
color: theme.colors.text.secondary,
76+
textAlign: 'center',
77+
maxWidth: '100px',
78+
backgroundColor: theme.colors.background.primary,
79+
padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`,
80+
borderRadius: theme.shape.radius.default,
81+
boxShadow: theme.shadows.z1,
82+
}),
83+
statusRecording: css({
84+
color: theme.colors.error.text,
85+
fontWeight: theme.typography.fontWeightMedium,
86+
}),
87+
iconWrapper: css({
88+
position: 'relative',
89+
}),
90+
});
91+
92+
export interface MinimizedSidebarIconProps {
93+
/** Whether full screen mode is active */
94+
isActive: boolean;
95+
/** Number of steps recorded */
96+
stepCount: number;
97+
/** Whether currently recording (in active state, not editing) */
98+
isRecording: boolean;
99+
/** Called when icon is clicked to expand sidebar */
100+
onClick: () => void;
101+
}
102+
103+
/**
104+
* Floating icon shown when sidebar is minimized in full screen mode
105+
*
106+
* @example
107+
* ```tsx
108+
* <MinimizedSidebarIcon
109+
* isActive={isFullScreenActive}
110+
* stepCount={recordedSteps.length}
111+
* isRecording={state === 'active'}
112+
* onClick={exitFullScreenMode}
113+
* />
114+
* ```
115+
*/
116+
export function MinimizedSidebarIcon({ isActive, stepCount, isRecording, onClick }: MinimizedSidebarIconProps) {
117+
const styles = useStyles2(getStyles);
118+
119+
if (!isActive) {
120+
return null;
121+
}
122+
123+
return (
124+
<Portal>
125+
<div
126+
className={styles.container}
127+
data-minimized-sidebar
128+
data-testid={testIds.wysiwygEditor.fullScreen.minimizedSidebar.container}
129+
>
130+
<div className={styles.iconWrapper}>
131+
<button
132+
className={`${styles.iconButton} ${isRecording ? styles.iconButtonRecording : ''}`}
133+
onClick={onClick}
134+
aria-label="Expand sidebar and exit full screen mode"
135+
title="Click to exit full screen mode"
136+
data-testid={testIds.wysiwygEditor.fullScreen.minimizedSidebar.button}
137+
>
138+
<img src={logoSvg} alt="Pathfinder" className={styles.logo} />
139+
</button>
140+
{stepCount > 0 && (
141+
<div className={styles.badge} data-testid={testIds.wysiwygEditor.fullScreen.minimizedSidebar.badge}>
142+
<Badge text={String(stepCount)} color={isRecording ? 'red' : 'blue'} />
143+
</div>
144+
)}
145+
</div>
146+
<div className={`${styles.statusText} ${isRecording ? styles.statusRecording : ''}`}>
147+
{isRecording ? 'Recording...' : 'Paused'}
148+
<br />
149+
Click to exit
150+
</div>
151+
</div>
152+
</Portal>
153+
);
154+
}
155+
156+
/**
157+
* Hook to listen for full screen mode changes and manage minimized state
158+
*/
159+
export interface UseMinimizedSidebarOptions {
160+
/** Callback when sidebar should minimize */
161+
onMinimize?: () => void;
162+
/** Callback when sidebar should expand */
163+
onExpand?: () => void;
164+
}
165+
166+
export interface UseMinimizedSidebarReturn {
167+
/** Whether sidebar is currently minimized */
168+
isMinimized: boolean;
169+
/** Full screen mode state from event */
170+
fullScreenState: string | null;
171+
/** Expand the sidebar (and exit full screen mode) */
172+
expandSidebar: () => void;
173+
}
174+
175+
/**
176+
* Hook for sidebar components to listen to full screen mode changes
177+
*/
178+
export function useMinimizedSidebar(options: UseMinimizedSidebarOptions = {}): UseMinimizedSidebarReturn {
179+
const { onMinimize, onExpand } = options;
180+
181+
const [isMinimized, setIsMinimized] = useState(false);
182+
const [fullScreenState, setFullScreenState] = useState<string | null>(null);
183+
184+
// Listen for full screen mode changes
185+
useEffect(() => {
186+
const handleFullScreenChange = (event: CustomEvent<{ state: string; isActive: boolean }>) => {
187+
const { state, isActive } = event.detail;
188+
setFullScreenState(state);
189+
190+
if (isActive && !isMinimized) {
191+
setIsMinimized(true);
192+
if (onMinimize) {
193+
onMinimize();
194+
}
195+
} else if (!isActive && isMinimized) {
196+
setIsMinimized(false);
197+
if (onExpand) {
198+
onExpand();
199+
}
200+
}
201+
};
202+
203+
window.addEventListener('pathfinder-fullscreen-mode-changed', handleFullScreenChange as EventListener);
204+
205+
return () => {
206+
window.removeEventListener('pathfinder-fullscreen-mode-changed', handleFullScreenChange as EventListener);
207+
};
208+
}, [isMinimized, onMinimize, onExpand]);
209+
210+
// Expand sidebar programmatically
211+
const expandSidebar = useCallback(() => {
212+
setIsMinimized(false);
213+
if (onExpand) {
214+
onExpand();
215+
}
216+
217+
// Dispatch event to notify full screen mode to exit
218+
const event = new CustomEvent('pathfinder-request-exit-fullscreen', {
219+
detail: { source: 'minimized-sidebar' },
220+
});
221+
window.dispatchEvent(event);
222+
}, [onExpand]);
223+
224+
return {
225+
isMinimized,
226+
fullScreenState,
227+
expandSidebar,
228+
};
229+
}
230+
231+
export default MinimizedSidebarIcon;

src/components/docs-panel/docs-panel.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,14 +1626,15 @@ function CombinedPanelRendererInner({ model }: SceneComponentProps<CombinedLearn
16261626
<div className={activeTab.type === 'docs' ? styles.docsContent : styles.journeyContent}>
16271627
{/* Return to Editor Banner - only shown for WYSIWYG preview */}
16281628
{isWysiwygPreview && (
1629-
<div className={styles.returnToEditorBanner}>
1630-
<div className={styles.returnToEditorLeft}>
1629+
<div className={styles.returnToEditorBanner} data-testid={testIds.wysiwygPreview.banner}>
1630+
<div className={styles.returnToEditorLeft} data-testid={testIds.wysiwygPreview.modeIndicator}>
16311631
<Icon name="eye" size="sm" />
16321632
<span>{t('docsPanel.previewMode', 'Preview Mode')}</span>
16331633
</div>
16341634
<button
16351635
className={styles.returnToEditorButton}
1636-
onClick={() => model.setActiveTab('recommendations')}
1636+
onClick={() => model.setActiveTab('devtools')}
1637+
data-testid={testIds.wysiwygPreview.returnToEditorButton}
16371638
>
16381639
<Icon name="arrow-left" size="sm" />
16391640
{t('docsPanel.returnToEditor', 'Return to editor')}

0 commit comments

Comments
 (0)