smart-on-fhir/ehi-app

Add page to list jobs

Closed this issue · 2 comments

As part of the admin UI we need a page that will list all the export jobs currently available on the server. We can start simple and just list them without any kind of sorting or pagination, but some might have to be added later.

The server will have a list of "current" jobs which the app can fetch from https://ehi-server.herokuapp.com/jobs. The response should be simple (for now) array of zero or more job objects. Each job object will implement the interface at https://github.com/smart-on-fhir/ehi-server/blob/main/index.d.ts#L200-L236 minus the manifest property which is not needed in this case.

Clicking on a job listing should navigate us to a dedicated job page (say /admin/jobs/:id) with detailed info and possible actions. This page will be described in separate issue.

Please make sure this page can also handle:

  1. Empty list (no current jobs)
  2. Render error responses from the server (although we will have to figure out how to trigger those so that you can test that)
  3. Aborting requests due to page unload or other type of component unmount

@vlad-ignatov as far as Aborting requests due to page unload goes, did you have a preferred technique? I was reading up a bit on the AbortController API and, considering that's what supported by client-js, I figure that's the right approach but wanted to confirm.

Here is something I have used in other projects. You could use that as is or borrow ideas from it, or perhaps it can also be improved?

interface State<T = any> {
    loading: boolean
    error  : Error | null
    result : T | null
}

function reducer(state: State, payload: Partial<State>): State {
    return { ...state, ...payload };
}

export function useBackend<T=any>(fn: (signal?: AbortSignal) => Promise<T>, immediate = false)
{
    const [state, dispatch] = useReducer(reducer, {
        loading: immediate,
        error: null,
        result: null
    });

    const abortController = useMemo(() => new AbortController(), [])

    const execute = useCallback(() => {
        dispatch({ loading: true, result: null, error: null });
        return fn(abortController.signal).then(
            (result: T) => {
                if (!abortController.signal.aborted) {
                    dispatch({ loading: false, result })
                }
            },
            (error: Error) => {
                if (!abortController.signal.aborted) {
                    dispatch({ loading: false, error })
                }
            }
        );
    }, [fn, abortController.signal]);
    
    useEffect(() => {
        if (immediate) { 
            execute()
        }
    }, [execute, immediate]);

    useEffect(() => () => abortController.abort(), [ abortController ]);

    return {
        execute,
        loading: state.loading,
        result: state.result as (T | null),
        error: state.error
    };
}

And here is an example component using that:

export default function CreateView()
{
    // The subscription ID from the URL params
    const { id } = useParams();

    // Fetch the subscription by ID
    const { loading, error, result } = useBackend(
        useCallback(() => request("/api/requests/" + id), [id]),
        true
    );

    // Show loader whole the subscription is being loaded
    if (loading) {
        return <Loader msg="Loading subscription..." />
    }

    // If the subscription failed to load exit with an error message
    if (error) {
        return <AlertError>{`Error fetching subscription with id "${id}": ${error}`}</AlertError>
    }

    // If the subscription request was successful but did not return the expected data exit with an error message
    if (!result) {
        return <AlertError>{`Fetching subscription with id "${id}" produced empty response`}</AlertError>
    }

    // Eventually render a Breadcrumbs and the dashboard
    return (
        <div className="container">
            <Breadcrumbs links={[
                { name: "Home" , href: "/" },
                { name: "Create New Graph" }
            ]}/>
            <Dashboard view={{}} dataRequest={result as app.DataRequest} />
        </div>
    )
}