diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 35cb6bc7ab..7b4449eee0 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -223,3 +223,54 @@ limitations under the License. content: ":"; } } + +.mx_DevTools_SettingsExplorer { + table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; + + th { + // Colour choice: first one autocomplete gave me. + border-bottom: 1px solid $accent-color; + text-align: left; + } + + td, th { + width: 360px; // "feels right" number + + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + td+td, th+th { + width: auto; + } + + tr:hover { + // Colour choice: first one autocomplete gave me. + background-color: $accent-color-50pct; + } + } + + .mx_DevTools_SettingsExplorer_mutable { + background-color: $accent-color; + } + + .mx_DevTools_SettingsExplorer_immutable { + background-color: $warning-color; + } + + .mx_DevTools_SettingsExplorer_edit { + float: right; + margin-right: 16px; + } + + .mx_DevTools_SettingsExplorer_warning { + border: 2px solid $warning-color; + border-radius: 4px; + padding: 4px; + margin-bottom: 8px; + } +} diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 7d4e3b462f..5b1ed86adb 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -34,6 +34,10 @@ import { } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import WidgetStore from "../../../stores/WidgetStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import {SETTINGS} from "../../../settings/Settings"; +import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore"; +import Modal from "../../../Modal"; +import ErrorDialog from "./ErrorDialog"; class GenericEditor extends React.PureComponent { // static propTypes = {onBack: PropTypes.func.isRequired}; @@ -794,6 +798,286 @@ class WidgetExplorer extends React.Component { } } +class SettingsExplorer extends React.Component { + static getLabel() { + return _t("Settings Explorer"); + } + + constructor(props) { + super(props); + + this.state = { + query: '', + editSetting: null, // set to a setting ID when editing + viewSetting: null, // set to a setting ID when exploring in detail + + explicitValues: null, // stringified JSON for edit view + explicitRoomValues: null, // stringified JSON for edit view + }; + } + + onQueryChange = (ev) => { + this.setState({query: ev.target.value}); + }; + + onExplValuesEdit = (ev) => { + this.setState({explicitValues: ev.target.value}); + }; + + onExplRoomValuesEdit = (ev) => { + this.setState({explicitRoomValues: ev.target.value}); + }; + + onBack = () => { + if (this.state.editSetting) { + this.setState({editSetting: null}); + } else if (this.state.viewSetting) { + this.setState({viewSetting: null}); + } else { + this.props.onBack(); + } + }; + + onViewClick = (ev, settingId) => { + ev.preventDefault(); + this.setState({viewSetting: settingId}); + }; + + onEditClick = (ev, settingId) => { + ev.preventDefault(); + this.setState({ + editSetting: settingId, + explicitValues: this.renderExplicitSettingValues(settingId, null), + explicitRoomValues: this.renderExplicitSettingValues(settingId, this.props.room.roomId), + }); + }; + + onSaveClick = async () => { + try { + const settingId = this.state.editSetting; + const parsedExplicit = JSON.parse(this.state.explicitValues); + const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues); + for (const level of Object.keys(parsedExplicit)) { + console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`); + try { + const val = parsedExplicit[level]; + await SettingsStore.setValue(settingId, null, level, val); + } catch (e) { + console.warn(e); + } + } + const roomId = this.props.room.roomId; + for (const level of Object.keys(parsedExplicit)) { + console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`); + try { + const val = parsedExplicitRoom[level]; + await SettingsStore.setValue(settingId, roomId, level, val); + } catch (e) { + console.warn(e); + } + } + this.setState({ + viewSetting: settingId, + editSetting: null, + }); + } catch (e) { + Modal.createTrackedDialog('Devtools - Failed to save settings', '', ErrorDialog, { + title: _t("Failed to save settings"), + description: e.message, + }); + } + }; + + renderSettingValue(val) { + // Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us + const toStringTypes = ['boolean', 'number']; + if (toStringTypes.includes(typeof(val))) { + return val.toString(); + } else { + return JSON.stringify(val); + } + } + + renderExplicitSettingValues(setting, roomId) { + const vals = {}; + for (const level of LEVEL_ORDER) { + try { + vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true); + if (vals[level] === undefined) { + vals[level] = null; + } + } catch (e) { + console.warn(e); + } + } + return JSON.stringify(vals, null, 4); + } + + renderCanEditLevel(roomId, level) { + let canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level); + const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable'; + return {canEdit.toString()}; + } + + render() { + const room = this.props.room; + + if (!this.state.viewSetting && !this.state.editSetting) { + // view all settings + const allSettings = Object.keys(SETTINGS) + .filter(n => this.state.query ? n.toLowerCase().includes(this.state.query.toLowerCase()) : true); + return ( +
+
+ + + + + + + + + + + {allSettings.map(i => ( + + + + + + ))} + +
{_t("Setting ID")}{_t("Value")}{_t("Value in this room")}
+ this.onViewClick(e, i)}> + {i} + + this.onEditClick(e, i)} + className='mx_DevTools_SettingsExplorer_edit' + > + ✏ + + + {this.renderSettingValue(SettingsStore.getValue(i))} + + + {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} + +
+
+
+ +
+
+ ); + } else if (this.state.editSetting) { + return ( +
+
+

{_t("Setting:")} {this.state.editSetting}

+ +
+ {_t("Caution:")} {_t( + "This UI does NOT check the types of the values. Use at your own risk.", + )} +
+ +
+ {_t("Setting definition:")} +
{JSON.stringify(SETTINGS[this.state.editSetting], null, 4)}
+
+ +
+ + + + + + + + + + {LEVEL_ORDER.map(lvl => ( + + + {this.renderCanEditLevel(null, lvl)} + {this.renderCanEditLevel(room.roomId, lvl)} + + ))} + +
{_t("Level")}{_t("Settable at global")}{_t("Settable at room")}
{lvl}
+
+ +
+ +
+ +
+ +
+ +
+
+ + +
+
+ ); + } else if (this.state.viewSetting) { + return ( +
+
+

{_t("Setting:")} {this.state.viewSetting}

+ +
+ {_t("Setting definition:")} +
{JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}
+
+ +
+ {_t("Value:")}  + {this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))} +
+ +
+ {_t("Value in this room:")}  + {this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))} +
+ +
+ {_t("Values at explicit levels:")} +
{this.renderExplicitSettingValues(this.state.viewSetting, null)}
+
+ +
+ {_t("Values at explicit levels in this room:")} +
{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}
+
+ +
+
+ + +
+
+ ); + } + } +} + const Entries = [ SendCustomEvent, RoomStateExplorer, @@ -802,6 +1086,7 @@ const Entries = [ ServersInRoomList, VerificationExplorer, WidgetExplorer, + SettingsExplorer, ]; export default class DevtoolsDialog extends React.PureComponent { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5bbbdf60b5..54f0f66eb4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2068,6 +2068,26 @@ "Verification Requests": "Verification Requests", "Active Widgets": "Active Widgets", "There was an error finding this widget.": "There was an error finding this widget.", + "Settings Explorer": "Settings Explorer", + "Failed to save settings": "Failed to save settings", + "Setting ID": "Setting ID", + "Value": "Value", + "Value in this room": "Value in this room", + "Setting:": "Setting:", + "Caution:": "Caution:", + "This UI does NOT check the types of the values. Use at your own risk.": "This UI does NOT check the types of the values. Use at your own risk.", + "Setting definition:": "Setting definition:", + "Level": "Level", + "Settable at global": "Settable at global", + "Settable at room": "Settable at room", + "Values at explicit levels": "Values at explicit levels", + "Values at explicit levels in this room": "Values at explicit levels in this room", + "Save setting values": "Save setting values", + "Value:": "Value:", + "Value in this room:": "Value in this room:", + "Values at explicit levels:": "Values at explicit levels:", + "Values at explicit levels in this room:": "Values at explicit levels in this room:", + "Edit Values": "Edit Values", "Toolbox": "Toolbox", "Developer Tools": "Developer Tools", "There was an error updating your community. The server is unable to process your request.": "There was an error updating your community. The server is unable to process your request.", diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 6dc2a76ae8..c2675bd8f8 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -61,7 +61,7 @@ for (const key of Object.keys(LEVEL_HANDLERS)) { LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]); } -const LEVEL_ORDER = [ +export const LEVEL_ORDER = [ SettingLevel.DEVICE, SettingLevel.ROOM_DEVICE, SettingLevel.ROOM_ACCOUNT,