diff --git a/.commitlintrc.yml b/.commitlintrc.yml index dab71d5..ef03960 100644 --- a/.commitlintrc.yml +++ b/.commitlintrc.yml @@ -15,3 +15,5 @@ rules: always, [build, chore, ci, docs, feat, fix, merge, perf, refactor, revert, test], ] +extends: + - '@commitlint/config-conventional' diff --git a/.github/workflows/build-test-publish.yml b/.github/workflows/build-test-publish.yml index a0ee908..29c7479 100644 --- a/.github/workflows/build-test-publish.yml +++ b/.github/workflows/build-test-publish.yml @@ -4,10 +4,11 @@ name: Build on: + pull_request: push: branches: ["*"] - pull_request: [master] tags: ["v*.*.*"] + workflow_dispatch: jobs: test: @@ -24,18 +25,13 @@ jobs: - os: windows-latest python_version: "3.11" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{matrix.python_version}} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: + cache: pip + cache-dependency-path: pyproject.toml python-version: ${{matrix.python_version}} - - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: ${{runner.os}}-pip-${{hashFiles('pyproject.toml')}} - restore-keys: | - ${{runner.os}}-pip- - ${{runner.os}}- - name: Upgrade Pip run: |- python -m pip install -U pip @@ -46,14 +42,29 @@ jobs: run: |- python -m pytest --cov-report=term --cov=capella_model_explorer --rootdir=. - publish: - name: Publish artifacts + pre-commit: runs-on: ubuntu-latest - needs: test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + cache: pip + cache-dependency-path: pyproject.toml + python-version: "3.11" + - name: Upgrade pip + run: python -m pip install -U pip + - name: Install pre-commit + run: python -m pip install 'pre-commit>=3,<4' + - name: Run Pre-Commit + run: pre-commit run --all-files + + build: + name: Build wheel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies @@ -67,10 +78,25 @@ jobs: run: |- python -m twine check dist/* - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: 'dist/*' + pypi: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: [build, test] + if: startsWith(github.ref, 'refs/tags/v') + environment: + name: pypi + url: https://pypi.org/project/capella-model-explorer + permissions: + id-token: write + steps: + - name: Download built wheel + uses: actions/download-artifact@v4 with: - name: Artifacts - path: "dist/*" - - name: Publish to PyPI (release only) - if: startsWith(github.ref, 'refs/tags/v') - run: python -m twine upload -u __token__ -p ${{ secrets.PYPI_TOKEN }} --non-interactive dist/* + name: python-package-distributions + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/commit-check.yml b/.github/workflows/commit-check.yml index f06701d..7b34e18 100644 --- a/.github/workflows/commit-check.yml +++ b/.github/workflows/commit-check.yml @@ -10,12 +10,15 @@ on: jobs: conventional-commits: runs-on: ubuntu-latest + concurrency: + group: commit-check-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install commitlint - run: npm install -g @commitlint/cli + run: npm install @commitlint/cli @commitlint/config-conventional - name: Validate commit messages id: conventional-commits env: @@ -25,14 +28,24 @@ jobs: delim="_EOF_$(uuidgen)" echo "validation-result<<$delim" >> "$GITHUB_OUTPUT" r=0 - commitlint --from "$SHA_FROM" --to "$SHA_TO" >> "$GITHUB_OUTPUT" 2>&1 || r=$? + npx commitlint --from "$SHA_FROM" --to "$SHA_TO" >> "$GITHUB_OUTPUT" 2>&1 || r=$? echo "$delim" >> "$GITHUB_OUTPUT" exit $r + - name: Find conventional commit comment on PR + uses: peter-evans/find-comment@v3 + if: always() && steps.conventional-commits.outcome == 'failure' + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: conventional commit - name: Post comment if validation failed + uses: peter-evans/create-or-update-comment@v4 if: always() && steps.conventional-commits.outcome == 'failure' - uses: actions/github-script@v6 - env: - TEXT: |- + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | The pull request does not conform to the conventional commit specification. Please ensure that your commit messages follow the spec: . We also strongly recommend that you set up your development environment with pre-commit, as described in our [CONTRIBUTING guidelines](https://github.com/DSD-DBS/capella-model-explorer/blob/master/CONTRIBUTING.md). This will run all the important checks right before you commit your changes, and avoids lengthy CI wait time and round trips. @@ -47,11 +60,4 @@ jobs: docs(user): Add model creation workflow feat: Add a monitoring dashboard ``` - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: process.env.TEXT - }) + edit-mode: replace diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 68e5d0a..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: CC0-1.0 - -name: Lint - -on: - push: - branches: ["*"] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: "3.11" - - name: Upgrade pip - run: |- - python -m pip install -U pip - - name: Install pre-commit - run: |- - python -m pip install pre-commit types-docutils - - name: Run Pre-Commit - run: |- - pre-commit run --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e892cd5..ddfaa50 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,12 @@ default_install_hook_types: [commit-msg, pre-commit] default_stages: [commit, merge-commit] +minimum_pre_commit_version: 3.2.0 repos: + - repo: https://github.com/gitleaks/gitleaks.git + rev: v8.19.3 + hooks: + - id: gitleaks - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: @@ -25,40 +30,6 @@ repos: - id: end-of-file-fixer - id: fix-byte-order-marker - id: trailing-whitespace - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.8.0 - hooks: - - id: black - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - repo: https://github.com/PyCQA/docformatter - rev: v1.7.5 - hooks: - - id: docformatter - additional_dependencies: - - docformatter[tomli] - - repo: https://github.com/PyCQA/pydocstyle - rev: 6.3.0 - hooks: - - id: pydocstyle - exclude: '^tests/' - additional_dependencies: - - pydocstyle[toml] - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 - hooks: - - id: mypy - additional_dependencies: - - capellambse==0.6.6 - - types-pyyaml==6.0.11 - - repo: https://github.com/pylint-dev/pylint - rev: v3.2.7 - hooks: - - id: pylint - require_serial: false - args: [-rn, -sn, -dfixme, -dduplicate-code] - repo: https://github.com/Lucas-C/pre-commit-hooks rev: v1.5.5 hooks: @@ -112,12 +83,39 @@ repos: - LICENSES/.license_header.txt - --comment-style - '//' + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.5 + hooks: + - id: docformatter + additional_dependencies: + - docformatter[tomli] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.8 + hooks: + - id: ruff-format + - id: ruff + args: [--extend-ignore=FIX] + - repo: https://github.com/rhysd/actionlint + rev: v1.7.3 + hooks: + - id: actionlint-docker + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.2 + hooks: + - id: mypy + additional_dependencies: + - capellambse==0.6.6 + - types-pyyaml==6.0.11 - repo: https://github.com/fsfe/reuse-tool rev: v4.0.3 hooks: - id: reuse - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook - rev: v9.17.0 + rev: v9.18.0 hooks: - id: commitlint stages: [commit-msg] @@ -127,10 +125,12 @@ repos: name: prettier entry: prettier --write language: node - types_or: [ts, css, html, markdown] + types_or: [javascript, jsx, ts, css, html, markdown] additional_dependencies: - 'prettier@^3.3.3' - 'prettier-plugin-tailwindcss@^0.6.6' + - 'prettier-plugin-classnames@^0.7.2' + - 'prettier-plugin-merge@^0.7.1' - 'tailwind-scrollbar@^3.1.0' - repo: https://github.com/pre-commit/mirrors-eslint rev: v9.10.0 @@ -147,3 +147,6 @@ repos: - 'eslint-plugin-react@^7.34.1' - 'eslint-plugin-react-hooks@^4.6.2' - 'eslint-plugin-react-refresh@^0.4.5' + args: ['--fix'] + types: [] + files: '^frontend/.*\.(js|jsx)$' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6736808..695b69f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,12 +10,11 @@ Thanks for your interest in our project. Contributions are always welcome! We are committed to fostering a welcoming, respectful, and harassment-free environment. Be kind! -If you have questions, ideas or want to report a bug, feel free to [open -an issue]. Or go ahead and [open a pull request] to contribute code. In order to -reduce the burden on our maintainers, please make sure that your code follows our -style guidelines outlined below. +If you have questions, ideas or want to report a bug, feel free to [open an +issue]. Or go ahead and [open a pull request] to contribute code. In order to +reduce the burden on our maintainers, please make sure that your code follows +our style guidelines outlined below. - [open an issue]: https://github.com/DSD-DBS/capella-model-explorer/issues [open a pull request]: https://github.com/DSD-DBS/capella-model-explorer/pulls @@ -39,8 +38,8 @@ We additionally recommend that you set up your editor / IDE as follows. - _If you use Visual Studio Code_: Consider using a platform which supports third-party language servers more easily, and continue with the next point. - Otherwise, set up the editor to run `black`, `pylint` and `mypy` when saving. - To enable automatic import sorting with `isort`, add the following to your + Otherwise, set up the editor to run `ruff` and `mypy` when saving. To enable + automatic import sorting with `isort`, add the following to your `settings.json`: ```json @@ -54,14 +53,16 @@ We additionally recommend that you set up your editor / IDE as follows. Note that the Pylance language server is not recommended, as it occasionally causes false-positive errors for perfectly valid code. -- _If you do not use VSC_: Set up your editor to use the [python-lsp-server], - and make sure that the relevant plugins are installed. You can install - everything that's needed into the virtualenv with pip: +- _If you do not use VSC_: Set up your editor to use the [python-lsp-server] + and [ruff], and make sure that the relevant pylsp plugins are installed. [python-lsp-server]: https://github.com/python-lsp/python-lsp-server + [ruff]: https://github.com/astral-sh/ruff + + You can install everything that's needed into the virtualenv with pip: ```sh - pip install "python-lsp-server[pylint]" python-lsp-black pyls-isort pylsp-mypy + pip install "python-lsp-server" pyls-isort pylsp-mypy ruff ``` This will provide as-you-type linting as well as automatic formatting on @@ -79,7 +80,7 @@ The key differences are: [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". + [PEP-257]. For example, write "Do X and Y" instead of "Does X and Y". [pep-257]: https://peps.python.org/pep-0257/ @@ -89,20 +90,18 @@ The key differences are: automated tools pick up the full base class docstring instead, and is therefore more useful in IDEs etc. -- **Linting**: Use [pylint] for static code analysis, and [mypy] for static +- **Linting**: Use [ruff] for static code analysis, and [mypy] for static type checking. - [pylint]: https://github.com/PyCQA/pylint [mypy]: https://github.com/python/mypy -- **Formatting**: Use [black] as code auto-formatter. The maximum line length +- **Formatting**: Use [ruff] as code auto-formatter. The maximum line length is 79, as per [PEP-8]. This setting should be automatically picked up from the `pyproject.toml` file. The reason for the shorter line length is that it avoids wrapping and overflows in side-by-side split views (e.g. diffs) if there's also information displayed to the side of it (e.g. a tree view of the modified files). - [black]: https://github.com/psf/black [pep-8]: https://www.python.org/dev/peps/pep-0008/ Be aware of the different line length of 72 for docstrings. We currently do @@ -122,24 +121,20 @@ The key differences are: writing `from typing import SomeName`, use `import typing as t` and access typing related classes like `t.TypedDict`. - 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` etc. - For classes that are not builtin (e.g. `Iterable`), `import collections.abc as cabc` and then use them like `cabc.Iterable`. - - Use [PEP-604-style unions], e.g. `int | float` instead of `t.Union[int, float]`. + - Use [PEP-604-style unions], e.g. `int | float` instead of + `t.Union[int, float]`. - Use `... | None` (with `None` always as the last union member) instead of `t.Optional[...]` and always explicitly annotate where `None` is possible. [pep-604-style unions]: https://www.python.org/dev/peps/pep-0604/ -- **Python style rules**: For conflicting parts, the [Black code style] wins. - 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 +- **Python style rules**: The auto-formatter wins. - 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/capella_model_explorer/__init__.py b/capella_model_explorer/__init__.py index 55462c7..7d6af1a 100644 --- a/capella_model_explorer/__init__.py +++ b/capella_model_explorer/__init__.py @@ -1,6 +1,7 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 """The capella_model_explorer package.""" + from importlib import metadata try: diff --git a/capella_model_explorer/backend/__init__.py b/capella_model_explorer/backend/__init__.py index 55462c7..7d6af1a 100644 --- a/capella_model_explorer/backend/__init__.py +++ b/capella_model_explorer/backend/__init__.py @@ -1,6 +1,7 @@ # Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 """The capella_model_explorer package.""" + from importlib import metadata try: diff --git a/capella_model_explorer/backend/__main__.py b/capella_model_explorer/backend/__main__.py index dfacaa6..3818491 100644 --- a/capella_model_explorer/backend/__main__.py +++ b/capella_model_explorer/backend/__main__.py @@ -25,7 +25,6 @@ default=PATH_TO_TEMPLATES, ) def run(model: capellambse.MelodyModel, templates: Path): - backend = explorer.CapellaModelExplorerBackend( Path(templates), model, diff --git a/capella_model_explorer/backend/explorer.py b/capella_model_explorer/backend/explorer.py index c56c541..8820efc 100644 --- a/capella_model_explorer/backend/explorer.py +++ b/capella_model_explorer/backend/explorer.py @@ -63,7 +63,7 @@ class CapellaModelExplorerBackend: templates_path: Path model: capellambse.MelodyModel - templates_index: t.Optional[tl.TemplateCategories] = dataclasses.field( + templates_index: tl.TemplateCategories | None = dataclasses.field( init=False ) @@ -97,10 +97,7 @@ def __post_init__(self): @self.app.middleware("http") async def update_last_interaction_time(request: Request, call_next): - if ( - not request.url.path == "/metrics" - and not request.url.path == "/favicon.ico" - ): + if request.url.path not in ("/metrics", "/favicon.ico"): self.last_interaction = time.time() return await call_next(request) @@ -116,7 +113,7 @@ def __make_href_filter(self, obj: object) -> str | None: if isinstance(obj, m.ElementList): raise TypeError("Cannot make an href to a list of elements") - if not isinstance(obj, (m.ModelElement, m.AbstractDiagram)): + if not isinstance(obj, m.ModelElement | m.AbstractDiagram): raise TypeError(f"Expected a model object, got {obj!r}") try: @@ -162,15 +159,15 @@ def render_instance_page(self, template_text, base, object=None): trace = markupsafe.escape(traceback.format_exc()) error_message = markupsafe.Markup( '

' - f"Unexpected error: {type(e).__name__}: {str(e)}" + f"Unexpected error: {type(e).__name__}: {e}" '

'
-                f"object={repr(object)}\nmodel={repr(self.model)}"
+                f"object={object!r}\nmodel={self.model!r}"
                 f"\n\n{trace}"
                 "
" ) return HTMLResponse(content=error_message) - def configure_routes(self): + def configure_routes(self): # noqa: C901 self.app.mount( f"{ROUTE_PREFIX}/assets", StaticFiles( @@ -206,12 +203,12 @@ def read_template(template_name: str): template_name = urlparse.unquote(template_name) if ( self.templates_index is None - or not template_name in self.templates_index.flat + or template_name not in self.templates_index.flat ): return { "error": ( - f"Template {template_name} not found or" - + " templates index not initialized" + f"Template {template_name} not found" + " or templates index not initialized" ) } base = self.templates_index.flat[template_name] @@ -226,7 +223,7 @@ def render_template(template_name: str, object_id: str): template_name = urlparse.unquote(template_name) if ( self.templates_index is None - or not template_name in self.templates_index.flat + or template_name not in self.templates_index.flat ): return {"error": f"Template {template_name} not found"} base = self.templates_index.flat[template_name] @@ -286,7 +283,9 @@ async def post_compare(commit_range: CommitRange): self.model, commit_range.head, commit_range.prev ) self.diff["lookup"] = create_diff_lookup(self.diff["objects"]) - return {"success": True} + if self.diff["lookup"]: + return {"success": True} + return {"success": False, "error": "No model changes to show"} except Exception as e: LOGGER.exception("Failed to compare versions") return {"success": False, "error": str(e)} @@ -301,8 +300,10 @@ async def post_object_diff(object_id: ObjectDiffID): @self.router.get("/api/commits") async def get_commits(): - result = model_diff.populate_commits(self.model) - return result + try: + return model_diff.populate_commits(self.model) + except Exception as e: + return {"error": str(e)} @self.router.get("/api/diff") async def get_diff(): @@ -364,9 +365,8 @@ def create_diff_lookup(data, lookup=None): "change": obj["change"], "attributes": obj["attributes"], } - if "children" in obj: - if obj["children"]: - create_diff_lookup(obj["children"], lookup) + if children := obj.get("children"): + create_diff_lookup(children, lookup) except Exception: LOGGER.exception("Cannot create diff lookup") return lookup diff --git a/capella_model_explorer/backend/model_diff.py b/capella_model_explorer/backend/model_diff.py index c4f7cce..a01d214 100644 --- a/capella_model_explorer/backend/model_diff.py +++ b/capella_model_explorer/backend/model_diff.py @@ -98,7 +98,7 @@ class ChangeSummaryDocument(te.TypedDict): objects: ObjectChanges -def init_model(model: capellambse.MelodyModel) -> t.Optional[str]: +def init_model(model: capellambse.MelodyModel) -> str | None: """Initialize the model and return the path if it's a git repository.""" file_handler = model.resources["\x00"] path = file_handler.path @@ -119,16 +119,15 @@ def populate_commits(model: capellambse.MelodyModel): path = init_model(model) if not path: return path - commits = get_commit_hashes(path) - return commits + return get_commit_hashes(path) def _serialize_obj(obj: t.Any) -> t.Any: if isinstance(obj, m.ModelElement): return {"uuid": obj.uuid, "display_name": _get_name(obj)} - elif isinstance(obj, m.ElementList): + if isinstance(obj, m.ElementList): return [{"uuid": i.uuid, "display_name": _get_name(i)} for i in obj] - elif isinstance(obj, (enum.Enum, enum.Flag)): + if isinstance(obj, enum.Enum | enum.Flag): return obj.name return obj @@ -168,7 +167,7 @@ def _get_revision_info( .strip() .split("\x00") ) - subject = description.splitlines()[0] + subject = description.splitlines()[0] if description.splitlines() else "" try: tag = subprocess.check_output( ["git", "tag", "--points-at", revision], @@ -197,8 +196,7 @@ def get_commit_hashes(path: str) -> list[RevisionInfo]: cwd=path, encoding="utf-8", ).splitlines() - commits = [_get_revision_info(path, c) for c in commit_hashes] - return commits + return [_get_revision_info(path, c) for c in commit_hashes] def _get_name(obj: m.ModelObject) -> str: @@ -232,7 +230,6 @@ def compare_objects( new_object: capellambse.ModelObject, old_model: capellambse.MelodyModel, ): - assert old_object is None or type(old_object) is type( new_object ), f"{type(old_object).__name__} != {type(new_object).__name__}" @@ -396,16 +393,16 @@ def _traverse_and_diff(data) -> dict[str, t.Any]: and "current" in value ): curr_type = type(value["current"]) - if curr_type == str: + if curr_type is str: diff = _diff_text( (value["previous"] or "").splitlines(), value["current"].splitlines(), ) updates[key] = {"diff": diff} - elif curr_type == dict: + elif curr_type is dict: diff = _diff_objects(value["previous"], value["current"]) updates[key] = {"diff": diff} - elif curr_type == list: + elif curr_type is list: diff = _diff_lists(value["previous"], value["current"]) updates[key] = {"diff": diff} elif key == "description": @@ -413,7 +410,7 @@ def _traverse_and_diff(data) -> dict[str, t.Any]: (value["previous"] or "").splitlines(), value["current"].splitlines(), ) - if prev == curr == None: + if prev is curr is None: continue updates[key] = {"diff": ""} value.update({"previous": prev, "current": curr}) @@ -462,8 +459,8 @@ def _diff_lists(previous, current): def _diff_description( previous, current -) -> t.Tuple[str, str] | t.Tuple[None, None]: - if previous == current == None: +) -> tuple[str, str] | tuple[None, None]: + if previous is current is None: return None, None dmp = diff_match_patch.diff_match_patch() diff = dmp.diff_main("\n".join(previous), "\n".join(current)) diff --git a/capella_model_explorer/backend/templates.py b/capella_model_explorer/backend/templates.py index 9e5bade..ca8fecd 100644 --- a/capella_model_explorer/backend/templates.py +++ b/capella_model_explorer/backend/templates.py @@ -4,7 +4,7 @@ import operator import traceback from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any import capellambse import capellambse.model as m @@ -12,7 +12,7 @@ from pydantic import BaseModel, Field -def simple_object(obj) -> Dict[str, Any]: +def simple_object(obj) -> dict[str, Any]: if not obj: return {} if obj.name: @@ -50,9 +50,8 @@ def find_objects(model, obj_type=None, below=None, attr=None, filters=None): if filter == "not_empty": if attr: filtered.append(object) - else: - if attr == filter: - filtered.append(object) + elif attr == filter: + filtered.append(object) objects = filtered return objects @@ -63,11 +62,11 @@ class InstanceDetails(BaseModel): class TemplateScope(BaseModel): - type: Optional[str] = Field(None, title="Model Element Type") - below: Optional[str] = Field( + type: str | None = Field(None, title="Model Element Type") + below: str | None = Field( None, title="Model element to search below, scope limiter" ) - filters: Optional[Dict[str, Any]] = Field( + filters: dict[str, Any] | None = Field( {}, title="Filters to apply to the search" ) @@ -78,19 +77,19 @@ class Template(BaseModel): template: Path = Field(..., title="Template File Path") description: str = Field(..., title="Template Description") - scope: Optional[TemplateScope] = Field( + scope: TemplateScope | None = Field( default_factory=dict, title="Template Scope" ) - single: Optional[bool] = Field(None, title="Single Instance Flag") - isStable: Optional[bool] = Field(None, title="Stable Template Flag") - isDocument: Optional[bool] = Field(None, title="Document Template Flag") - isExperimental: Optional[bool] = Field( + single: bool | None = Field(None, title="Single Instance Flag") + isStable: bool | None = Field(None, title="Stable Template Flag") + isDocument: bool | None = Field(None, title="Document Template Flag") + isExperimental: bool | None = Field( None, title="Experimental Template Flag" ) - error: Optional[str] = Field(None, title="Broken Template Flag") - traceback: Optional[str] = Field(None, title="Template Error Traceback") - instanceCount: Optional[int] = Field(None, title="Number of Instances") - instanceList: Optional[List[Dict]] = Field(None, title="List of Instances") + error: str | None = Field(None, title="Broken Template Flag") + traceback: str | None = Field(None, title="Template Error Traceback") + instanceCount: int | None = Field(None, title="Number of Instances") + instanceList: list[dict] | None = Field(None, title="List of Instances") def find_instances(self, model: capellambse.MelodyModel): if self.single: @@ -106,8 +105,7 @@ def find_instances(self, model: capellambse.MelodyModel): attr=None, filters=self.scope.filters, ) - else: - return [] + return [] except Exception as e: self.error = f"Template scope error: {e}" self.traceback = traceback.format_exc() @@ -124,7 +122,7 @@ def compute_instance_list(self, model: capellambse.MelodyModel): class TemplateCategory(BaseModel): idx: str = Field(..., title="Category Identifier") - templates: List[Template] = Field( + templates: list[Template] = Field( default_factory=list, title="Templates in this category" ) @@ -137,7 +135,7 @@ def __add__(self, other): class TemplateCategories(BaseModel): - categories: List[TemplateCategory] = Field( + categories: list[TemplateCategory] = Field( default_factory=list, title="Template Categories" ) diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 71ff1a6..dd09ade 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -13,7 +13,11 @@ module.exports = { ], ignorePatterns: ['dist', '.eslintrc.cjs'], parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, - settings: { react: { version: '18.2' } }, + settings: { + react: { + version: '18.2' + } + }, plugins: ['react-refresh'], rules: { 'react/jsx-no-target-blank': 'off', @@ -22,6 +26,6 @@ module.exports = { 'warn', { allowConstantExport: true } ], - 'max-len': ['error', { code: 79 }] + 'max-len': ['error', { code: 100, ignoreUrls: true, ignoreStrings: true }] } }; diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js index d17e3d4..2947e97 100644 --- a/frontend/.prettierrc.js +++ b/frontend/.prettierrc.js @@ -4,11 +4,18 @@ */ module.exports = { - plugins: [require.resolve('prettier-plugin-tailwindcss')], + plugins: [ + require.resolve('prettier-plugin-tailwindcss'), + require.resolve('prettier-plugin-classnames'), + require.resolve('prettier-plugin-merge') + ], semi: true, tabWidth: 2, printWidth: 79, singleQuote: true, trailingComma: 'none', - bracketSameLine: true + bracketSameLine: true, + endOfLine: 'lf', + useTabs: false, + endingPosition: 'absolute-with-indent' }; diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index b11f63a..14e108f 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -3,14 +3,14 @@ /** @type { import('@storybook/react-vite').StorybookConfig } */ const config = { - stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: [ - "@storybook/addon-links", - "@storybook/addon-essentials", - "@storybook/addon-onboarding", - "@storybook/addon-interactions", + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-onboarding', + '@storybook/addon-interactions', { - name: "@storybook/addon-postcss", + name: '@storybook/addon-postcss', options: { postcssLoaderOptions: { implementation: require('postcss') @@ -19,11 +19,11 @@ const config = { } ], framework: { - name: "@storybook/react-vite", - options: {}, + name: '@storybook/react-vite', + options: {} }, docs: { - autodocs: "tag", - }, + autodocs: 'tag' + } }; export default config; diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js index b27ad0f..8c248a6 100644 --- a/frontend/.storybook/preview.js +++ b/frontend/.storybook/preview.js @@ -1,19 +1,19 @@ // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 -import '../src/index.css' +import '../src/index.css'; /** @type { import('@storybook/react').Preview } */ const preview = { parameters: { - actions: { argTypesRegex: "^on[A-Z].*" }, + actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, - date: /Date$/i, - }, - }, - }, + date: /Date$/i + } + } + } }; export default preview; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f3f0d70..76ab84b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@icons-pack/react-simple-icons": "^10.0.0", "lucide-react": "^0.439.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -42,6 +43,8 @@ "globals": "^15.9.0", "postcss": "^8.4.45", "prettier": "^3.3.3", + "prettier-plugin-classnames": "^0.7.2", + "prettier-plugin-merge": "^0.7.1", "prettier-plugin-tailwindcss": "^0.6.6", "prop-types": "^15.8.1", "react-router-dom": "^6.26.2", @@ -2531,6 +2534,14 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@icons-pack/react-simple-icons": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@icons-pack/react-simple-icons/-/react-simple-icons-10.0.0.tgz", + "integrity": "sha512-oU0PVDx9sbNQjRxJN555dsHbRApYN+aBq/O9+wo3JgNkEfvBMgAEtsSGtXWWXQsLAxJcYiFOCzBWege/Xj/JFQ==", + "peerDependencies": { + "react": "^16.13 || ^17 || ^18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6039,6 +6050,15 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -10299,6 +10319,43 @@ "node": ">=6.0.0" } }, + "node_modules/prettier-plugin-classnames": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-classnames/-/prettier-plugin-classnames-0.7.2.tgz", + "integrity": "sha512-rocYqVSWV/YSJE+TA7b1IgYY9/I4bx0lxJjE5Iwv/kavNNEYhKh7Gl1+MQARQYgPisGMd5DU8Uj6ZEVX0KmTTA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "prettier": "^2 || ^3", + "prettier-plugin-astro": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/prettier-plugin-merge": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-merge/-/prettier-plugin-merge-0.7.1.tgz", + "integrity": "sha512-R3dSlv3kAlScjd/liWjTkGHcUrE4MBhPKKBxVOvHK7+FY2P5SEmLarZiD11VUEuaMRK0L7zqIurX6JcRYS9Y5Q==", + "dev": true, + "dependencies": { + "diff": "5.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + } + }, "node_modules/prettier-plugin-tailwindcss": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1609fed..cf9ecee 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,9 +10,10 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "lint:fix": "eslint . --fix", - "format": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc.json" + "format": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc.js" }, "dependencies": { + "@icons-pack/react-simple-icons": "^10.0.0", "lucide-react": "^0.439.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -39,7 +40,6 @@ "dompurify": "^3.1.6", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.35.2", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.11", @@ -47,6 +47,8 @@ "globals": "^15.9.0", "postcss": "^8.4.45", "prettier": "^3.3.3", + "prettier-plugin-classnames": "^0.7.2", + "prettier-plugin-merge": "^0.7.1", "prettier-plugin-tailwindcss": "^0.6.6", "prop-types": "^15.8.1", "react-router-dom": "^6.26.2", diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index f139503..61dd434 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -6,6 +6,6 @@ module.exports = { plugins: { tailwindcss: {}, - autoprefixer: {}, - }, -} + autoprefixer: {} + } +}; diff --git a/frontend/public/static/env.js b/frontend/public/static/env.js index 83ba5c4..1f83af3 100644 --- a/frontend/public/static/env.js +++ b/frontend/public/static/env.js @@ -4,6 +4,6 @@ */ window.env = { - ROUTE_PREFIX: "__ROUTE_PREFIX__", - API_BASE_URL: "__ROUTE_PREFIX__/api", + ROUTE_PREFIX: '__ROUTE_PREFIX__', + API_BASE_URL: '__ROUTE_PREFIX__/api' }; diff --git a/frontend/src/APIConfig.js b/frontend/src/APIConfig.js index b0b75f1..422211d 100644 --- a/frontend/src/APIConfig.js +++ b/frontend/src/APIConfig.js @@ -5,8 +5,8 @@ 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") { - ROUTE_PREFIX = ""; +if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { + ROUTE_PREFIX = ''; API_BASE_URL = `http://localhost:8000${ROUTE_PREFIX}/api`; } export { API_BASE_URL, ROUTE_PREFIX }; diff --git a/frontend/src/App.css b/frontend/src/App.css index ae634d3..071727e 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -44,3 +44,11 @@ .read-the-docs { color: #888; } + +html.dark .svg-display svg { + filter: hue-rotate(180deg) invert(80%) saturate(200%); +} + +html.dark .icon svg { + filter: none; +} diff --git a/frontend/src/components/AppInfo.jsx b/frontend/src/components/AppInfo.jsx new file mode 100644 index 0000000..8b69f11 --- /dev/null +++ b/frontend/src/components/AppInfo.jsx @@ -0,0 +1,37 @@ +// Copyright DB InfraGO AG and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useState, useEffect } from 'react'; +import { ROUTE_PREFIX } from '../APIConfig'; +import { SiGithub } from '@icons-pack/react-simple-icons'; + +export const AppInfo = () => { + const [currentVersion, setCurrentVersion] = useState(null); + + useEffect(() => { + fetch(`${ROUTE_PREFIX}/static/version.json`) + .then((response) => response.json()) + .then((data) => setCurrentVersion(data.git)) + .catch((error) => { + console.error(error); + setCurrentVersion({ version: 'Fetch failed' }); + }); + }, []); + + return ( +
+ {currentVersion &&

Version: {currentVersion.version}

} + + Contribute on GitHub +
+ +
+
+
+ ); +}; diff --git a/frontend/src/components/Badge.jsx b/frontend/src/components/Badge.jsx index a24b443..dd6b12c 100644 --- a/frontend/src/components/Badge.jsx +++ b/frontend/src/components/Badge.jsx @@ -1,18 +1,22 @@ - // Copyright DB InfraGO AG and contributors // SPDX-License-Identifier: Apache-2.0 const color_classes = { - "default": "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300", - "danger": "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300", - "success": "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300", - "warning": "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" -} + default: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300', + danger: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300', + success: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300', + warning: + 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300' +}; /* -* provides a badge that may have different colors -*/ -export const Badge = ({ children, color="default" }) => ( -
- {children} -
+ * provides a badge that may have different colors + */ +export const Badge = ({ children, color = 'default' }) => ( +
+ {children} +
); diff --git a/frontend/src/components/Breadcrumbs.jsx b/frontend/src/components/Breadcrumbs.jsx index 414d63e..d2805a6 100644 --- a/frontend/src/components/Breadcrumbs.jsx +++ b/frontend/src/components/Breadcrumbs.jsx @@ -51,10 +51,12 @@ export const Breadcrumbs = () => { } setBreadcrumbLabels(labels); + const modelName = Object.values(labels)[0]; + const instanceName = Object.values(labels).pop(); + document.title = `${instanceName} - ${modelName} - Model Explorer`; }; - updateLabels(); - }, [location]); + }, [location, document.title]); const visible_pathnames = [ breadcrumbLabels['/'], @@ -65,9 +67,7 @@ export const Breadcrumbs = () => { return (