diff --git a/api/apps.go b/api/apps.go
index 4b383cae..8070cc94 100644
--- a/api/apps.go
+++ b/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)
}
diff --git a/api/apps_test.go b/api/apps_test.go
new file mode 100644
index 00000000..dbec0c89
--- /dev/null
+++ b/api/apps_test.go
@@ -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)
+}
diff --git a/api/router.go b/api/router.go
index 03eeb93e..183118c3 100644
--- a/api/router.go
+++ b/api/router.go
@@ -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()
diff --git a/db/config.go b/db/config.go
index 574bf32a..06c62969 100644
--- a/db/config.go
+++ b/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
}
diff --git a/db/config_test.go b/db/config_test.go
index 2cb578b9..74bb8bba 100644
--- a/db/config_test.go
+++ b/db/config_test.go
@@ -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)
+ //}
}
diff --git a/util/App.go b/util/App.go
index 4801a613..b7e20478 100644
--- a/util/App.go
+++ b/util/App.go
@@ -1,6 +1,6 @@
package util
-type AppConfig struct {
+type App struct {
Active bool `json:"active"`
Order int `json:"order"`
Title string `json:"title"`
diff --git a/util/config.go b/util/config.go
index 85f09659..0009c29f 100644
--- a/util/config.go
+++ b/util/config.go
@@ -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,
}
}
diff --git a/web/src/views/Apps.vue b/web/src/views/Apps.vue
index 3550e28b..c33fa8d1 100644
--- a/web/src/views/Apps.vue
+++ b/web/src/views/Apps.vue
@@ -48,19 +48,12 @@
class="mt-4"
:footer-props="{ itemsPerPageOptions: [20] }"
>
-
- mdi-checkbox-marked
- mdi-checkbox-blank-outline
-
-
-
- mdi-checkbox-marked
- mdi-checkbox-blank-outline
-
-
-
- mdi-checkbox-marked
- mdi-checkbox-blank-outline
+
+
@@ -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,
+ },
+ });
+ },
},
};