Commit 2d25fc4c authored by Alex Kakiris's avatar Alex Kakiris
Browse files

Merge branch 'feat/invoker-discover-button' into 'dev'

Run discover operation from a button on the invoker page

See merge request !8
parents ce7f7bd7 c41b9f5a
Loading
Loading
Loading
Loading
+98 −29
Changes for capif_frontend/src/pages/me/invokers/[id].js: 98 added lines, 29 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'
@@ -7,49 +8,118 @@ 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 List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import ListItemText from '@mui/material/ListItemText'
import Typography from '@mui/material/Typography'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import { useQuery } from '@tanstack/react-query'

import { discoverApis } from 'src/configs/apiService'
import { errorMessage, retryServerErrorsOnly } from 'src/configs/queryHelpers'

const ApiList = ({ invokerId }) => {
  const { data, isPending, isError, error } = useQuery({
// Where an invoker would call the API: the first interface of the first AEF profile.
// Providers can publish without some of these fields, so every step is optional.
const endpointOf = api => {
  const iface = api?.aefProfiles?.[0]?.interfaceDescriptions?.[0]
  const host = iface?.fqdn ?? iface?.ipv4Addr ?? iface?.ipv6Addr

  if (!host) return '—'

  return iface?.port ? `${host}:${iface.port}` : host
}

const versionOf = api => api?.aefProfiles?.[0]?.versions?.[0]?.apiVersion ?? '—'

const DiscoverResults = ({ apis }) => {
  if (apis.length === 0) {
    return <Typography>No APIs found.</Typography>
  }

  return (
    <TableContainer>
      <Table aria-label='Discovered APIs'>
        <TableHead>
          <TableRow>
            <TableCell>API name</TableCell>
            <TableCell>API ID</TableCell>
            <TableCell>Description</TableCell>
            <TableCell>Version</TableCell>
            <TableCell>Endpoint</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {apis.map(api => (
            <TableRow key={api.apiId ?? api.apiName}>
              <TableCell>{api.apiName ?? '—'}</TableCell>
              <TableCell>{api.apiId ?? '—'}</TableCell>
              <TableCell>{api.description || '—'}</TableCell>
              <TableCell>{versionOf(api)}</TableCell>
              <TableCell>{endpointOf(api)}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  )
}

const DiscoverSection = ({ invokerId }) => {
  // A disabled query reports isPending before it has ever run, so "not run yet" comes from this flag.
  const [requested, setRequested] = useState(false)

  const { data, isPending, isFetching, isError, error, refetch } = useQuery({
    queryKey: ['invoker-apis', invokerId],
    queryFn: () => discoverApis(invokerId),
    enabled: Boolean(invokerId), // router.query is empty on the first render of a dynamic route
    retry: retryServerErrorsOnly
    enabled: Boolean(invokerId) && requested,
    retry: retryServerErrorsOnly,

    // Discover is an explicit action: only the button runs it, not returning to the tab.
    refetchOnWindowFocus: false,
    refetchOnReconnect: false
  })

  const apis = data?.serviceAPIDescriptions ?? []

  if (isPending) {
    return (
  // The first click enables the query; later clicks re-run it, since new publishes can appear.
  const handleDiscover = () => {
    if (requested) {
      refetch()
    } else {
      setRequested(true)
    }
  }

  let content

  if (!requested) {
    content = <Typography>Search for the APIs this invoker can call.</Typography>
  } else if (isPending) {
    content = (
      <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
        <CircularProgress />
      </Box>
    )
  }

  if (isError) {
    return <Alert severity='error'>{errorMessage(error, 'Could not load the APIs.')}</Alert>
  }

  if (apis.length === 0) {
    return <Typography>No APIs are available to this invoker yet.</Typography>
  } else if (isError) {
    content = <Alert severity='error'>{errorMessage(error, 'Could not discover APIs.')}</Alert>
  } else {
    content = <DiscoverResults apis={apis} />
  }

  return (
    <List>
      {apis.map(api => (
        <ListItem key={api.apiId} disableGutters>
          <ListItemText primary={api.apiName} secondary={api.apiId} />
        </ListItem>
      ))}
    </List>
    <Card>
      <CardHeader
        title='Discover APIs'
        action={
          <Button variant='contained' onClick={handleDiscover} disabled={isFetching}>
            {isFetching ? <CircularProgress size={24} /> : 'Discover'}
          </Button>
        }
      />
      <CardContent>{content}</CardContent>
    </Card>
  )
}

@@ -62,12 +132,11 @@ const InvokerDetailPage = () => {
      <Button component={Link} href='/me/invokers' sx={{ mb: 4 }}>
        Back to invokers
      </Button>
      <Card>
        <CardHeader title={invokerId ?? ''} subheader='Available APIs' />
        <CardContent>
          <ApiList invokerId={invokerId} />
        </CardContent>
      <Card sx={{ mb: 6 }}>
        <CardHeader title={invokerId ?? ''} subheader='Invoker' />
      </Card>
      {/* router.query is empty on the first render of a dynamic route */}
      {invokerId ? <DiscoverSection key={invokerId} invokerId={invokerId} /> : <CircularProgress />}
    </Box>
  )
}