packages feed

ihp-1.4.0: IHP/Job/Dashboard/View.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE GADTs #-}

{-|
Module: IHP.Job.Dashboard.View
Description:  Views for Job dashboard
-}
module IHP.Job.Dashboard.View where

import IHP.Prelude
import IHP.ViewPrelude (JobStatus(..), ControllerContext, Html, View, hsx, html, timeAgo)
import qualified Data.List as List
import IHP.Job.Dashboard.Types
import IHP.Job.Dashboard.Utils
import IHP.Pagination.Types
import IHP.Pagination.ViewFunctions

-- | Provides a type-erased view. This allows us to specify a view as a return type without needed
-- to know exactly what type the view will be, which in turn allows for custom implmentations of
-- almost all the view functions in this module. Go GADTs!
data SomeView where
    SomeView :: forall a. (View a) => a -> SomeView

-- | Since the only constructor for 'SomeView' requires that it is passed a 'View', we can use
-- that to implement a 'View' instance for 'SomeView'
instance View SomeView where
    html (SomeView a) = let ?view = a in IHP.ViewPrelude.html a

-- | Define how to render a list of views as a view. Just concatenate them together!
instance (View a) => View [a] where
    html [] = [hsx||]
    html (x:xs) =
        -- need to nest let's here in order to satisfy the implicit ?view parameter for 'html'.
        -- ?view needs to be the type of the view being rendered, so set it before each render
        -- here we render single view
        let ?view = x in
            let current = IHP.ViewPrelude.html x in
                -- now rendering a list view
                let ?view = xs in
                    let rest = IHP.ViewPrelude.html xs in
                        [hsx|{current}{rest}|]

-- | A view containing no data. Used occasionally as a default implementation for some functions.
data EmptyView = EmptyView
instance View EmptyView where
    html _ = [hsx||]

-- | A view constructed from some HTML.
newtype HtmlView = HtmlView Html
instance View HtmlView where
    html (HtmlView html) = [hsx|{html}|]

renderStatus job = case job.status of
    JobStatusNotStarted -> [hsx|<span class="badge badge-secondary">Not Started</span>|]
    JobStatusRunning -> [hsx|<span class="badge badge-primary">Running</span>|]
    JobStatusFailed -> [hsx|<span class="badge badge-danger" title="Last Error" data-container="body" data-toggle="popover" data-placement="left" data-content={fromMaybe "" (job.lastError)}>Failed</span>|]
    JobStatusSucceeded -> [hsx|<span class="badge badge-success">Succeeded</span>|]
    JobStatusRetry -> [hsx|<span class="badge badge-warning" title="Last Error" data-container="body" data-toggle="popover" data-placement="left" data-content={fromMaybe "" (job.lastError)}>Retry</span>|]
    JobStatusTimedOut -> [hsx|<span class="badge badge-danger" >Timed Out</span>|]

-- BASE JOB VIEW HELPERS --------------------------------

renderBaseJobTable :: Text -> [BaseJob] -> Html
renderBaseJobTable table rows =
    let
        headers :: [Text] = ["ID", "Updated At", "Status", "", ""]
        humanTitle = table |> columnNameToFieldLabel
    in [hsx|
    <div>
        <div class="d-flex justify-content-between align-items-center">
            <h3>{humanTitle}</h3>
            {renderNewBaseJobLink table}
        </div>
        <table class="table table-sm table-hover">
            <thead>
                <tr>
                    {forEach headers renderHeader}
                </tr>
            </thead>

            <tbody>
                {forEach rows renderBaseJobTableRow}
            </tbody>
        </table>
        <a href={ListJobAction table 1} class="link-primary">See all {humanTitle}</a>
        <hr />
    </div>
|]
    where renderHeader field = [hsx|<th>{field}</th>|]

renderBaseJobTablePaginated :: Text -> [BaseJob] -> Pagination -> Html
renderBaseJobTablePaginated table jobs pagination =
    let
        headers :: [Text] = ["ID", "Updated At", "Status", "", ""]
        lastJobIndex = (List.length jobs) - 1
    in
        [hsx|
            <div>
                <div class="d-flex justify-content-between align-items-center">
                    <h3>{table |> columnNameToFieldLabel}</h3>
                    {renderNewBaseJobLink table}
                </div>
                <table class="table table-sm table-hover">
                    <thead>
                        <tr>
                            {forEach headers renderHeader}
                        </tr>
                    </thead>

                    <tbody>
                        {forEach jobs renderBaseJobTableRow}
                    </tbody>
                </table>
            </div>
            {renderPagination pagination}
        |]
    where
        renderHeader field = [hsx|<th>{field}</th>|]

renderBaseJobTableRow :: BaseJob -> Html
renderBaseJobTableRow job = [hsx|
        <tr>
            <td>{job.id}</td>
            <td>{job.updatedAt |> timeAgo}</td>
            <td>{renderStatus job}</td>
            <td><a href={ViewJobAction (job.table) (job.id)} class="text-primary">Show</a></td>
            <td>
                <form action={RetryJobAction (job.table) (job.id)} method="POST">
                    <button type="submit" style={retryButtonStyle} class="btn btn-link text-secondary">Retry</button>
                </form>
            </td>
        </tr>
    |]

-- | Link included in table to create a new job.
renderNewBaseJobLink :: Text -> Html
renderNewBaseJobLink table =
    let
        link = "/jobs/CreateJob?tableName=" <> table
    in [hsx|
        <form action={link}>
            <button type="submit" class="btn btn-primary btn-sm">+ New Job</button>
        </form>
    |]

