[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 <tgross@hashicorp.com>

Co-authored-by: Tim Gross <tgross@hashicorp.com>
This commit is contained in:
Phil Renaud 2022-11-02 16:58:24 -04:00 committed by GitHub
parent 6d5fe56fa1
commit ffb4c63af7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 94 additions and 9 deletions

3
.changelog/14833.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Adds a "Pack" tag and logo on the jobs list index when appropriate
```

View File

@ -155,10 +155,31 @@ func (j *Jobs) RegisterOpts(job *Job, opts *RegisterOptions, q *WriteOptions) (*
return &resp, wm, nil return &resp, wm, nil
} }
type JobListFields struct {
Meta bool
}
type JobListOptions struct {
Fields *JobListFields
}
// List is used to list all of the existing jobs. // List is used to list all of the existing jobs.
func (j *Jobs) List(q *QueryOptions) ([]*JobListStub, *QueryMeta, error) { 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 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 { if err != nil {
return nil, qm, err return nil, qm, err
} }
@ -1063,6 +1084,7 @@ type JobListStub struct {
ModifyIndex uint64 ModifyIndex uint64
JobModifyIndex uint64 JobModifyIndex uint64
SubmitTime int64 SubmitTime int64
Meta map[string]string `json:",omitempty"`
} }
// JobIDSort is used to sort jobs by their job ID's. // JobIDSort is used to sort jobs by their job ID's.

View File

@ -37,6 +37,16 @@ func (s *HTTPServer) jobListRequest(resp http.ResponseWriter, req *http.Request)
return nil, nil 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 var out structs.JobListResponse
if err := s.agent.RPC("Job.List", &args, &out); err != nil { if err := s.agent.RPC("Job.List", &args, &out); err != nil {
return nil, err return nil, err

View File

@ -128,7 +128,7 @@ func (c *JobStatusCommand) Run(args []string) int {
// Invoke list mode if no job ID. // Invoke list mode if no job ID.
if len(args) == 0 { if len(args) == 0 {
jobs, _, err := client.Jobs().List(nil) jobs, _, err := client.Jobs().ListOptions(nil, nil)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying jobs: %s", err)) c.Ui.Error(fmt.Sprintf("Error querying jobs: %s", err))

View File

@ -1377,7 +1377,7 @@ func (j *Job) List(args *structs.JobListRequest, reply *structs.JobListResponse)
if err != nil || summary == nil { if err != nil || summary == nil {
return fmt.Errorf("unable to look up summary for job: %v", job.ID) 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 return nil
}) })
if err != nil { if err != nil {

View File

@ -5113,6 +5113,7 @@ func TestJobEndpoint_ListJobs(t *testing.T) {
require.Len(t, resp2.Jobs, 1) require.Len(t, resp2.Jobs, 1)
require.Equal(t, job.ID, resp2.Jobs[0].ID) require.Equal(t, job.ID, resp2.Jobs[0].ID)
require.Equal(t, job.Namespace, resp2.Jobs[0].Namespace) require.Equal(t, job.Namespace, resp2.Jobs[0].Namespace)
require.Nil(t, resp2.Jobs[0].Meta)
// Lookup the jobs by prefix // Lookup the jobs by prefix
get = &structs.JobListRequest{ get = &structs.JobListRequest{
@ -5129,6 +5130,22 @@ func TestJobEndpoint_ListJobs(t *testing.T) {
require.Len(t, resp3.Jobs, 1) require.Len(t, resp3.Jobs, 1)
require.Equal(t, job.ID, resp3.Jobs[0].ID) require.Equal(t, job.ID, resp3.Jobs[0].ID)
require.Equal(t, job.Namespace, resp3.Jobs[0].Namespace) 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 // TestJobEndpoint_ListJobs_AllNamespaces_OSS asserts that server

View File

@ -689,6 +689,12 @@ type JobSpecificRequest struct {
// JobListRequest is used to parameterize a list request // JobListRequest is used to parameterize a list request
type JobListRequest struct { type JobListRequest struct {
QueryOptions 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 // 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 // Stub is used to return a summary of the job
func (j *Job) Stub(summary *JobSummary) *JobListStub { func (j *Job) Stub(summary *JobSummary, fields *JobStubFields) *JobListStub {
return &JobListStub{ jobStub := &JobListStub{
ID: j.ID, ID: j.ID,
Namespace: j.Namespace, Namespace: j.Namespace,
ParentID: j.ParentID, ParentID: j.ParentID,
@ -4538,6 +4544,14 @@ func (j *Job) Stub(summary *JobSummary) *JobListStub {
SubmitTime: j.SubmitTime, SubmitTime: j.SubmitTime,
JobSummary: summary, JobSummary: summary,
} }
if fields != nil {
if fields.Meta {
jobStub.Meta = j.Meta
}
}
return jobStub
} }
// IsPeriodic returns whether a job is periodic. // IsPeriodic returns whether a job is periodic.
@ -4721,6 +4735,7 @@ type JobListStub struct {
ModifyIndex uint64 ModifyIndex uint64
JobModifyIndex uint64 JobModifyIndex uint64
SubmitTime int64 SubmitTime int64
Meta map[string]string `json:",omitempty"`
} }
// JobSummary summarizes the state of the allocations of a job // JobSummary summarizes the state of the allocations of a job

View File

@ -22,7 +22,7 @@ export default class IndexRoute extends Route.extend(
model(params) { model(params) {
return RSVP.hash({ return RSVP.hash({
jobs: this.store jobs: this.store
.query('job', { namespace: params.qpNamespace }) .query('job', { namespace: params.qpNamespace, meta: true })
.catch(notifyForbidden(this)), .catch(notifyForbidden(this)),
namespaces: this.store.findAll('namespace'), namespaces: this.store.findAll('namespace'),
}); });
@ -32,7 +32,7 @@ export default class IndexRoute extends Route.extend(
controller.set('namespacesWatch', this.watchNamespaces.perform()); controller.set('namespacesWatch', this.watchNamespaces.perform());
controller.set( controller.set(
'modelWatch', 'modelWatch',
this.watchJobs.perform({ namespace: controller.qpNamesapce }) this.watchJobs.perform({ namespace: controller.qpNamespace, meta: true })
); );
} }

View File

@ -35,7 +35,7 @@ export default class JobRoute extends Route {
const relatedModelsQueries = [ const relatedModelsQueries = [
job.get('allocations'), job.get('allocations'),
job.get('evaluations'), job.get('evaluations'),
this.store.query('job', { namespace }), this.store.query('job', { namespace, meta: true }),
this.store.findAll('namespace'), this.store.findAll('namespace'),
]; ];

View File

@ -31,7 +31,10 @@ export default class IndexRoute extends Route.extend(WithWatchers) {
this.watchLatestDeployment.perform(model), this.watchLatestDeployment.perform(model),
list: list:
model.get('hasChildren') && model.get('hasChildren') &&
this.watchAllJobs.perform({ namespace: model.namespace.get('name') }), this.watchAllJobs.perform({
namespace: model.namespace.get('name'),
meta: true,
}),
nodes: nodes:
model.get('hasClientStatus') && model.get('hasClientStatus') &&
this.can.can('read client') && this.can.can('read client') &&

View File

@ -68,6 +68,11 @@
vertical-align: 2px; vertical-align: 2px;
} }
&.is-pack {
position: relative;
top: 3px;
}
.icon { .icon {
height: 1rem; height: 1rem;
width: 1rem; width: 1rem;

View File

@ -10,6 +10,14 @@
class="is-primary" class="is-primary"
> >
{{this.job.name}} {{this.job.name}}
{{#if this.job.meta.structured.pack}}
<span data-test-pack-tag class="tag is-pack">
{{x-icon "box" class= "test"}}
<span>Pack</span>
</span>
{{/if}}
</LinkTo> </LinkTo>
</td> </td>
{{#if this.system.shouldShowNamespaces}} {{#if this.system.shouldShowNamespaces}}

View File

@ -46,6 +46,8 @@ The table below shows this endpoint's support for
- `namespace` `(string: "default")` - Specifies the target namespace. Specifying - `namespace` `(string: "default")` - Specifies the target namespace. Specifying
`*` would return all jobs across all the authorized namespaces. `*` 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 ### Sample Request
```shell-session ```shell-session