Skip to content

Commit

Permalink
[FEATURE] Afficher de manière graphique les parcours Pix Junior
Browse files Browse the repository at this point in the history
  • Loading branch information
pix-service-auto-merge authored Nov 25, 2024
2 parents 6bb90a3 + 146b1d7 commit c0994b5
Show file tree
Hide file tree
Showing 8 changed files with 473 additions and 1 deletion.
16 changes: 16 additions & 0 deletions admin/app/controllers/authenticated/tools.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';

import { createMermaidFlowchartLink } from '../../utils/create-tree-data';

export default class ToolsController extends Controller {
@service pixToast;
@service store;
@service currentUser;
@tracked juniorMermaidFlowchart;

@action
async archiveCampaigns(files) {
Expand All @@ -29,4 +33,16 @@ export default class ToolsController extends Controller {
}
}
}

@action
displayTree([file]) {
const reader = new FileReader();
reader.addEventListener('load', (event) => this._onFileLoad(event));
reader.readAsText(file);
}

_onFileLoad(event) {
const data = JSON.parse(event.target.result);
this.juniorMermaidFlowchart = createMermaidFlowchartLink(data);
}
}
4 changes: 4 additions & 0 deletions admin/app/styles/authenticated/tools.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
margin-bottom: $pix-spacing-s;
}

&__info {
margin-bottom: $pix-spacing-s;
}

