Skip to content

Commit

Permalink
Merge branch 'main' into feat/add-label-dynamically
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohamed-Hacene committed Oct 31, 2024
2 parents 36f7624 + 54a1e7f commit 126fdd5
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 210 deletions.
6 changes: 5 additions & 1 deletion backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2257,7 +2257,11 @@ def get_requirement_assessments(self, include_non_assessable=False):
requirement
for requirement in requirements
if selected_implementation_groups_set
& set(requirement.requirement.implementation_groups)
& set(
requirement.requirement.implementation_groups
if requirement.requirement.implementation_groups
else []
)
]

return requirement_assessments_list
Expand Down
53 changes: 53 additions & 0 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,59 @@ class AssetViewSet(BaseModelViewSet):
def type(self, request):
return Response(dict(Asset.Type.choices))

@action(detail=False, name="Get assets graph")
def graph(self, request):
nodes = []
links = []
nodes_idx = dict()
categories = []
N = 0
for domain in Folder.objects.all():
categories.append({"name": domain.name})
nodes_idx[domain.name] = N
nodes.append(
{
"name": domain.name,
"category": N,
"symbol": "roundRect",
"symbolSize": 30,
"value": "Domain",
}
)
N += 1
for asset in Asset.objects.all():
symbol = "circle"
if asset.type == "PR":
symbol = "diamond"
nodes.append(
{
"name": asset.name,
"symbol": symbol,
"symbolSize": 25,
"category": nodes_idx[asset.folder.name],
"value": "Primary" if asset.type == "PR" else "Support",
}
)
nodes_idx[asset.name] = N
links.append(
{"source": nodes_idx[asset.folder.name], "target": N, "value": "scope"}
)
N += 1
for asset in Asset.objects.all():
for relationship in asset.parent_assets.all():
links.append(
{
"source": nodes_idx[relationship.name],
"target": nodes_idx[asset.name],
"value": "parent",
}
)
meta = {"display_name": f"Assets Explorer"}

return Response(
{"nodes": nodes, "links": links, "categories": categories, "meta": meta}
)


