mirror of
https://github.com/cs01/gdbgui
synced 2024-11-16 07:47:46 +01:00
add dashboard
This commit is contained in:
parent
af49a8818c
commit
1030c89e28
9 changed files with 339 additions and 74 deletions
|
@ -33,6 +33,7 @@ blueprint = Blueprint(
|
|||
__name__,
|
||||
static_folder=str(STATIC_DIR),
|
||||
static_url_path="",
|
||||
template_folder="../templates",
|
||||
)
|
||||
|
||||
|
||||
|
@ -146,22 +147,8 @@ def help_route():
|
|||
return redirect("https://github.com/cs01/gdbgui/blob/master/HELP.md")
|
||||
|
||||
|
||||
@blueprint.route("/dashboard", methods=["GET"])
|
||||
@authenticate
|
||||
def dashboard():
|
||||
manager = current_app.config.get("_manager")
|
||||
|
||||
"""display a dashboard with a list of all running gdb processes
|
||||
and ability to kill them, or open a new tab to work with that
|
||||
GdbController instance"""
|
||||
return render_template(
|
||||
"dashboard.html",
|
||||
gdbgui_sessions=manager.get_dashboard_data(),
|
||||
default_command=current_app.config["gdb_command"],
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route("/", methods=["GET"])
|
||||
@blueprint.route("/dashboard", methods=["GET"])
|
||||
@authenticate
|
||||
def gdbgui():
|
||||
return send_from_directory(STATIC_DIR, "index.html")
|
||||
|
@ -188,7 +175,7 @@ def get_initial_data():
|
|||
@authenticate
|
||||
def dashboard_data():
|
||||
manager = current_app.config.get("_manager")
|
||||
|
||||
print(manager.get_dashboard_data())
|
||||
return jsonify(manager.get_dashboard_data())
|
||||
|
||||
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="text-gray-900 antialiased leading-tight font-sans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>gdbgui - dashboard</title>
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" />
|
||||
</head>
|
||||
|
||||
<body class="w-screen h-screen bg-gray-300">
|
||||
<div id="dashboard">Loading dashboard...</div>
|
||||
</body>
|
||||
<script>
|
||||
window.gdbgui_sessions = {{gdbgui_sessions | tojson}}
|
||||
window.default_command = {{default_command | tojson}}
|
||||
</script>
|
||||
<script type="text/javascript" src="static/js/dashboard.js?_={{version}}"></script>
|
||||
</html>
|
Binary file not shown.
|
@ -25,12 +25,14 @@
|
|||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@tanstack/react-query": "^5.8.2",
|
||||
"jquery": "^3.7.1",
|
||||
"lodash": "^4.17.21",
|
||||
"monaco-editor": "^0.44.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-reflex": "^4.1.0",
|
||||
"react-router-dom": "6",
|
||||
"recoil": "^0.7.7",
|
||||
"socket.io": "^4.7.2",
|
||||
"socket.io-client": "^4.7.2",
|
||||
|
@ -45,7 +47,7 @@
|
|||
"@types/jquery": "^3.5.27",
|
||||
"@types/lodash": "^4.14.201",
|
||||
"@types/socket.io": "^2.1.11",
|
||||
"@types/socket.io-client": "^1.4.33",
|
||||
"@types/socket.io-client": "^1.4.33",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/react": "^18.2.34",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
|
|
|
@ -17,40 +17,29 @@ import { Footer } from './Footer'
|
|||
import { GdbWebsocket } from './Websocket'
|
||||
import 'react-reflex/styles.css'
|
||||
import { SourceFileTabs } from './SourceFileTabs'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export function Gdbgui() {
|
||||
const [initialData, setInitialData] = useState<Nullable<InitialData>>(null)
|
||||
const [error, setError] = useState<Nullable<string>>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function initialize() {
|
||||
export default function Gdbgui() {
|
||||
const initialData = useQuery<InitialData>({
|
||||
queryKey: ['initial_data'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch('/initial_data')
|
||||
if (!response.ok) {
|
||||
setError(JSON.stringify(response, null, 3))
|
||||
}
|
||||
try {
|
||||
const initialData: InitialData = await response.json()
|
||||
const gdbWebsocket = new GdbWebsocket(
|
||||
initialData.gdb_command,
|
||||
initialData.gdbpid
|
||||
)
|
||||
store.set<typeof store.data.gdbWebsocket>('gdbWebsocket', gdbWebsocket)
|
||||
GlobalEvents.init()
|
||||
FileOps.init()
|
||||
setInitialData(initialData)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setError(
|
||||
`Failed to parse initial data from gdbgui server. Is it running? Error: ${String(
|
||||
e
|
||||
)}`
|
||||
)
|
||||
throw new Error(response.statusText)
|
||||
}
|
||||
const initialData: InitialData = await response.json()
|
||||
const gdbWebsocket = new GdbWebsocket(
|
||||
initialData.gdb_command,
|
||||
initialData.gdbpid
|
||||
)
|
||||
store.set<typeof store.data.gdbWebsocket>('gdbWebsocket', gdbWebsocket)
|
||||
GlobalEvents.init()
|
||||
FileOps.init()
|
||||
return initialData
|
||||
}
|
||||
initialize()
|
||||
}, [])
|
||||
})
|
||||
|
||||
if (error) {
|
||||
if (initialData.isError) {
|
||||
return (
|
||||
<div className=" h-screen w-screen bg-gray-900 text-red-800 text-2xl text-center">
|
||||
<div className="w-full ">
|
||||
|
@ -58,14 +47,14 @@ export function Gdbgui() {
|
|||
gdbgui failed to connect to the server. Is it still running?
|
||||
</div>
|
||||
<div className="pt-10">
|
||||
<pre>{error}</pre>
|
||||
<pre>{initialData.error.message}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!initialData) {
|
||||
if (initialData.isLoading) {
|
||||
return (
|
||||
<div className="flex-col h-screen w-screen bg-gray-900 text-gray-800 text-9xl text-center">
|
||||
<div className="w-full">Loading...</div>
|
||||
|
@ -73,6 +62,11 @@ export function Gdbgui() {
|
|||
)
|
||||
}
|
||||
|
||||
if (!initialData.data) {
|
||||
// This should be unreachable
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen text-gray-300 bg-black">
|
||||
<HoverVar />
|
||||
|
@ -100,7 +94,7 @@ export function Gdbgui() {
|
|||
className="bg-black text-gray-300"
|
||||
>
|
||||
<div className="fixed bg-black w-full z-10">
|
||||
<Nav initialData={initialData} />
|
||||
<Nav initialData={initialData.data} />
|
||||
</div>
|
||||
<ReflexContainer
|
||||
orientation="vertical"
|
||||
|
@ -117,9 +111,9 @@ export function Gdbgui() {
|
|||
<ReflexElement minSize={100}>
|
||||
<div className="pane-content">
|
||||
<RightSidebar
|
||||
signals={initialData.signals}
|
||||
signals={initialData.data.signals}
|
||||
debug={debug}
|
||||
initialDir={initialData.working_directory}
|
||||
initialDir={initialData.data.working_directory}
|
||||
/>
|
||||
</div>
|
||||
</ReflexElement>
|
||||
|
|
282
vite-app/src/components/dashboard/Dashboard.tsx
Normal file
282
vite-app/src/components/dashboard/Dashboard.tsx
Normal file
|
@ -0,0 +1,282 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
type DashboardData = {}
|
||||
|
||||
// export default function NewDashboard() {
|
||||
// const dashboard = useQuery<DashboardData>({
|
||||
// queryKey: ['initial_data'],
|
||||
// queryFn: async () => {
|
||||
// const response = await fetch('/initial_data')
|
||||
// if (!response.ok) {
|
||||
// throw new Error(response.statusText)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
// if (dashboard.isSuccess) {
|
||||
// return <OldDashboard sessions={dashboard.data.sessions} />
|
||||
// }
|
||||
// }
|
||||
|
||||
import ReactDOM from 'react-dom'
|
||||
import React, { useState } from 'react'
|
||||
// import '../../static/css/tailwind.css'
|
||||
|
||||
type GdbguiSession = {
|
||||
pid: number
|
||||
start_time: string
|
||||
command: string
|
||||
client_ids: string[]
|
||||
}
|
||||
const copyIcon = (
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'gdbgui_sessions' does not exist on type ... Remove this comment to see the full error message
|
||||
const data: GdbguiSession[] = window.gdbgui_sessions
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'csrf_token' does not exist on type 'Wind... Remove this comment to see the full error message
|
||||
const csrf_token: string = window.csrf_token
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'default_command' does not exist on type ... Remove this comment to see the full error message
|
||||
const default_command: string = window.default_command
|
||||
function GdbguiSession(props: {
|
||||
session: GdbguiSession
|
||||
updateData: Function
|
||||
}) {
|
||||
const session = props.session
|
||||
const params = new URLSearchParams({
|
||||
gdbpid: session.pid.toString()
|
||||
}).toString()
|
||||
const url = `${window.location.origin}/?${params}`
|
||||
const [shareButtonText, setShareButtonText] = useState(copyIcon)
|
||||
const [clickedKill, setClickedKill] = useState(false)
|
||||
let timeout: NodeJS.Timeout
|
||||
return (
|
||||
<tr>
|
||||
<td className="border px-4 py-2">{session.command}</td>
|
||||
<td className="border px-4 py-2">{session.pid}</td>
|
||||
<td className="border px-4 py-2">{session.client_ids.length}</td>
|
||||
<td className="border px-4 py-2">{session.start_time}</td>
|
||||
<td className="border px-4 py-2">
|
||||
<a
|
||||
href={url}
|
||||
className="leading-7 bg-blue-500 hover:bg-blue-700 border-blue-500 hover:border-blue-700 border-4 text-white py-2 px-2 rounded"
|
||||
type="button"
|
||||
>
|
||||
Connect to Session
|
||||
</a>
|
||||
<button
|
||||
className="bg-blue-500 hover:bg-blue-700 border-blue-500 hover:border-blue-700 border-4 text-white m-1 p-2 rounded align-middle"
|
||||
title="Copy Sharable URL"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(url)
|
||||
setShareButtonText(<span>Copied!</span>)
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => setShareButtonText(copyIcon), 3000)
|
||||
}}
|
||||
>
|
||||
{shareButtonText}
|
||||
</button>
|
||||
</td>
|
||||
<td className="border px-4 py-2">
|
||||
<button
|
||||
className="leading-7 bg-red-500 hover:bg-red-700 border-red-500 hover:border-red-700 border-4 text-white py-2 px-2 rounded"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (clickedKill) {
|
||||
await fetch('/kill_session', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ gdbpid: session.pid, csrf_token })
|
||||
})
|
||||
await props.updateData()
|
||||
} else {
|
||||
setClickedKill(true)
|
||||
setTimeout(() => {
|
||||
setClickedKill(false)
|
||||
}, 5000)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{clickedKill ? 'Click Again to Confirm' : 'Kill Session'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function redirect(url: string) {
|
||||
window.open(url, '_blank')
|
||||
setTimeout(() => window.location.reload(), 500)
|
||||
}
|
||||
class StartCommand extends React.Component<any, { value: string }> {
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
// @ts-expect-error
|
||||
this.state = { value: window.default_command }
|
||||
|
||||
this.handleChange = this.handleChange.bind(this)
|
||||
this.handleSubmit = this.handleSubmit.bind(this)
|
||||
}
|
||||
|
||||
handleChange(event: any) {
|
||||
this.setState({ value: event.target.value })
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const params = new URLSearchParams({
|
||||
gdb_command: this.state.value
|
||||
}).toString()
|
||||
redirect(`/?${params}`)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div>Enter the gdb command to run in the session.</div>
|
||||
<div className="flex w-full mx-auto items-center container">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-grow leading-9 bg-gray-900 text-gray-100 font-mono focus:outline-none focus:shadow-outline border border-gray-300 py-2 px-2 block appearance-none rounded-l-lg"
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key.toLowerCase() === 'enter') {
|
||||
this.handleSubmit()
|
||||
}
|
||||
}}
|
||||
placeholder="gdb --flag args"
|
||||
/>
|
||||
<button
|
||||
className="flex-grow-0 leading-7 bg-green-500 hover:bg-green-700 border-green-500 hover:border-green-700 border-4 text-white py-2 px-2 rounded-r-lg"
|
||||
type="button"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
Start New Session
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function Nav() {
|
||||
return (
|
||||
<nav className="flex items-center justify-between flex-wrap bg-blue-500 p-6">
|
||||
<div className="flex items-center flex-shrink-0 text-white mr-6">
|
||||
<a
|
||||
href={`${window.location.origin}/dashboard`}
|
||||
className="font-semibold text-xl tracking-tight"
|
||||
>
|
||||
gdbgui
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="w-full block flex-grow lg:flex lg:items-center lg:w-auto">
|
||||
<div className="text-sm lg:flex-grow">
|
||||
<a
|
||||
href="https://gdbgui.com"
|
||||
className="block mt-4 lg:inline-block lg:mt-0 text-blue-200 hover:text-white mr-4"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/channel/UCUCOSclB97r9nd54NpXMV5A"
|
||||
className="block mt-4 lg:inline-block lg:mt-0 text-blue-200 hover:text-white mr-4"
|
||||
>
|
||||
YouTube
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/cs01/gdbgui"
|
||||
className="block mt-4 lg:inline-block lg:mt-0 text-blue-200 hover:text-white mr-4"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://www.paypal.com/paypalme/grassfedcode/20"
|
||||
className="block mt-4 lg:inline-block lg:mt-0 text-blue-200 hover:text-white mr-4"
|
||||
>
|
||||
Donate
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default class Dashboard extends React.PureComponent<
|
||||
any,
|
||||
{ sessions: GdbguiSession[] }
|
||||
> {
|
||||
interval: NodeJS.Timeout | undefined
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { sessions: [] }
|
||||
this.updateData = this.updateData.bind(this)
|
||||
}
|
||||
async updateData() {
|
||||
const response = await fetch('/dashboard_data')
|
||||
// console.log(await response.text())
|
||||
const sessions = await response.json()
|
||||
this.setState({ sessions })
|
||||
}
|
||||
componentDidMount() {
|
||||
this.interval = setInterval(this.updateData, 5000)
|
||||
}
|
||||
componentWillUnmount() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const sessions = this.state.sessions.map((d, index) => (
|
||||
<GdbguiSession key={index} session={d} updateData={this.updateData} />
|
||||
))
|
||||
return (
|
||||
<div className="w-full h-full min-h-screen flex flex-col">
|
||||
<Nav />
|
||||
<div className="flex-grow w-full h-full bg-gray-300 text-center p-5">
|
||||
<div className="text-3xl font-semibold">Start new session</div>
|
||||
<StartCommand />
|
||||
<div className="mt-5 text-3xl font-semibold">
|
||||
{sessions.length === 1
|
||||
? 'There is 1 gdbgui session running'
|
||||
: `There are ${sessions.length} gdbgui sessions running`}
|
||||
</div>
|
||||
<table className="table-auto mx-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2">Command</th>
|
||||
<th className="px-4 py-2">PID</th>
|
||||
<th className="px-4 py-2">Connected Browsers</th>
|
||||
<th className="px-4 py-2">Start Time</th>
|
||||
<th className="px-4 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{sessions}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<footer className="h-40 bold text-lg bg-black text-gray-500 text-center flex flex-col justify-center">
|
||||
<p>gdbgui</p>
|
||||
<p>The browser-based frontend to gdb</p>
|
||||
<a href="https://chadsmith.dev">Copyright Chad Smith</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,17 +2,29 @@ import { createRoot } from 'react-dom/client'
|
|||
import React from 'react'
|
||||
import './index.css'
|
||||
import './App.css'
|
||||
import { Gdbgui } from './components/App'
|
||||
import { RecoilRoot } from 'recoil'
|
||||
import 'tailwindcss/tailwind.css'
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
const Dashboard = React.lazy(() => import('components/dashboard/Dashboard'))
|
||||
const Gdbgui = React.lazy(() => import('components/App'))
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const container = document.getElementById('root') as HTMLDivElement
|
||||
const root = createRoot(container)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<RecoilRoot>
|
||||
<Gdbgui />
|
||||
</RecoilRoot>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RecoilRoot>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Gdbgui />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</RecoilRoot>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
module.exports = {
|
||||
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
|
||||
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}', './public/**/*.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
|
|
|
@ -15,6 +15,13 @@ export default defineConfig({
|
|||
secure: false
|
||||
// Path rewrite is optional
|
||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/dashboard_data': {
|
||||
target: 'http://localhost:5000',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
// Path rewrite is optional
|
||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue