1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-25 23:03:47 +01:00

feat: syncs all profile localStorage to disk (#2537)

This commit is contained in:
Amir Raminfar
2023-11-27 15:57:54 -08:00
committed by GitHub
parent b54b419a08
commit 60650ddc2c
12 changed files with 115 additions and 57 deletions

View File

@@ -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']
@@ -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']>
@@ -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']>

View File

@@ -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>

View File

@@ -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);
}
},
});

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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>());

View File

@@ -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;
@@ -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 = {

View File

@@ -1,5 +1,4 @@
import { toRefs } from "@vueuse/core";
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export type Settings = {
search: boolean;
@@ -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,

View File

@@ -3,6 +3,8 @@ package profile
import (
"encoding/json"
"errors"
"io"
"sync"
"os"
"path/filepath"
@@ -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"`
@@ -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")
@@ -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
}
@@ -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
}

View File

@@ -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 {

View File

@@ -1,7 +1,6 @@
package web
import (
"encoding/json"
"net/http"
"github.com/amir20/dozzle/internal/auth"
@@ -9,20 +8,14 @@ import (
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

View File

@@ -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)