Parcourir la source

[192-webreport] Implemented file upload, file upload page and started overview page implementation

Evgeni il y a 4 ans
Parent
commit
c7a4c2ddcf

Fichier diff supprimé car celui-ci est trop grand
+ 17599 - 1
report-viewer/package-lock.json


+ 3 - 0
report-viewer/package.json

@@ -8,8 +8,11 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "chart.js": "^3.6.0",
+    "chartjs-plugin-datalabels": "^2.0.0",
     "core-js": "^3.6.5",
     "vue": "^3.0.0",
+    "vue-chart-3": "^3.0.3",
     "vue-router": "^4.0.0-0"
   },
   "devDependencies": {

+ 5 - 14
report-viewer/src/App.vue

@@ -1,8 +1,5 @@
 <template>
-  <div id="nav">
-
-  </div>
-  <router-view />
+  <router-view/>
 </template>
 
 <style>
@@ -14,15 +11,9 @@
   color: #2c3e50;
 }
 
-#nav {
-
-}
-
-#nav a {
-
-}
-
-#nav a.router-link-exact-active {
-
+html, body, #app {
+  height: 100%;
+  width: 100%;
+  margin: 0;
 }
 </style>

+ 1 - 0
report-viewer/src/assets/double_arrow_black_18dp.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="18px" viewBox="0 0 20 20" width="18px" fill="#000000"><g><rect fill="none" height="20" width="20"/></g><g><polygon points="9.21,15 12.43,15 16,10 12.43,5 9.21,5 12.79,10"/><polygon points="4.21,15 7.43,15 11,10 7.43,5 4.21,5 7.79,10"/></g></svg>

+ 1 - 0
report-viewer/src/assets/double_arrow_white_18dp.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="18px" viewBox="0 0 20 20" width="18px" fill="#FFFFFF"><g><rect fill="none" height="20" width="20"/></g><g><g><path d="M15.79,9.71l-2.97-4.16C12.57,5.2,12.18,5,11.76,5c-1.06,0-1.68,1.2-1.06,2.07L12.79,10l-2.09,2.93 c-0.62,0.87,0,2.07,1.06,2.07c0.42,0,0.82-0.2,1.06-0.55l2.97-4.16C15.92,10.12,15.92,9.88,15.79,9.71z"/><path d="M10.79,9.71L7.82,5.55C7.57,5.2,7.18,5,6.76,5C5.69,5,5.07,6.2,5.69,7.07L7.79,10l-2.09,2.93C5.07,13.8,5.69,15,6.76,15 c0.42,0,0.82-0.2,1.06-0.55l2.97-4.16C10.92,10.12,10.92,9.88,10.79,9.71z"/></g></g></svg>

+ 1 - 0
report-viewer/src/assets/double_arrow_white_24dp.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><g><rect fill="none" height="24" width="24"/><rect fill="none" height="24" width="24"/></g><g><g><path d="M20.08,11.42l-4.04-5.65C15.7,5.29,15.15,5,14.56,5h0c-1.49,0-2.35,1.68-1.49,2.89L16,12l-2.93,4.11 c-0.87,1.21,0,2.89,1.49,2.89h0c0.59,0,1.15-0.29,1.49-0.77l4.04-5.65C20.33,12.23,20.33,11.77,20.08,11.42z"/><path d="M13.08,11.42L9.05,5.77C8.7,5.29,8.15,5,7.56,5h0C6.07,5,5.2,6.68,6.07,7.89L9,12l-2.93,4.11C5.2,17.32,6.07,19,7.56,19h0 c0.59,0,1.15-0.29,1.49-0.77l4.04-5.65C13.33,12.23,13.33,11.77,13.08,11.42z"/></g></g></svg>

+ 1 - 0
report-viewer/src/assets/help_outline_black_18dp.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="18px" viewBox="0 0 24 24" width="18px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>

+ 1 - 0
report-viewer/src/assets/help_outline_black_24dp.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>

BIN
report-viewer/src/assets/logo.png


