From ffd588ae169696edaff726855b41cc260f03c9b7 Mon Sep 17 00:00:00 2001 From: freshavocado7 <nikidimitriou97@gmail.com> Date: Tue, 23 Apr 2024 16:20:54 +0200 Subject: [PATCH] style: Frontend overhaul with many small changes --- .pre-commit-config.yaml | 8 + CONTRIBUTING.md | 7 +- Dockerfile | 2 +- README.md | 4 +- capella_model_explorer/backend/explorer.py | 19 +- entrypoint.sh | 2 +- frontend/index.html | 27 +-- frontend/package.json | 10 +- frontend/src/App.css | 2 +- frontend/src/components/Breadcrumbs.jsx | 34 +-- frontend/src/components/Button.jsx | 12 +- frontend/src/components/Card.jsx | 16 +- frontend/src/components/Header.jsx | 25 ++- frontend/src/components/InstanceView.jsx | 175 +++++++-------- frontend/src/components/Spinner.jsx | 8 +- frontend/src/components/TemplateCard.jsx | 26 ++- frontend/src/components/TemplateDetails.jsx | 207 +++++++++--------- frontend/src/components/ThemeSwitcher.jsx | 40 ++-- frontend/src/components/ViewsList.jsx | 56 +++-- .../src/components/WiredTemplatesList.jsx | 79 +++---- frontend/src/index.css | 105 +++++++-- frontend/src/views/HomeView.jsx | 86 ++++---- frontend/src/views/TemplateView.jsx | 98 +++++---- frontend/tailwind.config.js | 30 ++- templates/__generic__.html.j2 | 2 +- templates/classes.html.j2 | 1 - templates/common_macros.html.j2 | 2 +- templates/operational-activity.html.j2 | 24 +- templates/operational-capability.html.j2 | 5 +- templates/operational-entity.html.j2 | 12 +- templates/req.html.j2 | 2 +- templates/req.yaml | 2 +- templates/system-capability.html.j2 | 2 +- templates/system_function.html.j2 | 2 +- 34 files changed, 641 insertions(+), 491 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7c5703e..54253a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -114,3 +114,11 @@ repos: hooks: - id: commitlint stages: [commit-msg] + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + types_or: [ts, css, html, markdown] + additional_dependencies: + - 'prettier@^3.2.5' + - 'prettier-plugin-tailwindcss@^0.5.14' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c98c895..b5dd31f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,8 +76,7 @@ The key differences are: - **Docstrings**: The [Numpy style guide] applies here. - [numpy style guide]: - https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard + [numpy style guide]: https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard When writing docstrings for functions, use the imperative style, as per [PEP-257]). For example, write "Do X and Y" instead of "Does X and Y". @@ -124,7 +123,6 @@ The key differences are: typing related classes like `t.TypedDict`. <!-- prettier-ignore --> - Use the new syntax and classes for typing introduced with Python 3.10. - Instead of `t.Tuple`, `t.List` etc. use the builtin classes `tuple`, `list` @@ -142,8 +140,7 @@ The key differences are: If you have set up black correctly, you don't need to worry about this though :) - [black code style]: - https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html + [black code style]: https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html - When working with `dict`s, consider using `t.TypedDict` instead of a more generic `dict[str, float|int|str]`-like annotation where possible, as the diff --git a/Dockerfile b/Dockerfile index 1e8b0a8..5ba7f75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,4 +43,4 @@ RUN chmod +x /entrypoint.sh ENV MODEL_ENTRYPOINT=/model RUN chmod -R 777 ./frontend/dist/ -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 5485cfa..2cd5ac5 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ We see a larger non-MBSE crowd struggling with the things hidden in the model. W **Use cases**: -- Provide insights into / "spell-out" the model for non-MBSE stakeholders via document-a-like dynamic views that describe model elements in a human-readable form. -- Provide meaningful default views (that can be further customized) for the key elements to kickstart the model exploration. +- Provide insights into / "spell-out" the model for non-MBSE stakeholders via document-a-like dynamic views that describe model elements in a human-readable form. +- Provide meaningful default views (that can be further customized) for the key elements to kickstart the model exploration. There are a few more use cases but we will reveal them a bit later. diff --git a/capella_model_explorer/backend/explorer.py b/capella_model_explorer/backend/explorer.py index a248726..bed0266 100644 --- a/capella_model_explorer/backend/explorer.py +++ b/capella_model_explorer/backend/explorer.py @@ -181,14 +181,23 @@ def read_template(template_name: str): filters=filters, ) base["objects"] = [ - {"idx": obj.uuid, - "name": str( - obj.name if obj.name else ( - obj.long_name if hasattr(obj, "long_name") else "undefined") - )} for obj in objects + { + "idx": obj.uuid, + "name": str( + obj.name + if obj.name + else ( + obj.long_name + if hasattr(obj, "long_name") + else "undefined" + ) + ), + } + for obj in objects ] except Exception as e: import traceback + LOGGER.exception( "Error finding objects for template %s", template_name ) diff --git a/entrypoint.sh b/entrypoint.sh index 29c0dbb..3ea167d 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -7,4 +7,4 @@ sed -i "s|__ROUTE_PREFIX__|${ROUTE_PREFIX}|g" ./frontend/dist/static/env.js sed -i "s|href=\"/|href=\"${ROUTE_PREFIX}/|g" ./frontend/dist/index.html sed -i "s|src=\"/|src=\"${ROUTE_PREFIX}/|g" ./frontend/dist/index.html -exec python -m capella_model_explorer.backend ${MODEL_ENTRYPOINT} /views \ No newline at end of file +exec python -m capella_model_explorer.backend ${MODEL_ENTRYPOINT} /views diff --git a/frontend/index.html b/frontend/index.html index aa9c08b..cbe16b5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,20 +3,17 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -<!DOCTYPE html> +<!doctype html> <html lang="en"> - <head> - <meta charset="UTF-8" /> - <link rel="icon" type="image/svg+xml" href="/static/vite.svg" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1.0" - /> - <title>Model Explorer</title> - </head> - <body> - <div id="root"></div> - <script type="module" src="/src/main.jsx"></script> - <script src="/static/env.js"></script> - </body> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/svg+xml" href="/static/vite.svg" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Model Explorer</title> + </head> + <body> + <div id="root"></div> + <script type="module" src="/src/main.jsx"></script> + <script src="/static/env.js"></script> + </body> </html> diff --git a/frontend/package.json b/frontend/package.json index 5c348b2..a79afc5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,6 @@ "name": "frontend", "private": true, "version": "0.0.0", - "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -12,9 +11,13 @@ "build-storybook": "storybook build" }, "dependencies": { + "lucide-react": "^0.372.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-zoom-pan-pinch": "^3.4.3" + "react-modal": "^3.16.1", + "react-responsive": "^10.0.0", + "react-zoom-pan-pinch": "^3.4.3", + "tailwind-scrollbar": "^3.1.0" }, "devDependencies": { "@storybook/addon-essentials": "^7.6.16", @@ -37,9 +40,10 @@ "eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-storybook": "^0.8.0", "postcss": "^8.4.35", + "prettier": "^3.2.5", + "prettier-plugin-tailwindcss": "^0.5.14", "prop-types": "^15.8.1", "react-router-dom": "^6.22.2", - "react-responsive": "^10.0.0", "storybook": "^7.6.16", "tailwindcss": "^3.4.1", "vite": "^5.1.4", diff --git a/frontend/src/App.css b/frontend/src/App.css index 0cc3c92..cb5363c 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -16,7 +16,7 @@ transition: filter 300ms; } .logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); + filter: drop-shadow(0 0 2em #646cff); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); diff --git a/frontend/src/components/Breadcrumbs.jsx b/frontend/src/components/Breadcrumbs.jsx index 51516d7..0a1bf12 100644 --- a/frontend/src/components/Breadcrumbs.jsx +++ b/frontend/src/components/Breadcrumbs.jsx @@ -1,14 +1,14 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { API_BASE_URL } from '../APIConfig'; +import React, { useEffect, useState } from "react"; +import { Link, useLocation } from "react-router-dom"; +import { API_BASE_URL } from "../APIConfig"; export const Breadcrumbs = () => { const location = useLocation(); const [breadcrumbLabels, setBreadcrumbLabels] = useState({}); - const pathnames = location.pathname.split('/').filter(x => x); + const pathnames = location.pathname.split("/").filter((x) => x); const [error, setError] = useState(null); const fetchModelInfo = async () => { @@ -23,9 +23,9 @@ export const Breadcrumbs = () => { const viewsDict = await response.json(); const allViews = Object.values(viewsDict).flat(); - const view = allViews.find(v => v.idx.toString() === idx); + const view = allViews.find((v) => v.idx.toString() === idx); return view ? view.name : idx; -}; + }; // Function to fetch object names const fetchObjectName = async (uuid) => { @@ -37,10 +37,10 @@ export const Breadcrumbs = () => { useEffect(() => { const updateLabels = async () => { const title = await fetchModelInfo(); - const labels = { '/': title }; + const labels = { "/": title }; for (let i = 0; i < pathnames.length; i++) { - const to = `/${pathnames.slice(0, i + 1).join('/')}`; + const to = `/${pathnames.slice(0, i + 1).join("/")}`; if (i === 0) { labels[to] = await fetchViewName(pathnames[i]); @@ -58,24 +58,30 @@ export const Breadcrumbs = () => { updateLabels(); }, [location]); - const visible_pathnames = [breadcrumbLabels['/'], ...location.pathname.split('/').filter(x => x)]; + const visible_pathnames = [ + breadcrumbLabels["/"], + ...location.pathname.split("/").filter((x) => x), + ]; return ( - <nav aria-label="breadcrumb" className="flex items-center"> + <nav + aria-label="breadcrumb" + className="flex items-center text-black dark:text-gray-200 font-medium" + > <ol className="flex items-center"> - <li className="flex items-center dark:text-gray-200"> - <Link to={"/"}>{breadcrumbLabels['/']}</Link> + <li className="flex items-center"> + <Link to={"/"}>{breadcrumbLabels["/"]}</Link> <span className="mx-2">/</span> </li> {visible_pathnames.slice(1).map((value, index) => { const last = index === visible_pathnames.length - 2; - const to = `/${visible_pathnames.slice(1, index + 2).join('/')}`; + const to = `/${visible_pathnames.slice(1, index + 2).join("/")}`; const label = breadcrumbLabels[to] || value; return ( <li className="flex items-center" key={to}> {!last && <Link to={to}>{label}</Link>} - {last && <span className='dark:text-gray-200' >{label}</span>} + {last && <span className=" text-custom-blue">{label}</span>} {!last && <span className="mx-2">/</span>} </li> ); diff --git a/frontend/src/components/Button.jsx b/frontend/src/components/Button.jsx index e8b6791..1a1d0e8 100644 --- a/frontend/src/components/Button.jsx +++ b/frontend/src/components/Button.jsx @@ -1,12 +1,16 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React from "react"; export const Button = ({ theme, children, ...props }) => { return ( - <a href="#" {...props} className="print:hidden rounded-md mx-1 bg-blue-800 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 hover:text-gray-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "> - {children} + <a + href="#" + {...props} + className="print:hidden rounded-md mx-1 bg-custom-blue px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm dark:shadow-dark hover:bg-custom-blue-hover hover:text-gray-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 " + > + {children} </a> ); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/Card.jsx b/frontend/src/components/Card.jsx index b97d49a..f0955e4 100644 --- a/frontend/src/components/Card.jsx +++ b/frontend/src/components/Card.jsx @@ -1,11 +1,13 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React from "react"; -export const Card = ({children, onClick}) => ( - <div onClick={onClick} - className='max-w-sm bg-white rounded-lg border border-gray-200 shadow-md m-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 dark:bg-gray-800 dark:border-gray-700'> - {children} - </div> - ); +export const Card = ({ children, onClick }) => ( + <div + onClick={onClick} + className="m-2 max-w-sm cursor-pointer rounded-lg border border-gray-200 bg-white shadow-md hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:shadow-dark dark:hover:bg-gray-700" + > + {children} + </div> +); diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 1cad8e3..33e968a 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -2,15 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 import React from "react"; -import {Breadcrumbs} from "./Breadcrumbs"; -import {ThemeSwitcher} from "./ThemeSwitcher"; +import { Breadcrumbs } from "./Breadcrumbs"; +import { ThemeSwitcher } from "./ThemeSwitcher"; export const Header = () => { - return ( - <header className="print:hidden text-gray-700 p-4 flex justify-between items-center"> - <div><Breadcrumbs /></div> - <div></div> - <div><ThemeSwitcher /></div> - </header> - ); - }; + return ( + <header className="flex items-center justify-between rounded-b-lg bg-gray-100 p-6 text-lg text-white shadow-lg dark:bg-custom-dark-2 dark:shadow-dark print:hidden"> + {" "} + <div> + <Breadcrumbs /> + </div> + <div></div> + <div> + <ThemeSwitcher /> + </div> + </header> + ); +}; diff --git a/frontend/src/components/InstanceView.jsx b/frontend/src/components/InstanceView.jsx index d1e41e9..e17bf9f 100644 --- a/frontend/src/components/InstanceView.jsx +++ b/frontend/src/components/InstanceView.jsx @@ -7,97 +7,92 @@ import { Spinner } from "./Spinner"; import { Button } from "./Button"; export const InstanceView = ({ templateName, objectID, endpoint }) => { - const [details, setDetails] = useState([]); - const [loading, setLoading] = useState(true); - const contentRef = useRef(null); - const [isHovering, setIsHovering] = useState(false); + const [details, setDetails] = useState([]); + const [loading, setLoading] = useState(true); + const contentRef = useRef(null); + const [isHovering, setIsHovering] = useState(false); - useEffect(() => { - setLoading(true); - const url = endpoint + `${templateName}/${objectID}`; - fetch(url, { - method: "GET", - headers: { - "Content-Type": "text/html", - }, - }) - .then((response) => response.text()) - .then((data) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(data, "text/html"); - const contentItems = []; - doc.body.childNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.tagName === "svg") { - contentItems.push({ - type: "SVGDisplay", - content: node.outerHTML, - }); - } else { - contentItems.push({ - type: "HTML", - content: node.outerHTML, - }); - } - } - }); - setDetails(contentItems); - setLoading(false); - if (contentRef.current) - contentRef.current.scrollIntoView(); - }) - .catch((error) => { - setLoading(false); - setDetails("Error fetching data ", error); - }); - }, [endpoint, objectID, templateName]); - if (loading) - return ( - <div> - <Spinner /> - </div> - ); + useEffect(() => { + setLoading(true); + const url = endpoint + `${templateName}/${objectID}`; + fetch(url, { + method: "GET", + headers: { + "Content-Type": "text/html", + }, + }) + .then((response) => response.text()) + .then((data) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(data, "text/html"); + const contentItems = []; + doc.body.childNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.tagName === "svg") { + contentItems.push({ + type: "SVGDisplay", + content: node.outerHTML, + }); + } else { + contentItems.push({ + type: "HTML", + content: node.outerHTML, + }); + } + } + }); + setDetails(contentItems); + setLoading(false); + if (contentRef.current) contentRef.current.scrollIntoView(); + }) + .catch((error) => { + setLoading(false); + setDetails("Error fetching data ", error); + }); + }, [endpoint, objectID, templateName]); + if (loading) return ( - <div - ref={contentRef} - className={`html-content bg-white shadow-lg dark:shadow-white text-gray-700 mx-auto md:my-8 p-8 md:w-[210mm] max-w-full max-h-full overflow-auto print:shadow-none print:m-0 print:p-0 print:bg-transparent relative box-border border-4 ${ - isHovering - ? "border-grey-700 shadow-md z-50" - : "border-transparent" - }`} - onMouseEnter={() => setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - > - {isHovering && ( - <Button - style={{ - position: "absolute", - top: 0, - right: 0, - margin: "1rem", - padding: "0.5rem", - }} - onClick={() => window.print()} - > - Print Content - </Button> - )} - {details.map((item, idx) => { - if (item.type === "SVGDisplay") { - return ( - <SVGDisplay key={idx} content={item.content} /> - ); - } else { - return ( - <div - key={idx} - dangerouslySetInnerHTML={{ - __html: item.content, - }} - /> - ); - } - })} - </div> + <div className="mx-auto md:w-[210mm]"> + <Spinner /> + </div> ); + return ( + <div + ref={contentRef} + className={`html-content relative mx-auto box-border h-[75vh] max-h-full overflow-auto rounded-lg border-4 border-transparent bg-gray-100 p-8 text-gray-700 shadow-lg scrollbar scrollbar-track-gray-200 scrollbar-thumb-gray-400 hover:border-gray-300 dark:bg-custom-dark-2 dark:text-gray-100 dark:shadow-dark dark:scrollbar-track-custom-dark-3 dark:scrollbar-thumb-slate-600 md:w-[210mm] print:m-0 print:bg-transparent print:p-0 print:shadow-none ${ + isHovering ? "z-50 shadow-md" : "" + }`} + onMouseEnter={() => setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + {isHovering && ( + <Button + style={{ + position: "absolute", + top: 0, + right: 0, + margin: "1rem", + padding: "0.5rem", + }} + onClick={() => window.print()} + > + Print Content + </Button> + )} + {details.map((item, idx) => { + if (item.type === "SVGDisplay") { + return <SVGDisplay key={idx} content={item.content} />; + } else { + return ( + <div + key={idx} + dangerouslySetInnerHTML={{ + __html: item.content, + }} + /> + ); + } + })} + </div> + ); }; diff --git a/frontend/src/components/Spinner.jsx b/frontend/src/components/Spinner.jsx index 4d45258..7cea3b4 100644 --- a/frontend/src/components/Spinner.jsx +++ b/frontend/src/components/Spinner.jsx @@ -1,10 +1,10 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React from "react"; export const Spinner = ({}) => ( - <div className='flex justify-center items-center'> - <div className='animate-spin-slow rounded-full h-12 w-12 border-t-4 border-b-4 border-sky-500 ease-linear'></div> - </div> + <div className="mt-32 flex items-center justify-center"> + <div className="h-12 w-12 animate-spin-slow rounded-full border-b-4 border-t-4 border-custom-blue ease-linear"></div> + </div> ); diff --git a/frontend/src/components/TemplateCard.jsx b/frontend/src/components/TemplateCard.jsx index 1f1740f..6b402f6 100644 --- a/frontend/src/components/TemplateCard.jsx +++ b/frontend/src/components/TemplateCard.jsx @@ -1,16 +1,20 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React from "react"; -export const TemplateCard = ({template, onClickCallback}) => ( - <div onClick={() => onClickCallback(template.idx)} - className='max-w-sm bg-white rounded-lg border border-gray-200 shadow-md m-2 cursor-pointer hover:bg-gray-100'> - <div className='p-5'> - <h5 className='mb-2 text-2xl font-bold text-gray-900'> - {template.name} - </h5> - <p className='mb-3 font-normal text-gray-700'>{template.description}</p> - </div> +export const TemplateCard = ({ template, onClickCallback }) => ( + <div + onClick={() => onClickCallback(template.idx)} + className="m-2 mt-6 max-w-sm cursor-pointer rounded-lg bg-gray-300 shadow-md hover:bg-custom-light dark:bg-custom-dark-2 dark:shadow-dark dark:hover:bg-custom-dark-4" + > + <div className="p-5"> + <h5 className="mb-2 text-2xl font-bold text-gray-900 dark:text-gray-100"> + {template.name} + </h5> + <p className="mb-3 font-normal text-gray-700 dark:text-gray-300"> + {template.description} + </p> </div> - ); + </div> +); diff --git a/frontend/src/components/TemplateDetails.jsx b/frontend/src/components/TemplateDetails.jsx index dd51e83..4c2dd17 100644 --- a/frontend/src/components/TemplateDetails.jsx +++ b/frontend/src/components/TemplateDetails.jsx @@ -5,114 +5,111 @@ import React, { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; export const TemplateDetails = ({ endpoint, onSingleInstance }) => { - let { templateName, objectID } = useParams(); - const [error, setError] = useState(null); - const [details, setDetails] = useState([]); - const navigate = useNavigate(); - const [filterText, setFilterText] = useState(""); + let { templateName, objectID } = useParams(); + const [error, setError] = useState(null); + const [details, setDetails] = useState([]); + const navigate = useNavigate(); + const [filterText, setFilterText] = useState(""); - useEffect(() => { - const fetchDetails = async () => { - try { - const response = await fetch(endpoint + templateName, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - const data = await response.json(); - setDetails(data); - if (data.single) { - onSingleInstance("render"); - } - } catch (error) { - setError(error.message); - } finally { - } - }; - fetchDetails(); - }, [endpoint, templateName, objectID, onSingleInstance]); - if (error) { - return ( - <div className="bg-red-500 text-white p-4 rounded text-2xl"> - {error} - </div> - ); - } + useEffect(() => { + const fetchDetails = async () => { + try { + const response = await fetch(endpoint + templateName, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + setDetails(data); + if (data.single) { + onSingleInstance("render"); + } + } catch (error) { + setError(error.message); + } finally { + } + }; + fetchDetails(); + }, [endpoint, templateName, objectID, onSingleInstance]); + if (error) { return ( - <div className="flex flex-col h-full"> - <h5 className="mb-2 text-2xl font-bold text-gray-900 dark:text-white"> - {details.name} - </h5> - <p className="mb-3 font-normal text-gray-700 dark:text-white"> - {details.description} - </p> - {details.error ? ( - <div> - <p> - We failed to find matching template instances due to - the following error:{" "} - </p> - <div className="bg-red-500 py-2 px-2 text-white rounded"> - {details.error} - </div> - </div> + <div className="dark:bg-custom-dark-error rounded bg-red-500 p-4 text-2xl text-white"> + {error} + </div> + ); + } + return ( + <div className="flex h-full max-h-[calc(90vh-32px)] flex-col overflow-hidden rounded-lg bg-gray-100 p-4 shadow-lg dark:bg-custom-dark-2 dark:shadow-dark"> + <h5 className="mb-2 text-2xl font-bold text-gray-900 dark:text-gray-100 "> + {details.name} + </h5> + <p className="mb-3 font-normal text-gray-700 dark:text-gray-300"> + {details.description} + </p> + {details.error ? ( + <div> + <p> + We failed to find matching template instances due to the following + error:{" "} + </p> + <div className="dark:bg-custom-dark-error rounded bg-red-500 px-2 py-2 text-white"> + {details.error} + </div> + </div> + ) : ( + <> + {details.single === false ? ( + <input + type="text" + value={filterText} + onChange={(e) => setFilterText(e.target.value)} + placeholder="Filter objects" + className="mx-auto mb-3 mr-6 w-80 rounded border-2 border-gray-300 p-2 shadow-sm dark:border-gray-500 dark:bg-custom-dark-3" + /> + ) : ( + <></> + )} + <div className="flex flex-wrap items-center justify-center overflow-auto border-2 border-transparent text-left scrollbar scrollbar-track-gray-200 scrollbar-thumb-gray-400 dark:scrollbar-track-custom-dark-3 dark:scrollbar-thumb-slate-600 "> + {details.objects && + details.single === false && + details.objects.length === 0 ? ( + <p>No objects found</p> ) : ( - <> - {details.single === false ? ( - <input - type="text" - value={filterText} - onChange={(e) => setFilterText(e.target.value)} - placeholder="Filter objects" - className="mb-3 p-2 border rounded" - />):( <></>)} - <div className="flex flex-wrap justify-center items-center overflow-auto"> - {details.objects && details.single === false && details.objects.length === 0 ? ( - <p>No objects found</p> - ) : ( - details.objects && - details.objects - .filter((object) => - object.name - .toLowerCase() - .includes(filterText.toLowerCase()) - ) - .map((object) => ( - <div - key={object.idx} - onClick={() => { - navigate( - `/${templateName}/${object.idx}` - ); - }} - className={ - (objectID && - object.idx === objectID - ? "bg-blue-800 dark:bg-blue-800 text-white hover:dark:text-white hover:text-blue-800" - : "text-gray-900") + - " max-w-sm rounded-lg border border-gray-200 shadow-md m-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 dark:border-gray-700" - } - > - <div className="p-2"> - <h5 - className={ - "text-md font-bold dark:text-white " + - (objectID && - object.idx === objectID - ? "" - : "") - } - > - {object.name} - </h5> - </div> - </div> - )) - )} + details.objects && + details.objects + .filter((object) => + object.name.toLowerCase().includes(filterText.toLowerCase()), + ) + .map((object) => ( + <div + key={object.idx} + onClick={() => { + navigate(`/${templateName}/${object.idx}`); + }} + className={ + (objectID && object.idx === objectID + ? "w-full bg-custom-blue text-white dark:bg-custom-blue dark:text-gray-100" + : "w-full bg-gray-200 text-gray-900 dark:bg-custom-dark-4") + + " dark:bg-dark-quaternary m-2 max-w-sm cursor-pointer rounded-lg shadow-md hover:bg-custom-blue hover:text-white dark:border-gray-700 dark:shadow-dark dark:hover:bg-blue-500 " + } + > + <div className="p-2"> + <h5 + className={ + "text-md font-bold dark:text-gray-100" + + (objectID && object.idx === objectID ? "" : "") + } + > + {object.name} + </h5> </div> - </> + </div> + )) )} - </div> - ); + </div> + </> + )} + </div> + ); }; diff --git a/frontend/src/components/ThemeSwitcher.jsx b/frontend/src/components/ThemeSwitcher.jsx index 8072bfd..1a20e9b 100644 --- a/frontend/src/components/ThemeSwitcher.jsx +++ b/frontend/src/components/ThemeSwitcher.jsx @@ -1,29 +1,43 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 - -import React, { useState, useEffect } from 'react'; -import { Button } from './Button'; +import React, { useState, useEffect } from "react"; +import { Sun, Moon } from "lucide-react"; export const ThemeSwitcher = () => { - const [theme, setTheme] = useState('light'); // default theme is light + const systemTheme = + window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + + const savedTheme = localStorage.getItem("theme") || systemTheme; + const [theme, setTheme] = useState(savedTheme); useEffect(() => { - if (theme === 'dark') { - document.documentElement.classList.add('dark'); + if (theme === "dark") { + document.documentElement.classList.add("dark"); + document.documentElement.classList.remove("light"); } else { - document.documentElement.classList.remove('dark'); + document.documentElement.classList.add("light"); + document.documentElement.classList.remove("dark"); } + localStorage.setItem("theme", theme); }, [theme]); - const switchTheme = (newTheme) => { - setTheme(newTheme); + const toggleTheme = () => { + setTheme(theme === "dark" ? "light" : "dark"); }; return ( - <div> - {theme !== 'light' && <Button onClick={() => switchTheme('light')}>Light</Button>} - {theme !== 'dark' && <Button onClick={() => switchTheme('dark')}>Dark</Button>} - {theme !== 'auto' && <Button onClick={() => switchTheme('auto')}>Auto</Button>} + <div + onClick={toggleTheme} + className="flex cursor-pointer items-center justify-center rounded-full bg-custom-light p-2 transition-colors duration-700 ease-in-out hover:bg-custom-dark-4 dark:bg-custom-dark-1 dark:hover:bg-custom-light" + > + {theme === "dark" ? ( + <Sun className="transform text-orange-500 transition-transform duration-500 ease-in-out" /> + ) : ( + <Moon className="transform text-black transition-transform duration-500 ease-in-out" /> + )} </div> ); }; diff --git a/frontend/src/components/ViewsList.jsx b/frontend/src/components/ViewsList.jsx index f74e66d..766ff24 100644 --- a/frontend/src/components/ViewsList.jsx +++ b/frontend/src/components/ViewsList.jsx @@ -1,30 +1,44 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; -import { TemplateCard } from './TemplateCard'; +import React from "react"; +import { TemplateCard } from "./TemplateCard"; -export const ViewsList = ({templates, cardClickCallback}) => { +export const ViewsList = ({ templates, cardClickCallback }) => { let categories = { - "oa": "Operational Analysis Reports", - "sa": "System Analysis Reports", - "la": "Logical Architecture Reports", - "pa": "Physical Architecture Reports", - "xc": "Cross-cutting Reports", - "other": "Other reports"}; + oa: "Operational Analysis Reports", + sa: "System Analysis Reports", + la: "Logical Architecture Reports", + pa: "Physical Architecture Reports", + xc: "Cross-cutting Reports", + other: "Other reports", + }; + return ( - <div> - {Object.keys(categories).map(cat => ( - (cat in templates) && (templates[cat].length > 0) && ( - <div key={cat}> - <h2 key={cat + "h2"} className='text-2xl p-2'>{categories[cat]}</h2> - <div key={cat + "div"} className='flex flex-wrap justify-center gap-y-2'> - {templates[cat].map(template => ( - <TemplateCard key={template.idx} template={template} onClickCallback={cardClickCallback} /> - ))} - </div> - </div>) - ))} + <div className="rounded-lg bg-gray-100 pb-2 pl-4 pr-4 pt-8 shadow-lg dark:bg-custom-dark-3"> + {Object.keys(categories).map((cat) => { + if (cat in templates && templates[cat].length > 0) { + return ( + <div key={cat} className="mb-8 p-4 "> + <h2 className="border-b border-gray-900 pb-2 text-2xl font-bold dark:border-gray-300"> + <span className=" px-4 py-2 text-gray-900 dark:text-gray-100"> + {categories[cat]} + </span> + </h2> + <div className="mt-2 flex flex-wrap justify-center gap-y-2"> + {templates[cat].map((template) => ( + <TemplateCard + key={template.idx} + template={template} + onClickCallback={cardClickCallback} + /> + ))} + </div> + </div> + ); + } + return null; + })} </div> ); }; diff --git a/frontend/src/components/WiredTemplatesList.jsx b/frontend/src/components/WiredTemplatesList.jsx index 08b07df..060bea7 100644 --- a/frontend/src/components/WiredTemplatesList.jsx +++ b/frontend/src/components/WiredTemplatesList.jsx @@ -1,45 +1,48 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 -import React, {useState, useEffect} from 'react'; -import { useNavigate } from 'react-router-dom'; -import { ViewsList } from './ViewsList'; -import { API_BASE_URL } from '../APIConfig'; - +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { ViewsList } from "./ViewsList"; +import { API_BASE_URL } from "../APIConfig"; export const WiredTemplatesList = () => { - const [templates, setTemplates] = useState([]) - const [error, setError] = useState(null); - const navigate = useNavigate(); + const [templates, setTemplates] = useState([]); + const [error, setError] = useState(null); + const navigate = useNavigate(); - useEffect(() => { - const fetchTemplates = async () => { - try { - const response = await fetch(API_BASE_URL + "/views", { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - }); - const data = await response.json(); - setTemplates(data); - } - catch (error) { - setError(error.message) - } - finally {} - }; - fetchTemplates(); - }, []); + useEffect(() => { + const fetchTemplates = async () => { + try { + const response = await fetch(API_BASE_URL + "/views", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + const data = await response.json(); + setTemplates(data); + } catch (error) { + setError(error.message); + } finally { + } + }; + fetchTemplates(); + }, []); - if (error) { - return ( - <div className='bg-red-500 text-white p-4 rounded text-2xl'> - {error === 'Failed to fetch' ? - "Can't connect to the server. Maybe your session was inactive for too long? if that's the case, request a new session / restart the app." - : - error - } - </div>); - } - return <ViewsList templates={templates} cardClickCallback={(idx) => navigate(`/${idx}`, {state: {idx: idx}})} />;} + if (error) { + return ( + <div className="dark:bg-custom-dark-error rounded bg-red-500 p-4 text-2xl text-white"> + {error === "Failed to fetch" + ? "Can't connect to the server. Maybe your session was inactive for too long? if that's the case, request a new session / restart the app." + : error} + </div> + ); + } + return ( + <ViewsList + templates={templates} + cardClickCallback={(idx) => navigate(`/${idx}`, { state: { idx: idx } })} + /> + ); +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index cf36d75..76ac967 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -7,7 +7,6 @@ @tailwind components; @tailwind utilities; - :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; @@ -15,7 +14,6 @@ color-scheme: light dark; color: rgba(255, 255, 255, 0.87); - background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; @@ -23,22 +21,13 @@ -moz-osx-font-smoothing: grayscale; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; - @apply bg-white dark:bg-gray-900; + @apply bg-custom-light dark:bg-custom-dark-1; } .react-transform-wrapper { @@ -78,47 +67,58 @@ button:focus-visible { color: #747bff; } button { - background-color: #f9f9f9; + background-color: blue; } } .collapsed { - width: 50px; + width: 50px; } .html-content { text-align: left; - } .html-content svg { display: block; - @apply max-w-full h-auto; + @apply h-auto max-w-full; text-align: center; } .html-content h1 { - @apply text-4xl mb-4 mt-5; + @apply mb-4 mt-5 text-4xl; } .html-content h2 { - @apply text-3xl mb-3 mt-5; + @apply mb-3 mt-5 text-3xl; } .html-content h3 { - @apply text-2xl mb-3 mt-5; + @apply mb-3 mt-5 text-2xl; } -.html-content table { @apply min-w-full divide-y divide-gray-200; } -.html-content p { margin-top: 0.75em; margin-bottom: 0.75em; line-height: 1.5em; } -.html-content p a, .html-content li a { @apply text-gray-500 italic hover:underline; } +.html-content table { + @apply min-w-full divide-y divide-gray-200; +} +.html-content p { + margin-top: 0.75em; + margin-bottom: 0.75em; + line-height: 1.5em; +} +.html-content p a, +.html-content li a { + @apply italic text-gray-500 hover:underline; +} ul { @apply list-disc pl-5; } @media print { - body, .html-wrapper, .flex-1, .html-content { + body, + .html-wrapper, + .flex-1, + .html-content { overflow: visible !important; } .print\\:hidden { @@ -128,5 +128,62 @@ ul { border: none !important; box-shadow: none !important; } - @page { margin: 15mm; } + @page { + margin: 15mm; + } +} + +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: 0.4s; +} + +input:checked + .slider { + background-color: #2196f3; +} + +input:focus + .slider { + box-shadow: 0 0 1px #2196f3; +} + +input:checked + .slider:before { + transform: translateX(26px); +} + +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; } diff --git a/frontend/src/views/HomeView.jsx b/frontend/src/views/HomeView.jsx index e933955..d07d7d9 100644 --- a/frontend/src/views/HomeView.jsx +++ b/frontend/src/views/HomeView.jsx @@ -1,49 +1,61 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useState, useEffect } from 'react'; -import {WiredTemplatesList} from '../components/WiredTemplatesList'; -import { API_BASE_URL } from '../APIConfig'; +import React, { useState, useEffect } from "react"; +import { WiredTemplatesList } from "../components/WiredTemplatesList"; +import { API_BASE_URL } from "../APIConfig"; +import { ThemeSwitcher } from "../components/ThemeSwitcher"; export const HomeView = () => { - const [modelInfo, setModelInfo] = useState(null); - const [error, setError] = useState(null); + const [modelInfo, setModelInfo] = useState(null); + const [error, setError] = useState(null); - useEffect(() => { - const fetchModelInfo = async () => { - try { - const response = await fetch(API_BASE_URL + '/model-info'); - const data = await response.json(); - setModelInfo(data); - } catch (err) { - setError('Failed to fetch model info: ' + err.message); - } - }; + useEffect(() => { + const fetchModelInfo = async () => { + try { + const response = await fetch(API_BASE_URL + "/model-info"); + const data = await response.json(); + setModelInfo(data); + } catch (err) { + setError("Failed to fetch model info: " + err.message); + } + }; - fetchModelInfo(); - }, []); - - if (error) { - return (<div className='bg-red-500 text-white text-xl p-2 rounded'>{error}</div>); - } + fetchModelInfo(); + }, []); + if (error) { return ( - <div className="flex flex-col justify-center h-full"> - <div className="mb-4 mx-auto text-center"> - {modelInfo && ( - <div className='bg-white dark:bg-gray-900 text-gray-700 dark:text-white'> - <h2 className='text-xl'>{modelInfo.title}</h2> - {modelInfo.capella_version && <p>Capella Version: {modelInfo.capella_version}</p>} - {modelInfo.revision && <p>Revision: {modelInfo.revision}</p>} - {modelInfo.branch && <p>Branch: {modelInfo.branch}</p>} - {modelInfo.hash && <p>Commit Hash: {modelInfo.hash}</p>} - <div dangerouslySetInnerHTML={{ __html: modelInfo.badge }}></div> - </div> - )} - </div> - <div> - <WiredTemplatesList /> + <div className="dark:bg-custom-dark-error rounded bg-red-500 p-2 text-xl text-white"> + {error} + </div> + ); + } + + return ( + <div className="mb-8 flex h-full flex-col justify-center"> + <div className="flex w-full items-start justify-between px-4 "> + <div className="mx-auto mb-8 text-center "> + {modelInfo && ( + <div className="rounded-b-lg bg-gray-100 p-4 text-gray-700 shadow-lg dark:bg-custom-dark-3 dark:text-gray-100 "> + <h2 className="text-xl">{modelInfo.title}</h2> + {modelInfo.capella_version && ( + <p>Capella Version: {modelInfo.capella_version}</p> + )} + {modelInfo.revision && <p>Revision: {modelInfo.revision}</p>} + {modelInfo.branch && <p>Branch: {modelInfo.branch}</p>} + {modelInfo.hash && <p>Commit Hash: {modelInfo.hash}</p>} + <div dangerouslySetInnerHTML={{ __html: modelInfo.badge }}></div> </div> + )} </div> - ); + <div className="flex items-start justify-end rounded-b-lg bg-gray-100 p-6 shadow-lg dark:bg-custom-dark-2"> + <ThemeSwitcher /> + </div> + </div> + <div className="mt-4"> + <WiredTemplatesList /> + </div> + </div> + ); }; diff --git a/frontend/src/views/TemplateView.jsx b/frontend/src/views/TemplateView.jsx index b1396a5..6e9be88 100644 --- a/frontend/src/views/TemplateView.jsx +++ b/frontend/src/views/TemplateView.jsx @@ -9,59 +9,63 @@ In this component we show list of template instances, and when we click on a tem import React, { useEffect, useState } from "react"; import { useLocation, useParams } from "react-router-dom"; -import { useMediaQuery } from 'react-responsive'; +import { useMediaQuery } from "react-responsive"; import { Header } from "../components/Header"; import { InstanceView } from "../components/InstanceView"; import { TemplateDetails } from "../components/TemplateDetails"; -import { Button } from '../components/Button'; +import { Button } from "../components/Button"; export const TemplateView = ({ endpoint }) => { - let { templateName, objectID } = useParams(); - const [singleObjectID, setObjectID] = useState(null); - const location = useLocation(); - const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); - const isSmallScreen = useMediaQuery({ query: '(max-width: 1024px)' }); + let { templateName, objectID } = useParams(); + const [singleObjectID, setObjectID] = useState(null); + const location = useLocation(); + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); + const isSmallScreen = useMediaQuery({ query: "(max-width: 1024px)" }); + useEffect(() => { + if (isSmallScreen) { + setIsSidebarCollapsed(true); + } + }, [isSmallScreen]); - useEffect(() => {}, [endpoint, templateName, objectID, location]); - - return ( - <div className="flex flex-col h-screen"> - <Header /> - {isSmallScreen && ( - <div className="flex justify-center"> - <Button - className="px-4 py-2" - onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)} - > - {isSidebarCollapsed ? 'Expand Sidebar' : 'Collapse Sidebar'} - </Button> - </div> - )} - <div className="flex flex-1 overflow-hidden"> - <aside className={`print:hidden lg:block lg:w-80 h-full p-4 overflow-y-auto ${isSidebarCollapsed ? 'hidden' : 'w-64 h-screen'}`}> - <TemplateDetails - endpoint={endpoint} - onSingleInstance={setObjectID} - /> - </aside> - <main className="flex-1 overflow-hidden p-4"> - <div className="html-wrapper w-full p-4 max-w-none lg:max-w-4xl min-w-0 lg:min-w-[850px] overflow-y-hidden h-full flex items-center justify-center"> - {!!!objectID && !!!singleObjectID && ( - <p className="text-xl text-gray-700"> - Select an Instance - </p> - )} - {(objectID || singleObjectID) && ( - <InstanceView - endpoint={endpoint} - objectID={objectID || singleObjectID} - templateName={templateName} - /> - )} - </div> - </main> - </div> + return ( + <div className="flex h-screen w-auto flex-col"> + <Header /> + {isSmallScreen && ( + <div className="mt-8 flex justify-center"> + <Button + className="px-4 py-2" + onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)} + > + {isSidebarCollapsed ? "Expand Sidebar" : "Collapse Sidebar"} + </Button> </div> - ); + )} + <div className="flex-32 flex"> + <aside + className={`mb-8 mt-8 flex max-h-[75vh] flex-col overflow-y-auto rounded-lg shadow-lg dark:shadow-dark lg:block lg:w-96 print:hidden ${ + isSidebarCollapsed ? "hidden" : "h-screen w-64" + }`} + > + <TemplateDetails endpoint={endpoint} onSingleInstance={setObjectID} /> + </aside> + <main className="flex-1 "> + <div className="html-wrapper ml-8 mt-8 flex h-[80vh] min-w-0 max-w-none items-start justify-center lg:min-w-[650px] lg:max-w-4xl"> + {!!!objectID && !!!singleObjectID && ( + <p className="text-xl text-gray-700 dark:text-gray-300"> + Select an Instance + </p> + )} + {(objectID || singleObjectID) && ( + <InstanceView + endpoint={endpoint} + objectID={objectID || singleObjectID} + templateName={templateName} + /> + )} + </div> + </main> + </div> + </div> + ); }; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 03e0f82..70e55ea 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -6,18 +6,30 @@ /** @type {import('tailwindcss').Config} */ export default { darkMode: "class", - content: [ - './src/**/*.{html,js,jsx}' - ], + content: ["./src/**/*.{html,js,jsx}"], theme: { extend: { boxShadow: { - white: '0 0 15px rgba(255, 255, 255, 0.1)', + white: "0 0 15px rgba(255, 255, 255, 0.1)", + dark: "0 4px 6px 0 rgba(0, 0, 0, 0.2)", }, animation: { - 'spin-slow': 'spin 1.6s linear infinite', - } - } + "spin-slow": "spin 1.6s linear infinite", + }, + opacity: { + 54: ".54", + }, + colors: { + "custom-light": "#dee4e7", + "custom-dark-1": "#24273a", + "custom-dark-2": "#363a4f", + "custom-dark-3": "#6e738d", + "custom-dark-4": "#5b6078", + "custom-dark-error": "#ed8796", + "custom-blue": "#2196f3", + "custom-blue-hover": "#1976d2", + }, + }, }, - plugins: [], -} + plugins: [require("tailwind-scrollbar")], +}; diff --git a/templates/__generic__.html.j2 b/templates/__generic__.html.j2 index 10f147d..77be0fa 100644 --- a/templates/__generic__.html.j2 +++ b/templates/__generic__.html.j2 @@ -11,4 +11,4 @@ <h1>Unnamed ({{ object.__class__.__name__ }})</small></h1> {% endif %} -{{show_other_attributes(object) | safe}} \ No newline at end of file +{{show_other_attributes(object) | safe}} diff --git a/templates/classes.html.j2 b/templates/classes.html.j2 index 656b446..76ee6fc 100644 --- a/templates/classes.html.j2 +++ b/templates/classes.html.j2 @@ -94,4 +94,3 @@ <h2>Other Object Attributes</h2> {{ show_other_attributes(object, handled_attrs) | safe }} - diff --git a/templates/common_macros.html.j2 b/templates/common_macros.html.j2 index 3185f04..99a0b55 100644 --- a/templates/common_macros.html.j2 +++ b/templates/common_macros.html.j2 @@ -74,4 +74,4 @@ {%- macro typed_name(object) -%}<b>{{ object.__class__.__name__ }}</b> <a href="{{ object | make_href }}">{{ object.name | trim }}</a>{%- endmacro -%} -{%- macro linked_name(object) -%}<a href="{{ object | make_href }}">{{ object.name | trim }}</a>{%- endmacro -%} \ No newline at end of file +{%- macro linked_name(object) -%}<a href="{{ object | make_href }}">{{ object.name | trim }}</a>{%- endmacro -%} diff --git a/templates/operational-activity.html.j2 b/templates/operational-activity.html.j2 index b97f455..e029286 100644 --- a/templates/operational-activity.html.j2 +++ b/templates/operational-activity.html.j2 @@ -8,12 +8,12 @@ {{ description(object) | safe }} {% if object.owner %} - <p>This activity is performed by - <b>{{ object.owner.__class__.__name__}}</b> + <p>This activity is performed by + <b>{{ object.owner.__class__.__name__}}</b> <a href="{{ object.owner | make_href }}">{{ object.owner.name }}</a>. </p> {% if object.available_in_states %} - <p><a href="{{ object.owner | make_href}}">{{ object.owner.name }}</a> may {{ object.name }} while in one of the following modes or states: + <p><a href="{{ object.owner | make_href}}">{{ object.owner.name }}</a> may {{ object.name }} while in one of the following modes or states: {%- for state in object.available_in_states -%} <a href="{{ state | make_href }}">{{ state.name }}</a>{%- if not loop.last -%}, {% endif -%} {%- endfor -%} @@ -21,7 +21,7 @@ {% endif %} <h2>Interface partners</h2> - <p>While performing this activity <a href="{{ object.owner | make_href}}">{{ object.owner.name }}</a> may interact with the following entities: + <p>While performing this activity <a href="{{ object.owner | make_href}}">{{ object.owner.name }}</a> may interact with the following entities: {% set ins = object.inputs | map(attribute="source") | selectattr("owner") | map(attribute="owner") | list %} {% set outs = object.outputs | map(attribute="target") | selectattr("owner") | map(attribute="owner") | list %} {% set partners = ins + outs | rejectattr("uuid", "equalto", object.owner.uuid) | list %} @@ -38,12 +38,12 @@ {% for input in object.inputs %} <li> {%- set act = input.source -%} - {{ linked_name(input) | safe }} produced by "{{ linked_name(act) | safe }}" activity of + {{ linked_name(input) | safe }} produced by "{{ linked_name(act) | safe }}" activity of {%- if act.owner -%} - {{ linked_name(act.owner) | safe }} + {{ linked_name(act.owner) | safe }} {%- else -%} - <span style="color: red;">Unassigned entity</span> - {%- endif -%} + <span style="color: red;">Unassigned entity</span> + {%- endif -%} </li> {% endfor %} </ul> @@ -57,12 +57,12 @@ <li> {%- set act = output.target -%} {%- if act.owner -%} - {{ linked_name(act.owner) | safe }} + {{ linked_name(act.owner) | safe }} {%- else -%} - <span style="color: red;">Unassigned entity</span> + <span style="color: red;">Unassigned entity</span> {%- endif -%} may require {{ linked_name(output) | safe }} to perform "{{ linked_name(act) | safe }}" activity. - + </li> {% endfor %} </ul> @@ -78,4 +78,4 @@ The figure below provides an overview of the interactions between the activity o <h2>Other properties of "{{ object.name }}"</h2> {% set excluded = ["name", "available_in_states", "related_exchanges", "inputs", "outputs", "context_diagram", "xtype", "parent", "owner", "owning_entity"] %} -{{ show_other_attributes(object, excluded) | safe }} \ No newline at end of file +{{ show_other_attributes(object, excluded) | safe }} diff --git a/templates/operational-capability.html.j2 b/templates/operational-capability.html.j2 index 47225db..9dbd011 100644 --- a/templates/operational-capability.html.j2 +++ b/templates/operational-capability.html.j2 @@ -1,3 +1,7 @@ +{# + Copyright DB InfraGO AG and contributors + SPDX-License-Identifier: Apache-2.0 +#} {% from 'common_macros.html.j2' import show_other_attributes, description, render_reqs_by_type %} <h1>{{ object.name }}</h1> @@ -40,4 +44,3 @@ {% set filtered = ["description", "xtype", "postcondition", "precondition", "context_diagram", "data_flow_view", "diagrams"]%} {{ show_other_attributes(object, filtered) | safe}} - diff --git a/templates/operational-entity.html.j2 b/templates/operational-entity.html.j2 index fa1c131..c86360f 100644 --- a/templates/operational-entity.html.j2 +++ b/templates/operational-entity.html.j2 @@ -1,3 +1,7 @@ +{# + Copyright DB InfraGO AG and contributors + SPDX-License-Identifier: Apache-2.0 +#} {% from 'common_macros.html.j2' import show_other_attributes, description, render_reqs_by_type, typed_name %} <h1>{{ object.name }}</h1> @@ -6,9 +10,9 @@ <p style="margin-top:1.5em; "><b>Parent:</b> {{ typed_name(object.parent) | safe }}</p> {% if object.capabilities %} -<p><i>{{ object.name }} {% if object.is_human %}(human){% endif %}</i> is involved in the following operational capabilities: +<p><i>{{ object.name }} {% if object.is_human %}(human){% endif %}</i> is involved in the following operational capabilities: {%- for cap in object.capabilities -%} -<a href="{{ cap | make_href }}">{{ cap.name }}</a>; +<a href="{{ cap | make_href }}">{{ cap.name }}</a>; {%- endfor -%} </p> {% endif %} @@ -52,10 +56,10 @@ {%- endif -%} {%- endfor -%} {%- if involving_caps -%} - <p>involving operational capabilities: + <p>involving operational capabilities: {%- for cap in object.capabilities -%} {%- if activity in cap.involved_activities -%} - <a href="{{ cap | make_href }}">{{ cap.name }}</a>; + <a href="{{ cap | make_href }}">{{ cap.name }}</a>; {%- endif -%} {%- endfor -%} </p> diff --git a/templates/req.html.j2 b/templates/req.html.j2 index d99366b..0151afa 100644 --- a/templates/req.html.j2 +++ b/templates/req.html.j2 @@ -27,4 +27,4 @@ {% from 'common_macros.html.j2' import show_other_attributes %} {% set filtered = ["parent", "owner", "requirements", "long_name", "to_reqif"]%} -{{show_other_attributes(object, filtered) | safe}} \ No newline at end of file +{{show_other_attributes(object, filtered) | safe}} diff --git a/templates/req.yaml b/templates/req.yaml index ba35e61..c20eeab 100644 --- a/templates/req.yaml +++ b/templates/req.yaml @@ -8,4 +8,4 @@ description: Basic template to visualize requirement objects category: other variable: name: object - type: CapellaModule \ No newline at end of file + type: CapellaModule diff --git a/templates/system-capability.html.j2 b/templates/system-capability.html.j2 index b0626d1..712f741 100644 --- a/templates/system-capability.html.j2 +++ b/templates/system-capability.html.j2 @@ -59,4 +59,4 @@ <h2>Other properties of "{{ object.name }}"</h2> {% set excluded = ["involved_functions", "postcondition", "precondition", "description", "name", "data_flow_view", "context_diagram", "xtype", "parent"] %} -{{ show_other_attributes(object, excluded) | safe }} \ No newline at end of file +{{ show_other_attributes(object, excluded) | safe }} diff --git a/templates/system_function.html.j2 b/templates/system_function.html.j2 index 22e6a89..2493e0b 100644 --- a/templates/system_function.html.j2 +++ b/templates/system_function.html.j2 @@ -39,4 +39,4 @@ <h2>Other properties of "{{ object.name }}"</h2> {% set excluded = ["context_diagram", "description", "name", "owner", "xtype"] %} -{{ show_other_attributes(object, excluded) | safe}} \ No newline at end of file +{{ show_other_attributes(object, excluded) | safe}}