From c05c2a723d2b8bb9c4f98861477e1eed3489d43d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:59:58 +0000 Subject: [PATCH 1/7] fix local storage js history --- src/dashboard/Data/Playground/Playground.react.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dashboard/Data/Playground/Playground.react.js b/src/dashboard/Data/Playground/Playground.react.js index 459cc684f1..8af5e953f1 100644 --- a/src/dashboard/Data/Playground/Playground.react.js +++ b/src/dashboard/Data/Playground/Playground.react.js @@ -11,6 +11,7 @@ import Icon from 'components/Icon/Icon.react'; import { CurrentApp } from 'context/currentApp'; import browserStyles from 'dashboard/Data/Browser/Browser.scss'; import Separator from 'components/BrowserMenu/Separator.react'; +import Notification from 'dashboard/Data/Browser/Notification.react'; import ScriptManager from 'lib/ScriptManager'; import styles from './Playground.scss'; @@ -187,6 +188,7 @@ export default function Playground() { const [savedTabs, setSavedTabs] = useState([]); // All saved tabs including closed ones const [, setCurrentMenu] = useState(null); // Track which menu is currently open const [, setForceUpdate] = useState({}); // Force re-render for unsaved changes detection + const [noteMessage, setNoteMessage] = useState(null); const renamingInputRef = useRef(null); // Drag and drop state @@ -992,8 +994,13 @@ export default function Playground() { if (window.localStorage) { try { window.localStorage.setItem(historyKey, JSON.stringify(newHistory)); - } catch (e) { - console.warn('Failed to save execution history:', e); + } catch { + window.localStorage.removeItem(historyKey); + try { + window.localStorage.setItem(historyKey, JSON.stringify([code])); + } catch { + setNoteMessage('Unable to save execution history: browser storage is full.'); + } } } } catch (e) { @@ -1504,6 +1511,7 @@ export default function Playground() { + ); } From b7845bd013ff0255925d88e7d41d3938c2309043 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:28:21 +0000 Subject: [PATCH 2/7] feat --- src/dashboard/Dashboard.js | 2 + src/dashboard/DashboardView.react.js | 4 + src/dashboard/Data/Config/Config.react.js | 20 +++ .../Settings/CloudConfigSettings.react.js | 152 ++++++++++++++++++ 4 files changed, 178 insertions(+) create mode 100644 src/dashboard/Settings/CloudConfigSettings.react.js diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index 19dbdf54de..b10e798fec 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -56,6 +56,7 @@ import Playground from './Data/Playground/Playground.react'; import DashboardSettings from './Settings/DashboardSettings/DashboardSettings.react'; import Security from './Settings/Security/Security.react'; import KeyboardShortcutsSettings from './Settings/KeyboardShortcutsSettings.react'; +import CloudConfigSettings from './Settings/CloudConfigSettings.react'; import semver from 'semver'; import packageInfo from '../../package.json'; @@ -237,6 +238,7 @@ export default class Dashboard extends React.Component { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js index 1240d5c901..3b9e3443c0 100644 --- a/src/dashboard/DashboardView.react.js +++ b/src/dashboard/DashboardView.react.js @@ -230,6 +230,10 @@ export default class DashboardView extends React.Component { name: 'Keyboard Shortcuts', link: '/settings/keyboard-shortcuts', }, + { + name: 'Cloud Config', + link: '/settings/cloud-config', + }, ]; if (this.context.enableSecurityChecks) { diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index f32018daf9..691ca53acf 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -27,6 +27,7 @@ import { CurrentApp } from 'context/currentApp'; import Modal from 'components/Modal/Modal.react'; import equal from 'fast-deep-equal'; import Notification from 'dashboard/Data/Browser/Notification.react'; +import ServerConfigStorage from 'lib/ServerConfigStorage'; @subscribeTo('Config', 'config') class Config extends TableView { @@ -76,11 +77,30 @@ class Config extends TableView { try { await this.props.config.dispatch(ActionTypes.FETCH); this.cacheData = new Map(this.props.config.data); + await this.loadCloudConfigSettings(); } finally { this.setState({ loading: false }); } } + async loadCloudConfigSettings() { + try { + const serverStorage = new ServerConfigStorage(this.context); + if (!serverStorage.isServerConfigEnabled()) { + return; + } + const settings = await serverStorage.getConfig( + 'config.settings', + this.context.applicationId + ); + if (settings && settings.historyLimit !== undefined) { + this.context.cloudConfigHistoryLimit = settings.historyLimit; + } + } catch { + // Fall back to existing context value + } + } + renderToolbar() { return ( diff --git a/src/dashboard/Settings/CloudConfigSettings.react.js b/src/dashboard/Settings/CloudConfigSettings.react.js new file mode 100644 index 0000000000..d80c25cc74 --- /dev/null +++ b/src/dashboard/Settings/CloudConfigSettings.react.js @@ -0,0 +1,152 @@ +import DashboardView from 'dashboard/DashboardView.react'; +import Field from 'components/Field/Field.react'; +import Fieldset from 'components/Fieldset/Fieldset.react'; +import Label from 'components/Label/Label.react'; +import React from 'react'; +import TextInput from 'components/TextInput/TextInput.react'; +import Toolbar from 'components/Toolbar/Toolbar.react'; +import Notification from 'dashboard/Data/Browser/Notification.react'; +import ServerConfigStorage from 'lib/ServerConfigStorage'; +import styles from 'dashboard/Settings/Settings.scss'; + +const CONFIG_KEY = 'config.settings'; + +export default class CloudConfigSettings extends DashboardView { + constructor() { + super(); + this.section = 'App Settings'; + this.subsection = 'Cloud Config'; + this.serverStorage = null; + + this.state = { + cloudConfigHistoryLimit: '', + message: undefined, + loading: true, + }; + } + + componentDidMount() { + if (this.context) { + this.serverStorage = new ServerConfigStorage(this.context); + this.loadSettings(); + } + } + + componentWillUnmount() { + clearTimeout(this.noteTimeout); + } + + async loadSettings() { + try { + const settings = await this.serverStorage.getConfig( + CONFIG_KEY, + this.context.applicationId + ); + if (settings && settings.historyLimit !== undefined) { + this.setState({ cloudConfigHistoryLimit: String(settings.historyLimit) }); + } + } catch (error) { + this.showNote('Failed to load Cloud Config settings.', true); + } finally { + this.setState({ loading: false }); + } + } + + handleCloudConfigHistoryLimitChange(value) { + this.setState({ cloudConfigHistoryLimit: value }); + } + + async saveCloudConfigHistoryLimit() { + const value = this.state.cloudConfigHistoryLimit.trim(); + + if (value === '') { + try { + await this.serverStorage.deleteConfig( + CONFIG_KEY, + this.context.applicationId + ); + this.context.cloudConfigHistoryLimit = undefined; + this.showNote('Cloud Config history limit reset to default.'); + } catch { + this.showNote('Failed to reset setting.', true); + } + return; + } + + const parsed = parseInt(value, 10); + if (isNaN(parsed) || parsed < 1) { + this.showNote('Please enter a valid positive number.', true); + return; + } + + try { + await this.serverStorage.setConfig( + CONFIG_KEY, + { historyLimit: parsed }, + this.context.applicationId + ); + this.context.cloudConfigHistoryLimit = parsed; + this.setState({ cloudConfigHistoryLimit: String(parsed) }); + this.showNote(`Cloud Config history limit set to ${parsed}.`); + } catch { + this.showNote('Failed to save setting.', true); + } + } + + showNote(message, isError = false) { + if (!message) { + return; + } + + clearTimeout(this.noteTimeout); + + this.setState({ message: { text: message, isError } }); + + this.noteTimeout = setTimeout(() => { + this.setState({ message: undefined }); + }, 3500); + } + + renderContent() { + const message = this.state.message; + const serverConfigEnabled = this.serverStorage && this.serverStorage.isServerConfigEnabled(); + const notAvailableMessage = !serverConfigEnabled + ? 'Server configuration is not enabled for this app. Please add a \'config\' section to your app configuration.' + : null; + + return ( +
+ + +
+
+ + } + input={ + + } + /> +
+
+
+ ); + } +} From 5f9b1771802caf134807c4b58b5fd00de7a51c64 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:34:08 +0000 Subject: [PATCH 3/7] optimize --- src/dashboard/Data/Config/Config.react.js | 123 +++++------------- .../Settings/CloudConfigSettings.react.js | 16 ++- 2 files changed, 40 insertions(+), 99 deletions(-) diff --git a/src/dashboard/Data/Config/Config.react.js b/src/dashboard/Data/Config/Config.react.js index 691ca53acf..56d02c02ea 100644 --- a/src/dashboard/Data/Config/Config.react.js +++ b/src/dashboard/Data/Config/Config.react.js @@ -54,6 +54,7 @@ class Config extends TableView { showRemoveEntryDialog: false, removeEntryParam: '', removeEntryArrayValue: [], + serverHistoryLimit: undefined, }; this.noteTimeout = null; } @@ -94,13 +95,37 @@ class Config extends TableView { this.context.applicationId ); if (settings && settings.historyLimit !== undefined) { - this.context.cloudConfigHistoryLimit = settings.historyLimit; + this.setState({ serverHistoryLimit: settings.historyLimit }); } } catch { // Fall back to existing context value } } + addToConfigHistory(name, value) { + const limit = this.context.cloudConfigHistoryLimit ?? this.state.serverHistoryLimit; + const applicationId = this.context.applicationId; + const configHistory = localStorage.getItem(`${applicationId}_configHistory`); + const newHistoryEntry = { time: new Date(), value }; + + if (!configHistory) { + localStorage.setItem( + `${applicationId}_configHistory`, + JSON.stringify({ [name]: [newHistoryEntry] }) + ); + } else { + const oldConfigHistory = JSON.parse(configHistory); + const updatedHistory = !oldConfigHistory[name] + ? [newHistoryEntry] + : [newHistoryEntry, ...oldConfigHistory[name]].slice(0, limit || 100); + + localStorage.setItem( + `${applicationId}_configHistory`, + JSON.stringify({ ...oldConfigHistory, [name]: updatedHistory }) + ); + } + } + renderToolbar() { return ( @@ -458,44 +483,14 @@ class Config extends TableView { this.setState({ modalOpen: false }); // Update config history in localStorage - const limit = this.context.cloudConfigHistoryLimit; - const applicationId = this.context.applicationId; let transformedValue = value; - if (type === 'Date') { transformedValue = { __type: 'Date', iso: value }; } if (type === 'File') { transformedValue = { name: value._name, url: value._url }; } - - const configHistory = localStorage.getItem(`${applicationId}_configHistory`); - const newHistoryEntry = { - time: new Date(), - value: transformedValue, - }; - - if (!configHistory) { - localStorage.setItem( - `${applicationId}_configHistory`, - JSON.stringify({ - [name]: [newHistoryEntry], - }) - ); - } else { - const oldConfigHistory = JSON.parse(configHistory); - const updatedHistory = !oldConfigHistory[name] - ? [newHistoryEntry] - : [newHistoryEntry, ...oldConfigHistory[name]].slice(0, limit || 100); - - localStorage.setItem( - `${applicationId}_configHistory`, - JSON.stringify({ - ...oldConfigHistory, - [name]: updatedHistory, - }) - ); - } + this.addToConfigHistory(name, transformedValue); } catch (error) { this.context.showError?.( `Failed to save parameter: ${error.message || 'Unknown error occurred'}` @@ -605,37 +600,7 @@ class Config extends TableView { await this.props.config.dispatch(ActionTypes.FETCH); // Update config history - const limit = this.context.cloudConfigHistoryLimit; - const applicationId = this.context.applicationId; - const params = this.props.config.data.get('params'); - const updatedValue = params.get(param); - const configHistory = localStorage.getItem(`${applicationId}_configHistory`); - const newHistoryEntry = { - time: new Date(), - value: updatedValue, - }; - - if (!configHistory) { - localStorage.setItem( - `${applicationId}_configHistory`, - JSON.stringify({ - [param]: [newHistoryEntry], - }) - ); - } else { - const oldConfigHistory = JSON.parse(configHistory); - const updatedHistory = !oldConfigHistory[param] - ? [newHistoryEntry] - : [newHistoryEntry, ...oldConfigHistory[param]].slice(0, limit || 100); - - localStorage.setItem( - `${applicationId}_configHistory`, - JSON.stringify({ - ...oldConfigHistory, - [param]: updatedHistory, - }) - ); - } + this.addToConfigHistory(param, this.props.config.data.get('params').get(param)); this.showNote(`Entry added to ${param}`); } catch (e) { @@ -717,37 +682,7 @@ class Config extends TableView { await this.props.config.dispatch(ActionTypes.FETCH); // Update config history - const limit = this.context.cloudConfigHistoryLimit; - const applicationId = this.context.applicationId; - const params = this.props.config.data.get('params'); - const updatedValue = params.get(param); - const configHistory = localStorage.getItem(`${applicationId}_configHistory`); - const newHistoryEntry = { - time: new Date(), - value: updatedValue, - }; - - if (!configHistory) { - localStorage.setItem( - `${applicationId}_configHistory`, - JSON.stringify({ - [param]: [newHistoryEntry], - }) - ); - } else { - const oldConfigHistory = JSON.parse(configHistory); - const updatedHistory = !oldConfigHistory[param] - ? [newHistoryEntry] - : [newHistoryEntry, ...oldConfigHistory[param]].slice(0, limit || 100); - - localStorage.setItem( - `${applicationId}_configHistory`, - JSON.stringify({ - ...oldConfigHistory, - [param]: updatedHistory, - }) - ); - } + this.addToConfigHistory(param, this.props.config.data.get('params').get(param)); const removedCount = objectsToRemove.length; const message = removedCount === 1 diff --git a/src/dashboard/Settings/CloudConfigSettings.react.js b/src/dashboard/Settings/CloudConfigSettings.react.js index d80c25cc74..83d8dcb522 100644 --- a/src/dashboard/Settings/CloudConfigSettings.react.js +++ b/src/dashboard/Settings/CloudConfigSettings.react.js @@ -65,7 +65,6 @@ export default class CloudConfigSettings extends DashboardView { CONFIG_KEY, this.context.applicationId ); - this.context.cloudConfigHistoryLimit = undefined; this.showNote('Cloud Config history limit reset to default.'); } catch { this.showNote('Failed to reset setting.', true); @@ -85,7 +84,6 @@ export default class CloudConfigSettings extends DashboardView { { historyLimit: parsed }, this.context.applicationId ); - this.context.cloudConfigHistoryLimit = parsed; this.setState({ cloudConfigHistoryLimit: String(parsed) }); this.showNote(`Cloud Config history limit set to ${parsed}.`); } catch { @@ -114,6 +112,14 @@ export default class CloudConfigSettings extends DashboardView { ? 'Server configuration is not enabled for this app. Please add a \'config\' section to your app configuration.' : null; + const configFileLimit = this.context.cloudConfigHistoryLimit; + const isOverriddenByConfigFile = configFileLimit !== undefined && configFileLimit !== null; + + let description = 'Maximum number of history entries stored per Cloud Config parameter. Leave empty to use the default (100).'; + if (isOverriddenByConfigFile) { + description = `This value is overridden by the dashboard config file (${configFileLimit}). Remove the "cloudConfigHistoryLimit" option from the config file to use this setting.`; + } + return (
@@ -131,14 +137,14 @@ export default class CloudConfigSettings extends DashboardView { label={