File validation

This commit is contained in:
1ilit 2023-09-19 15:49:28 +03:00
parent 423e4e9b2d
commit db843f32f3
5 changed files with 290 additions and 47 deletions

24
package-lock.json generated
View File

@ -20,6 +20,7 @@
"codemirror": "^5.65.13", "codemirror": "^5.65.13",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"jsonschema": "^1.4.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"node-sql-parser": "^4.7.0", "node-sql-parser": "^4.7.0",
"react": "^18.2.0", "react": "^18.2.0",
@ -28,6 +29,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.11.2", "react-router-dom": "^6.11.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"url": "^0.11.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@ -12507,6 +12509,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/jsonschema": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
"integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==",
"engines": {
"node": "*"
}
},
"node_modules/jspdf": { "node_modules/jspdf": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
@ -17290,6 +17300,15 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/url": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.1.tgz",
"integrity": "sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==",
"dependencies": {
"punycode": "^1.4.1",
"qs": "^6.11.0"
}
},
"node_modules/url-parse": { "node_modules/url-parse": {
"version": "1.5.10", "version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
@ -17299,6 +17318,11 @@
"requires-port": "^1.0.0" "requires-port": "^1.0.0"
} }
}, },
"node_modules/url/node_modules/punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -15,6 +15,7 @@
"codemirror": "^5.65.13", "codemirror": "^5.65.13",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"jsonschema": "^1.4.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"node-sql-parser": "^4.7.0", "node-sql-parser": "^4.7.0",
"react": "^18.2.0", "react": "^18.2.0",
@ -23,6 +24,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.11.2", "react-router-dom": "^6.11.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"url": "^0.11.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {

View File

@ -23,10 +23,16 @@ import {
Modal, Modal,
Spin, Spin,
Input, Input,
Upload,
Banner,
} from "@douyinfe/semi-ui"; } from "@douyinfe/semi-ui";
import { toPng, toJpeg, toSvg } from "html-to-image"; import { toPng, toJpeg, toSvg } from "html-to-image";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { enterFullscreen, exitFullscreen } from "../utils"; import {
diagramObjectIsValid,
enterFullscreen,
exitFullscreen,
} from "../utils";
import { import {
AreaContext, AreaContext,
LayoutContext, LayoutContext,
@ -45,6 +51,7 @@ export default function ControlPanel(props) {
NONE: 0, NONE: 0,
IMG: 1, IMG: 1,
CODE: 2, CODE: 2,
IMPORT: 3,
}; };
const [visible, setVisible] = useState(MODAL.NONE); const [visible, setVisible] = useState(MODAL.NONE);
const [exportData, setExportData] = useState({ const [exportData, setExportData] = useState({
@ -52,6 +59,7 @@ export default function ControlPanel(props) {
filename: `diagram_${new Date().toISOString()}`, filename: `diagram_${new Date().toISOString()}`,
extension: "", extension: "",
}); });
const [error, setError] = useState(null);
const { layout, setLayout } = useContext(LayoutContext); const { layout, setLayout } = useContext(LayoutContext);
const { setSettings } = useContext(SettingsContext); const { setSettings } = useContext(SettingsContext);
const { relationships, tables, setTables } = useContext(TableContext); const { relationships, tables, setTables } = useContext(TableContext);
@ -86,7 +94,9 @@ export default function ControlPanel(props) {
}, },
Import: { Import: {
children: [], children: [],
function: () => {}, function: () => {
setVisible(MODAL.IMPORT);
},
}, },
"Export as": { "Export as": {
children: [ children: [
@ -125,7 +135,7 @@ export default function ControlPanel(props) {
tables: tables, tables: tables,
relationships: relationships, relationships: relationships,
notes: notes, notes: notes,
"subject areas": areas, subjectAreas: areas,
}, },
null, null,
2 2
@ -155,8 +165,7 @@ export default function ControlPanel(props) {
{ {
PDF: () => { PDF: () => {
const canvas = document.getElementById("canvas"); const canvas = document.getElementById("canvas");
toJpeg(canvas).then( toJpeg(canvas).then(function (dataUrl) {
function (dataUrl) {
const doc = new jsPDF("l", "px", [ const doc = new jsPDF("l", "px", [
canvas.offsetWidth, canvas.offsetWidth,
canvas.offsetHeight, canvas.offsetHeight,
@ -170,8 +179,7 @@ export default function ControlPanel(props) {
canvas.offsetHeight canvas.offsetHeight
); );
doc.save(`${exportData.filename}.pdf`); doc.save(`${exportData.filename}.pdf`);
} });
);
}, },
}, },
], ],
@ -494,7 +502,7 @@ export default function ControlPanel(props) {
</button> </button>
</div> </div>
<Modal <Modal
title="Export diagram" title={`${visible === MODAL.IMPORT ? "Import" : "Export"} diagram`}
visible={visible !== MODAL.NONE} visible={visible !== MODAL.NONE}
onOk={() => { onOk={() => {
if (visible === MODAL.IMG) { if (visible === MODAL.IMG) {
@ -507,6 +515,7 @@ export default function ControlPanel(props) {
type: "text/plain;charset=utf-8", type: "text/plain;charset=utf-8",
}); });
saveAs(blob, `${exportData.filename}.${exportData.extension}`); saveAs(blob, `${exportData.filename}.${exportData.extension}`);
} else if (visible === MODAL.IMPORT) {
} }
}} }}
afterClose={() => { afterClose={() => {
@ -515,16 +524,72 @@ export default function ControlPanel(props) {
extension: "", extension: "",
filename: `diagram_${new Date().toISOString()}`, filename: `diagram_${new Date().toISOString()}`,
})); }));
setError(null);
}} }}
onCancel={() => setVisible(MODAL.NONE)} onCancel={() => setVisible(MODAL.NONE)}
centered centered
closeOnEsc={true} closeOnEsc={true}
okText="Export" okText={`${visible === MODAL.IMPORT ? "Import" : "Export"}`}
cancelText="Cancel" cancelText="Cancel"
width={520} width={520}
> >
{exportData.data !== "" || exportData.data ? ( {visible === MODAL.IMPORT ? (
visible === MODAL.IMG ? ( <div>
<Upload
action="#"
beforeUpload={({ file, fileList }) => {
const f = fileList[0].fileInstance;
if (!f) {
return;
}
const reader = new FileReader();
reader.onload = function (event) {
if (f.type === "application/json") {
let jsonObject = null;
try {
jsonObject = JSON.parse(event.target.result);
} catch (error) {
setError("Invalid JSON. The file contains an error.");
return;
}
if (!diagramObjectIsValid(jsonObject)) {
setError(
"The file is missing necessary properties for a diagram."
);
return;
}
}
};
reader.readAsText(f);
return {
autoRemove: false,
fileInstance: file.fileInstance,
status: "success",
shouldUpload: false,
};
}}
draggable={true}
dragMainText="Click to upload the file or drag and drop the file here"
dragSubText="Support json"
accept="application/json,.txt"
onRemove={() => setError(null)}
onFileChange={() => setError(null)}
limit={1}
></Upload>
{error && (
<Banner
type="danger"
fullMode={false}
description={<div className="text-red-800">{error}</div>}
/>
)}
</div>
) : exportData.data !== "" || exportData.data ? (
<>
{visible === MODAL.IMG ? (
<Image src={exportData.data} alt="Diagram" height={220} /> <Image src={exportData.data} alt="Diagram" height={220} />
) : ( ) : (
<div className="max-h-[400px] overflow-auto border border-gray-200"> <div className="max-h-[400px] overflow-auto border border-gray-200">
@ -537,11 +602,6 @@ export default function ControlPanel(props) {
}} }}
/> />
</div> </div>
)
) : (
<div className="text-center my-3">
<Spin tip="Loading..." size="large" />
</div>
)} )}
<div className="text-sm font-semibold mt-2">Filename:</div> <div className="text-sm font-semibold mt-2">Filename:</div>
<Input <Input
@ -553,6 +613,12 @@ export default function ControlPanel(props) {
} }
field="filename" field="filename"
/> />
</>
) : (
<div className="text-center my-3">
<Spin tip="Loading..." size="large" />
</div>
)}
</Modal> </Modal>
</div> </div>
); );

