feat: Add tag scopes to front and backend, initial commit

This commit is contained in:
Christoph Kluge 2024-08-01 18:59:24 +02:00
parent c80d3a6958
commit 017f9b2140
18 changed files with 361 additions and 98 deletions

View File

@ -113,6 +113,7 @@ type Tag {
id: ID!
type: String!
name: String!
scope: String!
}
type Resource {
@ -235,7 +236,7 @@ type Query {
}
type Mutation {
createTag(type: String!, name: String!): Tag!
createTag(type: String!, name: String!, scope: String!): Tag!
deleteTag(id: ID!): ID!
addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!

View File

@ -176,8 +176,9 @@ type ErrorResponse struct {
// ApiTag model
type ApiTag struct {
// Tag Type
Type string `json:"type" example:"Debug"`
Name string `json:"name" example:"Testjob"` // Tag Name
Type string `json:"type" example:"Debug"`
Name string `json:"name" example:"Testjob"` // Tag Name
Scope string `json:"scope" example:"global"` // Tag Scope for Frontend Display
}
// ApiMeta model
@ -419,7 +420,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
StartTime: job.StartTime.Unix(),
}
res.Tags, err = api.JobRepository.GetTags(&job.ID)
res.Tags, err = api.JobRepository.GetTags(r.Context(), &job.ID)
if err != nil {
handleError(err, http.StatusInternalServerError, rw)
return
@ -492,7 +493,7 @@ func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request)
return
}
job.Tags, err = api.JobRepository.GetTags(&job.ID)
job.Tags, err = api.JobRepository.GetTags(r.Context(), &job.ID)
if err != nil {
handleError(err, http.StatusInternalServerError, rw)
return
@ -578,7 +579,7 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
return
}
job.Tags, err = api.JobRepository.GetTags(&job.ID)
job.Tags, err = api.JobRepository.GetTags(r.Context(), &job.ID)
if err != nil {
handleError(err, http.StatusInternalServerError, rw)
return
@ -711,7 +712,7 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
return
}
job.Tags, err = api.JobRepository.GetTags(&job.ID)
job.Tags, err = api.JobRepository.GetTags(r.Context(), &job.ID)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
@ -724,16 +725,17 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
}
for _, tag := range req {
tagId, err := api.JobRepository.AddTagOrCreate(job.ID, tag.Type, tag.Name)
tagId, err := api.JobRepository.AddTagOrCreate(r.Context(), job.ID, tag.Type, tag.Name, tag.Scope)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
job.Tags = append(job.Tags, &schema.Tag{
ID: tagId,
Type: tag.Type,
Name: tag.Name,
ID: tagId,
Type: tag.Type,
Name: tag.Name,
Scope: tag.Scope,
})
}
@ -801,7 +803,7 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
unlockOnce.Do(api.RepositoryMutex.Unlock)
for _, tag := range req.Tags {
if _, err := api.JobRepository.AddTagOrCreate(id, tag.Type, tag.Name); err != nil {
if _, err := api.JobRepository.AddTagOrCreate(r.Context(), id, tag.Type, tag.Name, tag.Scope); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
handleError(fmt.Errorf("adding tag to new job %d failed: %w", id, err), http.StatusInternalServerError, rw)
return

View File

@ -229,7 +229,7 @@ type ComplexityRoot struct {
Mutation struct {
AddTagsToJob func(childComplexity int, job string, tagIds []string) int
CreateTag func(childComplexity int, typeArg string, name string) int
CreateTag func(childComplexity int, typeArg string, name string, scope string) int
DeleteTag func(childComplexity int, id string) int
RemoveTagsFromJob func(childComplexity int, job string, tagIds []string) int
UpdateConfiguration func(childComplexity int, name string, value string) int
@ -303,9 +303,10 @@ type ComplexityRoot struct {
}
Tag struct {
ID func(childComplexity int) int
Name func(childComplexity int) int
Type func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
Scope func(childComplexity int) int
Type func(childComplexity int) int
}
TimeRangeOutput struct {
@ -355,7 +356,7 @@ type MetricValueResolver interface {
Name(ctx context.Context, obj *schema.MetricValue) (*string, error)
}
type MutationResolver interface {
CreateTag(ctx context.Context, typeArg string, name string) (*schema.Tag, error)
CreateTag(ctx context.Context, typeArg string, name string, scope string) (*schema.Tag, error)
DeleteTag(ctx context.Context, id string) (string, error)
AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error)
RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error)
@ -1183,7 +1184,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
return e.complexity.Mutation.CreateTag(childComplexity, args["type"].(string), args["name"].(string)), true
return e.complexity.Mutation.CreateTag(childComplexity, args["type"].(string), args["name"].(string), args["scope"].(string)), true
case "Mutation.deleteTag":
if e.complexity.Mutation.DeleteTag == nil {
@ -1602,6 +1603,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Tag.Name(childComplexity), true
case "Tag.scope":
if e.complexity.Tag.Scope == nil {
break
}
return e.complexity.Tag.Scope(childComplexity), true
case "Tag.type":
if e.complexity.Tag.Type == nil {
break
@ -1949,6 +1957,7 @@ type Tag {
id: ID!
type: String!
name: String!
scope: String!
}
type Resource {
@ -2071,7 +2080,7 @@ type Query {
}
type Mutation {
createTag(type: String!, name: String!): Tag!
createTag(type: String!, name: String!, scope: String!): Tag!
deleteTag(id: ID!): ID!
addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!
@ -2244,6 +2253,15 @@ func (ec *executionContext) field_Mutation_createTag_args(ctx context.Context, r
}
}
args["name"] = arg1
var arg2 string
if tmp, ok := rawArgs["scope"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("scope"))
arg2, err = ec.unmarshalNString2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["scope"] = arg2
return args, nil
}
@ -4622,6 +4640,8 @@ func (ec *executionContext) fieldContext_Job_tags(_ context.Context, field graph
return ec.fieldContext_Tag_type(ctx, field)
case "name":
return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
},
@ -7680,7 +7700,7 @@ func (ec *executionContext) _Mutation_createTag(ctx context.Context, field graph
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().CreateTag(rctx, fc.Args["type"].(string), fc.Args["name"].(string))
return ec.resolvers.Mutation().CreateTag(rctx, fc.Args["type"].(string), fc.Args["name"].(string), fc.Args["scope"].(string))
})
if err != nil {
ec.Error(ctx, err)
@ -7711,6 +7731,8 @@ func (ec *executionContext) fieldContext_Mutation_createTag(ctx context.Context,
return ec.fieldContext_Tag_type(ctx, field)
case "name":
return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
},
@ -7829,6 +7851,8 @@ func (ec *executionContext) fieldContext_Mutation_addTagsToJob(ctx context.Conte
return ec.fieldContext_Tag_type(ctx, field)
case "name":
return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
},
@ -7892,6 +7916,8 @@ func (ec *executionContext) fieldContext_Mutation_removeTagsFromJob(ctx context.
return ec.fieldContext_Tag_type(ctx, field)
case "name":
return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
},
@ -8199,6 +8225,8 @@ func (ec *executionContext) fieldContext_Query_tags(_ context.Context, field gra
return ec.fieldContext_Tag_type(ctx, field)
case "name":
return ec.fieldContext_Tag_name(ctx, field)
case "scope":
return ec.fieldContext_Tag_scope(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
},
@ -10547,6 +10575,50 @@ func (ec *executionContext) fieldContext_Tag_name(_ context.Context, field graph
return fc, nil
}
func (ec *executionContext) _Tag_scope(ctx context.Context, field graphql.CollectedField, obj *schema.Tag) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Tag_scope(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Scope, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Tag_scope(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Tag",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _TimeRangeOutput_from(ctx context.Context, field graphql.CollectedField, obj *model.TimeRangeOutput) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_TimeRangeOutput_from(ctx, field)
if err != nil {
@ -15666,6 +15738,11 @@ func (ec *executionContext) _Tag(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "scope":
out.Values[i] = ec._Tag_scope(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}

View File

@ -29,7 +29,7 @@ func (r *clusterResolver) Partitions(ctx context.Context, obj *schema.Cluster) (
// Tags is the resolver for the tags field.
func (r *jobResolver) Tags(ctx context.Context, obj *schema.Job) ([]*schema.Tag, error) {
return r.Repo.GetTags(&obj.ID)
return r.Repo.GetTags(ctx, &obj.ID)
}
// ConcurrentJobs is the resolver for the concurrentJobs field.
@ -86,14 +86,14 @@ func (r *metricValueResolver) Name(ctx context.Context, obj *schema.MetricValue)
}
// CreateTag is the resolver for the createTag field.
func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string) (*schema.Tag, error) {
id, err := r.Repo.CreateTag(typeArg, name)
func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string, scope string) (*schema.Tag, error) {
id, err := r.Repo.CreateTag(typeArg, name, scope)
if err != nil {
log.Warn("Error while creating tag")
return nil, err
}
return &schema.Tag{ID: id, Type: typeArg, Name: name}, nil
return &schema.Tag{ID: id, Type: typeArg, Name: name, Scope: scope}, nil
}
// DeleteTag is the resolver for the deleteTag field.
@ -103,6 +103,7 @@ func (r *mutationResolver) DeleteTag(ctx context.Context, id string) (string, er
// AddTagsToJob is the resolver for the addTagsToJob field.
func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
// Selectable Tags Pre-Filtered by Scope in Frontend: No backend check required
jid, err := strconv.ParseInt(job, 10, 64)
if err != nil {
log.Warn("Error while adding tag to job")
@ -117,7 +118,7 @@ func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds
return nil, err
}
if tags, err = r.Repo.AddTag(jid, tid); err != nil {
if tags, err = r.Repo.AddTag(ctx, jid, tid); err != nil {
log.Warn("Error while adding tag")
return nil, err
}
@ -128,6 +129,7 @@ func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds
// RemoveTagsFromJob is the resolver for the removeTagsFromJob field.
func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
// Removable Tags Pre-Filtered by Scope in Frontend: No backend check required
jid, err := strconv.ParseInt(job, 10, 64)
if err != nil {
log.Warn("Error while parsing job id")
@ -142,7 +144,7 @@ func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, ta
return nil, err
}
if tags, err = r.Repo.RemoveTag(jid, tid); err != nil {
if tags, err = r.Repo.RemoveTag(ctx, jid, tid); err != nil {
log.Warn("Error while removing tag")
return nil, err
}
@ -168,7 +170,7 @@ func (r *queryResolver) Clusters(ctx context.Context) ([]*schema.Cluster, error)
// Tags is the resolver for the tags field.
func (r *queryResolver) Tags(ctx context.Context) ([]*schema.Tag, error) {
return r.Repo.GetTags(nil)
return r.Repo.GetTags(ctx, nil)
}
// GlobalMetrics is the resolver for the globalMetrics field.

View File

@ -112,8 +112,8 @@ func HandleImportFlag(flag string) error {
}
for _, tag := range job.Tags {
if _, err := r.AddTagOrCreate(id, tag.Type, tag.Name); err != nil {
log.Error("Error while adding or creating tag")
if err := r.ImportTag(id, tag.Type, tag.Name, tag.Scope); err != nil {
log.Error("Error while adding or creating tag on import")
return err
}
}

View File

@ -5,6 +5,8 @@
package repository
import (
"context"
"fmt"
"strings"
"github.com/ClusterCockpit/cc-backend/pkg/archive"
@ -14,7 +16,14 @@ import (
)
// Add the tag with id `tagId` to the job with the database id `jobId`.
func (r *JobRepository) AddTag(job int64, tag int64) ([]*schema.Tag, error) {
func (r *JobRepository) AddTag(ctx context.Context, job int64, tag int64) ([]*schema.Tag, error) {
j, err := r.FindById(ctx, job)
if err != nil {
log.Warn("Error while finding job by id")
return nil, err
}
q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(job, tag)
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
@ -23,49 +32,56 @@ func (r *JobRepository) AddTag(job int64, tag int64) ([]*schema.Tag, error) {
return nil, err
}
j, err := r.FindByIdDirect(job)
if err != nil {
log.Warn("Error while finding job by id")
return nil, err
}
tags, err := r.GetTags(&job)
tags, err := r.GetTags(ctx, &job)
if err != nil {
log.Warn("Error while getting tags for job")
return nil, err
}
return tags, archive.UpdateTags(j, tags)
archiveTags, err := r.getArchiveTags(&job)
if err != nil {
log.Warn("Error while getting tags for job")
return nil, err
}
return tags, archive.UpdateTags(j, archiveTags)
}
// Removes a tag from a job
func (r *JobRepository) RemoveTag(job, tag int64) ([]*schema.Tag, error) {
func (r *JobRepository) RemoveTag(ctx context.Context, job, tag int64) ([]*schema.Tag, error) {
j, err := r.FindById(ctx, job)
if err != nil {
log.Warn("Error while finding job by id")
return nil, err
}
q := sq.Delete("jobtag").Where("jobtag.job_id = ?", job).Where("jobtag.tag_id = ?", tag)
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
s, _, _ := q.ToSql()
log.Errorf("Error adding tag with %s: %v", s, err)
log.Errorf("Error removing tag with %s: %v", s, err)
return nil, err
}
j, err := r.FindByIdDirect(job)
if err != nil {
log.Warn("Error while finding job by id")
return nil, err
}
tags, err := r.GetTags(&job)
tags, err := r.GetTags(ctx, &job)
if err != nil {
log.Warn("Error while getting tags for job")
return nil, err
}
return tags, archive.UpdateTags(j, tags)
archiveTags, err := r.getArchiveTags(&job)
if err != nil {
log.Warn("Error while getting tags for job")
return nil, err
}
return tags, archive.UpdateTags(j, archiveTags)
}
// CreateTag creates a new tag with the specified type and name and returns its database id.
func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64, err error) {
q := sq.Insert("tag").Columns("tag_type", "tag_name").Values(tagType, tagName)
func (r *JobRepository) CreateTag(tagType string, tagName string, tagScope string) (tagId int64, err error) {
q := sq.Insert("tag").Columns("tag_type", "tag_name", "tag_scope").Values(tagType, tagName, tagScope)
res, err := q.RunWith(r.stmtCache).Exec()
if err != nil {
@ -77,9 +93,10 @@ func (r *JobRepository) CreateTag(tagType string, tagName string) (tagId int64,
return res.LastInsertId()
}
func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts map[string]int, err error) {
func (r *JobRepository) CountTags(ctx context.Context) (tags []schema.Tag, counts map[string]int, err error) {
// Fetch all Tags in DB for Display in Frontend Tag-View
tags = make([]schema.Tag, 0, 100)
xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name FROM tag")
xrows, err := r.DB.Queryx("SELECT id, tag_type, tag_name, tag_scope FROM tag")
if err != nil {
return nil, nil, err
}
@ -89,14 +106,36 @@ func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts
if err = xrows.StructScan(&t); err != nil {
return nil, nil, err
}
tags = append(tags, t)
// Handle Scope Filtering: Tag Scope is Global, Private (== Username) or User is auth'd to view Admin Tags
readable, err := r.checkScopeAuth(ctx, "read", t.Scope)
if err != nil {
return nil, nil, err
}
if readable {
tags = append(tags, t)
}
}
user := GetUserFromContext(ctx)
// Query and Count Jobs with attached Tags
q := sq.Select("t.tag_name, count(jt.tag_id)").
From("tag t").
LeftJoin("jobtag jt ON t.id = jt.tag_id").
GroupBy("t.tag_name")
// Handle Scope Filtering
scopeList := "\"global\""
if user != nil {
scopeList += ",\"" + user.Username + "\""
}
if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) {
scopeList += ",\"admin\""
}
q = q.Where("t.tag_scope IN (" + scopeList + ")")
// Handle Job Ownership
if user != nil && user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { // ADMIN || SUPPORT: Count all jobs
log.Debug("CountTags: User Admin or Support -> Count all Jobs for Tags")
// Unchanged: Needs to be own case still, due to UserRole/NoRole compatibility handling in else case
@ -123,21 +162,30 @@ func (r *JobRepository) CountTags(user *schema.User) (tags []schema.Tag, counts
}
err = rows.Err()
return
return tags, counts, err
}
// AddTagOrCreate adds the tag with the specified type and name to the job with the database id `jobId`.
// If such a tag does not yet exist, it is created.
func (r *JobRepository) AddTagOrCreate(jobId int64, tagType string, tagName string) (tagId int64, err error) {
tagId, exists := r.TagId(tagType, tagName)
func (r *JobRepository) AddTagOrCreate(ctx context.Context, jobId int64, tagType string, tagName string, tagScope string) (tagId int64, err error) {
writable, err := r.checkScopeAuth(ctx, "write", tagScope)
if err != nil {
return 0, err
}
if !writable {
return 0, fmt.Errorf("cannot write tag scope with current authorization")
}
tagId, exists := r.TagId(tagType, tagName, tagScope)
if !exists {
tagId, err = r.CreateTag(tagType, tagName)
tagId, err = r.CreateTag(tagType, tagName, tagScope)
if err != nil {
return 0, err
}
}
if _, err := r.AddTag(jobId, tagId); err != nil {
if _, err := r.AddTag(ctx, jobId, tagId); err != nil {
return 0, err
}
@ -145,19 +193,19 @@ func (r *JobRepository) AddTagOrCreate(jobId int64, tagType string, tagName stri
}
// TagId returns the database id of the tag with the specified type and name.
func (r *JobRepository) TagId(tagType string, tagName string) (tagId int64, exists bool) {
func (r *JobRepository) TagId(tagType string, tagName string, tagScope string) (tagId int64, exists bool) {
exists = true
if err := sq.Select("id").From("tag").
Where("tag.tag_type = ?", tagType).Where("tag.tag_name = ?", tagName).
Where("tag.tag_type = ?", tagType).Where("tag.tag_name = ?", tagName).Where("tag.tag_scope = ?", tagScope).
RunWith(r.stmtCache).QueryRow().Scan(&tagId); err != nil {
exists = false
}
return
}
// GetTags returns a list of all tags if job is nil or of the tags that the job with that database ID has.
func (r *JobRepository) GetTags(job *int64) ([]*schema.Tag, error) {
q := sq.Select("id", "tag_type", "tag_name").From("tag")
// GetTags returns a list of all scoped tags if job is nil or of the tags that the job with that database ID has.
func (r *JobRepository) GetTags(ctx context.Context, job *int64) ([]*schema.Tag, error) {
q := sq.Select("id", "tag_type", "tag_name", "tag_scope").From("tag")
if job != nil {
q = q.Join("jobtag ON jobtag.tag_id = tag.id").Where("jobtag.job_id = ?", *job)
}
@ -172,7 +220,41 @@ func (r *JobRepository) GetTags(job *int64) ([]*schema.Tag, error) {
tags := make([]*schema.Tag, 0)
for rows.Next() {
tag := &schema.Tag{}
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name); err != nil {
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
log.Warn("Error while scanning rows")
return nil, err
}
// Handle Scope Filtering: Tag Scope is Global, Private (== Username) or User is auth'd to view Admin Tags
readable, err := r.checkScopeAuth(ctx, "read", tag.Scope)
if err != nil {
return nil, err
}
if readable {
tags = append(tags, tag)
}
}
return tags, nil
}
// GetArchiveTags returns a list of all tags *regardless of scope* if job is nil or of the tags that the job with that database ID has.
func (r *JobRepository) getArchiveTags(job *int64) ([]*schema.Tag, error) {
q := sq.Select("id", "tag_type", "tag_name", "tag_scope").From("tag")
if job != nil {
q = q.Join("jobtag ON jobtag.tag_id = tag.id").Where("jobtag.job_id = ?", *job)
}
rows, err := q.RunWith(r.stmtCache).Query()
if err != nil {
s, _, _ := q.ToSql()
log.Errorf("Error get tags with %s: %v", s, err)
return nil, err
}
tags := make([]*schema.Tag, 0)
for rows.Next() {
tag := &schema.Tag{}
if err := rows.Scan(&tag.ID, &tag.Type, &tag.Name, &tag.Scope); err != nil {
log.Warn("Error while scanning rows")
return nil, err
}
@ -181,3 +263,60 @@ func (r *JobRepository) GetTags(job *int64) ([]*schema.Tag, error) {
return tags, nil
}
func (r *JobRepository) ImportTag(jobId int64, tagType string, tagName string, tagScope string) (err error) {
// Import has no scope ctx, only import from metafile to DB (No recursive archive update required), only returns err
tagId, exists := r.TagId(tagType, tagName, tagScope)
if !exists {
tagId, err = r.CreateTag(tagType, tagName, tagScope)
if err != nil {
return err
}
}
q := sq.Insert("jobtag").Columns("job_id", "tag_id").Values(jobId, tagId)
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
s, _, _ := q.ToSql()
log.Errorf("Error adding tag on import with %s: %v", s, err)
return err
}
return nil
}
func (r *JobRepository) checkScopeAuth(ctx context.Context, operation string, scope string) (pass bool, err error) {
user := GetUserFromContext(ctx)
if user != nil {
switch {
case operation == "write" && scope == "admin":
if user.HasRole(schema.RoleAdmin) || (len(user.Roles) == 1 && user.HasRole(schema.RoleApi)) {
return true, nil
}
return false, nil
case operation == "write" && scope == "global":
if user.HasRole(schema.RoleAdmin) || (len(user.Roles) == 1 && user.HasRole(schema.RoleApi)) {
return true, nil
}
return false, nil
case operation == "write" && scope == user.Username:
return true, nil
case operation == "read" && scope == "admin":
return user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}), nil
case operation == "read" && scope == "global":
return true, nil
case operation == "read" && scope == user.Username:
return true, nil
default:
if operation == "read" || operation == "write" {
// No acceptable scope: deny tag
return false, nil
} else {
return false, fmt.Errorf("error while checking tag operation auth: unknown operation (%s)", operation)
}
}
} else {
return false, fmt.Errorf("error while checking tag operation auth: no user in context")
}
}

View File

@ -124,9 +124,8 @@ func setupAnalysisRoute(i InfoType, r *http.Request) InfoType {
func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
jobRepo := repository.GetJobRepository()
user := repository.GetUserFromContext(r.Context())
tags, counts, err := jobRepo.CountTags(user)
tags, counts, err := jobRepo.CountTags(r.Context())
tagMap := make(map[string][]map[string]interface{})
if err != nil {
log.Warnf("GetTags failed: %s", err.Error())
@ -138,6 +137,7 @@ func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
tagItem := map[string]interface{}{
"id": tag.ID,
"name": tag.Name,
"scope": tag.Scope,
"count": counts[tag.Name],
}
tagMap[tag.Type] = append(tagMap[tag.Type], tagItem)

View File

@ -171,8 +171,9 @@ func UpdateTags(job *schema.Job, tags []*schema.Tag) error {
jobMeta.Tags = make([]*schema.Tag, 0)
for _, tag := range tags {
jobMeta.Tags = append(jobMeta.Tags, &schema.Tag{
Name: tag.Name,
Type: tag.Type,
Name: tag.Name,
Type: tag.Type,
Scope: tag.Scope,
})
}

View File

@ -117,9 +117,10 @@ type JobStatistics struct {
// Tag model
// @Description Defines a tag using name and type.
type Tag struct {
Type string `json:"type" db:"tag_type" example:"Debug"`
Name string `json:"name" db:"tag_name" example:"Testjob"`
ID int64 `json:"id" db:"id"`
Type string `json:"type" db:"tag_type" example:"Debug"`
Name string `json:"name" db:"tag_name" example:"Testjob"`
Scope string `json:"scope" db:"tag_scope" example:"global"`
ID int64 `json:"id" db:"id"`
}
// Resource model

View File

@ -2,9 +2,9 @@
@component Main single job display component; displays plots for every metric as well as various information
Properties:
- `dbid Number`: The jobs DB ID
- `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level
- `clusters [String]`: List of cluster names
- `roles [Number]`: Enum containing available roles
-->
@ -45,6 +45,7 @@
import MetricSelection from "./generic/select/MetricSelection.svelte";
export let dbid;
export let username;
export let authlevel;
export let roles;
@ -58,8 +59,7 @@
selectedScopes = [];
let plots = {},
jobTags,
statsTable
jobTags
let missingMetrics = [],
missingHosts = [],
@ -322,7 +322,7 @@
<Row class="mb-3">
<Col xs="auto">
{#if $initq.data}
<TagManagement job={$initq.data.job} bind:jobTags />
<TagManagement job={$initq.data.job} {username} {authlevel} {roles} bind:jobTags />
{/if}
</Col>
<Col xs="auto">
@ -418,7 +418,6 @@
{#if $jobMetrics?.data?.jobMetrics}
{#key $jobMetrics.data.jobMetrics}
<StatsTable
bind:this={statsTable}
job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics}
/>

View File

@ -27,7 +27,7 @@
import {
init,
checkMetricDisabled,
} from "./utils.js";
} from "./generic/utils.js";
import PlotTable from "./generic/PlotTable.svelte";
import MetricPlot from "./generic/plots/MetricPlot.svelte";
import TimeSelection from "./generic/select/TimeSelection.svelte";

View File

@ -81,6 +81,7 @@
id
type
name
scope
}
userData {
name

View File

@ -37,7 +37,13 @@
<a target={clickable ? "_blank" : null} href={clickable ? `/monitoring/jobs/?tag=${id}` : null}>
{#if tag}
<span class="badge bg-warning text-dark">{tag.type}: {tag.name}</span>
{#if tag?.scope === "global"}
<span style="background-color:#9e9e9e;" class="badge text-dark">{tag.type}: {tag.name}</span>
{:else if tag.scope === "admin"}
<span style="background-color:#80deea;" class="badge text-dark">{tag.type}: {tag.name}</span>
{:else}
<span class="badge bg-warning text-dark">{tag.type}: {tag.name}</span>
{/if}
{:else}
Loading...
{/if}

View File

@ -77,7 +77,7 @@ export function init(extraInitQuery = "") {
footprint
}
}
tags { id, name, type }
tags { id, name, type, scope }
globalMetrics {
name
scope

View File

@ -5,6 +5,7 @@ new Job({
target: document.getElementById('svelte-app'),
props: {
dbid: jobInfos.id,
username: username,
authlevel: authlevel,
roles: roles
},

View File

@ -4,6 +4,9 @@
Properties:
- `job Object`: The job object
- `jobTags [Number]`: The array of currently designated tags
- `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles
-->
<script>
import { getContext } from "svelte";
@ -25,30 +28,35 @@
export let job;
export let jobTags = job.tags;
export let username;
export let authlevel;
export let roles;
let allTags = getContext("tags"),
initialized = getContext("initialized");
let newTagType = "",
newTagName = "";
newTagName = "",
newTagScope = username;
let filterTerm = "";
let pendingChange = false;
let isOpen = false;
const client = getContextClient();
const createTagMutation = ({ type, name }) => {
const createTagMutation = ({ type, name, scope }) => {
return mutationStore({
client: client,
query: gql`
mutation ($type: String!, $name: String!) {
createTag(type: $type, name: $name) {
mutation ($type: String!, $name: String!, $scope: String!) {
createTag(type: $type, name: $name, scope: $scope) {
id
type
name
scope
}
}
`,
variables: { type, name },
variables: { type, name, scope },
});
};
@ -61,6 +69,7 @@
id
type
name
scope
}
}
`,
@ -77,6 +86,7 @@
id
type
name
scope
}
}
`,
@ -103,9 +113,9 @@
return true;
}
function createTag(type, name) {
function createTag(type, name, scope) {
pendingChange = true;
createTagMutation({ type: type, name: name }).subscribe((res) => {
createTagMutation({ type: type, name: name, scope: scope }).subscribe((res) => {
if (res.fetching === false && !res.error) {
pendingChange = false;
allTags = [...allTags, res.data.createTag];
@ -206,16 +216,30 @@
</ul>
<br />
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
<Button
outline
color="success"
on:click={(e) => (
e.preventDefault(), createTag(newTagType, newTagName)
)}
>
Create & Add Tag:
<Tag tag={{ type: newTagType, name: newTagName }} clickable={false} />
</Button>
<div class="d-flex">
<Button
style="margin-right: 10px;"
outline
color="success"
on:click={(e) => (
e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
)}
>
Create & Add Tag:
<Tag tag={{ type: newTagType, name: newTagName, scope: newTagScope }} clickable={false}/>
</Button>
{#if roles && authlevel >= roles.admin}
<select
style="max-width: 175px;"
class="form-select"
bind:value={newTagScope}
>
<option value={username}>Scope: Private</option>
<option value={"global"}>Scope: Global</option>
<option value={"admin"}>Scope: Admin</option>
</select>
{/if}
</div>
{:else if allTagsFiltered.length == 0}
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
{/if}

View File

@ -11,6 +11,7 @@
id: "{{ .Infos.id }}",
};
const clusterCockpitConfig = {{ .Config }};
const username = {{ .User.Username }};
const authlevel = {{ .User.GetAuthLevel }};
const roles = {{ .Roles }};
</script>

View File

@ -7,8 +7,16 @@
{{ $tagType }}
</div>
{{ range $tagList }}
<a class="btn btn-lg btn-warning" href="/monitoring/jobs/?tag={{ .id }}" role="button">
{{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a>
{{if eq .scope "global"}}
<a style="background-color:#9e9e9e;" class="btn btn-lg" href="/monitoring/jobs/?tag={{ .id }}" role="button">
{{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a>
{{else if eq .scope "admin"}}
<a style="background-color:#80deea;" class="btn btn-lg" href="/monitoring/jobs/?tag={{ .id }}" role="button">
{{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a>
{{else}}
<a class="btn btn-lg btn-warning" href="/monitoring/jobs/?tag={{ .id }}" role="button">
{{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a>
{{end}}
{{end}}
{{end}}
</div>