[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:
parent
6d5fe56fa1
commit
ffb4c63af7
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
ui: Adds a "Pack" tag and logo on the jobs list index when appropriate
|
||||||
|
```
|
24
api/jobs.go
24
api/jobs.go
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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') &&
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue