Merge pull request #5664 from matrix-org/travis/settings/v3-explorer

Add developer tool to explore and edit settings
pull/21833/head
Travis Ralston 2021-02-18 15:38:05 -07:00 committed by GitHub
commit de7e2ed37e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 357 additions and 1 deletions

View File

@ -223,3 +223,54 @@ limitations under the License.
content: ":"; 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;
}
}

View File

@ -34,6 +34,10 @@ import {
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import WidgetStore from "../../../stores/WidgetStore"; import WidgetStore from "../../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore"; 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 { class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired}; // 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) {
const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
return <td className={className}><code>{canEdit.toString()}</code></td>;
}
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 (
<div>
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
<Field
label={_t('Filter results')} autoFocus={true} size={64}
type="text" autoComplete="off" value={this.state.query} onChange={this.onQueryChange}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
/>
<table>
<thead>
<tr>
<th>{_t("Setting ID")}</th>
<th>{_t("Value")}</th>
<th>{_t("Value in this room")}</th>
</tr>
</thead>
<tbody>
{allSettings.map(i => (
<tr key={i}>
<td>
<a href="" onClick={(e) => this.onViewClick(e, i)}>
<code>{i}</code>
</a>
<a href="" onClick={(e) => this.onEditClick(e, i)}
className='mx_DevTools_SettingsExplorer_edit'
>
</a>
</td>
<td>
<code>{this.renderSettingValue(SettingsStore.getValue(i))}</code>
</td>
<td>
<code>
{this.renderSettingValue(SettingsStore.getValue(i, room.roomId))}
</code>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{_t("Back")}</button>
</div>
</div>
);
} else if (this.state.editSetting) {
return (
<div>
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
<h3>{_t("Setting:")} <code>{this.state.editSetting}</code></h3>
<div className='mx_DevTools_SettingsExplorer_warning'>
<b>{_t("Caution:")}</b> {_t(
"This UI does NOT check the types of the values. Use at your own risk.",
)}
</div>
<div>
{_t("Setting definition:")}
<pre><code>{JSON.stringify(SETTINGS[this.state.editSetting], null, 4)}</code></pre>
</div>
<div>
<table>
<thead>
<tr>
<th>{_t("Level")}</th>
<th>{_t("Settable at global")}</th>
<th>{_t("Settable at room")}</th>
</tr>
</thead>
<tbody>
{LEVEL_ORDER.map(lvl => (
<tr key={lvl}>
<td><code>{lvl}</code></td>
{this.renderCanEditLevel(null, lvl)}
{this.renderCanEditLevel(room.roomId, lvl)}
</tr>
))}
</tbody>
</table>
</div>
<div>
<Field
id="valExpl" label={_t("Values at explicit levels")} type="text"
className="mx_DevTools_textarea" element="textarea"
autoComplete="off" value={this.state.explicitValues}
onChange={this.onExplValuesEdit}
/>
</div>
<div>
<Field
id="valExpl" label={_t("Values at explicit levels in this room")} type="text"
className="mx_DevTools_textarea" element="textarea"
autoComplete="off" value={this.state.explicitRoomValues}
onChange={this.onExplRoomValuesEdit}
/>
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onSaveClick}>{_t("Save setting values")}</button>
<button onClick={this.onBack}>{_t("Back")}</button>
</div>
</div>
);
} else if (this.state.viewSetting) {
return (
<div>
<div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
<h3>{_t("Setting:")} <code>{this.state.viewSetting}</code></h3>
<div>
{_t("Setting definition:")}
<pre><code>{JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}</code></pre>
</div>
<div>
{_t("Value:")}&nbsp;
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}</code>
</div>
<div>
{_t("Value in this room:")}&nbsp;
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))}</code>
</div>
<div>
{_t("Values at explicit levels:")}
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, null)}</code></pre>
</div>
<div>
{_t("Values at explicit levels in this room:")}
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}</code></pre>
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{_t("Edit Values")}</button>
<button onClick={this.onBack}>{_t("Back")}</button>
</div>
</div>
);
}
}
}
const Entries = [ const Entries = [
SendCustomEvent, SendCustomEvent,
RoomStateExplorer, RoomStateExplorer,
@ -802,6 +1086,7 @@ const Entries = [
ServersInRoomList, ServersInRoomList,
VerificationExplorer, VerificationExplorer,
WidgetExplorer, WidgetExplorer,
SettingsExplorer,
]; ];
export default class DevtoolsDialog extends React.PureComponent { export default class DevtoolsDialog extends React.PureComponent {

View File

@ -2068,6 +2068,26 @@
"Verification Requests": "Verification Requests", "Verification Requests": "Verification Requests",
"Active Widgets": "Active Widgets", "Active Widgets": "Active Widgets",
"There was an error finding this widget.": "There was an error finding this widget.", "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", "Toolbox": "Toolbox",
"Developer Tools": "Developer Tools", "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.", "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.",

View File

@ -61,7 +61,7 @@ for (const key of Object.keys(LEVEL_HANDLERS)) {
LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]); LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]);
} }
const LEVEL_ORDER = [ export const LEVEL_ORDER = [
SettingLevel.DEVICE, SettingLevel.DEVICE,
SettingLevel.ROOM_DEVICE, SettingLevel.ROOM_DEVICE,
SettingLevel.ROOM_ACCOUNT, SettingLevel.ROOM_ACCOUNT,