mirror of
https://github.com/kopia/kopia.git
synced 2026-10-26 08:08:53 -04:00
ui: apply dark mode default and persist user choice (#1621)
This commit is contained in:
1 parent
76fcf5ac66
commit
f56ad31d41
8 files changed
+122
-3
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ''
|
||||
}
|
||||
Reference in new issue
Block a user