Skip to content

Commit

Permalink
Merge branch 'storybook' of github.com:DSD-DBS/capella-model-explorer…
Browse files Browse the repository at this point in the history
… into storybook
  • Loading branch information
vik378 committed Mar 19, 2024
2 parents a0c4c8c + 2f5a9eb commit 2439722
Showing 7 changed files with 137 additions and 67 deletions.
11 changes: 9 additions & 2 deletions capella_model_explorer/backend/__main__.py
Original file line number Diff line number Diff line change
@@ -13,11 +13,18 @@
HOST = os.getenv("CAPELLA_MODEL_EXPLORER_HOST_IP", "0.0.0.0")
PORT = os.getenv("CAPELLA_MODEL_EXPLORER_PORT", "8000")

PATH_TO_TEMPLATES = Path(__file__).parent.parent.parent / "templates"


@click.command()
@click.argument("model", type=capellambse.ModelCLI())
@click.argument("templates", type=click.Path(exists=True))
def run(model: capellambse.MelodyModel, templates: Path | str):
@click.argument(
"templates",
type=click.Path(path_type=Path, exists=True),
required=False,
default=PATH_TO_TEMPLATES,
)
def run(model: capellambse.MelodyModel, templates: Path):
backend = explorer.CapellaModelExplorerBackend(Path(templates), model)
uvicorn.run(backend.app, host=HOST, port=int(PORT))

35 changes: 19 additions & 16 deletions capella_model_explorer/backend/explorer.py
Original file line number Diff line number Diff line change
@@ -7,14 +7,14 @@
import typing as t
import urllib.parse as urlparse
from pathlib import Path
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

import capellambse
import yaml
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from jinja2 import Environment

PATH_TO_FRONTEND = Path("./frontend/dist")
@@ -42,12 +42,19 @@ def __post_init__(self):
)
self.env = Environment()
self.templates = index_templates(self.templates_path)
self.app.templates =templates = Jinja2Templates(directory=PATH_TO_FRONTEND)
self.app.state.templates = templates = Jinja2Templates(
directory=PATH_TO_FRONTEND
)

self.configure_routes()

def configure_routes(self):
self.app.mount("/assets", StaticFiles(directory=PATH_TO_FRONTEND.joinpath("assets"), html=True))
self.app.mount(
"/assets",
StaticFiles(
directory=PATH_TO_FRONTEND.joinpath("assets"), html=True
),
)

@self.app.get("/api/views")
def read_templates():
@@ -57,16 +64,11 @@ def read_templates():
{"idx": key, **template}
for key, template in self.templates.items()
]

@self.app.get("/api/objects/{uuid}")
def read_object(uuid: str):
obj = self.model.by_uuid(uuid)
return {
"idx": obj.uuid,
"name": obj.name,
"type": obj.xtype
}

return {"idx": obj.uuid, "name": obj.name, "type": obj.xtype}

@self.app.get("/api/views/{template_name}")
def read_template(template_name: str):
@@ -99,15 +101,16 @@ def render_template(template_name: str, object_id: str):
# render the template with the object
rendered = template.render(object=object)
return HTMLResponse(content=rendered, status_code=200)

@self.app.get("/api/model-info")
def model_info():
return self.model.info

@self.app.get("/{rest_of_path:path}")
async def catch_all(request: Request, rest_of_path: str):
return self.app.templates.TemplateResponse("index.html", {"request": request})

return self.app.state.templates.TemplateResponse(
"index.html", {"request": request}
)


def index_templates(path: pathlib.Path) -> dict[str, t.Any]:
103 changes: 59 additions & 44 deletions frontend/src/components/InstanceView.jsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,73 @@
import React, {useEffect, useState, useRef} from 'react';
import { useParams } from 'react-router-dom';
import { Spinner } from './Spinner';
import { SVGDisplay } from './SVGDisplay';
import React, { useEffect, useRef, useState } from "react";
import { SVGDisplay } from "./SVGDisplay";
import { Spinner } from "./Spinner";


export const InstanceView = ({templateName, objectID, endpoint}) => {
export const InstanceView = ({ templateName, objectID, endpoint }) => {
const [details, setDetails] = useState([]);
const [loading, setLoading] = useState(true);
const contentRef = useRef(null);

useEffect(() => {
setLoading(true);
const url = endpoint + `${templateName}/${objectID}`;
fetch(url, {
method: 'GET',
method: "GET",
headers: {
'Content-Type': 'text/html'
}
"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});
.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);
});
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>)
if (loading)
return (
<div>
<Spinner />;
</div>
);
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 overflow-auto print:shadow-none print:m-0 print:p-0 print:bg-transparent'>
{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
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 overflow-auto print:shadow-none print:m-0 print:p-0 print:bg-transparent min-w-full"
>
{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>
);
};
4 changes: 2 additions & 2 deletions frontend/src/components/Spinner.jsx
Original file line number Diff line number Diff line change
@@ -2,6 +2,6 @@ import React from 'react';

export const Spinner = ({}) => (
<div className='flex justify-center items-center'>
<div className='animate-spin rounded full h-8 w-8 border-t-2 border-b-2 border-gray-500'></div>
<div className='animate-spin-slow rounded-full h-12 w-12 border-t-4 border-b-4 border-sky-500 ease-linear'></div>
</div>
);
);
17 changes: 17 additions & 0 deletions frontend/src/stories/Spinner.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Spinner.stories.js
import React from 'react';
import { Spinner } from '../components/Spinner';

export default {
title: 'Components/Spinner',
component: Spinner,
};
export const Demo = {
args: {
template: {
name: "Spinner",
description: "Spins and looks cool.",
},
},
};

3 changes: 3 additions & 0 deletions frontend/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -8,6 +8,9 @@ export default {
extend: {
boxShadow: {
white: '0 0 15px rgba(255, 255, 255, 0.1)',
},
animation: {
'spin-slow': 'spin 1.6s linear infinite',
}
}
},
31 changes: 28 additions & 3 deletions templates/classes.html.j2
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
{% macro properties_table(properties, object) %}
<style>
.table {
border-collapse: collapse;
width: 100%;
}
th, td {
padding: 8px;
text-align: left;
}
tr:hover, th {
background-color: #f2f2f2;
}
</style>

<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th>Property Name</th>
<th>Type</th>
<th>Multiplicity</th>
<th>Description</th>
</tr>
</thead>
<tbody
@@ -18,23 +36,30 @@
{{ property.type.name }}</td>
{% endif %}
<td>{{ property.min_card.value }} .. {{ property.max_card.value }}</td>

<td>{% if property.description %}
{{ property.description }}
{% else %}
<p style="color:red">No description available.</p>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}

<h1>{{ object.name | capitalize }}</h1>
<h1>{{ object.name }}</h1>
<p>{{ object.parent._short_html_() }}</p><br>
{% if object.description %}
<p>{{ object.description }}</p>
{% else %}
<p style="color:red">No description available.</p>
{% endif %}


<h2>Owned Properties</h2>
{% if object.owned_properties %}
<p>The object owns the following properties:</p>
<p>The object owns the following properties:</p><br>
{{ properties_table(object.owned_properties, object) }}
{% else %}
<p style="color:red">No properties are owned by this object.</p>

0 comments on commit 2439722

Please sign in to comment.