Files
docspell/modules/webapp/src/main/elm/Page/Queue/View2.elm
2021-09-21 22:35:38 +02:00

498 lines
16 KiB
Elm

{-
Copyright 2020 Eike K. & Contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-}
module Page.Queue.View2 exposing (viewContent, viewSidebar)
import Api.Model.JobDetail exposing (JobDetail)
import Api.Model.JobLogEvent exposing (JobLogEvent)
import Comp.Progress
import Comp.YesNoDimmer
import Data.Flags exposing (Flags)
import Data.Priority
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Page.Queue exposing (Texts)
import Page.Queue.Data exposing (..)
import Styles as S
import Util.Time exposing (formatIsoDateTime)
viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar texts visible _ _ model =
let
count v =
case v of
CurrentJobs ->
List.length model.state.progress
QueueAll ->
List.length model.state.queued
+ List.length model.state.completed
QueueWaiting ->
List.length model.state.queued
QueueSuccess ->
filterJobDetails model.state.completed "success"
|> List.length
QueueError ->
filterJobDetails model.state.completed "failed"
|> List.length
QueueCancelled ->
filterJobDetails model.state.completed "cancelled"
|> List.length
tabLink cls v icon label =
a
[ href "#"
, class S.sidebarLink
, class cls
, classList [ ( "bg-blue-100 dark:bg-bluegray-600", model.queueView == v ) ]
, onClick (SetQueueView v)
]
[ i [ class icon ]
[]
, div
[ class "ml-3" ]
[ text label ]
, div [ class "ml-auto bg-gray-200 border rounded-full h-6 w-6 flex items-center justify-center text-xs dark:bg-bluegray-800 dark:text-bluegray-200 dark:border-bluegray-800 dark:bg-opacity-50" ]
[ count v |> String.fromInt |> text
]
]
in
div
[ id "sidebar"
, class S.sidebar
, class S.sidebarBg
, classList [ ( "hidden", not visible ) ]
]
[ div [ class "" ]
[ h1 [ class S.header1 ]
[ text texts.sidebarTitle
]
]
, div [ class "flex flex-col my-2" ]
[ tabLink "" CurrentJobs "fa fa-play-circle" texts.currentlyRunning
, tabLink "" QueueAll "fa fa-hourglass-half" texts.queue
, tabLink "ml-8" QueueWaiting "fa fa-clock" texts.waiting
, tabLink "ml-8" QueueError "fa fa-bolt" texts.errored
, tabLink "ml-8" QueueSuccess "fa fa-check" texts.success
, tabLink "ml-8" QueueCancelled "fa fa-times-circle" texts.cancelled
]
]
viewContent : Texts -> Flags -> UiSettings -> Model -> Html Msg
viewContent texts _ _ model =
let
gridStyle =
"grid gap-4 grid-cols-1 md:grid-cols-2"
message str =
div [ class "h-28 flex flex-col items-center justify-center w-full" ]
[ div [ class S.header2 ]
[ text str
]
]
in
div
[ class "py-2"
, class S.content
]
[ case model.showLog of
Just job ->
renderJobLog job
Nothing ->
span [ class "hidden" ] []
, case model.queueView of
CurrentJobs ->
if List.isEmpty model.state.progress then
message texts.noJobsRunning
else
div [ class "flex flex-col space-y-2" ]
(List.map (renderProgressCard texts model) model.state.progress)
QueueAll ->
if List.isEmpty model.state.completed && List.isEmpty model.state.completed then
message texts.noJobsDisplay
else
div [ class gridStyle ]
(List.map (renderInfoCard texts model)
(model.state.queued ++ model.state.completed)
)
QueueWaiting ->
if List.isEmpty model.state.queued then
message texts.noJobsWaiting
else
div [ class gridStyle ]
(List.map (renderInfoCard texts model) model.state.queued)
QueueError ->
let
items =
filterJobDetails model.state.completed "failed"
in
if List.isEmpty items then
message texts.noJobsFailed
else
div [ class gridStyle ]
(List.map (renderInfoCard texts model) items)
QueueSuccess ->
let
items =
filterJobDetails model.state.completed "success"
in
if List.isEmpty items then
message texts.noJobsSuccess
else
div [ class gridStyle ]
(List.map (renderInfoCard texts model) items)
QueueCancelled ->
let
items =
filterJobDetails model.state.completed "cancelled"
in
if List.isEmpty items then
message texts.noJobsCancelled
else
div [ class gridStyle ]
(List.map (renderInfoCard texts model) items)
]
filterJobDetails : List JobDetail -> String -> List JobDetail
filterJobDetails list state =
let
isState job =
state == job.state
in
List.filter isState list
renderJobLog : JobDetail -> Html Msg
renderJobLog job =
div
[ class " absolute top-12 left-0 w-full h-full-12 z-40 flex flex-col items-center px-4 py-2 "
, class "bg-white bg-opacity-80 dark:bg-black dark:bg-bluegray-900 dark:bg-opacity-90"
]
[ div [ class (S.box ++ "py-2 px-2 flex flex-col w-full") ]
[ div [ class "flex flex-row mb-4 px-2" ]
[ span [ class "font-semibold" ]
[ text job.name
]
, div [ class "flex-grow flex flex-row justify-end" ]
[ a
[ href "#"
, class S.link
, onClick QuitShowLog
]
[ i [ class "fa fa-times" ] []
]
]
]
, div [ class styleJobLog ]
(List.map renderLogLine job.logs)
]
]
renderProgressCard : Texts -> Model -> JobDetail -> Html Msg
renderProgressCard texts model job =
div [ class (S.box ++ "px-2 flex flex-col") ]
[ Comp.Progress.topAttachedIndicating job.progress
, Html.map (DimmerMsg job)
(Comp.YesNoDimmer.viewN
(model.cancelJobRequest == Just job.id)
(dimmerSettings texts)
model.deleteConfirm
)
, div [ class "py-2 flex flex-row x-space-2 items-center" ]
[ div [ class "flex flex-row items-center py-0.5" ]
[ i [ class "fa fa-circle-notch animate-spin" ] []
, span [ class "ml-2" ]
[ text job.name
]
]
, div [ class "flex-grow flex flex-row items-center justify-end" ]
[ div [ class S.basicLabel ]
[ text job.state
, div [ class "ml-3" ]
[ Maybe.withDefault "" job.worker |> text
]
]
, div [ class (S.basicLabel ++ "ml-2") ]
[ i [ class "fa fa-clock" ] []
, div [ class "ml-3" ]
[ getDuration model job |> Maybe.withDefault "-:-" |> text
]
]
]
]
, div [ class "py-2", id "joblog" ]
[ div [ class styleJobLog ]
(List.map renderLogLine job.logs)
]
, div [ class "py-2 flex flex-row justify-end" ]
[ button [ class S.secondaryButton, onClick (RequestCancelJob job) ]
[ text texts.basics.cancel
]
]
]
styleJobLog : String
styleJobLog =
"bg-gray-900 text-xs leading-5 px-2 py-1 font-mono text-gray-100 overflow-auto max-h-96 rounded"
renderLogLine : JobLogEvent -> Html Msg
renderLogLine log =
let
lineStyle =
case String.toLower log.level of
"info" ->
""
"debug" ->
"opacity-50"
"warn" ->
"text-yellow-400"
"error" ->
"text-red-400"
_ ->
""
in
span [ class lineStyle ]
[ formatIsoDateTime log.time |> text
, text ": "
, text log.message
, br [] []
]
isFinal : JobDetail -> Bool
isFinal job =
case job.state of
"failed" ->
True
"success" ->
True
"cancelled" ->
True
_ ->
False
dimmerSettings : Texts -> Comp.YesNoDimmer.Settings
dimmerSettings texts =
let
defaults =
Comp.YesNoDimmer.defaultSettings texts.deleteThisJob texts.basics.yes texts.basics.no
in
{ defaults
| headerClass = "text-lg text-white"
, headerIcon = ""
, extraClass = "rounded"
}
renderInfoCard : Texts -> Model -> JobDetail -> Html Msg
renderInfoCard texts model job =
let
prio =
Data.Priority.fromString job.priority
|> Maybe.withDefault Data.Priority.Low
color solid =
jobStateColor job solid
labelStyle solid =
" label min-h-6 inline text-xs font-semibold " ++ color solid ++ " "
in
div
[ class (S.box ++ "px-4 py-4 flex flex-col rounded relative")
]
[ Html.map (DimmerMsg job)
(Comp.YesNoDimmer.viewN
(model.cancelJobRequest == Just job.id)
(dimmerSettings texts)
model.deleteConfirm
)
, div [ class "flex flex-row" ]
[ div [ class "flex-grow items-center" ]
[ i
[ classList
[ ( "fa fa-check", job.state == "success" )
, ( "fa fa-redo", job.state == "stuck" )
, ( "fa fa-bolt", job.state == "failed" )
, ( "fa fa-meh-outline", job.state == "canceled" )
, ( "fa fa-cog", not (isFinal job) && job.state /= "stuck" )
]
, class "justify-center"
]
[]
, div [ class (labelStyle True ++ " ml-2") ]
[ text job.state
]
, div [ class "ml-2 break-all hidden sm:inline-block" ]
[ text job.name
]
]
, div [ class "flex flex-row space-x-2" ]
[ a
[ onClick (ShowLog job)
, href "#"
, class S.link
, classList [ ( "hidden", not (isFinal job || job.state == "stuck") ) ]
]
[ i
[ class "fa fa-file"
, title texts.showLog
]
[]
]
, a
[ title texts.remove
, href "#"
, class S.link
, onClick (RequestCancelJob job)
]
[ i
[ class "fa fa-times"
]
[]
]
, div
[ classList [ ( "hidden", isFinal job ) ]
]
[ div [ class "font-mono" ]
[ getDuration model job |> Maybe.withDefault "3:12" |> text
]
]
]
]
, div [ class "sm:hidden mt-1 break-all" ]
[ text job.name
]
, div [ class "my-2" ]
[ hr [ class S.border ] []
]
, div [ class "flex flex-row space-x-2 items-center flex-wrap" ]
[ div [ class "flex flex-row justify-start " ]
[ div [ class "text-xs font-semibold" ]
[ texts.formatDateTime job.submitted |> text
]
]
, div [ class "flex-grow flex flex-row justify-end space-x-2 flex-wrap" ]
[ div
[ class (labelStyle False)
, classList [ ( "hidden", not (isFinal job) ) ]
]
[ i [ class "fa fa-clock mr-3" ] []
, span []
[ getDuration model job |> Maybe.withDefault "-:-" |> text
]
]
, div [ class (labelStyle False) ]
[ span [ class "mr-3" ]
[ text texts.retries
]
, span []
[ job.retries |> String.fromInt |> text
]
]
, case job.state of
"waiting" ->
a
[ class (labelStyle False)
, onClick (ChangePrio job.id (Data.Priority.next prio))
, href "#"
, title texts.changePriority
]
[ i [ class "sort numeric up icon" ] []
, text "Prio"
, div [ class "detail" ]
[ code []
[ Data.Priority.fromString job.priority
|> Maybe.map Data.Priority.toName
|> Maybe.withDefault job.priority
|> text
]
]
]
_ ->
div
[ class (labelStyle False)
]
[ span [ class "mr-3" ]
[ text texts.prio
]
, code [ class "font-mono" ]
[ Data.Priority.fromString job.priority
|> Maybe.map Data.Priority.toName
|> Maybe.withDefault job.priority
|> text
]
]
]
]
]
jobStateColor : JobDetail -> Bool -> String
jobStateColor job solid =
case job.state of
"success" ->
if solid then
S.greenSolidLabel
else
S.greenBasicLabel
"failed" ->
if solid then
S.redSolidLabel
else
S.redBasicLabel
"canceled" ->
"text-orange-500 border-orange-500"
"stuck" ->
"text-purple-500 border-purple-500"
"scheduled" ->
"text-blue-500 border-blue-500"
"waiting" ->
"text-gray-500 border-gray-500"
_ ->
""