.pix-button {
display: inline-flex;
}
Expand Down
34 changes: 34 additions & 0 deletions admin/app/templates/authenticated/tools.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,39 @@
Envoyer le fichier des campagnes à archiver
</PixButtonUpload>
</section>
<section class="page-section">
<header class="page-section__header">
<h2 class="page-section__title">Afficher un arbre de passage de missions Pix Junior</h2>
</header>
<PixMessage class="tools__warning" @type="warning" @withIcon={{true}}>
Cette fonctionnalité est un POC destiné à l'analyse des résultats de passage Pix Junior.<br />
Elle permet de visualiser les résultats d'une question Metabase sous la forme d'un arbre sur le site
<a href="https://mermaid.live/">https://mermaid.live/</a>
</PixMessage>
<PixMessage class="tools__info" @type="info" @withIcon={{true}}>
<strong>Mode d'emploi</strong>
<ol>
<li>1. Exécuter la question Metabase
<a
href="https://metabase.pix.fr/question/14059-chemins-des-eleves"
target="_blank"
rel="noreferrer noopener"
>Chemin des élèves</a>
avec les paramètres souhaités.
</li>
<li>2. Télécharger les résultats au format json.</li>
<li>3. Envoyer le fichier json grace au bouton ci-dessous.</li>
<li>4. À l'aide du lien généré, accéder à la représentation graphique de l'arbre de résultats.</li>
</ol>
</PixMessage>
<PixButtonUpload @id="json-file-upload" @onChange={{this.displayTree}} accept=".json">
Envoyer le fichier des chemins Pix Junior
</PixButtonUpload>
{{#if this.juniorMermaidFlowchart}}
<a href={{this.juniorMermaidFlowchart}} target="_blank" rel="noreferrer noopener">
Lien vers l'arbre de passage de missions.
</a>
{{/if}}
</section>
</main>
</div>
168 changes: 168 additions & 0 deletions admin/app/utils/create-tree-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
const RESULT_STATUSES = ['FAILED', 'STARTED', 'SUCCEEDED', 'SKIPPED'];

const COLORS = {
FAILED: '#f1c4c4',
SUCCEEDED: '#b9d8cd',
SKIPPED: '#ffe5c0',
STARTED: '#f4f5f7',
};

const NODE_ORDER = [
'STARTED',
'SKIPPED',
'FAILED',
'1-TUTORIAL',
'1-TRAINING',
'1-VALIDATION',
'2-TUTORIAL',
'2-TRAINING',
'2-VALIDATION',
'SUCCEEDED',
'-CHALLENGE',
];

import { fromUint8Array } from 'js-base64';
import { deflate } from 'pako';

const formatJSON = (data) => JSON.stringify(data, undefined, 2);
const serialize = (state) => {
const data = new TextEncoder().encode(state);
const compressed = deflate(data, { level: 9 }); // zlib level 9
return fromUint8Array(compressed, true); // url safe base64 encoding
};

function createTreeFromData(data) {
const relations = data.flatMap((pathWithNumber) =>
createRelationsFromPath(pathWithNumber.fullPath).map((path) => {
return { ...path, number: pathWithNumber.nombre };
}),
);
const uniqueRelations = [];

relations.forEach((relation) => {
const uniqueRelation = uniqueRelations.filter((uniqueRelation) => uniqueRelation.to === relation.to)?.[0];
if (uniqueRelation) {
uniqueRelation.number = new String(Number(uniqueRelation.number) + Number(relation.number));
} else {
uniqueRelations.push(relation);
}
});

const nodes = [...new Set(uniqueRelations.flatMap((relation) => [relation.from, relation.to]))].map((nodeId) => {
return { id: nodeId };
});
return { relations: uniqueRelations, nodes };
}

function createRelationsFromPath(path) {
const nodeMatcher = new RegExp('(?<node1>.*)\\s\\>\\s(?<node2>.*)');
let matched = nodeMatcher.exec(path);
const relations = [];
while (matched) {
relations.push({ from: matched.groups.node1, to: `${matched.groups.node1} > ${matched.groups.node2}` });
matched = nodeMatcher.exec(matched.groups.node1);
}
return relations;
}

function sortTree(tree) {
return {
relations: tree.relations.sort((relation1, relation2) => {
return lastWordValue(relation2.to) - lastWordValue(relation1.to);
}),
nodes: tree.nodes.sort((node1, node2) => {
return lastWordValue(node2.id) - lastWordValue(node1.id);
}),
};
}

function lastWordValue(path) {
return NODE_ORDER.findIndex((nodeLabel) => nodeLabel === lastWord(path));
}

function lastWord(path) {
return path.split(' ').slice(-1)[0];
}

function minifyTree(tree) {
const nodesMinifiedNamesByPath = new Map(Array.from(tree.nodes, (node, i) => [node.id, i]));
const nodeLabelsById = new Map(
Array.from(nodesMinifiedNamesByPath.entries()).map(([path, minifiedName]) => [minifiedName, lastWord(path)]),
);
return {
nodeLabelsById,
relations: tree.relations.map((relation) => {
return {
from: nodesMinifiedNamesByPath.get(relation.from),
to: nodesMinifiedNamesByPath.get(relation.to),
number: relation.number,
};
}),
};
}

function applyBoxStyle([id, label]) {
const nodeColor = COLORS[label];
if (nodeColor) {
return `style ${id} fill:${nodeColor}`;
}
const stepNumber = label.charAt(0);
if (stepNumber === '0') {
return `style ${id} stroke:#ff3f94,stroke-width:4px`;
} else if (stepNumber === '1') {
return `style ${id} stroke:#3d68ff,stroke-width:4px`;
} else if (stepNumber === '-') {
return `style ${id} stroke:#ac008d,stroke-width:4px`;
}
return `style ${id} stroke:#52d987,stroke-width:4px`;
}

function applyMermaidStyle(tree) {
return Array.from(tree.nodeLabelsById.entries(), applyBoxStyle).join('\n');
}

function createMermaidFlowchart(tree) {
return (
'flowchart LR\n' +
tree.relations
.map((relation) => {
const fromLabel = tree.nodeLabelsById.get(relation.from);
const toLabel = tree.nodeLabelsById.get(relation.to);
const finalNode = RESULT_STATUSES.includes(toLabel) ? `(${toLabel})` : `[${toLabel}]`;
return `${relation.from}[${fromLabel}] -->|${relation.number}| ${relation.to}${finalNode}`;
})
.join('\n')
);
}

function createMermaidFlowchartLink(data) {
const rawTree = createTreeFromData(data);
const sortedTree = sortTree(rawTree);
const minifiedTree = minifyTree(sortedTree);

const flowChart = createMermaidFlowchart(minifiedTree);
const flowChartStyle = applyMermaidStyle(minifiedTree);

const defaultState = {
code: flowChart + '\n' + flowChartStyle,
mermaid: formatJSON({
theme: 'default',
}),
autoSync: true,
updateDiagram: true,
};

const json = JSON.stringify(defaultState);
const serialized = serialize(json);
return `https://mermaid.live/edit#pako:${serialized}`;
}

export {
applyMermaidStyle,
createMermaidFlowchart,
createMermaidFlowchartLink,
createRelationsFromPath,
createTreeFromData,
minifyTree,
sortTree,
};
15 changes: 15 additions & 0 deletions admin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,12 @@
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-qunit": "^8.0.0",
"joi": "^17.12.2",
"js-base64": "^3.7.7",
"jwt-decode": "^4.0.0",
"loader.js": "^4.7.0",
"lodash": "^4.17.21",
"npm-run-all2": "^6.0.0",
"pako": "^2.1.0",
"p-queue": "^8.0.0",
"prettier": "^3.3.2",
"prettier-plugin-ember-template-tag": "^2.0.2",
Expand Down
Loading

0 comments on commit c0994b5

Please sign in to comment.