Skip to content

Commit

Permalink
Plugin settings (stashapp#4143)
Browse files Browse the repository at this point in the history
* Add backend support for plugin settings
* Add plugin settings config
* Add UI support for plugin settings
  • Loading branch information
WithoutPants authored and halkeye committed Sep 1, 2024
1 parent bdce5f5 commit 1823f06
Show file tree
Hide file tree
Showing 24 changed files with 444 additions and 56 deletions.
5 changes: 5 additions & 0 deletions gqlgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,9 @@ models:
model: github.com/stashapp/stash/pkg/scraper.Source
SavedFindFilterType:
model: github.com/stashapp/stash/pkg/models.FindFilterType
# force resolvers
ConfigResult:
fields:
plugins:
resolver: true

1 change: 1 addition & 0 deletions graphql/documents/data/config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,5 @@ fragment ConfigData on ConfigResult {
...ConfigDefaultSettingsData
}
ui
plugins
}
4 changes: 4 additions & 0 deletions graphql/documents/mutations/plugins.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ mutation RunPluginTask(
runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args: $args)
}

mutation ConfigurePlugin($plugin_id: ID!, $input: Map!) {
configurePlugin(plugin_id: $plugin_id, input: $input)
}

mutation SetPluginsEnabled($enabledMap: BoolMap!) {
setPluginsEnabled(enabledMap: $enabledMap)
}
7 changes: 7 additions & 0 deletions graphql/documents/queries/plugins.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ query Plugins {
description
hooks
}

