Manager: show worker tag in job details

Show the worker tag name (and its description in a tooltip) in the job
details. When no worker tag is assigned, "All Workers" is shown in a more
dimmed colour.

This also renames the "Type" field to "Job Type". "Tag" and "Type" could
be confused, and now they're displayed as "Worker Tag" and "Job Type".

The UI in the add-on's submission interface is also updated for this, so
that that also shows "Worker Tag" (instead of just "Tag").
This commit is contained in:
Sybren A. Stüvel 2024-07-29 17:48:28 +02:00
parent e8438bb645
commit df4f94c642
9 changed files with 140 additions and 10 deletions

View File

@ -51,7 +51,7 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel):
) )
if not job_types.are_job_types_available(): if not job_types.are_job_types_available():
return return
col.prop(context.scene, "flamenco_worker_tag", text="Tag") col.prop(context.scene, "flamenco_worker_tag", text="Worker Tag")
# Job properties: # Job properties:
job_col = layout.column(align=True) job_col = layout.column(align=True)

View File

@ -687,7 +687,7 @@ func jobDBtoAPI(dbJob *persistence.Job) api.Job {
apiJob.DeleteRequestedAt = &dbJob.DeleteRequestedAt.Time apiJob.DeleteRequestedAt = &dbJob.DeleteRequestedAt.Time
} }
if dbJob.WorkerTag != nil { if dbJob.WorkerTag != nil {
apiJob.WorkerTag = &dbJob.WorkerTag.UUID apiJob.WorkerTag = workerTagDBtoAPI(dbJob.WorkerTag)
} }
return apiJob return apiJob

View File

@ -89,6 +89,60 @@ func TestQueryJobs(t *testing.T) {
assertResponseJSON(t, echoCtx, http.StatusOK, expectedJobs) assertResponseJSON(t, echoCtx, http.StatusOK, expectedJobs)
} }
func TestFetchJob(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mf := newMockedFlamenco(mockCtrl)
dbJob := persistence.Job{
UUID: "afc47568-bd9d-4368-8016-e91d945db36d",
Name: "работа",
JobType: "test",
Priority: 50,
Status: api.JobStatusActive,
Settings: persistence.StringInterfaceMap{
"result": "/render/frames/exploding.kittens",
},
Metadata: persistence.StringStringMap{
"project": "/projects/exploding-kittens",
},
WorkerTag: &persistence.WorkerTag{
UUID: "d86e1b84-5ee2-4784-a178-65963eeb484b",
Name: "Tikkie terug Kees!",
Description: "",
},
}
echoCtx := mf.prepareMockedRequest(nil)
mf.persistence.EXPECT().FetchJob(gomock.Any(), dbJob.UUID).Return(&dbJob, nil)
require.NoError(t, mf.flamenco.FetchJob(echoCtx, dbJob.UUID))
expectedJob := api.Job{
SubmittedJob: api.SubmittedJob{
Name: "работа",
Type: "test",
Priority: 50,
Settings: &api.JobSettings{AdditionalProperties: map[string]interface{}{
"result": "/render/frames/exploding.kittens",
}},
Metadata: &api.JobMetadata{AdditionalProperties: map[string]string{
"project": "/projects/exploding-kittens",
}},
},
Id: "afc47568-bd9d-4368-8016-e91d945db36d",
Status: api.JobStatusActive,
WorkerTag: &api.WorkerTag{
Id: ptr("d86e1b84-5ee2-4784-a178-65963eeb484b"),
Name: "Tikkie terug Kees!",
Description: nil, // Empty description should just be excluded from the JSON.
},
}
assertResponseJSON(t, echoCtx, http.StatusOK, expectedJob)
}
func TestFetchTask(t *testing.T) { func TestFetchTask(t *testing.T) {
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish() defer mockCtrl.Finish()

View File

@ -415,6 +415,11 @@ func TestSubmitJobWithWorkerTag(t *testing.T) {
DeleteRequestedAt: nil, DeleteRequestedAt: nil,
Activity: "", Activity: "",
Status: api.JobStatusQueued, Status: api.JobStatusQueued,
WorkerTag: &api.WorkerTag{
Id: ptr(workerTagUUID),
Name: "first tag",
Description: ptr("my first tag"),
},
}) })
} }

