Loading capif_frontend/src/configs/apiService.js +6 −0 Changes for capif_frontend/src/configs/apiService.js: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -18,6 +18,12 @@ export const onboardProvider = async ({ apiProvDomInfo }) => { return response.data } export const publishApi = async (providerId, api) => { const response = await axiosInstance.post(`/me/providers/${encodeURIComponent(providerId)}/apis`, api) return response.data } // GET invokers with pagination and date order export const getInvokersPagesOrder = async () => { try { Loading capif_frontend/src/pages/me/providers/[id].js 0 → 100644 +191 −0 Changes for capif_frontend/src/pages/me/providers/[id].js: 191 added lines, 0 removed lines. Original line number Diff line number Diff line import { useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/router' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Card from '@mui/material/Card' import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' import CircularProgress from '@mui/material/CircularProgress' import Alert from '@mui/material/Alert' import TextField from '@mui/material/TextField' import MenuItem from '@mui/material/MenuItem' import { useMutation, useQueryClient } from '@tanstack/react-query' import { publishApi } from 'src/configs/apiService' import { errorMessage } from 'src/configs/queryHelpers' const OPERATIONS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] const REQUIRED_TEXT = ['apiName', 'apiVersion', 'resourceName', 'uri', 'address'] const EMPTY_API = { apiName: '', apiVersion: 'v1', description: '', resourceName: '', resourceDescription: '', uri: '', operations: [], address: '', port: '' } // Mirrors the backend's parse_api_request, so invalid input never leaves the browser. const findInvalid = form => { const invalid = {} REQUIRED_TEXT.forEach(field => { if (!form[field].trim()) invalid[field] = true }) if (form.operations.length === 0) invalid.operations = true const port = Number(form.port) if (!/^\d+$/.test(form.port.trim()) || port < 1 || port > 65535) invalid.port = true return invalid } // A 400 from the backend lists the invalid fields; show them rather than a generic message. const publishErrorMessage = error => { const fields = error?.response?.data?.fields if (Array.isArray(fields) && fields.length > 0) return `Missing or invalid fields: ${fields.join(', ')}.` return errorMessage(error, 'Could not publish the API.') } const PublishApiForm = ({ providerId }) => { const queryClient = useQueryClient() const [form, setForm] = useState(EMPTY_API) const [submitted, setSubmitted] = useState(false) const [publishedId, setPublishedId] = useState(null) const mutation = useMutation({ mutationFn: api => publishApi(providerId, api), onSuccess: data => { // Invokers' discover results may now include this API. queryClient.invalidateQueries({ queryKey: ['invoker-apis'] }) setPublishedId(data?.apiId ?? null) setForm(EMPTY_API) setSubmitted(false) } }) const invalid = findInvalid(form) const showError = name => submitted && Boolean(invalid[name]) const handleChange = event => { const { name, value } = event.target setForm({ ...form, [name]: value }) } const handleSubmit = () => { setSubmitted(true) setPublishedId(null) if (Object.keys(invalid).length > 0) return mutation.mutate({ apiName: form.apiName.trim(), apiVersion: form.apiVersion.trim(), description: form.description.trim(), resourceName: form.resourceName.trim(), resourceDescription: form.resourceDescription.trim(), uri: form.uri.trim(), operations: form.operations, address: form.address.trim(), port: Number(form.port) }) } const textField = (name, label, props = {}) => ( <TextField margin='dense' fullWidth name={name} label={label} value={form[name]} onChange={handleChange} error={showError(name)} helperText={showError(name) ? 'Required' : ' '} {...props} /> ) return ( <> {publishedId && ( <Alert severity='success' sx={{ mb: 2 }}> API published: {publishedId} </Alert> )} {mutation.isError && ( <Alert severity='error' sx={{ mb: 2 }}> {publishErrorMessage(mutation.error)} </Alert> )} {textField('apiName', 'API name', { required: true, placeholder: 'hello_api' })} {textField('apiVersion', 'API version', { required: true })} {textField('description', 'Description')} {textField('resourceName', 'Resource name', { required: true, placeholder: 'hello-endpoint' })} {textField('resourceDescription', 'Resource description', { helperText: 'Defaults to the API description' })} {textField('uri', 'Resource URI', { required: true, placeholder: '/hello' })} <TextField select margin='dense' fullWidth required name='operations' label='Operations' value={form.operations} onChange={handleChange} SelectProps={{ multiple: true }} error={showError('operations')} helperText={showError('operations') ? 'Select at least one operation' : ' '} > {OPERATIONS.map(op => ( <MenuItem key={op} value={op}> {op} </MenuItem> ))} </TextField> {textField('address', 'Address', { required: true, placeholder: 'localhost' })} {textField('port', 'Port', { required: true, placeholder: '8088', helperText: showError('port') ? 'Enter a port between 1 and 65535' : ' ' })} <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}> <Button variant='contained' onClick={handleSubmit} disabled={mutation.isPending}> {mutation.isPending ? <CircularProgress size={24} /> : 'Publish'} </Button> </Box> </> ) } const ProviderDetailPage = () => { const router = useRouter() const providerId = typeof router.query.id === 'string' ? router.query.id : undefined return ( <Box sx={{ p: 6 }}> <Button component={Link} href='/me/providers' sx={{ mb: 4 }}> Back to providers </Button> <Card sx={{ mb: 6 }}> <CardHeader title={providerId ?? ''} subheader='Provider' /> </Card> <Card> <CardHeader title='Publish an API' /> <CardContent> {/* router.query is empty on the first render of a dynamic route */} {providerId ? <PublishApiForm key={providerId} providerId={providerId} /> : <CircularProgress />} </CardContent> </Card> </Box> ) } ProviderDetailPage.acl = { action: 'read', subject: 'user-area' } export default ProviderDetailPage No newline at end of file capif_frontend/src/pages/me/providers/index.js +6 −2 Changes for capif_frontend/src/pages/me/providers/index.js: 6 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -20,6 +20,9 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getMyProviders, onboardProvider } from 'src/configs/apiService' import { errorMessage, retryServerErrorsOnly } from 'src/configs/queryHelpers' import Link from 'next/link' import ListItemButton from '@mui/material/ListItemButton' const MY_PROVIDERS_KEY = ['my-providers'] const ProviderList = () => { Loading Loading @@ -48,12 +51,13 @@ const ProviderList = () => { return <Typography>{"You don't have a provider yet."}</Typography> } // Plain items for now: the detail page (/me/providers/[id]) comes with the publish form. return ( <List> {providers.map(id => ( <ListItem key={id} disableGutters> <ListItem key={id} disablePadding> <ListItemButton component={Link} href={`/me/providers/${encodeURIComponent(id)}`}> <ListItemText primary={id} /> </ListItemButton> </ListItem> ))} </List> Loading Loading
capif_frontend/src/configs/apiService.js +6 −0 Changes for capif_frontend/src/configs/apiService.js: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -18,6 +18,12 @@ export const onboardProvider = async ({ apiProvDomInfo }) => { return response.data } export const publishApi = async (providerId, api) => { const response = await axiosInstance.post(`/me/providers/${encodeURIComponent(providerId)}/apis`, api) return response.data } // GET invokers with pagination and date order export const getInvokersPagesOrder = async () => { try { Loading
capif_frontend/src/pages/me/providers/[id].js 0 → 100644 +191 −0 Changes for capif_frontend/src/pages/me/providers/[id].js: 191 added lines, 0 removed lines. Original line number Diff line number Diff line import { useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/router' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Card from '@mui/material/Card' import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' import CircularProgress from '@mui/material/CircularProgress' import Alert from '@mui/material/Alert' import TextField from '@mui/material/TextField' import MenuItem from '@mui/material/MenuItem' import { useMutation, useQueryClient } from '@tanstack/react-query' import { publishApi } from 'src/configs/apiService' import { errorMessage } from 'src/configs/queryHelpers' const OPERATIONS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] const REQUIRED_TEXT = ['apiName', 'apiVersion', 'resourceName', 'uri', 'address'] const EMPTY_API = { apiName: '', apiVersion: 'v1', description: '', resourceName: '', resourceDescription: '', uri: '', operations: [], address: '', port: '' } // Mirrors the backend's parse_api_request, so invalid input never leaves the browser. const findInvalid = form => { const invalid = {} REQUIRED_TEXT.forEach(field => { if (!form[field].trim()) invalid[field] = true }) if (form.operations.length === 0) invalid.operations = true const port = Number(form.port) if (!/^\d+$/.test(form.port.trim()) || port < 1 || port > 65535) invalid.port = true return invalid } // A 400 from the backend lists the invalid fields; show them rather than a generic message. const publishErrorMessage = error => { const fields = error?.response?.data?.fields if (Array.isArray(fields) && fields.length > 0) return `Missing or invalid fields: ${fields.join(', ')}.` return errorMessage(error, 'Could not publish the API.') } const PublishApiForm = ({ providerId }) => { const queryClient = useQueryClient() const [form, setForm] = useState(EMPTY_API) const [submitted, setSubmitted] = useState(false) const [publishedId, setPublishedId] = useState(null) const mutation = useMutation({ mutationFn: api => publishApi(providerId, api), onSuccess: data => { // Invokers' discover results may now include this API. queryClient.invalidateQueries({ queryKey: ['invoker-apis'] }) setPublishedId(data?.apiId ?? null) setForm(EMPTY_API) setSubmitted(false) } }) const invalid = findInvalid(form) const showError = name => submitted && Boolean(invalid[name]) const handleChange = event => { const { name, value } = event.target setForm({ ...form, [name]: value }) } const handleSubmit = () => { setSubmitted(true) setPublishedId(null) if (Object.keys(invalid).length > 0) return mutation.mutate({ apiName: form.apiName.trim(), apiVersion: form.apiVersion.trim(), description: form.description.trim(), resourceName: form.resourceName.trim(), resourceDescription: form.resourceDescription.trim(), uri: form.uri.trim(), operations: form.operations, address: form.address.trim(), port: Number(form.port) }) } const textField = (name, label, props = {}) => ( <TextField margin='dense' fullWidth name={name} label={label} value={form[name]} onChange={handleChange} error={showError(name)} helperText={showError(name) ? 'Required' : ' '} {...props} /> ) return ( <> {publishedId && ( <Alert severity='success' sx={{ mb: 2 }}> API published: {publishedId} </Alert> )} {mutation.isError && ( <Alert severity='error' sx={{ mb: 2 }}> {publishErrorMessage(mutation.error)} </Alert> )} {textField('apiName', 'API name', { required: true, placeholder: 'hello_api' })} {textField('apiVersion', 'API version', { required: true })} {textField('description', 'Description')} {textField('resourceName', 'Resource name', { required: true, placeholder: 'hello-endpoint' })} {textField('resourceDescription', 'Resource description', { helperText: 'Defaults to the API description' })} {textField('uri', 'Resource URI', { required: true, placeholder: '/hello' })} <TextField select margin='dense' fullWidth required name='operations' label='Operations' value={form.operations} onChange={handleChange} SelectProps={{ multiple: true }} error={showError('operations')} helperText={showError('operations') ? 'Select at least one operation' : ' '} > {OPERATIONS.map(op => ( <MenuItem key={op} value={op}> {op} </MenuItem> ))} </TextField> {textField('address', 'Address', { required: true, placeholder: 'localhost' })} {textField('port', 'Port', { required: true, placeholder: '8088', helperText: showError('port') ? 'Enter a port between 1 and 65535' : ' ' })} <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}> <Button variant='contained' onClick={handleSubmit} disabled={mutation.isPending}> {mutation.isPending ? <CircularProgress size={24} /> : 'Publish'} </Button> </Box> </> ) } const ProviderDetailPage = () => { const router = useRouter() const providerId = typeof router.query.id === 'string' ? router.query.id : undefined return ( <Box sx={{ p: 6 }}> <Button component={Link} href='/me/providers' sx={{ mb: 4 }}> Back to providers </Button> <Card sx={{ mb: 6 }}> <CardHeader title={providerId ?? ''} subheader='Provider' /> </Card> <Card> <CardHeader title='Publish an API' /> <CardContent> {/* router.query is empty on the first render of a dynamic route */} {providerId ? <PublishApiForm key={providerId} providerId={providerId} /> : <CircularProgress />} </CardContent> </Card> </Box> ) } ProviderDetailPage.acl = { action: 'read', subject: 'user-area' } export default ProviderDetailPage No newline at end of file
capif_frontend/src/pages/me/providers/index.js +6 −2 Changes for capif_frontend/src/pages/me/providers/index.js: 6 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -20,6 +20,9 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getMyProviders, onboardProvider } from 'src/configs/apiService' import { errorMessage, retryServerErrorsOnly } from 'src/configs/queryHelpers' import Link from 'next/link' import ListItemButton from '@mui/material/ListItemButton' const MY_PROVIDERS_KEY = ['my-providers'] const ProviderList = () => { Loading Loading @@ -48,12 +51,13 @@ const ProviderList = () => { return <Typography>{"You don't have a provider yet."}</Typography> } // Plain items for now: the detail page (/me/providers/[id]) comes with the publish form. return ( <List> {providers.map(id => ( <ListItem key={id} disableGutters> <ListItem key={id} disablePadding> <ListItemButton component={Link} href={`/me/providers/${encodeURIComponent(id)}`}> <ListItemText primary={id} /> </ListItemButton> </ListItem> ))} </List> Loading