Skip to content

Commit

Permalink
Merge pull request #1885 from AntaresSimulatorTeam/feature/ANT-966-de…
Browse files Browse the repository at this point in the history
…bug-view-editor
  • Loading branch information
skamril committed Jan 10, 2024
2 parents abb141f + b54aa65 commit c711df3
Show file tree
Hide file tree
Showing 26 changed files with 1,121 additions and 946 deletions.
485 changes: 318 additions & 167 deletions webapp/package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"i18next-xhr-backend": "3.2.2",
"immer": "10.0.3",
"js-cookie": "3.0.5",
"jsoneditor": "9.10.4",
"jwt-decode": "3.1.2",
"lodash": "4.17.21",
"material-react-table": "2.0.5",
Expand Down Expand Up @@ -102,9 +103,11 @@
"proxy": "http://localhost:8080",
"homepage": "/",
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@total-typescript/ts-reset": "0.5.1",
"@types/debug": "4.1.9",
"@types/js-cookie": "3.0.4",
"@types/jsoneditor": "9.9.5",
"@types/lodash": "4.14.199",
"@types/ramda": "0.29.5",
"@types/react-beautiful-dnd": "13.1.5",
Expand All @@ -127,6 +130,7 @@
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"husky": "8.0.3",
"immutable": "3.8.2",
"jest-sonar-reporter": "2.0.0",
"prettier": "3.0.3",
"process": "0.11.10",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { AxiosError } from "axios";
import { useSnackbar } from "notistack";
import SaveIcon from "@mui/icons-material/Save";
import { Box, Button, Typography } from "@mui/material";
import { useUpdateEffect } from "react-use";
import {
editStudy,
getStudyData,
} from "../../../../../../../services/api/study";
import { Header, Root } from "./style";
import SimpleLoader from "../../../../../../common/loaders/SimpleLoader";
import JSONEditor from "../../../../../../common/JSONEditor";
import usePromiseWithSnackbarError from "../../../../../../../hooks/usePromiseWithSnackbarError";
import UsePromiseCond from "../../../../../../common/utils/UsePromiseCond";
import SimpleContent from "../../../../../../common/page/SimpleContent";
import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar";

interface Props {
path: string;
studyId: string;
}

function Json({ path, studyId }: Props) {
const [t] = useTranslation();
const { enqueueSnackbar } = useSnackbar();
const enqueueErrorSnackbar = useEnqueueErrorSnackbar();
const [jsonData, setJsonData] = useState<string | null>(null);
const [isSaveAllowed, setSaveAllowed] = useState(false);

const res = usePromiseWithSnackbarError(
() => getStudyData(studyId, path, -1),
{
errorMessage: t("studies.error.retrieveData"),
deps: [studyId, path],
},
);

/* Reset save button when path changes */
useUpdateEffect(() => {
setSaveAllowed(false);
}, [studyId, path]);

////////////////////////////////////////////////////////////////
// Event Handlers
////////////////////////////////////////////////////////////////

const handleSaveJson = async () => {
if (isSaveAllowed && jsonData) {
try {
await editStudy(jsonData, studyId, path);
enqueueSnackbar(t("studies.success.saveData"), {
variant: "success",
});
setSaveAllowed(false);
} catch (e) {
enqueueErrorSnackbar(t("studies.error.saveData"), e as AxiosError);
}
}
};

const handleJsonChange = (newJson: string) => {
setJsonData(newJson);
setSaveAllowed(true);
};

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

return (
<Root>
<Header>
<Button
variant="outlined"
color="primary"
startIcon={<SaveIcon sx={{ width: 15, height: 15 }} />}
onClick={handleSaveJson}
disabled={!isSaveAllowed}
>
<Typography sx={{ fontSize: "12px" }}>{t("global.save")}</Typography>
</Button>
</Header>
<UsePromiseCond
response={res}
ifPending={() => <SimpleLoader />}
ifResolved={(json) => (
<Box
sx={{
width: 1,
height: 1,
}}
>
<JSONEditor
json={json}
onChangeJSON={handleJsonChange}
onChangeText={handleJsonChange} // only for code mode
modes={["tree", "code"]}
enableSort={false}
enableTransform={false}
/>
</Box>
)}
ifRejected={(error) => <SimpleContent title={error?.toString()} />}
/>
</Root>
);
}

export default Json;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useOutletContext } from "react-router";
import { MatrixStats, StudyMetadata } from "../../../../../../../common/types";
import { Root, Content } from "./style";
import MatrixInput from "../../../../../../common/MatrixInput";

interface Props {
path: string;
}

function Matrix({ path }: Props) {
const { study } = useOutletContext<{ study: StudyMetadata }>();

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

return (
<Root>
<Content>
<MatrixInput study={study} url={path} computStats={MatrixStats.NOCOL} />
</Content>
</Root>
);
}

