mirror of
https://github.com/semaphoreui/semaphore.git
synced 2024-11-21 08:51:05 +01:00
fix(apps): update single prop in config
This commit is contained in:
parent
10d7f5045e
commit
413bb8bc0c
96
api/apps.go
96
api/apps.go
@ -2,13 +2,62 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"github.com/gorilla/context"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func structToFlatMap(obj interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
val := reflect.ValueOf(obj)
|
||||
typ := reflect.TypeOf(obj)
|
||||
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return result
|
||||
}
|
||||
|
||||
// Iterate over the struct fields
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
jsonTag := fieldType.Tag.Get("json")
|
||||
|
||||
// Use the json tag if it is set, otherwise use the field name
|
||||
fieldName := jsonTag
|
||||
if fieldName == "" || fieldName == "-" {
|
||||
fieldName = fieldType.Name
|
||||
} else {
|
||||
// Handle the case where the json tag might have options like `json:"name,omitempty"`
|
||||
fieldName = strings.Split(fieldName, ",")[0]
|
||||
}
|
||||
|
||||
// Check if the field is a struct itself
|
||||
if field.Kind() == reflect.Struct {
|
||||
// Convert nested struct to map
|
||||
nestedMap := structToFlatMap(field.Interface())
|
||||
// Add nested map to result with a prefixed key
|
||||
for k, v := range nestedMap {
|
||||
result[fieldName+"."+k] = v
|
||||
}
|
||||
} else {
|
||||
result[fieldName] = field.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func validateAppID(str string) error {
|
||||
return nil
|
||||
}
|
||||
@ -85,8 +134,55 @@ func deleteApp(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func setApp(w http.ResponseWriter, r *http.Request) {
|
||||
appID := context.Get(r, "app_id").(string)
|
||||
|
||||
store := helpers.Store(r)
|
||||
|
||||
var app util.App
|
||||
|
||||
if !helpers.Bind(w, r, &app) {
|
||||
return
|
||||
}
|
||||
|
||||
options := structToFlatMap(app)
|
||||
|
||||
for k, v := range options {
|
||||
if err := store.SetOption("apps."+appID+"."+k, fmt.Sprintf("%v", v)); err != nil {
|
||||
helpers.WriteErrorStatus(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func setAppActive(w http.ResponseWriter, r *http.Request) {
|
||||
appID := context.Get(r, "app_id").(string)
|
||||
|
||||
store := helpers.Store(r)
|
||||
|
||||
var body struct {
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
if !helpers.Bind(w, r, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
key := "apps." + appID + ".active"
|
||||
val := strconv.FormatBool(body.Active)
|
||||
|
||||
if err := store.SetOption(key, val); err != nil {
|
||||
helpers.WriteErrorStatus(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
opts := make(map[string]string)
|
||||
opts[key] = val
|
||||
|
||||
options := db.ConvertFlatToNested(opts)
|
||||
|
||||
_ = db.AssignMapToStruct(options, util.Config)
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
42
api/apps_test.go
Normal file
42
api/apps_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStructToMap(t *testing.T) {
|
||||
type Address struct {
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Email string `json:"email"`
|
||||
Active bool `json:"active"`
|
||||
Address Address `json:"address"`
|
||||
}
|
||||
|
||||
// Create an instance of the struct
|
||||
p := Person{
|
||||
Name: "John Doe",
|
||||
Age: 30,
|
||||
Email: "johndoe@example.com",
|
||||
Active: true,
|
||||
Address: Address{
|
||||
City: "New York",
|
||||
State: "NY",
|
||||
},
|
||||
}
|
||||
|
||||
// Convert the struct to a flat map
|
||||
flatMap := structToFlatMap(&p)
|
||||
|
||||
if flatMap["address.city"] != "New York" {
|
||||
t.Fail()
|
||||
}
|
||||
// Print the map
|
||||
fmt.Println(flatMap)
|
||||
}
|
@ -102,7 +102,6 @@ func Route() *mux.Router {
|
||||
authenticatedWS.Path("/ws").HandlerFunc(sockets.Handler).Methods("GET", "HEAD")
|
||||
|
||||
authenticatedAPI := r.PathPrefix(webPath + "api").Subrouter()
|
||||
|
||||
authenticatedAPI.Use(StoreMiddleware, JSONMiddleware, authentication)
|
||||
|
||||
authenticatedAPI.Path("/info").HandlerFunc(getSystemInfo).Methods("GET", "HEAD")
|
||||
@ -129,10 +128,11 @@ func Route() *mux.Router {
|
||||
adminAPI.Path("/options").HandlerFunc(getOptions).Methods("GET", "HEAD")
|
||||
adminAPI.Path("/options").HandlerFunc(setOption).Methods("POST", "HEAD")
|
||||
|
||||
appsAPI := adminAPI.Path("/apps").Subrouter()
|
||||
appsAPI := adminAPI.PathPrefix("/apps").Subrouter()
|
||||
appsAPI.Use(appMiddleware)
|
||||
appsAPI.Path("/{app_id}").HandlerFunc(getApp).Methods("GET", "HEAD")
|
||||
appsAPI.Path("/{app_id}").HandlerFunc(setApp).Methods("POST", "HEAD")
|
||||
appsAPI.Path("/{app_id}/active").HandlerFunc(setAppActive).Methods("POST", "HEAD")
|
||||
appsAPI.Path("/{app_id}").HandlerFunc(deleteApp).Methods("POST", "HEAD")
|
||||
|
||||
userAPI := authenticatedAPI.Path("/users/{user_id}").Subrouter()
|
||||
|
37
db/config.go
37
db/config.go
@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func convertFlatToNested(flatMap map[string]string) map[string]interface{} {
|
||||
func ConvertFlatToNested(flatMap map[string]string) map[string]interface{} {
|
||||
nestedMap := make(map[string]interface{})
|
||||
|
||||
for key, value := range flatMap {
|
||||
@ -29,11 +29,27 @@ func convertFlatToNested(flatMap map[string]string) map[string]interface{} {
|
||||
return nestedMap
|
||||
}
|
||||
|
||||
func assignMapToStruct[P *S, S any](m map[string]interface{}, s P) error {
|
||||
func AssignMapToStruct[P *S, S any](m map[string]interface{}, s P) error {
|
||||
v := reflect.ValueOf(s).Elem()
|
||||
return assignMapToStructRecursive(m, v)
|
||||
}
|
||||
|
||||
func cloneStruct(origValue reflect.Value) reflect.Value {
|
||||
// Create a new instance of the same type as the original struct
|
||||
cloneValue := reflect.New(origValue.Type()).Elem()
|
||||
|
||||
// Iterate over the fields of the struct
|
||||
for i := 0; i < origValue.NumField(); i++ {
|
||||
// Get the field value
|
||||
fieldValue := origValue.Field(i)
|
||||
// Set the field value in the clone
|
||||
cloneValue.Field(i).Set(fieldValue)
|
||||
}
|
||||
|
||||
// Return the cloned struct
|
||||
return cloneValue
|
||||
}
|
||||
|
||||
func assignMapToStructRecursive(m map[string]interface{}, structValue reflect.Value) error {
|
||||
structType := structValue.Type()
|
||||
|
||||
@ -71,11 +87,18 @@ func assignMapToStructRecursive(m map[string]interface{}, structValue reflect.Va
|
||||
if val.Kind() != reflect.Map {
|
||||
return fmt.Errorf("expected map for field %s but got %T", field.Name, value)
|
||||
}
|
||||
//mapValue := reflect.MakeMap(fieldValue.Type())
|
||||
|
||||
for _, key := range val.MapKeys() {
|
||||
mapElemValue := val.MapIndex(key)
|
||||
mapElemType := fieldValue.Type().Elem()
|
||||
mapElem := reflect.New(mapElemType).Elem()
|
||||
|
||||
srcVal := fieldValue.MapIndex(key)
|
||||
var mapElem reflect.Value
|
||||
if srcVal.IsValid() {
|
||||
mapElem = cloneStruct(srcVal)
|
||||
} else {
|
||||
mapElem = reflect.New(mapElemType).Elem()
|
||||
}
|
||||
|
||||
if mapElemType.Kind() == reflect.Struct {
|
||||
if err := assignMapToStructRecursive(mapElemValue.Interface().(map[string]interface{}), mapElem); err != nil {
|
||||
@ -98,7 +121,7 @@ func assignMapToStructRecursive(m map[string]interface{}, structValue reflect.Va
|
||||
|
||||
fieldValue.SetMapIndex(key, mapElem)
|
||||
}
|
||||
//fieldValue.Set(mapValue)
|
||||
|
||||
default:
|
||||
// Handle simple types
|
||||
if val.Type().ConvertibleTo(fieldValue.Type()) {
|
||||
@ -128,13 +151,13 @@ func FillConfigFromDB(store Store) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
options := convertFlatToNested(opts)
|
||||
options := ConvertFlatToNested(opts)
|
||||
|
||||
if options["apps"] == nil {
|
||||
options["apps"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
err = assignMapToStruct(options, util.Config)
|
||||
err = AssignMapToStruct(options, util.Config)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -8,43 +8,68 @@ func TestConfig_assignMapToStruct(t *testing.T) {
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
type Detail struct {
|
||||
Value string `json:"value"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Email string `json:"email"`
|
||||
Address Address `json:"address"`
|
||||
Details map[string]string `json:"details"`
|
||||
//Name string `json:"name"`
|
||||
//Age int `json:"age"`
|
||||
//Email string `json:"email"`
|
||||
//Address Address `json:"address"`
|
||||
Details map[string]Detail `json:"details"`
|
||||
}
|
||||
|
||||
johnData := map[string]interface{}{
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"email": "john.doe@example.com",
|
||||
"address": map[string]interface{}{
|
||||
"street": "123 Main St",
|
||||
"city": "Anytown",
|
||||
},
|
||||
"details": map[string]string{
|
||||
"occupation": "engineer",
|
||||
"hobby": "hiking",
|
||||
//"name": "John Doe",
|
||||
//"age": 30,
|
||||
//"email": "john.doe@example.com",
|
||||
//"address": map[string]interface{}{
|
||||
// "street": "123 Main St",
|
||||
// "city": "Anytown",
|
||||
//},
|
||||
"details": map[string]interface{}{
|
||||
//"occupation": map[string]interface{}{
|
||||
// "value": "engineer",
|
||||
// "description": "Works with computers",
|
||||
//},
|
||||
//"hobby": map[string]interface{}{
|
||||
// "value": "hiking",
|
||||
// "description": "Enjoys the outdoors",
|
||||
//},
|
||||
"interests": map[string]interface{}{
|
||||
"description": "Ho ho ho",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var john User
|
||||
john.Details = make(map[string]string)
|
||||
john.Details["interests"] = "politics"
|
||||
john.Details = make(map[string]Detail)
|
||||
john.Details["interests"] = Detail{
|
||||
Value: "politics",
|
||||
Description: "Follows current events",
|
||||
}
|
||||
|
||||
err := assignMapToStruct(johnData, &john)
|
||||
err := AssignMapToStruct(johnData, &john)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if john.Name != "John Doe" {
|
||||
t.Errorf("Expected name to be John Doe but got %s", john.Name)
|
||||
//if john.Name != "John Doe" {
|
||||
// t.Errorf("Expected name to be John Doe but got %s", john.Name)
|
||||
//}
|
||||
|
||||
if john.Details["interests"].Description != "Ho ho ho" {
|
||||
t.Errorf("Expected interests description to be 'Ho ho ho' but got %s", john.Details["interests"].Description)
|
||||
}
|
||||
|
||||
if john.Details["interests"] != "politics" {
|
||||
t.Errorf("Expected interests to be politics but got %s", john.Details["interests"])
|
||||
if john.Details["interests"].Value != "politics" {
|
||||
t.Errorf("Expected interests to be politics but got '%s'", john.Details["interests"].Value)
|
||||
}
|
||||
|
||||
//if john.Details["occupation"].Value != "engineer" {
|
||||
// t.Errorf("Expected occupation to be engineer but got %s", john.Details["occupation"].Value)
|
||||
//}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package util
|
||||
|
||||
type AppConfig struct {
|
||||
type App struct {
|
||||
Active bool `json:"active"`
|
||||
Order int `json:"order"`
|
||||
Title string `json:"title"`
|
||||
|
@ -193,7 +193,7 @@ type ConfigType struct {
|
||||
|
||||
IntegrationAlias string `json:"global_integration_alias" env:"SEMAPHORE_INTEGRATION_ALIAS"`
|
||||
|
||||
Apps map[string]AppConfig `json:"apps" env:"SEMAPHORE_APPS"`
|
||||
Apps map[string]App `json:"apps" env:"SEMAPHORE_APPS"`
|
||||
}
|
||||
|
||||
// Config exposes the application configuration storage for use in the application
|
||||
@ -806,7 +806,7 @@ func CheckDefaultApps() {
|
||||
continue
|
||||
}
|
||||
|
||||
Config.Apps[app] = AppConfig{
|
||||
Config.Apps[app] = App{
|
||||
Active: true,
|
||||
}
|
||||
}
|
||||
|
@ -48,19 +48,12 @@
|
||||
class="mt-4"
|
||||
:footer-props="{ itemsPerPageOptions: [20] }"
|
||||
>
|
||||
<template v-slot:item.external="{ item }">
|
||||
<v-icon v-if="item.external">mdi-checkbox-marked</v-icon>
|
||||
<v-icon v-else>mdi-checkbox-blank-outline</v-icon>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.alert="{ item }">
|
||||
<v-icon v-if="item.alert">mdi-checkbox-marked</v-icon>
|
||||
<v-icon v-else>mdi-checkbox-blank-outline</v-icon>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.admin="{ item }">
|
||||
<v-icon v-if="item.admin">mdi-checkbox-marked</v-icon>
|
||||
<v-icon v-else>mdi-checkbox-blank-outline</v-icon>
|
||||
<template v-slot:item.active="{ item }">
|
||||
<v-switch
|
||||
v-model="item.active"
|
||||
inset
|
||||
@change="setActive(item.id, item.active)"
|
||||
></v-switch>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
@ -91,6 +84,7 @@ import EventBus from '@/event-bus';
|
||||
import YesNoDialog from '@/components/YesNoDialog.vue';
|
||||
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||
import EditDialog from '@/components/EditDialog.vue';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
mixins: [ItemListPageBase],
|
||||
@ -104,11 +98,14 @@ export default {
|
||||
getHeaders() {
|
||||
return [{
|
||||
text: '',
|
||||
value: 'active',
|
||||
}, {
|
||||
text: 'ID',
|
||||
value: 'id',
|
||||
}, {
|
||||
text: this.$i18n.t('title'),
|
||||
value: 'Title',
|
||||
text: this.$i18n.t('name'),
|
||||
width: '100%',
|
||||
value: 'title',
|
||||
}, {
|
||||
text: this.$i18n.t('actions'),
|
||||
value: 'actions',
|
||||
@ -131,6 +128,17 @@ export default {
|
||||
getEventName() {
|
||||
return 'i-app';
|
||||
},
|
||||
|
||||
async setActive(appId, active) {
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/api/apps/${appId}/active`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
active,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user