mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-10-31 16:05:06 +01:00 
			
		
		
		
	Merge pull request #379 from ClusterCockpit/add_tag_delete
Add Tag Deletion: API and Frontend
This commit is contained in:
		| @@ -277,6 +277,7 @@ type Mutation { | |||||||
|   deleteTag(id: ID!): ID! |   deleteTag(id: ID!): ID! | ||||||
|   addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]! |   addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]! | ||||||
|   removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]! |   removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]! | ||||||
|  |   removeTagFromList(tagIds: [ID!]!): [Int!]! | ||||||
|  |  | ||||||
|   updateConfiguration(name: String!, value: String!): String |   updateConfiguration(name: String!, value: String!): String | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| module github.com/ClusterCockpit/cc-backend | module github.com/ClusterCockpit/cc-backend | ||||||
|  |  | ||||||
| go 1.23.5 | go 1.23.5 | ||||||
|  |  | ||||||
| toolchain go1.24.1 | toolchain go1.24.1 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
|   | |||||||
| @@ -78,12 +78,14 @@ func (api *RestApi) MountApiRoutes(r *mux.Router) { | |||||||
| 	r.HandleFunc("/jobs/{id}", api.getJobById).Methods(http.MethodPost) | 	r.HandleFunc("/jobs/{id}", api.getJobById).Methods(http.MethodPost) | ||||||
| 	r.HandleFunc("/jobs/{id}", api.getCompleteJobById).Methods(http.MethodGet) | 	r.HandleFunc("/jobs/{id}", api.getCompleteJobById).Methods(http.MethodGet) | ||||||
| 	r.HandleFunc("/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch) | 	r.HandleFunc("/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch) | ||||||
|  | 	r.HandleFunc("/jobs/tag_job/{id}", api.removeTagJob).Methods(http.MethodDelete) | ||||||
| 	r.HandleFunc("/jobs/edit_meta/{id}", api.editMeta).Methods(http.MethodPost, http.MethodPatch) | 	r.HandleFunc("/jobs/edit_meta/{id}", api.editMeta).Methods(http.MethodPost, http.MethodPatch) | ||||||
| 	r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet) | 	r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet) | ||||||
| 	r.HandleFunc("/jobs/delete_job/", api.deleteJobByRequest).Methods(http.MethodDelete) | 	r.HandleFunc("/jobs/delete_job/", api.deleteJobByRequest).Methods(http.MethodDelete) | ||||||
| 	r.HandleFunc("/jobs/delete_job/{id}", api.deleteJobById).Methods(http.MethodDelete) | 	r.HandleFunc("/jobs/delete_job/{id}", api.deleteJobById).Methods(http.MethodDelete) | ||||||
| 	r.HandleFunc("/jobs/delete_job_before/{ts}", api.deleteJobBefore).Methods(http.MethodDelete) | 	r.HandleFunc("/jobs/delete_job_before/{ts}", api.deleteJobBefore).Methods(http.MethodDelete) | ||||||
|  |  | ||||||
|  | 	r.HandleFunc("/tags/", api.removeTags).Methods(http.MethodDelete) | ||||||
| 	r.HandleFunc("/clusters/", api.getClusters).Methods(http.MethodGet) | 	r.HandleFunc("/clusters/", api.getClusters).Methods(http.MethodGet) | ||||||
|  |  | ||||||
| 	if api.MachineStateDir != "" { | 	if api.MachineStateDir != "" { | ||||||
| @@ -750,6 +752,114 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { | |||||||
| 	json.NewEncoder(rw).Encode(job) | 	json.NewEncoder(rw).Encode(job) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // removeTagJob godoc | ||||||
|  | // @summary     Removes one or more tags from a job | ||||||
|  | // @tags Job add and modify | ||||||
|  | // @description Removes tag(s) from a job specified by DB ID. Name and Type of Tag(s) must match. | ||||||
|  | // @description Tag Scope is required for matching, options: "global", "admin". Private tags can not be deleted via API. | ||||||
|  | // @description If tagged job is already finished: Tag will be removed from respective archive files. | ||||||
|  | // @accept      json | ||||||
|  | // @produce     json | ||||||
|  | // @param       id      path     int                  true "Job Database ID" | ||||||
|  | // @param       request body     api.TagJobApiRequest true "Array of tag-objects to remove" | ||||||
|  | // @success     200     {object} schema.Job                "Updated job resource" | ||||||
|  | // @failure     400     {object} api.ErrorResponse         "Bad Request" | ||||||
|  | // @failure     401     {object} api.ErrorResponse         "Unauthorized" | ||||||
|  | // @failure     404     {object} api.ErrorResponse         "Job or tag does not exist" | ||||||
|  | // @failure     500     {object} api.ErrorResponse         "Internal Server Error" | ||||||
|  | // @security    ApiKeyAuth | ||||||
|  | // @router      /jobs/tag_job/{id} [delete] | ||||||
|  | func (api *RestApi) removeTagJob(rw http.ResponseWriter, r *http.Request) { | ||||||
|  | 	id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		http.Error(rw, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	job, err := api.JobRepository.FindById(r.Context(), id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		http.Error(rw, err.Error(), http.StatusNotFound) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	job.Tags, err = api.JobRepository.GetTags(repository.GetUserFromContext(r.Context()), &job.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		http.Error(rw, err.Error(), http.StatusInternalServerError) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var req TagJobApiRequest | ||||||
|  | 	if err := decode(r.Body, &req); err != nil { | ||||||
|  | 		http.Error(rw, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, rtag := range req { | ||||||
|  | 		// Only Global and Admin Tags | ||||||
|  | 		if rtag.Scope != "global" && rtag.Scope != "admin" { | ||||||
|  | 			log.Warnf("Cannot delete private tag for job %d: Skip", job.JobID) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		remainingTags, err := api.JobRepository.RemoveJobTagByRequest(repository.GetUserFromContext(r.Context()), job.ID, rtag.Type, rtag.Name, rtag.Scope) | ||||||
|  | 		if err != nil { | ||||||
|  | 			http.Error(rw, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		job.Tags = remainingTags | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rw.Header().Add("Content-Type", "application/json") | ||||||
|  | 	rw.WriteHeader(http.StatusOK) | ||||||
|  | 	json.NewEncoder(rw).Encode(job) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // removeTags godoc | ||||||
|  | // @summary     Removes all tags and job-relations for type:name tuple | ||||||
|  | // @tags Tag remove | ||||||
|  | // @description Removes tags by type and name. Name and Type of Tag(s) must match. | ||||||
|  | // @description Tag Scope is required for matching, options: "global", "admin". Private tags can not be deleted via API. | ||||||
|  | // @description Tag wills be removed from respective archive files. | ||||||
|  | // @accept      json | ||||||
|  | // @produce     plain | ||||||
|  | // @param       request body     api.TagJobApiRequest true "Array of tag-objects to remove" | ||||||
|  | // @success     200     {string} string                    "Success Response" | ||||||
|  | // @failure     400     {object} api.ErrorResponse         "Bad Request" | ||||||
|  | // @failure     401     {object} api.ErrorResponse         "Unauthorized" | ||||||
|  | // @failure     404     {object} api.ErrorResponse         "Job or tag does not exist" | ||||||
|  | // @failure     500     {object} api.ErrorResponse         "Internal Server Error" | ||||||
|  | // @security    ApiKeyAuth | ||||||
|  | // @router      /tags/ [delete] | ||||||
|  | func (api *RestApi) removeTags(rw http.ResponseWriter, r *http.Request) { | ||||||
|  | 	var req TagJobApiRequest | ||||||
|  | 	if err := decode(r.Body, &req); err != nil { | ||||||
|  | 		http.Error(rw, err.Error(), http.StatusBadRequest) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	targetCount := len(req) | ||||||
|  | 	currentCount := 0 | ||||||
|  | 	for _, rtag := range req { | ||||||
|  | 		// Only Global and Admin Tags | ||||||
|  | 		if rtag.Scope != "global" && rtag.Scope != "admin" { | ||||||
|  | 			log.Warn("Cannot delete private tag: Skip") | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		err := api.JobRepository.RemoveTagByRequest(rtag.Type, rtag.Name, rtag.Scope) | ||||||
|  | 		if err != nil { | ||||||
|  | 			http.Error(rw, err.Error(), http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} else { | ||||||
|  | 			currentCount++ | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rw.WriteHeader(http.StatusOK) | ||||||
|  | 	rw.Write([]byte(fmt.Sprintf("Deleted Tags from DB: %d successfull of %d requested\n", currentCount, targetCount))) | ||||||
|  | } | ||||||
|  |  | ||||||
| // startJob godoc | // startJob godoc | ||||||
| // @summary     Adds a new job as "running" | // @summary     Adds a new job as "running" | ||||||
| // @tags Job add and modify | // @tags Job add and modify | ||||||
|   | |||||||
| @@ -250,6 +250,7 @@ type ComplexityRoot struct { | |||||||
| 		AddTagsToJob        func(childComplexity int, job string, tagIds []string) int | 		AddTagsToJob        func(childComplexity int, job string, tagIds []string) int | ||||||
| 		CreateTag           func(childComplexity int, typeArg string, name string, scope string) int | 		CreateTag           func(childComplexity int, typeArg string, name string, scope string) int | ||||||
| 		DeleteTag           func(childComplexity int, id string) int | 		DeleteTag           func(childComplexity int, id string) int | ||||||
|  | 		RemoveTagFromList   func(childComplexity int, tagIds []string) int | ||||||
| 		RemoveTagsFromJob   func(childComplexity int, job string, tagIds []string) int | 		RemoveTagsFromJob   func(childComplexity int, job string, tagIds []string) int | ||||||
| 		UpdateConfiguration func(childComplexity int, name string, value string) int | 		UpdateConfiguration func(childComplexity int, name string, value string) int | ||||||
| 	} | 	} | ||||||
| @@ -399,6 +400,7 @@ type MutationResolver interface { | |||||||
| 	DeleteTag(ctx context.Context, id string) (string, error) | 	DeleteTag(ctx context.Context, id string) (string, error) | ||||||
| 	AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) | 	AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) | ||||||
| 	RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) | 	RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) | ||||||
|  | 	RemoveTagFromList(ctx context.Context, tagIds []string) ([]int, error) | ||||||
| 	UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) | 	UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) | ||||||
| } | } | ||||||
| type QueryResolver interface { | type QueryResolver interface { | ||||||
| @@ -1310,6 +1312,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in | |||||||
|  |  | ||||||
| 		return e.complexity.Mutation.DeleteTag(childComplexity, args["id"].(string)), true | 		return e.complexity.Mutation.DeleteTag(childComplexity, args["id"].(string)), true | ||||||
|  |  | ||||||
|  | 	case "Mutation.removeTagFromList": | ||||||
|  | 		if e.complexity.Mutation.RemoveTagFromList == nil { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		args, err := ec.field_Mutation_removeTagFromList_args(context.TODO(), rawArgs) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return 0, false | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return e.complexity.Mutation.RemoveTagFromList(childComplexity, args["tagIds"].([]string)), true | ||||||
|  |  | ||||||
| 	case "Mutation.removeTagsFromJob": | 	case "Mutation.removeTagsFromJob": | ||||||
| 		if e.complexity.Mutation.RemoveTagsFromJob == nil { | 		if e.complexity.Mutation.RemoveTagsFromJob == nil { | ||||||
| 			break | 			break | ||||||
| @@ -2339,6 +2353,7 @@ type Mutation { | |||||||
|   deleteTag(id: ID!): ID! |   deleteTag(id: ID!): ID! | ||||||
|   addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]! |   addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]! | ||||||
|   removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]! |   removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]! | ||||||
|  |   removeTagFromList(tagIds: [ID!]!): [Int!]! | ||||||
|  |  | ||||||
|   updateConfiguration(name: String!, value: String!): String |   updateConfiguration(name: String!, value: String!): String | ||||||
| } | } | ||||||
| @@ -2617,6 +2632,34 @@ func (ec *executionContext) field_Mutation_deleteTag_argsID( | |||||||
| 	return zeroVal, nil | 	return zeroVal, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (ec *executionContext) field_Mutation_removeTagFromList_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { | ||||||
|  | 	var err error | ||||||
|  | 	args := map[string]any{} | ||||||
|  | 	arg0, err := ec.field_Mutation_removeTagFromList_argsTagIds(ctx, rawArgs) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	args["tagIds"] = arg0 | ||||||
|  | 	return args, nil | ||||||
|  | } | ||||||
|  | func (ec *executionContext) field_Mutation_removeTagFromList_argsTagIds( | ||||||
|  | 	ctx context.Context, | ||||||
|  | 	rawArgs map[string]any, | ||||||
|  | ) ([]string, error) { | ||||||
|  | 	if _, ok := rawArgs["tagIds"]; !ok { | ||||||
|  | 		var zeroVal []string | ||||||
|  | 		return zeroVal, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("tagIds")) | ||||||
|  | 	if tmp, ok := rawArgs["tagIds"]; ok { | ||||||
|  | 		return ec.unmarshalNID2ᚕstringᚄ(ctx, tmp) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var zeroVal []string | ||||||
|  | 	return zeroVal, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (ec *executionContext) field_Mutation_removeTagsFromJob_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { | func (ec *executionContext) field_Mutation_removeTagsFromJob_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { | ||||||
| 	var err error | 	var err error | ||||||
| 	args := map[string]any{} | 	args := map[string]any{} | ||||||
| @@ -9690,6 +9733,61 @@ func (ec *executionContext) fieldContext_Mutation_removeTagsFromJob(ctx context. | |||||||
| 	return fc, nil | 	return fc, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (ec *executionContext) _Mutation_removeTagFromList(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { | ||||||
|  | 	fc, err := ec.fieldContext_Mutation_removeTagFromList(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) (any, error) { | ||||||
|  | 		ctx = rctx // use context from middleware stack in children | ||||||
|  | 		return ec.resolvers.Mutation().RemoveTagFromList(rctx, fc.Args["tagIds"].([]string)) | ||||||
|  | 	}) | ||||||
|  | 	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.([]int) | ||||||
|  | 	fc.Result = res | ||||||
|  | 	return ec.marshalNInt2ᚕintᚄ(ctx, field.Selections, res) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ec *executionContext) fieldContext_Mutation_removeTagFromList(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { | ||||||
|  | 	fc = &graphql.FieldContext{ | ||||||
|  | 		Object:     "Mutation", | ||||||
|  | 		Field:      field, | ||||||
|  | 		IsMethod:   true, | ||||||
|  | 		IsResolver: true, | ||||||
|  | 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { | ||||||
|  | 			return nil, errors.New("field of type Int does not have child fields") | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	defer func() { | ||||||
|  | 		if r := recover(); r != nil { | ||||||
|  | 			err = ec.Recover(ctx, r) | ||||||
|  | 			ec.Error(ctx, err) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 	ctx = graphql.WithFieldContext(ctx, fc) | ||||||
|  | 	if fc.Args, err = ec.field_Mutation_removeTagFromList_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { | ||||||
|  | 		ec.Error(ctx, err) | ||||||
|  | 		return fc, err | ||||||
|  | 	} | ||||||
|  | 	return fc, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (ec *executionContext) _Mutation_updateConfiguration(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { | func (ec *executionContext) _Mutation_updateConfiguration(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { | ||||||
| 	fc, err := ec.fieldContext_Mutation_updateConfiguration(ctx, field) | 	fc, err := ec.fieldContext_Mutation_updateConfiguration(ctx, field) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -17765,6 +17863,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) | |||||||
| 			if out.Values[i] == graphql.Null { | 			if out.Values[i] == graphql.Null { | ||||||
| 				out.Invalids++ | 				out.Invalids++ | ||||||
| 			} | 			} | ||||||
|  | 		case "removeTagFromList": | ||||||
|  | 			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { | ||||||
|  | 				return ec._Mutation_removeTagFromList(ctx, field) | ||||||
|  | 			}) | ||||||
|  | 			if out.Values[i] == graphql.Null { | ||||||
|  | 				out.Invalids++ | ||||||
|  | 			} | ||||||
| 		case "updateConfiguration": | 		case "updateConfiguration": | ||||||
| 			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { | 			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { | ||||||
| 				return ec._Mutation_updateConfiguration(ctx, field) | 				return ec._Mutation_updateConfiguration(ctx, field) | ||||||
|   | |||||||
| @@ -125,23 +125,41 @@ func (r *metricValueResolver) Name(ctx context.Context, obj *schema.MetricValue) | |||||||
|  |  | ||||||
| // CreateTag is the resolver for the createTag field. | // CreateTag is the resolver for the createTag field. | ||||||
| func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string, scope string) (*schema.Tag, error) { | func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string, scope string) (*schema.Tag, error) { | ||||||
|  | 	user := repository.GetUserFromContext(ctx) | ||||||
|  | 	if user == nil { | ||||||
|  | 		return nil, fmt.Errorf("no user in context") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag | ||||||
|  | 	if user.HasRole(schema.RoleAdmin) && scope == "admin" || | ||||||
|  | 		user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && scope == "global" || | ||||||
|  | 		user.Username == scope { | ||||||
|  | 		// Create in DB | ||||||
| 		id, err := r.Repo.CreateTag(typeArg, name, scope) | 		id, err := r.Repo.CreateTag(typeArg, name, scope) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Warn("Error while creating tag") | 			log.Warn("Error while creating tag") | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return &schema.Tag{ID: id, Type: typeArg, Name: name, Scope: scope}, nil | 		return &schema.Tag{ID: id, Type: typeArg, Name: name, Scope: scope}, nil | ||||||
|  | 	} else { | ||||||
|  | 		log.Warnf("Not authorized to create tag with scope: %s", scope) | ||||||
|  | 		return nil, fmt.Errorf("Not authorized to create tag with scope: %s", scope) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // DeleteTag is the resolver for the deleteTag field. | // DeleteTag is the resolver for the deleteTag field. | ||||||
| func (r *mutationResolver) DeleteTag(ctx context.Context, id string) (string, error) { | func (r *mutationResolver) DeleteTag(ctx context.Context, id string) (string, error) { | ||||||
|  | 	// This Uses ID string <-> ID string, removeTagFromList uses []string <-> []int | ||||||
| 	panic(fmt.Errorf("not implemented: DeleteTag - deleteTag")) | 	panic(fmt.Errorf("not implemented: DeleteTag - deleteTag")) | ||||||
| } | } | ||||||
|  |  | ||||||
| // AddTagsToJob is the resolver for the addTagsToJob field. | // AddTagsToJob is the resolver for the addTagsToJob field. | ||||||
| func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) { | 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 | 	user := repository.GetUserFromContext(ctx) | ||||||
|  | 	if user == nil { | ||||||
|  | 		return nil, fmt.Errorf("no user in context") | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	jid, err := strconv.ParseInt(job, 10, 64) | 	jid, err := strconv.ParseInt(job, 10, 64) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Warn("Error while adding tag to job") | 		log.Warn("Error while adding tag to job") | ||||||
| @@ -150,16 +168,33 @@ func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds | |||||||
|  |  | ||||||
| 	tags := []*schema.Tag{} | 	tags := []*schema.Tag{} | ||||||
| 	for _, tagId := range tagIds { | 	for _, tagId := range tagIds { | ||||||
|  | 		// Get ID | ||||||
| 		tid, err := strconv.ParseInt(tagId, 10, 64) | 		tid, err := strconv.ParseInt(tagId, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Warn("Error while parsing tag id") | 			log.Warn("Error while parsing tag id") | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if tags, err = r.Repo.AddTag(repository.GetUserFromContext(ctx), jid, tid); err != nil { | 		// Test Exists | ||||||
|  | 		_, _, tscope, exists := r.Repo.TagInfo(tid) | ||||||
|  | 		if !exists { | ||||||
|  | 			log.Warnf("Tag does not exist (ID): %d", tid) | ||||||
|  | 			return nil, fmt.Errorf("Tag does not exist (ID): %d", tid) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag | ||||||
|  | 		if user.HasRole(schema.RoleAdmin) && tscope == "admin" || | ||||||
|  | 			user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && tscope == "global" || | ||||||
|  | 			user.Username == tscope { | ||||||
|  | 			// Add to Job | ||||||
|  | 			if tags, err = r.Repo.AddTag(user, jid, tid); err != nil { | ||||||
| 				log.Warn("Error while adding tag") | 				log.Warn("Error while adding tag") | ||||||
| 				return nil, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
|  | 		} else { | ||||||
|  | 			log.Warnf("Not authorized to add tag: %d", tid) | ||||||
|  | 			return nil, fmt.Errorf("Not authorized to add tag: %d", tid) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return tags, nil | 	return tags, nil | ||||||
| @@ -167,7 +202,11 @@ func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds | |||||||
|  |  | ||||||
| // RemoveTagsFromJob is the resolver for the removeTagsFromJob field. | // RemoveTagsFromJob is the resolver for the removeTagsFromJob field. | ||||||
| func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) { | 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 | 	user := repository.GetUserFromContext(ctx) | ||||||
|  | 	if user == nil { | ||||||
|  | 		return nil, fmt.Errorf("no user in context") | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	jid, err := strconv.ParseInt(job, 10, 64) | 	jid, err := strconv.ParseInt(job, 10, 64) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Warn("Error while parsing job id") | 		log.Warn("Error while parsing job id") | ||||||
| @@ -176,18 +215,77 @@ func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, ta | |||||||
|  |  | ||||||
| 	tags := []*schema.Tag{} | 	tags := []*schema.Tag{} | ||||||
| 	for _, tagId := range tagIds { | 	for _, tagId := range tagIds { | ||||||
|  | 		// Get ID | ||||||
| 		tid, err := strconv.ParseInt(tagId, 10, 64) | 		tid, err := strconv.ParseInt(tagId, 10, 64) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Warn("Error while parsing tag id") | 			log.Warn("Error while parsing tag id") | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if tags, err = r.Repo.RemoveTag(repository.GetUserFromContext(ctx), jid, tid); err != nil { | 		// Test Exists | ||||||
|  | 		_, _, tscope, exists := r.Repo.TagInfo(tid) | ||||||
|  | 		if !exists { | ||||||
|  | 			log.Warnf("Tag does not exist (ID): %d", tid) | ||||||
|  | 			return nil, fmt.Errorf("Tag does not exist (ID): %d", tid) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag | ||||||
|  | 		if user.HasRole(schema.RoleAdmin) && tscope == "admin" || | ||||||
|  | 			user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && tscope == "global" || | ||||||
|  | 			user.Username == tscope { | ||||||
|  | 			// Remove from Job | ||||||
|  | 			if tags, err = r.Repo.RemoveTag(user, jid, tid); err != nil { | ||||||
| 				log.Warn("Error while removing tag") | 				log.Warn("Error while removing tag") | ||||||
| 				return nil, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
|  | 		} else { | ||||||
|  | 			log.Warnf("Not authorized to remove tag: %d", tid) | ||||||
|  | 			return nil, fmt.Errorf("Not authorized to remove tag: %d", tid) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return tags, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RemoveTagFromList is the resolver for the removeTagFromList field. | ||||||
|  | func (r *mutationResolver) RemoveTagFromList(ctx context.Context, tagIds []string) ([]int, error) { | ||||||
|  | 	// Needs Contextuser | ||||||
|  | 	user := repository.GetUserFromContext(ctx) | ||||||
|  | 	if user == nil { | ||||||
|  | 		return nil, fmt.Errorf("no user in context") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tags := []int{} | ||||||
|  | 	for _, tagId := range tagIds { | ||||||
|  | 		// Get ID | ||||||
|  | 		tid, err := strconv.ParseInt(tagId, 10, 64) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Warn("Error while parsing tag id for removal") | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Test Exists | ||||||
|  | 		_, _, tscope, exists := r.Repo.TagInfo(tid) | ||||||
|  | 		if !exists { | ||||||
|  | 			log.Warnf("Tag does not exist (ID): %d", tid) | ||||||
|  | 			return nil, fmt.Errorf("Tag does not exist (ID): %d", tid) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Test Access: Admins && Admin Tag OR Everyone && Private Tag | ||||||
|  | 		if user.HasRole(schema.RoleAdmin) && (tscope == "global" || tscope == "admin") || user.Username == tscope { | ||||||
|  | 			// Remove from DB | ||||||
|  | 			if err = r.Repo.RemoveTagById(tid); err != nil { | ||||||
|  | 				log.Warn("Error while removing tag") | ||||||
|  | 				return nil, err | ||||||
|  | 			} else { | ||||||
|  | 				tags = append(tags, int(tid)) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			log.Warnf("Not authorized to remove tag: %d", tid) | ||||||
|  | 			return nil, fmt.Errorf("Not authorized to remove tag: %d", tid) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return tags, nil | 	return tags, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -45,7 +45,7 @@ func (r *JobRepository) AddTag(user *schema.User, job int64, tag int64) ([]*sche | |||||||
| 	return tags, archive.UpdateTags(j, archiveTags) | 	return tags, archive.UpdateTags(j, archiveTags) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Removes a tag from a job | // Removes a tag from a job by tag id | ||||||
| func (r *JobRepository) RemoveTag(user *schema.User, job, tag int64) ([]*schema.Tag, error) { | func (r *JobRepository) RemoveTag(user *schema.User, job, tag int64) ([]*schema.Tag, error) { | ||||||
| 	j, err := r.FindByIdWithUser(user, job) | 	j, err := r.FindByIdWithUser(user, job) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -76,6 +76,99 @@ func (r *JobRepository) RemoveTag(user *schema.User, job, tag int64) ([]*schema. | |||||||
| 	return tags, archive.UpdateTags(j, archiveTags) | 	return tags, archive.UpdateTags(j, archiveTags) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Removes a tag from a job by tag info | ||||||
|  | func (r *JobRepository) RemoveJobTagByRequest(user *schema.User, job int64, tagType string, tagName string, tagScope string) ([]*schema.Tag, error) { | ||||||
|  | 	// Get Tag ID to delete | ||||||
|  | 	tagID, exists := r.TagId(tagType, tagName, tagScope) | ||||||
|  | 	if !exists { | ||||||
|  | 		log.Warnf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope) | ||||||
|  | 		return nil, fmt.Errorf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get Job | ||||||
|  | 	j, err := r.FindByIdWithUser(user, job) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Warn("Error while finding job by id") | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Handle Delete | ||||||
|  | 	q := sq.Delete("jobtag").Where("jobtag.job_id = ?", job).Where("jobtag.tag_id = ?", tagID) | ||||||
|  |  | ||||||
|  | 	if _, err := q.RunWith(r.stmtCache).Exec(); err != nil { | ||||||
|  | 		s, _, _ := q.ToSql() | ||||||
|  | 		log.Errorf("Error removing tag from table 'jobTag' with %s: %v", s, err) | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tags, err := r.GetTags(user, &job) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Warn("Error while getting tags for job") | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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 db by tag info | ||||||
|  | func (r *JobRepository) RemoveTagByRequest(tagType string, tagName string, tagScope string) error { | ||||||
|  | 	// Get Tag ID to delete | ||||||
|  | 	tagID, exists := r.TagId(tagType, tagName, tagScope) | ||||||
|  | 	if !exists { | ||||||
|  | 		log.Warnf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope) | ||||||
|  | 		return fmt.Errorf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Handle Delete JobTagTable | ||||||
|  | 	qJobTag := sq.Delete("jobtag").Where("jobtag.tag_id = ?", tagID) | ||||||
|  |  | ||||||
|  | 	if _, err := qJobTag.RunWith(r.stmtCache).Exec(); err != nil { | ||||||
|  | 		s, _, _ := qJobTag.ToSql() | ||||||
|  | 		log.Errorf("Error removing tag from table 'jobTag' with %s: %v", s, err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Handle Delete TagTable | ||||||
|  | 	qTag := sq.Delete("tag").Where("tag.id = ?", tagID) | ||||||
|  |  | ||||||
|  | 	if _, err := qTag.RunWith(r.stmtCache).Exec(); err != nil { | ||||||
|  | 		s, _, _ := qTag.ToSql() | ||||||
|  | 		log.Errorf("Error removing tag from table 'tag' with %s: %v", s, err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Removes a tag from db by tag id | ||||||
|  | func (r *JobRepository) RemoveTagById(tagID int64) error { | ||||||
|  | 	// Handle Delete JobTagTable | ||||||
|  | 	qJobTag := sq.Delete("jobtag").Where("jobtag.tag_id = ?", tagID) | ||||||
|  |  | ||||||
|  | 	if _, err := qJobTag.RunWith(r.stmtCache).Exec(); err != nil { | ||||||
|  | 		s, _, _ := qJobTag.ToSql() | ||||||
|  | 		log.Errorf("Error removing tag from table 'jobTag' with %s: %v", s, err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Handle Delete TagTable | ||||||
|  | 	qTag := sq.Delete("tag").Where("tag.id = ?", tagID) | ||||||
|  |  | ||||||
|  | 	if _, err := qTag.RunWith(r.stmtCache).Exec(); err != nil { | ||||||
|  | 		s, _, _ := qTag.ToSql() | ||||||
|  | 		log.Errorf("Error removing tag from table 'tag' with %s: %v", s, err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // CreateTag creates a new tag with the specified type and name and returns its database id. | // CreateTag creates a new tag with the specified type and name and returns its database id. | ||||||
| func (r *JobRepository) CreateTag(tagType string, tagName string, tagScope string) (tagId int64, err error) { | func (r *JobRepository) CreateTag(tagType string, tagName string, tagScope string) (tagId int64, err error) { | ||||||
| 	// Default to "Global" scope if none defined | 	// Default to "Global" scope if none defined | ||||||
| @@ -209,6 +302,16 @@ func (r *JobRepository) TagId(tagType string, tagName string, tagScope string) ( | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // TagInfo returns the database infos of the tag with the specified id. | ||||||
|  | func (r *JobRepository) TagInfo(tagId int64) (tagType string, tagName string, tagScope string, exists bool) { | ||||||
|  | 	exists = true | ||||||
|  | 	if err := sq.Select("tag.tag_type", "tag.tag_name", "tag.tag_scope").From("tag").Where("tag.id = ?", tagId). | ||||||
|  | 		RunWith(r.stmtCache).QueryRow().Scan(&tagType, &tagName, &tagScope); err != nil { | ||||||
|  | 		exists = false | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
| // GetTags returns a list of all scoped tags if job is nil or of the tags that the job with that database ID has. | // 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(user *schema.User, job *int64) ([]*schema.Tag, error) { | func (r *JobRepository) GetTags(user *schema.User, job *int64) ([]*schema.Tag, error) { | ||||||
| 	q := sq.Select("id", "tag_type", "tag_name", "tag_scope").From("tag") | 	q := sq.Select("id", "tag_type", "tag_name", "tag_scope").From("tag") | ||||||
|   | |||||||
| @@ -85,6 +85,7 @@ func IsValidRole(role string) bool { | |||||||
| 	return getRoleEnum(role) != RoleError | 	return getRoleEnum(role) != RoleError | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Check if User has SPECIFIED role AND role is VALID | ||||||
| func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) { | func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) { | ||||||
| 	if IsValidRole(role) { | 	if IsValidRole(role) { | ||||||
| 		for _, r := range u.Roles { | 		for _, r := range u.Roles { | ||||||
| @@ -97,6 +98,7 @@ func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) { | |||||||
| 	return false, false | 	return false, false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Check if User has SPECIFIED role | ||||||
| func (u *User) HasRole(role Role) bool { | func (u *User) HasRole(role Role) bool { | ||||||
| 	for _, r := range u.Roles { | 	for _, r := range u.Roles { | ||||||
| 		if r == GetRoleString(role) { | 		if r == GetRoleString(role) { | ||||||
| @@ -106,7 +108,7 @@ func (u *User) HasRole(role Role) bool { | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| // Role-Arrays are short: performance not impacted by nested loop | // Check if User has ANY of the listed roles | ||||||
| func (u *User) HasAnyRole(queryroles []Role) bool { | func (u *User) HasAnyRole(queryroles []Role) bool { | ||||||
| 	for _, ur := range u.Roles { | 	for _, ur := range u.Roles { | ||||||
| 		for _, qr := range queryroles { | 		for _, qr := range queryroles { | ||||||
| @@ -118,7 +120,7 @@ func (u *User) HasAnyRole(queryroles []Role) bool { | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| // Role-Arrays are short: performance not impacted by nested loop | // Check if User has ALL of the listed roles | ||||||
| func (u *User) HasAllRoles(queryroles []Role) bool { | func (u *User) HasAllRoles(queryroles []Role) bool { | ||||||
| 	target := len(queryroles) | 	target := len(queryroles) | ||||||
| 	matches := 0 | 	matches := 0 | ||||||
| @@ -138,7 +140,7 @@ func (u *User) HasAllRoles(queryroles []Role) bool { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // Role-Arrays are short: performance not impacted by nested loop | // Check if User has NONE of the listed roles | ||||||
| func (u *User) HasNotRoles(queryroles []Role) bool { | func (u *User) HasNotRoles(queryroles []Role) bool { | ||||||
| 	matches := 0 | 	matches := 0 | ||||||
| 	for _, ur := range u.Roles { | 	for _, ur := range u.Roles { | ||||||
|   | |||||||
| @@ -62,6 +62,7 @@ export default [ | |||||||
|     entrypoint('jobs', 'src/jobs.entrypoint.js'), |     entrypoint('jobs', 'src/jobs.entrypoint.js'), | ||||||
|     entrypoint('user', 'src/user.entrypoint.js'), |     entrypoint('user', 'src/user.entrypoint.js'), | ||||||
|     entrypoint('list', 'src/list.entrypoint.js'), |     entrypoint('list', 'src/list.entrypoint.js'), | ||||||
|  |     entrypoint('taglist', 'src/tags.entrypoint.js'), | ||||||
|     entrypoint('job', 'src/job.entrypoint.js'), |     entrypoint('job', 'src/job.entrypoint.js'), | ||||||
|     entrypoint('systems', 'src/systems.entrypoint.js'), |     entrypoint('systems', 'src/systems.entrypoint.js'), | ||||||
|     entrypoint('node', 'src/node.entrypoint.js'), |     entrypoint('node', 'src/node.entrypoint.js'), | ||||||
|   | |||||||
							
								
								
									
										110
									
								
								web/frontend/src/Tags.root.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								web/frontend/src/Tags.root.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | <!-- | ||||||
|  |     @component Tag List Svelte Component. Displays All Tags, Allows deletion. | ||||||
|  |  | ||||||
|  |     Properties: | ||||||
|  |     - `username String!`: Users username. | ||||||
|  |     - `isAdmin Bool!`: User has Admin Auth. | ||||||
|  |     - `tagmap Object!`: Map of accessible, appwide tags. Prefiltered in backend. | ||||||
|  |  --> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  |   import { | ||||||
|  |     gql, | ||||||
|  |     getContextClient, | ||||||
|  |     mutationStore, | ||||||
|  |   } from "@urql/svelte"; | ||||||
|  |   import { | ||||||
|  |     Badge, | ||||||
|  |     InputGroup, | ||||||
|  |     Icon, | ||||||
|  |     Button, | ||||||
|  |     Spinner, | ||||||
|  |   } from "@sveltestrap/sveltestrap"; | ||||||
|  |   import { | ||||||
|  |     init, | ||||||
|  |   } from "./generic/utils.js"; | ||||||
|  |  | ||||||
|  |   export let username; | ||||||
|  |   export let isAdmin; | ||||||
|  |   export let tagmap; | ||||||
|  |  | ||||||
|  |   const {} = init(); | ||||||
|  |   const client = getContextClient(); | ||||||
|  |  | ||||||
|  |   let pendingChange = "none"; | ||||||
|  |  | ||||||
|  |   const removeTagMutation = ({ tagIds }) => { | ||||||
|  |     return mutationStore({ | ||||||
|  |       client: client, | ||||||
|  |       query: gql` | ||||||
|  |         mutation ($tagIds: [ID!]!) { | ||||||
|  |           removeTagFromList(tagIds: $tagIds) | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |       variables: { tagIds }, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   function removeTag(tag, tagType) { | ||||||
|  |     if (confirm("Are you sure you want to completely remove this tag?\n\n" + tagType + ':' + tag.name)) { | ||||||
|  |       pendingChange = tagType; | ||||||
|  |       removeTagMutation({tagIds: [tag.id] }).subscribe( | ||||||
|  |         (res) => { | ||||||
|  |           if (res.fetching === false && !res.error) { | ||||||
|  |             tagmap[tagType] = tagmap[tagType].filter((t) => !res.data.removeTagFromList.includes(t.id)); | ||||||
|  |             if (tagmap[tagType].length === 0) { | ||||||
|  |               delete tagmap[tagType] | ||||||
|  |             } | ||||||
|  |             pendingChange = "none"; | ||||||
|  |           } else if (res.fetching === false && res.error) { | ||||||
|  |             throw res.error; | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <div class="container"> | ||||||
|  |   <div class="row justify-content-center"> | ||||||
|  |     <div class="col-10"> | ||||||
|  |       {#each Object.entries(tagmap) as [tagType, tagList]} | ||||||
|  |         <div class="my-3 p-2 bg-secondary rounded text-white"> <!-- text-capitalize --> | ||||||
|  |           Tag Type: <b>{tagType}</b> | ||||||
|  |           {#if pendingChange === tagType} | ||||||
|  |             <Spinner size="sm" secondary /> | ||||||
|  |           {/if} | ||||||
|  |           <span style="float: right; padding-bottom: 0.4rem; padding-top: 0.4rem;" class="badge bg-light text-secondary"> | ||||||
|  |             {tagList.length} Tag{(tagList.length != 1)?'s':''} | ||||||
|  |           </span> | ||||||
|  |         </div> | ||||||
|  |         <div class="d-inline-flex flex-wrap"> | ||||||
|  |           {#each tagList as tag (tag.id)} | ||||||
|  |             <InputGroup class="w-auto flex-nowrap" style="margin-right: 0.5rem; margin-bottom: 0.5rem;"> | ||||||
|  |               <Button outline color="secondary" href="/monitoring/jobs/?tag={tag.id}" target="_blank"> | ||||||
|  |                 <Badge color="light" style="font-size:medium;" border>{tag.name}</Badge> :  | ||||||
|  |                 <Badge color="primary" pill>{tag.count} Job{(tag.count != 1)?'s':''}</Badge> | ||||||
|  |                 {#if tag.scope == "global"} | ||||||
|  |                   <Badge style="background-color:#c85fc8 !important;" pill>Global</Badge> | ||||||
|  |                 {:else if tag.scope == "admin"} | ||||||
|  |                   <Badge style="background-color:#19e5e6 !important;" pill>Admin</Badge> | ||||||
|  |                 {:else} | ||||||
|  |                   <Badge color="warning" pill>Private</Badge> | ||||||
|  |                 {/if} | ||||||
|  |               </Button> | ||||||
|  |               {#if (isAdmin && (tag.scope == "admin" || tag.scope == "global")) || tag.scope == username } | ||||||
|  |                 <Button | ||||||
|  |                   size="sm" | ||||||
|  |                   color="danger" | ||||||
|  |                   on:click={() => removeTag(tag, tagType)} | ||||||
|  |                 > | ||||||
|  |                   <Icon name="x" /> | ||||||
|  |                 </Button> | ||||||
|  |               {/if} | ||||||
|  |             </InputGroup> | ||||||
|  |           {/each} | ||||||
|  |         </div> | ||||||
|  |       {/each} | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
							
								
								
									
										16
									
								
								web/frontend/src/tags.entrypoint.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								web/frontend/src/tags.entrypoint.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | import {} from './header.entrypoint.js' | ||||||
|  | import Tags from './Tags.root.svelte' | ||||||
|  |  | ||||||
|  | new Tags({ | ||||||
|  |     target: document.getElementById('svelte-app'), | ||||||
|  |     props: { | ||||||
|  |         username: username, | ||||||
|  |         isAdmin: isAdmin, | ||||||
|  |         tagmap: tagmap, | ||||||
|  |     }, | ||||||
|  |     context: new Map([ | ||||||
|  |         ['cc-config', clusterCockpitConfig] | ||||||
|  |     ]) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1,37 +1,15 @@ | |||||||
| {{define "content"}} | {{define "content"}} | ||||||
|     <div class="container"> |     <div id="svelte-app"></div> | ||||||
|         <div class="row  justify-content-center"> | {{end}} | ||||||
|             <div class="col-10"> | {{define "stylesheets"}} | ||||||
|             {{ range $tagType, $tagList := .Infos.tagmap }} |     <link rel='stylesheet' href='/build/taglist.css'> | ||||||
|                 <div class="my-3 p-2 bg-secondary rounded text-white"> <!-- text-capitalize --> | {{end}} | ||||||
|                     Tag Type: <b>{{ $tagType }}</b> | {{define "javascript"}} | ||||||
|                     <span style="float: right; padding-bottom: 0.4rem; padding-top: 0.4rem;" class="badge bg-light text-secondary"> |     <script> | ||||||
|                         {{len $tagList}} Tag{{if ne (len $tagList) 1}}s{{end}} |         const username = {{ .User.Username }}; | ||||||
|                     </span> |         const isAdmin = {{ .User.HasRole .Roles.admin }}; | ||||||
|                 </div> |         const tagmap = {{ .Infos.tagmap }}; | ||||||
|                 {{ range $tagList }} |         const clusterCockpitConfig = {{ .Config }}; | ||||||
|                     {{if eq .scope "global"}} |     </script> | ||||||
|                         <a class="btn btn-outline-secondary" href="/monitoring/jobs/?tag={{ .id }}" role="button"> |     <script src='/build/taglist.js'></script> | ||||||
|                             {{ .name }} |  | ||||||
|                             <span class="badge bg-primary mr-1">{{ .count }} Job{{if ne .count 1}}s{{end}}</span> |  | ||||||
|                             <span style="background-color:#c85fc8;" class="badge text-dark">Global</span> |  | ||||||
|                         </a> |  | ||||||
|                     {{else if eq .scope "admin"}} |  | ||||||
|                         <a class="btn btn-outline-secondary" href="/monitoring/jobs/?tag={{ .id }}" role="button"> |  | ||||||
|                             {{ .name }} |  | ||||||
|                             <span class="badge bg-primary mr-1">{{ .count }} Job{{if ne .count 1}}s{{end}}</span> |  | ||||||
|                             <span style="background-color:#19e5e6;" class="badge text-dark">Admin</span> |  | ||||||
|                         </a> |  | ||||||
|                     {{else}} |  | ||||||
|                         <a class="btn btn-outline-secondary" href="/monitoring/jobs/?tag={{ .id }}" role="button"> |  | ||||||
|                             {{ .name }} |  | ||||||
|                             <span class="badge bg-primary mr-1">{{ .count }} Job{{if ne .count 1}}s{{end}}</span> |  | ||||||
|                             <span class="badge bg-warning text-dark">Private</span> |  | ||||||
|                         </a> |  | ||||||
|                     {{end}} |  | ||||||
|                 {{end}} |  | ||||||
|             {{end}} |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user