mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-02 21:42:52 +00:00
Merge pull request #1167 from eikek/feature/job-count
Indicate number of running jobs in tob nav
This commit is contained in:
commit
63d5290761
@ -10,12 +10,12 @@ import cats.effect._
|
|||||||
|
|
||||||
import docspell.backend.auth.Login
|
import docspell.backend.auth.Login
|
||||||
import docspell.backend.fulltext.CreateIndex
|
import docspell.backend.fulltext.CreateIndex
|
||||||
|
import docspell.backend.msg.JobQueuePublish
|
||||||
import docspell.backend.ops._
|
import docspell.backend.ops._
|
||||||
import docspell.backend.signup.OSignup
|
import docspell.backend.signup.OSignup
|
||||||
import docspell.ftsclient.FtsClient
|
import docspell.ftsclient.FtsClient
|
||||||
import docspell.pubsub.api.PubSubT
|
import docspell.pubsub.api.PubSubT
|
||||||
import docspell.store.Store
|
import docspell.store.Store
|
||||||
import docspell.store.queue.JobQueue
|
|
||||||
import docspell.store.usertask.UserTaskStore
|
import docspell.store.usertask.UserTaskStore
|
||||||
import docspell.totp.Totp
|
import docspell.totp.Totp
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ object BackendApp {
|
|||||||
): Resource[F, BackendApp[F]] =
|
): Resource[F, BackendApp[F]] =
|
||||||
for {
|
for {
|
||||||
utStore <- UserTaskStore(store)
|
utStore <- UserTaskStore(store)
|
||||||
queue <- JobQueue(store)
|
queue <- JobQueuePublish(store, pubSubT)
|
||||||
totpImpl <- OTotp(store, Totp.default)
|
totpImpl <- OTotp(store, Totp.default)
|
||||||
loginImpl <- Login[F](store, Totp.default)
|
loginImpl <- Login[F](store, Totp.default)
|
||||||
signupImpl <- OSignup[F](store)
|
signupImpl <- OSignup[F](store)
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Eike K. & Contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package docspell.backend.msg
|
||||||
|
|
||||||
|
import cats.effect._
|
||||||
|
import cats.implicits._
|
||||||
|
|
||||||
|
import docspell.common.{Duration, Ident, Priority}
|
||||||
|
import docspell.pubsub.api.PubSubT
|
||||||
|
import docspell.store.Store
|
||||||
|
import docspell.store.queue.JobQueue
|
||||||
|
import docspell.store.records.RJob
|
||||||
|
|
||||||
|
final class JobQueuePublish[F[_]: Sync](delegate: JobQueue[F], pubsub: PubSubT[F])
|
||||||
|
extends JobQueue[F] {
|
||||||
|
|
||||||
|
private def msg(job: RJob): JobSubmitted =
|
||||||
|
JobSubmitted(job.id, job.group, job.task, job.args)
|
||||||
|
|
||||||
|
private def publish(job: RJob): F[Unit] =
|
||||||
|
pubsub.publish1(JobSubmitted.topic, msg(job)).as(())
|
||||||
|
|
||||||
|
def insert(job: RJob) =
|
||||||
|
delegate.insert(job).flatTap(_ => publish(job))
|
||||||
|
|
||||||
|
def insertIfNew(job: RJob) =
|
||||||
|
delegate.insertIfNew(job).flatTap {
|
||||||
|
case true => publish(job)
|
||||||
|
case false => ().pure[F]
|
||||||
|
}
|
||||||
|
|
||||||
|
def insertAll(jobs: Seq[RJob]) =
|
||||||
|
delegate.insertAll(jobs).flatTap { results =>
|
||||||
|
results.zip(jobs).traverse { case (res, job) =>
|
||||||
|
if (res) publish(job)
|
||||||
|
else ().pure[F]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def insertAllIfNew(jobs: Seq[RJob]) =
|
||||||
|
delegate.insertAllIfNew(jobs).flatTap { results =>
|
||||||
|
results.zip(jobs).traverse { case (res, job) =>
|
||||||
|
if (res) publish(job)
|
||||||
|
else ().pure[F]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def nextJob(prio: Ident => F[Priority], worker: Ident, retryPause: Duration) =
|
||||||
|
delegate.nextJob(prio, worker, retryPause)
|
||||||
|
}
|
||||||
|
|
||||||
|
object JobQueuePublish {
|
||||||
|
def apply[F[_]: Async](store: Store[F], pubSub: PubSubT[F]): Resource[F, JobQueue[F]] =
|
||||||
|
JobQueue(store).map(q => new JobQueuePublish[F](q, pubSub))
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Eike K. & Contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package docspell.backend.msg
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.pubsub.api.{Topic, TypedTopic}
|
||||||
|
|
||||||
|
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
|
||||||
|
import io.circe.{Decoder, Encoder}
|
||||||
|
|
||||||
|
final case class JobSubmitted(jobId: Ident, group: Ident, task: Ident, args: String)
|
||||||
|
|
||||||
|
object JobSubmitted {
|
||||||
|
|
||||||
|
implicit val jsonDecoder: Decoder[JobSubmitted] =
|
||||||
|
deriveDecoder
|
||||||
|
|
||||||
|
implicit val jsonEncoder: Encoder[JobSubmitted] =
|
||||||
|
deriveEncoder
|
||||||
|
|
||||||
|
val topic: TypedTopic[JobSubmitted] =
|
||||||
|
TypedTopic(Topic("job-submitted"))
|
||||||
|
}
|
@ -19,5 +19,5 @@ object Topics {
|
|||||||
|
|
||||||
/** A list of all topics. It is required to list every topic in use here! */
|
/** A list of all topics. It is required to list every topic in use here! */
|
||||||
val all: NonEmptyList[TypedTopic[_]] =
|
val all: NonEmptyList[TypedTopic[_]] =
|
||||||
NonEmptyList.of(JobDone.topic, CancelJob.topic, jobsNotify)
|
NonEmptyList.of(JobDone.topic, CancelJob.topic, jobsNotify, JobSubmitted.topic)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,8 @@ trait OJob[F[_]] {
|
|||||||
def cancelJob(id: Ident, collective: Ident): F[JobCancelResult]
|
def cancelJob(id: Ident, collective: Ident): F[JobCancelResult]
|
||||||
|
|
||||||
def setPriority(id: Ident, collective: Ident, prio: Priority): F[UpdateResult]
|
def setPriority(id: Ident, collective: Ident, prio: Priority): F[UpdateResult]
|
||||||
|
|
||||||
|
def getUnfinishedJobCount(collective: Ident): F[Int]
|
||||||
}
|
}
|
||||||
|
|
||||||
object OJob {
|
object OJob {
|
||||||
@ -93,5 +95,8 @@ object OJob {
|
|||||||
} yield result)
|
} yield result)
|
||||||
.getOrElse(JobCancelResult.jobNotFound)
|
.getOrElse(JobCancelResult.jobNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def getUnfinishedJobCount(collective: Ident): F[Int] =
|
||||||
|
store.transact(RJob.getUnfinishedCount(collective))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import fs2.concurrent.SignallingRef
|
|||||||
|
|
||||||
import docspell.analysis.TextAnalyser
|
import docspell.analysis.TextAnalyser
|
||||||
import docspell.backend.fulltext.CreateIndex
|
import docspell.backend.fulltext.CreateIndex
|
||||||
import docspell.backend.msg.{CancelJob, Topics}
|
import docspell.backend.msg.{CancelJob, JobQueuePublish, Topics}
|
||||||
import docspell.backend.ops._
|
import docspell.backend.ops._
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.ftsclient.FtsClient
|
import docspell.ftsclient.FtsClient
|
||||||
@ -126,13 +126,13 @@ object JoexAppImpl {
|
|||||||
pubSub: PubSub[F]
|
pubSub: PubSub[F]
|
||||||
): Resource[F, JoexApp[F]] =
|
): Resource[F, JoexApp[F]] =
|
||||||
for {
|
for {
|
||||||
queue <- JobQueue(store)
|
|
||||||
pstore <- PeriodicTaskStore.create(store)
|
pstore <- PeriodicTaskStore.create(store)
|
||||||
client = JoexClient(httpClient)
|
client = JoexClient(httpClient)
|
||||||
pubSubT = PubSubT(
|
pubSubT = PubSubT(
|
||||||
pubSub,
|
pubSub,
|
||||||
Logger.log4s(org.log4s.getLogger(s"joex-${cfg.appId.id}"))
|
Logger.log4s(org.log4s.getLogger(s"joex-${cfg.appId.id}"))
|
||||||
)
|
)
|
||||||
|
queue <- JobQueuePublish(store, pubSubT)
|
||||||
joex <- OJoex(pubSubT)
|
joex <- OJoex(pubSubT)
|
||||||
upload <- OUpload(store, queue, joex)
|
upload <- OUpload(store, queue, joex)
|
||||||
fts <- createFtsClient(cfg)(httpClient)
|
fts <- createFtsClient(cfg)(httpClient)
|
||||||
|
@ -138,7 +138,7 @@ object RestServer {
|
|||||||
token: AuthToken
|
token: AuthToken
|
||||||
): HttpRoutes[F] =
|
): HttpRoutes[F] =
|
||||||
Router(
|
Router(
|
||||||
"ws" -> WebSocketRoutes(token, topic, wsB),
|
"ws" -> WebSocketRoutes(token, restApp.backend, topic, wsB),
|
||||||
"auth" -> LoginRoutes.session(restApp.backend.login, cfg, token),
|
"auth" -> LoginRoutes.session(restApp.backend.login, cfg, token),
|
||||||
"tag" -> TagRoutes(restApp.backend, token),
|
"tag" -> TagRoutes(restApp.backend, token),
|
||||||
"equipment" -> EquipmentRoutes(restApp.backend, token),
|
"equipment" -> EquipmentRoutes(restApp.backend, token),
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
package docspell.restserver
|
package docspell.restserver
|
||||||
|
|
||||||
|
import cats.effect.Async
|
||||||
import fs2.Stream
|
import fs2.Stream
|
||||||
import fs2.concurrent.Topic
|
import fs2.concurrent.Topic
|
||||||
|
|
||||||
import docspell.backend.msg.JobDone
|
import docspell.backend.msg.{JobDone, JobSubmitted}
|
||||||
import docspell.common.ProcessItemArgs
|
|
||||||
import docspell.pubsub.api.PubSubT
|
import docspell.pubsub.api.PubSubT
|
||||||
import docspell.restserver.ws.OutputEvent
|
import docspell.restserver.ws.OutputEvent
|
||||||
|
|
||||||
@ -18,15 +18,20 @@ import docspell.restserver.ws.OutputEvent
|
|||||||
*/
|
*/
|
||||||
object Subscriptions {
|
object Subscriptions {
|
||||||
|
|
||||||
def apply[F[_]](
|
def apply[F[_]: Async](
|
||||||
wsTopic: Topic[F, OutputEvent],
|
wsTopic: Topic[F, OutputEvent],
|
||||||
pubSub: PubSubT[F]
|
pubSub: PubSubT[F]
|
||||||
): Stream[F, Nothing] =
|
): Stream[F, Nothing] =
|
||||||
jobDone(pubSub).through(wsTopic.publish)
|
jobDone(pubSub).merge(jobSubmitted(pubSub)).through(wsTopic.publish)
|
||||||
|
|
||||||
def jobDone[F[_]](pubSub: PubSubT[F]): Stream[F, OutputEvent] =
|
def jobDone[F[_]](pubSub: PubSubT[F]): Stream[F, OutputEvent] =
|
||||||
pubSub
|
pubSub
|
||||||
.subscribe(JobDone.topic)
|
.subscribe(JobDone.topic)
|
||||||
.filter(m => m.body.task == ProcessItemArgs.taskName)
|
.map(m => OutputEvent.JobDone(m.body.group, m.body.task))
|
||||||
.map(m => OutputEvent.ItemProcessed(m.body.group))
|
|
||||||
|
def jobSubmitted[F[_]](pubSub: PubSubT[F]): Stream[F, OutputEvent] =
|
||||||
|
pubSub
|
||||||
|
.subscribe(JobSubmitted.topic)
|
||||||
|
.map(m => OutputEvent.JobSubmitted(m.body.group, m.body.task))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -31,18 +31,26 @@ object OutputEvent {
|
|||||||
Msg("keep-alive", ()).asJson
|
Msg("keep-alive", ()).asJson
|
||||||
}
|
}
|
||||||
|
|
||||||
final case class ItemProcessed(collective: Ident) extends OutputEvent {
|
final case class JobSubmitted(group: Ident, task: Ident) extends OutputEvent {
|
||||||
def forCollective(token: AuthToken): Boolean =
|
|
||||||
token.account.collective == collective
|
|
||||||
|
|
||||||
def asJson: Json =
|
|
||||||
Msg("item-processed", ()).asJson
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class JobsWaiting(group: Ident, count: Int) extends OutputEvent {
|
|
||||||
def forCollective(token: AuthToken): Boolean =
|
def forCollective(token: AuthToken): Boolean =
|
||||||
token.account.collective == group
|
token.account.collective == group
|
||||||
|
|
||||||
|
def asJson: Json =
|
||||||
|
Msg("job-submitted", task).asJson
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class JobDone(group: Ident, task: Ident) extends OutputEvent {
|
||||||
|
def forCollective(token: AuthToken): Boolean =
|
||||||
|
token.account.collective == group
|
||||||
|
|
||||||
|
def asJson: Json =
|
||||||
|
Msg("job-done", task).asJson
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class JobsWaiting(collective: Ident, count: Int) extends OutputEvent {
|
||||||
|
def forCollective(token: AuthToken): Boolean =
|
||||||
|
token.account.collective == collective
|
||||||
|
|
||||||
def asJson: Json =
|
def asJson: Json =
|
||||||
Msg("jobs-waiting", count).asJson
|
Msg("jobs-waiting", count).asJson
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,11 @@
|
|||||||
package docspell.restserver.ws
|
package docspell.restserver.ws
|
||||||
|
|
||||||
import cats.effect.Async
|
import cats.effect.Async
|
||||||
|
import cats.implicits._
|
||||||
import fs2.concurrent.Topic
|
import fs2.concurrent.Topic
|
||||||
import fs2.{Pipe, Stream}
|
import fs2.{Pipe, Stream}
|
||||||
|
|
||||||
|
import docspell.backend.BackendApp
|
||||||
import docspell.backend.auth.AuthToken
|
import docspell.backend.auth.AuthToken
|
||||||
|
|
||||||
import org.http4s.HttpRoutes
|
import org.http4s.HttpRoutes
|
||||||
@ -22,6 +24,7 @@ object WebSocketRoutes {
|
|||||||
|
|
||||||
def apply[F[_]: Async](
|
def apply[F[_]: Async](
|
||||||
user: AuthToken,
|
user: AuthToken,
|
||||||
|
backend: BackendApp[F],
|
||||||
topic: Topic[F, OutputEvent],
|
topic: Topic[F, OutputEvent],
|
||||||
wsb: WebSocketBuilder2[F]
|
wsb: WebSocketBuilder2[F]
|
||||||
): HttpRoutes[F] = {
|
): HttpRoutes[F] = {
|
||||||
@ -29,11 +32,18 @@ object WebSocketRoutes {
|
|||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of { case GET -> Root =>
|
HttpRoutes.of { case GET -> Root =>
|
||||||
|
val init =
|
||||||
|
for {
|
||||||
|
jc <- backend.job.getUnfinishedJobCount(user.account.collective)
|
||||||
|
msg = OutputEvent.JobsWaiting(user.account.collective, jc)
|
||||||
|
} yield Text(msg.encode)
|
||||||
|
|
||||||
val toClient: Stream[F, WebSocketFrame.Text] =
|
val toClient: Stream[F, WebSocketFrame.Text] =
|
||||||
topic
|
Stream.eval(init) ++
|
||||||
.subscribe(500)
|
topic
|
||||||
.filter(_.forCollective(user))
|
.subscribe(500)
|
||||||
.map(msg => Text(msg.encode))
|
.filter(_.forCollective(user))
|
||||||
|
.map(msg => Text(msg.encode))
|
||||||
|
|
||||||
val toServer: Pipe[F, WebSocketFrame, Unit] =
|
val toServer: Pipe[F, WebSocketFrame, Unit] =
|
||||||
_.map(_ => ())
|
_.map(_ => ())
|
||||||
|
@ -30,9 +30,9 @@ trait JobQueue[F[_]] {
|
|||||||
*/
|
*/
|
||||||
def insertIfNew(job: RJob): F[Boolean]
|
def insertIfNew(job: RJob): F[Boolean]
|
||||||
|
|
||||||
def insertAll(jobs: Seq[RJob]): F[Int]
|
def insertAll(jobs: Seq[RJob]): F[List[Boolean]]
|
||||||
|
|
||||||
def insertAllIfNew(jobs: Seq[RJob]): F[Int]
|
def insertAllIfNew(jobs: Seq[RJob]): F[List[Boolean]]
|
||||||
|
|
||||||
def nextJob(
|
def nextJob(
|
||||||
prio: Ident => F[Priority],
|
prio: Ident => F[Priority],
|
||||||
@ -77,26 +77,24 @@ object JobQueue {
|
|||||||
else insert(job).as(true)
|
else insert(job).as(true)
|
||||||
} yield ret
|
} yield ret
|
||||||
|
|
||||||
def insertAll(jobs: Seq[RJob]): F[Int] =
|
def insertAll(jobs: Seq[RJob]): F[List[Boolean]] =
|
||||||
jobs.toList
|
jobs.toList
|
||||||
.traverse(j => insert(j).attempt)
|
.traverse(j => insert(j).attempt)
|
||||||
.flatMap(_.traverse {
|
.flatMap(_.traverse {
|
||||||
case Right(()) => 1.pure[F]
|
case Right(()) => true.pure[F]
|
||||||
case Left(ex) =>
|
case Left(ex) =>
|
||||||
logger.error(ex)("Could not insert job. Skipping it.").as(0)
|
logger.error(ex)("Could not insert job. Skipping it.").as(false)
|
||||||
|
|
||||||
})
|
})
|
||||||
.map(_.sum)
|
|
||||||
|
|
||||||
def insertAllIfNew(jobs: Seq[RJob]): F[Int] =
|
def insertAllIfNew(jobs: Seq[RJob]): F[List[Boolean]] =
|
||||||
jobs.toList
|
jobs.toList
|
||||||
.traverse(j => insertIfNew(j).attempt)
|
.traverse(j => insertIfNew(j).attempt)
|
||||||
.flatMap(_.traverse {
|
.flatMap(_.traverse {
|
||||||
case Right(true) => 1.pure[F]
|
case Right(true) => true.pure[F]
|
||||||
case Right(false) => 0.pure[F]
|
case Right(false) => false.pure[F]
|
||||||
case Left(ex) =>
|
case Left(ex) =>
|
||||||
logger.error(ex)("Could not insert job. Skipping it.").as(0)
|
logger.error(ex)("Could not insert job. Skipping it.").as(false)
|
||||||
})
|
})
|
||||||
.map(_.sum)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -300,4 +300,10 @@ object RJob {
|
|||||||
where(T.tracker === trackerId, T.state.in(JobState.notDone))
|
where(T.tracker === trackerId, T.state.in(JobState.notDone))
|
||||||
).query[RJob].option
|
).query[RJob].option
|
||||||
|
|
||||||
|
def getUnfinishedCount(group: Ident): ConnectionIO[Int] =
|
||||||
|
run(
|
||||||
|
select(count(T.id)),
|
||||||
|
from(T),
|
||||||
|
T.group === group && T.state.in(JobState.notDone)
|
||||||
|
).query[Int].unique
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ type alias Model =
|
|||||||
, anonymousUiLang : UiLanguage
|
, anonymousUiLang : UiLanguage
|
||||||
, langMenuOpen : Bool
|
, langMenuOpen : Bool
|
||||||
, showNewItemsArrived : Bool
|
, showNewItemsArrived : Bool
|
||||||
|
, jobsWaiting : Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -129,6 +130,7 @@ init key url flags_ settings =
|
|||||||
, anonymousUiLang = Messages.UiLanguage.English
|
, anonymousUiLang = Messages.UiLanguage.English
|
||||||
, langMenuOpen = False
|
, langMenuOpen = False
|
||||||
, showNewItemsArrived = False
|
, showNewItemsArrived = False
|
||||||
|
, jobsWaiting = 0
|
||||||
}
|
}
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ Cmd.map UserSettingsMsg uc
|
[ Cmd.map UserSettingsMsg uc
|
||||||
|
@ -311,20 +311,28 @@ updateWithSub msg model =
|
|||||||
|
|
||||||
ReceiveWsMessage data ->
|
ReceiveWsMessage data ->
|
||||||
case data of
|
case data of
|
||||||
Ok ItemProcessed ->
|
Ok (JobDone task) ->
|
||||||
let
|
let
|
||||||
newModel =
|
isProcessItem =
|
||||||
{ model | showNewItemsArrived = True }
|
task == "process-item"
|
||||||
in
|
|
||||||
case model.page of
|
|
||||||
HomePage ->
|
|
||||||
updateHome texts Page.Home.Data.RefreshView newModel
|
|
||||||
|
|
||||||
_ ->
|
newModel =
|
||||||
( newModel, Cmd.none, Sub.none )
|
{ model
|
||||||
|
| showNewItemsArrived = isProcessItem
|
||||||
|
, jobsWaiting = max 0 (model.jobsWaiting - 1)
|
||||||
|
}
|
||||||
|
in
|
||||||
|
if model.page == HomePage && isProcessItem then
|
||||||
|
updateHome texts Page.Home.Data.RefreshView newModel
|
||||||
|
|
||||||
|
else
|
||||||
|
( newModel, Cmd.none, Sub.none )
|
||||||
|
|
||||||
|
Ok (JobSubmitted _) ->
|
||||||
|
( { model | jobsWaiting = model.jobsWaiting + 1 }, Cmd.none, Sub.none )
|
||||||
|
|
||||||
Ok (JobsWaiting n) ->
|
Ok (JobsWaiting n) ->
|
||||||
( model, Cmd.none, Sub.none )
|
( { model | jobsWaiting = max 0 n }, Cmd.none, Sub.none )
|
||||||
|
|
||||||
Err err ->
|
Err err ->
|
||||||
( model, Cmd.none, Sub.none )
|
( model, Cmd.none, Sub.none )
|
||||||
|
@ -259,10 +259,21 @@ dataMenu texts _ model =
|
|||||||
div [ class "relative" ]
|
div [ class "relative" ]
|
||||||
[ a
|
[ a
|
||||||
[ class dropdownLink
|
[ class dropdownLink
|
||||||
|
, class "inline-block relative"
|
||||||
, onClick ToggleNavMenu
|
, onClick ToggleNavMenu
|
||||||
, href "#"
|
, href "#"
|
||||||
]
|
]
|
||||||
[ i [ class "fa fa-cogs" ] []
|
[ i [ class "fa fa-cogs" ] []
|
||||||
|
, div
|
||||||
|
[ class "h-5 w-5 rounded-full text-xs px-1 py-1 absolute top-1 left-1 font-bold"
|
||||||
|
, class "dark:bg-lightblue-500 dark:border-gray-50 dark:text-gray-800"
|
||||||
|
, class "bg-blue-500 text-gray-50"
|
||||||
|
, classList [ ( "hidden", model.jobsWaiting <= 0 ) ]
|
||||||
|
]
|
||||||
|
[ div [ class "-mt-0.5 ml-0.5" ]
|
||||||
|
[ text (String.fromInt model.jobsWaiting)
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
, div
|
, div
|
||||||
[ class dropdownMenu
|
[ class dropdownMenu
|
||||||
@ -301,7 +312,14 @@ dataMenu texts _ model =
|
|||||||
, dataPageLink model
|
, dataPageLink model
|
||||||
QueuePage
|
QueuePage
|
||||||
[]
|
[]
|
||||||
[ i [ class "fa fa-tachometer-alt w-6" ] []
|
[ i
|
||||||
|
[ if model.jobsWaiting <= 0 then
|
||||||
|
class "fa fa-tachometer-alt w-6"
|
||||||
|
|
||||||
|
else
|
||||||
|
class "fa fa-circle dark:text-lightblue-500 text-blue-500"
|
||||||
|
]
|
||||||
|
[]
|
||||||
, span [ class "ml-1" ]
|
, span [ class "ml-1" ]
|
||||||
[ text texts.processingQueue
|
[ text texts.processingQueue
|
||||||
]
|
]
|
||||||
|
@ -11,7 +11,8 @@ import Json.Decode as D
|
|||||||
|
|
||||||
|
|
||||||
type ServerEvent
|
type ServerEvent
|
||||||
= ItemProcessed
|
= JobSubmitted String
|
||||||
|
| JobDone String
|
||||||
| JobsWaiting Int
|
| JobsWaiting Int
|
||||||
|
|
||||||
|
|
||||||
@ -30,8 +31,13 @@ decode json =
|
|||||||
decodeTag : String -> D.Decoder ServerEvent
|
decodeTag : String -> D.Decoder ServerEvent
|
||||||
decodeTag tag =
|
decodeTag tag =
|
||||||
case tag of
|
case tag of
|
||||||
"item-processed" ->
|
"job-done" ->
|
||||||
D.succeed ItemProcessed
|
D.field "content" D.string
|
||||||
|
|> D.map JobDone
|
||||||
|
|
||||||
|
"job-submitted" ->
|
||||||
|
D.field "content" D.string
|
||||||
|
|> D.map JobSubmitted
|
||||||
|
|
||||||
"jobs-waiting" ->
|
"jobs-waiting" ->
|
||||||
D.field "content" D.int
|
D.field "content" D.int
|
||||||
|
Loading…
x
Reference in New Issue
Block a user