builder.go 23 KB

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