Skip to content

Commit

Permalink
Merge pull request #93 from alan-turing-institute/feature/74-sanitize…
Browse files Browse the repository at this point in the history
…-markdown

move jsonToMermaid from CaseContainer into utils, and add new functio…
  • Loading branch information
mhauru committed Apr 5, 2022
2 parents 3bdce96 + 64bd31e commit cde750f
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 78 deletions.
112 changes: 112 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"file-saver": "^2.0.5",
"grommet": "^2.20.1",
"grommet-icons": "^4.7.0",
"jest-fetch-mock": "^3.0.3",
"jest-transform-stub": "^2.0.0",
"mermaid": "^8.13.10",
"pattern.css": "^1.0.0",
Expand Down
76 changes: 4 additions & 72 deletions frontend/src/components/CaseContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ import React, { Component } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Grid, Box, DropButton, Layer, Button, Text } from "grommet";
import { FormClose, ZoomIn, ZoomOut } from "grommet-icons";
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";

import MermaidChart from "./Mermaid";
import configData from "../config.json";

import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import EditableText from "./EditableText.js";
import ItemViewer from "./ItemViewer.js";
import ItemEditor from "./ItemEditor.js";
import ItemCreator from "./ItemCreator.js";

import { jsonToMermaid } from "./utils.js";
import configData from "../config.json";
import "./CaseContainer.css";

class CaseContainer extends Component {
Expand Down Expand Up @@ -48,7 +47,7 @@ class CaseContainer extends Component {
assurance_case: json_response,
});
this.setState({
mermaid_md: this.jsonToMermaid(this.state.assurance_case),
mermaid_md: jsonToMermaid(this.state.assurance_case),
});
this.setState({ loading: false });
}
Expand Down Expand Up @@ -115,71 +114,6 @@ class CaseContainer extends Component {
}
}

jsonToMermaid(in_json) {
// function to convert the JSON response from a GET request to the /cases/id
// API endpoint, into the markdown string required for Mermaid to render a flowchart.

// Nodes in the flowchart will be named [TypeName]_[ID]
function getNodeName(itemType, itemId) {
return itemType + "_" + itemId;
}

function makeBox(text, shape) {
if (shape === "square") return "[" + text + "]";
else if (shape === "diamond") return "{" + text + "}";
else if (shape === "rounded") return "(" + text + ")";
else if (shape === "circle") return "((" + text + "))";
else if (shape === "data") return "[(" + text + ")]";
else return "";
}

let arrow = " --- ";
/// Recursive function to go down the tree adding components
function addTree(itemType, parent, parentNode, outputmd) {
// look up the 'API name', e.g. "goals" for "TopLevelNormativeGoal"
let thisType = configData.navigation[itemType]["db_name"];
let boxShape = configData.navigation[itemType]["shape"];
// loop over all objects of this type
for (let i = 0; i < parent[thisType].length; i++) {
let thisObj = parent[thisType][i];
let thisNode = getNodeName(itemType, thisObj.id);
if (parentNode != null) {
outputmd +=
parentNode +
arrow +
thisNode +
makeBox(thisObj.name, boxShape) +
"\n";
} else {
outputmd += thisNode + makeBox(thisObj.name, boxShape) + "\n";
}
// add a click link to the node
outputmd +=
"\n click " +
thisNode +
' callback "' +
thisObj.short_description +
'"\n';
for (
let j = 0;
j < configData.navigation[itemType]["children"].length;
j++
) {
let childType = configData.navigation[itemType]["children"][j];
outputmd = addTree(childType, thisObj, thisNode, outputmd);
}
}
// console.log(outputmd)
return outputmd;
}

let outputmd = "graph TB; \n";
// call the recursive addTree function, starting with the Goal as the top node
outputmd = addTree("TopLevelNormativeGoal", in_json, null, outputmd);

return outputmd;
}

updateView() {
// render() will be called again anytime setState is called, which
// is done both by hideEditLayer() and hideCreateLayer()
Expand Down Expand Up @@ -209,7 +143,6 @@ class CaseContainer extends Component {
}

showEditLayer(itemType, itemId, event) {
console.log("in showEditLayer", this, itemId);
event.preventDefault();
// this should be redundant, as the itemId and itemType should already
// be set when showViewLayer is called, but they can't do any harm..
Expand All @@ -219,7 +152,6 @@ class CaseContainer extends Component {
}

showCreateLayer(itemType, parentId, event) {
console.log("in showCreateLayer", this, parentId);
event.preventDefault();
this.setState({ createItemType: itemType, createItemParentId: parentId });
this.setState({ showCreateLayer: true });
Expand Down
19 changes: 13 additions & 6 deletions frontend/src/components/tests/CaseContainer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { render, screen, waitFor } from "@testing-library/react";
import "regenerator-runtime/runtime";
import React from "react";
import "@testing-library/jest-dom";
import fetchMock from "jest-fetch-mock";

import CaseContainer from "../CaseContainer.js";

const mockedUsedNavigate = jest.fn();
Expand All @@ -10,20 +12,25 @@ jest.mock("react-router-dom", () => ({
useNavigate: () => mockedUsedNavigate,
}));

global.fetch = jest.fn(() =>
Promise.resolve({
json: () =>
Promise.resolve({ id: 1, name: "Test case", description: "", goals: [] }),
})
);
fetchMock.enableMocks();

beforeEach(() => {
fetch.resetMocks();
});

test("renders loading screen", () => {
fetch.mockResponseOnce(
JSON.stringify({ id: 1, name: "Test case", description: "", goals: [] })
);
render(<CaseContainer id="1" />);
const textElement = screen.getByText("loading");
expect(textElement).toBeInTheDocument();
});

test("renders case view", async () => {
fetch.mockResponseOnce(
JSON.stringify({ id: 1, name: "Test case", description: "", goals: [] })
);
render(<CaseContainer id="1" />);
await waitFor(() =>
expect(screen.getByDisplayValue("Test case")).toBeInTheDocument()
Expand Down
53 changes: 53 additions & 0 deletions frontend/src/components/tests/MermaidSyntax.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { render, screen, waitFor } from "@testing-library/react";
import "regenerator-runtime/runtime";
import React from "react";
import "@testing-library/jest-dom";
import { jsonToMermaid, sanitizeForMermaid } from "../utils.js";

test("Simple JSON translation", () => {
let input = {
id: 1,
name: "Test case",
description: "",
goals: [
{
id: 1,
name: "test goal",
short_description: "short",
long_description: "long",
context: [],
system_description: [],
property_claims: [],
},
],
};
let output = jsonToMermaid(input);
expect(output.includes("test goal"));
});

test("Sanitize removes brackets, semicolon", () => {
let input = "test (st;rin[g";
let output = sanitizeForMermaid(input);
expect(output == "test string");
});

test("jsonToMermaid sanitizes goal name", () => {
let input = {
id: 1,
name: "Test case",
description: "",
goals: [
{
id: 1,
name: "test ()goal;}",
short_description: "short",
long_description: "long",
context: [],
system_description: [],
property_claims: [],
},
],
};
let output = jsonToMermaid(input);
expect(output.includes("test goal"));
});
Loading

0 comments on commit cde750f

Please sign in to comment.