Skip to content

Commit

Permalink
Merge branch 'responsive' into storybook
Browse files Browse the repository at this point in the history
  • Loading branch information
vik378 committed Mar 19, 2024
2 parents 3c89d3c + fea3797 commit 55f415d
Show file tree
Hide file tree
Showing 27 changed files with 256 additions and 46 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ COPY --from=build-frontend /app/dist/ ./frontend/dist/
EXPOSE 8000

# Start the application
CMD ["python", "-m", "capella_model_explorer.backend", "/model", "/templates"]
CMD ["python", "-m", "capella_model_explorer.backend", "/model", "/views"]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ Clone, then build and run locally with Docker:

```bash
docker build -t model-explorer:latest .
docker run -v /absolute/path/to/your/model/folder/on/host:/model -v $(pwd)/templates:/templates -p 8000:8000 model-explorer
docker run -v /absolute/path/to/your/model/folder/on/host:/model -v $(pwd)/views:/views -p 8000:8000 model-explorer
```

Then open your browser at `http://localhost:8000/templates` and start exploring your model.
Then open your browser at `http://localhost:8000/views` and start exploring your model.

While the thing is running you can edit the templates in the `templates` folder and see the changes immediately in the browser.

Expand Down
21 changes: 18 additions & 3 deletions capella_model_explorer/backend/explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,26 @@ def __post_init__(self):
def configure_routes(self):
self.app.mount("/assets", StaticFiles(directory=PATH_TO_FRONTEND.joinpath("assets"), html=True))

@self.app.get("/api/templates")
@self.app.get("/api/views")
def read_templates():
# list all templates in the templates folder from .yaml
self.templates = index_templates(self.templates_path)
return [
{"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
}


@self.app.get("/api/templates/{template_name}")
@self.app.get("/api/views/{template_name}")
def read_template(template_name: str):
base = self.templates[urlparse.quote(template_name)]
variable = base["variable"]
Expand All @@ -72,7 +82,7 @@ def read_template(template_name: str):
]
return base

@self.app.get("/api/templates/{template_name}/{object_id}")
@self.app.get("/api/views/{template_name}/{object_id}")
def render_template(template_name: str, object_id: str):
base = self.templates[urlparse.quote(template_name)]
template_filename = base["template"]
Expand All @@ -86,9 +96,14 @@ def render_template(template_name: str, object_id: str):
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})



def index_templates(path: pathlib.Path) -> dict[str, t.Any]:
Expand Down
16 changes: 15 additions & 1 deletion frontend/package-lock.json

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

3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-zoom-pan-pinch": "^3.4.3"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.6.16",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/APIConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const API_BASE_URL = 'http://localhost:8000/api';

export { API_BASE_URL };
1 change: 0 additions & 1 deletion frontend/src/App.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

