action_job.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. package worker
  2. import (
  3. "fmt"
  4. "github.com/qor5/admin/activity"
  5. "github.com/qor5/admin/presets"
  6. "github.com/qor5/ui/vuetify"
  7. "github.com/qor5/web"
  8. h "github.com/theplant/htmlgo"
  9. )
  10. const (
  11. ActionJobInputParams = "worker_action_job_input_params"
  12. ActionJobCreate = "worker_action_job_create"
  13. ActionJobResponse = "worker_action_job_response"
  14. ActionJobClose = "worker_action_job_close"
  15. ActionJobProgressing = "worker_action_job_progressing"
  16. )
  17. var (
  18. DefaultOriginalPageContextHandler = func(ctx *web.EventContext) map[string]interface{} {
  19. return map[string]interface{}{
  20. "URL": ctx.R.Header.Get("Referer"),
  21. }
  22. }
  23. actionJobs = map[string]*ActionJobBuilder{}
  24. )
  25. type ActionJobBuilder struct {
  26. fullname string
  27. shortname string
  28. description string //optional
  29. hasParams bool
  30. displayLog bool //optional
  31. progressingInterval int
  32. b *Builder // worker builder
  33. jb *JobBuilder // job builder
  34. }
  35. func (b *Builder) ActionJob(jobName string, model *presets.ModelBuilder, hander JobHandler) *ActionJobBuilder {
  36. if jobName == "" {
  37. panic("job name is required")
  38. }
  39. if hander == nil {
  40. panic("job handler is required")
  41. }
  42. fullname := fmt.Sprintf("Action Job - %s - %s", model.Info().Label(), jobName)
  43. if actionJobs[fullname] != nil {
  44. return actionJobs[fullname]
  45. }
  46. action := &ActionJobBuilder{
  47. fullname: fullname,
  48. shortname: jobName,
  49. progressingInterval: 2000,
  50. jb: b.NewJob(fullname).Handler(hander),
  51. b: b,
  52. }
  53. actionJobs[fullname] = action
  54. action.jb.global = false
  55. return action
  56. }
  57. func (action *ActionJobBuilder) Params(params interface{}) *ActionJobBuilder {
  58. action.hasParams = true
  59. action.jb.Resource(params)
  60. return action
  61. }
  62. func (action *ActionJobBuilder) Global(b bool) *ActionJobBuilder {
  63. action.jb.global = b
  64. return action
  65. }
  66. func (action *ActionJobBuilder) ProgressingInterval(interval int) *ActionJobBuilder {
  67. action.progressingInterval = interval
  68. return action
  69. }
  70. func (action *ActionJobBuilder) ContextHandler(handler func(*web.EventContext) map[string]interface{}) *ActionJobBuilder {
  71. action.jb.contextHandler = handler
  72. return action
  73. }
  74. func (action *ActionJobBuilder) DisplayLog(b bool) *ActionJobBuilder {
  75. action.displayLog = b
  76. return action
  77. }
  78. func (action *ActionJobBuilder) Description(description string) *ActionJobBuilder {
  79. action.description = description
  80. return action
  81. }
  82. func (action ActionJobBuilder) GetParamsModelBuilder() *presets.ModelBuilder {
  83. return action.jb.rmb
  84. }
  85. func (action ActionJobBuilder) URL() string {
  86. return web.Plaid().URL(action.b.mb.Info().ListingHref()).EventFunc(ActionJobInputParams).Query("jobName", action.fullname).Go()
  87. }
  88. func (b *Builder) eventActionJobCreate(ctx *web.EventContext) (r web.EventResponse, err error) {
  89. var (
  90. jobName = ctx.R.FormValue("jobName")
  91. config = actionJobs[jobName]
  92. qorJob = &QorJob{Job: jobName}
  93. )
  94. if config == nil {
  95. return r, fmt.Errorf("job %s not found", jobName)
  96. }
  97. job, err := b.createJob(ctx, qorJob)
  98. if err != nil {
  99. return
  100. }
  101. if b.ab != nil {
  102. b.ab.AddRecords(activity.ActivityCreate, ctx.R.Context(), job)
  103. }
  104. r.VarsScript = web.Plaid().
  105. URL(b.mb.Info().ListingHref()).
  106. EventFunc(ActionJobResponse).
  107. Query(presets.ParamID, fmt.Sprint(job.ID)).
  108. Query("jobID", fmt.Sprintf("%d", job.ID)).
  109. Query("jobName", job.Job).
  110. Go()
  111. return
  112. }
  113. func (b *Builder) eventActionJobInputParams(ctx *web.EventContext) (r web.EventResponse, err error) {
  114. var (
  115. jobName = ctx.R.FormValue("jobName")
  116. msgr = presets.MustGetMessages(ctx.R)
  117. config = actionJobs[jobName]
  118. )
  119. if config == nil {
  120. return r, fmt.Errorf("job %s not found", jobName)
  121. }
  122. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  123. Name: "presets_DialogPortalName",
  124. Body: web.Scope(
  125. vuetify.VDialog(
  126. vuetify.VCard(
  127. vuetify.VCardTitle(
  128. h.Text(config.shortname),
  129. vuetify.VSpacer(),
  130. vuetify.VBtn("").Icon(true).Children(
  131. vuetify.VIcon("close"),
  132. ).Attr("@click.stop", "vars.presetsDialog=false"),
  133. ),
  134. h.If(config.description != "", vuetify.VCardSubtitle(
  135. h.Text(config.description),
  136. )),
  137. h.If(config.hasParams, vuetify.VCardText(
  138. b.jobEditingContent(ctx, jobName, nil),
  139. )),
  140. vuetify.VCardActions(
  141. vuetify.VSpacer(),
  142. vuetify.VBtn(msgr.Cancel).Elevation(0).Attr("@click", "vars.presetsDialog=false"),
  143. vuetify.VBtn(msgr.OK).Color("primary").Large(true).
  144. Attr("@click", web.Plaid().
  145. URL(b.mb.Info().ListingHref()).
  146. EventFunc(ActionJobCreate).
  147. Query("jobName", jobName).
  148. Go()),
  149. ),
  150. )).
  151. Attr("v-model", "vars.presetsDialog").
  152. Width("600").Persistent(true),
  153. ).VSlot("{ plaidForm }"),
  154. })
  155. r.VarsScript = "setTimeout(function(){vars.presetsDialog = true; }, 100)"
  156. return
  157. }
  158. func (b *Builder) eventActionJobResponse(ctx *web.EventContext) (r web.EventResponse, err error) {
  159. var (
  160. jobName = ctx.R.FormValue("jobName")
  161. jobID = ctx.R.FormValue("jobID")
  162. config = actionJobs[jobName]
  163. )
  164. if config == nil {
  165. return r, fmt.Errorf("job %s not found", jobName)
  166. }
  167. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  168. Name: "presets_DialogPortalName",
  169. Body: web.Scope(
  170. vuetify.VDialog(
  171. vuetify.VAppBar(
  172. vuetify.VToolbarTitle(config.shortname).Class("pl-2"),
  173. vuetify.VSpacer(),
  174. vuetify.VBtn("").Icon(true).Children(
  175. vuetify.VIcon("close"),
  176. ).Attr("@click.stop", web.Plaid().
  177. URL(b.mb.Info().ListingHref()).
  178. EventFunc(ActionJobClose).
  179. Query("jobID", jobID).
  180. Query("jobName", jobName).
  181. Go()),
  182. ).Color("white").Elevation(0).Dense(true),
  183. vuetify.VCard(
  184. vuetify.VCardText(
  185. h.Div(
  186. web.Portal().Loader(
  187. web.Plaid().EventFunc(ActionJobProgressing).
  188. URL(b.mb.Info().ListingHref()).
  189. Query("jobID", jobID).
  190. Query("jobName", jobName),
  191. ).AutoReloadInterval("vars.actionJobProgressingInterval"),
  192. ).Attr(web.InitContextVars, fmt.Sprintf("{actionJobProgressingInterval: %d}", config.progressingInterval)),
  193. ),
  194. ).Tile(true).Attr("style", "box-shadow: none;")).
  195. Attr("v-model", "vars.presetsDialog").
  196. Width("600").Persistent(true),
  197. ).VSlot("{ plaidForm }"),
  198. })
  199. r.VarsScript = "setTimeout(function(){vars.presetsDialog = true; }, 100)"
  200. return
  201. }
  202. func (b *Builder) eventActionJobClose(ctx *web.EventContext) (er web.EventResponse, err error) {
  203. var (
  204. qorJobID = uint(ctx.QueryAsInt("jobID"))
  205. qorJobName = ctx.R.FormValue("jobName")
  206. )
  207. er.VarsScript = "vars.presetsDialog = false;vars.actionJobProgressingInterval = 0;"
  208. if pErr := editIsAllowed(ctx.R, qorJobName); pErr != nil {
  209. return er, pErr
  210. }
  211. jb := b.mustGetJobBuilder(qorJobName)
  212. inst, err := jb.getJobInstance(qorJobID)
  213. if err != nil {
  214. return er, err
  215. }
  216. switch inst.Status {
  217. case JobStatusRunning:
  218. err = b.q.Kill(ctx.R.Context(), inst)
  219. case JobStatusNew, JobStatusScheduled:
  220. err = b.q.Remove(ctx.R.Context(), inst)
  221. }
  222. return er, err
  223. }
  224. func (b *Builder) eventActionJobProgressing(ctx *web.EventContext) (er web.EventResponse, err error) {
  225. var (
  226. qorJobID = uint(ctx.QueryAsInt("jobID"))
  227. qorJobName = ctx.R.FormValue("jobName")
  228. config = actionJobs[qorJobName]
  229. )
  230. if config == nil {
  231. return er, fmt.Errorf("job %s not found", qorJobName)
  232. }
  233. inst, err := getModelQorJobInstance(b.db, qorJobID)
  234. if err != nil {
  235. return er, err
  236. }
  237. er.Body = h.Div(
  238. h.Div(vuetify.VProgressLinear(
  239. h.Strong(fmt.Sprintf("%d%%", inst.Progress)),
  240. ).Value(int(inst.Progress)).Height(20)).Class("mb-5"),
  241. h.If(config.displayLog, actionJobLog(*config.b, inst)),
  242. h.If(inst.ProgressText != "",
  243. h.Div().Class("mb-3").Children(
  244. h.RawHTML(inst.ProgressText),
  245. ),
  246. ),
  247. )
  248. if inst.Status == JobStatusDone || inst.Status == JobStatusException {
  249. er.VarsScript = "vars.actionJobProgressingInterval = 0;"
  250. } else {
  251. er.VarsScript = fmt.Sprintf("vars.actionJobProgressingInterval = %d;", config.progressingInterval)
  252. }
  253. return er, nil
  254. }
  255. func actionJobLog(b Builder, inst *QorJobInstance) h.HTMLComponent {
  256. var logLines []h.HTMLComponent
  257. logs := make([]string, 0, 100)
  258. var mLogs []*QorJobLog
  259. b.db.Where("qor_job_instance_id = ?", inst.ID).
  260. Order("created_at desc").
  261. Limit(100).
  262. Find(&mLogs)
  263. for i := len(mLogs) - 1; i >= 0; i-- {
  264. logs = append(logs, mLogs[i].Log)
  265. }
  266. var reverseStyle string
  267. if len(logs) > 18 {
  268. reverseStyle = "display: flex;flex-direction: column-reverse;"
  269. for i := len(logs) - 1; i >= 0; i-- {
  270. logLines = append(logLines, h.P().Style(`margin: 0;margin-bottom: 4px;`).Children(h.Text(logs[i])))
  271. }
  272. } else {
  273. for _, l := range logs {
  274. logLines = append(logLines, h.P().Style(`margin: 0;margin-bottom: 4px;`).Children(h.Text(l)))
  275. }
  276. }
  277. return h.Div().Class("mb-3").Style(fmt.Sprintf(`
  278. background-color: #222;
  279. color: #fff;
  280. font-family: menlo,Roboto,Helvetica,Arial,sans-serif;
  281. height: 300px;
  282. padding: 8px;
  283. overflow: auto;
  284. box-sizing: border-box;
  285. font-size: 12px;
  286. line-height: 1;
  287. %s
  288. `, reverseStyle)).Children(logLines...)
  289. }