Merge pull request #5471 from vector-im/t3chguy/devtools-1

T3chguy/devtools 1
This commit is contained in:
Matthew Hodgson 2017-11-03 23:10:00 +00:00 committed by GitHub
commit c0b343c89f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 509 additions and 116 deletions

View file

@ -15,35 +15,24 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from 'matrix-react-sdk';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
class SendCustomEvent extends React.Component {
static propTypes = {
roomId: React.PropTypes.string.isRequired,
onBack: React.PropTypes.func.isRequired,
eventType: React.PropTypes.string.isRequired,
evContent: React.PropTypes.string.isRequired,
class DevtoolsComponent extends React.Component {
static contextTypes = {
roomId: PropTypes.string.isRequired,
};
}
static defaultProps = {
eventType: '',
evContent: '{\n\n}',
};
class GenericEditor extends DevtoolsComponent {
// static propTypes = {onBack: PropTypes.func.isRequired};
constructor(props, context) {
super(props, context);
this._send = this._send.bind(this);
this.onBack = this.onBack.bind(this);
this._onChange = this._onChange.bind(this);
this.state = {
message: null,
input_eventType: this.props.eventType,
input_evContent: this.props.evContent,
};
this.onBack = this.onBack.bind(this);
}
onBack() {
@ -54,6 +43,10 @@ class SendCustomEvent extends React.Component {
}
}
_onChange(e) {
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
}
_buttons() {
return <div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
@ -61,19 +54,64 @@ class SendCustomEvent extends React.Component {
</div>;
}
textInput(id, label) {
return <div className="mx_DevTools_inputRow">
<div className="mx_DevTools_inputLabelCell">
<label htmlFor={id}>{ label }</label>
</div>
<div className="mx_DevTools_inputCell">
<input id={id} onChange={this._onChange} value={this.state[id]} size="32" />
</div>
</div>;
}
}
class SendCustomEvent extends GenericEditor {
static getLabel() { return _t('Send Custom Event'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
forceStateEvent: PropTypes.bool,
inputs: PropTypes.object,
};
constructor(props, context) {
super(props, context);
this._send = this._send.bind(this);
const {eventType, stateKey, evContent} = Object.assign({
eventType: '',
stateKey: '',
evContent: '{\n\n}',
}, this.props.inputs);
this.state = {
isStateEvent: Boolean(this.props.forceStateEvent),
eventType,
stateKey,
evContent,
};
}
send(content) {
return MatrixClientPeg.get().sendEvent(this.props.roomId, this.state.input_eventType, content);
const cli = MatrixClientPeg.get();
if (this.state.isStateEvent) {
return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey);
} else {
return cli.sendEvent(this.context.roomId, this.state.eventType, content);
}
}
async _send() {
if (this.state.input_eventType === '') {
if (this.state.eventType === '') {
this.setState({ message: _t('You must specify an event type!') });
return;
}
let message;
try {
const content = JSON.parse(this.state.input_evContent);
const content = JSON.parse(this.state.evContent);
await this.send(content);
message = _t('Event sent!');
} catch (e) {
@ -82,14 +120,6 @@ class SendCustomEvent extends React.Component {
this.setState({ message });
}
_additionalFields() {
return <div />;
}
_onChange(e) {
this.setState({[`input_${e.target.id}`]: e.target.value});
}
render() {
if (this.state.message) {
return <div>
@ -102,87 +132,176 @@ class SendCustomEvent extends React.Component {
return <div>
<div className="mx_Dialog_content">
{ this._additionalFields() }
<div className="mx_TextInputDialog_label">
<label htmlFor="eventType"> { _t('Event Type') } </label>
</div>
<div>
<input id="eventType" onChange={this._onChange} value={this.state.input_eventType} className="mx_TextInputDialog_input" size="64" />
</div>
{ this.textInput('eventType', _t('Event Type')) }
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
<div className="mx_TextInputDialog_label">
<br />
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor="evContent"> { _t('Event Content') } </label>
</div>
<div>
<textarea id="evContent" onChange={this._onChange} value={this.state.input_evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
</div>
</div>
{ this._buttons() }
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
{ !this.state.message && !this.props.forceStateEvent && <div style={{float: "right"}}>
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
</div> }
</div>
</div>;
}
}
class SendCustomStateEvent extends SendCustomEvent {
class SendAccountData extends GenericEditor {
static getLabel() { return _t('Send Account Data'); }
static propTypes = {
roomId: React.PropTypes.string.isRequired,
onBack: React.PropTypes.func.isRequired,
eventType: React.PropTypes.string.isRequired,
evContent: React.PropTypes.string.isRequired,
stateKey: React.PropTypes.string.isRequired,
};
static defaultProps = {
eventType: '',
evContent: '{\n\n}',
stateKey: '',
isRoomAccountData: PropTypes.bool,
forceMode: PropTypes.bool,
inputs: PropTypes.object,
};
constructor(props, context) {
super(props, context);
this.state['input_stateKey'] = this.props.stateKey;
this._send = this._send.bind(this);
const {eventType, evContent} = Object.assign({
eventType: '',
evContent: '{\n\n}',
}, this.props.inputs);
this.state = {
isRoomAccountData: Boolean(this.props.isRoomAccountData),
eventType,
evContent,
};
}
send(content) {
const cli = MatrixClientPeg.get();
return cli.sendStateEvent(this.props.roomId, this.state.input_eventType, content, this.state.input_stateKey);
if (this.state.isRoomAccountData) {
return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content);
}
return cli.setAccountData(this.state.eventType, content);
}
_additionalFields() {
async _send() {
if (this.state.eventType === '') {
this.setState({ message: _t('You must specify an event type!') });
return;
}
let message;
try {
const content = JSON.parse(this.state.evContent);
await this.send(content);
message = _t('Event sent!');
} catch (e) {
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
}
this.setState({ message });
}
render() {
if (this.state.message) {
return <div>
<div className="mx_TextInputDialog_label">
<label htmlFor="stateKey"> { _t('State Key') } </label>
<div className="mx_Dialog_content">
{ this.state.message }
</div>
{ this._buttons() }
</div>;
}
return <div>
<div className="mx_Dialog_content">
{ this.textInput('eventType', _t('Event Type')) }
<br />
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor="evContent"> { _t('Event Content') } </label>
</div>
<div>
<input id="stateKey" onChange={this._onChange} value={this.state.input_stateKey} className="mx_TextInputDialog_input" size="64" />
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
{ !this.state.message && <div style={{float: "right"}}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
</div> }
</div>
</div>;
}
}
class RoomStateExplorer extends React.Component {
class FilteredList extends React.Component {
static propTypes = {
setMode: React.PropTypes.func.isRequired,
roomId: React.PropTypes.string.isRequired,
onBack: React.PropTypes.func.isRequired,
children: PropTypes.any,
};
constructor(props, context) {
super(props, context);
this.onQuery = this.onQuery.bind(this);
this.state = {
query: '',
};
}
onQuery(ev) {
this.setState({ query: ev.target.value });
}
filterChildren() {
if (this.state.query) {
const lowerQuery = this.state.query.toLowerCase();
return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery));
}
return this.props.children;
}
render() {
return <div>
<input size="64"
onChange={this.onQuery}
value={this.state.query}
placeholder={_t('Filter results')}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
{ this.filterChildren() }
</div>;
}
}
class RoomStateExplorer extends DevtoolsComponent {
static getLabel() { return _t('Explore Room State'); }
static propTypes = {
onBack: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const room = MatrixClientPeg.get().getRoom(this.context.roomId);
this.roomStateEvents = room.currentState.events;
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
this.onQuery = this.onQuery.bind(this);
}
state = {
query: '',
this.state = {
eventType: null,
event: null,
editing: false,
};
}
browseEventType(eventType) {
return () => {
@ -197,7 +316,9 @@ class RoomStateExplorer extends React.Component {
}
onBack() {
if (this.state.event) {
if (this.state.editing) {
this.setState({ editing: false });
} else if (this.state.event) {
this.setState({ event: null });
} else if (this.state.eventType) {
this.setState({ eventType: null });
@ -207,20 +328,19 @@ class RoomStateExplorer extends React.Component {
}
editEv() {
const ev = this.state.event;
this.props.setMode(SendCustomStateEvent, {
eventType: ev.getType(),
evContent: JSON.stringify(ev.getContent(), null, '\t'),
stateKey: ev.getStateKey(),
});
}
onQuery(ev) {
this.setState({ query: ev.target.value });
this.setState({ editing: true });
}
render() {
if (this.state.event) {
if (this.state.editing) {
return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
stateKey: this.state.event.getStateKey(),
}} />;
}
return <div className="mx_ViewSource">
<div className="mx_Dialog_content">
<pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre>
@ -234,11 +354,9 @@ class RoomStateExplorer extends React.Component {
const rows = [];
const classes = 'mx_DevTools_RoomStateExplorer_button';
if (this.state.eventType === null) {
Object.keys(this.roomStateEvents).forEach((evType) => {
// Skip this entry if does not contain search query
if (this.state.query && !evType.toLowerCase().includes(this.state.query.toLowerCase())) return;
const stateGroup = this.roomStateEvents[evType];
const stateKeys = Object.keys(stateGroup);
@ -249,7 +367,7 @@ class RoomStateExplorer extends React.Component {
onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
}
rows.push(<button className="mx_DevTools_RoomStateExplorer_button" key={evType} onClick={onClickFn}>
rows.push(<button className={classes} key={evType} onClick={onClickFn}>
{ evType }
</button>);
});
@ -257,12 +375,8 @@ class RoomStateExplorer extends React.Component {
const evType = this.state.eventType;
const stateGroup = this.roomStateEvents[evType];
Object.keys(stateGroup).forEach((stateKey) => {
// Skip this entry if does not contain search query
if (this.state.query && !stateKey.toLowerCase().includes(this.state.query.toLowerCase())) return;
const ev = stateGroup[stateKey];
rows.push(<button className="mx_DevTools_RoomStateExplorer_button" key={stateKey}
onClick={this.onViewSourceClick(ev)}>
rows.push(<button className={classes} key={stateKey} onClick={this.onViewSourceClick(ev)}>
{ stateKey }
</button>);
});
@ -270,8 +384,9 @@ class RoomStateExplorer extends React.Component {
return <div>
<div className="mx_Dialog_content">
<input onChange={this.onQuery} placeholder={_t('Filter results')} size="64" className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" value={this.state.query} />
<FilteredList>
{ rows }
</FilteredList>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
@ -280,41 +395,158 @@ class RoomStateExplorer extends React.Component {
}
}
export default class DevtoolsDialog extends React.Component {
class AccountDataExplorer extends DevtoolsComponent {
static getLabel() { return _t('Explore Account Data'); }
static propTypes = {
roomId: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
state = {
mode: null,
modeArgs: {},
constructor(props, context) {
super(props, context);
this.onBack = this.onBack.bind(this);
this.editEv = this.editEv.bind(this);
this._onChange = this._onChange.bind(this);
this.state = {
isRoomAccountData: false,
event: null,
editing: false,
};
}
getData() {
const cli = MatrixClientPeg.get();
if (this.state.isRoomAccountData) {
return cli.getRoom(this.context.roomId).accountData;
}
return cli.store.accountData;
}
onViewSourceClick(event) {
return () => {
this.setState({ event });
};
}
onBack() {
if (this.state.editing) {
this.setState({ editing: false });
} else if (this.state.event) {
this.setState({ event: null });
} else {
this.props.onBack();
}
}
_onChange(e) {
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
}
editEv() {
this.setState({ editing: true });
}
render() {
if (this.state.event) {
if (this.state.editing) {
return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{
eventType: this.state.event.getType(),
evContent: JSON.stringify(this.state.event.getContent(), null, '\t'),
}} forceMode={true} />;
}
return <div className="mx_ViewSource">
<div className="mx_Dialog_content">
<pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
<button onClick={this.editEv}>{ _t('Edit') }</button>
</div>
</div>;
}
const rows = [];
const classes = 'mx_DevTools_RoomStateExplorer_button';
const data = this.getData();
Object.keys(data).forEach((evType) => {
const ev = data[evType];
rows.push(<button className={classes} key={evType} onClick={this.onViewSourceClick(ev)}>
{ evType }
</button>);
});
return <div>
<div className="mx_Dialog_content">
<FilteredList>
{ rows }
</FilteredList>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onBack}>{ _t('Back') }</button>
{ !this.state.message && <div style={{float: "right"}}>
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} />
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
</div> }
</div>
</div>;
}
}
const Entries = [
SendCustomEvent,
RoomStateExplorer,
SendAccountData,
AccountDataExplorer,
];
export default class DevtoolsDialog extends React.Component {
static childContextTypes = {
roomId: PropTypes.string.isRequired,
// client: PropTypes.instanceOf(MatixClient),
};
static propTypes = {
roomId: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
this.onBack = this.onBack.bind(this);
this.setMode = this.setMode.bind(this);
this.onCancel = this.onCancel.bind(this);
this.state = {
mode: null,
};
}
componentWillUnmount() {
this._unmounted = true;
}
getChildContext() {
return { roomId: this.props.roomId };
}
_setMode(mode) {
return () => {
this.setMode(mode);
this.setState({ mode });
};
}
setMode(mode, modeArgs={}) {
this.setState({ mode, modeArgs });
}
onBack() {
if (this.prevMode) {
this.setState({ mode: this.prevMode });
this.prevMode = null;
} else {
this.setState({ mode: null });
}
}
onCancel() {
this.props.onFinished(false);
@ -324,14 +556,27 @@ export default class DevtoolsDialog extends React.Component {
let body;
if (this.state.mode) {
body =
<this.state.mode {...this.props} {...this.state.modeArgs} onBack={this.onBack} setMode={this.setMode} />;
} else {
body = <div>
<div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
<this.state.mode onBack={this.onBack} />
</div>;
} else {
const classes = "mx_DevTools_RoomStateExplorer_button";
body = <div>
<div>
<div className="mx_DevTools_label_left">{ _t('Toolbox') }</div>
<div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
<div className="mx_DevTools_label_bottom" />
<div className="mx_Dialog_content">
<button onClick={this._setMode(SendCustomEvent)}>{ _t('Send Custom Event') }</button>
<button onClick={this._setMode(SendCustomStateEvent)}>{ _t('Send Custom State Event') }</button>
<button onClick={this._setMode(RoomStateExplorer)}>{ _t('Explore Room State') }</button>
{ Entries.map((Entry) => {
const label = Entry.getLabel();
const onClick = this._setMode(Entry);
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
}) }
</div>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>{ _t('Cancel') }</button>
@ -342,7 +587,6 @@ export default class DevtoolsDialog extends React.Component {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} title={_t('Developer Tools')}>
<div>Room ID: { this.props.roomId }</div>
{ body }
</BaseDialog>
);

View file

@ -70,6 +70,7 @@
"What's new?": "What's new?",
"A new version of Riot is available.": "A new version of Riot is available.",
"To return to your account in future you need to <u>set a password</u>": "To return to your account in future you need to <u>set a password</u>",
"Toolbox": "Toolbox",
"Set Password": "Set Password",
"Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).",
"Checking for an update...": "Checking for an update...",
@ -106,7 +107,8 @@
"Edit": "Edit",
"Filter results": "Filter results",
"Send Custom Event": "Send Custom Event",
"Send Custom State Event": "Send Custom State Event",
"Send Account Data": "Send Account Data",
"Explore Account Data": "Explore Account Data",
"Explore Room State": "Explore Room State",
"Developer Tools": "Developer Tools",
"You have successfully set a password!": "You have successfully set a password!",

View file

@ -17,3 +17,150 @@ limitations under the License.
.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_RoomStateExplorer_query {
margin-bottom: 10px;
}
.mx_DevTools_label_left {
float: left;
}
.mx_DevTools_label_right {
float: right;
}
.mx_DevTools_label_bottom {
clear: both;
border-bottom: 1px solid #e5e5e5;
}
.mx_DevTools_inputRow
{
display: table-row;
}
.mx_DevTools_inputLabelCell
{
padding-bottom: 21px;
display: table-cell;
font-weight: bold;
padding-right: 24px;
}
.mx_DevTools_inputCell {
display: table-cell;
padding-bottom: 21px;
width: 240px;
}
.mx_DevTools_inputCell input
{
display: inline-block;
border: 0;
border-bottom: 1px solid $input-underline-color;
padding: 0;
width: 240px;
color: $input-fg-color;
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-size: 16px;
}
.mx_DevTools_tgl {
display: none;
// add default box-sizing for this scope
&,
&:after,
&:before,
& *,
& *:after,
& *:before,
& + .mx_DevTools_tgl-btn {
box-sizing: border-box;
&::selection {
background: none;
}
}
+ .mx_DevTools_tgl-btn {
outline: 0;
display: block;
width: 7em;
height: 2em;
position: relative;
cursor: pointer;
user-select: none;
&:after,
&:before {
position: relative;
display: block;
content: "";
width: 50%;
height: 100%;
}
&:after {
left: 0;
}
&:before {
display: none;
}
}
&:checked + .mx_DevTools_tgl-btn:after {
left: 50%;
}
}
.mx_DevTools_tgl-flip {
+ .mx_DevTools_tgl-btn {
padding: 2px;
transition: all .2s ease;
font-family: sans-serif;
perspective: 100px;
&:after,
&:before {
display: inline-block;
transition: all .4s ease;
width: 100%;
text-align: center;
position: absolute;
line-height: 2em;
font-weight: bold;
color: #fff;
top: 0;
left: 0;
backface-visibility: hidden;
border-radius: 4px;
}
&:after {
content: attr(data-tg-on);
background: #02C66F;
transform: rotateY(-180deg);
}
&:before {
background: #FF3A19;
content: attr(data-tg-off);
}
&:active:before {
transform: rotateY(-20deg);
}
}
&:checked + .mx_DevTools_tgl-btn {
&:before {
transform: rotateY(180deg);
}
&:after {
transform: rotateY(0);
left: 0;
background: #7FC6A6;
}
&:active:after {
transform: rotateY(20deg);
}
}
}