Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions electron/main/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ function buildTemplate(): MenuItemConstructorOptions[] {
template.push({
label: "View",
submenu: [
{
label: "Command Palette…",
accelerator: "CmdOrCtrl+K",
click: () => getMainWindow()?.webContents.send("menu:open-command-palette"),
},
{ type: "separator" },
{ role: "reload" },
{ role: "forceReload" },
{ role: "toggleDevTools" },
Expand Down
1 change: 1 addition & 0 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const ALLOWED_EVENT_CHANNELS: Set<string> = new Set<EventChannel>([
"file:open-runbook",
"menu:open-url-prompt",
"menu:close-runbook",
"menu:open-command-palette",
"registry:updated",
])

Expand Down
1 change: 1 addition & 0 deletions electron/shared/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export interface IpcEventMap {
"file:open-runbook": { path: string; remoteSource?: string }
"menu:open-url-prompt": void
"menu:close-runbook": void
"menu:open-command-palette": void
"registry:updated": void
}

Expand Down
73 changes: 72 additions & 1 deletion web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { BookOpen, Code, AlertTriangle } from "lucide-react"
import { Header } from './components/layout/Header'
import { WelcomeScreen } from './components/layout/WelcomeScreen'
import { OpenUrlModal } from './components/layout/OpenUrlModal'
import { AboutDialog } from './components/layout/AboutDialog'
import { CommandPalette } from './components/layout/CommandPalette'
import { ErrorSummaryBanner } from './components/layout/ErrorSummaryBanner'
import MDXContainer from './components/MDXContainer'
import { ArtifactsContainer } from './components/layout/ArtifactsContainer'
Expand All @@ -19,6 +21,13 @@ import { useIpcWatchMode } from './hooks/useIpcWatchMode'
import { useIpcGeneratedFilesCheck } from './hooks/useIpcGeneratedFilesCheck'
import { useErrorReporting } from './contexts/useErrorReporting'
import { useApi } from './contexts/ApiContext'
import { useLogs } from './contexts/useLogs'
import {
createLogsZipRaw,
createLogsZipJson,
downloadBlob,
generateAllLogsZipFilename,
} from './lib/logs'
import { cn } from './lib/utils'

function App() {
Expand All @@ -29,11 +38,25 @@ function App() {
const [showGeneratedFilesAlert, setShowGeneratedFilesAlert] = useState(false);
const [alertDismissedThisSession, setAlertDismissedThisSession] = useState(false);
const [isUrlModalOpen, setIsUrlModalOpen] = useState(false);
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
const [isPaletteOpen, setIsPaletteOpen] = useState(false);

const { getAllLogs, hasLogs } = useLogs();

const handleOpenRunbook = useCallback(async () => {
await api.invoke('native:open-runbook-dialog')
}, [api])

const handleDownloadLogsRaw = useCallback(async () => {
const blob = await createLogsZipRaw(getAllLogs())
downloadBlob(blob, generateAllLogsZipFilename())
}, [getAllLogs])

const handleDownloadLogsJson = useCallback(async () => {
const blob = await createLogsZipJson(getAllLogs())
downloadBlob(blob, generateAllLogsZipFilename())
}, [getAllLogs])

// Listen for "Open from URL" menu command
useEffect(() => {
const cleanup = api.on('menu:open-url-prompt', () => {
Expand All @@ -42,6 +65,26 @@ function App() {
return cleanup
}, [api])

// Listen for "Command Palette" menu command (sent by the View menu accelerator).
useEffect(() => {
const cleanup = api.on('menu:open-command-palette', () => {
setIsPaletteOpen(true)
})
return cleanup
}, [api])

// Global Cmd/Ctrl+K keydown to toggle the palette.
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault()
setIsPaletteOpen((open) => !open)
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [])

// Use the useApi hook to fetch runbook data
const getRunbookResult = useIpcGetRunbook()

Expand Down Expand Up @@ -192,7 +235,13 @@ function App() {
return (
<>
<div className="flex flex-col">
<Header pathName={pathName} localPath={getRunbookResult.data?.path} />
<Header
pathName={pathName}
localPath={getRunbookResult.data?.path}
onShowAbout={() => setIsAboutDialogOpen(true)}
onDownloadLogsRaw={handleDownloadLogsRaw}
onDownloadLogsJson={handleDownloadLogsJson}
/>

{/* Error Summary Banner */}
{(errorCount > 0 || warningCount > 0) && (
Expand Down Expand Up @@ -361,6 +410,28 @@ function App() {

{/* Open from URL Modal */}
<OpenUrlModal open={isUrlModalOpen} onOpenChange={setIsUrlModalOpen} />

{/* About Dialog */}
<AboutDialog open={isAboutDialogOpen} onOpenChange={setIsAboutDialogOpen} />

{/* Command Palette (Cmd/Ctrl+K) */}
<CommandPalette
open={isPaletteOpen}
onOpenChange={setIsPaletteOpen}
ctx={{
hasRunbookOpen: Boolean(getRunbookResult.data),
hasLogs,
onOpenRunbook: handleOpenRunbook,
onOpenUrl: () => setIsUrlModalOpen(true),
onCloseRunbook: () => { void api.invoke('native:close-runbook') },
onToggleArtifacts: () => setIsArtifactsHidden((v) => !v),
onToggleMobileView: () =>
setActiveMobileSection((v) => (v === 'markdown' ? 'code' : 'markdown')),
onDownloadLogsRaw: handleDownloadLogsRaw,
onDownloadLogsJson: handleDownloadLogsJson,
onShowAbout: () => setIsAboutDialogOpen(true),
}}
/>
</>
)
}
Expand Down
37 changes: 37 additions & 0 deletions web/src/components/layout/AboutDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logoDarkColor from '@/assets/runbooks-logo-dark-color.svg';
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
} from '../ui/alert-dialog';

interface AboutDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}

export function AboutDialog({ open, onOpenChange }: AboutDialogProps) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<div className="relative">
<AlertDialogHeader>
<AlertDialogTitle className="sr-only">About Gruntwork Runbooks</AlertDialogTitle>
<img src={logoDarkColor} alt="Gruntwork Runbooks" className="h-16 mb-2" />

<AlertDialogDescription className="text-left space-y-4">
<p>Runbooks enables DevOps subject matter experts to capture and share their expertise in a way that is easy to understand and use.</p>
<p>Runbooks is published by <a target="_blank" rel="noreferrer" href="https://gruntwork.io">Gruntwork</a> and is <a target="_blank" rel="noreferrer" href="https://github.com/gruntwork-io/runbooks">open source</a>! Check out the <a target="_blank" rel="noreferrer" href="https://runbooks.gruntwork.io">Runbooks docs</a> for more information.</p>
<AlertDialogAction className="block mt-4" onClick={() => onOpenChange(false)}>
Close
</AlertDialogAction>
</AlertDialogDescription>
</AlertDialogHeader>
</div>
</AlertDialogContent>
</AlertDialog>
);
}
Loading
Loading