feat: support advanced editor in model edit page (#3176)

* feat: integrate external model editor and handle message events for model updates

* feat: add CasbinEditor and IframeEditor components for model editing

* feat: add tabbed editor interface for CasbinEditor

* fix: Synchronize content between basic and advanced editors

* refactor: simplify CasbinEditor and ModelEditPage components

* refactor: Refactor CasbinEditor for improved iframe initialization and model synchronization

* refactor: update default state of CasbinEditor active tab to "advanced

* chore: add Apache License header to CasbinEditor.js and IframeEditor.js files

* refactor: update CasbinEditor class names for consistency
This commit is contained in:
Coki 2024-09-16 22:25:25 +08:00 committed by GitHub
parent 8df965b98d
commit ed158d4981
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 184 additions and 15 deletions

97
web/src/CasbinEditor.js Normal file
View File

@ -0,0 +1,97 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {useCallback, useEffect, useRef, useState} from "react";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
import "codemirror/mode/properties/properties";
import * as Setting from "./Setting";
import IframeEditor from "./IframeEditor";
import {Tabs} from "antd";
const {TabPane} = Tabs;
const CasbinEditor = ({model, onModelTextChange}) => {
const [activeKey, setActiveKey] = useState("advanced");
const iframeRef = useRef(null);
const [localModelText, setLocalModelText] = useState(model.modelText);
const handleModelTextChange = useCallback((newModelText) => {
if (!Setting.builtInObject(model)) {
setLocalModelText(newModelText);
onModelTextChange(newModelText);
}
}, [model, onModelTextChange]);
const syncModelText = useCallback(() => {
return new Promise((resolve) => {
if (activeKey === "advanced" && iframeRef.current) {
const handleSyncMessage = (event) => {
if (event.data.type === "modelUpdate") {
window.removeEventListener("message", handleSyncMessage);
handleModelTextChange(event.data.modelText);
resolve();
}
};
window.addEventListener("message", handleSyncMessage);
iframeRef.current.getModelText();
} else {
resolve();
}
});
}, [activeKey, handleModelTextChange]);
const handleTabChange = (key) => {
syncModelText().then(() => {
setActiveKey(key);
if (key === "advanced" && iframeRef.current) {
iframeRef.current.updateModelText(localModelText);
}
});
};
useEffect(() => {
setLocalModelText(model.modelText);
}, [model.modelText]);
return (
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column"}}>
<Tabs activeKey={activeKey} onChange={handleTabChange} style={{flex: "0 0 auto", marginTop: "-10px"}}>
<TabPane tab="Basic Editor" key="basic" />
<TabPane tab="Advanced Editor" key="advanced" />
</Tabs>
<div style={{flex: "1 1 auto", overflow: "hidden"}}>
{activeKey === "advanced" ? (
<IframeEditor
ref={iframeRef}
initialModelText={localModelText}
onModelTextChange={handleModelTextChange}
style={{width: "100%", height: "100%"}}
/>
) : (
<CodeMirror
value={localModelText}
className="full-height-editor no-horizontal-scroll-editor"
options={{mode: "properties", theme: "default"}}
onBeforeChange={(editor, data, value) => {
handleModelTextChange(value);
}}
/>
)}
</div>
</div>
);
};
export default CasbinEditor;

66
web/src/IframeEditor.js Normal file
View File

@ -0,0 +1,66 @@
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react";
const IframeEditor = forwardRef(({initialModelText, onModelTextChange}, ref) => {
const iframeRef = useRef(null);
const [iframeReady, setIframeReady] = useState(false);
useEffect(() => {
const handleMessage = (event) => {
if (event.origin !== "https://editor.casbin.org") {return;}
if (event.data.type === "modelUpdate") {
onModelTextChange(event.data.modelText);
} else if (event.data.type === "iframeReady") {
setIframeReady(true);
iframeRef.current?.contentWindow.postMessage({
type: "initializeModel",
modelText: initialModelText,
}, "*");
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [onModelTextChange, initialModelText]);
useImperativeHandle(ref, () => ({
getModelText: () => {
iframeRef.current?.contentWindow.postMessage({type: "getModelText"}, "*");
},
updateModelText: (newModelText) => {
if (iframeReady) {
iframeRef.current?.contentWindow.postMessage({
type: "updateModelText",
modelText: newModelText,
}, "*");
}
},
}));
return (
<iframe
ref={iframeRef}
src="https://editor.casbin.org/model-editor"
frameBorder="0"
width="100%"
height="500px"
title="Casbin Model Editor"
/>
);
});
export default IframeEditor;

View File

@ -18,11 +18,7 @@ import * as ModelBackend from "./backend/ModelBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import {Controlled as CodeMirror} from "react-codemirror2";
import "codemirror/lib/codemirror.css";
require("codemirror/mode/properties/properties");
import ModelEditor from "./CasbinEditor";
const {Option} = Select;
@ -147,16 +143,10 @@ class ModelEditPage extends React.Component {
{Setting.getLabel(i18next.t("model:Model text"), i18next.t("model:Model text - Tooltip"))} :
</Col>
<Col span={22}>
<div style={{width: "100%"}} >
<CodeMirror
value={this.state.model.modelText}
options={{mode: "properties", theme: "default"}}
onBeforeChange={(editor, data, value) => {
if (Setting.builtInObject(this.state.model)) {
return;
}
this.updateModelField("modelText", value);
}}
<div style={{position: "relative", height: "500px"}} >
<ModelEditor
model={this.state.model}
onModelTextChange={(value) => this.updateModelField("modelText", value)}
/>
</div>
</Col>

View File

@ -51,3 +51,19 @@ code {
.custom-link:hover {
color: rgb(64 64 64) !important;
}
.full-height-editor {
height: 100%;
}
.full-height-editor [class*="CodeMirror"] {
height: 100%;
}
.no-horizontal-scroll-editor [class*="CodeMirror-hscrollbar"] {
display: none !important;
}
.no-horizontal-scroll-editor [class*="CodeMirror-scroll"] {
overflow-x: hidden !important;
}