mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-07-04 16:48:26 +00:00
498 lines
16 KiB
Elm
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"
|
|
|
|
_ ->
|
|
""
|