ui: apply dark mode default and persist user choice (#1621)

This commit is contained in:
Jarek Kowalski authored and GitHub committed 2021-12-23 12:09:55 -08:00
1 parent 76fcf5ac66
commit f56ad31d41
8 files changed
+122 -3

No files matched your search

+9
View File
@@ -12,6 +12,7 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
@@ -58,6 +59,7 @@ type commandServerStart struct {
serverStartTLSGenerateCertNames []string
serverStartTLSPrintFullServerCert bool
uiTitlePrefix string
uiPreferencesFile string
logServerRequests bool
@@ -95,6 +97,7 @@ func (c *commandServerStart) setup(svc advancedAppServices, parent commandParent
cmd.Flag("tls-print-server-cert", "Print server certificate").Hidden().BoolVar(&c.serverStartTLSPrintFullServerCert)
cmd.Flag("ui-title-prefix", "UI title prefix").Hidden().Envar("KOPIA_UI_TITLE_PREFIX").StringVar(&c.uiTitlePrefix)
cmd.Flag("ui-preferences-file", "Path to JSON file storing UI preferences").StringVar(&c.uiPreferencesFile)
cmd.Flag("log-server-requests", "Log server requests").Hidden().BoolVar(&c.logServerRequests)
@@ -115,6 +118,11 @@ func (c *commandServerStart) run(ctx context.Context, rep repo.Repository) error
return errors.Wrap(err, "unable to initialize authentication")
}
uiPreferencesFile := c.uiPreferencesFile
if uiPreferencesFile == "" {
uiPreferencesFile = filepath.Join(filepath.Dir(c.svc.repositoryConfigFileName()), "ui-preferences.json")
}
srv, err := server.New(ctx, server.Options{
ConfigFile: c.svc.repositoryConfigFileName(),
ConnectOptions: c.co.toRepoConnectOptions(),
@@ -126,6 +134,7 @@ func (c *commandServerStart) run(ctx context.Context, rep repo.Repository) error
UIUser: c.sf.serverUsername,
LogRequests: c.logServerRequests,
PasswordPersist: c.svc.passwordPersistenceStrategy(),
UIPreferencesFile: uiPreferencesFile,
})
if err != nil {
return errors.Wrap(err, "unable to initialize server")
+1 -1
View File
@@ -93,7 +93,7 @@ require (
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kopia/htmluibuild v0.0.0-20211223073627-e6b6141bba94
github.com/kopia/htmluibuild v0.0.0-20211223182507-6b6a50b2f0d1
github.com/kr/fs v0.1.0 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
+2
View File
@@ -391,6 +391,8 @@ github.com/kopia/htmluibuild v0.0.0-20211218222024-ddc4c4aa34ee h1:72jpX5y1qR8SH
github.com/kopia/htmluibuild v0.0.0-20211218222024-ddc4c4aa34ee/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw=
github.com/kopia/htmluibuild v0.0.0-20211223073627-e6b6141bba94 h1:cetYBxizQNLfwF8MKxuwra/mOYwuBmC3Asy+hVAl9wA=
github.com/kopia/htmluibuild v0.0.0-20211223073627-e6b6141bba94/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw=
github.com/kopia/htmluibuild v0.0.0-20211223182507-6b6a50b2f0d1 h1:stwETPrkIzdLAyQ+yzD1pfl/YHVRoy1kFDJbcyCSfSE=
github.com/kopia/htmluibuild v0.0.0-20211223182507-6b6a50b2f0d1/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+63
View File
@@ -0,0 +1,63 @@
package server
import (
"bytes"
"context"
"encoding/json"
"net/http"
"os"
"github.com/natefinch/atomic"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/serverapi"
)
func (s *Server) getUIPreferencesOrEmpty() (serverapi.UIPreferences, error) {
p := serverapi.UIPreferences{}
if s.options.UIPreferencesFile == "" {
return p, nil
}
f, err := os.Open(s.options.UIPreferencesFile)
if os.IsNotExist(err) {
return p, nil
}
if err != nil {
return p, errors.Wrap(err, "unable to open UI preferences file")
}
defer f.Close() //nolint:errcheck,gosec
if err := json.NewDecoder(f).Decode(&p); err != nil {
return p, errors.Wrap(err, "invalid UI preferences file")
}
return p, nil
}
func (s *Server) handleGetUIPreferences(ctx context.Context, r *http.Request, body []byte) (interface{}, *apiError) {
p, err := s.getUIPreferencesOrEmpty()
if err != nil {
return nil, internalServerError(err)
}
return &p, nil
}
func (s *Server) handleSetUIPreferences(ctx context.Context, r *http.Request, body []byte) (interface{}, *apiError) {
var p serverapi.UIPreferences
// verify the JSON is valid by unmarshaling it
if err := json.Unmarshal(body, &p); err != nil {
return nil, requestError(serverapi.ErrorMalformedRequest, "malformed request")
}
if err := atomic.WriteFile(s.options.UIPreferencesFile, bytes.NewReader(body)); err != nil {
return nil, internalServerError(err)
}
return serverapi.Empty{}, nil
}
+35
View File
@@ -0,0 +1,35 @@
package server_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/internal/apiclient"
"github.com/kopia/kopia/internal/repotesting"
"github.com/kopia/kopia/internal/serverapi"
)
func TestUIPreferences(t *testing.T) {
ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
srvInfo := startServer(t, env, false)
cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
BaseURL: srvInfo.BaseURL,
TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
Username: testUIUsername,
Password: testUIPassword,
})
require.NoError(t, err)
var p, p2 serverapi.UIPreferences
require.NoError(t, cli.Get(ctx, "ui-preferences", nil, &p))
require.Equal(t, "", p.Theme)
p.Theme = "dark"
require.NoError(t, cli.Put(ctx, "ui-preferences", &p, &serverapi.Empty{}))
require.NoError(t, cli.Get(ctx, "ui-preferences", nil, &p2))
require.Equal(t, p, p2)
}
+3
View File
@@ -136,6 +136,8 @@ func (s *Server) APIHandlers(legacyAPI bool) http.Handler {
m.HandleFunc("/api/v1/mounts", s.handleAPI(requireUIUser, s.handleMountList)).Methods(http.MethodGet)
m.HandleFunc("/api/v1/current-user", s.handleAPIPossiblyNotConnected(requireUIUser, s.handleCurrentUser)).Methods(http.MethodGet)
m.HandleFunc("/api/v1/ui-preferences", s.handleAPIPossiblyNotConnected(requireUIUser, s.handleGetUIPreferences)).Methods(http.MethodGet)
m.HandleFunc("/api/v1/ui-preferences", s.handleAPIPossiblyNotConnected(requireUIUser, s.handleSetUIPreferences)).Methods(http.MethodPut)
m.HandleFunc("/api/v1/tasks-summary", s.handleAPI(requireUIUser, s.handleTaskSummary)).Methods(http.MethodGet)
m.HandleFunc("/api/v1/tasks", s.handleAPI(requireUIUser, s.handleTaskList)).Methods(http.MethodGet)
@@ -669,6 +671,7 @@ type Options struct {
AuthCookieSigningKey string
LogRequests bool
UIUser string // name of the user allowed to access the UI
UIPreferencesFile string // name of the JSON file storing UI preferences
}
// New creates a Server.
+4 -2
View File
@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
@@ -52,8 +53,9 @@ func startServer(t *testing.T, env *repotesting.Environment, tls bool) *repo.API
auth.AuthenticateSingleUser(testUsername+"@"+testHostname, testPassword),
auth.AuthenticateSingleUser(testUIUsername, testUIPassword),
),
RefreshInterval: 1 * time.Minute,
UIUser: testUIUsername,
RefreshInterval: 1 * time.Minute,
UIUser: testUIUsername,
UIPreferencesFile: filepath.Join(testutil.TempDirectory(t), "ui-pref.json"),
})
require.NoError(t, err)
+5
View File
@@ -251,3 +251,8 @@ type ResolvePathResponse struct {
type CLIInfo struct {
Executable string `json:"executable"`
}
// UIPreferences represents JSON object storing UI preferences.
type UIPreferences struct {
Theme string `json:"theme"` // 'dark', 'light' or ''
}