Expand Down
8 changes: 4 additions & 4 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ function App() {
return (
<Router>
<Routes>
<Route path="/" element={<p>this view is WIP, checkout <a href='/templates'>templates</a> instead.</p>} />
<Route path="/templates" element={<WiredTemplatesList endpoint="http://localhost:8000/api/templates" />} />
<Route path="/templates/:templateName" element={<TemplateView endpoint="http://localhost:8000/api/templates/" />} />
<Route path="/templates/:templateName/:objectID" element={<TemplateView endpoint="http://localhost:8000/api/templates/" />} />
<Route path="/" element={<p>this view is WIP, checkout <a href='/views'>templates</a> instead.</p>} />
<Route path="/views" element={<WiredTemplatesList endpoint="http://localhost:8000/api/views" />} />
<Route path="/views/:templateName" element={<TemplateView endpoint="http://localhost:8000/api/views/" />} />
<Route path="/views/:templateName/:objectID" element={<TemplateView endpoint="http://localhost:8000/api/views/" />} />
</Routes>
</Router>
)
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/components/Breadcrumbs.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 fetchModelInfo = async () => {
const response = await fetch(API_BASE_URL + `/model-info`);
const modelInfo = await response.json();
return modelInfo.title;
};

// Function to fetch view names
const fetchViewName = async (idx) => {
const response = await fetch(API_BASE_URL + `/views`);

const views = await response.json();
const view = views.find(v => v.idx.toString() === idx);
return view ? view.name : idx;
};

// Function to fetch object names
const fetchObjectName = async (uuid) => {
const response = await fetch(API_BASE_URL + `/objects/${uuid}`);
const object = await response.json();
return object.name;
};

useEffect(() => {
const updateLabels = async () => {
const title = await fetchModelInfo();
const labels = { '/': title };

for (let i = 0; i < pathnames.length; i++) {
const to = `/${pathnames.slice(0, i + 1).join('/')}`;

if (pathnames[i] === 'views') {
labels[to] = 'Views';
} else if (i === 1 && pathnames[0] === 'views') {
labels[to] = await fetchViewName(pathnames[i]);
} else if (i === 2 && pathnames[0] === 'views') {
labels[to] = await fetchObjectName(pathnames[i]);
} else {
labels[to] = pathnames[i];
}
}
console.log(labels);

setBreadcrumbLabels(labels);
};

updateLabels();
}, [location]);

const visible_pathnames = [breadcrumbLabels['/'], ...location.pathname.split('/').filter(x => x)];

return (
<nav aria-label="breadcrumb" className="flex items-center">
<ol className="flex items-center">
<li className="flex items-center dark:text-gray-200">
<span>{breadcrumbLabels['/']}</span>
<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 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="mx-2">/</span>}
</li>
);
})}
</ol>
</nav>
);
};
9 changes: 9 additions & 0 deletions frontend/src/components/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