renderNewBaseJobForm :: Text -> Html
renderNewBaseJobForm table = [hsx|
    <br>
        <h5>New Job: {table}</h5>
    <br>
    <form action="/jobs/CreateJob" method="POST">
        <input type="hidden" id="tableName" name="tableName" value={table}>
        <button type="submit" class="btn btn-primary">New Job</button>
    </form>
|]

renderBaseJobDetailView :: BaseJob -> Html
renderBaseJobDetailView job = let table = job.table in [hsx|
    <br>
        <h5>Viewing Job {job.id} in {table |> columnNameToFieldLabel}</h5>
    <br>
    <table class="table">
        <tbody>
            <tr>
                <th>Updated At</th>
                <td>{job.updatedAt |> timeAgo} ({job.updatedAt})</td>
            </tr>
            <tr>
                <th>Created At</th>
                <td>{job.createdAt |> timeAgo} ({job.createdAt})</td>
            </tr>
            <tr>
                <th>Status</th>
                <td>{renderStatus job}</td>
            </tr>
            <tr>
                <th>Last Error</th>
                <td>{fromMaybe "No error" (job.lastError)}</td>
            </tr>
        </tbody>
    </table>

    <div class="d-flex flex-row">
        <form class="mr-2" action="/jobs/DeleteJob" method="POST">
            <input type="hidden" id="tableName" name="tableName" value={table}>
            <input type="hidden" id="id" name="id" value={tshow $ job.id}>
            <button type="submit" class="btn btn-danger">Delete</button>
        </form>
        <form action="/jobs/RetryJob" method="POST">
            <input type="hidden" id="tableName" name="tableName" value={table}>
            <input type="hidden" id="id" name="id" value={tshow $ job.id}>
            <button type="submit" class="btn btn-primary">Run again</button>
        </form>
    </div>
|]
------------------------------------------------------------------

-- TABLE VIEWABLE view helpers -----------------------------------
makeDashboardSectionFromTableViewable :: forall a. (TableViewable a
    , ?context :: ControllerContext
    , ?modelContext :: ModelContext) => IO SomeView
makeDashboardSectionFromTableViewable = do
    indexRows <- getIndex @a
    pure $ SomeView $ HtmlView $ renderTableViewableTable indexRows

renderTableViewableTable :: forall a. TableViewable a => [a] -> Html
renderTableViewableTable rows = let
        headers = tableHeaders @a
        title = tableTitle @a
        link = newJobLink @a
        renderRow = renderTableRow @a
        table = modelTableName @a
    in [hsx|
    <div>
        <div class="d-flex justify-content-between align-items-center">
            <h3>{title}</h3>
            {link}
        </div>
        <table class="table table-sm table-hover">
            <thead>
                <tr>
                    {forEach headers renderHeader}
                </tr>
            </thead>

            <tbody>
                {forEach rows renderRow}
            </tbody>
        </table>
        <a href={ListJobAction table 1} class="link-primary">See all {title}</a>
        <hr />
    </div>
|]
    where renderHeader field = [hsx|<th>{field}</th>|]



makeListPageFromTableViewable :: forall a. (TableViewable a, ?context :: ControllerContext, ?modelContext :: ModelContext) => Int -> Int -> IO SomeView
makeListPageFromTableViewable page pageSize = do
    pageData <- getPage @a (page - 1) pageSize
    numPages <- numberOfPagesForTable (modelTableName @a) pageSize
    pure $ SomeView $ HtmlView $ renderTableViewableTablePaginated pageData page numPages

renderTableViewableTablePaginated :: forall a. TableViewable a => [a] -> Int -> Int -> Html
renderTableViewableTablePaginated jobs page totalPages =
    let
        title = tableTitle @a
        table = modelTableName @a
        headers = tableHeaders @a
        lastJobIndex = (List.length jobs) - 1
        newLink = newJobLink @a
    in
        [hsx|
            <div>
                <div class="d-flex justify-content-between align-items-center">
                    <h3>{title}</h3>
                    {newLink}
                </div>
                <table class="table table-sm table-hover">
                    <thead>
                        <tr>
                            {forEach headers renderHeader}
                        </tr>
                    </thead>

                    <tbody>
                        {forEach jobs renderTableRow}
                    </tbody>
                </table>
            </div>
            <nav aria-label="Page navigation example">
                <ul class="pagination justify-content-end">
                    {renderPrev}
                    {when (totalPages /= 1) renderDest}
                    {renderNext}
                </ul>
            </nav>
        |]
    where
        renderHeader field = [hsx|<th>{field}</th>|]
        renderDest = let table = modelTableName @a in [hsx|<li class="page-item active"><a class="page-link" href={ListJobAction table page}>{page}</a></li>|]
        renderPrev
            | page == 1 = [hsx||]
            | otherwise = let table = modelTableName @a in [hsx|
                <li class="page-item">
                    <a class="page-link" href={ListJobAction table (page - 1)} aria-label="Previous">
                        <span aria-hidden="true">&laquo;</span>
                        <span class="sr-only">Previous</span>
                    </a>
                </li>
        |]
        renderNext
            | page == totalPages || totalPages == 0 = [hsx||]
            | otherwise = let table = modelTableName @a in [hsx|
                <li class="page-item">
                    <a class="page-link" href={ListJobAction table (page + 1)} aria-label="Next">
                        <span aria-hidden="true">&raquo;</span>
                        <span class="sr-only">Next</span>
                    </a>
                </li>
            |]
------------------------------------------------------------

retryButtonStyle :: Text
retryButtonStyle = "outline: none !important; padding: 0; border: 0; vertical-align: baseline;"