class ReferenceControlViewSet(BaseModelViewSet):
"""
Expand Down
7 changes: 2 additions & 5 deletions frontend/src/lib/components/DataViz/Article.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<article
class="hover:animate-background rounded-xl bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 p-0.5 shadow-xl transition hover:bg-[length:400%_400%] hover:shadow-sm hover:[animation-duration:_4s]"
>
<div class="rounded-[10px] bg-white p-4 !pt-20 sm:p-6">
<time datetime="2022-10-10" class="block text-xs text-gray-500"> {desc} </time>
<div class="rounded-[10px] bg-white p-4 !pt-20 sm:p-6 h-full">
<div class="block text-xs text-gray-500 min-h-[2.5rem] flex items-end">{desc}</div>

<a href={link}>
<h3 class="mt-0.5 text-lg font-medium text-gray-900">
Expand All @@ -28,6 +28,3 @@
</div>
</div>
</article>

<style>
</style>
214 changes: 214 additions & 0 deletions frontend/src/lib/components/DataViz/GraphExplorer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
<script lang="ts">
import { onMount } from 'svelte';
import type * as echarts from 'echarts';
export let data;
export let width = '';
export let height = 'h-full';
export let classesContainer = '';
export let title = '';
export let layout = 'force';
export let edgeLength = 50;
export let name = 'graph';
let searchQuery = '';
let chart: echarts.ECharts;
let currentEmphasisNodeId: number | null = null;
const chart_id = `${name}_div`;
let resizeTimeout: ReturnType<typeof setTimeout>;
const getChartOptions = () => ({
legend: [
{
data: data.categories.map(function (a) {
return a.name;
})
}
],
tooltip: {
label: {
position: 'right',
show: true
}
},
title: {
text: title,
subtext: 'Force layout',
top: '30',
left: 'right'
},
series: [
{
type: 'graph',
layout: layout,
symbolSize: 20,
animation: false,
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
edgeSymbol: ['circle', 'arrow'],
label: {
position: 'right',
formatter: '{b}',
show: false
},
draggable: true,
roam: true,
data: data.nodes.map(function (node, idx) {
node.id = idx;
return node;
}),
emphasis: {
focus: 'adjacency',
label: {
position: 'right',
show: true
}
},
selectedMode: 'series',
select: {
itemStyle: {
borderColor: '#666',
borderWidth: 2
},
label: {
show: true
}
},
categories: data.categories,
force: {
edgeLength: edgeLength,
repulsion: 50,
gravity: 0.1,
layoutAnimation: true,
friction: 0.1,
initLayout: 'circular'
},
labelLayout: {
hideOverlap: true
},
edges: data.links,
lineStyle: {
curveness: 0.2,
color: 'source'
}
}
]
});
const handleNodeEmphasis = (nodeId: number | null) => {
if (!chart) return;
if (currentEmphasisNodeId !== null) {
chart.dispatchAction({
type: 'downplay',
dataIndex: currentEmphasisNodeId
});
}
if (nodeId !== null && nodeId !== currentEmphasisNodeId) {
chart.dispatchAction({
type: 'highlight',
dataIndex: nodeId
});
}
currentEmphasisNodeId = nodeId !== currentEmphasisNodeId ? nodeId : null;
};
// Search function, maybe we can improve it later for fuzzy search?
const searchNode = (query: string) => {
if (!query.trim()) return;
const normalizedQuery = query.toLowerCase().trim();
const node = data.nodes.find((n) => n.name.toLowerCase().includes(normalizedQuery));
if (node && node.id !== undefined) {
handleNodeEmphasis(node.id);
chart?.dispatchAction({
type: 'focusNodeAdjacency',
dataIndex: node.id
});
} else {
alert('No matching nodes found');
}
};
onMount(async () => {
const echarts = await import('echarts');
const element = document.getElementById(chart_id);
if (!element) {
console.error(`Element with id ${chart_id} not found`);
return;
}
chart = echarts.init(element);
const options = getChartOptions();
chart.setOption(options);
chart.on('click', (params) => {
if (params.dataType === 'node') {
handleNodeEmphasis(params.data.id);
} else {
handleNodeEmphasis(null);
}
});
const handleResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
chart?.resize();
}, 250);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
clearTimeout(resizeTimeout);
chart?.dispose();
};
});
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
searchNode(searchQuery);
}
};
</script>

<div class="relative p-2">
<label for="graph-search" class="sr-only">Search</label>
<input
id="graph-search"
type="text"
class="w-full rounded-md border-gray-200 py-2.5 pe-10 shadow-sm"
bind:value={searchQuery}
on:keydown={handleKeyDown}
placeholder="Find a node ..."
/>
<span class="absolute inset-y-0 end-0 grid w-10 place-content-center">
<button
type="button"
class="text-gray-600 hover:text-gray-700"
on:click={() => searchNode(searchQuery)}
aria-label="Search"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
</button>
</span>
</div>
<div id={chart_id} class="{width} {height} {classesContainer} p-8" role="presentation" />
12 changes: 12 additions & 0 deletions frontend/src/routes/(app)/(internal)/assets/graph/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BASE_API_URL } from '$lib/utils/constants';

import type { PageServerLoad } from './$types';

export const load = (async ({ fetch }) => {
const endpoint = `${BASE_API_URL}/assets/graph/`;

const res = await fetch(endpoint);
const data = await res.json();

return { data };
}) satisfies PageServerLoad;
13 changes: 13 additions & 0 deletions frontend/src/routes/(app)/(internal)/assets/graph/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
import GraphExplorer from '$lib/components/DataViz/GraphExplorer.svelte';
import { pageTitle } from '$lib/utils/stores';
pageTitle.set('Assets Explorer');
</script>

<div class="bg-white shadow flex overflow-x-auto">
<div class="w-full h-screen">
<GraphExplorer title="Assets Explorer" data={data.data} edgeLength={100} />
</div>
</div>
10 changes: 8 additions & 2 deletions frontend/src/routes/(app)/(internal)/experimental/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import Article from '$lib/components/DataViz/Article.svelte';
</script>

<div class="bg-white grid p-8 grid-cols-3 space-x-8">
<div class="bg-white grid grid-cols-2 p-4 gap-4">
<Article
title="Controls Graph"
desc="Visualize dependencies and the impact of controls"
Expand All @@ -19,6 +19,12 @@
title="Frameworks mapping"
desc="Visualize applied mapping data as a graph"
link="experimental/mapping"
tags={['schedule', 'controls']}
tags={['analysis', 'mapping']}
/>
<Article
title="Assets explorer"
desc="Visualize assets and their relationships"
link="assets/graph"
tags={['analysis', 'assets']}
/>
</div>
Loading

0 comments on commit 126fdd5

Please sign in to comment.