export default Matrix;
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useState } from "react";
import { AxiosError } from "axios";
import { useSnackbar } from "notistack";
import { useTranslation } from "react-i18next";
import { Button } from "@mui/material";
import UploadOutlinedIcon from "@mui/icons-material/UploadOutlined";
import {
getStudyData,
importFile,
} from "../../../../../../../services/api/study";
import { Content, Header, Root } from "./style";
import useEnqueueErrorSnackbar from "../../../../../../../hooks/useEnqueueErrorSnackbar";
import SimpleLoader from "../../../../../../common/loaders/SimpleLoader";
import ImportDialog from "../../../../../../common/dialogs/ImportDialog";
import usePromiseWithSnackbarError from "../../../../../../../hooks/usePromiseWithSnackbarError";
import SimpleContent from "../../../../../../common/page/SimpleContent";
import UsePromiseCond from "../../../../../../common/utils/UsePromiseCond";
import { useDebugContext } from "../DebugContext";

interface Props {
studyId: string;
path: string;
}

function Text({ studyId, path }: Props) {
const [t] = useTranslation();
const { reloadTreeData } = useDebugContext();
const { enqueueSnackbar } = useSnackbar();
const enqueueErrorSnackbar = useEnqueueErrorSnackbar();
const [openImportDialog, setOpenImportDialog] = useState(false);

const res = usePromiseWithSnackbarError(() => getStudyData(studyId, path), {
errorMessage: t("studies.error.retrieveData"),
deps: [studyId, path],
});

////////////////////////////////////////////////////////////////
// Event Handlers
////////////////////////////////////////////////////////////////

const handleImport = async (file: File) => {
try {
await importFile(file, studyId, path);
reloadTreeData();
enqueueSnackbar(t("studies.success.saveData"), {
variant: "success",
});
} catch (e) {
enqueueErrorSnackbar(t("studies.error.saveData"), e as AxiosError);
}
};

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

return (
<Root>
<Header>
<Button
variant="outlined"
color="primary"
startIcon={<UploadOutlinedIcon />}
onClick={() => setOpenImportDialog(true)}
sx={{ mb: 1 }}
>
{t("global.import")}
</Button>
</Header>
<UsePromiseCond
response={res}
ifPending={() => <SimpleLoader />}
ifResolved={(data) => (
<Content>
<code style={{ whiteSpace: "pre" }}>{data}</code>
</Content>
)}
ifRejected={(error) => <SimpleContent title={error?.toString()} />}
/>
{openImportDialog && (
<ImportDialog
open={openImportDialog}
onClose={() => setOpenImportDialog(false)}
onImport={handleImport}
/>
)}
</Root>
);
}

export default Text;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Text from "./Text";
import Json from "./Json";
import Matrix from "./Matrix";
import { FileType } from "../utils";

interface Props {
studyId: string;
fileType: FileType;
filePath: string;
}

const componentByFileType = {
matrix: Matrix,
json: Json,
file: Text,
} as const;

function Data({ studyId, fileType, filePath }: Props) {
const DataViewer = componentByFileType[fileType];

return <DataViewer studyId={studyId} path={filePath} />;
}

export default Data;
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const Header = styled(Box)(({ theme }) => ({
flexFlow: "row nowrap",
justifyContent: "space-between",
alignItems: "center",
padding: theme.spacing(0, 2),
marginBottom: theme.spacing(1),
}));

export const Content = styled(Paper)(({ theme }) => ({
Expand All @@ -32,5 +32,3 @@ export const Content = styled(Paper)(({ theme }) => ({
overflow: "auto",
position: "relative",
}));

export default {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createContext, useContext } from "react";
import { FileType, TreeData } from "./utils";

interface DebugContextProps {
treeData: TreeData;
onFileSelect: (fileType: FileType, filePath: string) => void;
reloadTreeData: () => void;
}

const initialDebugContextValue: DebugContextProps = {
treeData: {},
onFileSelect: () => {},
reloadTreeData: () => {},
};

const DebugContext = createContext<DebugContextProps>(initialDebugContextValue);

export const useDebugContext = (): DebugContextProps =>
useContext(DebugContext);

export default DebugContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Box } from "@mui/material";
import { TreeItem } from "@mui/x-tree-view";
import { TreeData, determineFileType, getFileIcon } from "../utils";
import { useDebugContext } from "../DebugContext";

interface Props {
name: string;
content: TreeData;
path: string;
}

function FileTreeItem({ name, content, path }: Props) {
const { onFileSelect } = useDebugContext();
const fullPath = `${path}/${name}`;
const fileType = determineFileType(content);
const FileIcon = getFileIcon(fileType);
const isFolderEmpty = !Object.keys(content).length;

////////////////////////////////////////////////////////////////
// Event handlers
////////////////////////////////////////////////////////////////

const handleClick = () => {
if (fileType !== "folder") {
onFileSelect(fileType, fullPath);
}
};

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

return (
<TreeItem
nodeId={fullPath}
label={
<Box
role="button"
sx={{
display: "flex",
alignItems: "center",
...(isFolderEmpty && { opacity: 0.5 }),
}}
onClick={handleClick}
>
<FileIcon sx={{ width: 20, height: "auto", p: 0.2 }} />
<span style={{ marginLeft: 4 }}>{name}</span>
</Box>
}
>
{typeof content === "object" &&
Object.keys(content).map((childName) => (
<FileTreeItem
key={childName}
name={childName}
content={content[childName]}
path={fullPath}
/>
))}
</TreeItem>
);
}

export default FileTreeItem;
Loading

0 comments on commit c711df3

Please sign in to comment.