blob: b853ae1f222d5880933b60f21ee7a4c54a0db6d6 [file]
'use client';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { monokai } from '@uiw/codemirror-theme-monokai';
import { basicSetup } from 'codemirror';
import { indentUnit } from '@codemirror/language';
import { EditorView } from '@codemirror/view';
import { CasbinConfSupport } from '@/app/components/editor/casbin-mode/casbin-conf';
import { linter, lintGutter } from '@codemirror/lint';
import { casbinLinter } from '@/app/utils/casbinLinter';
import { newModel } from 'casbin';
import { buttonPlugin } from '@/app/components/editor/ButtonPlugin';
import { extractPageContent } from '@/app/utils/contentExtractor';
import { useLang } from '@/app/context/LangContext';
import SidePanelChat from '@/app/components/SidePanelChat';
import { example } from '@/app/components/editor/casbin-mode/example';
import { clsx } from 'clsx';
import { parseError, setError } from '@/app/utils/errorManager';
export const ModelEditor = () => {
const [modelText, setModelText] = useState('');
const [modelKind, setModelKind] = useState('');
const [initialText, setInitialText] = useState('');
const editorRef = useRef<EditorView | null>(null);
const cursorPosRef = useRef<{ from: number; to: number } | null>(null);
const sidePanelChatRef = useRef<{ openDrawer: (message: string) => void } | null>(null);
const { t, lang, theme, setLang } = useLang();
const textClass = clsx(theme === 'dark' ? 'text-gray-200' : 'text-gray-800');
const openDrawerWithMessage = (message: string) => {
if (sidePanelChatRef.current) {
sidePanelChatRef.current.openDrawer(message);
}
};
const extractContent = (boxType: string) => {
const { message } = extractPageContent(boxType, t, lang);
return message;
};
const validateModel = useCallback(async (text: string) => {
try {
await newModel(text);
setError(null);
} catch (e) {
setError(parseError((e as Error).message));
}
}, []);
useEffect(() => {
validateModel(modelText);
}, [modelText, validateModel]);
const handleMessage = useCallback(
(event: MessageEvent) => {
if (event.data.type === 'initializeModel') {
if (event.data.modelText) {
setModelText(event.data.modelText);
setInitialText(event.data.modelText);
}
if (event.data.lang) {
setLang(event.data.lang);
}
} else if (event.data.type === 'getModelText') {
window.parent.postMessage(
{
type: 'modelUpdate',
modelText: modelText,
},
'*',
);
} else if (event.data.type === 'updateModelText') {
if (event.data.modelText) {
setModelText(event.data.modelText);
}
} else if (event.data.type === 'updateLanguage') {
if (event.data.language) {
setLang(event.data.language);
}
}
},
[modelText, setLang],
);
useEffect(() => {
window.addEventListener('message', handleMessage);
window.parent.postMessage({ type: 'iframeReady' }, '*');
return () => {
window.removeEventListener('message', handleMessage);
};
}, [handleMessage]);
const handleModelTextChange = useCallback((value: string, viewUpdate: any) => {
setModelText(value);
cursorPosRef.current = viewUpdate.state.selection.main;
window.parent.postMessage(
{
type: 'modelUpdate',
modelText: value,
},
'*',
);
}, []);
useEffect(() => {
if (editorRef.current && cursorPosRef.current) {
const { from, to } = cursorPosRef.current;
const docLength = editorRef.current.state.doc.length;
editorRef.current.dispatch({
selection: {
anchor: Math.min(from, docLength),
head: Math.min(to, docLength),
},
});
}
}, [modelText]);
useEffect(() => {
if (modelKind && example[modelKind]) {
setModelText(example[modelKind].model);
}
}, [modelKind]);
return (
<div className="flex-grow overflow-auto h-full">
<div className="flex flex-col h-full">
<div className={clsx('h-10 pl-2', 'flex items-center justify-start gap-2')}>
<div className={clsx(textClass, 'font-bold')}>{t('Model')}</div>
<select
value={modelKind}
onChange={(e) => {
const selectedKind = e.target.value;
if (selectedKind && example[selectedKind]) {
setModelText(example[selectedKind].model);
setModelKind('');
window.parent.postMessage(
{
type: 'modelUpdate',
modelText: example[selectedKind].model,
},
'*',
);
}
}}
className={'border-[#767676] border rounded'}
>
<option value="" disabled>
{t('Select your model')}
</option>
{Object.keys(example).map((n) => {
return (
<option key={n} value={n}>
{example[n].name}
</option>
);
})}
</select>
<button
className={clsx(
'rounded',
'text-[#453d7d]',
'px-1',
'border border-[#453d7d]',
'bg-[#efefef]',
'hover:bg-[#453d7d] hover:text-white',
'transition-colors duration-500',
)}
onClick={() => {
const ok = window.confirm('Confirm Reset?');
if (ok) {
const rbacModel = example['rbac'].model;
setModelText(rbacModel);
setModelKind('');
window.parent.postMessage(
{
type: 'modelUpdate',
modelText: rbacModel,
},
'*',
);
}
}}
>
{t('RESET')}
</button>
</div>
<CodeMirror
height="100%"
theme={monokai}
onChange={handleModelTextChange}
basicSetup={{
lineNumbers: true,
highlightActiveLine: true,
bracketMatching: true,
indentOnInput: true,
}}
extensions={[
basicSetup,
CasbinConfSupport(),
indentUnit.of(' '),
EditorView.lineWrapping,
linter(casbinLinter),
lintGutter(),
buttonPlugin(openDrawerWithMessage, extractContent, 'model'),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
editorRef.current = update.view;
}
}),
]}
className={'function flex-grow h-[300px]'}
value={modelText}
/>
<div className="mr-4">
<SidePanelChat ref={sidePanelChatRef} />
</div>
</div>
</div>
);
};