Skip to content

Commit

Permalink
feat: add master config option to provide custom logo (#9664)
Browse files Browse the repository at this point in the history
add config options with paths to different logo variations
to be used by the webclient to display on the login and loading pages.
  • Loading branch information
hamidzr authored Jul 31, 2024
1 parent f42daca commit 15226b7
Show file tree
Hide file tree
Showing 18 changed files with 438 additions and 165 deletions.
13 changes: 13 additions & 0 deletions docs/reference/deploy/master-config-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,19 @@ warning is returned. The default value is ``true``.

Optional. Specify a human-readable name for this cluster.

**********************
``ui_customization``
**********************

Optional. Applies only to the Determined Enterprise Edition. This section contains options to
customize the UI.

``logo_path``
=============

Specifies the path to a user-provided logo to be shown in the UI. Ensure the path is accessible and
reachable by the master service. The logo file should be a valid image format, with SVG recommended.

*************************
``tensorboard_timeout``
*************************
Expand Down
6 changes: 6 additions & 0 deletions docs/release-notes/custom-logo.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:orphan:

**New Features**

- Add a ``ui_customization`` option to the :ref:`master configuration <master-config-reference>`
for specifying a custom logo for the WebUI.
4 changes: 4 additions & 0 deletions harness/determined/common/api/bindings.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion master/internal/api_master.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ func (a *apiServer) GetMaster(
if license.IsEE() {
brand = "hpe"
}

masterResp := &apiv1.GetMasterResponse{
Version: version.Version,
MasterId: a.m.MasterID,
ClusterId: a.m.ClusterID,
ClusterName: a.m.config.ClusterName,
TelemetryEnabled: a.m.config.Telemetry.Enabled && a.m.config.Telemetry.SegmentWebUIKey != "",
HasCustomLogo: a.m.config.UICustomization.HasCustomLogo(),
ExternalLoginUri: a.m.config.InternalConfig.ExternalSessions.LoginURI,
ExternalLogoutUri: a.m.config.InternalConfig.ExternalSessions.LogoutURI,
Branding: brand,
Expand Down
1 change: 1 addition & 0 deletions master/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ type Config struct {
EnableCors bool `json:"enable_cors"`
LaunchError bool `json:"launch_error"`
ClusterName string `json:"cluster_name"`
UICustomization UICustomizationConfig `json:"ui_customization"`
Logging model.LoggingConfig `json:"logging"`
RetentionPolicy model.LogRetentionPolicy `json:"retention_policy"`
Observability ObservabilityConfig `json:"observability"`
Expand Down
92 changes: 92 additions & 0 deletions master/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,3 +939,95 @@ func TestMultiRMPreemptionAndPriority(t *testing.T) {
priority = ReadPriority("nil-rp", model.CommandConfig{})
require.Equal(t, KubernetesDefaultPriority, priority)
}

func TestPickVariation(t *testing.T) {
tests := []struct {
name string
variations MediaAssetVariations
mode string
orientation string
expected string
}{
{
name: "Light Horizontal prioritized",
variations: MediaAssetVariations{
LightHorizontal: "light-horizontal",
LightVeritical: "light-vertical",
DarkHorizontal: "dark-horizontal",
DarkVeritical: "dark-vertical",
},
mode: "",
orientation: "",
expected: "light-horizontal",
},
{
name: "Light Vertical when Light Horizontal is empty",
variations: MediaAssetVariations{
LightHorizontal: "",
LightVeritical: "light-vertical",
DarkHorizontal: "dark-horizontal",
DarkVeritical: "dark-vertical",
},
mode: "",
orientation: "vertical",
expected: "light-vertical",
},
{
name: "Dark Horizontal when mode is dark",
variations: MediaAssetVariations{
LightHorizontal: "light-horizontal",
LightVeritical: "light-vertical",
DarkHorizontal: "dark-horizontal",
DarkVeritical: "dark-vertical",
},
mode: "dark",
orientation: "",
expected: "dark-horizontal",
},
{
name: "Dark Vertical when mode is dark and orientation is vertical",
variations: MediaAssetVariations{
LightHorizontal: "light-horizontal",
LightVeritical: "light-vertical",
DarkHorizontal: "dark-horizontal",
DarkVeritical: "dark-vertical",
},
mode: "dark",
orientation: "vertical",
expected: "dark-vertical",
},
{
name: "Fallback to Light Horizontal if no matches",
variations: MediaAssetVariations{
LightHorizontal: "light-horizontal",
LightVeritical: "",
DarkHorizontal: "",
DarkVeritical: "",
},
mode: "dark",
orientation: "vertical",
expected: "light-horizontal",
},
{
name: "Empty variations fallback to empty string",
variations: MediaAssetVariations{
LightHorizontal: "",
LightVeritical: "",
DarkHorizontal: "",
DarkVeritical: "",
},
mode: "",
orientation: "",
expected: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.variations.PickVariation(tt.mode, tt.orientation)
if result != tt.expected {
t.Errorf("PickVariation(%v, %v) = %v; want %v", tt.mode, tt.orientation, result, tt.expected)
}
})
}
}
101 changes: 101 additions & 0 deletions master/internal/config/ui_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package config

import (
"os"

"github.com/pkg/errors"

"github.com/determined-ai/determined/master/internal/license"
)

// MediaAssetVariations allow variations of a media asset to be defined.
type MediaAssetVariations struct {
DarkHorizontal string `json:"dark_horizontal"`
DarkVeritical string `json:"dark_vertical"`
LightHorizontal string `json:"light_horizontal"`
LightVeritical string `json:"light_vertical"`
}

// PickVariation returns the best variation for the given mode and orientation.
func (m MediaAssetVariations) PickVariation(mode, orientation string) string {
const (
orientationHorizontal = "horizontal"
orientationVertical = "vertical"
)
if mode == "" || mode == "light" {
if orientation == "" || orientation == orientationHorizontal {
if m.LightHorizontal != "" {
return m.LightHorizontal
}
}
if orientation == "" || orientation == orientationVertical {
if m.LightVeritical != "" {
return m.LightVeritical
}
if m.LightHorizontal != "" {
return m.LightHorizontal
}
}
}

if mode == "dark" {
if orientation == "" || orientation == orientationHorizontal {
if m.DarkHorizontal != "" {
return m.DarkHorizontal
}
}
if orientation == "" || orientation == orientationVertical {
if m.DarkVeritical != "" {
return m.DarkVeritical
}
if m.DarkHorizontal != "" {
return m.DarkHorizontal
}
}
}

return m.LightHorizontal
}

// UICustomizationConfig holds the configuration for customizing the UI.
type UICustomizationConfig struct {
// LogoPath is the path to variation of custom logo to use in the web UI.
LogoPath MediaAssetVariations `json:"logo_path"`
}

// Validate checks if the paths in UICustomizationConfig are valid filesystem paths and reachable.
func (u UICustomizationConfig) Validate() []error {
var errs []error

paths := map[string]string{
"LightHorizontal": u.LogoPath.LightHorizontal,
"LightVeritical": u.LogoPath.LightVeritical,
"DarkHorizontal": u.LogoPath.DarkHorizontal,
"DarkVeritical": u.LogoPath.DarkVeritical,
}

for name, path := range paths {
if path == "" {
continue
}
license.RequireLicense("UI Customization")
info, err := os.Stat(path)
switch {
case os.IsNotExist(err):
errs = append(errs, errors.New(name+" path is not reachable: "+path))
case err != nil:
errs = append(errs, errors.New(name+" path error: "+err.Error()))
case info.IsDir():
errs = append(errs, errors.New(name+" path is a directory, not a file: "+path))
}
}

return errs
}

// HasCustomLogo returns whether the UI customization has a custom logo.
func (u UICustomizationConfig) HasCustomLogo() bool {
// If one exists, we're good
return u.LogoPath.LightHorizontal != "" || u.LogoPath.LightVeritical != "" ||
u.LogoPath.DarkHorizontal != "" || u.LogoPath.DarkVeritical != ""
}
8 changes: 8 additions & 0 deletions master/internal/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,14 @@ func (m *Master) Run(ctx context.Context, gRPCLogInitDone chan struct{}) error {
m.echo.Static("/docs", filepath.Join(webuiRoot, "docs"))

webuiGroup := m.echo.Group(webuiBaseRoute)
webuiGroup.GET("/customer-assets/logo", func(c echo.Context) error {
if !m.config.UICustomization.HasCustomLogo() {
return echo.NewHTTPError(http.StatusNotFound)
}
return c.File(m.config.UICustomization.LogoPath.PickVariation(
c.QueryParam("mode"), c.QueryParam("orientation"),
))
})
webuiGroup.File("/design", designIndex)
webuiGroup.File("/design/", designIndex)
webuiGroup.File("", reactIndex)
Expand Down
Loading

0 comments on commit 15226b7

Please sign in to comment.