Commit 8c640a33 authored by Alex Kakiris's avatar Alex Kakiris
Browse files

add invoker onboarding dialog

parent 60cb5ec1
Loading
Loading
Loading
Loading
+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'
@@ -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) => {
@@ -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
  })
@@ -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) {
@@ -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',