144
src/schemas/index.js Normal file
View File

@ -0,0 +1,144 @@
const jsonSchema = {
type: "object",
properties: {
tables: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" },
x: { type: "number" },
y: { type: "number" },
fields: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
type: { type: "string" },
default: { type: "string" },
check: { type: "string" },
primary: { type: "boolean" },
unique: { type: "boolean" },
notNull: { type: "boolean" },
increment: { type: "boolean" },
comment: { type: "string" },
},
required: [
"name",
"type",
"default",
"check",
"primary",
"unique",
"notNull",
"increment",
"comment",
],
},
},
comment: { type: "string" },
indices: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
fields: {
type: "array",
items: { type: "string" },
},
},
required: ["name", "fields"],
},
},
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$" },
},
required: [
"id",
"name",
"x",
"y",
"fields",
"comment",
"indices",
"color",
],
},
},
relationships: {
type: "array",
items: {
type: "object",
properties: {
startTableId: { type: "integer" },
startFieldId: { type: "integer" },
endTableId: { type: "integer" },
endFieldId: { type: "integer" },
startX: { type: "number" },
startY: { type: "number" },
endX: { type: "number" },
endY: { type: "number" },
name: { type: "string" },
cardinality: { type: "string" },
updateConstraint: { type: "string" },
deleteConstraint: { type: "string" },
mandatory: { type: "boolean" },
id: { type: "integer" },
},
required: [
"startTableId",
"startFieldId",
"endTableId",
"endFieldId",
"startX",
"startY",
"endX",
"endY",
"name",
"cardinality",
"updateConstraint",
"deleteConstraint",
"mandatory",
"id",
],
},
},
notes: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "integer" },
x: { type: "number" },
y: { type: "number" },
title: { type: "string" },
content: { type: "string" },
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$" },
height: { type: "number" },
},
required: ["id", "x", "y", "title", "content", "color", "height"],
},
},
subjectAreas: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" },
x: { type: "number" },
y: { type: "number" },
width: { type: "number" },
height: { type: "number" },
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$" },
},
required: ["id", "name", "x", "y", "width", "height", "color"],
},
},
},
required: ["tables", "relationships", "notes", "subjectAreas"],
};
export { jsonSchema };

View File

@ -1,3 +1,6 @@
import { Validator } from "jsonschema";
import { jsonSchema } from "../schemas";
const enterFullscreen = () => { const enterFullscreen = () => {
const element = document.documentElement; const element = document.documentElement;
if (element.requestFullscreen) { if (element.requestFullscreen) {
@ -23,4 +26,8 @@ const exitFullscreen = () => {
} }
}; };
export { enterFullscreen, exitFullscreen }; const diagramObjectIsValid = (obj) => {
return new Validator().validate(obj, jsonSchema).valid;
};
export { enterFullscreen, exitFullscreen, diagramObjectIsValid };