add dashboard

This commit is contained in:
Chad Smith 2023-11-11 17:10:48 -08:00
parent af49a8818c
commit 1030c89e28
9 changed files with 339 additions and 74 deletions

View file

@ -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())

View file

@ -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.

View file

@ -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",

View file

@ -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>

View 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>
)
}
}

View file

@ -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>
)

View file

@ -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: {

View file

@ -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/, '')
}
}
},