From ffb4c63af76842a5aad398dd7e23066ed7672215 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 2 Nov 2022 16:58:24 -0400 Subject: [PATCH] [ui] Adds meta to job list stub and displays a pack logo on the jobs index (#14833) * Adds meta to job list stub and displays a pack logo on the jobs index * Changelog * Modifying struct for optional meta param * Explicitly ask for meta anytime I look up a job from index or job page * Test case for the endpoint * adding meta field to API struct and ommitting from response if empty * passthru method added to api/jobs.list * Meta param listed in docs for jobs list * Update api/jobs.go Co-authored-by: Tim Gross Co-authored-by: Tim Gross --- .changelog/14833.txt | 3 +++ api/jobs.go | 24 +++++++++++++++++++++++- command/agent/job_endpoint.go | 10 ++++++++++ command/job_status.go | 2 +- nomad/job_endpoint.go | 2 +- nomad/job_endpoint_test.go | 17 +++++++++++++++++ nomad/structs/structs.go | 19 +++++++++++++++++-- ui/app/routes/jobs/index.js | 4 ++-- ui/app/routes/jobs/job.js | 2 +- ui/app/routes/jobs/job/index.js | 5 ++++- ui/app/styles/core/tag.scss | 5 +++++ ui/app/templates/components/job-row.hbs | 8 ++++++++ website/content/api-docs/jobs.mdx | 2 ++ 13 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 .changelog/14833.txt diff --git a/.changelog/14833.txt b/.changelog/14833.txt new file mode 100644 index 000000000..4cb7b34b4 --- /dev/null +++ b/.changelog/14833.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Adds a "Pack" tag and logo on the jobs list index when appropriate +``` diff --git a/api/jobs.go b/api/jobs.go index de77db1a5..a90111897 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -155,10 +155,31 @@ func (j *Jobs) RegisterOpts(job *Job, opts *RegisterOptions, q *WriteOptions) (* return &resp, wm, nil } +type JobListFields struct { + Meta bool +} +type JobListOptions struct { + Fields *JobListFields +} + // List is used to list all of the existing jobs. func (j *Jobs) List(q *QueryOptions) ([]*JobListStub, *QueryMeta, error) { + return j.ListOptions(nil, q) +} + +// List is used to list all of the existing jobs. +func (j *Jobs) ListOptions(opts *JobListOptions, q *QueryOptions) ([]*JobListStub, *QueryMeta, error) { var resp []*JobListStub - qm, err := j.client.query("/v1/jobs", &resp, q) + + destinationURL := "/v1/jobs" + + if opts != nil && opts.Fields != nil { + qp := url.Values{} + qp.Add("meta", fmt.Sprint(opts.Fields.Meta)) + destinationURL = destinationURL + "?" + qp.Encode() + } + + qm, err := j.client.query(destinationURL, &resp, q) if err != nil { return nil, qm, err } @@ -1063,6 +1084,7 @@ type JobListStub struct { ModifyIndex uint64 JobModifyIndex uint64 SubmitTime int64 + Meta map[string]string `json:",omitempty"` } // JobIDSort is used to sort jobs by their job ID's. diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index dc31ea6d5..7a4977da8 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -37,6 +37,16 @@ func (s *HTTPServer) jobListRequest(resp http.ResponseWriter, req *http.Request) return nil, nil } + args.Fields = &structs.JobStubFields{} + // Parse meta query param + jobMeta, err := parseBool(req, "meta") + if err != nil { + return nil, err + } + if jobMeta != nil { + args.Fields.Meta = *jobMeta + } + var out structs.JobListResponse if err := s.agent.RPC("Job.List", &args, &out); err != nil { return nil, err diff --git a/command/job_status.go b/command/job_status.go index f129fc922..ea464974c 100644 --- a/command/job_status.go +++ b/command/job_status.go @@ -128,7 +128,7 @@ func (c *JobStatusCommand) Run(args []string) int { // Invoke list mode if no job ID. if len(args) == 0 { - jobs, _, err := client.Jobs().List(nil) + jobs, _, err := client.Jobs().ListOptions(nil, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error querying jobs: %s", err)) diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index 80ef7afb5..155ff1222 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -1377,7 +1377,7 @@ func (j *Job) List(args *structs.JobListRequest, reply *structs.JobListResponse) if err != nil || summary == nil { return fmt.Errorf("unable to look up summary for job: %v", job.ID) } - jobs = append(jobs, job.Stub(summary)) + jobs = append(jobs, job.Stub(summary, args.Fields)) return nil }) if err != nil { diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index 94242c22e..655baf9c6 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -5113,6 +5113,7 @@ func TestJobEndpoint_ListJobs(t *testing.T) { require.Len(t, resp2.Jobs, 1) require.Equal(t, job.ID, resp2.Jobs[0].ID) require.Equal(t, job.Namespace, resp2.Jobs[0].Namespace) + require.Nil(t, resp2.Jobs[0].Meta) // Lookup the jobs by prefix get = &structs.JobListRequest{ @@ -5129,6 +5130,22 @@ func TestJobEndpoint_ListJobs(t *testing.T) { require.Len(t, resp3.Jobs, 1) require.Equal(t, job.ID, resp3.Jobs[0].ID) require.Equal(t, job.Namespace, resp3.Jobs[0].Namespace) + + // Lookup jobs with a meta parameter + get = &structs.JobListRequest{ + QueryOptions: structs.QueryOptions{ + Region: "global", + Namespace: job.Namespace, + Prefix: resp2.Jobs[0].ID[:4], + }, + Fields: &structs.JobStubFields{ + Meta: true, + }, + } + var resp4 structs.JobListResponse + err = msgpackrpc.CallWithCodec(codec, "Job.List", get, &resp4) + require.NoError(t, err) + require.Equal(t, job.Meta["owner"], resp4.Jobs[0].Meta["owner"]) } // TestJobEndpoint_ListJobs_AllNamespaces_OSS asserts that server diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index cf08db0be..c05f7e1bc 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -689,6 +689,12 @@ type JobSpecificRequest struct { // JobListRequest is used to parameterize a list request type JobListRequest struct { QueryOptions + Fields *JobStubFields +} + +// Stub returns a summarized version of the job +type JobStubFields struct { + Meta bool } // JobPlanRequest is used for the Job.Plan endpoint to trigger a dry-run @@ -4517,8 +4523,8 @@ func (j *Job) HasUpdateStrategy() bool { } // Stub is used to return a summary of the job -func (j *Job) Stub(summary *JobSummary) *JobListStub { - return &JobListStub{ +func (j *Job) Stub(summary *JobSummary, fields *JobStubFields) *JobListStub { + jobStub := &JobListStub{ ID: j.ID, Namespace: j.Namespace, ParentID: j.ParentID, @@ -4538,6 +4544,14 @@ func (j *Job) Stub(summary *JobSummary) *JobListStub { SubmitTime: j.SubmitTime, JobSummary: summary, } + + if fields != nil { + if fields.Meta { + jobStub.Meta = j.Meta + } + } + + return jobStub } // IsPeriodic returns whether a job is periodic. @@ -4721,6 +4735,7 @@ type JobListStub struct { ModifyIndex uint64 JobModifyIndex uint64 SubmitTime int64 + Meta map[string]string `json:",omitempty"` } // JobSummary summarizes the state of the allocations of a job diff --git a/ui/app/routes/jobs/index.js b/ui/app/routes/jobs/index.js index 93bc9d39e..f894ba9d2 100644 --- a/ui/app/routes/jobs/index.js +++ b/ui/app/routes/jobs/index.js @@ -22,7 +22,7 @@ export default class IndexRoute extends Route.extend( model(params) { return RSVP.hash({ jobs: this.store - .query('job', { namespace: params.qpNamespace }) + .query('job', { namespace: params.qpNamespace, meta: true }) .catch(notifyForbidden(this)), namespaces: this.store.findAll('namespace'), }); @@ -32,7 +32,7 @@ export default class IndexRoute extends Route.extend( controller.set('namespacesWatch', this.watchNamespaces.perform()); controller.set( 'modelWatch', - this.watchJobs.perform({ namespace: controller.qpNamesapce }) + this.watchJobs.perform({ namespace: controller.qpNamespace, meta: true }) ); } diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js index dba817d7b..c8f94ae33 100644 --- a/ui/app/routes/jobs/job.js +++ b/ui/app/routes/jobs/job.js @@ -35,7 +35,7 @@ export default class JobRoute extends Route { const relatedModelsQueries = [ job.get('allocations'), job.get('evaluations'), - this.store.query('job', { namespace }), + this.store.query('job', { namespace, meta: true }), this.store.findAll('namespace'), ]; diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index 2c527ef9f..68ff254dd 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -31,7 +31,10 @@ export default class IndexRoute extends Route.extend(WithWatchers) { this.watchLatestDeployment.perform(model), list: model.get('hasChildren') && - this.watchAllJobs.perform({ namespace: model.namespace.get('name') }), + this.watchAllJobs.perform({ + namespace: model.namespace.get('name'), + meta: true, + }), nodes: model.get('hasClientStatus') && this.can.can('read client') && diff --git a/ui/app/styles/core/tag.scss b/ui/app/styles/core/tag.scss index 4682b8bdb..aa1b627d5 100644 --- a/ui/app/styles/core/tag.scss +++ b/ui/app/styles/core/tag.scss @@ -68,6 +68,11 @@ vertical-align: 2px; } + &.is-pack { + position: relative; + top: 3px; + } + .icon { height: 1rem; width: 1rem; diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index f2212ebdf..7031db97c 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -10,6 +10,14 @@ class="is-primary" > {{this.job.name}} + + {{#if this.job.meta.structured.pack}} + + {{x-icon "box" class= "test"}} + Pack + + {{/if}} + {{#if this.system.shouldShowNamespaces}} diff --git a/website/content/api-docs/jobs.mdx b/website/content/api-docs/jobs.mdx index e8339a859..aefb12651 100644 --- a/website/content/api-docs/jobs.mdx +++ b/website/content/api-docs/jobs.mdx @@ -46,6 +46,8 @@ The table below shows this endpoint's support for - `namespace` `(string: "default")` - Specifies the target namespace. Specifying `*` would return all jobs across all the authorized namespaces. +- `meta` `(bool: false)` - If set, jobs returned will include a [meta](/docs/job-specification/meta) field containing all + ### Sample Request ```shell-session