diff --git a/.github/workflows/push-docker-image.yml b/.github/workflows/push-docker-image.yml
index 720b0d2..b9cded6 100644
--- a/.github/workflows/push-docker-image.yml
+++ b/.github/workflows/push-docker-image.yml
@@ -1,9 +1,12 @@
+# Copyright DB InfraGO AG and contributors
+# SPDX-License-Identifier: CC0-1.0
+
name: Create and publish Docker image
on:
push:
- branches: ['**']
- tags: ['v*.*.*']
+ branches: ["**"]
+ tags: ["v*.*.*"]
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 54253a2..43bfb28 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -120,5 +120,21 @@ repos:
- id: prettier
types_or: [ts, css, html, markdown]
additional_dependencies:
- - 'prettier@^3.2.5'
- - 'prettier-plugin-tailwindcss@^0.5.14'
+ - "prettier@^3.2.5"
+ - "prettier-plugin-tailwindcss@^0.5.14"
+ - "tailwind-scrollbar@^3.1.0"
+ - repo: https://github.com/pre-commit/mirrors-eslint
+ rev: v9.2.0
+ hooks:
+ - id: eslint
+ additional_dependencies:
+ - "eslint@^8.57.0"
+ - "eslint-config-prettier@^9.1.0"
+ - "eslint-plugin-import@^2.29.1"
+ - "eslint-plugin-unused-imports@^3.1.0"
+ - "eslint-plugin-deprecation@^2.0.0"
+ - "eslint-plugin-tailwindcss@^3.15.1"
+ - "eslint-plugin-storybook@^0.8.0"
+ - "eslint-plugin-react@^7.34.1"
+ - "eslint-plugin-react-hooks@^4.6.2"
+ - "eslint-plugin-react-refresh@^0.4.5"
diff --git a/Dockerfile b/Dockerfile
index 6599136..7eacb83 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -47,6 +47,11 @@ RUN chmod +x /entrypoint.sh
ENV MODEL_ENTRYPOINT=/model
RUN chmod -R 777 ./frontend/dist/
+# Run script to get software version
+ENV MODE=production
+COPY frontend/fetch-version.py ./frontend/
+RUN python frontend/fetch-version.py
+
# Run as non-root user per default
USER 1001
diff --git a/README.md b/README.md
index 7a1c0d4..8017cdb 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,9 @@ a terminal and
another terminal. The backend and statically built frontend will be served at
`http://localhost:8000`. The live frontend will be served by vite at
`http://localhost:5173`(or similar, it will be printed in the terminal where
-you ran `npm run dev`).
+you ran `npm run dev`). If you wish to display the Frontend Software Version,
+it will initially show 'Fetch Failed'. To successfully fetch and display the
+version, you need to run the command `python frontend/fetch_version.py`.
# Installation
@@ -101,7 +103,7 @@ Navigate to `Menu` > `Settings` > `Tools` > `Add a new tool` and fill in the
following configuration:
```yaml
-name: 'Capella model explorer'
+name: "Capella model explorer"
integrations:
t4c: false
pure_variants: false
@@ -117,8 +119,8 @@ config:
environment:
MODEL_ENTRYPOINT:
stage: before
- value: '{CAPELLACOLLAB_SESSION_PROVISIONING[0][path]}'
- ROUTE_PREFIX: '{CAPELLACOLLAB_SESSIONS_BASE_PATH}'
+ value: "{CAPELLACOLLAB_SESSION_PROVISIONING[0][path]}"
+ ROUTE_PREFIX: "{CAPELLACOLLAB_SESSIONS_BASE_PATH}"
connection:
methods:
- id: f51872a8-1a4f-4a4d-b4f4-b39cbd31a75b
@@ -127,7 +129,7 @@ config:
ports:
metrics: 8000
http: 8000
- redirect_url: '{CAPELLACOLLAB_SESSIONS_SCHEME}://{CAPELLACOLLAB_SESSIONS_HOST}:{CAPELLACOLLAB_SESSIONS_PORT}{CAPELLACOLLAB_SESSIONS_BASE_PATH}/'
+ redirect_url: "{CAPELLACOLLAB_SESSIONS_SCHEME}://{CAPELLACOLLAB_SESSIONS_HOST}:{CAPELLACOLLAB_SESSIONS_PORT}{CAPELLACOLLAB_SESSIONS_BASE_PATH}/"
monitoring:
prometheus:
path: /metrics
@@ -145,7 +147,7 @@ Since the Capella Model Explorer can load different Capella versions, we can
use one generic version:
```yaml
-name: 'Generic'
+name: "Generic"
config:
is_recommended: true
is_deprecated: false
diff --git a/capella_model_explorer/backend/__init__.py b/capella_model_explorer/backend/__init__.py
index dd5d085..55462c7 100644
--- a/capella_model_explorer/backend/__init__.py
+++ b/capella_model_explorer/backend/__init__.py
@@ -1,2 +1,10 @@
# Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
+"""The capella_model_explorer package."""
+from importlib import metadata
+
+try:
+ __version__ = metadata.version("capella_model_explorer")
+except metadata.PackageNotFoundError: # pragma: no cover
+ __version__ = "0.0.0+unknown"
+del metadata
diff --git a/capella_model_explorer/backend/explorer.py b/capella_model_explorer/backend/explorer.py
index e5cef81..fc9150e 100644
--- a/capella_model_explorer/backend/explorer.py
+++ b/capella_model_explorer/backend/explorer.py
@@ -28,6 +28,8 @@
is_undefined,
)
+from . import __version__
+
esc = markupsafe.escape
PATH_TO_FRONTEND = Path("./frontend/dist")
@@ -47,7 +49,7 @@ class CapellaModelExplorerBackend:
model: capellambse.MelodyModel
def __post_init__(self):
- self.app = FastAPI()
+ self.app = FastAPI(version=__version__)
self.router = APIRouter(prefix=ROUTE_PREFIX)
self.app.add_middleware(
CORSMiddleware,
@@ -281,6 +283,10 @@ async def catch_all(request: Request, rest_of_path: str):
"index.html", {"request": request}
)
+ @self.app.get(f"{ROUTE_PREFIX}/api/metadata")
+ async def version():
+ return {"version": self.app.version}
+
def index_template(template, templates, templates_grouped, filename=None):
idx = filename if filename else template["idx"]
diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
index 2a1e925..71ff1a6 100644
--- a/frontend/.eslintrc.cjs
+++ b/frontend/.eslintrc.cjs
@@ -3,17 +3,25 @@
module.exports = {
root: true,
- env: { browser: true, es2020: true },
- extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
+ env: { browser: true, es2020: true, node: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:react/recommended',
+ 'plugin:react/jsx-runtime',
+ 'plugin:react-hooks/recommended',
+ 'plugin:storybook/recommended'
+ ],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
+ 'react/prop-types': 'off',
'react-refresh/only-export-components': [
'warn',
- { allowConstantExport: true },
+ { allowConstantExport: true }
],
- },
-}
+ 'max-len': ['error', { code: 79 }]
+ }
+};
diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js
index 05379c7..d17e3d4 100644
--- a/frontend/.prettierrc.js
+++ b/frontend/.prettierrc.js
@@ -4,5 +4,11 @@
*/
module.exports = {
- plugins: [require.resolve("prettier-plugin-tailwindcss")],
-};
\ No newline at end of file
+ plugins: [require.resolve('prettier-plugin-tailwindcss')],
+ semi: true,
+ tabWidth: 2,
+ printWidth: 79,
+ singleQuote: true,
+ trailingComma: 'none',
+ bracketSameLine: true
+};
diff --git a/frontend/fetch-version.py b/frontend/fetch-version.py
new file mode 100644
index 0000000..c839100
--- /dev/null
+++ b/frontend/fetch-version.py
@@ -0,0 +1,38 @@
+# Copyright DB InfraGO AG and contributors
+# SPDX-License-Identifier: Apache-2.0
+
+import json
+import os
+import pathlib
+import subprocess
+
+
+def run_git_command(cmd: list[str]):
+ try:
+ return subprocess.run(
+ ["git", *cmd],
+ check=True,
+ capture_output=True,
+ cwd=pathlib.Path(__file__).parent,
+ ).stdout.decode()
+ except subprocess.CalledProcessError:
+ return "No tags found"
+
+
+if os.getenv("MODE") == "production":
+ path = pathlib.Path(__file__).parent / "dist" / "static" / "version.json"
+else:
+ path = pathlib.Path(__file__).parent / "public" / "static" / "version.json"
+
+path.write_text(
+ json.dumps(
+ {
+ "git": {
+ "version": run_git_command(["describe", "--tags"]).strip(),
+ "tag": run_git_command(
+ ["describe", "--tags", "--abbrev=0"]
+ ).strip(),
+ }
+ }
+ )
+)
diff --git a/frontend/index.html b/frontend/index.html
index cbe16b5..769244a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -7,7 +7,6 @@
-
Model Explorer
diff --git a/frontend/package.json b/frontend/package.json
index a79afc5..3bd4c23 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -8,7 +8,9 @@
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
- "build-storybook": "storybook build"
+ "build-storybook": "storybook build",
+ "lint:fix": "eslint . --fix",
+ "format": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc.json"
},
"dependencies": {
"lucide-react": "^0.372.0",
@@ -20,6 +22,7 @@
"tailwind-scrollbar": "^3.1.0"
},
"devDependencies": {
+ "@eslint/js": "^9.3.0",
"@storybook/addon-essentials": "^7.6.16",
"@storybook/addon-interactions": "^7.6.16",
"@storybook/addon-links": "^7.6.16",
@@ -34,11 +37,14 @@
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"dompurify": "^3.0.9",
- "eslint": "^8.56.0",
- "eslint-plugin-react": "^7.33.2",
- "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint": "^8.57.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-react": "^7.34.1",
+ "eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-storybook": "^0.8.0",
+ "globals": "^15.3.0",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.14",
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
index af0c1fc..f139503 100644
--- a/frontend/postcss.config.js
+++ b/frontend/postcss.config.js
@@ -8,4 +8,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
-}
\ No newline at end of file
+}
diff --git a/frontend/src/APIConfig.js b/frontend/src/APIConfig.js
index 61639d0..b0b75f1 100644
--- a/frontend/src/APIConfig.js
+++ b/frontend/src/APIConfig.js
@@ -6,7 +6,7 @@
var API_BASE_URL = window.env.API_BASE_URL;
var ROUTE_PREFIX = window.env.ROUTE_PREFIX;
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
- API_BASE_URL = "http://localhost:8000/api";
- ROUTE_PREFIX = "";
+ ROUTE_PREFIX = "";
+ API_BASE_URL = `http://localhost:8000${ROUTE_PREFIX}/api`;
}
export { API_BASE_URL, ROUTE_PREFIX };
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 9344744..ce8c1a8 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,7 +1,6 @@
// Copyright DB InfraGO AG and contributors
// SPDX-License-Identifier: Apache-2.0
-import { useState } from "react";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import { API_BASE_URL, ROUTE_PREFIX } from "./APIConfig";
import "./App.css";
@@ -9,27 +8,21 @@ import { HomeView } from "./views/HomeView";
import { TemplateView } from "./views/TemplateView";
function App() {
- const [count, setCount] = useState(0);
-
- return (
-
-
- } />
-
- }
- />
-
- }
- />
-
-
- );
+ return (
+
+
+ } />
+ }
+ />
+ }
+ />
+
+
+ );
}
export default App;
diff --git a/frontend/src/components/Breadcrumbs.jsx b/frontend/src/components/Breadcrumbs.jsx
index b3bd668..31cb7b2 100644
--- a/frontend/src/components/Breadcrumbs.jsx
+++ b/frontend/src/components/Breadcrumbs.jsx
@@ -1,15 +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 { 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 [error, setError] = useState(null);
+ const pathnames = location.pathname.split('/').filter((x) => x);
const fetchModelInfo = async () => {
const response = await fetch(API_BASE_URL + `/model-info`);
@@ -37,10 +36,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]);
@@ -59,30 +58,53 @@ export const Breadcrumbs = () => {
}, [location]);
const visible_pathnames = [
- breadcrumbLabels["/"],
- ...location.pathname.split("/").filter((x) => x),
+ breadcrumbLabels['/'],
+ ...location.pathname.split('/').filter((x) => x)
];
+ const slicedPathnames = visible_pathnames.slice(1);
return (