settings {
name
display_name
description
type
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ type Mutation {
input: ConfigDefaultSettingsInput!
): ConfigDefaultSettingsResult!

# overwrites the entire plugin configuration for the given plugin
configurePlugin(plugin_id: ID!, input: Map!): Map!

# overwrites the entire UI configuration
configureUI(input: Map!): Map!
# sets a single UI key value
Expand Down
1 change: 1 addition & 0 deletions graphql/schema/types/config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ type ConfigResult {
scraping: ConfigScrapingResult!
defaults: ConfigDefaultSettingsResult!
ui: Map!
plugins(include: [String!]): Map!
}

"Directory structure of a path"
Expand Down
14 changes: 14 additions & 0 deletions graphql/schema/types/plugin.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Plugin {

tasks: [PluginTask!]
hooks: [PluginHook!]
settings: [PluginSetting!]
}

type PluginTask {
Expand Down Expand Up @@ -42,3 +43,16 @@ input PluginValueInput {
o: [PluginArgInput!]
a: [PluginValueInput!]
}

enum PluginSettingTypeEnum {
STRING
NUMBER
BOOLEAN
}

type PluginSetting {
name: String!
display_name: String
description: String
type: PluginSettingTypeEnum!
}
4 changes: 4 additions & 0 deletions internal/api/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ func (r *Resolver) Tag() TagResolver {
func (r *Resolver) SavedFilter() SavedFilterResolver {
return &savedFilterResolver{r}
}
func (r *Resolver) ConfigResult() ConfigResultResolver {
return &configResultResolver{r}
}

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Expand All @@ -99,6 +102,7 @@ type studioResolver struct{ *Resolver }
type movieResolver struct{ *Resolver }
type tagResolver struct{ *Resolver }
type savedFilterResolver struct{ *Resolver }
type configResultResolver struct{ *Resolver }

func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return r.repository.WithTxn(ctx, fn)
Expand Down
25 changes: 25 additions & 0 deletions internal/api/resolver_model_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package api

import (
"context"

"github.com/stashapp/stash/internal/manager/config"
)

func (r *configResultResolver) Plugins(ctx context.Context, obj *ConfigResult, include []string) (map[string]interface{}, error) {
if len(include) == 0 {
ret := config.GetInstance().GetAllPluginConfiguration()
return ret, nil
}

ret := make(map[string]interface{})

for _, plugin := range include {
c := config.GetInstance().GetPluginConfiguration(plugin)
if len(c) > 0 {
ret[plugin] = c
}
}

return ret, nil
}
11 changes: 11 additions & 0 deletions internal/api/resolver_mutation_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,3 +629,14 @@ func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, v

return r.ConfigureUI(ctx, cfg)
}

func (r *mutationResolver) ConfigurePlugin(ctx context.Context, pluginID string, input map[string]interface{}) (map[string]interface{}, error) {
c := config.GetInstance()
c.SetPluginConfiguration(pluginID, input)

if err := c.Write(); err != nil {
return c.GetPluginConfiguration(pluginID), err
}

return c.GetPluginConfiguration(pluginID), nil
}
53 changes: 51 additions & 2 deletions internal/manager/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ const (
PythonPath = "python_path"

// plugin options
PluginsPath = "plugins_path"
DisabledPlugins = "plugins.disabled"
PluginsPath = "plugins_path"
PluginsSetting = "plugins.settings"
PluginsSettingPrefix = PluginsSetting + "."
DisabledPlugins = "plugins.disabled"

// i18n
Language = "language"
Expand Down Expand Up @@ -723,6 +725,53 @@ func (i *Instance) GetPluginsPath() string {
return i.getString(PluginsPath)
}

func (i *Instance) GetAllPluginConfiguration() map[string]interface{} {
i.RLock()
defer i.RUnlock()

ret := make(map[string]interface{})

sub := i.viper(PluginsSetting).GetStringMap(PluginsSetting)
if sub == nil {
return ret
}

for plugin := range sub {
// HACK: viper changes map keys to case insensitive values, so the workaround is to
// convert map keys to snake case for storage
name := fromSnakeCase(plugin)
ret[name] = fromSnakeCaseMap(i.viper(PluginsSetting).GetStringMap(PluginsSettingPrefix + plugin))
}

return ret
}

func (i *Instance) GetPluginConfiguration(pluginID string) map[string]interface{} {
i.RLock()
defer i.RUnlock()

key := PluginsSettingPrefix + toSnakeCase(pluginID)

// HACK: viper changes map keys to case insensitive values, so the workaround is to
// convert map keys to snake case for storage
v := i.viper(key).GetStringMap(key)

return fromSnakeCaseMap(v)
}

func (i *Instance) SetPluginConfiguration(pluginID string, v map[string]interface{}) {
i.RLock()
defer i.RUnlock()

pluginID = toSnakeCase(pluginID)

key := PluginsSettingPrefix + pluginID

// HACK: viper changes map keys to case insensitive values, so the workaround is to
// convert map keys to snake case for storage
i.viper(key).Set(key, toSnakeCaseMap(v))
}

func (i *Instance) GetDisabledPlugins() []string {
return i.getStringSlice(DisabledPlugins)
}
Expand Down
52 changes: 50 additions & 2 deletions pkg/plugin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type Config struct {

// Javascript files that will be injected into the stash UI.
UI UIConfig `yaml:"ui"`

// Settings that will be used to configure the plugin.
Settings map[string]SettingConfig `yaml:"settings"`
}

type UIConfig struct {
Expand Down Expand Up @@ -87,6 +90,14 @@ func (c UIConfig) getJavascriptFiles(parent Config) []string {
return ret
}

type SettingConfig struct {
// defaults to string
Type PluginSettingTypeEnum `yaml:"type"`
// defaults to key name
DisplayName string `yaml:"displayName"`
Description string `yaml:"description"`
}

func (c Config) getPluginTasks(includePlugin bool) []*PluginTask {
var ret []*PluginTask

Expand Down Expand Up @@ -133,6 +144,28 @@ func convertHooks(hooks []HookTriggerEnum) []string {
return ret
}

func (c Config) getPluginSettings() []PluginSetting {
ret := []PluginSetting{}

for k, o := range c.Settings {
t := o.Type
if t == "" {
t = PluginSettingTypeEnumString
}

s := PluginSetting{
Name: k,
DisplayName: o.DisplayName,
Description: o.Description,
Type: t,
}

ret = append(ret, s)
}

return ret
}

func (c Config) getName() string {
if c.Name != "" {
return c.Name
Expand All @@ -154,6 +187,7 @@ func (c Config) toPlugin() *Plugin {
Javascript: c.UI.getJavascriptFiles(c),
CSS: c.UI.getCSSFiles(c),
},
Settings: c.getPluginSettings(),
}
}

Expand Down Expand Up @@ -211,6 +245,20 @@ func (c Config) getExecCommand(task *OperationConfig) []string {
return ret
}

func (c Config) valid() error {
if c.Interface != "" && !c.Interface.Valid() {
return fmt.Errorf("invalid interface type %s", c.Interface)
}

for k, o := range c.Settings {
if o.Type != "" && !o.Type.IsValid() {
return fmt.Errorf("invalid type %s for setting %s", k, o.Type)
}
}

return nil
}

type interfaceEnum string

// Valid interfaceEnum values
Expand Down Expand Up @@ -292,8 +340,8 @@ func loadPluginFromYAML(reader io.Reader) (*Config, error) {
ret.Interface = InterfaceEnumRaw
}

if !ret.Interface.Valid() {
return nil, fmt.Errorf("invalid interface type %s", ret.Interface)
if err := ret.valid(); err != nil {
return nil, err
}

return ret, nil
Expand Down
26 changes: 18 additions & 8 deletions pkg/plugin/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ import (
)

type Plugin struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
URL *string `json:"url"`
Version *string `json:"version"`
Tasks []*PluginTask `json:"tasks"`
Hooks []*PluginHook `json:"hooks"`
UI PluginUI `json:"ui"`
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
URL *string `json:"url"`
Version *string `json:"version"`
Tasks []*PluginTask `json:"tasks"`
Hooks []*PluginHook `json:"hooks"`
UI PluginUI `json:"ui"`
Settings []PluginSetting `json:"settings"`

Enabled bool `json:"enabled"`
}
Expand All @@ -44,6 +45,15 @@ type PluginUI struct {
CSS []string `json:"css"`
}

type PluginSetting struct {
Name string `json:"name"`
// defaults to string
Type PluginSettingTypeEnum `json:"type"`
// defaults to key name
DisplayName string `json:"displayName"`
Description string `json:"description"`
}

type ServerConfig interface {
GetHost() string
GetPort() int
Expand Down
50 changes: 50 additions & 0 deletions pkg/plugin/setting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package plugin

import (
"fmt"
"io"
"strconv"
)

type PluginSettingTypeEnum string

const (
PluginSettingTypeEnumString PluginSettingTypeEnum = "STRING"
PluginSettingTypeEnumNumber PluginSettingTypeEnum = "NUMBER"
PluginSettingTypeEnumBoolean PluginSettingTypeEnum = "BOOLEAN"
)

var AllPluginSettingTypeEnum = []PluginSettingTypeEnum{
PluginSettingTypeEnumString,
PluginSettingTypeEnumNumber,
PluginSettingTypeEnumBoolean,
}

func (e PluginSettingTypeEnum) IsValid() bool {
switch e {
case PluginSettingTypeEnumString, PluginSettingTypeEnumNumber, PluginSettingTypeEnumBoolean:
return true
}
return false
}

func (e PluginSettingTypeEnum) String() string {
return string(e)
}

func (e *PluginSettingTypeEnum) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}

*e = PluginSettingTypeEnum(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid PluginSettingTypeEnum", str)
}
return nil
}

func (e PluginSettingTypeEnum) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
Loading

0 comments on commit 1823f06

Please sign in to comment.