builder.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901
  1. package worker
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "net/url"
  8. "path"
  9. "reflect"
  10. "strings"
  11. "time"
  12. "github.com/qor5/admin/activity"
  13. "github.com/qor5/admin/presets"
  14. . "github.com/qor5/ui/vuetify"
  15. "github.com/qor5/ui/vuetifyx"
  16. "github.com/qor5/web"
  17. "github.com/qor5/x/i18n"
  18. "github.com/qor5/x/perm"
  19. . "github.com/theplant/htmlgo"
  20. "golang.org/x/text/language"
  21. "gorm.io/gorm"
  22. )
  23. type Builder struct {
  24. db *gorm.DB
  25. q Queue
  26. jpb *presets.Builder // for render job form
  27. pb *presets.Builder
  28. jbs []*JobBuilder
  29. mb *presets.ModelBuilder
  30. getCurrentUserIDFunc func(r *http.Request) string
  31. ab *activity.ActivityBuilder
  32. }
  33. func New(db *gorm.DB) *Builder {
  34. return newWithConfigs(db, NewGoQueQueue(db))
  35. }
  36. func NewWithQueue(db *gorm.DB, q Queue) *Builder {
  37. return newWithConfigs(db, q)
  38. }
  39. func newWithConfigs(db *gorm.DB, q Queue) *Builder {
  40. if db == nil {
  41. panic("db can not be nil")
  42. }
  43. err := db.AutoMigrate(&QorJob{}, &QorJobInstance{}, &QorJobLog{}, &GoQueError{})
  44. if err != nil {
  45. panic(err)
  46. }
  47. r := &Builder{
  48. db: db,
  49. q: q,
  50. jpb: presets.New(),
  51. }
  52. return r
  53. }
  54. // default queue is go-que queue
  55. func (b *Builder) Queue(q Queue) *Builder {
  56. b.q = q
  57. return b
  58. }
  59. func (b *Builder) GetCurrentUserIDFunc(f func(r *http.Request) string) *Builder {
  60. b.getCurrentUserIDFunc = f
  61. return b
  62. }
  63. // Activity sets Activity Builder to log activities
  64. func (b *Builder) Activity(ab *activity.ActivityBuilder) *Builder {
  65. b.ab = ab
  66. return b
  67. }
  68. func (b *Builder) NewJob(name string) *JobBuilder {
  69. for _, jb := range b.jbs {
  70. if jb.name == name {
  71. panic(fmt.Sprintf("worker %s already exists", name))
  72. }
  73. }
  74. j := newJob(b, name)
  75. b.jbs = append(b.jbs, j)
  76. return j
  77. }
  78. func (b *Builder) getJobBuilder(name string) *JobBuilder {
  79. for _, jb := range b.jbs {
  80. if jb.name == name {
  81. return jb
  82. }
  83. }
  84. return nil
  85. }
  86. func (b *Builder) mustGetJobBuilder(name string) *JobBuilder {
  87. jb := b.getJobBuilder(name)
  88. if jb == nil {
  89. panic(fmt.Sprintf("no job %s", name))
  90. }
  91. return jb
  92. }
  93. func (b *Builder) getJobBuilderByQorJobID(id uint) (*JobBuilder, error) {
  94. j := QorJob{}
  95. err := b.db.Where("id = ?", id).First(&j).Error
  96. if err != nil {
  97. return nil, err
  98. }
  99. return b.getJobBuilder(j.Job), nil
  100. }
  101. func (b *Builder) setStatus(id uint, status string) error {
  102. return b.db.Model(&QorJob{}).Where("id = ?", id).
  103. Updates(map[string]interface{}{
  104. "status": status,
  105. }).
  106. Error
  107. }
  108. var permVerifier *perm.Verifier
  109. func (b *Builder) Configure(pb *presets.Builder) *presets.ModelBuilder {
  110. b.pb = pb
  111. permVerifier = perm.NewVerifier("workers", pb.GetPermission())
  112. pb.I18n().
  113. RegisterForModule(language.English, I18nWorkerKey, Messages_en_US).
  114. RegisterForModule(language.SimplifiedChinese, I18nWorkerKey, Messages_zh_CN)
  115. mb := pb.Model(&QorJob{}).
  116. Label("Workers").
  117. URIName("workers").
  118. MenuIcon("smart_toy")
  119. b.mb = mb
  120. mb.RegisterEventFunc("worker_selectJob", b.eventSelectJob)
  121. mb.RegisterEventFunc("worker_abortJob", b.eventAbortJob)
  122. mb.RegisterEventFunc("worker_rerunJob", b.eventRerunJob)
  123. mb.RegisterEventFunc("worker_updateJob", b.eventUpdateJob)
  124. mb.RegisterEventFunc("worker_updateJobProgressing", b.eventUpdateJobProgressing)
  125. mb.RegisterEventFunc("worker_loadHiddenLogs", b.eventLoadHiddenLogs)
  126. mb.RegisterEventFunc(ActionJobInputParams, b.eventActionJobInputParams)
  127. mb.RegisterEventFunc(ActionJobCreate, b.eventActionJobCreate)
  128. mb.RegisterEventFunc(ActionJobResponse, b.eventActionJobResponse)
  129. mb.RegisterEventFunc(ActionJobClose, b.eventActionJobClose)
  130. mb.RegisterEventFunc(ActionJobProgressing, b.eventActionJobProgressing)
  131. lb := mb.Listing("ID", "Job", "Status", "CreatedAt")
  132. lb.RowMenu().Empty()
  133. lb.FilterDataFunc(func(ctx *web.EventContext) vuetifyx.FilterData {
  134. msgr := i18n.MustGetModuleMessages(ctx.R, I18nWorkerKey, Messages_en_US).(*Messages)
  135. return []*vuetifyx.FilterItem{
  136. {
  137. Key: "status",
  138. Label: "Status",
  139. ItemType: vuetifyx.ItemTypeSelect,
  140. SQLCondition: `status %s ?`,
  141. Options: []*vuetifyx.SelectItem{
  142. {Text: msgr.StatusNew, Value: JobStatusNew},
  143. {Text: msgr.StatusScheduled, Value: JobStatusScheduled},
  144. {Text: msgr.StatusRunning, Value: JobStatusRunning},
  145. {Text: msgr.StatusCancelled, Value: JobStatusCancelled},
  146. {Text: msgr.StatusDone, Value: JobStatusDone},
  147. {Text: msgr.StatusException, Value: JobStatusException},
  148. {Text: msgr.StatusKilled, Value: JobStatusKilled},
  149. },
  150. },
  151. }
  152. })
  153. lb.FilterTabsFunc(func(ctx *web.EventContext) []*presets.FilterTab {
  154. msgr := i18n.MustGetModuleMessages(ctx.R, I18nWorkerKey, Messages_en_US).(*Messages)
  155. return []*presets.FilterTab{
  156. {
  157. Label: msgr.FilterTabAll,
  158. Query: url.Values{"all": []string{"1"}},
  159. },
  160. {
  161. Label: msgr.FilterTabRunning,
  162. Query: url.Values{"status": []string{JobStatusRunning}},
  163. },
  164. {
  165. Label: msgr.FilterTabScheduled,
  166. Query: url.Values{"status": []string{JobStatusScheduled}},
  167. },
  168. {
  169. Label: msgr.FilterTabDone,
  170. Query: url.Values{"status": []string{JobStatusDone}},
  171. },
  172. {
  173. Label: msgr.FilterTabErrors,
  174. Query: url.Values{"status": []string{JobStatusException}},
  175. },
  176. }
  177. })
  178. lb.Field("Job").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) HTMLComponent {
  179. qorJob := obj.(*QorJob)
  180. return Td(Text(getTJob(ctx.R, qorJob.Job)))
  181. })
  182. lb.Field("Status").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) HTMLComponent {
  183. msgr := i18n.MustGetModuleMessages(ctx.R, I18nWorkerKey, Messages_en_US).(*Messages)
  184. qorJob := obj.(*QorJob)
  185. return Td(Text(getTStatus(msgr, qorJob.Status)))
  186. })
  187. eb := mb.Editing("Job", "Args")
  188. eb.ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) {
  189. msgr := i18n.MustGetModuleMessages(ctx.R, I18nWorkerKey, Messages_en_US).(*Messages)
  190. qorJob := obj.(*QorJob)
  191. if qorJob.Job == "" {
  192. err.FieldError("Job", msgr.PleaseSelectJob)
  193. }
  194. return err
  195. })
  196. type JobSelectItem struct {
  197. Label string
  198. Value string
  199. }
  200. eb.Field("Job").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) HTMLComponent {
  201. qorJob := obj.(*QorJob)
  202. return web.Portal(b.jobSelectList(ctx, qorJob.Job)).Name("worker_jobSelectList")
  203. })
  204. eb.Field("Args").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) HTMLComponent {
  205. var vErr web.ValidationErrors
  206. if ve, ok := ctx.Flash.(*web.ValidationErrors); ok {
  207. vErr = *ve
  208. if fvErr := vErr.GetFieldErrors(field.Name); len(fvErr) > 0 {
  209. errM := make(map[string][]string)
  210. if err := json.Unmarshal([]byte(fvErr[0]), &errM); err == nil {
  211. for f, es := range errM {
  212. for _, e := range es {
  213. ve.FieldError(f, e)
  214. }
  215. }
  216. }
  217. }
  218. }
  219. qorJob := obj.(*QorJob)
  220. return web.Portal(b.jobEditingContent(ctx, qorJob.Job, qorJob.Args)).Name("worker_jobEditingContent")
  221. })
  222. eb.SaveFunc(func(obj interface{}, id string, ctx *web.EventContext) (err error) {
  223. qorJob := obj.(*QorJob)
  224. if qorJob.Job == "" {
  225. return errors.New("job is required")
  226. }
  227. j, err := b.createJob(ctx, qorJob)
  228. if err != nil {
  229. return err
  230. }
  231. if b.ab != nil {
  232. b.ab.AddRecords(activity.ActivityCreate, ctx.R.Context(), j)
  233. }
  234. return
  235. })
  236. mb.Detailing("DetailingPage").Field("DetailingPage").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) HTMLComponent {
  237. msgr := i18n.MustGetModuleMessages(ctx.R, I18nWorkerKey, Messages_en_US).(*Messages)
  238. qorJob := obj.(*QorJob)
  239. inst, err := getModelQorJobInstance(b.db, qorJob.ID)
  240. if err != nil {
  241. return Text(err.Error())
  242. }
  243. var scheduledJobDetailing []HTMLComponent
  244. eURL := path.Join(b.mb.Info().ListingHref(), fmt.Sprint(qorJob.ID))
  245. if inst.Status == JobStatusScheduled {
  246. jb := b.getJobBuilder(qorJob.Job)
  247. if jb != nil && jb.r != nil {
  248. args := jb.newResourceObject()
  249. err := json.Unmarshal([]byte(inst.Args), &args)
  250. if err != nil {
  251. return Text(err.Error())
  252. }
  253. body := jb.rmb.Editing().ToComponent(jb.rmb.Info(), args, ctx)
  254. scheduledJobDetailing = []HTMLComponent{
  255. body,
  256. If(editIsAllowed(ctx.R, qorJob.Job) == nil,
  257. Div().Class("d-flex mt-3").Children(
  258. VSpacer(),
  259. VBtn(msgr.ActionCancelJob).Color("error").Class("mr-2").
  260. Attr("@click", web.Plaid().
  261. URL(eURL).
  262. EventFunc("worker_abortJob").
  263. Query("jobID", fmt.Sprintf("%d", qorJob.ID)).
  264. Query("job", qorJob.Job).
  265. Go()),
  266. VBtn(msgr.ActionUpdateJob).Color("primary").
  267. Attr("@click", web.Plaid().
  268. URL(eURL).
  269. EventFunc("worker_updateJob").
  270. Query("jobID", fmt.Sprintf("%d", qorJob.ID)).
  271. Query("job", qorJob.Job).
  272. Go()),
  273. ),
  274. ),
  275. }
  276. } else {
  277. scheduledJobDetailing = []HTMLComponent{
  278. VAlert().Dense(true).Type("warning").Children(
  279. Text(msgr.NoticeJobWontBeExecuted),
  280. ),
  281. Div(Text("args: " + inst.Args)),
  282. }
  283. }
  284. }
  285. return Div(
  286. Div(Text(getTJob(ctx.R, qorJob.Job))).Class("mb-3 text-h6 font-weight-regular"),
  287. If(inst.Status == JobStatusScheduled,
  288. scheduledJobDetailing...,
  289. ).Else(
  290. Div(
  291. web.Portal().
  292. Loader(web.Plaid().EventFunc("worker_updateJobProgressing").
  293. URL(eURL).
  294. Query("jobID", fmt.Sprintf("%d", qorJob.ID)).
  295. Query("job", qorJob.Job),
  296. ).
  297. AutoReloadInterval("vars.worker_updateJobProgressingInterval"),
  298. ).Attr(web.InitContextVars, "{worker_updateJobProgressingInterval: 2000}"),
  299. ),
  300. web.Portal().Name("worker_snackbar"),
  301. )
  302. })
  303. if b.ab != nil {
  304. b.ab.RegisterModel(mb).SkipCreate().SkipUpdate().SkipDelete().
  305. AddTypeHanders(time.Time{}, func(old, now interface{}, prefixField string) []activity.Diff {
  306. fm := "2006-01-02 15:04:05"
  307. oldString := old.(time.Time).Format(fm)
  308. nowString := now.(time.Time).Format(fm)
  309. if oldString != nowString {
  310. return []activity.Diff{
  311. {Field: prefixField, Old: oldString, Now: nowString},
  312. }
  313. }
  314. return []activity.Diff{}
  315. }).
  316. AddTypeHanders(Schedule{}, func(old, now interface{}, prefixField string) []activity.Diff {
  317. fm := "2006-01-02 15:04:05"
  318. oldString := old.(Schedule).ScheduleTime.Format(fm)
  319. nowString := now.(Schedule).ScheduleTime.Format(fm)
  320. if oldString != nowString {
  321. return []activity.Diff{
  322. {Field: prefixField, Old: oldString, Now: nowString},
  323. }
  324. }
  325. return []activity.Diff{}
  326. })
  327. }
  328. return mb
  329. }
  330. func (b *Builder) Listen() {
  331. var jds []*QorJobDefinition
  332. for _, jb := range b.jbs {
  333. jds = append(jds, &QorJobDefinition{
  334. Name: jb.name,
  335. Handler: jb.h,
  336. })
  337. }
  338. err := b.q.Listen(jds, func(qorJobID uint) (QueJobInterface, error) {
  339. jb, err := b.getJobBuilderByQorJobID(qorJobID)
  340. if err != nil {
  341. return nil, err
  342. }
  343. if jb == nil {
  344. return nil, errors.New("failed to find job (job name modified?)")
  345. }
  346. return jb.getJobInstance(qorJobID)
  347. })
  348. if err != nil {
  349. panic(err)
  350. }
  351. }
  352. func (b *Builder) createJob(ctx *web.EventContext, qorJob *QorJob) (j *QorJob, err error) {
  353. if err = editIsAllowed(ctx.R, qorJob.Job); err != nil {
  354. return
  355. }
  356. jb := b.mustGetJobBuilder(qorJob.Job)
  357. // encode args
  358. args, vErr := jb.unmarshalForm(ctx)
  359. if vErr.HaveErrors() {
  360. errM := make(map[string][]string)
  361. argsT := reflect.TypeOf(jb.r).Elem()
  362. for i := 0; i < argsT.NumField(); i++ {
  363. fName := argsT.Field(i).Name
  364. errM[fName] = vErr.GetFieldErrors(fName)
  365. }
  366. bErrM, _ := json.Marshal(errM)
  367. err = errors.New(string(bErrM))
  368. return
  369. }
  370. // encode context
  371. var context = make(map[string]interface{})
  372. for key, v := range DefaultOriginalPageContextHandler(ctx) {
  373. context[key] = v
  374. }
  375. if jb.contextHandler != nil {
  376. for key, v := range jb.contextHandler(ctx) {
  377. context[key] = v
  378. }
  379. }
  380. err = b.db.Transaction(func(tx *gorm.DB) error {
  381. j = &QorJob{
  382. Job: qorJob.Job,
  383. Status: JobStatusNew,
  384. }
  385. err = b.db.Create(j).Error
  386. if err != nil {
  387. return err
  388. }
  389. var inst *QorJobInstance
  390. inst, err = jb.newJobInstance(ctx.R, j.ID, qorJob.Job, args, context)
  391. if err != nil {
  392. return err
  393. }
  394. return b.q.Add(inst)
  395. })
  396. return
  397. }
  398. func (b *Builder) eventSelectJob(ctx *web.EventContext) (er web.EventResponse, err error) {
  399. job := ctx.R.FormValue("jobName")
  400. er.UpdatePortals = append(er.UpdatePortals,
  401. &web.PortalUpdate{
  402. Name: "worker_jobEditingContent",
  403. Body: b.jobEditingContent(ctx, job, nil),
  404. },
  405. &web.PortalUpdate{
  406. Name: "worker_jobSelectList",
  407. Body: b.jobSelectList(ctx, job),
  408. },
  409. )
  410. return
  411. }
  412. func (b *Builder) eventAbortJob(ctx *web.EventContext) (er web.EventResponse, err error) {
  413. msgr := i18n.MustGetModuleMessages(ctx.R, I18nWorkerKey, Messages_en_US).(*Messages)
  414. qorJobID := uint(ctx.QueryAsInt("jobID"))
  415. qorJobName := ctx.R.FormValue("job")
  416. if pErr := editIsAllowed(ctx.R, qorJobName); pErr != nil {
  417. return er, pErr
  418. }
  419. jb := b.mustGetJobBuilder(qorJobName)
  420. inst, err := jb.getJobInstance(qorJobID)
  421. if err != nil {
  422. return er, err
  423. }
  424. isScheduled := inst.Status == JobStatusScheduled
  425. err = b.doAbortJob(inst)
  426. if err != nil {
  427. _, ok := err.(*cannotAbortError)
  428. if !ok {
  429. return er, err
  430. }
  431. er.UpdatePortals = append(er.UpdatePortals, &web.PortalUpdate{
  432. Name: "worker_snackbar",
  433. Body: VSnackbar().Value(true).Timeout(3000).Color("warning").Children(
  434. Text(msgr.NoticeJobCannotBeAborted),
  435. ),
  436. })
  437. }
  438. er.Reload = true
  439. er.VarsScript = "vars.worker_updateJobProgressingInterval = 2000"
  440. if b.ab != nil {
  441. action := "Abort"
  442. if isScheduled {
  443. action = "Cancel"
  444. }
  445. b.ab.AddCustomizedRecord(action, false, ctx.R.Context(), &QorJob{
  446. Model: gorm.Model{
  447. ID: inst.QorJobID,
  448. },
  449. })
  450. }
  451. return er, nil
  452. }
  453. type cannotAbortError struct {
  454. err error
  455. }
  456. func (e *cannotAbortError) Error() string {
  457. return e.err.Error()
  458. }
  459. func (b *Builder) doAbortJob(inst *QorJobInstance) (err error) {
  460. switch inst.Status {
  461. case JobStatusRunning:
  462. return b.q.Kill(inst)
  463. case JobStatusNew, JobStatusScheduled:
  464. return b.q.Remove(inst)
  465. default:
  466. return &cannotAbortError{
  467. err: fmt.Errorf("job status is %s, cannot be aborted/canceled", inst.Status),
  468. }
  469. }
  470. }
  471. func (b *Builder) eventRerunJob(ctx *web.EventContext) (er web.EventResponse, err error) {
  472. qorJobID := uint(ctx.QueryAsInt("jobID"))
  473. qorJobName := ctx.R.FormValue("job")
  474. if pErr := editIsAllowed(ctx.R, qorJobName); pErr != nil {
  475. return er, pErr
  476. }
  477. jb := b.mustGetJobBuilder(qorJobName)
  478. old, err := jb.getJobInstance(qorJobID)
  479. if err != nil {
  480. return er, err
  481. }
  482. if old.Status != JobStatusDone {
  483. return er, errors.New("job is not done")
  484. }
  485. inst, err := jb.newJobInstance(ctx.R, qorJobID, qorJobName, old.Args, old.Context)
  486. if err != nil {
  487. return er, err
  488. }
  489. err = b.setStatus(qorJobID, JobStatusNew)
  490. if err != nil {
  491. return er, err
  492. }
  493. err = b.q.Add(inst)
  494. if err != nil {
  495. return er, err
  496. }
  497. er.Reload = true
  498. er.VarsScript = "vars.worker_updateJobProgressingInterval = 2000"
  499. if b.ab != nil {
  500. b.ab.AddCustomizedRecord("Rerun", false, ctx.R.Context(), &QorJob{
  501. Model: gorm.Model{
  502. ID: inst.QorJobID,
  503. },
  504. })
  505. }
  506. return
  507. }
  508. func (b *Builder) eventUpdateJob(ctx *web.EventContext) (er web.EventResponse, err error) {
  509. msgr := i18n.MustGetModuleMessages(ctx.R, I18nWorkerKey, Messages_en_US).(*Messages)
  510. qorJobID := uint(ctx.QueryAsInt("jobID"))
  511. qorJobName := ctx.R.FormValue("job")
  512. if pErr := editIsAllowed(ctx.R, qorJobName); pErr != nil {
  513. return er, pErr
  514. }
  515. jb := b.mustGetJobBuilder(qorJobName)
  516. newArgs, argsVErr := jb.unmarshalForm(ctx)
  517. if argsVErr.HaveErrors() {
  518. return er, errors.New("invalid arguments")
  519. }
  520. var contexts = make(map[string]interface{})
  521. for key, v := range DefaultOriginalPageContextHandler(ctx) {
  522. contexts[key] = v
  523. }
  524. if jb.contextHandler != nil {
  525. for key, v := range jb.contextHandler(ctx) {
  526. contexts[key] = v
  527. }
  528. }
  529. old, err := jb.getJobInstance(qorJobID)
  530. if err != nil {
  531. return er, err
  532. }
  533. oldArgs, _ := jb.parseArgs(old.Args)
  534. err = b.doAbortJob(old)
  535. if err != nil {
  536. _, ok := err.(*cannotAbortError)
  537. if !ok {
  538. return er, err
  539. }
  540. er.UpdatePortals = append(er.UpdatePortals, &web.PortalUpdate{
  541. Name: "worker_snackbar",
  542. Body: VSnackbar().Value(true).Timeout(3000).Color("warning").Children(
  543. Text(msgr.NoticeJobCannotBeAborted),
  544. ),
  545. })
  546. er.Reload = true
  547. return er, nil
  548. }
  549. newInst, err := jb.newJobInstance(ctx.R, qorJobID, qorJobName, newArgs, contexts)
  550. if err != nil {
  551. return er, err
  552. }
  553. err = b.q.Add(newInst)
  554. if err != nil {
  555. return er, err
  556. }
  557. er.Reload = true
  558. er.VarsScript = "vars.worker_updateJobProgressingInterval = 2000"
  559. if b.ab != nil {
  560. b.ab.AddEditRecordWithOldAndContext(
  561. ctx.R.Context(),
  562. &QorJob{
  563. Model: gorm.Model{
  564. ID: newInst.QorJobID,
  565. },
  566. Args: oldArgs,
  567. },
  568. &QorJob{
  569. Model: gorm.Model{
  570. ID: newInst.QorJobID,
  571. },
  572. Args: newArgs,
  573. },
  574. )
  575. }
  576. return er, nil
  577. }
  578. func (b *Builder) eventUpdateJobProgressing(ctx *web.EventContext) (er web.EventResponse, err error) {
  579. msgr := i18n.MustGetModuleMessages(ctx.R, I18nWorkerKey, Messages_en_US).(*Messages)
  580. qorJobID := uint(ctx.QueryAsInt("jobID"))
  581. qorJobName := ctx.R.FormValue("job")
  582. inst, err := getModelQorJobInstance(b.db, qorJobID)
  583. if err != nil {
  584. return er, err
  585. }
  586. canEdit := editIsAllowed(ctx.R, qorJobName) == nil
  587. logs := make([]string, 0, 100)
  588. hasMoreLogs := false
  589. {
  590. var count int64
  591. err = b.db.Model(&QorJobLog{}).
  592. Where("qor_job_instance_id = ?", inst.ID).
  593. Count(&count).
  594. Error
  595. if err != nil {
  596. return er, err
  597. }
  598. if count > 100 {
  599. hasMoreLogs = true
  600. }
  601. if count > 0 {
  602. var mLogs []*QorJobLog
  603. err = b.db.Where("qor_job_instance_id = ?", inst.ID).
  604. Order("created_at desc").
  605. Limit(100).
  606. Find(&mLogs).
  607. Error
  608. if err != nil {
  609. return er, err
  610. }
  611. for i := len(mLogs) - 1; i >= 0; i-- {
  612. logs = append(logs, mLogs[i].Log)
  613. }
  614. }
  615. }
  616. er.Body = b.jobProgressing(canEdit, msgr, qorJobID, qorJobName, inst.Status, inst.Progress, logs, hasMoreLogs, inst.ProgressText)
  617. if inst.Status != JobStatusNew && inst.Status != JobStatusRunning && inst.Status != JobStatusKilled {
  618. er.VarsScript = "vars.worker_updateJobProgressingInterval = 0"
  619. } else {
  620. er.VarsScript = "vars.worker_updateJobProgressingInterval = 2000"
  621. }
  622. return er, nil
  623. }
  624. func (b *Builder) eventLoadHiddenLogs(ctx *web.EventContext) (er web.EventResponse, err error) {
  625. qorJobID := uint(ctx.QueryAsInt("jobID"))
  626. currentCount := ctx.QueryAsInt("currentCount")
  627. inst, err := getModelQorJobInstance(b.db, qorJobID)
  628. if err != nil {
  629. return er, err
  630. }
  631. var logs []*QorJobLog
  632. err = b.db.Where("qor_job_instance_id = ?", inst.ID).
  633. Order("created_at desc").
  634. Offset(currentCount).
  635. Find(&logs).
  636. Error
  637. if err != nil {
  638. return er, err
  639. }
  640. logLines := make([]HTMLComponent, 0, len(logs))
  641. for i := len(logs) - 1; i >= 0; i-- {
  642. logLines = append(logLines, P().Style(`
  643. margin: 0;
  644. margin-bottom: 4px;`).Children(Text(logs[i].Log)))
  645. }
  646. er.UpdatePortals = append(er.UpdatePortals,
  647. &web.PortalUpdate{
  648. Name: "worker_hiddenLogs",
  649. Body: Div(logLines...),
  650. },
  651. )
  652. return er, nil
  653. }
  654. func (b *Builder) jobProgressing(
  655. canEdit bool,
  656. msgr *Messages,
  657. id uint,
  658. job string,
  659. status string,
  660. progress uint,
  661. logs []string,
  662. hasMoreLogs bool,
  663. progressText string,
  664. ) HTMLComponent {
  665. logLines := make([]HTMLComponent, 0, len(logs)+1)
  666. if hasMoreLogs {
  667. logLines = append(logLines, web.Portal(
  668. VBtn("Load hidden logs").Attr("@click", web.Plaid().EventFunc("worker_loadHiddenLogs").
  669. Query("jobID", id).
  670. Query("currentCount", len(logs)).Go()).
  671. Small(true).
  672. Depressed(true).
  673. Class("mb-3"),
  674. ).Name("worker_hiddenLogs"))
  675. }
  676. for _, l := range logs {
  677. logLines = append(logLines, P().Style(`
  678. margin: 0;
  679. margin-bottom: 4px;`).Children(Text(l)))
  680. }
  681. // https://stackoverflow.com/a/44051405/10150757
  682. var reverseStyle string
  683. if len(logs) > 18 {
  684. reverseStyle = "display: flex;flex-direction: column-reverse;"
  685. for i, j := 0, len(logLines)-1; i < j; i, j = i+1, j-1 {
  686. logLines[i], logLines[j] = logLines[j], logLines[i]
  687. }
  688. }
  689. inRefresh := status == JobStatusNew || status == JobStatusRunning
  690. eURL := path.Join(b.mb.Info().ListingHref(), fmt.Sprint(id))
  691. return Div(
  692. Div(Text(msgr.DetailTitleStatus)).Class("text-caption"),
  693. Div().Class("d-flex align-center mb-5").Children(
  694. Div().Style("width: 120px").Children(
  695. Text(fmt.Sprintf("%s (%d%%)", getTStatus(msgr, status), progress)),
  696. ),
  697. VProgressLinear().Value(int(progress)),
  698. ),
  699. Div(Text(msgr.DetailTitleLog)).Class("text-caption"),
  700. Div().Class("mb-3").Style(fmt.Sprintf(`
  701. background-color: #222;
  702. color: #fff;
  703. font-family: menlo,Roboto,Helvetica,Arial,sans-serif;
  704. height: 300px;
  705. padding: 8px;
  706. overflow: auto;
  707. box-sizing: border-box;
  708. font-size: 12px;
  709. line-height: 1;
  710. %s
  711. `, reverseStyle)).Children(
  712. logLines...,
  713. ),
  714. If(progressText != "",
  715. Div().Class("mb-3").Children(
  716. RawHTML(progressText),
  717. ),
  718. ),
  719. If(canEdit,
  720. Div().Class("d-flex mt-3").Children(
  721. VSpacer(),
  722. If(inRefresh,
  723. VBtn(msgr.ActionAbortJob).Color("error").
  724. Attr("@click", web.Plaid().
  725. URL(eURL).
  726. EventFunc("worker_abortJob").
  727. Query("jobID", fmt.Sprintf("%d", id)).
  728. Query("job", job).
  729. Go()),
  730. ),
  731. If(status == JobStatusDone,
  732. VBtn(msgr.ActionRerunJob).Color("primary").
  733. Attr("@click", web.Plaid().
  734. URL(eURL).
  735. EventFunc("worker_rerunJob").
  736. Query("jobID", fmt.Sprintf("%d", id)).
  737. Query("job", job).
  738. Go()),
  739. ),
  740. ),
  741. ),
  742. )
  743. }
  744. func (b *Builder) jobSelectList(
  745. ctx *web.EventContext,
  746. job string,
  747. ) HTMLComponent {
  748. var vErr web.ValidationErrors
  749. if ve, ok := ctx.Flash.(*web.ValidationErrors); ok {
  750. vErr = *ve
  751. }
  752. var alert HTMLComponent
  753. if v := vErr.GetFieldErrors("Job"); len(v) > 0 {
  754. alert = VAlert(Text(strings.Join(v, ","))).Type("error")
  755. }
  756. items := make([]HTMLComponent, 0, len(b.jbs))
  757. for _, jb := range b.jbs {
  758. if !jb.global {
  759. continue
  760. }
  761. label := getTJob(ctx.R, jb.name)
  762. if editIsAllowed(ctx.R, jb.name) == nil {
  763. items = append(items,
  764. VListItem(VListItemContent(VListItemTitle(
  765. A(Text(label)).Attr("@click",
  766. web.Plaid().EventFunc("worker_selectJob").
  767. Query("jobName", jb.name).
  768. Go(),
  769. ),
  770. ))),
  771. )
  772. }
  773. }
  774. return Div(
  775. Input("").Type("hidden").Value(job).Attr(web.VFieldName("Job")...),
  776. If(job == "",
  777. alert,
  778. VList(items...).Nav(true).Dense(true),
  779. ).Else(
  780. Div(
  781. VIcon("arrow_back").Attr("@click",
  782. web.Plaid().EventFunc("worker_selectJob").
  783. Query("jobName", "").
  784. Go(),
  785. ),
  786. ).Class("mb-3"),
  787. Div(Text(getTJob(ctx.R, job))).Class("mb-3 text-h6").Style("font-weight: inherit"),
  788. ),
  789. )
  790. }
  791. func (b *Builder) jobEditingContent(
  792. ctx *web.EventContext,
  793. job string,
  794. args interface{},
  795. ) HTMLComponent {
  796. if job == "" {
  797. return Template()
  798. }
  799. jb := b.mustGetJobBuilder(job)
  800. var argsObj interface{}
  801. if args != nil {
  802. argsObj = args
  803. } else {
  804. argsObj = jb.r
  805. }
  806. if jb.rmb == nil {
  807. return Template()
  808. }
  809. return jb.rmb.Editing().ToComponent(jb.rmb.Info(), argsObj, ctx)
  810. }