dashy/src/store.js

465 lines
17 KiB
JavaScript

/* eslint-disable no-param-reassign, prefer-destructuring */
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
import yaml from 'js-yaml';
import Keys from '@/utils/StoreMutations';
import { makePageName, formatConfigPath, componentVisibility } from '@/utils/ConfigHelpers';
import { applyItemId } from '@/utils/SectionHelpers';
import filterUserSections from '@/utils/CheckSectionVisibility';
import ErrorHandler, { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
import { isUserAdmin, makeBasicAuthHeaders, isLoggedInAsGuest } from '@/utils/Auth';
import { localStorageKeys, theme as defaultTheme } from './utils/defaults';
Vue.use(Vuex);
const {
INITIALIZE_CONFIG,
INITIALIZE_ROOT_CONFIG,
SET_CONFIG,
SET_ROOT_CONFIG,
SET_CURRENT_CONFIG_INFO,
SET_IS_USING_LOCAL_CONFIG,
SET_MODAL_OPEN,
SET_LANGUAGE,
SET_ITEM_LAYOUT,
SET_ITEM_SIZE,
SET_THEME,
SET_CUSTOM_COLORS,
UPDATE_ITEM,
USE_MAIN_CONFIG,
SET_EDIT_MODE,
SET_PAGE_INFO,
SET_APP_CONFIG,
SET_SECTIONS,
SET_PAGES,
UPDATE_SECTION,
INSERT_SECTION,
REMOVE_SECTION,
COPY_ITEM,
REMOVE_ITEM,
INSERT_ITEM,
UPDATE_CUSTOM_CSS,
CONF_MENU_INDEX,
CRITICAL_ERROR_MSG,
} = Keys;
const emptyConfig = {
appConfig: {},
pageInfo: { title: 'Dashy' },
sections: [],
};
const store = new Vuex.Store({
state: {
config: {}, // The current config being used, and rendered to the UI
rootConfig: null, // Always the content of main config file, never used directly
editMode: false, // While true, the user can drag and edit items + sections
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
currentConfigInfo: {}, // For multi-page support, will store info about config file
isUsingLocalConfig: false, // If true, will use local config instead of fetched
criticalError: null, // Will store a message, if a critical error occurs
navigateConfToTab: undefined, // Used to switch active tab in config modal
},
getters: {
config(state) {
return state.config;
},
pageInfo(state) {
if (!state.config) return {};
return state.config.pageInfo || {};
},
appConfig(state) {
if (!state.config) return {};
return state.config.appConfig || {};
},
sections(state) {
return filterUserSections(state.config.sections || []);
},
pages(state) {
return state.config.pages || [];
},
theme(state) {
const localStorageKey = state.currentConfigInfo.confId
? `${localStorageKeys.THEME}-${state.currentConfigInfo.confId}` : localStorageKeys.THEME;
const localTheme = localStorage[localStorageKey];
// Return either theme from local storage, or from appConfig
return localTheme || state.config.appConfig.theme || defaultTheme;
},
webSearch(state, getters) {
return getters.appConfig.webSearch || {};
},
visibleComponents(state, getters) {
return componentVisibility(getters.appConfig);
},
/* Make config read/ write permissions object */
permissions(state, getters) {
const appConfig = getters.appConfig;
const perms = {
allowWriteToDisk: true,
allowSaveLocally: true,
allowViewConfig: true,
};
// Disable saving changes locally, only
if (appConfig.preventLocalSave) {
perms.allowSaveLocally = false;
}
// Disable saving changes to disk, only
if (appConfig.preventWriteToDisk || !isUserAdmin()) {
perms.allowWriteToDisk = false;
}
// Legacy Option: Will be removed in V 2.1.0
if (appConfig.allowConfigEdit === false) {
perms.allowWriteToDisk = false;
}
// Disable everything
if (appConfig.disableConfiguration
|| (appConfig.disableConfigurationForNonAdmin && !isUserAdmin())
|| isLoggedInAsGuest()) {
perms.allowWriteToDisk = false;
perms.allowSaveLocally = false;
perms.allowViewConfig = false;
}
return perms;
},
// eslint-disable-next-line arrow-body-style
getSectionByIndex: (state, getters) => (index) => {
return getters.sections[index];
},
getItemById: (state, getters) => (id) => {
let item;
getters.sections.forEach(sec => {
if (sec.items) {
const foundItem = sec.items.find((itm) => itm.id === id);
if (foundItem) item = foundItem;
}
});
return item;
},
getParentSectionOfItem: (state, getters) => (itemId) => {
let foundSection;
getters.sections.forEach((section) => {
(section.items || []).forEach((item) => {
if (item.id === itemId) foundSection = section;
});
});
return foundSection;
},
layout(state) {
const pageId = state.currentConfigInfo.confId;
const layoutStoreKey = pageId
? `${localStorageKeys.LAYOUT_ORIENTATION}-${pageId}` : localStorageKeys.LAYOUT_ORIENTATION;
const appConfigLayout = state.config.appConfig.layout;
return localStorage.getItem(layoutStoreKey) || appConfigLayout || 'auto';
},
iconSize(state) {
const pageId = state.currentConfigInfo.confId;
const sizeStoreKey = pageId
? `${localStorageKeys.ICON_SIZE}-${pageId}` : localStorageKeys.ICON_SIZE;
const appConfigSize = state.config.appConfig.iconSize;
return localStorage.getItem(sizeStoreKey) || appConfigSize || 'medium';
},
},
mutations: {
/* Set the master config */
[SET_ROOT_CONFIG](state, config) {
if (!config.appConfig) config.appConfig = {};
state.config = config;
},
/* The config to display and edit. Will differ from ROOT_CONFIG when using multi-page */
[SET_CONFIG](state, config) {
if (!config.appConfig) config.appConfig = {};
state.config = config;
},
[SET_CURRENT_CONFIG_INFO](state, subConfigInfo) {
state.currentConfigInfo = subConfigInfo;
},
[SET_IS_USING_LOCAL_CONFIG](state, isUsingLocalConfig) {
state.isUsingLocalConfig = isUsingLocalConfig;
},
[SET_LANGUAGE](state, lang) {
const newConfig = state.config;
newConfig.appConfig.language = lang;
state.config = newConfig;
},
[SET_MODAL_OPEN](state, modalOpen) {
state.modalOpen = modalOpen;
},
[SET_EDIT_MODE](state, editMode) {
if (editMode !== state.editMode) {
InfoHandler(editMode ? 'Edit session started' : 'Edit session ended', InfoKeys.EDITOR);
state.editMode = editMode;
}
},
[CRITICAL_ERROR_MSG](state, message) {
if (message) ErrorHandler(message);
state.criticalError = message;
},
[UPDATE_ITEM](state, payload) {
const { itemId, newItem } = payload;
const newConfig = { ...state.config };
newConfig.sections.forEach((section, secIndex) => {
(section.items || []).forEach((item, itemIndex) => {
if (item.id === itemId) {
newConfig.sections[secIndex].items[itemIndex] = newItem;
InfoHandler('Item updated', InfoKeys.EDITOR);
}
});
});
state.config = newConfig;
},
[SET_PAGE_INFO](state, newPageInfo) {
const newConfig = state.config;
newConfig.pageInfo = newPageInfo;
state.config = newConfig;
InfoHandler('Page info updated', InfoKeys.EDITOR);
},
[SET_APP_CONFIG](state, newAppConfig) {
const newConfig = state.config;
newConfig.appConfig = newAppConfig;
state.config = newConfig;
InfoHandler('App config updated', InfoKeys.EDITOR);
},
[SET_PAGES](state, multiPages) {
const newConfig = state.config;
newConfig.pages = multiPages;
state.config = newConfig;
InfoHandler('Pages updated', InfoKeys.EDITOR);
},
[SET_SECTIONS](state, newSections) {
const newConfig = state.config;
newConfig.sections = newSections;
state.config = newConfig;
InfoHandler('Sections updated', InfoKeys.EDITOR);
},
[UPDATE_SECTION](state, payload) {
const { sectionIndex, sectionData } = payload;
const newConfig = { ...state.config };
newConfig.sections[sectionIndex] = sectionData;
state.config = newConfig;
InfoHandler('Section updated', InfoKeys.EDITOR);
},
[INSERT_SECTION](state, newSection) {
const newConfig = { ...state.config };
newSection.items = [];
newConfig.sections.push(newSection);
state.config = newConfig;
InfoHandler('New section added', InfoKeys.EDITOR);
},
[REMOVE_SECTION](state, payload) {
const { sectionIndex, sectionName } = payload;
const newConfig = { ...state.config };
if (newConfig.sections[sectionIndex].name === sectionName) {
newConfig.sections.splice(sectionIndex, 1);
InfoHandler('Section removed', InfoKeys.EDITOR);
}
state.config = newConfig;
},
[INSERT_ITEM](state, payload) {
const { newItem, targetSection } = payload;
const config = { ...state.config };
config.sections.forEach((section) => {
if (section.name === targetSection) {
if (!section.items) section.items = [];
section.items.push(newItem);
InfoHandler('New item added', InfoKeys.EDITOR);
}
});
config.sections = applyItemId(config.sections);
state.config = config;
},
[COPY_ITEM](state, payload) {
const { item, toSection, appendTo } = payload;
const config = { ...state.config };
const newItem = { ...item };
config.sections.forEach((section) => {
if (section.name === toSection) {
if (!section.items) section.items = [];
if (appendTo === 'beginning') {
section.items.unshift(newItem);
} else {
section.items.push(newItem);
}
InfoHandler('Item copied', InfoKeys.EDITOR);
}
});
config.sections = applyItemId(config.sections);
state.config = config;
},
[REMOVE_ITEM](state, payload) {
const { itemId, sectionName } = payload;
const config = { ...state.config };
config.sections.forEach((section) => {
if (section.name === sectionName && section.items) {
section.items.forEach((item, index) => {
if (item.id === itemId) {
section.items.splice(index, 1);
InfoHandler('Item removed', InfoKeys.EDITOR);
}
});
}
});
config.sections = applyItemId(config.sections);
state.config = config;
},
[SET_THEME](state, theme) {
const newConfig = { ...state.config };
newConfig.appConfig.theme = theme;
state.config = newConfig;
const pageId = state.currentConfigInfo.confId;
const themeStoreKey = pageId
? `${localStorageKeys.THEME}-${pageId}` : localStorageKeys.THEME;
localStorage.setItem(themeStoreKey, theme);
InfoHandler('Theme updated', InfoKeys.VISUAL);
},
[SET_CUSTOM_COLORS](state, customColors) {
const newConfig = { ...state.config };
newConfig.appConfig.customColors = customColors;
state.config = newConfig;
InfoHandler('Color palette updated', InfoKeys.VISUAL);
},
[SET_ITEM_LAYOUT](state, layout) {
const newConfig = { ...state.config };
newConfig.appConfig.layout = layout;
state.config = newConfig;
const pageId = state.currentConfigInfo.confId;
const layoutStoreKey = pageId
? `${localStorageKeys.LAYOUT_ORIENTATION}-${pageId}` : localStorageKeys.LAYOUT_ORIENTATION;
localStorage.setItem(layoutStoreKey, layout);
InfoHandler('Layout updated', InfoKeys.VISUAL);
},
[SET_ITEM_SIZE](state, iconSize) {
const newConfig = { ...state.config };
newConfig.appConfig.iconSize = iconSize;
state.config = newConfig;
const pageId = state.currentConfigInfo.confId;
const sizeStoreKey = pageId
? `${localStorageKeys.ICON_SIZE}-${pageId}` : localStorageKeys.ICON_SIZE;
localStorage.setItem(sizeStoreKey, iconSize);
InfoHandler('Item size updated', InfoKeys.VISUAL);
},
[UPDATE_CUSTOM_CSS](state, customCss) {
state.config.appConfig.customCss = customCss;
InfoHandler('Custom colors updated', InfoKeys.VISUAL);
},
[CONF_MENU_INDEX](state, index) {
state.navigateConfToTab = index;
},
/* Set config to rootConfig, by calling initialize with no params */
async [USE_MAIN_CONFIG]() {
this.dispatch(Keys.INITIALIZE_CONFIG);
},
},
actions: {
/* Fetches the root config file, only ever called by INITIALIZE_CONFIG */
async [INITIALIZE_ROOT_CONFIG]({ commit }) {
const configFilePath = process.env.VUE_APP_CONFIG_PATH || '/conf.yml';
try {
// Attempt to fetch the YAML file
const response = await axios.get(configFilePath, makeBasicAuthHeaders());
let data;
try {
data = yaml.load(response.data);
} catch (parseError) {
commit(CRITICAL_ERROR_MSG, `Failed to parse YAML: ${parseError.message}`);
return { ...emptyConfig };
}
// Replace missing root properties with empty objects
if (!data.appConfig) data.appConfig = {};
if (!data.pageInfo) data.pageInfo = {};
if (!data.sections) data.sections = [];
// Set the state, and return data
commit(SET_ROOT_CONFIG, data);
commit(CRITICAL_ERROR_MSG, null);
return data;
} catch (fetchError) {
if (fetchError.response) {
commit(
CRITICAL_ERROR_MSG,
'Failed to fetch configuration: Server responded with status '
+ `${fetchError.response?.status || 'mystery status'}`,
);
} else if (fetchError.request) {
commit(CRITICAL_ERROR_MSG, 'Failed to fetch configuration: No response from server');
} else {
commit(CRITICAL_ERROR_MSG, `Failed to fetch configuration: ${fetchError.message}`);
}
return { ...emptyConfig };
}
},
/**
* Fetches config and updates state
* If not on sub-page, will trigger the fetch of main config, then use that
* If using sub-page config, then fetch that sub-config, then
* override certain fields (appConfig, pages) and update config
*/
async [INITIALIZE_CONFIG]({ commit, state }, subConfigId) {
const rootConfig = state.rootConfig || await this.dispatch(Keys.INITIALIZE_ROOT_CONFIG);
commit(SET_IS_USING_LOCAL_CONFIG, false);
if (!subConfigId) { // Use root config as config
commit(SET_CONFIG, rootConfig);
commit(SET_CURRENT_CONFIG_INFO, {});
let localSections = [];
const localSectionsRaw = localStorage[localStorageKeys.CONF_SECTIONS];
if (localSectionsRaw) {
try {
const json = JSON.parse(localSectionsRaw);
if (json.length >= 1) localSections = json;
} catch (e) {
commit(CRITICAL_ERROR_MSG, 'Malformed section data in local storage');
}
}
if (localSections.length > 0) {
rootConfig.sections = localSections;
commit(SET_IS_USING_LOCAL_CONFIG, true);
}
return rootConfig;
} else {
// Find and format path to fetch sub-config from
const subConfigPath = formatConfigPath(rootConfig?.pages?.find(
(page) => makePageName(page.name) === subConfigId,
)?.path);
if (!subConfigPath) {
commit(CRITICAL_ERROR_MSG, `Unable to find config for '${subConfigId}'`);
return { ...emptyConfig };
}
axios.get(subConfigPath, makeBasicAuthHeaders()).then((response) => {
// Parse the YAML
const configContent = yaml.load(response.data) || {};
// Certain values must be inherited from root config
const theme = configContent?.appConfig?.theme || rootConfig.appConfig?.theme || 'default';
configContent.appConfig = rootConfig.appConfig;
configContent.pages = rootConfig.pages;
configContent.appConfig.theme = theme;
// Load local sections if they exist
const localSectionsRaw = localStorage[`${localStorageKeys.CONF_SECTIONS}-${subConfigId}`];
if (localSectionsRaw) {
try {
const json = JSON.parse(localSectionsRaw);
if (json.length >= 1) {
configContent.sections = json;
commit(SET_IS_USING_LOCAL_CONFIG, true);
}
} catch (e) {
commit(CRITICAL_ERROR_MSG, 'Malformed section data in local storage for sub-config');
}
}
// Set the config
commit(SET_CONFIG, configContent);
commit(SET_CURRENT_CONFIG_INFO, { confPath: subConfigPath, confId: subConfigId });
}).catch((err) => {
commit(CRITICAL_ERROR_MSG, `Unable to load config from '${subConfigPath}'`, err);
});
}
return { ...emptyConfig };
},
},
modules: {},
});
export default store;