viewer tutorial/tip (#104)

Co-authored-by: moon19960501@gmail.com <wenzhenhe1@gmail.com>
diff --git a/frontend/src/components/frame/containers/ServerStatusContainer.js b/frontend/src/components/frame/containers/ServerStatusContainer.js
index 07d5e87..b9f4b4c 100644
--- a/frontend/src/components/frame/containers/ServerStatusContainer.js
+++ b/frontend/src/components/frame/containers/ServerStatusContainer.js
@@ -21,16 +21,18 @@
 import { pinFrame, removeFrame } from '../../../features/frame/FrameSlice';
 import { generateCytoscapeMetadataElement } from '../../../features/cypher/CypherUtil';
 import ServerStatusFrame from '../presentations/ServerStatusFrame';
+import { openTutorial } from '../../../features/modal/ModalSlice';
 
 const mapStateToProps = (state) => {
   const generateElements = () => generateCytoscapeMetadataElement(state.metadata.rows);
 
   return {
     serverInfo: state.database,
+    isTutorial: state.modal.isTutorial,
     data: generateElements(),
   };
 };
 
-const mapDispatchToProps = { removeFrame, pinFrame };
+const mapDispatchToProps = { removeFrame, pinFrame, openTutorial };
 
 export default connect(mapStateToProps, mapDispatchToProps)(ServerStatusFrame);
diff --git a/frontend/src/components/frame/presentations/ServerStatusFrame.jsx b/frontend/src/components/frame/presentations/ServerStatusFrame.jsx
index 5d8d036..2e0e7b2 100644
--- a/frontend/src/components/frame/presentations/ServerStatusFrame.jsx
+++ b/frontend/src/components/frame/presentations/ServerStatusFrame.jsx
@@ -18,19 +18,23 @@
  */
 
 import React, { useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
 import PropTypes from 'prop-types';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faPlayCircle } from '@fortawesome/free-regular-svg-icons';
+import { faPlayCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
 import { Col, Row } from 'antd';
 import { Button } from 'react-bootstrap';
 import MetadataCytoscapeChart from '../../cytoscape/MetadataCytoscapeChart';
 import InitGraphModal from '../../initializer/presentation/GraphInitializer';
 import Frame from '../Frame';
 import FrameStyles from '../Frame.module.scss';
+import Tutorial from '../../modal/containers/Tutorial';
+import { openTutorial } from '../../../features/modal/ModalSlice';
 
 const ServerStatusFrame = ({
-  refKey, isPinned, reqString, serverInfo, data,
+  refKey, isPinned, reqString, serverInfo, data, isTutorial,
 }) => {
+  const dispatch = useDispatch();
   const [elements, setElements] = useState({ edges: [], nodes: [] });
   const [showModal, setShow] = useState(false);
   const {
@@ -46,50 +50,54 @@
   const setContent = () => {
     if (status === 'connected') {
       return (
-        <div className={FrameStyles.FlexContentWrapper}>
-          <InitGraphModal show={showModal} setShow={setShow} />
-          <Row>
-            <Col span={6}>
-              <h3>Connection Status</h3>
-              <p>This is your current connection information.</p>
-            </Col>
-            <Col span={18}>
-              <p>
-                You are connected as user&nbsp;
-                <strong>{user}</strong>
-              </p>
-              <p>
-                to&nbsp;
-                <strong>
-                  {host}
-                  :
-                  {port}
-                  /
-                  {database}
-                </strong>
-              </p>
-              <p>
-                Graph path has been set to&nbsp;
-                <strong>{graph}</strong>
-              </p>
-            </Col>
-            <Col>
-              <p>
-                <Button onClick={() => setShow(!showModal)}>Create Graph</Button>
-              </p>
-            </Col>
-          </Row>
+        <>
+          { isTutorial && <Tutorial />}
+          <div className={FrameStyles.FlexContentWrapper}>
+            <InitGraphModal show={showModal} setShow={setShow} />
+            <Row>
+              <Col span={6}>
+                <h3>Connection Status</h3>
+                <p>This is your current connection information.</p>
+              </Col>
+              <Col span={18}>
+                <p>
+                  You are connected as user&nbsp;
+                  <strong>{user}</strong>
+                </p>
+                <p>
+                  to&nbsp;
+                  <strong>
+                    {host}
+                    :
+                    {port}
+                    /
+                    {database}
+                  </strong>
+                </p>
+                <p>
+                  Graph path has been set to&nbsp;
+                  <strong>{graph}</strong>
+                </p>
+              </Col>
+              <Col>
+                <p>
+                  <Button onClick={() => setShow(!showModal)}>Create Graph</Button>
+                  <FontAwesomeIcon onClick={() => dispatch(openTutorial())} icon={faQuestionCircle} size="lg" style={{ marginLeft: '1rem', cursor: 'pointer' }} />
+                </p>
+              </Col>
+            </Row>
 
-          <hr style={{
-            color: 'rgba(0,0,0,.125)',
-            backgroundColor: '#fff',
-            margin: '0px 10px 0px 10px',
-            height: '0.3px',
-          }}
-          />
+            <hr style={{
+              color: 'rgba(0,0,0,.125)',
+              backgroundColor: '#fff',
+              margin: '0px 10px 0px 10px',
+              height: '0.3px',
+            }}
+            />
 
-          <MetadataCytoscapeChart elements={elements} />
-        </div>
+            <MetadataCytoscapeChart elements={elements} />
+          </div>
+        </>
       );
     }
     if (status === 'disconnected') {
@@ -145,6 +153,7 @@
   }).isRequired,
   // eslint-disable-next-line react/forbid-prop-types
   data: PropTypes.any.isRequired,
+  isTutorial: PropTypes.bool.isRequired,
 };
 
 export default ServerStatusFrame;
diff --git a/frontend/src/components/modal/containers/Tutorial.js b/frontend/src/components/modal/containers/Tutorial.js
new file mode 100644
index 0000000..d44ac38
--- /dev/null
+++ b/frontend/src/components/modal/containers/Tutorial.js
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 { connect } from 'react-redux';
+import TutorialDialog from '../presentations/TutorialDialog';
+import { closeTutorial } from '../../../features/modal/ModalSlice';
+
+const mapStateToProps = () => ({});
+const mapDispatchToProps = { closeTutorial };
+
+export default connect(mapStateToProps, mapDispatchToProps)(TutorialDialog);
diff --git a/frontend/src/components/modal/presentations/TutorialBody.jsx b/frontend/src/components/modal/presentations/TutorialBody.jsx
new file mode 100644
index 0000000..2c313ca
--- /dev/null
+++ b/frontend/src/components/modal/presentations/TutorialBody.jsx
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { Modal, Image } from 'react-bootstrap';
+import Agce from '../../../images/agce.gif';
+import Query from '../../../images/queryEditor.png';
+import Copy from '../../../images/copy.png';
+import CSV from '../../../images/graphCSV.png';
+import Menu from '../../../images/graphMenu.png';
+
+const TutorialBody = ({ page, text, addiText }) => {
+  const [curImg, setCurImg] = useState();
+  const [addiImg, setAddiImg] = useState();
+  const [link, setLink] = useState();
+  useEffect(() => {
+    if (page === 1) {
+      setCurImg(Agce);
+      setAddiImg();
+    }
+    if (page === 2) {
+      setCurImg(Query);
+      setAddiImg(Copy);
+    }
+    if (page === 3) {
+      setLink(addiText);
+      setCurImg(CSV);
+      setAddiImg();
+    }
+    if (page === 4) setCurImg(Menu);
+    if (page === 5) setCurImg();
+  }, [page]);
+
+  return (
+    <Modal.Body style={{ height: '16rem', display: 'border-box', overflowY: 'scroll' }}>
+      <p>
+        {text}
+      </p>
+      <Image className="tutorial-img" src={curImg} fluid />
+      <br />
+      <br />
+      { page === 3
+        && (
+          <p>
+            The format of the csv files is as described here.
+            <br />
+            <br />
+            {link}
+          </p>
+        )}
+      { addiImg
+        ? (
+          <div style={{ margin: '1rem 0' }}>
+            <p>
+              {addiText}
+            </p>
+            <Image className="tutorial-img" src={addiImg} fluid />
+          </div>
+        ) : (<></>)}
+    </Modal.Body>
+  );
+};
+
+TutorialBody.propTypes = {
+  page: PropTypes.number.isRequired,
+  text: PropTypes.string.isRequired,
+  addiText: PropTypes.string.isRequired,
+};
+
+export default TutorialBody;
diff --git a/frontend/src/components/modal/presentations/TutorialDialog.jsx b/frontend/src/components/modal/presentations/TutorialDialog.jsx
new file mode 100644
index 0000000..d8bc626
--- /dev/null
+++ b/frontend/src/components/modal/presentations/TutorialDialog.jsx
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { Modal } from 'react-bootstrap';
+import TutorialFooter from './TutorialFooter';
+import TutorialHeader from './TutorialHeader';
+import TutorialBody from './TutorialBody';
+
+const TutorialDialog = ({ closeTutorial }) => {
+  const [page, setPage] = useState(1);
+  const firstPage = (curPage) => {
+    if (curPage === 1) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="Apache-Age Viewer is a web based user interface that provides visualization of graph data stored in a postgreSQL database with AGE extension. It is graph visualisation tool, for Apache AGE."
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    if (curPage === 2) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="A graph consists of a set of vertices and edges, where each individual node and edge possesses a map of properties. You can use the Query Editor to create and run scripts containing cypher query statements."
+                addiText="You can get previous commands using ctrl + ↑/ctrl+ ↓ keyboard shortcut. The below icon also allows previous queries to be copied to editor."
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    if (curPage === 3) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="This feature allows users to create and initialize a graph using csv file.ID is required when initializing."
+                addiText="https://age.apache.org/age-manual/master/intro/agload.html#explanation-about-the-csv-format"
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    if (curPage === 4) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="After creating nodes/edges and running queries, you can select a menu item and perform a command on either a node or a edge in frame."
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    if (curPage === 5) {
+      return (
+        <div className="tutorialModal-container">
+          <div
+            style={{ display: 'block', position: 'initial' }}
+          >
+            <Modal
+              show
+              backdrop="static"
+              keyboard={false}
+              style={{ width: '32rem' }}
+              className="tutorial-modal"
+            >
+              <TutorialHeader page={page} />
+              <TutorialBody
+                page={curPage}
+                text="There are multiple ways you can contribute to the Apache AGE and Apache AGE Viewer projects. If you are interested in the project and looking for ways to help, consult the list of projects in the Apache AGE and AGE Viewer GitHubs."
+              />
+              <TutorialFooter page={curPage} setPage={setPage} closeTutorial={closeTutorial} />
+            </Modal>
+          </div>
+        </div>
+      );
+    }
+    return null;
+  };
+
+  return (
+    <>
+      {firstPage(page)}
+    </>
+  );
+};
+
+TutorialDialog.propTypes = {
+  closeTutorial: PropTypes.func.isRequired,
+};
+
+export default TutorialDialog;
diff --git a/frontend/src/components/modal/presentations/TutorialFooter.jsx b/frontend/src/components/modal/presentations/TutorialFooter.jsx
new file mode 100644
index 0000000..8085597
--- /dev/null
+++ b/frontend/src/components/modal/presentations/TutorialFooter.jsx
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { Modal, Button } from 'react-bootstrap';
+
+const TutorialFooter = ({ page, setPage, closeTutorial }) => {
+  const [curPage, setCurPage] = useState();
+
+  useEffect(() => {
+    setCurPage(page);
+  }, [page]);
+
+  return (
+    <Modal.Footer>
+      <div>
+        <Button onClick={() => closeTutorial()} className="tutorial-button" variant="secondary">Close</Button>
+      </div>
+      <div>
+        <Button className="tutorial-button" variant={curPage === 1 ? 'outline-secondary' : 'secondary'} style={{ marginRight: '1rem' }} onClick={() => { setPage(curPage > 1 ? curPage - 1 : curPage); }}>Previous Tip</Button>
+        <Button className="tutorial-button" variant={curPage === 5 ? 'outline-primary' : 'primary'} onClick={() => { setPage(curPage < 5 ? curPage + 1 : curPage); }}>Next Tip</Button>
+      </div>
+    </Modal.Footer>
+  );
+};
+
+TutorialFooter.propTypes = {
+  page: PropTypes.number.isRequired,
+  setPage: PropTypes.func.isRequired,
+  closeTutorial: PropTypes.func.isRequired,
+};
+
+export default TutorialFooter;
diff --git a/frontend/src/components/modal/presentations/TutorialHeader.jsx b/frontend/src/components/modal/presentations/TutorialHeader.jsx
new file mode 100644
index 0000000..5628a6b
--- /dev/null
+++ b/frontend/src/components/modal/presentations/TutorialHeader.jsx
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { Modal } from 'react-bootstrap';
+
+const TutorialHeader = ({ page }) => {
+  const [curPage, setCurPage] = useState();
+
+  useEffect(() => {
+    setCurPage(page);
+  }, [page]);
+
+  return (
+    <Modal.Header
+      style={{
+        padding: '0.3rem 0.5rem 0 0.5rem', borderBottom: '1px solid black', margin: '0', background: '#A9A9A9',
+      }}
+    >
+      <Modal.Title style={{ fontSize: '0.88rem', paddingBottom: '0px', color: '#F0FFF0' }}>
+        {`Tip of AGE Viewer -${curPage}`}
+      </Modal.Title>
+    </Modal.Header>
+  );
+};
+
+TutorialHeader.propTypes = {
+  page: PropTypes.string.isRequired,
+};
+
+export default TutorialHeader;
diff --git a/frontend/src/features/modal/ModalSlice.js b/frontend/src/features/modal/ModalSlice.js
index a9003df..4eb2697 100644
--- a/frontend/src/features/modal/ModalSlice.js
+++ b/frontend/src/features/modal/ModalSlice.js
@@ -24,6 +24,7 @@
   name: 'modal',
   initialState: {
     isOpen: false,
+    isTutorial: false,
     graphHistory: [],
     elementHistory: [],
   },
@@ -38,6 +39,16 @@
         state.isOpen = false;
       },
     },
+    openTutorial: {
+      reducer: (state) => {
+        state.isTutorial = true;
+      },
+    },
+    closeTutorial: {
+      reducer: (state) => {
+        state.isTutorial = false;
+      },
+    },
     addGraphHistory: {
       reducer: (state, action) => {
         state.graphHistory.push(action.payload.graph);
@@ -66,6 +77,8 @@
 export const {
   openModal,
   closeModal,
+  openTutorial,
+  closeTutorial,
   addGraphHistory,
   addElementHistory,
   removeGraphHistory,
diff --git a/frontend/src/images/agce.gif b/frontend/src/images/agce.gif
new file mode 100644
index 0000000..8536175
--- /dev/null
+++ b/frontend/src/images/agce.gif
Binary files differ
diff --git a/frontend/src/images/copy.png b/frontend/src/images/copy.png
new file mode 100644
index 0000000..f70d29c
--- /dev/null
+++ b/frontend/src/images/copy.png
Binary files differ
diff --git a/frontend/src/images/graphCSV.png b/frontend/src/images/graphCSV.png
new file mode 100644
index 0000000..578f53b
--- /dev/null
+++ b/frontend/src/images/graphCSV.png
Binary files differ
diff --git a/frontend/src/images/graphMenu.png b/frontend/src/images/graphMenu.png
new file mode 100644
index 0000000..14bff11
--- /dev/null
+++ b/frontend/src/images/graphMenu.png
Binary files differ
diff --git a/frontend/src/images/queryEditor.png b/frontend/src/images/queryEditor.png
new file mode 100644
index 0000000..820e5d2
--- /dev/null
+++ b/frontend/src/images/queryEditor.png
Binary files differ
diff --git a/frontend/src/static/style.css b/frontend/src/static/style.css
index 2dd133d..62a8f43 100644
--- a/frontend/src/static/style.css
+++ b/frontend/src/static/style.css
@@ -808,8 +808,26 @@
     height: 100%;
     background: rgba(0, 0, 0, 0.7);
     z-index: 9999;
+    display: flex;
     font-size: 16px;
     display:flex;
     align-items: center;
     justify-content: center;
+}
+
+.tutorial-modal {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    display:flex;
+    align-items: center;
+    justify-content: center;
+    height: auto;
+}
+
+.tutorial-button {
+    font-size: 0.7rem;
+    font-weight: bold;
+    padding: 0.2rem 0.35rem;
 }
\ No newline at end of file