Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3914fb2ac1 | |||
| ac8fbd15e3 | |||
| 119f67d20d | |||
| 404316140f | |||
| 03302046bf |
@@ -1991,6 +1991,12 @@ default_datasource_uid =
|
|||||||
;feature1 = true
|
;feature1 = true
|
||||||
;feature2 = false
|
;feature2 = false
|
||||||
|
|
||||||
|
# default for the navigation sidebar docking behavior
|
||||||
|
# true = docked by default
|
||||||
|
# false = undocked by default
|
||||||
|
# When unspecified the current Grafana default remains (true).
|
||||||
|
;default_sidebar_docked = false
|
||||||
|
|
||||||
[date_formats]
|
[date_formats]
|
||||||
# For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/
|
# For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note: This is a minimal example showing how to add feature toggles to the boot payload.
|
||||||
|
// Integrate with Grafana's actual frontend boot data code path (where the server renders JSON into the page).
|
||||||
|
func frontendSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Example existing boot data:
|
||||||
|
boot := map[string]interface{}{
|
||||||
|
"settings": map[string]interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add our feature toggle into the front-end settings payload.
|
||||||
|
settings := boot["settings"].(map[string]interface{})
|
||||||
|
|
||||||
|
settings["featureToggles"] = map[string]interface{}{
|
||||||
|
"default_sidebar_docked": setting.FeatureToggleConfig.DefaultSidebarDocked,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(boot)
|
||||||
|
}
|
||||||
@@ -220,6 +220,12 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
|||||||
|
|
||||||
hs.HooksService.RunIndexDataHooks(&data, c)
|
hs.HooksService.RunIndexDataHooks(&data, c)
|
||||||
|
|
||||||
|
if data.Settings.FeatureToggles == nil {
|
||||||
|
data.Settings.FeatureToggles = map[string]bool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Settings.FeatureToggles["default_sidebar_docked"] = setting.FeatureToggleConfig.DefaultSidebarDocked
|
||||||
|
|
||||||
data.NavTree.Sort()
|
data.NavTree.Sort()
|
||||||
|
|
||||||
return &data, nil
|
return &data, nil
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeatureToggles holds lightweight feature flags that can be set via grafana.ini
|
||||||
|
type FeatureToggles struct {
|
||||||
|
// DefaultSidebarDocked determines the default state of the left navigation when no per-browser
|
||||||
|
// preference exists in localStorage (grafana.navigation.docked).
|
||||||
|
DefaultSidebarDocked bool `json:"default_sidebar_docked"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var FeatureToggleConfig = &FeatureToggles{DefaultSidebarDocked: true}
|
||||||
|
|
||||||
|
// loadFeatureToggles reads [feature_toggles] section from the provided INI file.
|
||||||
|
// This is a minimal helper; integrate it into your existing settings loader.
|
||||||
|
func loadFeatureToggles(cfg *ini.File) {
|
||||||
|
sec := cfg.Section("feature_toggles")
|
||||||
|
FeatureToggleConfig.DefaultSidebarDocked = sec.Key("default_sidebar_docked").MustBool(true)
|
||||||
|
}
|
||||||
@@ -980,6 +980,10 @@ func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) {
|
|||||||
defaultConfigFile := path.Join(cfg.HomePath, "conf/defaults.ini")
|
defaultConfigFile := path.Join(cfg.HomePath, "conf/defaults.ini")
|
||||||
cfg.configFiles = append(cfg.configFiles, defaultConfigFile)
|
cfg.configFiles = append(cfg.configFiles, defaultConfigFile)
|
||||||
|
|
||||||
|
// M@ DEBUG do not commit
|
||||||
|
cfg.Logger.Info("In loadConfiguration M@-----------")
|
||||||
|
fmt.Printf("In loadConfiguration M@-----------\n")
|
||||||
|
|
||||||
// check if config file exists
|
// check if config file exists
|
||||||
if _, err := os.Stat(defaultConfigFile); os.IsNotExist(err) {
|
if _, err := os.Stat(defaultConfigFile); os.IsNotExist(err) {
|
||||||
fmt.Println("Grafana-server Init Failed: Could not find config defaults, make sure homepath command line parameter is set or working directory is homepath")
|
fmt.Println("Grafana-server Init Failed: Could not find config defaults, make sure homepath command line parameter is set or working directory is homepath")
|
||||||
@@ -1019,6 +1023,8 @@ func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) {
|
|||||||
// apply command line overrides
|
// apply command line overrides
|
||||||
cfg.applyCommandLineProperties(commandLineProps, parsedFile)
|
cfg.applyCommandLineProperties(commandLineProps, parsedFile)
|
||||||
|
|
||||||
|
loadFeatureToggles(parsedFile)
|
||||||
|
|
||||||
// evaluate config values containing environment variables
|
// evaluate config values containing environment variables
|
||||||
err = expandConfig(parsedFile)
|
err = expandConfig(parsedFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
// Early script to initialize grafana.navigation.docked from server bootdata.
|
||||||
|
// Behavior:
|
||||||
|
// - If the user already has a preference (no companion ".auto" key) we never override it.
|
||||||
|
// - If we set the value automatically, we set a companion key to mark it.
|
||||||
|
// - If later the authoritative bootdata promise resolves, we will override only if
|
||||||
|
// the current value was previously auto-set by us.
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
let key = 'grafana.navigation.docked';
|
||||||
|
let autoKey = key + '.auto';
|
||||||
|
|
||||||
|
function isAutoSet() {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(autoKey) === '1';
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAuto(val) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, val ? 'true' : 'false');
|
||||||
|
localStorage.setItem(autoKey, '1');
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setIfAbsent(val) {
|
||||||
|
try {
|
||||||
|
if (localStorage.getItem(key) === null) {
|
||||||
|
setAuto(val);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setIfAutoOrAbsent(val) {
|
||||||
|
try {
|
||||||
|
let cur = localStorage.getItem(key);
|
||||||
|
if (cur === null || isAutoSet()) {
|
||||||
|
setAuto(val);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDefault(preferServer) {
|
||||||
|
try {
|
||||||
|
// If a real user preference exists (not marked as auto) and preferServer is true,
|
||||||
|
// we should NOT override it. setIfAutoOrAbsent will only override if auto or absent.
|
||||||
|
// preferServer indicates this call is from authoritative bootdata (true) or inline (false).
|
||||||
|
let serverDefault;
|
||||||
|
if (
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
window.grafanaBootData &&
|
||||||
|
window.grafanaBootData.settings &&
|
||||||
|
window.grafanaBootData.settings.featureToggles &&
|
||||||
|
typeof window.grafanaBootData.settings.featureToggles.default_sidebar_docked !== 'undefined'
|
||||||
|
) {
|
||||||
|
serverDefault = window.grafanaBootData.settings.featureToggles.default_sidebar_docked;
|
||||||
|
}
|
||||||
|
|
||||||
|
let globalDefault = typeof window !== 'undefined' ? window.__defaultSidebarDocked : undefined;
|
||||||
|
|
||||||
|
let val =
|
||||||
|
typeof serverDefault !== 'undefined'
|
||||||
|
? serverDefault
|
||||||
|
: typeof globalDefault !== 'undefined'
|
||||||
|
? globalDefault
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (typeof val === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is called for authoritative data (preferServer === true), allow override of previously
|
||||||
|
// auto-set values; if it's non-authoritative (inline), only set if absent.
|
||||||
|
if (preferServer) {
|
||||||
|
setIfAutoOrAbsent(val);
|
||||||
|
} else {
|
||||||
|
setIfAbsent(val);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast-path: try immediate apply from inline bootdata (non-authoritative)
|
||||||
|
applyDefault(false);
|
||||||
|
|
||||||
|
// If async bootdata is fetched, wait for the promise and apply authoritative value
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
window.__grafana_boot_data_promise &&
|
||||||
|
typeof window.__grafana_boot_data_promise.then === 'function'
|
||||||
|
) {
|
||||||
|
window.__grafana_boot_data_promise
|
||||||
|
.then(function () {
|
||||||
|
applyDefault(true);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
// ignore
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try again shortly after load as a final chance (authoritative)
|
||||||
|
setTimeout(function () {
|
||||||
|
applyDefault(true);
|
||||||
|
}, 1500);
|
||||||
|
} catch (e) {
|
||||||
|
// Do not let this break the page
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -267,6 +267,8 @@
|
|||||||
|
|
||||||
<div id="reactRoot"></div>
|
<div id="reactRoot"></div>
|
||||||
|
|
||||||
|
<script src="/public/scripts/set-default-sidebar.js"></script>
|
||||||
|
|
||||||
<script nonce="[[.Nonce]]">
|
<script nonce="[[.Nonce]]">
|
||||||
window.grafanaBootData = {
|
window.grafanaBootData = {
|
||||||
user: [[.User]],
|
user: [[.User]],
|
||||||
|
|||||||
Reference in New Issue
Block a user