Loading capif_frontend/src/pages/me/invokers/index.js +116 −8 Changes for capif_frontend/src/pages/me/invokers/index.js: 116 added lines, 8 removed lines. Original line number Diff line number Diff line import { useState } from 'react' import Box from '@mui/material/Box' import Card from '@mui/material/Card' import CardHeader from '@mui/material/CardHeader' Loading @@ -8,9 +9,18 @@ import List from '@mui/material/List' import ListItem from '@mui/material/ListItem' import ListItemText from '@mui/material/ListItemText' import Typography from '@mui/material/Typography' import { useQuery } from '@tanstack/react-query' import Button from '@mui/material/Button' import Dialog from '@mui/material/Dialog' import DialogTitle from '@mui/material/DialogTitle' import DialogContent from '@mui/material/DialogContent' import DialogActions from '@mui/material/DialogActions' import TextField from '@mui/material/TextField' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getMyInvokers } from 'src/configs/apiService' import { getMyInvokers, onboardInvoker } from 'src/configs/apiService' const MY_INVOKERS_KEY = ['my-invokers'] const EMPTY_FORM = { apiInvokerInformation: '', notificationDestination: '' } // Client errors (e.g. 403 no CAPIF credentials) won't change on retry; only retry server-side failures. const retryServerErrorsOnly = (failureCount, error) => { Loading @@ -20,15 +30,16 @@ const retryServerErrorsOnly = (failureCount, error) => { return failureCount < 3 } const errorMessage = error => { // Backend messages are deliberately generic, so show them with a fallback. const errorMessage = (error, fallback) => { if (error?.response?.status === 403) return 'This account has no CAPIF credentials.' return error?.response?.data?.error || 'Could not load your invokers.' return error?.response?.data?.error || fallback } const InvokerList = () => { const { data, isPending, isError, error } = useQuery({ queryKey: ['my-invokers'], queryKey: MY_INVOKERS_KEY, queryFn: getMyInvokers, retry: retryServerErrorsOnly }) Loading @@ -45,7 +56,7 @@ const InvokerList = () => { } if (isError) { return <Alert severity='error'>{errorMessage(error)}</Alert> return <Alert severity='error'>{errorMessage(error, 'Could not load your invokers.')}</Alert> } if (invokers.length === 0) { Loading @@ -63,16 +74,113 @@ const InvokerList = () => { ) } const MyInvokersPage = () => ( const NewInvokerDialog = ({ open, onClose }) => { const queryClient = useQueryClient() const [form, setForm] = useState(EMPTY_FORM) const [submitted, setSubmitted] = useState(false) const mutation = useMutation({ mutationFn: onboardInvoker, onSuccess: () => { queryClient.invalidateQueries({ queryKey: MY_INVOKERS_KEY }) resetAndClose() } }) const resetAndClose = () => { setForm(EMPTY_FORM) setSubmitted(false) mutation.reset() onClose() } const missing = { apiInvokerInformation: !form.apiInvokerInformation.trim(), notificationDestination: !form.notificationDestination.trim() } const handleChange = event => { const { name, value } = event.target setForm({ ...form, [name]: value }) } const handleSubmit = () => { setSubmitted(true) if (missing.apiInvokerInformation || missing.notificationDestination) return mutation.mutate({ apiInvokerInformation: form.apiInvokerInformation.trim(), notificationDestination: form.notificationDestination.trim() }) } return ( <Dialog open={open} onClose={mutation.isPending ? undefined : resetAndClose} fullWidth maxWidth='sm'> <DialogTitle>New invoker</DialogTitle> <DialogContent> {mutation.isError && ( <Alert severity='error' sx={{ mb: 2 }}> {errorMessage(mutation.error, 'Could not onboard the invoker.')} </Alert> )} <TextField margin='dense' name='apiInvokerInformation' label='Invoker information' fullWidth required value={form.apiInvokerInformation} onChange={handleChange} error={submitted && missing.apiInvokerInformation} helperText={submitted && missing.apiInvokerInformation ? 'Required' : ' '} /> <TextField margin='dense' name='notificationDestination' label='Notification destination' placeholder='http://localhost:8086/callback' fullWidth required value={form.notificationDestination} onChange={handleChange} error={submitted && missing.notificationDestination} helperText={submitted && missing.notificationDestination ? 'Required' : ' '} /> </DialogContent> <DialogActions> <Button onClick={resetAndClose} disabled={mutation.isPending}> Cancel </Button> <Button onClick={handleSubmit} disabled={mutation.isPending}> {mutation.isPending ? <CircularProgress size={24} /> : 'Create'} </Button> </DialogActions> </Dialog> ) } const MyInvokersPage = () => { const [dialogOpen, setDialogOpen] = useState(false) return ( <Box sx={{ p: 6 }}> <Card> <CardHeader title='Your invokers' /> <CardHeader title='Your invokers' action={ <Button variant='contained' onClick={() => setDialogOpen(true)}> New invoker </Button> } /> <CardContent> <InvokerList /> </CardContent> </Card> <NewInvokerDialog open={dialogOpen} onClose={() => setDialogOpen(false)} /> </Box> ) } MyInvokersPage.acl = { action: 'read', Loading Loading
capif_frontend/src/pages/me/invokers/index.js +116 −8 Changes for capif_frontend/src/pages/me/invokers/index.js: 116 added lines, 8 removed lines. Original line number Diff line number Diff line import { useState } from 'react' import Box from '@mui/material/Box' import Card from '@mui/material/Card' import CardHeader from '@mui/material/CardHeader' Loading @@ -8,9 +9,18 @@ import List from '@mui/material/List' import ListItem from '@mui/material/ListItem' import ListItemText from '@mui/material/ListItemText' import Typography from '@mui/material/Typography' import { useQuery } from '@tanstack/react-query' import Button from '@mui/material/Button' import Dialog from '@mui/material/Dialog' import DialogTitle from '@mui/material/DialogTitle' import DialogContent from '@mui/material/DialogContent' import DialogActions from '@mui/material/DialogActions' import TextField from '@mui/material/TextField' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getMyInvokers } from 'src/configs/apiService' import { getMyInvokers, onboardInvoker } from 'src/configs/apiService' const MY_INVOKERS_KEY = ['my-invokers'] const EMPTY_FORM = { apiInvokerInformation: '', notificationDestination: '' } // Client errors (e.g. 403 no CAPIF credentials) won't change on retry; only retry server-side failures. const retryServerErrorsOnly = (failureCount, error) => { Loading @@ -20,15 +30,16 @@ const retryServerErrorsOnly = (failureCount, error) => { return failureCount < 3 } const errorMessage = error => { // Backend messages are deliberately generic, so show them with a fallback. const errorMessage = (error, fallback) => { if (error?.response?.status === 403) return 'This account has no CAPIF credentials.' return error?.response?.data?.error || 'Could not load your invokers.' return error?.response?.data?.error || fallback } const InvokerList = () => { const { data, isPending, isError, error } = useQuery({ queryKey: ['my-invokers'], queryKey: MY_INVOKERS_KEY, queryFn: getMyInvokers, retry: retryServerErrorsOnly }) Loading @@ -45,7 +56,7 @@ const InvokerList = () => { } if (isError) { return <Alert severity='error'>{errorMessage(error)}</Alert> return <Alert severity='error'>{errorMessage(error, 'Could not load your invokers.')}</Alert> } if (invokers.length === 0) { Loading @@ -63,16 +74,113 @@ const InvokerList = () => { ) } const MyInvokersPage = () => ( const NewInvokerDialog = ({ open, onClose }) => { const queryClient = useQueryClient() const [form, setForm] = useState(EMPTY_FORM) const [submitted, setSubmitted] = useState(false) const mutation = useMutation({ mutationFn: onboardInvoker, onSuccess: () => { queryClient.invalidateQueries({ queryKey: MY_INVOKERS_KEY }) resetAndClose() } }) const resetAndClose = () => { setForm(EMPTY_FORM) setSubmitted(false) mutation.reset() onClose() } const missing = { apiInvokerInformation: !form.apiInvokerInformation.trim(), notificationDestination: !form.notificationDestination.trim() } const handleChange = event => { const { name, value } = event.target setForm({ ...form, [name]: value }) } const handleSubmit = () => { setSubmitted(true) if (missing.apiInvokerInformation || missing.notificationDestination) return mutation.mutate({ apiInvokerInformation: form.apiInvokerInformation.trim(), notificationDestination: form.notificationDestination.trim() }) } return ( <Dialog open={open} onClose={mutation.isPending ? undefined : resetAndClose} fullWidth maxWidth='sm'> <DialogTitle>New invoker</DialogTitle> <DialogContent> {mutation.isError && ( <Alert severity='error' sx={{ mb: 2 }}> {errorMessage(mutation.error, 'Could not onboard the invoker.')} </Alert> )} <TextField margin='dense' name='apiInvokerInformation' label='Invoker information' fullWidth required value={form.apiInvokerInformation} onChange={handleChange} error={submitted && missing.apiInvokerInformation} helperText={submitted && missing.apiInvokerInformation ? 'Required' : ' '} /> <TextField margin='dense' name='notificationDestination' label='Notification destination' placeholder='http://localhost:8086/callback' fullWidth required value={form.notificationDestination} onChange={handleChange} error={submitted && missing.notificationDestination} helperText={submitted && missing.notificationDestination ? 'Required' : ' '} /> </DialogContent> <DialogActions> <Button onClick={resetAndClose} disabled={mutation.isPending}> Cancel </Button> <Button onClick={handleSubmit} disabled={mutation.isPending}> {mutation.isPending ? <CircularProgress size={24} /> : 'Create'} </Button> </DialogActions> </Dialog> ) } const MyInvokersPage = () => { const [dialogOpen, setDialogOpen] = useState(false) return ( <Box sx={{ p: 6 }}> <Card> <CardHeader title='Your invokers' /> <CardHeader title='Your invokers' action={ <Button variant='contained' onClick={() => setDialogOpen(true)}> New invoker </Button> } /> <CardContent> <InvokerList /> </CardContent> </Card> <NewInvokerDialog open={dialogOpen} onClose={() => setDialogOpen(false)} /> </Box> ) } MyInvokersPage.acl = { action: 'read', Loading