Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 52c73a7

Browse files
committed
Add developer tool to explore and edit settings
1 parent a581c36 commit 52c73a7

File tree

4 files changed

+357
-1
lines changed

4 files changed

+357
-1
lines changed

res/css/views/dialogs/_DevtoolsDialog.scss

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,54 @@ limitations under the License.
223223
content: ":";
224224
}
225225
}
226+
227+
.mx_DevTools_SettingsExplorer {
228+
table {
229+
width: 100%;
230+
table-layout: fixed;
231+
border-collapse: collapse;
232+
233+
th {
234+
// Colour choice: first one autocomplete gave me.
235+
border-bottom: 1px solid $accent-color;
236+
text-align: left;
237+
}
238+
239+
td, th {
240+
width: 360px; // "feels right" number
241+
242+
text-overflow: ellipsis;
243+
overflow: hidden;
244+
white-space: nowrap;
245+
}
246+
247+
td+td, th+th {
248+
width: auto;
249+
}
250+
251+
tr:hover {
252+
// Colour choice: first one autocomplete gave me.
253+
background-color: $accent-color-50pct;
254+
}
255+
}
256+
257+
.mx_DevTools_SettingsExplorer_mutable {
258+
background-color: $accent-color;
259+
}
260+
261+
.mx_DevTools_SettingsExplorer_immutable {
262+
background-color: $warning-color;
263+
}
264+
265+
.mx_DevTools_SettingsExplorer_edit {
266+
float: right;
267+
margin-right: 16px;
268+
}
269+
270+
.mx_DevTools_SettingsExplorer_warning {
271+
border: 2px solid $warning-color;
272+
border-radius: 4px;
273+
padding: 4px;
274+
margin-bottom: 8px;
275+
}
276+
}

