feat: v0.1 boilerplate web-ui for testing actions.#70
feat: v0.1 boilerplate web-ui for testing actions.#70chrisdoehring wants to merge 1 commit intomainfrom
Conversation
|
@marianobrc this is the same code I had previously PR'd from the onyesha repo (minus the Onyesha specific things). |
There was a problem hiding this comment.
Pull request overview
Adds a new local React web UI to exercise the Action Runner service and view Gundi API connections, with OpenID Connect (OIDC) authentication support and local Docker Compose/docs updates to run everything together.
Changes:
- Introduces a React + MUI UI for listing actions, executing actions (including schema-driven forms), and viewing connections.
- Adds OIDC login/logout flow, token handling helpers, and protected routing.
- Updates local Docker Compose + local dev docs to include the new
web-uiservice and troubleshooting guides.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| local/web-ui/start-dev.sh | Dev helper script to install deps and run the UI locally. |
| local/web-ui/src/services/authService.js | OIDC client wrapper for login/logout/token/header helpers. |
| local/web-ui/src/index.js | React entrypoint mounting the app. |
| local/web-ui/src/index.css | Base CSS styles. |
| local/web-ui/src/contexts/ConnectionContext.js | Global state for selected connection. |
| local/web-ui/src/contexts/AuthContext.js | React auth state + helpers exposed via context. |
| local/web-ui/src/config/auth.js | Central auth/API configuration values for the UI. |
| local/web-ui/src/components/SilentCallback.js | Route handler for OIDC silent renew callback. |
| local/web-ui/src/components/ProtectedRoute.js | Route guard for authenticated-only content. |
| local/web-ui/src/components/Login.js | Login screen initiating OIDC redirect. |
| local/web-ui/src/components/Header.js | Top navigation + user menu + logout. |
| local/web-ui/src/components/DynamicForm.js | Schema-driven action execution form + load stored config. |
| local/web-ui/src/components/ConfigViewer.js | Connections list + filtering/search + details drawer. |
| local/web-ui/src/components/Callback.js | OIDC redirect callback handler. |
| local/web-ui/src/components/ActionsList.js | Lists actions exposed by the local FastAPI service. |
| local/web-ui/src/components/ActionExecute.js | Action execution page (dynamic form + manual overrides). |
| local/web-ui/src/App.js | App routing + theming + provider wiring. |
| local/web-ui/public/manifest.json | PWA manifest. |
| local/web-ui/public/index.html | UI HTML shell. |
| local/web-ui/package.json | UI dependencies and scripts. |
| local/web-ui/TROUBLESHOOTING.md | OIDC/CORS troubleshooting guide. |
| local/web-ui/README.md | UI overview, usage, and configuration notes. |
| local/web-ui/Dockerfile | Container build for the UI (build + serve static assets). |
| local/web-ui/AUTHENTICATION.md | OIDC setup instructions. |
| local/web-ui/.gitignore | Ignores node/build/env artifacts. |
| local/docker-compose.yml | Adds web-ui service + healthcheck adjustments. |
| local/LOCAL_DEVELOPMENT.md | Documents new UI and authentication in local workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| this.userManager.events.addAccessTokenExpired(() => { | ||
| console.log('Access token expired'); | ||
| this.signinSilent(); |
There was a problem hiding this comment.
addAccessTokenExpired calls this.signinSilent() without awaiting or handling rejections. If silent signin fails, this can produce an unhandled promise rejection. Handle the promise (e.g., void ...catch(...)) and decide how the app should react on failure.
| this.signinSilent(); | |
| void this.signinSilent().catch((error) => { | |
| console.error('Silent signin failed after access token expiration:', error); | |
| }); |
| ### Environment Variables | ||
| - `REACT_APP_API_URL` - URL of the FastAPI service (defaults to `http://localhost:8080`) |
There was a problem hiding this comment.
REACT_APP_API_URL is documented here, but the code currently hardcodes FastAPI URLs (e.g., http://localhost:8080) and does not read this env var. Either update the code to use REACT_APP_API_URL or adjust the docs to reflect the actual configuration mechanism.
| ### Environment Variables | |
| - `REACT_APP_API_URL` - URL of the FastAPI service (defaults to `http://localhost:8080`) | |
| The FastAPI service URL is currently configured via the React development server proxy | |
| (see the `proxy` setting in `package.json`) and the local Docker Compose setup, which | |
| connects the web UI to the FastAPI service running on port `8080`. The `REACT_APP_API_URL` | |
| environment variable is not used by the current implementation. |
| Drawer, | ||
| Slide, | ||
| Backdrop |
There was a problem hiding this comment.
Slide and Backdrop are imported from @mui/material but never used, which adds noise and can trigger lint warnings. Remove unused imports.
| Drawer, | |
| Slide, | |
| Backdrop | |
| Drawer |
| const fetchActions = async () => { | ||
| try { | ||
| setLoading(true); | ||
| // Use the FastAPI service URL that the browser can access | ||
| const response = await axios.get('http://localhost:8080/v1/actions/'); | ||
| setActions(response.data); | ||
| setError(null); |
There was a problem hiding this comment.
FastAPI calls are hardcoded to http://localhost:8080, which bypasses CRA's proxy setting and makes the UI harder to run in non-localhost setups. Consider centralizing the API base URL (e.g., from process.env.REACT_APP_API_URL) and using relative paths or an axios instance.
| const fetchSchema = useCallback(async () => { | ||
| try { | ||
| setSchemaLoading(true); | ||
| const response = await axios.get(`http://localhost:8080/v1/actions/${actionId}/schema`); | ||
| setSchema(response.data); | ||
| } catch (err) { | ||
| console.error('Error fetching schema:', err); | ||
| // Don't set error state here, just log it | ||
| } finally { | ||
| setSchemaLoading(false); | ||
| } | ||
| }, [actionId]); | ||
|
|
||
| // Fetch schema when component mounts | ||
| useEffect(() => { | ||
| fetchSchema(); | ||
| }, [fetchSchema]); | ||
|
|
||
| const handleInputChange = (field) => (event) => { | ||
| setFormData({ | ||
| ...formData, | ||
| [field]: event.target.value | ||
| }); | ||
| }; | ||
|
|
||
| const handleSwitchChange = (field) => (event) => { | ||
| setFormData({ | ||
| ...formData, | ||
| [field]: event.target.checked | ||
| }); | ||
| }; | ||
|
|
||
| const handleConfigOverrideChange = (key) => (event) => { | ||
| setFormData({ | ||
| ...formData, | ||
| config_overrides: { | ||
| ...formData.config_overrides, | ||
| [key]: event.target.value | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| const handleTabChange = (event, newValue) => { | ||
| setTabValue(newValue); | ||
| }; | ||
|
|
||
| const handleSubmit = async (event) => { | ||
| event.preventDefault(); | ||
| setLoading(true); | ||
| setError(null); | ||
| setResult(null); | ||
|
|
||
| try { | ||
| // Use the FastAPI service URL that the browser can access | ||
| const response = await axios.post('http://localhost:8080/v1/actions/execute', formData); | ||
| setResult(response.data); | ||
| } catch (err) { | ||
| setError(err.response?.data?.detail || 'Failed to execute action'); | ||
| console.error('Error executing action:', err); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| const handleDynamicFormSubmit = async (formData) => { | ||
| setLoading(true); | ||
| setError(null); | ||
| setResult(null); | ||
|
|
||
| try { | ||
| const requestData = { | ||
| integration_id: formData.integration_id, | ||
| action_id: actionId, | ||
| run_in_background: formData.run_in_background, | ||
| config_overrides: formData.config_overrides | ||
| }; | ||
|
|
||
| const response = await axios.post('http://localhost:8080/v1/actions/execute', requestData); | ||
| setResult(response.data); |
There was a problem hiding this comment.
Action execution and schema fetch are hardcoded to http://localhost:8080. This makes the UI inflexible outside the default local setup and ignores package.json proxy. Prefer a centralized/configurable API base URL (env-driven) and reuse it for all FastAPI requests.
| type="number" | ||
| label={fieldSchema.title || fieldName} | ||
| value={value} | ||
| onChange={(e) => handleConfigOverrideChange(fieldName, parseFloat(e.target.value) || '')} |
There was a problem hiding this comment.
Number parsing uses parseFloat(e.target.value) || '', which converts an entered 0 into '' and prevents users from setting a value of zero. Handle the empty-string case explicitly and otherwise parse to a number (and consider Number.isNaN checks).
| onChange={(e) => handleConfigOverrideChange(fieldName, parseFloat(e.target.value) || '')} | |
| onChange={(e) => { | |
| const rawValue = e.target.value; | |
| if (rawValue === '') { | |
| handleConfigOverrideChange(fieldName, ''); | |
| return; | |
| } | |
| const parsedValue = parseFloat(rawValue); | |
| handleConfigOverrideChange( | |
| fieldName, | |
| Number.isNaN(parsedValue) ? '' : parsedValue | |
| ); | |
| }} |
| const initializeAuth = async () => { | ||
| try { | ||
| setIsLoading(true); | ||
| setError(null); | ||
| await authService.initialize(); | ||
| setUser(authService.getUser()); | ||
| setIsAuthenticated(authService.isUserAuthenticated()); | ||
| } catch (err) { | ||
| console.error('Error initializing authentication:', err); | ||
| setError(err.message); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| initializeAuth(); |
There was a problem hiding this comment.
AuthProvider initializes auth state once but doesn't subscribe to oidc-client user/token events. As a result, user/isAuthenticated in context can become stale after silent renew, token expiry, or signout from another tab. Consider wiring userManager.events (userLoaded/userUnloaded/accessTokenExpired) to update React state and cleaning up listeners on unmount.
| const initializeAuth = async () => { | |
| try { | |
| setIsLoading(true); | |
| setError(null); | |
| await authService.initialize(); | |
| setUser(authService.getUser()); | |
| setIsAuthenticated(authService.isUserAuthenticated()); | |
| } catch (err) { | |
| console.error('Error initializing authentication:', err); | |
| setError(err.message); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| initializeAuth(); | |
| let isMounted = true; | |
| const syncAuthStateFromService = () => { | |
| if (!isMounted) { | |
| return; | |
| } | |
| setUser(authService.getUser()); | |
| setIsAuthenticated(authService.isUserAuthenticated()); | |
| }; | |
| const initializeAuth = async () => { | |
| try { | |
| setIsLoading(true); | |
| setError(null); | |
| await authService.initialize(); | |
| syncAuthStateFromService(); | |
| } catch (err) { | |
| console.error('Error initializing authentication:', err); | |
| setError(err.message); | |
| } finally { | |
| if (isMounted) { | |
| setIsLoading(false); | |
| } | |
| } | |
| }; | |
| initializeAuth(); | |
| const userManager = authService.userManager; | |
| const events = userManager && userManager.events; | |
| if (events) { | |
| const handleUserLoaded = () => { | |
| syncAuthStateFromService(); | |
| }; | |
| const handleUserUnloaded = () => { | |
| if (!isMounted) { | |
| return; | |
| } | |
| setUser(null); | |
| setIsAuthenticated(false); | |
| }; | |
| const handleAccessTokenExpired = () => { | |
| if (!isMounted) { | |
| return; | |
| } | |
| // Conservatively mark the user as signed out when the access token expires. | |
| setUser(null); | |
| setIsAuthenticated(false); | |
| }; | |
| events.addUserLoaded(handleUserLoaded); | |
| events.addUserUnloaded(handleUserUnloaded); | |
| events.addAccessTokenExpired(handleAccessTokenExpired); | |
| return () => { | |
| isMounted = false; | |
| events.removeUserLoaded(handleUserLoaded); | |
| events.removeUserUnloaded(handleUserUnloaded); | |
| events.removeAccessTokenExpired(handleAccessTokenExpired); | |
| }; | |
| } | |
| return () => { | |
| isMounted = false; | |
| }; |
| const { handleSigninCallback } = useAuth(); | ||
| const navigate = useNavigate(); | ||
| const [error, setError] = useState(null); | ||
| const [isLoading, setIsLoading] = useState(true); |
There was a problem hiding this comment.
Unused variable isLoading.
| @@ -0,0 +1,30 @@ | |||
| import React, { useEffect, useRef } from 'react'; | |||
There was a problem hiding this comment.
Unused import React.
| import React, { useEffect, useRef } from 'react'; | |
| import { useEffect, useRef } from 'react'; |
| Drawer, | ||
| Slide, | ||
| Backdrop |
There was a problem hiding this comment.
Unused imports Backdrop, Slide.
| Drawer, | |
| Slide, | |
| Backdrop | |
| Drawer |
This pull request introduces a new React-based web UI for the Gundi Integration Actions Execution Service, integrates OpenID Connect authentication, and updates local development documentation and Docker Compose setup to support the new UI. The changes add user-friendly features for action execution and API integration, along with comprehensive documentation and troubleshooting guides for authentication.
Major changes include:
Web UI Addition & Features
local/web-ui/with features such as action listing and execution, dynamic forms, real-time results, Gundi API integration, and user authentication/profile management. [1] [2] [3] [4] [5].gitignorefor the web UI to exclude dependencies, build artifacts, and environment files from version control.Authentication Integration
AUTHENTICATION.mdandTROUBLESHOOTING.md. [1] [2]Docker Compose & Local Development Enhancements
docker-compose.ymlto add theweb-uiservice, configure health checks, and ensure the web UI is built and served automatically. Also improved the FastAPI health check and added platform specification for the pubsub emulator. [1] [2]LOCAL_DEVELOPMENT.mdto document the new web UI, authentication setup, and troubleshooting steps for local deployment.These changes collectively provide a modern, authenticated, and user-friendly interface for managing and executing Gundi integration actions, along with robust developer documentation and improved local development workflows.