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 (