src/components/views/dialogs/DevtoolsDialog.js

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ import {
3434
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
3535
import WidgetStore from "../../../stores/WidgetStore";
3636
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
37+
import {SETTINGS} from "../../../settings/Settings";
38+
import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
39+
import Modal from "../../../Modal";
40+
import ErrorDialog from "./ErrorDialog";
3741

3842
class GenericEditor extends React.PureComponent {
3943
// static propTypes = {onBack: PropTypes.func.isRequired};
@@ -794,6 +798,286 @@ class WidgetExplorer extends React.Component {
794798
}
795799
}
796800

801+
class SettingsExplorer extends React.Component {
802+
static getLabel() {
803+
return _t("Settings Explorer");
804+
}
805+
806+
constructor(props) {
807+
super(props);
808+
809+
this.state = {
810+
query: '',
811+
editSetting: null, // set to a setting ID when editing
812+
viewSetting: null, // set to a setting ID when exploring in detail
813+
814+
explicitValues: null, // stringified JSON for edit view
815+
explicitRoomValues: null, // stringified JSON for edit view
816+
};
817+
}
818+
819+
onQueryChange = (ev) => {
820+
this.setState({query: ev.target.value});
821+
};
822+
823+
onExplValuesEdit = (ev) => {
824+
this.setState({explicitValues: ev.target.value});
825+
};
826+
827+
onExplRoomValuesEdit = (ev) => {
828+
this.setState({explicitRoomValues: ev.target.value});
829+
};
830+
831+
onBack = () => {
832+
if (this.state.editSetting) {
833+
this.setState({editSetting: null});
834+
} else if (this.state.viewSetting) {
835+
this.setState({viewSetting: null});
836+
} else {
837+
this.props.onBack();
838+
}
839+
};
840+
841+
onViewClick = (ev, settingId) => {
842+
ev.preventDefault();
843+
this.setState({viewSetting: settingId});
844+
};
845+
846+
onEditClick = (ev, settingId) => {
847+
ev.preventDefault();
848+
this.setState({
849+
editSetting: settingId,
850+
explicitValues: this.renderExplicitSettingValues(settingId, null),
851+
explicitRoomValues: this.renderExplicitSettingValues(settingId, this.props.room.roomId),
852+
});
853+
};
854+
855+
onSaveClick = async () => {
856+
try {
857+
const settingId = this.state.editSetting;
858+
const parsedExplicit = JSON.parse(this.state.explicitValues);
859+
const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues);
860+
for (const level of Object.keys(parsedExplicit)) {
861+
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
862+
try {
863+
const val = parsedExplicit[level];
864+
await SettingsStore.setValue(settingId, null, level, val);
865+
} catch (e) {
866+
console.warn(e);
867+
}
868+
}
869+
const roomId = this.props.room.roomId;
870+
for (const level of Object.keys(parsedExplicit)) {
871+
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
872+
try {
873+
const val = parsedExplicitRoom[level];
874+
await SettingsStore.setValue(settingId, roomId, level, val);
875+
} catch (e) {
876+
console.warn(e);
877+
}
878+
}
879+
this.setState({
880+
viewSetting: settingId,
881+
editSetting: null,
882+
});
883+
} catch (e) {
884+
Modal.createTrackedDialog('Devtools - Failed to save settings', '', ErrorDialog, {
885+
title: _t("Failed to save settings"),
886+
description: e.message,
887+
});
888+
}
889+
};
890+
891+
renderSettingValue(val) {
892+
// Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
893+
const toStringTypes = ['boolean', 'number'];
894+
if (toStringTypes.includes(typeof(val))) {
895+
return val.toString();
896+
} else {
897+
return JSON.stringify(val);
898+
}
899+
}
900+
901+
renderExplicitSettingValues(setting, roomId) {
902+
const vals = {};
903+
for (const level of LEVEL_ORDER) {
904+
try {
905+
vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true);
906+
if (vals[level] === undefined) {
907+
vals[level] = null;
908+
}
909+
} catch (e) {
910+
console.warn(e);
911+
}
912+
}
913+
return JSON.stringify(vals, null, 4);
914+
}
915+
916+
renderCanEditLevel(roomId, level) {
917+
let canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
918+
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
919+
return <td className={className}><code>{canEdit.toString()}</code></td>;
920+
}
921+
922+
render() {
923+
const room = this.props.room;
924+
925+
if (!this.state.viewSetting && !this.state.editSetting) {
926+
// view all settings
927+
const allSettings = Object.keys(SETTINGS)
928+
.filter(n => this.state.query ? n.toLowerCase().includes(this.state.query.toLowerCase()) : true);
929+
return (
930+
<div>
931+
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
932+
<Field
933+
label={_t('Filter results')} autoFocus={true} size={64}
934+
type="text" autoComplete="off" value={this.state.query} onChange={this.onQueryChange}
935+
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
936+
/>
937+
<table>
938+
<thead>
939+
<tr>
940+
<th>{_t("Setting ID")}</th>
941+
<th>{_t("Value")}</th>
942+
<th>{_t("Value in this room")}</th>
943+
</tr>
944+
</thead>
945+
<tbody>
946+
{allSettings.map(i => (
947+
<tr key={i}>
948+
<td>
949+
<a href="" onClick={(e) => this.onViewClick(e, i)}>
950+
<code>{i}</code>
951+
</a>
952+
<a href="" onClick={(e) => this.onEditClick(e, i)}
953+
className='mx_DevTools_SettingsExplorer_edit'
954+
>
955+
956+
</a>
957+
</td>
958+
<td>
959+
<code>{this.renderSettingValue(SettingsStore.getValue(i))}</code>
960+
</td>
961+
<td>
962+
<code>
963+
{this.renderSettingValue(SettingsStore.getValue(i, room.roomId))}
964+
</code>
965+
</td>
966+
</tr>
967+
))}
968+
</tbody>
969+
</table>
970+
</div>
971+
<div className="mx_Dialog_buttons">
972+
<button onClick={this.onBack}>{_t("Back")}</button>
973+
</div>
974+
</div>
975+
);
976+
} else if (this.state.editSetting) {
977+
return (
978+
<div>
979+
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
980+
<h3>{_t("Setting:")} <code>{this.state.editSetting}</code></h3>
981+
982+
<div className='mx_DevTools_SettingsExplorer_warning'>
983+
<b>{_t("Caution:")}</b> {_t(
984+
"This UI does NOT check the types of the values. Use at your own risk.",
985+
)}
986+
</div>
987+
988+
<div>
989+
{_t("Setting definition:")}
990+
<pre><code>{JSON.stringify(SETTINGS[this.state.editSetting], null, 4)}</code></pre>
991+
</div>
992+
993+
<div>
994+
<table>
995+
<thead>
996+
<tr>
997+
<th>{_t("Level")}</th>
998+
<th>{_t("Settable at global")}</th>
999+
<th>{_t("Settable at room")}</th>
1000+
</tr>
1001+
</thead>
1002+
<tbody>
1003+
{LEVEL_ORDER.map(lvl => (
1004+
<tr key={lvl}>
1005+
<td><code>{lvl}</code></td>
1006+
{this.renderCanEditLevel(null, lvl)}
1007+
{this.renderCanEditLevel(room.roomId, lvl)}
1008+
</tr>
1009+
))}
1010+
</tbody>
1011+
</table>
1012+
</div>
1013+
1014+
<div>
1015+
<Field
1016+
id="valExpl" label={_t("Values at explicit levels")} type="text"
1017+
className="mx_DevTools_textarea" element="textarea"
1018+
autoComplete="off" value={this.state.explicitValues}
1019+
onChange={this.onExplValuesEdit}
1020+
/>
1021+
</div>
1022+
1023+
<div>
1024+
<Field
1025+
id="valExpl" label={_t("Values at explicit levels in this room")} type="text"
1026+
className="mx_DevTools_textarea" element="textarea"
1027+
autoComplete="off" value={this.state.explicitRoomValues}
1028+
onChange={this.onExplRoomValuesEdit}
1029+
/>
1030+
</div>
1031+
1032+
</div>
1033+
<div className="mx_Dialog_buttons">
1034+
<button onClick={this.onSaveClick}>{_t("Save setting values")}</button>
1035+
<button onClick={this.onBack}>{_t("Back")}</button>
1036+
</div>
1037+
</div>
1038+
);
1039+
} else if (this.state.viewSetting) {
1040+
return (
1041+
<div>
1042+
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
1043+
<h3>{_t("Setting:")} <code>{this.state.viewSetting}</code></h3>
1044+
1045+
<div>
1046+
{_t("Setting definition:")}
1047+
<pre><code>{JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}</code></pre>
1048+
</div>
1049+
1050+
<div>
1051+
{_t("Value:")}&nbsp;
1052+
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}</code>
1053+
</div>
1054+
1055+
<div>
1056+
{_t("Value in this room:")}&nbsp;
1057+
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))}</code>
1058+
</div>
1059+
1060+
<div>
1061+
{_t("Values at explicit levels:")}
1062+
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, null)}</code></pre>
1063+
</div>
1064+
1065+
<div>
1066+
{_t("Values at explicit levels in this room:")}
1067+
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}</code></pre>
1068+
</div>
1069+
1070+
</div>
1071+
<div className="mx_Dialog_buttons">
1072+
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{_t("Edit Values")}</button>
1073+
<button onClick={this.onBack}>{_t("Back")}</button>
1074+
</div>
1075+
</div>
1076+
);
1077+
}
1078+
}
1079+
}
1080+
7971081
const Entries = [
7981082
SendCustomEvent,
7991083
RoomStateExplorer,
@@ -802,6 +1086,7 @@ const Entries = [
8021086
ServersInRoomList,
8031087
VerificationExplorer,
8041088
WidgetExplorer,
1089+
SettingsExplorer,
8051090
];
8061091

8071092
export default class DevtoolsDialog extends React.PureComponent {

src/i18n/strings/en_EN.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,6 +2068,26 @@
20682068
"Verification Requests": "Verification Requests",
20692069
"Active Widgets": "Active Widgets",
20702070
"There was an error finding this widget.": "There was an error finding this widget.",
2071+
"Settings Explorer": "Settings Explorer",
2072+
"Failed to save settings": "Failed to save settings",
2073+
"Setting ID": "Setting ID",
2074+
"Value": "Value",
2075+
"Value in this room": "Value in this room",
2076+
"Setting:": "Setting:",
2077+
"Caution:": "Caution:",
2078+
"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.",
2079+
"Setting definition:": "Setting definition:",
2080+
"Level": "Level",
2081+
"Settable at global": "Settable at global",
2082+
"Settable at room": "Settable at room",
2083+
"Values at explicit levels": "Values at explicit levels",
2084+
"Values at explicit levels in this room": "Values at explicit levels in this room",
2085+
"Save setting values": "Save setting values",
2086+
"Value:": "Value:",
2087+
"Value in this room:": "Value in this room:",
2088+
"Values at explicit levels:": "Values at explicit levels:",
2089+
"Values at explicit levels in this room:": "Values at explicit levels in this room:",
2090+
"Edit Values": "Edit Values",
20712091
"Toolbox": "Toolbox",
20722092
"Developer Tools": "Developer Tools",
20732093
"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.",

src/settings/SettingsStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ for (const key of Object.keys(LEVEL_HANDLERS)) {
6161
LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]);
6262
}
6363

64-
const LEVEL_ORDER = [
64+
export const LEVEL_ORDER = [
6565
SettingLevel.DEVICE,
6666
SettingLevel.ROOM_DEVICE,
6767
SettingLevel.ROOM_ACCOUNT,

0 commit comments

Comments
 (0)