diff --git a/frontend/resources/vue/utils/logsFilterStorage.js b/frontend/resources/vue/utils/logsFilterStorage.js new file mode 100644 index 0000000..cd6fae1 --- /dev/null +++ b/frontend/resources/vue/utils/logsFilterStorage.js @@ -0,0 +1,21 @@ +export const LOGS_FILTER_STORAGE_KEY = 'olivetin-logs-filter' + +export function loadStoredLogsFilter () { + try { + return sessionStorage.getItem(LOGS_FILTER_STORAGE_KEY) || '' + } catch { + return '' + } +} + +export function storeLogsFilter (value) { + try { + if (value) { + sessionStorage.setItem(LOGS_FILTER_STORAGE_KEY, value) + } else { + sessionStorage.removeItem(LOGS_FILTER_STORAGE_KEY) + } + } catch { + // Ignore storage failures (private mode, quota, etc.) + } +} diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 81b83ee..c036d90 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -1217,6 +1217,9 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq currentVersion = installationinfo.Build.Version availableVersion = installationinfo.Runtime.AvailableVersion } + + rootDashboardEntries := api.buildRootDashboardEntries(user, api.cfg.Dashboards) + res := &apiv1.InitResponse{ ShowFooter: api.cfg.ShowFooter, ShowNavigation: api.cfg.ShowNavigation, @@ -1232,7 +1235,8 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq OAuth2Providers: buildPublicOAuth2ProvidersList(api.cfg), AdditionalLinks: buildAdditionalLinks(api.cfg.AdditionalNavigationLinks), StyleMods: api.cfg.StyleMods, - RootDashboards: api.buildRootDashboards(user, api.cfg.Dashboards), + RootDashboards: rootDashboardTitles(rootDashboardEntries), + RootDashboardEntries: rootDashboardEntries, AuthenticatedUser: user.Username, AuthenticatedUserProvider: user.Provider, EffectivePolicy: buildEffectivePolicy(user.EffectivePolicy), @@ -1300,30 +1304,47 @@ func getValidThemeName(themesDir string, entry os.DirEntry) string { } func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string { - var rootDashboards []string - dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "") - - api.addDefaultDashboardIfNeeded(&rootDashboards, dashboardRenderRequest) - api.addCustomDashboards(&rootDashboards, dashboards, dashboardRenderRequest) - - return rootDashboards + return rootDashboardTitles(api.buildRootDashboardEntries(user, dashboards)) } -func (api *oliveTinAPI) addDefaultDashboardIfNeeded(rootDashboards *[]string, rr *DashboardRenderRequest) { +func rootDashboardTitles(entries []*apiv1.RootDashboard) []string { + titles := make([]string, 0, len(entries)) + + for _, entry := range entries { + titles = append(titles, entry.Title) + } + + return titles +} + +func (api *oliveTinAPI) buildRootDashboardEntries(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []*apiv1.RootDashboard { + var entries []*apiv1.RootDashboard + dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "") + + api.addDefaultDashboardEntryIfNeeded(&entries, dashboardRenderRequest) + api.addCustomDashboardEntries(&entries, dashboards, dashboardRenderRequest) + + return entries +} + +func (api *oliveTinAPI) addDefaultDashboardEntryIfNeeded(entries *[]*apiv1.RootDashboard, rr *DashboardRenderRequest) { defaultDashboard := buildDefaultDashboard(rr) if defaultDashboard != nil && len(defaultDashboard.Contents) > 0 { log.Tracef("defaultDashboard: %+v", defaultDashboard.Contents) - *rootDashboards = append(*rootDashboards, "Actions") + *entries = append(*entries, &apiv1.RootDashboard{Title: "Actions"}) } } -func (api *oliveTinAPI) addCustomDashboards(rootDashboards *[]string, dashboards []*config.DashboardComponent, rr *DashboardRenderRequest) { +func (api *oliveTinAPI) addCustomDashboardEntries(entries *[]*apiv1.RootDashboard, dashboards []*config.DashboardComponent, rr *DashboardRenderRequest) { for _, dashboard := range dashboards { // We have to build the dashboard response instead of just looping over config.dashboards, // because we need to check if the user has access to the dashboard db := renderDashboard(rr, dashboard.Title) if db != nil { - *rootDashboards = append(*rootDashboards, dashboard.Title) + *entries = append(*entries, &apiv1.RootDashboard{ + Title: dashboard.Title, + Category: dashboard.Category, + }) } } } diff --git a/service/internal/api/dashboard_acl_test.go b/service/internal/api/dashboard_acl_test.go index 312a945..d0734ce 100644 --- a/service/internal/api/dashboard_acl_test.go +++ b/service/internal/api/dashboard_acl_test.go @@ -68,6 +68,61 @@ func TestDashboardAclsRootNavAndGetDashboard(t *testing.T) { assert.Equal(t, "Services", db.Title) } +func TestRootDashboardEntriesIncludeCategory(t *testing.T) { + cfg := config.DefaultConfig() + cfg.Dashboards = []*config.DashboardComponent{ + { + Title: "Misc Tools", + Contents: []*config.DashboardComponent{{Title: "Hello", Type: "display"}}, + }, + { + Title: "My Servers", + Category: "Infrastructure", + Contents: []*config.DashboardComponent{{Title: "Ping", Type: "display"}}, + }, + { + Title: "Status Board", + Category: "Monitoring", + Contents: []*config.DashboardComponent{{Title: "Uptime", Type: "display"}}, + }, + { + Title: "My Containers", + Category: "Infrastructure", + Contents: []*config.DashboardComponent{{Title: "Restart", Type: "display"}}, + }, + } + + ex := executor.DefaultExecutor(cfg) + api := newServer(ex) + user := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"} + user.BuildUserAcls(cfg) + + entries := api.buildRootDashboardEntries(user, cfg.Dashboards) + require.Len(t, entries, 4) + assert.Equal(t, []string{"Misc Tools", "My Servers", "Status Board", "My Containers"}, rootDashboardTitles(entries)) + assert.Equal(t, "", entries[0].Category) + assert.Equal(t, "Infrastructure", entries[1].Category) + assert.Equal(t, "Monitoring", entries[2].Category) + assert.Equal(t, "Infrastructure", entries[3].Category) +} + +func TestRootDashboardEntriesOmitAclHiddenCategories(t *testing.T) { + cfg := buildDashboardAclTestConfig() + cfg.Dashboards[0].Category = "Public" + cfg.Dashboards[1].Category = "Admin only" + + ex := executor.DefaultExecutor(cfg) + api := newServer(ex) + + guest := &authpublic.AuthenticatedUser{Username: "guest", Provider: "system"} + guest.BuildUserAcls(cfg) + + entries := api.buildRootDashboardEntries(guest, cfg.Dashboards) + require.Len(t, entries, 1) + assert.Equal(t, "Public tools", entries[0].Title) + assert.Equal(t, "Public", entries[0].Category) +} + func TestDashboardAclsNestedDirectoryDeepLink(t *testing.T) { cfg := buildDashboardAclTestConfig() cfg.Dashboards = []*config.DashboardComponent{ diff --git a/service/internal/config/port_env_test.go b/service/internal/config/port_env_test.go new file mode 100644 index 0000000..e4034b8 --- /dev/null +++ b/service/internal/config/port_env_test.go @@ -0,0 +1,47 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestApplyPortEnvironmentOverride(t *testing.T) { + t.Setenv("PORT", "8080") + + cfg := DefaultConfig() + cfg.ListenAddressSingleHTTPFrontend = "0.0.0.0:1337" + applyPortEnvironmentOverride(cfg) + + assert.Equal(t, "0.0.0.0:8080", cfg.ListenAddressSingleHTTPFrontend) +} + +func TestApplyPortEnvironmentOverridePreservesHost(t *testing.T) { + t.Setenv("PORT", "9000") + + cfg := DefaultConfig() + cfg.ListenAddressSingleHTTPFrontend = "127.0.0.1:1337" + applyPortEnvironmentOverride(cfg) + + assert.Equal(t, "127.0.0.1:9000", cfg.ListenAddressSingleHTTPFrontend) +} + +func TestApplyPortEnvironmentOverrideUnsetLeavesConfig(t *testing.T) { + t.Setenv("PORT", "") + + cfg := DefaultConfig() + cfg.ListenAddressSingleHTTPFrontend = "0.0.0.0:2337" + applyPortEnvironmentOverride(cfg) + + assert.Equal(t, "0.0.0.0:2337", cfg.ListenAddressSingleHTTPFrontend) +} + +func TestApplyPortEnvironmentOverrideIgnoresInvalid(t *testing.T) { + t.Setenv("PORT", "not-a-port") + + cfg := DefaultConfig() + cfg.ListenAddressSingleHTTPFrontend = "0.0.0.0:1337" + applyPortEnvironmentOverride(cfg) + + assert.Equal(t, "0.0.0.0:1337", cfg.ListenAddressSingleHTTPFrontend) +}