export const Button = ({ theme, children, ...props }) => {
return (
<a href="#" {...props} className="rounded-md mx-1 bg-blue-800 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ">
{children}
</a>
);
};
Empty file.
2 changes: 1 addition & 1 deletion frontend/src/components/InstanceView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const InstanceView = ({templateName, objectID, endpoint}) => {
}, [endpoint, objectID, templateName]);
if (loading) return (<div><Spinner />;</div>)
return (
<div ref={contentRef} className='html-content bg-white shadow-lg dark:shadow-white dark:text-gray-700 mx-auto my-8 p-8 w-[210mm] max-w-full overflow-auto print:shadow-none print:m-0 print:p-0 print:bg-transparent'>
<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 (
Expand Down
Empty file.
35 changes: 28 additions & 7 deletions frontend/src/components/Lightbox.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
import React, {useEffect} from "react";
import React, { useEffect, useState } from 'react';
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";

export const Lightbox = ({ onClose, imageSource }) => {
const [isSelecting, setIsSelecting] = useState(false);

export const Lightbox = ({imageSource, onClose}) => {
useEffect(() => {
let isMouseDown = false;

const handleEscape = (event) => {
if (event.key === 'Escape') {
onClose();
}
};

const handleClick = (event) => {
if (event.target.tagName === 'text') {
navigator.clipboard.writeText(event.target.textContent);
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
document.addEventListener('click', handleClick);

return () => {
document.removeEventListener('keydown', handleEscape);
document.removeEventListener('click', handleClick);
};
}, [onClose]);

return (
<div onClick={onClose}
className='fixed inset-0 z-50 flex justify-center items-center bg-black bg-opacity-50'>
<div style={{ maxWidth: '100%'}}>
{imageSource && <div dangerouslySetInnerHTML={{__html: imageSource}}></div>}
<div className='fixed inset-0 z-50 flex justify-center items-center'>
<div className='fixed inset-0 bg-black bg-opacity-50' onClick={onClose}></div>
<div style={{ position: 'absolute', maxWidth: '100%', height: 'auto', zIndex: 1 }}>
{imageSource &&
<TransformWrapper>
<TransformComponent>
<div dangerouslySetInnerHTML={{__html: imageSource}} style={{ width: '100%', overflow: 'auto', userSelect: 'text' }}></div>
</TransformComponent>
</TransformWrapper>}
</div>
</div>
);
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/components/TemplateDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ export const TemplateDetails = ({endpoint}) => {
finally {}
};
fetchDetails();
}, [endpoint, objectID]);
}, [endpoint, templateName, objectID]);

return (
<div className='flex flex-col h-screen'>
<a href='/templates' className='flex-initial p-4'>Back to template selection</a>
<div className='flex flex-col h-full'>
<div className='p-5'>
<h5 className='mb-2 text-2xl font-bold text-gray-900 dark:text-white'>
{details.name}
Expand All @@ -47,7 +46,7 @@ export const TemplateDetails = ({endpoint}) => {
{details.objects && details.objects.length === 0 && <p>No objects found</p>}
{details.objects && details.objects.length > 0 && details.objects.filter(object => object.name.toLowerCase().includes(filterText.toLowerCase())).map(object => (
<div key={object.idx}
onClick={() => {navigate(`/templates/${templateName}/${object.idx}`);}}
onClick={() => {navigate(`/views/${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'>
Expand Down
36 changes: 26 additions & 10 deletions frontend/src/components/TemplateView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import React, {useEffect, useState} from 'react';
import { TemplateDetails } from './TemplateDetails';
import { useLocation, useParams, Navigate } from 'react-router-dom';
import { InstanceView } from './InstanceView';
import { Breadcrumbs } from './Breadcrumbs';
import { ThemeSwitcher } from './ThemeSwitcher';

export const TemplateView = ({endpoint}) => {
let { templateName, objectID } = useParams();
Expand All @@ -17,15 +19,29 @@ export const TemplateView = ({endpoint}) => {
}, [endpoint, templateName, objectID, location]);

return (
<div className='flex flex-row h-screen'>
<div className='flex-initial p-4' style={{ flexBasis: '24%'}}>
<TemplateDetails endpoint={endpoint} />
</div>
<div className='flex-auto p-4 overflow-auto h-full' style={{minWidth: 0}}>
{ !!!objectID && <p>Select an Instance</p>}
{ objectID && <InstanceView endpoint={endpoint} objectID={objectID} templateName={templateName} /> }

<div className="flex flex-col h-screen"> {/* Use h-screen to ensure the container fits the viewport height */}
{/* Header */}
<header className=" text-gray-700 p-4 flex justify-between items-center">
<div><Breadcrumbs /></div>
<div></div>
<div><ThemeSwitcher /></div>
</header>

{/* Body: Sidebar + Main Content */}
<div className="flex flex-1 overflow-hidden"> {/* This ensures the remaining height is distributed here */}
{/* Sidebar - Adjust visibility/responsiveness as needed */}
<aside className="hidden lg:block lg:w-80 p-4 overflow-y-auto"> {/* Use overflow-y-auto to enable vertical scrolling */}
<TemplateDetails endpoint={endpoint} />
</aside>

{/* Main Content */}
<main className="flex-1 overflow-hidden p-4">
<div className="w-full p-4 max-w-none lg:max-w-4xl min-w-0 lg:min-w-[850px] overflow-y-auto h-full flex items-center justify-center"> {/* Ensure main content is scrollable and fills the height */}
{ !!!objectID && <p>Select an Instance</p>}
{ objectID && <InstanceView endpoint={endpoint} objectID={objectID} templateName={templateName} /> }
</div>
</main>
</div>
</div>
)
}
);
}
26 changes: 26 additions & 0 deletions frontend/src/components/ThemeSwitcher.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useState, useEffect } from 'react';
import { Button } from './Button';

export const ThemeSwitcher = () => {
const [theme, setTheme] = useState('light'); // default theme is light

useEffect(() => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [theme]);

const switchTheme = (newTheme) => {
setTheme(newTheme);
};

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>
);
};
Loading

0 comments on commit 55f415d

Please sign in to comment.