+ 72 - 0
report-viewer/src/components/ComparisonListElement.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="wrapper-list-element">
+    <p class="index">{{ index }}</p>
+      <p class="submission-id">{{ submission1 }}</p>
+      <img src="@/assets/double_arrow_black_18dp.svg" alt=">>"/>
+      <p class="submission-id">{{ submission2 }}</p>
+    <p class="match-percentage">{{ matchPercentage }}%</p>
+  </div>
+</template>
+
+<script>
+import {defineComponent, ref } from "vue";
+
+export default defineComponent({
+  name: "ComparisonListElement",
+  props: {
+    index: {
+      type: Number,
+      required: true
+    },
+    submission1: {
+      type: String,
+      required: true
+    },
+    submission2: {
+      type: String,
+      required: true
+    },
+    matchPercentage: {
+      type: String,
+      required: true
+    }
+  },
+  setup() {
+
+    return {
+
+    }
+  }
+})
+</script>
+
+<style scoped>
+.wrapper-list-element {
+  background: white;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 1%;
+  margin-bottom: 1%;
+  box-shadow: #777777 0 3px 3px;
+  border-radius: 40px;
+  width: 25%;
+}
+
+.wrapper-list-element > * {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+img {
+  flex-shrink: 2;
+}
+
+p {
+  font-weight: bold;
+  font-size: small;
+  text-align: center;
+  text-anchor: middle;
+}
+</style>

+ 50 - 0
report-viewer/src/components/ComparisonsLists.vue

@@ -0,0 +1,50 @@
+<template>
+  <div class="wrapper">
+    <ul id="list">
+      <li v-for="(comparison, index) in comparisons" :key="index">
+        <ComparisonListElement :index="index + 1"
+                               :submission1="comparison.submission1"
+                               :submission2="comparison.submission2"
+                               :match-percentage="comparison.matchPercentage"/>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script>
+import { defineComponent, ref} from "vue";
+import TopComparison from "../model/TopComparison";
+import ComparisonListElement from "./ComparisonListElement";
+
+export default defineComponent({
+  name: "ComparisonsLists",
+  components: {ComparisonListElement},
+  props: {
+    comparisons: {
+      type: [TopComparison],
+      required: true
+    }
+  },
+})
+</script>
+
+<style scoped>
+.wrapper {
+  display: flex;
+  background: white;
+  border-radius: 10px;
+  align-items: center;
+  box-shadow: #777777 2px 3px 3px;
+  flex-wrap: wrap;
+}
+
+ul {
+  width: 100%;
+  list-style-type: none;
+  padding-right: 1%;
+  padding-left: 1%;
+  columns: 2;
+  -webkit-columns: 2;
+  -moz-columns: 2;
+}
+</style>

+ 88 - 0
report-viewer/src/components/DistributionDiagram.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="wrapper">
+    <BarChart class="chart" :chartData="chartData" :options="options"/>
+  </div>
+</template>
+
+<script>
+import {defineComponent, ref, reactive, watch, watchEffect} from "vue";
+import { BarChart } from "vue-chart-3"
+import { Chart, registerables } from 'chart.js';
+import ChartDataLabels from 'chartjs-plugin-datalabels';
+Chart.register(...registerables);
+Chart.register(ChartDataLabels);
+
+export default defineComponent({
+  name: "DistributionDiagram",
+  components: { BarChart },
+  props: {
+    distribution: {
+      type: Array,
+      required: true
+    }
+  },
+  setup(props) {
+    const distributionRef = ref(props.distribution)
+
+    let chartData = ref( {
+        labels: ['0-10%', '11-20%', '21-30%', '31-40%', '41-50%', '51-60%', '61-70%', '71-80%', '81-90%', '91-100%'],
+        datasets: [{
+          label: 'Count',
+          data: props.distribution,
+          backgroundColor: '#BA1616'
+        }]
+    })
+
+    watch( () => props.distribution, (val, oldVal) => {
+      chartData.value = {
+        labels: ['0-10%', '11-20%', '21-30%', '31-40%', '41-50%', '51-60%', '61-70%', '71-80%', '81-90%', '91-100%'],
+        datasets: [{
+          label: 'Count',
+          data: val,
+          backgroundColor: '#BA1616'
+        }]
+      }
+    })
+
+    const options = {
+      responsive: true,
+      maintainAspectRatio: false,
+      plugins : {
+        datalabels: {
+          color: '#000000',
+          font: {
+            weight: 'bold'
+          },
+          anchor: 'end',
+          align: 'end',
+          clamp: true
+        },
+        legend: {
+          display: false,
+        }
+      }
+    }
+
+    return {
+      chartData,
+      options
+    }
+  }
+
+})
+</script>
+
+<style scoped>
+.wrapper {
+  background: white;
+  border-radius: 10px;
+  box-shadow: #777777 2px 3px 3px;
+  display: flex;
+  padding: 2%;
+}
+
+.chart {
+  max-height: 200px;
+  width: 100%;
+}
+</style>

+ 86 - 0
report-viewer/src/components/MetricButton.vue

@@ -0,0 +1,86 @@
+<template>
+<div class="wrapper" v-bind:class="{ selected : isSelected }">
+  <div class="metric">
+    <p class="metric-name">{{ metricName }}</p>
+    <img src="@/assets/help_outline_black_18dp.svg" alt="?"/>
+  </div>
+  <div class="threshold">
+    <p>Threshold: </p>
+    <p>{{ metricThreshold }}%</p>
+  </div>
+</div>
+</template>
+
+<script>
+import {defineComponent} from "vue";
+
+export default defineComponent({
+  name: "MetricButton",
+  props: {
+    metricName: {
+      type: String,
+      required: true
+    },
+    metricThreshold: {
+      type: Number,
+      required: true
+    },
+    isSelected : {
+      type: Boolean,
+      default: false
+    },
+  },
+  setup() {
+
+    return {
+    }
+  }
+})
+</script>
+
+<style scoped>
+.wrapper {
+  display: flex;
+  flex-direction: column;
+  background: white;
+  border-radius: 5px;
+  box-shadow: #777777 2px 3px 3px;
+  padding: 5%;
+  width: 7vw;
+}
+
+
+.wrapper > div > * {
+  margin: 0;
+  padding: 0;
+}
+
+.wrapper > div {
+  display: flex;
+  justify-content: space-between;
+}
+
+.metric {
+  margin-bottom: 2%;
+}
+
+.metric-name {
+  font-size: xx-large;
+  font-weight: bold;
+  margin: 0;
+}
+
+.threshold {
+  font-size: small;
+  font-weight: bold;
+}
+
+.selected {
+  background: #FF5353;
+  box-shadow: #777777 0 0 0;
+}
+
+.selected p {
+  color: white;
+}
+</style>

+ 96 - 0
report-viewer/src/components/TextInformation.vue

@@ -0,0 +1,96 @@
+<template>
+  <div class="wrapper">
+    <div class="label">
+      <p class="label-text">{{ label }}</p>
+      <div class="additional-info" v-if="hasAdditionalInfo">
+        <p class="additional-label">{{ additionalInfoLabel }}</p>
+        <img alt="icon" src="@/assets/double_arrow_white_18dp.svg">
+      </div>
+    </div>
+    <p class="value-text">{{ value }}</p>
+    <hr>
+  </div>
+</template>
+
+<script>
+import {defineComponent, ref} from "vue";
+
+export default defineComponent({
+  name: "TextInformation",
+  props: {
+    label: {
+      type: String,
+      required: true
+    },
+    value: {
+      required: true
+    },
+    hasAdditionalInfo: {
+      type: Boolean,
+      required: true
+    },
+    additionalInfoLabel : String,
+    additionalInfo : [String]
+  },
+  setup() {
+
+    return {
+
+    }
+  }
+
+})
+</script>
+
+<style scoped>
+p {
+  margin: 0;
+  align-items: flex-start;
+  font-size: larger;
+}
+
+hr {
+  border: 0;
+  height: 2px;
+  background: linear-gradient(to right, #ea4848, transparent, transparent);
+  width: 100%;
+  box-shadow: #ea4864 0 1px;
+}
+
+.wrapper {
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+}
+
+.label {
+  display: flex;
+  justify-content: space-between;
+}
+
+.label-text {
+  font-weight: lighter;
+  text-align: left;
+  color: #ECECEC;
+}
+
+.value-text {
+  font-weight: bold;
+  text-align: left;
+  color: white;
+}
+
+.additional-info {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.additional-label {
+  font-weight: lighter;
+  font-size: smaller;
+  white-space: nowrap;
+  color: #ECECEC;
+  margin: 4% 0 0 0;
+}
+
+</style>

+ 17 - 0
report-viewer/src/model/TopComparison.ts

@@ -0,0 +1,17 @@
+class TopComparison {
+    comparisonName: string;
+    submission1: string;
+    submission2: string
+    matchPercentage: string;
+
+    constructor(comparisonName: string, matchPercentage: number) {
+        this.comparisonName = comparisonName
+        const split: string[]  = comparisonName.split("-")
+        this.submission1 = split[0]
+        this.submission2 = split[1]
+        this.matchPercentage = matchPercentage.toFixed(2)
+    }
+
+}
+
+export default TopComparison;

+ 15 - 1
report-viewer/src/router/index.ts

@@ -1,6 +1,20 @@
 import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
+import Overview from "@/views/Overview.vue";
+import FileUpload from "@/views/FileUpload.vue"
 
-const routes: Array<RouteRecordRaw> = [];
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: "/",
+    name: "FileUpload",
+    component: FileUpload
+  },
+  {
+    path: "/overview",
+    name: "Overview",
+    component: Overview,
+    props: route => ({ jsonString : route.params.str })
+  },
+];
 
 const router = createRouter({
   history: createWebHashHistory(),

+ 87 - 0
report-viewer/src/views/FileUpload.vue

@@ -0,0 +1,87 @@
+<template>
+<div class="container" @dragover.prevent @drop.prevent>
+  <img src="@/assets/logo.png" alt="JPlag"/>
+  <h1>JPlag Report Viewer</h1>
+  <h2>Select an overview or comparison file to display.</h2>
+  <div class="drop-container" @drop="uploadFile">
+    <p> Drop a .json file here</p>
+  </div>
+  <label for="browse">Or browse for a .json file</label>
+  <input type="file" id="browse" name="browse">
+</div>
+</template>
+
+<script>
+import { defineComponent } from "vue";
+import router from "@/router";
+
+export default defineComponent({
+  name: "FileUpload",
+  setup() {
+
+    const navigateToOverview = (file) => {
+      router.push({
+          name: "Overview",
+          params: {str: file},
+      }
+      )
+    }
+
+    const uploadFile = (e) => {
+      let dropped = e.dataTransfer.files
+      if (!dropped) return
+      let files = [...dropped]
+      if (files.length > 1 || files.length === 0) return
+      let read = new FileReader()
+      read.onload = (e) => {
+        navigateToOverview(e.target.result)
+      }
+      read.readAsText(files[0])
+    }
+    return {
+      uploadFile
+    }
+  }
+})
+</script>
+
+<style scoped>
+.container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(to right, #FF5353,#ECECEC, #ECECEC);
+}
+
+.drop-container {
+  background: lightgrey;
+  border: dashed dodgerblue;
+  align-items: center;
+  justify-content: space-around;
+  border-radius: 10px;
+  padding: 2%;
+}
+
+.drop-container > p {
+  color: dodgerblue;
+  text-align: center;
+}
+
+input {
+  display: none;
+}
+
+label {
+  font-weight: bold;
+  font-size: larger;
+  background: #ECECEC;
+  border-radius: 10px;
+  box-shadow: #777777 2px 3px 3px;
+  padding: 2%;
+  margin-top: 1%;
+}
+
+</style>

+ 164 - 0
report-viewer/src/views/Overview.vue

@@ -0,0 +1,164 @@
+<template>
+  <div class="container">
+    <div id="leftPanel">
+      <img id="logo" src="@/assets/logo.png" alt="JPlag">
+      <TextInformation label="Directory path: " :value="json.submission_folder_path" :has-additional-info="false"/>
+      <TextInformation label="Base code path: " :value="json.base_code_folder_path" :has-additional-info="false"/>
+      <TextInformation label="Language: " :value="json.language" :has-additional-info="true" additional-info-label="see extensions"/>
+      <TextInformation label="Submissions: " :value="json.submission_ids.length" :has-additional-info="true" additional-info-label="see ids"/>
+      <TextInformation label="Failed to parse: " value="No info yet" :has-additional-info="true" additional-info-label="see ids"/>
+      <TextInformation label="Date of execution: " :value="json.date_of_execution" :has-additional-info="false"/>
+      <TextInformation label="Duration (in ms): " :value="json.execution_time" :has-additional-info="false"/>
+    </div>
+    <div id="rightPanel">
+      <div id="metricsButtons" class="section">
+        <p class="section-title">Metrics: </p>
+        <ul class="metrics-list" v-for="(metric, i) in json.metrics" :key="metric.name">
+          <li><MetricButton :id="metric.name"
+                            :metric-threshold="metric.threshold"
+                            :metric-name="metric.name"
+                            :is-selected="selectedMetric[i]"
+                            @click="selectMetric(i)"/></li>
+        </ul>
+      </div>
+      <div id="distribution" class="section">
+        <p class="section-title ">Distribution:</p>
+        <DistributionDiagram :distribution="distributions[selectedMetricIndex]"/>
+      </div>
+      <div id="topComparisonsList" class="section">
+        <p class="section-title">Top comparisons:</p>
+        <p class="section-subtitle">(Top 25)</p>
+
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {defineComponent, ref, watchEffect} from "vue";
+import TextInformation from "@/components/TextInformation";
+import MetricButton from "@/components/MetricButton";
+import DistributionDiagram from "@/components/DistributionDiagram";
+import ComparisonsLists from "@/components/ComparisonsLists";
+
+export default defineComponent({
+  name: "Overview",
+  components: { DistributionDiagram, MetricButton, TextInformation},
+  props: {
+    jsonString: {
+      type: String,
+
+    }
+  },
+  setup(props) {
+    const json = JSON.parse(props.jsonString)
+    let selectedMetric = ref(json.metrics.map( () => false ))
+    let distributions = ref(json.metrics.map( (m) =>  m.distribution ))
+    let selectedMetricIndex = ref(0)
+    selectedMetric.value[0] = true;
+
+    const selectMetric = (metric) => {
+      selectedMetric.value = selectedMetric.value.map( () => { return false })
+      selectedMetric.value[metric] = true
+      selectedMetricIndex.value = metric
+    }
+
+    watchEffect( () => {
+      console.log("Overview - selected dist " + JSON.stringify(distributions.value[selectedMetricIndex.value]))
+    })
+
+    return {
+      json, selectedMetric,selectedMetricIndex, distributions, selectMetric
+    }
+  }
+})
+</script>
+
+<style scoped>
+.section {
+  display: flex;
+  flex-direction: column;
+  margin: 0;
+  padding: 0;
+}
+
+.container {
+  display: flex;
+  align-items: stretch;
+  width: 100%;
+  height: 100%;
+  margin: 0;
+}
+
+.section-title {
+  font-size: x-large;
+  font-weight: bold;
+  text-align: start;
+  margin-top: 3%;
+  margin-bottom: 1%;
+}
+
+.section-subtitle {
+  font-size: small;
+  text-align: start;
+  margin-top: 0;
+}
+
+
+.metrics-list {
+  list-style: none;
+  display: flex;
+  padding: 0 1% 0 0;
+  margin-bottom: 0;
+
+}
+
+#leftPanel {
+  width: 30%;
+  background: #FF5353;
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  padding: 1%;
+}
+
+#rightPanel {
+  width: 70%;
+  background: #ECECEC;
+  display: flex;
+  flex-direction: column;
+  flex-wrap: nowrap;
+  padding: 1%;
+}
+
+#metricsButtons {
+  flex-direction: row;
+  flex-shrink: 3;
+  align-items: flex-start;
+}
+
+#metricsButtons > * {
+  margin-right: 1%;
+}
+
+#distribution {
+  flex-shrink: 2;
+  align-items: stretch;
+}
+
+#topComparisonsList {
+  min-height: 0;
+  align-items: stretch;
+}
+
+
+#logo {
+  width: 40%;
+  height: 20%;
+  margin-bottom: 5%;
+}
+
+
+
+
+</style>

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff