Skip to content

Commit

Permalink
feat: Display model diff on objects in sidebar
Browse files Browse the repository at this point in the history
  • Loading branch information
freshavocado7 committed Jul 16, 2024
1 parent 71be165 commit bc19bec
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 81 deletions.
7 changes: 5 additions & 2 deletions capella_model_explorer/backend/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import click
import uvicorn

from . import explorer
from . import explorer, model_diff

HOST = os.getenv("CAPELLA_MODEL_EXPLORER_HOST_IP", "0.0.0.0")
PORT = os.getenv("CAPELLA_MODEL_EXPLORER_PORT", "8000")
Expand All @@ -25,7 +25,10 @@
default=PATH_TO_TEMPLATES,
)
def run(model: capellambse.MelodyModel, templates: Path):
backend = explorer.CapellaModelExplorerBackend(Path(templates), model)
diff = model_diff.model_diff()
backend = explorer.CapellaModelExplorerBackend(
Path(templates), model, diff
)
uvicorn.run(backend.app, host=HOST, port=int(PORT))


Expand Down
5 changes: 5 additions & 0 deletions capella_model_explorer/backend/explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class CapellaModelExplorerBackend:

templates_path: Path
model: capellambse.MelodyModel
model_diff: str

templates_index: t.Optional[tl.TemplateCategories] = dataclasses.field(
init=False
Expand Down Expand Up @@ -278,6 +279,10 @@ async def catch_all(request: Request, rest_of_path: str):
async def version():
return {"version": self.app.version}

@self.app.get("/api/model-diff")
async def model_diff():
return self.model_diff


def index_template(template, templates, templates_grouped, filename=None):
idx = filename if filename else template["idx"]
Expand Down
118 changes: 118 additions & 0 deletions capella_model_explorer/backend/model_diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import argparse
import subprocess
from pathlib import Path

import capellambse
from capella_diff_tools import __main__ as capella_diff_tools


def model_diff():
data: dict = {
"created": {},
"modified": {},
"deleted": {},
}
parser = argparse.ArgumentParser()
parser.add_argument("file_path", type=Path)
p = parser.parse_args()
model_path = p.file_path
model_dict = {"path": capella_diff_tools._ensure_git(model_path)}
if model_dict["path"]:
print(f"The model at {model_path} is inside a Git repository.")
commit_hashes_result = subprocess.run(
["git", "log", "--format=%H"],
cwd=model_path,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
commit_hashes = commit_hashes_result.stdout.strip().split("\n")
if len(commit_hashes) > 1:

old_model = capellambse.MelodyModel(
**model_dict, revision=commit_hashes[7]
)
new_model = capellambse.MelodyModel(
**model_dict, revision=commit_hashes[0]
)
objects = capella_diff_tools.compare.compare_all_objects(
old_model, new_model
)
diagrams = capella_diff_tools.compare.compare_all_diagrams(
old_model, new_model
)
print(objects)
transform_object_dict(objects)
data = {
"Diagrams": transform_diagram_dict(diagrams),
"Objects": transform_object_dict(objects),
}
return data
else:
pass

return data


def transform_diagram_dict(dict):
modified: list = []
created: list = []
deleted: list = []
traverse_diagrams(dict, created, modified, deleted)
created_dict = [
{"name": item["display_name"], "uuid": item["uuid"]}
for item in created
]
modified_dict = [
{"name": item["display_name"], "uuid": item["uuid"]}
for item in modified
]
deleted_dict = [
{"name": item["display_name"], "uuid": item["uuid"]}
for item in deleted
]
diff_dict = {
"Created": created_dict,
"Modified": modified_dict,
"Deleted": deleted_dict,
}
return diff_dict


def traverse_diagrams(node, created, modified, deleted):
if isinstance(node, dict):
for key, value in node.items():
if key == "modified":
modified.extend(value)
elif key == "created":
created.extend(value)
elif key == "deleted":
deleted.extend(value)
else:
traverse_diagrams(value, created, modified, deleted)
elif isinstance(node, list):
for item in node:
traverse_diagrams(item, created, modified, deleted)


def transform_object_dict(original_dict):
obj: dict = {}
for _, object in original_dict.items():
for category, actions in object.items():
if category not in obj:
obj[category] = {
"created": [],
"modified": [],
"deleted": [],
}
for action, items in actions.items():
for item in items:
display_name = item["display_name"]
obj[category][action].append(
{"name": display_name, "uuid": item["uuid"]}
)
return obj
139 changes: 81 additions & 58 deletions frontend/src/components/TemplateCard.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright DB InfraGO AG and contributors
// SPDX-License-Identifier: Apache-2.0

import React, { useState, useEffect } from 'react';
import { API_BASE_URL } from '../APIConfig';

import {
FlaskConical,
TriangleAlert,
Expand Down Expand Up @@ -29,63 +32,83 @@ export const TemplateCard = ({
isStable = true,
instanceCount = 0,
error = false
}) => (
<div
onClick={() => onClickCallback(idx)}
className={
'm-2 mt-6 max-w-sm cursor-pointer rounded-lg bg-gray-200 shadow-md ' +
'hover:bg-custom-light dark:bg-custom-dark-2 dark:shadow-dark ' +
'dark:hover:bg-custom-dark-4'
}>
<div className="p-5">
<div className="flex flex-row justify-between">
<h5
className={
'mb-2 text-left text-2xl font-normal text-gray-900 dark:text-gray-100'
}
style={{ overflowWrap: 'break-word' }}>
{name}
</h5>
{instanceCount === 1 && (
<span className="ml-4 text-gray-900 dark:text-gray-100">
<FileText size={16} style={{ display: 'inline-block' }} />
</span>
)}
{instanceCount > 1 && (
<span className="ml-4 text-gray-900 dark:text-gray-100">
<FileStack size={16} style={{ display: 'inline-block' }} />{' '}
{instanceCount}
</span>
)}
</div>
<p className="mb-3 text-left font-normal text-gray-700 dark:text-gray-300">
{description}
</p>
<div className={'text-left'}>
{error && (
<Badge color={'danger'}>
<TriangleAlert size={16} style={{ display: 'inline-block' }} />{' '}
{error}
</Badge>
)}
{isStable && (
<Badge color={'success'}>
<ShieldCheck size={16} style={{ display: 'inline-block' }} />{' '}
Stable
</Badge>
)}
{isExperimental && (
<Badge color={'warning'}>
<FlaskConical size={16} style={{ display: 'inline-block' }} />{' '}
Experimental
</Badge>
)}
{isDocument && (
<Badge>
<Book size={16} style={{ display: 'inline-block' }} /> Document
</Badge>
)}
}) => {
const [modelDiff, setModelDiff] = useState(null);
const [errorTest, setError] = useState(null);

useEffect(() => {
const fetchModelDiff = async () => {
try {
const response = await fetch(API_BASE_URL + '/model-diff');
const data = await response.json();
setModelDiff(data);
} catch (err) {
setError('Failed to fetch model info: ' + err.message);
}
document.body.style.height = 'auto';
};

fetchModelDiff();
}, []);

return (
<div
onClick={() => onClickCallback(idx)}
className={
'm-2 mt-6 max-w-sm cursor-pointer rounded-lg bg-gray-200 shadow-md ' +
'hover:bg-custom-light dark:bg-custom-dark-2 dark:shadow-dark ' +
'dark:hover:bg-custom-dark-4'
}>
<div className="p-5">
<div className="flex flex-row justify-between">
<h5
className={
'mb-2 text-left text-2xl font-normal text-gray-900 dark:text-gray-100'
}
style={{ overflowWrap: 'break-word' }}>
{name}
</h5>
{instanceCount === 1 && (
<span className="ml-4 text-gray-900 dark:text-gray-100">
<FileText size={16} style={{ display: 'inline-block' }} />
</span>
)}
{instanceCount > 1 && (
<span className="ml-4 text-gray-900 dark:text-gray-100">
<FileStack size={16} style={{ display: 'inline-block' }} />{' '}
{instanceCount}
</span>
)}
</div>
<p className="mb-3 text-left font-normal text-gray-700 dark:text-gray-300">
{description}
</p>
<div className={'text-left'}>
{error && (
<Badge color={'danger'}>
<TriangleAlert size={16} style={{ display: 'inline-block' }} />{' '}
{error}
</Badge>
)}
{isStable && (
<Badge color={'success'}>
<ShieldCheck size={16} style={{ display: 'inline-block' }} />{' '}
Stable
</Badge>
)}
{isExperimental && (
<Badge color={'warning'}>
<FlaskConical size={16} style={{ display: 'inline-block' }} />{' '}
Experimental
</Badge>
)}
{isDocument && (
<Badge>
<Book size={16} style={{ display: 'inline-block' }} /> Document
</Badge>
)}
</div>
</div>
</div>
</div>
);
);
};
Loading

0 comments on commit bc19bec

Please sign in to comment.