Quick and dirty devtool to explore state history (#11197)

* Quick and dirty devtool to explore state history

* Include error in unsigned

* iterate

* Fix silly copy paste
This commit is contained in:
Michael Telatynski 2023-07-07 09:16:11 +01:00 committed by GitHub
parent 285847560b
commit 706a42f390
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 72 additions and 5 deletions

View file

@ -31,6 +31,7 @@ export interface IDevtoolsProps {
interface IMinProps extends Pick<IDevtoolsProps, "onBack"> { interface IMinProps extends Pick<IDevtoolsProps, "onBack"> {
className?: string; className?: string;
children?: ReactNode; children?: ReactNode;
extraButton?: ReactNode;
} }
interface IProps extends IMinProps { interface IProps extends IMinProps {
@ -38,7 +39,14 @@ interface IProps extends IMinProps {
onAction(): Promise<string | void>; onAction(): Promise<string | void>;
} }
const BaseTool: React.FC<XOR<IMinProps, IProps>> = ({ className, actionLabel, onBack, onAction, children }) => { const BaseTool: React.FC<XOR<IMinProps, IProps>> = ({
className,
actionLabel,
onBack,
onAction,
children,
extraButton,
}) => {
const [message, setMessage] = useState<string | null>(null); const [message, setMessage] = useState<string | null>(null);
const onBackClick = (): void => { const onBackClick = (): void => {
@ -68,6 +76,7 @@ const BaseTool: React.FC<XOR<IMinProps, IProps>> = ({ className, actionLabel, on
<> <>
<div className={classNames("mx_DevTools_content", className)}>{children}</div> <div className={classNames("mx_DevTools_content", className)}>{children}</div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
{extraButton}
<button onClick={onBackClick}>{_t("Back")}</button> <button onClick={onBackClick}>{_t("Back")}</button>
{actionButton} {actionButton}
</div> </div>

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ChangeEvent, useContext, useMemo, useRef, useState } from "react"; import React, { ChangeEvent, ReactNode, useContext, useMemo, useRef, useState } from "react";
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t, _td } from "../../../../languageHandler"; import { _t, _td } from "../../../../languageHandler";
@ -143,9 +143,10 @@ export interface IEditorProps extends Pick<IDevtoolsProps, "onBack"> {
interface IViewerProps extends Required<IEditorProps> { interface IViewerProps extends Required<IEditorProps> {
Editor: React.FC<IEditorProps>; Editor: React.FC<IEditorProps>;
extraButton?: ReactNode;
} }
export const EventViewer: React.FC<IViewerProps> = ({ mxEvent, onBack, Editor }) => { export const EventViewer: React.FC<IViewerProps> = ({ mxEvent, onBack, Editor, extraButton }) => {
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
if (editing) { if (editing) {
@ -160,7 +161,7 @@ export const EventViewer: React.FC<IViewerProps> = ({ mxEvent, onBack, Editor })
}; };
return ( return (
<BaseTool onBack={onBack} actionLabel={_t("Edit")} onAction={onAction}> <BaseTool onBack={onBack} actionLabel={_t("Edit")} onAction={onAction} extraButton={extraButton}>
<SyntaxHighlight language="json">{stringify(mxEvent.event)}</SyntaxHighlight> <SyntaxHighlight language="json">{stringify(mxEvent.event)}</SyntaxHighlight>
</BaseTool> </BaseTool>
); );

View file

@ -24,6 +24,9 @@ import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { EventEditor, EventViewer, eventTypeField, stateKeyField, IEditorProps, stringify } from "./Event"; import { EventEditor, EventViewer, eventTypeField, stateKeyField, IEditorProps, stringify } from "./Event";
import FilteredList from "./FilteredList"; import FilteredList from "./FilteredList";
import Spinner from "../../elements/Spinner";
import SyntaxHighlight from "../../elements/SyntaxHighlight";
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
export const StateEventEditor: React.FC<IEditorProps> = ({ mxEvent, onBack }) => { export const StateEventEditor: React.FC<IEditorProps> = ({ mxEvent, onBack }) => {
const context = useContext(DevtoolsContext); const context = useContext(DevtoolsContext);
@ -47,6 +50,48 @@ interface StateEventButtonProps {
onClick(): void; onClick(): void;
} }
const RoomStateHistory: React.FC<{
mxEvent: MatrixEvent;
onBack(): void;
}> = ({ mxEvent, onBack }) => {
const cli = useContext(MatrixClientContext);
const events = useAsyncMemo(
async () => {
const events = [mxEvent.event];
while (!!events[0].unsigned?.replaces_state) {
try {
events.unshift(await cli.fetchRoomEvent(mxEvent.getRoomId()!, events[0].unsigned.replaces_state));
} catch (e) {
events.unshift({
event_id: events[0].unsigned.replaces_state,
unsigned: {
error: e instanceof Error ? e.message : String(e),
},
});
}
}
return events;
},
[cli, mxEvent],
null,
);
let body = <Spinner />;
if (events !== null) {
body = (
<>
{events.map((ev) => (
<SyntaxHighlight language="json" key={ev.event_id}>
{stringify(ev)}
</SyntaxHighlight>
))}
</>
);
}
return <BaseTool onBack={onBack}>{body}</BaseTool>;
};
const StateEventButton: React.FC<StateEventButtonProps> = ({ label, onClick }) => { const StateEventButton: React.FC<StateEventButtonProps> = ({ label, onClick }) => {
const trimmed = label.trim(); const trimmed = label.trim();
@ -71,6 +116,7 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
const context = useContext(DevtoolsContext); const context = useContext(DevtoolsContext);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [event, setEvent] = useState<MatrixEvent | null>(null); const [event, setEvent] = useState<MatrixEvent | null>(null);
const [history, setHistory] = useState(false);
const events = context.room.currentState.events.get(eventType)!; const events = context.room.currentState.events.get(eventType)!;
@ -82,6 +128,12 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
} }
}, [events]); }, [events]);
if (event && history) {
const _onBack = (): void => {
setHistory(false);
};
return <RoomStateHistory mxEvent={event} onBack={_onBack} />;
}
if (event) { if (event) {
const _onBack = (): void => { const _onBack = (): void => {
if (events?.size === 1 && events.has("")) { if (events?.size === 1 && events.has("")) {
@ -90,7 +142,11 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
setEvent(null); setEvent(null);
} }
}; };
return <EventViewer mxEvent={event} onBack={_onBack} Editor={StateEventEditor} />; const onHistoryClick = (): void => {
setHistory(true);
};
const extraButton = <button onClick={onHistoryClick}>{_t("See history")}</button>;
return <EventViewer mxEvent={event} onBack={_onBack} Editor={StateEventEditor} extraButton={extraButton} />;
} }
return ( return (

View file

@ -3224,6 +3224,7 @@
"<%(count)s spaces>|other": "<%(count)s spaces>", "<%(count)s spaces>|other": "<%(count)s spaces>",
"<%(count)s spaces>|one": "<space>", "<%(count)s spaces>|one": "<space>",
"<%(count)s spaces>|zero": "<empty string>", "<%(count)s spaces>|zero": "<empty string>",
"See history": "See history",
"Send custom state event": "Send custom state event", "Send custom state event": "Send custom state event",
"Capabilities": "Capabilities", "Capabilities": "Capabilities",
"Failed to load.": "Failed to load.", "Failed to load.": "Failed to load.",