dashboard/shell/store/prefs.js

542 lines
15 KiB
JavaScript

import { SETTING } from '@shell/config/settings';
import { MANAGEMENT, STEVE } from '@shell/config/types';
import { clone } from '@shell/utils/object';
const definitions = {};
/**
* Key/value of prefrences are stored before login here and cookies due lack of access permission.
* Once user is logged in while setting asUserPreference, update stored before login Key/value to the backend in loadServer function.
*/
let prefsBeforeLogin = {};
export const create = function(name, def, opt = {}) {
const parseJSON = opt.parseJSON === true;
const asCookie = opt.asCookie === true;
const asUserPreference = opt.asUserPreference !== false;
const options = opt.options;
const inheritFrom = opt.inheritFrom;
definitions[name] = {
def,
options,
parseJSON,
asCookie,
asUserPreference,
inheritFrom, // if value is not defined on server, we can default it to another pref
mangleRead: opt.mangleRead, // Alter the value read from the API (to match old Rancher expectations)
mangleWrite: opt.mangleWrite, // Alter the value written back to the API (ditto)
};
return name;
};
export const mapPref = function(name) {
return {
get() {
return this.$store.getters['prefs/get'](name);
},
set(value) {
this.$store.dispatch('prefs/set', { key: name, value });
}
};
};
// --------------------
const parseJSON = true; // Shortcut for setting it below
const asCookie = true; // Store as a cookie so that it's available before auth + on server-side
// Keys must be lowercase and valid dns label (a-z 0-9 -)
export const CLUSTER = create('cluster', '');
export const LAST_NAMESPACE = create('last-namespace', '');
export const NAMESPACE_FILTERS = create('ns-by-cluster', {}, { parseJSON });
export const WORKSPACE = create('workspace', '');
export const EXPANDED_GROUPS = create('open-groups', ['cluster', 'policy', 'rbac', 'serviceDiscovery', 'storage', 'workload'], { parseJSON });
export const FAVORITE_TYPES = create('fav-type', [], { parseJSON });
export const PINNED_CLUSTERS = create('pinned-clusters', [], { parseJSON });
export const GROUP_RESOURCES = create('group-by', 'namespace');
export const DIFF = create('diff', 'unified', { options: ['unified', 'split'] });
export const THEME = create('theme', 'auto', {
options: ['light', 'auto', 'dark'],
asCookie,
parseJSON,
mangleRead: (x) => x.replace(/^ui-/, ''),
mangleWrite: (x) => `ui-${ x }`,
});
export const PREFERS_SCHEME = create('pcs', '', { asCookie, asUserPreference: false });
export const LOCALE = create('locale', 'en-us', { asCookie });
export const KEYMAP = create('keymap', 'sublime', { options: ['sublime', 'emacs', 'vim'] });
export const ROWS_PER_PAGE = create('per-page', 100, { options: [10, 25, 50, 100], parseJSON });
export const LOGS_WRAP = create('logs-wrap', true, { parseJSON });
export const LOGS_TIME = create('logs-time', true, { parseJSON });
export const LOGS_RANGE = create('logs-range', '30 minutes', { parseJSON });
export const HIDE_REPOS = create('hide-repos', [], { parseJSON });
export const HIDE_DESC = create('hide-desc', [], { parseJSON });
export const HIDE_SENSITIVE = create('hide-sensitive', true, { options: [true, false], parseJSON });
export const SHOW_PRE_RELEASE = create('show-pre-release', false, { options: [false, true], parseJSON });
export const DATE_FORMAT = create('date-format', 'ddd, MMM D YYYY', {
options: [
'ddd, MMM D YYYY',
'ddd, D MMM YYYY',
'D/M/YYYY',
'M/D/YYYY',
'YYYY-MM-DD'
]
});
export const TIME_FORMAT = create('time-format', 'h:mm:ss a', {
options: [
'h:mm:ss a',
'HH:mm:ss'
]
});
export const TIME_ZONE = create('time-zone', 'local');
// DEV will be deprecated on v2.7.0, but is needed so that we can grab the value for the new settings that derived from it
// such as: VIEW_IN_API, ALL_NAMESPACES, THEME_SHORTCUT
export const DEV = create('dev', false, { parseJSON });
export const VIEW_IN_API = create('view-in-api', false, { parseJSON, inheritFrom: DEV });
export const ALL_NAMESPACES = create('all-namespaces', false, { parseJSON, inheritFrom: DEV });
export const THEME_SHORTCUT = create('theme-shortcut', false, { parseJSON, inheritFrom: DEV });
export const LAST_VISITED = create('last-visited', 'home', { parseJSON });
export const SEEN_WHATS_NEW = create('seen-whatsnew', '', { parseJSON });
export const READ_WHATS_NEW = create('read-whatsnew', '', { parseJSON });
export const AFTER_LOGIN_ROUTE = create('after-login-route', 'home', { parseJSON } );
export const HIDE_HOME_PAGE_CARDS = create('home-page-cards', {}, { parseJSON } );
export const PLUGIN_DEVELOPER = create('plugin-developer', false, { parseJSON, inheritFrom: DEV }); // Is the user a plugin developer?
export const _RKE1 = 'rke1';
export const _RKE2 = 'rke2';
export const PROVISIONER = create('provisioner', _RKE2, { options: [_RKE1, _RKE2] });
// Maximum number of clusters to show in the slide-in menu
export const MENU_MAX_CLUSTERS = 10;
// Prompt for confirm when scaling down node pool in GUI and save the pref
export const SCALE_POOL_PROMPT = create('scale-pool-prompt', null, { parseJSON });
// Dynamic content
export const READ_NEW_RELEASE = create('read-new-release', '', { parseJSON });
export const READ_SUPPORT_NOTICE = create('read-support-notice', '', { parseJSON });
export const READ_UPCOMING_SUPPORT_NOTICE = create('read-upcoming-support-notice', '', { parseJSON });
// --------------------
const cookiePrefix = 'R_';
const cookieOptions = {
maxAge: 365 * 86400,
path: '/',
sameSite: true,
secure: true,
};
export const state = function() {
return {
cookiesLoaded: false,
data: {},
definitions,
authRedirect: null
};
};
export const getters = {
get: (state) => (key) => {
const definition = state.definitions[key];
if (!definition) {
throw new Error(`Unknown preference: ${ key }`);
}
const user = state.data[key];
if (user !== undefined) {
return clone(user);
}
const def = clone(definition.def);
return def;
},
defaultValue: (state) => (key) => {
const definition = state.definitions[key];
if (!definition) {
throw new Error(`Unknown preference: ${ key }`);
}
return clone(definition.def);
},
options: (state) => (key) => {
const definition = state.definitions[key];
if (!definition) {
throw new Error(`Unknown preference: ${ key }`);
}
if (!definition.options) {
throw new Error(`Preference does not have options: ${ key }`);
}
return definition.options.slice();
},
theme: (state, getters, rootState, rootGetters) => {
const setting = rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.THEME);
if (setting?.value) {
return setting?.value;
}
let theme = getters['get'](THEME);
const pcs = getters['get'](PREFERS_SCHEME);
// console.log('Get Theme', theme, pcs);
// Ember UI uses this prefix
if ( theme.startsWith('ui-') ) {
theme = theme.substr(3);
}
if ( theme === 'auto' ) {
if ( pcs === 'light' || pcs === 'dark' ) {
return pcs;
}
return 'dark';
}
return theme;
},
afterLoginRoute: (state, getters) => {
const afterLoginRoutePref = getters['get'](AFTER_LOGIN_ROUTE);
if (typeof afterLoginRoutePref !== 'string') {
return afterLoginRoutePref;
}
switch (true) {
case (afterLoginRoutePref === 'home'):
return { name: 'home' };
case (afterLoginRoutePref === 'last-visited'): {
if (state.authRedirect) {
return state.authRedirect;
}
const lastVisitedPref = getters['get'](LAST_VISITED);
if (lastVisitedPref) {
return lastVisitedPref;
}
const clusterPref = getters['get'](CLUSTER);
return { name: 'c-cluster-explorer', params: { product: 'explorer', cluster: clusterPref } };
}
case (!!afterLoginRoutePref.match(/.+-dashboard$/)):
{
const clusterId = afterLoginRoutePref.split('-dashboard')[0];
return { name: 'c-cluster-explorer', params: { product: 'explorer', cluster: clusterId } };
}
default:
return { name: afterLoginRoutePref };
}
},
dev: (state, getters) => {
try {
return getters['get'](PLUGIN_DEVELOPER);
} catch {
return getters['get'](DEV);
}
}
};
export const mutations = {
load(state, { key, value }) {
state.data[key] = value;
},
cookiesLoaded(state) {
state.cookiesLoaded = true;
},
reset(state) {
for (const key in state.definitions) {
if ( state.definitions[key]?.asCookie ) {
continue;
}
delete state.data[key];
}
},
setDefinition(state, { name, definition = {} }) {
state.definitions[name] = definition;
},
setAuthRedirect(state, route) {
state.authRedirect = route;
}
};
export const actions = {
async set({
dispatch, commit, rootGetters, state
}, opt) {
let { key, value } = opt; // eslint-disable-line prefer-const
const definition = state.definitions[key];
let server;
if ( opt.val ) {
throw new Error('Use value, not val');
}
commit('load', { key, value });
if ( definition.asCookie ) {
const options = {
...cookieOptions,
parseJSON: definition.parseJSON === true
};
const computedKey = `${ cookiePrefix }${ key }`.toUpperCase();
commit('cookies/set', {
key: computedKey, value, options
}, { root: true });
}
if ( definition.asUserPreference ) {
const checkLogin = rootGetters['auth/loggedIn'];
// Check for login status
if (!checkLogin) {
prefsBeforeLogin[key] = value;
return;
}
try {
server = await dispatch('loadServer', key); // There's no watch on prefs, so get before set...
if ( server?.data ) {
if ( definition.mangleWrite ) {
value = definition.mangleWrite(value);
}
if ( definition.parseJSON ) {
server.data[key] = JSON.stringify(value);
} else {
server.data[key] = value;
}
await server.save({ redirectUnauthorized: false });
}
} catch (e) {
// Well it failed, but not much to do about it...
// Return the error
return { type: e.type, status: e.status };
}
}
},
async setTheme({ dispatch }, val) {
await dispatch('set', { key: THEME, value: val });
},
loadCookies({ state, commit, rootGetters }) {
if ( state.cookiesLoaded ) {
return;
}
for (const key in state.definitions) {
const definition = state.definitions[key];
if ( !definition.asCookie ) {
continue;
}
const options = { parseJSON: definition.parseJSON === true };
const computedKey = `${ cookiePrefix }${ key }`.toUpperCase();
const value = rootGetters['cookies/get']({ key: computedKey, options });
if (value !== undefined) {
commit('load', { key, value });
}
}
commit('cookiesLoaded');
},
loadTheme({ dispatch }) {
const watchDark = window.matchMedia('(prefers-color-scheme: dark)');
const watchLight = window.matchMedia('(prefers-color-scheme: light)');
const watchNone = window.matchMedia('(prefers-color-scheme: no-preference)');
const interval = 30 * 60 * 1000;
const nextHalfHour = interval - Math.round(new Date().getTime()) % interval;
setTimeout(() => {
dispatch('loadTheme');
}, nextHalfHour);
// console.log('Update theme in', nextHalfHour, 'ms');
if ( watchDark.matches ) {
changed('dark');
} else if ( watchLight.matches ) {
changed('light');
} else {
changed(fromClock());
}
watchDark.addListener((e) => {
if ( e.matches ) {
changed('dark');
}
});
watchLight.addListener((e) => {
if ( e.matches ) {
changed('light');
}
});
watchNone.addListener((e) => {
if ( e.matches ) {
changed(fromClock());
}
});
function changed(value) {
// console.log('Prefers Theme:', value);
dispatch('set', { key: PREFERS_SCHEME, value });
}
function fromClock() {
const hour = new Date().getHours();
if ( hour < 7 || hour >= 18 ) {
return 'dark';
}
return 'light';
}
},
async loadServer( {
state, dispatch, commit, rootState, rootGetters
}, ignoreKey) {
let server = { data: {} };
try {
const all = await dispatch('management/findAll', {
type: STEVE.PREFERENCE,
opt: {
url: 'userpreferences',
force: true,
watch: false,
redirectUnauthorized: false,
stream: false,
}
}, { root: true });
server = all?.[0];
} catch (e) {
console.error('Error loading preferences', e); // eslint-disable-line no-console
return;
}
if ( !server?.data ) {
return;
}
// if prefsBeforeLogin has values from login page, update the backend
if (Object.keys(prefsBeforeLogin).length > 0) {
Object.keys(prefsBeforeLogin).forEach((key) => {
server.data[key] = prefsBeforeLogin[key];
});
await server.save({ redirectUnauthorized: false });
// Clear prefsBeforeLogin, as we have now saved theses
prefsBeforeLogin = {};
}
for (const key in state.definitions) {
const definition = state.definitions[key];
let value = clone(server.data[key]);
if (value === undefined && definition.inheritFrom) {
value = clone(server.data[definition.inheritFrom]);
}
if ( value === undefined || key === ignoreKey) {
continue;
}
if ( definition.parseJSON ) {
try {
value = JSON.parse(value);
} catch (err) {
console.error('Error parsing server pref', key, value, err); // eslint-disable-line no-console
continue;
}
}
if ( definition.mangleRead ) {
value = definition.mangleRead(value);
}
commit('load', { key, value });
}
return server;
},
setLastVisited({ state, dispatch, getters }, route) {
if (!route) {
return;
}
// Only save the last visited page if the user has that set as the login route preference
const afterLoginRoutePref = getters['get'](AFTER_LOGIN_ROUTE);
const doNotTrackLastVisited = typeof afterLoginRoutePref !== 'string' || afterLoginRoutePref !== 'last-visited';
if (doNotTrackLastVisited) {
return;
}
return dispatch('set', { key: LAST_VISITED, value: route });
},
toggleTheme({ getters, dispatch }) {
const value = getters[THEME] === 'light' ? 'dark' : 'light';
return dispatch('set', { key: THEME, value });
},
setBrandStyle({ rootState, rootGetters }, dark = false) {
if (rootState.managementReady) {
try {
const brandSetting = rootGetters['management/byId'](MANAGEMENT.SETTING, SETTING.BRAND);
if (brandSetting && brandSetting.value && brandSetting.value !== '') {
const brand = brandSetting.value;
const brandMeta = require(`~shell/assets/brand/${ brand }/metadata.json`);
const hasStylesheet = brandMeta.hasStylesheet === 'true';
if (hasStylesheet) {
document.body.classList.add(brand);
} else {
// TODO option apply color at runtime
}
}
} catch {}
}
}
};