feat: make experiment control buttons prettier

Use actual icons for the play, pause, skip, etc. buttons.
This commit is contained in:
Twirre Meulenbelt
2026-02-04 11:00:14 +01:00
parent b826b8ae47
commit 8fa2adc973
7 changed files with 59 additions and 25 deletions

22
src/App.module.css Normal file
View File

@@ -0,0 +1,22 @@
/* Spread header items. Make home button centered until there is not enough space. */
.header {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
/* Center each */
& > div {
flex: 1;
display: flex;
justify-content: center;
}
/* Except first and last */
& > div:first-child {
justify-content: flex-start;
}
& > div:last-child {
justify-content: flex-end;
}
}

View File

@@ -8,6 +8,7 @@ import UserManual from './pages/Manuals/Manuals.tsx';
import VisProg from "./pages/VisProgPage/VisProg.tsx"; import VisProg from "./pages/VisProgPage/VisProg.tsx";
import {useState} from "react"; import {useState} from "react";
import Logging from "./components/Logging/Logging.tsx"; import Logging from "./components/Logging/Logging.tsx";
import styles from "./App.module.css";
function App(){ function App(){
@@ -15,10 +16,10 @@ function App(){
return ( return (
<> <>
<header> <header className={styles.header}>
<span>© Utrecht University (ICS)</span> <div><span>© Utrecht University (ICS)</span></div>
<Link to={"/"}>Home</Link> <div><Link to={"/"}>Home</Link></div>
<button onClick={() => setShowLogs(!showLogs)}>Developer Logs</button> <div><button onClick={() => setShowLogs(!showLogs)}>Developer Logs</button></div>
</header> </header>
<div className={"flex-row justify-center flex-1 min-height-0"}> <div className={"flex-row justify-center flex-1 min-height-0"}>
<main className={"flex-col align-center flex-1 scroll-y"}> <main className={"flex-col align-center flex-1 scroll-y"}>

View File

@@ -1,5 +0,0 @@
export default function Redo({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M390.98-191.87q-98.44 0-168.77-65.27-70.34-65.27-70.34-161.43 0-96.15 70.34-161.54 70.33-65.39 168.77-65.39h244.11l-98.98-98.98 63.65-63.65L808.13-600 599.76-391.87l-63.65-63.65 98.98-98.98H390.98q-60.13 0-104.12 38.92-43.99 38.93-43.99 96.78 0 57.84 43.99 96.89 43.99 39.04 104.12 39.04h286.15v91H390.98Z"/>
</svg>;
}

View File

@@ -0,0 +1,5 @@
export default function Stop({ fill }: { fill?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill={fill ?? "canvas"}>
<path d="M240-240v-480h480v480H240Z"/>
</svg>;
}

View File

@@ -46,6 +46,7 @@ University within the Software Project course.
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: .5rem;
} }
} }

View File

@@ -35,6 +35,11 @@ import {
RobotConnected RobotConnected
} from './MonitoringPageComponents'; } from './MonitoringPageComponents';
import ExperimentLogs from "./components/ExperimentLogs.tsx"; import ExperimentLogs from "./components/ExperimentLogs.tsx";
import Pause from "../../components/Icons/Pause.tsx";
import Play from "../../components/Icons/Play.tsx";
import Next from "../../components/Icons/Next.tsx";
import Replay from "../../components/Icons/Replay.tsx";
import Stop from "../../components/Icons/Stop.tsx";
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// 1. State management // 1. State management
@@ -238,34 +243,39 @@ function ControlPanel({
<h3>Experiment Controls</h3> <h3>Experiment Controls</h3>
<div className={styles.controlsButtons}> <div className={styles.controlsButtons}>
<button <button
aria-label="Pause"
className={!isPlaying ? styles.pausePlayActive : styles.pausePlayInactive} className={!isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
onClick={() => onAction("pause")} onClick={() => onAction("pause")}
disabled={loading} disabled={loading}
></button> ><Pause fill={"white"} /></button>
<button <button
aria-label="Play"
className={isPlaying ? styles.pausePlayActive : styles.pausePlayInactive} className={isPlaying ? styles.pausePlayActive : styles.pausePlayInactive}
onClick={() => onAction("play")} onClick={() => onAction("play")}
disabled={loading} disabled={loading}
></button> ><Play fill={"white"} /></button>
<button <button
aria-label="Next phase"
className={styles.next} className={styles.next}
onClick={() => onAction("nextPhase")} onClick={() => onAction("nextPhase")}
disabled={loading} disabled={loading}
></button> ><Next fill={"white"} /></button>
<button <button
aria-label="Reset experiment"
className={styles.restartExperiment} className={styles.restartExperiment}
onClick={onReset} onClick={onReset}
disabled={loading} disabled={loading}
></button> ><Replay fill={"white"} /></button>
<button <button
aria-label="Stop"
className={styles.stop} className={styles.stop}
onClick={() => onAction("stop")} onClick={() => onAction("stop")}
disabled={loading} disabled={loading}
></button> ><Stop fill={"white"} /></button>
</div> </div>
</div> </div>
); );

View File

@@ -122,8 +122,8 @@ describe('MonitoringPage', () => {
describe('Control Buttons', () => { describe('Control Buttons', () => {
test('Pause calls API and updates UI', async () => { test('Pause calls API and updates UI', async () => {
render(<MonitoringPage />); render(<MonitoringPage />);
const pauseBtn = screen.getByText('❚❚'); const pauseBtn = screen.getByRole('button', { name: /pause/i });
await act(async () => { await act(async () => {
fireEvent.click(pauseBtn); fireEvent.click(pauseBtn);
}); });
@@ -134,8 +134,8 @@ describe('MonitoringPage', () => {
test('Play calls API and updates UI', async () => { test('Play calls API and updates UI', async () => {
render(<MonitoringPage />); render(<MonitoringPage />);
const playBtn = screen.getByText('▶'); const playBtn = screen.getByRole('button', { name: /play/i });
await act(async () => { await act(async () => {
fireEvent.click(playBtn); fireEvent.click(playBtn);
}); });
@@ -146,35 +146,35 @@ describe('MonitoringPage', () => {
test('Next Phase calls API', async () => { test('Next Phase calls API', async () => {
render(<MonitoringPage />); render(<MonitoringPage />);
await act(async () => { await act(async () => {
fireEvent.click(screen.getByText('⏭')); fireEvent.click(screen.getByRole('button', { name: /next phase/i }));
}); });
expect(MonitoringAPI.nextPhase).toHaveBeenCalled(); expect(MonitoringAPI.nextPhase).toHaveBeenCalled();
}); });
test('Reset Experiment calls logic and resets state', async () => { test('Reset Experiment calls logic and resets state', async () => {
render(<MonitoringPage />); render(<MonitoringPage />);
// Mock graph reducer return // Mock graph reducer return
(VisProg.graphReducer as jest.Mock).mockReturnValue([{ id: 'new-phase' }]); (VisProg.graphReducer as jest.Mock).mockReturnValue([{ id: 'new-phase' }]);
await act(async () => { await act(async () => {
fireEvent.click(screen.getByText('⟲')); fireEvent.click(screen.getByRole('button', { name: /reset experiment/i }));
}); });
expect(VisProg.graphReducer).toHaveBeenCalled(); expect(VisProg.graphReducer).toHaveBeenCalled();
expect(mockSetProgramState).toHaveBeenCalledWith({ phases: [{ id: 'new-phase' }] }); expect(mockSetProgramState).toHaveBeenCalledWith({ phases: [{ id: 'new-phase' }] });
expect(VisProg.runProgram).toHaveBeenCalled(); expect(VisProg.runProgram).toHaveBeenCalled();
}); });
test('Reset Experiment handles errors gracefully', async () => { test('Reset Experiment handles errors gracefully', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
(VisProg.runProgram as jest.Mock).mockRejectedValue(new Error('Fail')); (VisProg.runProgram as jest.Mock).mockRejectedValue(new Error('Fail'));
render(<MonitoringPage />); render(<MonitoringPage />);
await act(async () => { await act(async () => {
fireEvent.click(screen.getByText('⟲')); fireEvent.click(screen.getByRole('button', { name: /reset experiment/i }));
}); });
expect(consoleSpy).toHaveBeenCalledWith('Failed to reset program:', expect.any(Error)); expect(consoleSpy).toHaveBeenCalledWith('Failed to reset program:', expect.any(Error));
consoleSpy.mockRestore(); consoleSpy.mockRestore();
}); });