View File

@ -313,8 +313,11 @@ func (f *Flamenco) FetchWorkerTag(e echo.Context, tagUUID string) error {
logger.Error().Err(err).Msg("fetching worker tag") logger.Error().Err(err).Msg("fetching worker tag")
return sendAPIError(e, http.StatusInternalServerError, "error fetching worker tag: %v", err) return sendAPIError(e, http.StatusInternalServerError, "error fetching worker tag: %v", err)
} }
if tag == nil {
panic("Could fetch a worker tag without error, but then the returned tag was still nil")
}
return e.JSON(http.StatusOK, workerTagDBtoAPI(*tag)) return e.JSON(http.StatusOK, workerTagDBtoAPI(tag))
} }
func (f *Flamenco) UpdateWorkerTag(e echo.Context, tagUUID string) error { func (f *Flamenco) UpdateWorkerTag(e echo.Context, tagUUID string) error {
@ -387,8 +390,8 @@ func (f *Flamenco) FetchWorkerTags(e echo.Context) error {
apiTags := []api.WorkerTag{} apiTags := []api.WorkerTag{}
for _, dbTag := range dbTags { for _, dbTag := range dbTags {
apiTag := workerTagDBtoAPI(*dbTag) apiTag := workerTagDBtoAPI(dbTag)
apiTags = append(apiTags, apiTag) apiTags = append(apiTags, *apiTag)
} }
tagList := api.WorkerTagList{ tagList := api.WorkerTagList{
@ -443,7 +446,7 @@ func (f *Flamenco) CreateWorkerTag(e echo.Context) error {
sioUpdate := eventbus.NewWorkerTagUpdate(&dbTag) sioUpdate := eventbus.NewWorkerTagUpdate(&dbTag)
f.broadcaster.BroadcastNewWorkerTag(sioUpdate) f.broadcaster.BroadcastNewWorkerTag(sioUpdate)
return e.JSON(http.StatusOK, workerTagDBtoAPI(dbTag)) return e.JSON(http.StatusOK, workerTagDBtoAPI(&dbTag))
} }
func workerSummary(w persistence.Worker) api.WorkerSummary { func workerSummary(w persistence.Worker) api.WorkerSummary {
@ -479,7 +482,7 @@ func workerDBtoAPI(w persistence.Worker) api.Worker {
if len(w.Tags) > 0 { if len(w.Tags) > 0 {
tags := []api.WorkerTag{} tags := []api.WorkerTag{}
for i := range w.Tags { for i := range w.Tags {
tags = append(tags, workerTagDBtoAPI(*w.Tags[i])) tags = append(tags, *workerTagDBtoAPI(w.Tags[i]))
} }
apiWorker.Tags = &tags apiWorker.Tags = &tags
} }
@ -487,7 +490,11 @@ func workerDBtoAPI(w persistence.Worker) api.Worker {
return apiWorker return apiWorker
} }
func workerTagDBtoAPI(wc persistence.WorkerTag) api.WorkerTag { func workerTagDBtoAPI(wc *persistence.WorkerTag) *api.WorkerTag {
if wc == nil {
return nil
}
uuid := wc.UUID // Take a copy for safety. uuid := wc.UUID // Take a copy for safety.
apiTag := api.WorkerTag{ apiTag := api.WorkerTag{
@ -497,5 +504,5 @@ func workerTagDBtoAPI(wc persistence.WorkerTag) api.WorkerTag {
if len(wc.Description) > 0 { if len(wc.Description) > 0 {
apiTag.Description = &wc.Description apiTag.Description = &wc.Description
} }
return apiTag return &apiTag
} }

View File

@ -359,6 +359,18 @@ func (db *DB) FetchJob(ctx context.Context, jobUUID string) (*Job, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if sqlcJob.WorkerTagID.Valid {
workerTag, err := fetchWorkerTagByID(db.gormDB, uint(sqlcJob.WorkerTagID.Int64))
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrWorkerTagNotFound
case err != nil:
return nil, workerTagError(err, "fetching worker tag of job")
}
gormJob.WorkerTag = workerTag
}
return &gormJob, nil return &gormJob, nil
} }

View File

@ -75,6 +75,38 @@ func TestStoreAuthoredJobWithShamanCheckoutID(t *testing.T) {
assert.Equal(t, job.Storage.ShamanCheckoutID, fetchedJob.Storage.ShamanCheckoutID) assert.Equal(t, job.Storage.ShamanCheckoutID, fetchedJob.Storage.ShamanCheckoutID)
} }
func TestStoreAuthoredJobWithWorkerTag(t *testing.T) {
ctx, cancel, db := persistenceTestFixtures(1 * time.Second)
defer cancel()
workerTagUUID := "daa811ac-6861-4004-8748-7700aebc244c"
require.NoError(t, db.CreateWorkerTag(ctx, &WorkerTag{
UUID: workerTagUUID,
Name: "🐈",
Description: "Mrieuw",
}))
workerTag, err := db.FetchWorkerTag(ctx, workerTagUUID)
require.NoError(t, err)
job := createTestAuthoredJobWithTasks()
job.WorkerTagUUID = workerTagUUID
err = db.StoreAuthoredJob(ctx, job)
require.NoError(t, err)
fetchedJob, err := db.FetchJob(ctx, job.JobID)
require.NoError(t, err)
require.NotNil(t, fetchedJob)
require.NotNil(t, fetchedJob.WorkerTagID)
assert.Equal(t, *fetchedJob.WorkerTagID, workerTag.ID)
require.NotNil(t, fetchedJob.WorkerTag)
assert.Equal(t, fetchedJob.WorkerTag.Name, workerTag.Name)
assert.Equal(t, fetchedJob.WorkerTag.Description, workerTag.Description)
assert.Equal(t, fetchedJob.WorkerTag.UUID, workerTagUUID)
}
func TestFetchTaskJobUUID(t *testing.T) { func TestFetchTaskJobUUID(t *testing.T) {
ctx, cancel, db := persistenceTestFixtures(1 * time.Second) ctx, cancel, db := persistenceTestFixtures(1 * time.Second)
defer cancel() defer cancel()

View File

@ -53,6 +53,16 @@ func fetchWorkerTag(gormDB *gorm.DB, uuid string) (*WorkerTag, error) {
return &w, nil return &w, nil
} }
// fetchWorkerTagByID fetches the worker tag using the given database instance.
func fetchWorkerTagByID(gormDB *gorm.DB, id uint) (*WorkerTag, error) {
w := WorkerTag{}
tx := gormDB.First(&w, "id = ?", id)
if tx.Error != nil {
return nil, workerTagError(tx.Error, "fetching worker tag")
}
return &w, nil
}
func (db *DB) SaveWorkerTag(ctx context.Context, tag *WorkerTag) error { func (db *DB) SaveWorkerTag(ctx context.Context, tag *WorkerTag) error {
if err := db.gormDB.WithContext(ctx).Save(tag).Error; err != nil { if err := db.gormDB.WithContext(ctx).Save(tag).Error; err != nil {
return workerTagError(err, "saving worker tag") return workerTagError(err, "saving worker tag")

View File

@ -52,9 +52,15 @@
{{ jobData.status }} {{ jobData.status }}
</dd> </dd>
<dt class="field-type" title="Type">Type</dt> <dt class="field-type" title="Job Type">Job Type</dt>
<dd>{{ jobType ? jobType.label : jobData.type }}</dd> <dd>{{ jobType ? jobType.label : jobData.type }}</dd>
<dt class="field-worker-tag" title="Worker Tag">Worker Tag</dt>
<dd v-if="jobData.worker_tag" :title="jobData.worker_tag.description">
{{ jobData.worker_tag.name }}
</dd>
<dd v-else class="no-worker-tag">All Workers</dd>
<dt class="field-priority" title="Priority">Priority</dt> <dt class="field-priority" title="Priority">Priority</dt>
<dd> <dd>
<PopoverEditableJobPriority :jobId="jobData.id" :priority="jobData.priority" /> <PopoverEditableJobPriority :jobId="jobData.id" :priority="jobData.priority" />
@ -289,4 +295,8 @@ export default {
color: var(--indicator-color); color: var(--indicator-color);
font-weight: bold; font-weight: bold;
} }
dd.no-worker-tag {
color: var(--color-text-muted);
}
</style> </style>