Skip to content

Commit

Permalink
feat: syncs all profile localStorage to disk (#2537)
Browse files Browse the repository at this point in the history
  • Loading branch information
amir20 authored Nov 27, 2023
1 parent b54b419 commit 60650dd
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 57 deletions.
3 changes: 3 additions & 0 deletions assets/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ declare global {
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useProfileStorage: typeof import('./composable/profileStorage')['useProfileStorage']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useReleases: typeof import('./stores/releases')['useReleases']
Expand Down Expand Up @@ -617,6 +618,7 @@ declare module 'vue' {
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
Expand Down Expand Up @@ -959,6 +961,7 @@ declare module '@vue/runtime-core' {
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
Expand Down
2 changes: 1 addition & 1 deletion assets/components/Links.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ async function logout() {
}
const { hasUpdate, latest } = useReleases();
const latestTag = useStorage("DOZZLE_LATEST_TAG", config.version);
const latestTag = useProfileStorage("releaseSeen", config.version);
</script>
6 changes: 3 additions & 3 deletions assets/components/LogViewer/ContainerTitle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
<script lang="ts" setup>
const { container } = useContainerContext();
const pinned = computed({
get: () => pinnedContainers.value.has(container.value.storageKey),
get: () => pinnedContainers.value.has(container.value.name),
set: (value) => {
if (value) {
pinnedContainers.value.add(container.value.storageKey);
pinnedContainers.value.add(container.value.name);
} else {
pinnedContainers.value.delete(container.value.storageKey);
pinnedContainers.value.delete(container.value.name);
}
},
});
Expand Down
2 changes: 1 addition & 1 deletion assets/components/SideMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const sortedContainers = computed(() =>
const groupedContainers = computed(() =>
sortedContainers.value.reduce(
(acc, item) => {
if (debouncedIds.value.has(item.storageKey)) {
if (debouncedIds.value.has(item.name)) {
acc.pinned.push(item);
} else {
acc.unpinned.push(item);
Expand Down
36 changes: 36 additions & 0 deletions assets/composable/profileStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Profile } from "@/stores/config";

export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue: NonNullable<Profile[K]>) {
const storageKey = "DOZZLE_" + key.toUpperCase();
const storage = useStorage<NonNullable<Profile[K]>>(storageKey, defaultValue, undefined, {
writeDefaults: false,
mergeDefaults: true,
});

if (config.profile?.[key]) {
if (storage.value instanceof Set && config.profile[key] instanceof Array) {
storage.value = new Set([...(config.profile[key] as Iterable<any>)]) as unknown as NonNullable<Profile[K]>;
} else if (config.profile[key] instanceof Array) {
storage.value = config.profile[key] as NonNullable<Profile[K]>;
} else if (config.profile[key] instanceof Object) {
Object.assign(storage.value, config.profile[key]);
} else {
storage.value = config.profile[key] as NonNullable<Profile[K]>;
}
}

if (config.user) {
watch(
storage,
(value) => {
fetch(withBase("/api/profile"), {
method: "PATCH",
body: JSON.stringify({ [key]: value }, (_, value) => (value instanceof Set ? [...value] : value)),
});
},
{ deep: true },
);
}

return storage;
}
12 changes: 7 additions & 5 deletions assets/composable/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0].id;
}

export function persistentVisibleKeys(container: Ref<Container>) {
const storage = useStorage<{ [key: string]: string[][] }>("DOZZLE_VISIBLE_KEYS", {});
export function persistentVisibleKeys(container: Ref<Container>): Ref<string[][]> {
const storage = useProfileStorage("visibleKeys", {});
return computed(() => {
if (!(container.value.storageKey in storage.value)) {
storage.value[container.value.storageKey] = [];
// Returning a temporary ref here to avoid writing an empty array to storage
const visibleKeys = ref<string[][]>([]);
watchOnce(visibleKeys, () => (storage.value[container.value.storageKey] = visibleKeys.value), { deep: true });
return visibleKeys.value;
}

return storage.value[container.value.storageKey];
});
}

const DOZZLE_PINNED_CONTAINERS = "DOZZLE_PINNED_CONTAINERS";
export const pinnedContainers = useStorage(DOZZLE_PINNED_CONTAINERS, new Set<string>());
export const pinnedContainers = useProfileStorage("pinned", new Set<string>());
11 changes: 9 additions & 2 deletions assets/stores/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type Settings } from "@/stores/settings";

const text = document.querySelector("script#config__json")?.textContent || "{}";

interface Config {
export interface Config {
version: string;
base: string;
authorizationNeeded: boolean;
Expand All @@ -17,10 +17,17 @@ interface Config {
name: string;
avatar: string;
};
serverSettings?: Settings;
profile?: Profile;
pages?: { id: string; title: string }[];
}

export interface Profile {
settings?: Settings;
pinned?: Set<string>;
visibleKeys?: { [key: string]: string[][] };
releaseSeen?: string;
}

const pageConfig = JSON.parse(text);

const config: Config = {
Expand Down
13 changes: 1 addition & 12 deletions assets/stores/settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { toRefs } from "@vueuse/core";
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";

export type Settings = {
search: boolean;
Expand Down Expand Up @@ -30,17 +29,7 @@ export const DEFAULT_SETTINGS: Settings = {
automaticRedirect: true,
};

export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = { ...DEFAULT_SETTINGS, ...settings.value, ...config.serverSettings };

if (config.user) {
watch(settings, (value) => {
fetch(withBase("/api/profile/settings"), {
method: "PUT",
body: JSON.stringify(value),
});
});
}
export const settings = useProfileStorage("settings", DEFAULT_SETTINGS);

export const {
collapseNav,
Expand Down
68 changes: 48 additions & 20 deletions internal/profile/settings.go → internal/profile/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package profile
import (
"encoding/json"
"errors"
"io"
"sync"

"os"
"path/filepath"
Expand All @@ -11,6 +13,12 @@ import (
log "github.com/sirupsen/logrus"
)

const (
profileFilename = "profile.json"
)

var missingProfileErr = errors.New("Profile file does not exist")

type Settings struct {
Search bool `json:"search"`
MenuWidth float32 `json:"menuWidth"`
Expand All @@ -26,7 +34,15 @@ type Settings struct {
HourStyle string `json:"hourStyle,omitempty"`
}

var data_path string
type Profile struct {
Settings *Settings `json:"settings,omitempty"`
Pinned []string `json:"pinned,omitempty"`
VisibleKeys map[string][][]string `json:"visibleKeys,omitempty"`
ReleaseSeen string `json:"releaseSeen,omitempty"`
}

var dataPath string
var mux = &sync.Mutex{}

func init() {
path, err := filepath.Abs("./data")
Expand All @@ -40,28 +56,40 @@ func init() {
return
}
}
data_path = path
dataPath = path
}

func SaveUserSettings(user auth.User, settings Settings) error {
path := filepath.Join(data_path, user.Username)
func UpdateFromReader(user auth.User, reader io.Reader) error {
mux.Lock()
defer mux.Unlock()
existingProfile, err := Load(user)
if err != nil && err != missingProfileErr {
return err
}

// Create user directory if it doesn't exist
if err := json.NewDecoder(reader).Decode(&existingProfile); err != nil {
return err
}

return Save(user, existingProfile)
}

func Save(user auth.User, profile Profile) error {
path := filepath.Join(dataPath, user.Username)
if _, err := os.Stat(path); os.IsNotExist(err) {
if err := os.Mkdir(path, 0755); err != nil {
return err
}
}

settings_path := filepath.Join(path, "settings.json")

data, err := json.MarshalIndent(settings, "", " ")
filePath := filepath.Join(path, profileFilename)
data, err := json.MarshalIndent(profile, "", " ")

if err != nil {
return err
}

f, err := os.Create(settings_path)
f, err := os.Create(filePath)
if err != nil {
return err
}
Expand All @@ -76,24 +104,24 @@ func SaveUserSettings(user auth.User, settings Settings) error {
return f.Sync()
}

func LoadUserSettings(user auth.User) (Settings, error) {
path := filepath.Join(data_path, user.Username)
settings_path := filepath.Join(path, "settings.json")
func Load(user auth.User) (Profile, error) {
path := filepath.Join(dataPath, user.Username)
profilePath := filepath.Join(path, profileFilename)

if _, err := os.Stat(settings_path); os.IsNotExist(err) {
return Settings{}, errors.New("Settings file does not exist")
if _, err := os.Stat(profilePath); os.IsNotExist(err) {
return Profile{}, missingProfileErr
}

f, err := os.Open(settings_path)
f, err := os.Open(profilePath)
if err != nil {
return Settings{}, err
return Profile{}, err
}
defer f.Close()

var settings Settings
if err := json.NewDecoder(f).Decode(&settings); err != nil {
return Settings{}, err
var profile Profile
if err := json.NewDecoder(f).Decode(&profile); err != nil {
return Profile{}, err
}

return settings, nil
return profile, nil
}
6 changes: 3 additions & 3 deletions internal/web/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {

user := auth.UserFromContext(req.Context())
if user != nil {
if settings, err := profile.LoadUserSettings(*user); err == nil {
config["serverSettings"] = settings
if profile, err := profile.Load(*user); err == nil {
config["profile"] = profile
} else {
config["serverSettings"] = struct{}{}
config["profile"] = struct{}{}
}
config["user"] = user
} else if h.config.Authorization.Provider == FORWARD_PROXY {
Expand Down
11 changes: 2 additions & 9 deletions internal/web/profile.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
package web

import (
"encoding/json"
"net/http"

"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/profile"
log "github.com/sirupsen/logrus"
)

func (h *handler) saveSettings(w http.ResponseWriter, r *http.Request) {
var settings profile.Settings
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

func (h *handler) updateProfile(w http.ResponseWriter, r *http.Request) {
user := auth.UserFromContext(r.Context())
if user == nil {
http.Error(w, "Unable to find user", http.StatusInternalServerError)
return
}

if err := profile.SaveUserSettings(*user, settings); err != nil {
if err := profile.UpdateFromReader(*user, r.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Errorf("Unable to save user settings: %s", err)
return
Expand Down
2 changes: 1 addition & 1 deletion internal/web/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func createRouter(h *handler) *chi.Mux {
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
r.Get("/api/events/stream", h.streamEvents)
r.Get("/api/releases", h.releases)
r.Put("/api/profile/settings", h.saveSettings)
r.Patch("/api/profile", h.updateProfile)
r.Get("/api/content/{id}", h.staticContent)
r.Get("/logout", h.clearSession) // TODO remove this
r.Get("/version", h.version)
Expand Down

0 comments on commit 60650dd

Please sign in to comment.