فهرست منبع

Merge remote-tracking branch 'origin/master' into feature/cleanup-pom

Dominik Fuchß 4 سال پیش
والد
کامیت
b37182999d
36فایلهای تغییر یافته به همراه1593 افزوده شده و 1099 حذف شده
  1. 1 1
      .github/workflows/report-viewer.yml
  2. 75 20
      jplag.frontend-utils/src/main/java/de/jplag/TokenPrinter.java
  3. 0 0
      jplag.frontend.rlang/src/main/antlr4/de/jplag/rlang/grammar/R.g4
  4. 0 0
      jplag.frontend.rlang/src/main/antlr4/de/jplag/rlang/grammar/RFilter.g4
  5. 2 2
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/JplagRListener.java
  6. 1 1
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/Language.java
  7. 2 2
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/RCombinedBaseListener.java
  8. 4 4
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/RParserAdapter.java
  9. 1 1
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/RToken.java
  10. 1 1
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/RTokenConstants.java
  11. 2 2
      jplag.frontend.rlang/src/test/java/de/jplag/rlang/RFrontendTest.java
  12. 0 0
      jplag.frontend.rlang/src/test/resources/de/jplag/rlang/Complete.R
  13. 0 0
      jplag.frontend.rlang/src/test/resources/de/jplag/rlang/Game.R
  14. 1 1
      jplag/src/main/java/de/jplag/ErrorCollector.java
  15. 9 0
      jplag/src/test/java/de/jplag/special/TokenPrinterTest.java
  16. 2 2
      pom.xml
  17. 284 268
      report-viewer/package-lock.json
  18. 18 18
      report-viewer/package.json
  19. 4 4
      report-viewer/src/App.vue
  20. 22 0
      report-viewer/src/assets/radar-chart-configuration.ts
  21. 89 64
      report-viewer/src/components/ClusterRadarChart.vue
  22. 54 20
      report-viewer/src/components/ClustersList.vue
  23. 70 47
      report-viewer/src/components/CodePanel.vue
  24. 151 76
      report-viewer/src/components/ComparisonsTable.vue
  25. 33 31
      report-viewer/src/components/FilesContainer.vue
  26. 1 1
      report-viewer/src/components/LineOfCode.vue
  27. 1 1
      report-viewer/src/components/TextInformation.vue
  28. 10 0
      report-viewer/src/model/ClusterListElement.ts
  29. 95 91
      report-viewer/src/model/Comparison.ts
  30. 66 57
      report-viewer/src/model/Overview.ts
  31. 108 93
      report-viewer/src/model/factories/ComparisonFactory.ts
  32. 22 25
      report-viewer/src/router/index.ts
  33. 93 59
      report-viewer/src/store/store.ts
  34. 147 70
      report-viewer/src/views/ComparisonView.vue
  35. 89 64
      report-viewer/src/views/FileUploadView.vue
  36. 135 73
      report-viewer/src/views/OverviewView.vue

+ 1 - 1
.github/workflows/report-viewer.yml

@@ -24,7 +24,7 @@ jobs:
           npm run build
 
       - name: Deploy 🚀
-        uses: JamesIves/github-pages-deploy-action@v4.3.3
+        uses: JamesIves/github-pages-deploy-action@v4.3.4
         with:
           branch: gh-pages
           folder: report-viewer/dist

+ 75 - 20
jplag.frontend-utils/src/main/java/de/jplag/TokenPrinter.java

@@ -28,7 +28,10 @@ public final class TokenPrinter {
     private static final String BAR = "|";
     private static final String TAB = "\t";
     private static final String SPACE = " ";
-    private static final String TAB_REPLACEMENT = "        "; // might depend on files
+    public static final String NON_WHITESPACE = "\\S";
+    public static final int MIN_PADDING = 1;
+    private static final int TAB_LENGTH = 8;
+    private static final String TAB_REPLACEMENT = SPACE.repeat(TAB_LENGTH); // might depend on files
 
     // Configuration:
     private static final boolean INDICATE_TINY_TOKEN = true; // print token with length <= 1 in lowercase
@@ -62,6 +65,7 @@ public final class TokenPrinter {
         int lineIndex = 0;
         int columnIndex = 1;
         String file = null; // no file yet
+        String currentLine = null;
 
         for (Token token : tokens.allTokens()) {
             // New code file:
@@ -80,24 +84,22 @@ public final class TokenPrinter {
                 columnIndex = 1;
 
                 String fileName = token.getFile().isEmpty() ? root.getName() : token.getFile();
-                String currentLine = filesToLines.get(fileName).get(lineIndex - 1);
-                if (REPLACE_TABS) {
-                    currentLine = currentLine.replace(TAB, TAB_REPLACEMENT);
-                }
-                appendCodeLinePrefix(builder, lineIndex).append(currentLine);
-                appendTokenLinePrefix(builder);
+                currentLine = filesToLines.get(fileName).get(lineIndex - 1);
+
+                appendCodeLinePrefix(builder, lineIndex);
+                appendCodeLine(builder, currentLine);
+                appendTokenLinePrefix(builder, lineIndex);
             }
 
+            assert currentLine != null;
+
             // New line if already past token index:
             if (columnIndex > token.getColumn()) {
-                appendTokenLinePrefix(builder);
+                appendTokenLinePrefix(builder, lineIndex);
                 columnIndex = 1;
             }
             // Move to token index:
-            while (columnIndex < token.getColumn()) {
-                builder.append(SPACE);
-                columnIndex++;
-            }
+            columnIndex = appendPadding(builder, currentLine, columnIndex, token.getColumn());
 
             // Print the actual token:
             String stringRepresentation = getStringRepresentation(token);
@@ -106,10 +108,8 @@ public final class TokenPrinter {
 
             // Move up to token end:
             int tokenEndIndex = token.getColumn() + token.getLength() - 1;
-            while (columnIndex < tokenEndIndex) {
-                builder.append(SPACE);
-                columnIndex++;
-            }
+            columnIndex = appendPadding(builder, currentLine, columnIndex, tokenEndIndex);
+
             // Print token end if not already past it:
             if (columnIndex == tokenEndIndex) {
                 builder.append(BAR);
@@ -119,12 +119,55 @@ public final class TokenPrinter {
         return builder.toString();
     }
 
+    /**
+     * Appends the code line to the StringBuilder, applying the configuration of this TokenPrinter.
+     * @param builder The StringBuilder to add code to
+     * @param currentLine The line of code to append
+     * @return a reference to that same StringBuilder
+     */
+    private static StringBuilder appendCodeLine(StringBuilder builder, String currentLine) {
+        if (REPLACE_TABS) {
+            currentLine = currentLine.replaceAll(TAB, TAB_REPLACEMENT);
+        }
+        return builder.append(currentLine);
+    }
+
+    /**
+     * Appends whitespace padding to the given StringBuilder in order to reach the targetPosition. Note that <b>the indices
+     * are 1-based</b>, whereas the positions in currentLine are 0-based. The convention that lines start at position 1
+     * comes from the frontends, specifically the Java frontend.
+     * @param builder The StringBuilder to add padding to
+     * @param currentLine The current line in the code file, indicating where it containes tab characters
+     * @param currentPosition The (1-based) index of the last character within the currentLine that is labeled, or 1.
+     * @param targetPosition The (1-based) index of the next character within the currentLine that should be labeled
+     * @return the new position, which is the targetPosition or the end of the line.
+     */
+    private static int appendPadding(StringBuilder builder, String currentLine, int currentPosition, int targetPosition) {
+        targetPosition = Math.min(targetPosition, currentLine.length());
+        if (currentPosition >= targetPosition) {
+            return currentPosition;
+        }
+
+        // The replacement operation preserves TAB characters, which is essential for correct alignment
+        String padding = currentLine.substring(currentPosition - 1, targetPosition - 1).replaceAll(TAB, REPLACE_TABS ? TAB_REPLACEMENT : TAB)
+                .replaceAll(NON_WHITESPACE, SPACE);
+        builder.append(padding);
+
+        return targetPosition;
+    }
+
     private static StringBuilder appendCodeLinePrefix(StringBuilder builder, int index) {
-        return builder.append(System.lineSeparator()).append(index + TAB);
+        builder.append(System.lineSeparator()).append(index);
+
+        // Code line should be padded up to at least one tab length, and no less than MIN_PADDING
+        int paddingLength = Math.max(TAB_LENGTH - digitCount(index), MIN_PADDING);
+        return builder.append(SPACE.repeat(paddingLength));
     }
 
-    private static StringBuilder appendTokenLinePrefix(StringBuilder builder) {
-        return builder.append(System.lineSeparator()).append(TAB);
+    private static StringBuilder appendTokenLinePrefix(StringBuilder builder, int index) {
+        int paddingLength = Math.max(digitCount(index) + MIN_PADDING, TAB_LENGTH);
+        String padding = SPACE.repeat(paddingLength);
+        return builder.append(System.lineSeparator()).append(padding);
     }
 
     /**
@@ -138,7 +181,7 @@ public final class TokenPrinter {
      * Turns a list of files into a map which maps the file names to a list of the contained lines of that file.
      */
     private static Map<String, List<String>> readFiles(Collection<File> allFiles) {
-        return allFiles.stream().collect(toMap(it -> it.getName(), it -> linesFromFile(it)));
+        return allFiles.stream().collect(toMap(File::getName, TokenPrinter::linesFromFile));
     }
 
     /**
@@ -152,4 +195,16 @@ public final class TokenPrinter {
         }
         return Collections.emptyList();
     }
+
+    /**
+     * Returns the number of digits (including a minus) of the given number.
+     */
+    private static int digitCount(int number) {
+        if (number == 0) {
+            return 1;
+        }
+        int minusLength = number < 0 ? 1 : 0;
+        // The 'log10' variant is supposedly faster than the 'toString' variant.
+        return (int) Math.log10(Math.abs(number)) + minusLength + 1;
+    }
 }

+ 0 - 0
jplag.frontend.rlang/src/main/antlr4/de/jplag/R/grammar/R.g4 → jplag.frontend.rlang/src/main/antlr4/de/jplag/rlang/grammar/R.g4


+ 0 - 0
jplag.frontend.rlang/src/main/antlr4/de/jplag/R/grammar/RFilter.g4 → jplag.frontend.rlang/src/main/antlr4/de/jplag/rlang/grammar/RFilter.g4


+ 2 - 2
jplag.frontend.rlang/src/main/java/de/jplag/R/JplagRListener.java → jplag.frontend.rlang/src/main/java/de/jplag/rlang/JplagRListener.java

@@ -1,8 +1,8 @@
-package de.jplag.R;
+package de.jplag.rlang;
 
 import org.antlr.v4.runtime.Token;
 
-import de.jplag.R.grammar.*;
+import de.jplag.rlang.grammar.*;
 
 /**
  * Listener class for visiting the R ANTLR parse tree. Transforms the visited ANTLR token into JPlag tokens. Based on an

+ 1 - 1
jplag.frontend.rlang/src/main/java/de/jplag/R/Language.java → jplag.frontend.rlang/src/main/java/de/jplag/rlang/Language.java

@@ -1,4 +1,4 @@
-package de.jplag.R;
+package de.jplag.rlang;
 
 import java.io.File;
 

+ 2 - 2
jplag.frontend.rlang/src/main/java/de/jplag/R/RCombinedBaseListener.java → jplag.frontend.rlang/src/main/java/de/jplag/rlang/RCombinedBaseListener.java

@@ -1,10 +1,10 @@
-package de.jplag.R;
+package de.jplag.rlang;
 
 import org.antlr.v4.runtime.ParserRuleContext;
 import org.antlr.v4.runtime.tree.ErrorNode;
 import org.antlr.v4.runtime.tree.TerminalNode;
 
-import de.jplag.R.grammar.*;
+import de.jplag.rlang.grammar.*;
 
 /**
  * Empty base implementation for {@link RListener} and {@link RFilterListener}.

+ 4 - 4
jplag.frontend.rlang/src/main/java/de/jplag/R/RParserAdapter.java → jplag.frontend.rlang/src/main/java/de/jplag/rlang/RParserAdapter.java

@@ -1,4 +1,4 @@
-package de.jplag.R;
+package de.jplag.rlang;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -12,10 +12,10 @@ import org.antlr.v4.runtime.tree.ParseTreeWalker;
 
 import de.jplag.AbstractParser;
 import de.jplag.ErrorConsumer;
-import de.jplag.R.grammar.RFilter;
-import de.jplag.R.grammar.RLexer;
-import de.jplag.R.grammar.RParser;
 import de.jplag.TokenList;
+import de.jplag.rlang.grammar.RFilter;
+import de.jplag.rlang.grammar.RLexer;
+import de.jplag.rlang.grammar.RParser;
 
 /**
  * This class sets up the lexer and parser generated by ANTLR4, feeds the submissions through them and passes the

+ 1 - 1
jplag.frontend.rlang/src/main/java/de/jplag/R/RToken.java → jplag.frontend.rlang/src/main/java/de/jplag/rlang/RToken.java

@@ -1,4 +1,4 @@
-package de.jplag.R;
+package de.jplag.rlang;
 
 /**
  * This class represents the occurrence of an R Token in the source code. Based on an R frontend for JPlag v2.15 by Olmo

+ 1 - 1
jplag.frontend.rlang/src/main/java/de/jplag/R/RTokenConstants.java → jplag.frontend.rlang/src/main/java/de/jplag/rlang/RTokenConstants.java

@@ -1,4 +1,4 @@
-package de.jplag.R;
+package de.jplag.rlang;
 
 import de.jplag.TokenConstants;
 

+ 2 - 2
jplag.frontend.rlang/src/test/java/de/jplag/R/RFrontendTest.java → jplag.frontend.rlang/src/test/java/de/jplag/rlang/RFrontendTest.java

@@ -1,4 +1,4 @@
-package de.jplag.R;
+package de.jplag.rlang;
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.fail;
@@ -36,7 +36,7 @@ public class RFrontendTest {
 
     private final Logger logger = LoggerFactory.getLogger("R frontend test");
     private final String[] testFiles = new String[] {"Game.R", COMPLETE_TEST_FILE};
-    private final File testFileLocation = Path.of("src", "test", "resources", "de", "jplag", "R").toFile();
+    private final File testFileLocation = Path.of("src", "test", "resources", "de", "jplag", "rlang").toFile();
     private Language language;
 
     @BeforeEach

+ 0 - 0
jplag.frontend.rlang/src/test/resources/de/jplag/R/Complete.R → jplag.frontend.rlang/src/test/resources/de/jplag/rlang/Complete.R


+ 0 - 0
jplag.frontend.rlang/src/test/resources/de/jplag/R/Game.R → jplag.frontend.rlang/src/test/resources/de/jplag/rlang/Game.R


+ 1 - 1
jplag/src/main/java/de/jplag/ErrorCollector.java

@@ -44,7 +44,7 @@ public class ErrorCollector implements ErrorConsumer {
             logger.info(message);
         }
         if (longMessage != null && verbosity == LONG) {
-            logger.info(message);
+            logger.info(longMessage);
         }
     }
 

+ 9 - 0
jplag/src/test/java/de/jplag/special/TokenPrinterTest.java

@@ -43,6 +43,15 @@ public class TokenPrinterTest extends TestBase {
         });
     }
 
+    @Disabled
+    @Test
+    public void printRLangFiles() {
+        printSubmissions(options -> {
+            options.setLanguageOption(LanguageOption.R_LANG);
+            options.setMinimumTokenMatch(MIN_TOKEN_MATCH); // for printing also allow small files
+        });
+    }
+
     private void printSubmissions(Consumer<JPlagOptions> optionsCustomization) {
         try {
             JPlagResult result = runJPlag(PRINTER_FOLDER, optionsCustomization);

+ 2 - 2
pom.xml

@@ -67,7 +67,7 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <maven.compiler.source>17</maven.compiler.source>
         <maven.compiler.target>17</maven.compiler.target>
-        <spotless.version>2.22.8</spotless.version>
+        <spotless.version>2.23.0</spotless.version>
         <slf4j.version>1.7.36</slf4j.version>
 
         <antlr2.version>2.7.7</antlr2.version>
@@ -255,7 +255,7 @@
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-assembly-plugin</artifactId>
-                    <version>3.3.0</version>
+                    <version>3.4.0</version>
                     <configuration>
                         <descriptorRefs>
                             <descriptorRef>jar-with-dependencies</descriptorRef>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 284 - 268
report-viewer/package-lock.json


+ 18 - 18
report-viewer/package.json

@@ -11,34 +11,34 @@
     "@highlightjs/vue-plugin": "^2.1.0",
     "chart.js": "^3.8.0",
     "chartjs-plugin-datalabels": "^2.0.0",
-    "core-js": "^3.22.7",
-    "gitart-vue-dialog": "^2.0.5",
-    "highlight.js": "^11.5.1",
+    "core-js": "^3.23.4",
+    "gitart-vue-dialog": "^2.4.0",
+    "highlight.js": "^11.6.0",
     "jszip": "^3.10.0",
-    "vue": "^3.2.36",
+    "vue": "^3.2.37",
     "vue-chart-3": "^3.1.8",
     "vue-draggable-next": "^2.1.1",
-    "vue-router": "^4.0.15",
+    "vue-router": "^4.1.2",
     "vue3-highlightjs": "^1.0.5",
     "vuex": "^4.0.2"
   },
   "devDependencies": {
-    "@typescript-eslint/eslint-plugin": "^5.27.0",
-    "@typescript-eslint/parser": "^5.27.0",
-    "@vue/cli-plugin-babel": "~5.0.4",
-    "@vue/cli-plugin-eslint": "~5.0.4",
-    "@vue/cli-plugin-router": "~5.0.1",
-    "@vue/cli-plugin-typescript": "~5.0.4",
-    "@vue/cli-service": "~5.0.4",
-    "@vue/compiler-sfc": "^3.2.36",
+    "@typescript-eslint/eslint-plugin": "^5.30.6",
+    "@typescript-eslint/parser": "^5.30.6",
+    "@vue/cli-plugin-babel": "~5.0.8",
+    "@vue/cli-plugin-eslint": "~5.0.8",
+    "@vue/cli-plugin-router": "~5.0.8",
+    "@vue/cli-plugin-typescript": "~5.0.8",
+    "@vue/cli-service": "~5.0.8",
+    "@vue/compiler-sfc": "^3.2.37",
     "@vue/eslint-config-prettier": "^7.0.0",
     "@vue/eslint-config-typescript": "^10.0.0",
-    "eslint": "^8.16.0",
-    "eslint-plugin-prettier": "^4.0.0",
+    "eslint": "^8.19.0",
+    "eslint-plugin-prettier": "^4.2.1",
     "eslint-plugin-vue": "^8.7.1",
-    "lint-staged": "^13.0.0",
-    "prettier": "^2.6.2",
-    "typescript": "~4.7.2"
+    "lint-staged": "^13.0.3",
+    "prettier": "^2.7.1",
+    "typescript": "~4.7.4"
   },
   "gitHooks": {
     "pre-commit": "lint-staged"

+ 4 - 4
report-viewer/src/App.vue

@@ -5,13 +5,13 @@
 
 <style>
 @font-face {
-  font-family: "JetBrains Mono";
-  src: url("./fonts/jetbrains-mono/JetBrainsMono-Light.ttf");
+  font-family: "JetBrains Mono NL";
+  src: url("./fonts/jetbrains-mono/JetBrainsMonoNL-Light.ttf");
 }
 
 @font-face {
-  font-family: "JetBrains Mono";
-  src: url("./fonts/jetbrains-mono/JetBrainsMono-Bold.ttf");
+  font-family: "JetBrains Mono NL";
+  src: url("./fonts/jetbrains-mono/JetBrainsMonoNL-Bold.ttf");
   font-weight: bold;
 }
 

+ 22 - 0
report-viewer/src/assets/radar-chart-configuration.ts

@@ -0,0 +1,22 @@
+
+ const radarChartStyle ={
+    fill: true,
+    backgroundColor: "rgba(149, 168, 241, 0.5)",
+    borderColor: "rgba(149, 168, 241, 1)",
+    pointBackgroundColor: "rgba(149, 168, 241, 1)",
+    pointBorderColor: "#fff",
+    pointHoverBackgroundColor: "#fff",
+    pointHoverBorderColor: "rgb(255, 99, 132)",
+  }
+  const radarChartOptions = {
+    legend: {
+      display: false,
+    },
+    scales: {
+      r: {
+        suggestedMin: 50,
+        suggestedMax: 100,
+      },
+    },
+  }
+  export {radarChartStyle,radarChartOptions}

+ 89 - 64
report-viewer/src/components/ClusterRadarChart.vue

@@ -4,88 +4,110 @@
 -->
 <template>
   <div class="wrapper">
-    <select v-model="selectedMember">
-      <option v-for="(member, index) in Object.keys(cluster.members)" :key="index">{{ member }}</option>
-    </select>
-    <RadarChart :chartData="chartData" :options="options" class="chart"></RadarChart>
+    <div v-if="!hasNoMember" class="wrapper">
+      <select v-model="selectedMember">
+        <option v-for="(member, index) in cluster.members.keys()" :key="index">
+          {{ member }}
+        </option>
+      </select>
+      <RadarChart
+        :chartData="chartData"
+        :options="options"
+        class="chart"
+      ></RadarChart>
+    </div>
+    <div v-if="hasNoMember" class="no-member">
+      <span>This cluster has no members.</span>
+    </div>
   </div>
 </template>
 
-<script>
-import {defineComponent, ref, watch} from "vue";
-import {RadarChart} from "vue-chart-3"
-import {Chart, registerables} from 'chart.js';
-import ChartDataLabels from 'chartjs-plugin-datalabels';
+<script lang="ts">
+import { defineComponent, PropType, Ref, ref, watch } from "vue";
+import { RadarChart } from "vue-chart-3";
+import { Chart, ChartData, registerables } from "chart.js";
+import ChartDataLabels from "chartjs-plugin-datalabels";
+import { ClusterListElement } from "@/model/ClusterListElement";
+import {
+  radarChartStyle,
+  radarChartOptions,
+} from "@/assets/radar-chart-configuration";
 
 Chart.register(...registerables);
 Chart.register(ChartDataLabels);
 
 export default defineComponent({
   name: "ClusterRadarChart",
-  components: {RadarChart},
+  components: { RadarChart },
   props: {
-    cluster: {}
+    cluster: {
+      type: Object as PropType<ClusterListElement>,
+      required: true,
+    },
   },
   setup(props) {
-    const selectedMember = ref("")
+    const getIdOfFirstSubmission = () => {
+      const firstMember = props.cluster.members.keys().next().value;
+      hasNoMember = !firstMember;
+      return firstMember;
+    };
 
-    const createLabelsFor = (member) => {
-      let matchedWith = []
-      props.cluster.members[member].forEach(m => matchedWith.push(m.matchedWith))
-      return matchedWith
-    }
-    const createDataSetFor = (member) => {
-      let data = []
-      props.cluster.members[member].forEach(m => data.push(m.percentage))
-      return data
-    }
+    const createLabelsFor = (member: string): Array<string> => {
+      let matchedWith = new Array<string>();
+      props.cluster.members
+        .get(member)
+        ?.forEach((m) => matchedWith.push(m.matchedWith));
+      return matchedWith;
+    };
+    const createDataSetFor = (member: string) => {
+      let data = new Array<number>();
+      props.cluster.members
+        .get(member)
+        ?.forEach((m) => data.push(roundToTwoDecimals(m.percentage)));
+      return data;
+    };
+    const roundToTwoDecimals = (num: number): number =>
+      Math.round((num + Number.EPSILON) * 100) / 100;
 
-    const chartStyle = {
-      fill: true,
-      backgroundColor: 'rgba(149, 168, 241, 0.5)',
-      borderColor: 'rgba(149, 168, 241, 1)',
-      pointBackgroundColor: 'rgba(149, 168, 241, 1)',
-      pointBorderColor: '#fff',
-      pointHoverBackgroundColor: '#fff',
-      pointHoverBorderColor: 'rgb(255, 99, 132)'
-    }
+    const selectedMember = ref(getIdOfFirstSubmission());
 
-    const chartData = ref({
-      labels: createLabelsFor(Object.keys(props.cluster.members)[0]),
-      datasets: [{
-        ...chartStyle,
-        label: Object.keys(props.cluster.members)[0],
-        data: createDataSetFor(Object.keys(props.cluster.members)[0])
-      }]
-    })
+    const chartData: Ref<ChartData<"radar", (number | null)[], unknown>> = ref({
+      labels: createLabelsFor(getIdOfFirstSubmission()),
+      datasets: [
+        {
+          ...radarChartStyle,
+          label: getIdOfFirstSubmission(),
+          data: createDataSetFor(getIdOfFirstSubmission()),
+        },
+      ],
+    });
 
-    const options = ref({
-      scales: {
-        r: {
-          suggestedMin: 50,
-          suggestedMax: 100
-        }
+    const options = ref(radarChartOptions);
+    let hasNoMember = false;
+    watch(
+      () => selectedMember.value,
+      (val) => {
+        chartData.value = {
+          labels: createLabelsFor(val),
+          datasets: [
+            {
+              ...radarChartStyle,
+              label: val,
+              data: createDataSetFor(val),
+            },
+          ],
+        };
       }
-    })
-
-    watch(() => selectedMember.value, (val) => {
-      chartData.value = {
-        labels: createLabelsFor(val),
-        datasets: [{
-          ...chartStyle,
-          label: val,
-          data: createDataSetFor(val),
-        }]
-      }
-    })
+    );
 
     return {
       selectedMember,
       chartData,
-      options
-    }
-  }
-})
+      options,
+      hasNoMember,
+    };
+  },
+});
 </script>
 
 <style scoped>
@@ -93,10 +115,13 @@ export default defineComponent({
   display: flex;
   flex-direction: column;
   flex-wrap: nowrap;
-
 }
-
+.no-member {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+}
 .chart {
   height: 50vw;
 }
-</style>
+</style>

+ 54 - 20
report-viewer/src/components/ClustersList.vue

@@ -3,38 +3,66 @@
 -->
 <template>
   <div class="wrapper">
-    <h1>Clusters for comparison {{ comparison.firstSubmissionId }} > {{ comparison.secondSubmissionId }}</h1>
-    <p v-for="( cluster, index ) in clusters" :key="index" @click="toggleDialog">
-      {{ index }} - {{ cluster.averageSimilarity }}
+    <h1>
+      Clusters for comparison {{ comparison.firstSubmissionId }} >
+      {{ comparison.secondSubmissionId }}
+    </h1>
+    <p v-for="(cluster, index) in clusters" :key="index" @click="toggleDialog">
+      {{ index + 1 }}. Members:
+      <span id="members">{{ getMemberNames(cluster) }}</span> - Average
+      similarity: {{ cluster.averageSimilarity }}%
       <GDialog v-model="dialog" fullscreen>
-        <button @click="toggleDialog">Close</button>
+        <div id="dialog-header">
+          <button @click="toggleDialog">Close</button>
+        </div>
         <ClusterRadarChart :cluster="cluster"></ClusterRadarChart>
       </GDialog>
     </p>
   </div>
 </template>
 
-<script>
-import {defineComponent, ref} from "vue";
-import {GDialog} from "gitart-vue-dialog";
-import ClusterRadarChart from "@/components/ClusterRadarChart";
-
+<script lang="ts">
+import { defineComponent, ref, PropType } from "vue";
+import { GDialog } from "gitart-vue-dialog";
+import ClusterRadarChart from "@/components/ClusterRadarChart.vue";
+import { ClusterListElement } from "@/model/ClusterListElement";
+import { ComparisonListElement } from "@/model/ComparisonListElement";
 export default defineComponent({
   name: "ClustersList",
-  components: {ClusterRadarChart, GDialog},
+  components: { ClusterRadarChart, GDialog },
   props: {
-    comparison: {},
-    clusters: Array
+    comparison: {
+      type: Object as PropType<ComparisonListElement>,
+      required: true,
+
+    },
+    clusters: {
+      type: Array<ClusterListElement>,
+      required: true
   },
+  },
+
   setup() {
-    const dialog = ref(false)
-    const toggleDialog = () => dialog.value = !dialog.value
+    const dialog = ref(false);
+    const toggleDialog = () => (dialog.value = !dialog.value);
+    const getMemberNames = (cluster: ClusterListElement) => {
+      const membersIterator = cluster.members.keys();
+      const members = Array.from(membersIterator);
+      let concatenatedMembers = "";
+      const maxMembersToShow = 5;
+      concatenatedMembers = members.slice(0, maxMembersToShow).join(", ");
+      if (members.length > maxMembersToShow) {
+        concatenatedMembers += ",...";
+      }
+      return concatenatedMembers;
+    };
     return {
       dialog,
-      toggleDialog
-    }
-  }
-})
+      toggleDialog,
+      getMemberNames,
+    };
+  },
+});
 </script>
 
 <style scoped>
@@ -60,5 +88,11 @@ p:hover {
   background: var(--primary-color-dark);
   cursor: pointer;
 }
-
-</style>
+#dialog-header {
+  display: flex;
+  flex-direction: row-reverse;
+  margin-right: 1%;
+  margin-top: 0.5%;
+  box-shadow: 5px 10x #888888;
+}
+</style>

+ 70 - 47
report-viewer/src/components/CodePanel.vue

@@ -2,36 +2,61 @@
   Panel which displays a submission files with its line of code.
 -->
 <template>
-  <div :id="panelId.toString().concat(title).concat(fileIndex.toString())" class="code-panel-container">
+  <div
+    :id="panelId.toString().concat(title).concat(fileIndex.toString())"
+    class="code-panel-container"
+  >
     <div class="file-title">
       <p style="width: 90%">{{ title }}</p>
-      <button class="collapse-button" style="width: 10%" @click="$emit('toggleCollapse')">
-        <img v-if="collapse" alt="hide info" src="../assets/keyboard_double_arrow_up_black_18dp.svg">
-        <img v-else alt="additional info" src="../assets/keyboard_double_arrow_down_black_18dp.svg">
+      <button
+        class="collapse-button"
+        style="width: 10%"
+        @click="$emit('toggleCollapse')"
+      >
+        <img
+          v-if="collapse"
+          alt="hide info"
+          src="../assets/keyboard_double_arrow_up_black_18dp.svg"
+        />
+        <img
+          v-else
+          alt="additional info"
+          src="../assets/keyboard_double_arrow_down_black_18dp.svg"
+        />
       </button>
     </div>
-    <div :class="{ hidden : !collapse }" class="code-container">
-      <LineOfCode v-for="(line, index) in lines"
-                  :id="String(panelId).concat(title).concat(index)"
-                  :key="index"
-                  :color="coloringArray[index]"
-                  :is-first="isFirst[index]"
-                  :is-last="isLast[index]"
-                  :line-number="index"
-                  :text="line"
-                  :visible="collapse"
-                  @click="$emit('lineSelected', $event, linksArray[index].panel, linksArray[index].file, linksArray[index].line )"/>
+    <div :class="{ hidden: !collapse }" class="code-container">
+      <LineOfCode
+        v-for="(line, index) in lines"
+        :id="String(panelId).concat(title).concat(index)"
+        :key="index"
+        :color="coloringArray[index]"
+        :is-first="isFirst[index]"
+        :is-last="isLast[index]"
+        :line-number="index + 1"
+        :text="line"
+        :visible="collapse"
+        @click="
+          $emit(
+            'lineSelected',
+            $event,
+            linksArray[index].panel,
+            linksArray[index].file,
+            linksArray[index].line
+          )
+        "
+      />
     </div>
   </div>
 </template>
 
 <script>
-import {defineComponent, ref} from "vue";
+import { defineComponent, ref } from "vue";
 import LineOfCode from "./LineOfCode";
 
 export default defineComponent({
   name: "CodePanel",
-  components: {LineOfCode},
+  components: { LineOfCode },
   props: {
     /**
      * Title of the displayed file.
@@ -43,7 +68,7 @@ export default defineComponent({
      * Index of file amongst other files in submission.
      */
     fileIndex: {
-      type: Number
+      type: Number,
     },
     /**
      * Code lines of the file.
@@ -51,27 +76,27 @@ export default defineComponent({
      */
     lines: {
       type: Array,
-      required: true
+      required: true,
     },
     /**
      * Matches in the file
      * type: Array<MatchInSingleFile>
      */
     matches: {
-      type: Array
+      type: Array,
     },
     /**
      * Id of the FilesContainer. Needed for lines link generation.
      */
     panelId: {
-      type: Number
+      type: Number,
     },
     /**
      * Indicates whether files is collapsed or not.
      */
     collapse: {
-      type: Boolean
-    }
+      type: Boolean,
+    },
   },
   setup(props) {
     /**
@@ -86,7 +111,7 @@ export default defineComponent({
      * }
      * @type {Ref<UnwrapRef<{}>>}
      */
-    const coloringArray = ref({})
+    const coloringArray = ref({});
     /**
      * An object containing an object from which an id is to of the line to which this is linked is constructed.
      * Id object contains panel, file name, first line number of linked matched.
@@ -101,44 +126,42 @@ export default defineComponent({
      * Key is line number, value is id of linked line.
      * @type {Ref<UnwrapRef<{}>>}
      */
-    const linksArray = ref({})
+    const linksArray = ref({});
     /**
      * Indicates whether the line is last line of match. Key is line number, value is true or false.
      * @type {Ref<UnwrapRef<{}>>}
      */
-    const isLast = ref({})
+    const isLast = ref({});
     /**
      * Indicates whether the line is the first line of a match. Key is line number, value is true or false.
      * @type {Ref<UnwrapRef<{}>>}
      */
-    const isFirst = ref({})
+    const isFirst = ref({});
 
     /**
      * Initializing the the upper arrays.
      */
-    props.matches.forEach(m => {
+    props.matches.forEach((m) => {
       for (let i = m.start; i <= m.end; i++) {
         //assign match color to line
-        coloringArray.value[i] = m.color
+        coloringArray.value[i] = m.color;
         //assign link object to line.
-        linksArray.value[i] = {panel: m.linked_panel, file: m.linked_file, line: m.linked_line}
-        //check whether line is start of match and assign true if yes
-        if (i === m.start) {
-          isFirst.value[i] = true
-        }
-        //check whether line is end of match and assign true if yes
-        if (i === m.end) {
-          isLast.value[i] = true
-        }
+        linksArray.value[i] = {
+          panel: m.linked_panel,
+          file: m.linked_file,
+          line: m.linked_line,
+        };
       }
-    })
+      isFirst.value[m.start] = true;
+      isLast.value[m.end] = true;
+    });
     //assign default values for all line which are not contained in matches
     for (let i = 0; i < props.lines.length; i++) {
       if (!coloringArray.value[i]) {
-        coloringArray.value[i] = "#FFFFFF"
-        linksArray.value[i] = "-1"
-        isFirst.value[i] = false
-        isLast.value[i] = false
+        coloringArray.value[i] = "#FFFFFF";
+        linksArray.value[i] = "-1";
+        isFirst.value[i] = false;
+        isLast.value[i] = false;
       }
     }
 
@@ -147,9 +170,9 @@ export default defineComponent({
       linksArray,
       isFirst,
       isLast,
-    }
-  }
-})
+    };
+  },
+});
 </script>
 
 <style scoped>
@@ -202,4 +225,4 @@ export default defineComponent({
 .hidden {
   display: none !important;
 }
-</style>
+</style>

+ 151 - 76
report-viewer/src/components/ComparisonsTable.vue

@@ -10,120 +10,195 @@
       <th>Submission 2</th>
       <th>Match %</th>
     </tr>
-    <tr v-for="(comparison, index) in topComparisons"
-        :key="comparison.firstSubmissionId
-            + comparison.secondSubmissionId
-            + comparison.matchPercentage"
-        class="selectable"
+    <tr
+      v-for="(comparison, index) in topComparisons"
+      :key="
+        comparison.firstSubmissionId +
+        comparison.secondSubmissionId +
+        comparison.matchPercentage
+      "
+      class="selectable"
     >
-      <td @click="navigateToComparisonView(comparison.firstSubmissionId, comparison.secondSubmissionId)">{{
-          index + 1
-        }}.
+      <td
+        @click="
+          navigateToComparisonView(
+            comparison.firstSubmissionId,
+            comparison.secondSubmissionId
+          )
+        "
+      >
+        {{ index + 1 }}.
       </td>
-      <td :class="{ 'anonymous-style' : isAnonymous(comparison.firstSubmissionId)}"
-          @click="navigateToComparisonView(comparison.firstSubmissionId, comparison.secondSubmissionId)">
-        {{ isAnonymous(comparison.firstSubmissionId) ? "Hidden" : comparison.firstSubmissionId }}
+      <td
+        :class="{
+          'anonymous-style': isAnonymous(comparison.firstSubmissionId),
+        }"
+        @click="
+          navigateToComparisonView(
+            comparison.firstSubmissionId,
+            comparison.secondSubmissionId
+          )
+        "
+      >
+        {{
+          isAnonymous(comparison.firstSubmissionId)
+            ? "Hidden"
+            : comparison.firstSubmissionId
+        }}
       </td>
       <td
-          @click="navigateToComparisonView(comparison.firstSubmissionId, comparison.secondSubmissionId)">
-        <img alt=">>" src="@/assets/double_arrow_black_18dp.svg"/>
+        @click="
+          navigateToComparisonView(
+            comparison.firstSubmissionId,
+            comparison.secondSubmissionId
+          )
+        "
+      >
+        <img alt=">>" src="@/assets/double_arrow_black_18dp.svg" />
       </td>
-      <td :class="{ 'anonymous-style' : isAnonymous(comparison.secondSubmissionId)}"
-          @click="navigateToComparisonView(comparison.firstSubmissionId, comparison.secondSubmissionId)">
-        {{ isAnonymous(comparison.secondSubmissionId) ? "Hidden" : comparison.secondSubmissionId }}
+      <td
+        :class="{
+          'anonymous-style': isAnonymous(comparison.secondSubmissionId),
+        }"
+        @click="
+          navigateToComparisonView(
+            comparison.firstSubmissionId,
+            comparison.secondSubmissionId
+          )
+        "
+      >
+        {{
+          isAnonymous(comparison.secondSubmissionId)
+            ? "Hidden"
+            : comparison.secondSubmissionId
+        }}
       </td>
       <td>{{ formattedMatchPercentage(comparison.matchPercentage) }}</td>
       <td>
-        <img v-if="isInCluster(comparison.firstSubmissionId, comparison.secondSubmissionId)"
-             alt=">>"
-             src="@/assets/keyboard_double_arrow_down_black_18dp.svg"
-             @click="toggleDialog(index)"
+        <img
+          v-if="
+            isInCluster(
+              comparison.firstSubmissionId,
+              comparison.secondSubmissionId
+            )
+          "
+          alt=">>"
+          src="@/assets/keyboard_double_arrow_down_black_18dp.svg"
+          @click="toggleDialog(index)"
         />
       </td>
       <GDialog v-model="dialog[index]">
-        <ClustersList :clusters="getClustersFor(comparison.firstSubmissionId, comparison.secondSubmissionId)"
-                      :comparison="comparison"/>
+        <ClustersList
+          :clusters="
+            getClustersFor(
+              comparison.firstSubmissionId,
+              comparison.secondSubmissionId
+            )
+          "
+          :comparison="comparison"
+        />
       </GDialog>
     </tr>
   </table>
 </template>
 
-<script>
-import {defineComponent, ref} from "vue";
+<script lang="ts">
+import { defineComponent, Ref, ref } from "vue";
 import router from "@/router";
-import store from "@/store/store";
-import {GDialog} from "gitart-vue-dialog";
-import ClustersList from "@/components/ClustersList";
+import { GDialog } from "gitart-vue-dialog";
+import ClustersList from "@/components/ClustersList.vue";
+import { useStore } from "vuex";
+import { Cluster } from "@/model/Cluster";
+import { ComparisonListElement } from "@/model/ComparisonListElement";
+import { ClusterListElement } from "@/model/ClusterListElement";
 
 export default defineComponent({
   name: "ComparisonsTable",
-  components: {ClustersList, GDialog},
+  components: { ClustersList, GDialog },
   props: {
     topComparisons: {
-      type: Array,
-      required: true
+      type: Array<ComparisonListElement>,
+      required: true,
     },
     clusters: {
-      type: Array
-    }
+      type: Array<Cluster>,
+      required: true,
+    },
   },
   setup(props) {
-    let formattedMatchPercentage = (number) => number.toFixed(2)
-    const dialog = ref([])
-    props.topComparisons.forEach(() => dialog.value.push(false))
+    const store = useStore();
+    let formattedMatchPercentage = (num: number) => num.toFixed(2);
+    const dialog :Ref<Array<boolean>> = ref([]);
+    props.topComparisons.forEach(() => dialog.value.push(false));
 
-    const toggleDialog = (index) => {
-      dialog.value[index] = true
-    }
+    const toggleDialog = (index:number) => {
+      dialog.value[index] = true;
+    };
 
-    const navigateToComparisonView = (firstId, secondId) => {
+    const navigateToComparisonView = (firstId: string, secondId: string) => {
       if (!store.state.single) {
         router.push({
           name: "ComparisonView",
-          query: {firstId: firstId, secondId: secondId},
-        })
+          params: { firstId, secondId },
+        });
       }
-    }
-
-    const isInCluster = (id1, id2) => {
-      return props.clusters.some(c => c.members.includes(id1) && c.members.includes(id2))
-    }
+    };
 
-    const isAnonymous = (id) => {
-      return store.state.anonymous.has(id)
-    }
+    const isInCluster = (id1: string, id2: string) => {
+      return props.clusters.some(
+        (c: Cluster) => c.members.includes(id1) && c.members.includes(id2)
+      );
+    };
 
+    const isAnonymous = (id: string) => {
+      return store.state.anonymous.has(id);
+    };
 
-    const getParticipatingMatchesForId = (id, others) => {
-      let matches = []
-      props.topComparisons.forEach(comparison => {
-        if (comparison.firstSubmissionId.includes(id) && others.includes(comparison.secondSubmissionId)) {
-          matches.push({matchedWith: comparison.secondSubmissionId, percentage: comparison.matchPercentage})
-        } else if (comparison.secondSubmissionId.includes(id) && others.includes(comparison.firstSubmissionId)) {
-          matches.push({matchedWith: comparison.firstSubmissionId, percentage: comparison.matchPercentage})
+    const getParticipatingMatchesForId = (id: string, others : Array<string>) => {
+      let matches : Array<{matchedWith: string, percentage: number,}>= [];
+      props.topComparisons.forEach((comparison) => {
+        if (
+          comparison.firstSubmissionId.includes(id) &&
+          others.includes(comparison.secondSubmissionId)
+        ) {
+          matches.push({
+            matchedWith: comparison.secondSubmissionId,
+            percentage: comparison.matchPercentage,
+          });
+        } else if (
+          comparison.secondSubmissionId.includes(id) &&
+          others.includes(comparison.firstSubmissionId)
+        ) {
+          matches.push({
+            matchedWith: comparison.firstSubmissionId,
+            percentage: comparison.matchPercentage,
+          });
         }
-      })
+      });
       return matches;
-    }
+    };
 
-    const clustersWithParticipatingMatches = props.clusters.map(cluster => {
-      let membersArray = {}
-      cluster.members.forEach(member => {
-        let others = cluster.members.filter(m => !m.includes(member))
-        membersArray[member] = getParticipatingMatchesForId(member, others)
-      })
+    const clustersWithParticipatingMatches: Array<ClusterListElement> = props.clusters.map((cluster) => {
+      let membersArray = new Map<string,Array<{matchedWith: string, percentage: number,}>>();
+      cluster.members.forEach((member: string) => {
+        let others = cluster.members.filter((m) => !m.includes(member));
+        membersArray.set(member ,getParticipatingMatchesForId(member, others));
+      });
 
       return {
         averageSimilarity: cluster.averageSimilarity,
         strength: cluster.strength,
-        members: membersArray
-      }
-    })
+        members: membersArray,
+      };
+    });
 
-    const getClustersFor = (id1, id2) => {
-      return clustersWithParticipatingMatches.filter(c => Object.keys(c.members).includes(id1)
-          && Object.keys(c.members).includes(id2))
-    }
+    const getClustersFor = (id1: string, id2: string) : Array<ClusterListElement> => {
+      return clustersWithParticipatingMatches.filter(
+        (c) =>
+          c.members.has(id1) &&
+          c.members.has(id2)
+      );
+    };
 
     return {
       clustersWithParticipatingMatches,
@@ -134,10 +209,10 @@ export default defineComponent({
       toggleDialog,
       formattedMatchPercentage,
       navigateToComparisonView,
-      isInCluster
-    }
-  }
-})
+      isInCluster,
+    };
+  },
+});
 </script>
 
 <style scoped>
@@ -180,4 +255,4 @@ td {
   background: var(--primary-color-dark);
   cursor: pointer;
 }
-</style>
+</style>

+ 33 - 31
report-viewer/src/components/FilesContainer.vue

@@ -5,63 +5,65 @@
   <div class="files-container">
     <h1>Files of {{ filesOwner }}</h1>
     <VueDraggableNext>
-      <CodePanel v-for="(file, index) in Object.keys(files)"
-                 :key="file.concat(index)"
-                 :collapse="files[file].collapsed"
-                 :file-index="index"
-                 :lines="files[file].lines"
-                 :matches="!matches[file] ? [] : matches[file]"
-                 :panel-id="containerId"
-                 :title="file"
-                 @toggle-collapse="$emit('toggle-collapse', file)"
-                 @line-selected="lineSelected"
+      <CodePanel
+        v-for="(file, index) in files.keys()"
+        :key="file.concat(index.toString())"
+        :collapse="files.get(file)?.collapsed"
+        :file-index="index"
+        :lines="!files.get(file)?.lines ? [] : files.get(file)?.lines"
+        :matches="!matches.get(file) ? [] : matches.get(file)"
+        :panel-id="containerId"
+        :title="file"
+        @toggle-collapse="$emit('toggle-collapse', file)"
+        @line-selected="lineSelected"
       />
     </VueDraggableNext>
   </div>
 </template>
 
-<script>
-import {defineComponent} from "vue";
-import CodePanel from "@/components/CodePanel";
-import {VueDraggableNext} from "vue-draggable-next"
+<script lang="ts">
+import { defineComponent } from "vue";
+import CodePanel from "../components/CodePanel.vue";
+import { VueDraggableNext } from "vue-draggable-next";
+import { SubmissionFile } from "@/model/SubmissionFile";
+import { MatchInSingleFile } from "@/model/MatchInSingleFile";
 
 export default defineComponent({
   name: "FilesContainer",
-  components: {CodePanel, VueDraggableNext},
+  components: { CodePanel, VueDraggableNext },
   props: {
     /**
      * Id of the files container. We have only two so it is either 1 or 2.
      */
     containerId: {
       type: Number,
-      required: true
+      required: true,
     },
     /**
      * Id of the submission to thich the files belong.
      */
     filesOwner: {
       type: String,
-      required: true
+      required: true,
     },
     /**
      * Files of the submission.
      * type: Array<SubmissionFile>
      */
     files: {
+      type: Map<string,SubmissionFile>,
       required: true,
-
     },
     /**
      * Matche of submission.
-     * type: Array<MatchInSingleFile>
      */
     matches: {
-      required: true
-    }
+     type: Map<string,MatchInSingleFile>,
+      required: true,
+    },
   },
 
-  setup(props, {emit}) {
-
+  setup(props, { emit }) {
     /**
      * Passes lineSelected event, emitted from LineOfCode, to parent.
      * @param e
@@ -69,14 +71,14 @@ export default defineComponent({
      * @param file
      * @param line
      */
-    const lineSelected = (e, index, file, line) => {
-      emit('lineSelected', e, index, file, line)
-    }
+    const lineSelected = (e: unknown, index: number, file: string, line: number) => {
+      emit("lineSelected", e, index, file, line);
+    };
     return {
-      lineSelected
-    }
-  }
-})
+      lineSelected,
+    };
+  },
+});
 </script>
 
 <style scoped>
@@ -92,4 +94,4 @@ h1 {
   width: 100%;
   overflow: auto;
 }
-</style>
+</style>

+ 1 - 1
report-viewer/src/components/LineOfCode.vue

@@ -82,7 +82,7 @@ code.hljs {
 
 .hljs {
   background: transparent !important;
-  font-family: "JetBrains Mono", serif !important;
+  font-family: "JetBrains Mono NL", serif !important;
   font-weight: bold;    font-size: x-small !important;
 }
 

+ 1 - 1
report-viewer/src/components/TextInformation.vue

@@ -130,7 +130,7 @@ hr {
   margin: 3% 0;
   box-shadow: inset var(--shadow-color) 0 0 3px;
   border-radius: 10px;
-  font-family: "JetBrains Mono", serif;
+  font-family: "JetBrains  NL", serif;
   font-size: smaller;
 }
 

+ 10 - 0
report-viewer/src/model/ClusterListElement.ts

@@ -0,0 +1,10 @@
+type ClusterListElement = {
+  averageSimilarity: number;
+  strength: number;
+  members: ClusterListElementMember;
+};
+type ClusterListElementMember = Map<
+  string,
+  Array<{ matchedWith: string; percentage: number }>
+>;
+export { ClusterListElement, ClusterListElementMember };

+ 95 - 91
report-viewer/src/model/Comparison.ts

@@ -1,96 +1,100 @@
-import {Match} from "./Match";
-import {SubmissionFile} from "./SubmissionFile"
-import {MatchInSingleFile} from "./MatchInSingleFile";
+import { Match } from "./Match";
+import { SubmissionFile } from "./SubmissionFile";
+import { MatchInSingleFile } from "./MatchInSingleFile";
 
 /**
  * Comparison model used by the ComparisonView
  */
 export class Comparison {
-    private readonly _firstSubmissionId: string
-    private readonly _secondSubmissionId: string
-    private readonly _match_percentage: number
-
-    constructor(firstSubmissionId: string, secondSubmissionId: string, match_percentage: number) {
-        this._firstSubmissionId = firstSubmissionId;
-        this._secondSubmissionId = secondSubmissionId;
-        this._match_percentage = match_percentage;
-        this._filesOfFirstSubmission = {}
-        this._filesOfSecondSubmission = {}
-        this._colors = []
-        this._allMatches = []
-        this._matchesInFirstSubmission = {}
-        this._matchesInSecondSubmissions = {}
-    }
-
-    private _filesOfFirstSubmission: Record<string, SubmissionFile>
-
-    get filesOfFirstSubmission(): Record<string, SubmissionFile> {
-        return this._filesOfFirstSubmission;
-    }
-
-    set filesOfFirstSubmission(value: Record<string, SubmissionFile>) {
-        this._filesOfFirstSubmission = value;
-    }
-
-    private _filesOfSecondSubmission: Record<string, SubmissionFile>
-
-    get filesOfSecondSubmission(): Record<string, SubmissionFile> {
-        return this._filesOfSecondSubmission;
-    }
-
-    set filesOfSecondSubmission(value: Record<string, SubmissionFile>) {
-        this._filesOfSecondSubmission = value;
-    }
-
-    private _colors: Array<string>
-
-    get colors(): Array<string> {
-        return this._colors;
-    }
-
-    set colors(value: Array<string>) {
-        this._colors = value;
-    }
-
-    private _allMatches: Array<Match>
-
-    get allMatches(): Array<Match> {
-        return this._allMatches;
-    }
-
-    set allMatches(value: Array<Match>) {
-        this._allMatches = value;
-    }
-
-    private _matchesInFirstSubmission: Record<string, Array<MatchInSingleFile>>
-
-    get matchesInFirstSubmission(): Record<string, Array<MatchInSingleFile>> {
-        return this._matchesInFirstSubmission;
-    }
-
-    set matchesInFirstSubmission(value: Record<string, Array<MatchInSingleFile>>) {
-        this._matchesInFirstSubmission = value;
-    }
-
-    private _matchesInSecondSubmissions: Record<string, Array<MatchInSingleFile>>
-
-    get matchesInSecondSubmissions(): Record<string, Array<MatchInSingleFile>> {
-        return this._matchesInSecondSubmissions;
-    }
-
-    set matchesInSecondSubmissions(value: Record<string, Array<MatchInSingleFile>>) {
-        this._matchesInSecondSubmissions = value;
-    }
-
-    get firstSubmissionId(): string {
-        return this._firstSubmissionId;
-    }
-
-    get secondSubmissionId(): string {
-        return this._secondSubmissionId;
-    }
-
-    get match_percentage(): number {
-        return this._match_percentage;
-    }
-}
+  private readonly _firstSubmissionId: string;
+  private readonly _secondSubmissionId: string;
+  private readonly _match_percentage: number;
+
+  constructor(
+    firstSubmissionId: string,
+    secondSubmissionId: string,
+    match_percentage: number
+  ) {
+    this._firstSubmissionId = firstSubmissionId;
+    this._secondSubmissionId = secondSubmissionId;
+    this._match_percentage = match_percentage;
+    this._filesOfFirstSubmission = new Map();
+    this._filesOfSecondSubmission = new Map();
+    this._colors = [];
+    this._allMatches = [];
+    this._matchesInFirstSubmission = new Map();
+    this._matchesInSecondSubmissions = new Map();
+  }
+
+  private _filesOfFirstSubmission: Map<string, SubmissionFile>;
+
+  get filesOfFirstSubmission(): Map<string, SubmissionFile> {
+    return this._filesOfFirstSubmission;
+  }
+
+  set filesOfFirstSubmission(value: Map<string, SubmissionFile>) {
+    this._filesOfFirstSubmission = value;
+  }
+
+  private _filesOfSecondSubmission: Map<string, SubmissionFile>;
+
+  get filesOfSecondSubmission(): Map<string, SubmissionFile> {
+    return this._filesOfSecondSubmission;
+  }
+
+  set filesOfSecondSubmission(value: Map<string, SubmissionFile>) {
+    this._filesOfSecondSubmission = value;
+  }
+
+  private _colors: Array<string>;
+
+  get colors(): Array<string> {
+    return this._colors;
+  }
+
+  set colors(value: Array<string>) {
+    this._colors = value;
+  }
+
+  private _allMatches: Array<Match>;
+
+  get allMatches(): Array<Match> {
+    return this._allMatches;
+  }
+
+  set allMatches(value: Array<Match>) {
+    this._allMatches = value;
+  }
+
+  private _matchesInFirstSubmission: Map<string, Array<MatchInSingleFile>>;
+
+  get matchesInFirstSubmission(): Map<string, Array<MatchInSingleFile>> {
+    return this._matchesInFirstSubmission;
+  }
+
+  set matchesInFirstSubmission(value: Map<string, Array<MatchInSingleFile>>) {
+    this._matchesInFirstSubmission = value;
+  }
+
+  private _matchesInSecondSubmissions: Map<string, Array<MatchInSingleFile>>;
+
+  get matchesInSecondSubmissions(): Map<string, Array<MatchInSingleFile>> {
+    return this._matchesInSecondSubmissions;
+  }
+
+  set matchesInSecondSubmissions(value: Map<string, Array<MatchInSingleFile>>) {
+    this._matchesInSecondSubmissions = value;
+  }
+
+  get firstSubmissionId(): string {
+    return this._firstSubmissionId;
+  }
+
+  get secondSubmissionId(): string {
+    return this._secondSubmissionId;
+  }
+
+  get match_percentage(): number {
+    return this._match_percentage;
+  }
+}

+ 66 - 57
report-viewer/src/model/Overview.ts

@@ -1,70 +1,79 @@
-import {Metric} from "./Metric";
-import {Cluster} from "@/model/Cluster";
+import { Metric } from "./Metric";
+import { Cluster } from "@/model/Cluster";
 
 export class Overview {
-    private readonly _submissionFolderPath: Array<string>
-    private readonly _baseCodeFolderPath: string
-    private readonly _language: string
-    private readonly _fileExtensions: Array<string>
-    private readonly _matchSensitivity: number
-    private readonly _submissionIds: Array<string>
-    private readonly _dateOfExecution: string
-    private readonly _durationOfExecution: number
-    private readonly _metrics: Array<Metric>
-    private readonly _clusters: Array<Cluster>
+  private readonly _submissionFolderPath: Array<string>;
+  private readonly _baseCodeFolderPath: string;
+  private readonly _language: string;
+  private readonly _fileExtensions: Array<string>;
+  private readonly _matchSensitivity: number;
+  private readonly _submissionIds: Array<string>;
+  private readonly _dateOfExecution: string;
+  private readonly _durationOfExecution: number;
+  private readonly _metrics: Array<Metric>;
+  private readonly _clusters: Array<Cluster>;
 
+  constructor(
+    submissionFolderPath: Array<string>,
+    baseCodeFolderPath: string,
+    language: string,
+    fileExtensions: Array<string>,
+    matchSensitivity: number,
+    submissionIds: Array<string>,
+    dateOfExecution: string,
+    durationOfExecution: number,
+    metrics: Array<Metric>,
+    clusters: Array<Cluster>
+  ) {
+    this._submissionFolderPath = submissionFolderPath;
+    this._baseCodeFolderPath = baseCodeFolderPath;
+    this._language = language;
+    this._fileExtensions = fileExtensions;
+    this._matchSensitivity = matchSensitivity;
+    this._submissionIds = submissionIds;
+    this._dateOfExecution = dateOfExecution;
+    this._durationOfExecution = durationOfExecution;
+    this._metrics = metrics;
+    this._clusters = clusters;
+  }
 
-    constructor(submissionFolderPath: Array<string>, baseCodeFolderPath: string, language: string, fileExtensions: Array<string>, matchSensitivity: number, submissionIds: Array<string>, dateOfExecution: string, durationOfExecution: number, metrics: Array<Metric>, clusters: Array<Cluster>) {
-        this._submissionFolderPath = submissionFolderPath;
-        this._baseCodeFolderPath = baseCodeFolderPath;
-        this._language = language;
-        this._fileExtensions = fileExtensions
-        this._matchSensitivity = matchSensitivity;
-        this._submissionIds = submissionIds;
-        this._dateOfExecution = dateOfExecution;
-        this._durationOfExecution = durationOfExecution;
-        this._metrics = metrics;
-        this._clusters = clusters
-    }
+  get submissionFolderPath(): Array<string> {
+    return this._submissionFolderPath;
+  }
 
+  get baseCodeFolderPath(): string {
+    return this._baseCodeFolderPath;
+  }
 
-    get submissionFolderPath(): Array<string> {
-        return this._submissionFolderPath;
-    }
+  get language(): string {
+    return this._language;
+  }
 
-    get baseCodeFolderPath(): string {
-        return this._baseCodeFolderPath;
-    }
+  get fileExtensions(): Array<string> {
+    return this._fileExtensions;
+  }
 
-    get language(): string {
-        return this._language;
-    }
+  get matchSensitivity(): number {
+    return this._matchSensitivity;
+  }
 
-    get fileExtensions(): Array<string> {
-        return this._fileExtensions
-    }
+  get submissionIds(): Array<string> {
+    return this._submissionIds;
+  }
 
-    get matchSensitivity(): number {
-        return this._matchSensitivity;
-    }
+  get dateOfExecution(): string {
+    return this._dateOfExecution;
+  }
 
-    get submissionIds(): Array<string> {
-        return this._submissionIds;
-    }
+  get durationOfExecution(): number {
+    return this._durationOfExecution;
+  }
 
-    get dateOfExecution(): string {
-        return this._dateOfExecution;
-    }
+  get metrics(): Array<Metric> {
+    return this._metrics;
+  }
 
-    get durationOfExecution(): number {
-        return this._durationOfExecution;
-    }
-
-    get metrics(): Array<Metric> {
-        return this._metrics;
-    }
-
-    get clusters(): Array<Cluster> {
-        return this._clusters
-    }
-}
+  get clusters(): Array<Cluster> {
+    return this._clusters;
+  }
+}

+ 108 - 93
report-viewer/src/model/factories/ComparisonFactory.ts

@@ -1,106 +1,121 @@
-import {Comparison} from "../Comparison";
-import {Match} from "../Match";
-import {SubmissionFile} from "../SubmissionFile";
-import {MatchInSingleFile} from "../MatchInSingleFile";
+import { Comparison } from "../Comparison";
+import { Match } from "../Match";
+import { SubmissionFile } from "../SubmissionFile";
+import { MatchInSingleFile } from "../MatchInSingleFile";
 
 export class ComparisonFactory {
+  static getComparison(json: Record<string, unknown>): Comparison {
+    const filesOfFirst = this.convertToFilesByName(
+      json.files_of_first_submission as Array<Record<string, unknown>>
+    );
+    const filesOfSecond = this.convertToFilesByName(
+      json.files_of_second_submission as Array<Record<string, unknown>>
+    );
 
-    static getComparison(json: Record<string, unknown>): Comparison {
-        const filesOfFirst = this.convertToFilesByName(json.files_of_first_submission as Array<Record<string, unknown>>)
-        const filesOfSecond = this.convertToFilesByName(json.files_of_second_submission as Array<Record<string, unknown>>)
+    const matches = json.matches as Array<Record<string, unknown>>;
+    const colors = this.generateColorsForMatches(matches.length);
+    const coloredMatches = matches.map((match, index) =>
+      this.mapMatch(match, colors[index])
+    );
 
-        const matches = json.matches as Array<Record<string, unknown>>
-        const colors = this.generateColorsForMatches(matches.length)
-        const coloredMatches = matches.map((match, index) => this.mapMatch(match, colors[index]))
+    const matchesInFirst = this.groupMatchesByFileName(coloredMatches, 1);
+    const matchesInSecond = this.groupMatchesByFileName(coloredMatches, 2);
 
-        const matchesInFirst = this.groupMatchesByFileName(coloredMatches, 1)
-        const matchesInSecond = this.groupMatchesByFileName(coloredMatches, 2)
+    const comparison = new Comparison(
+      json.first_submission_id as string,
+      json.second_submission_id as string,
+      json.match_percentage as number
+    );
+    comparison.filesOfFirstSubmission = filesOfFirst;
+    comparison.filesOfSecondSubmission = filesOfSecond;
+    comparison.colors = colors;
+    comparison.allMatches = coloredMatches;
+    comparison.matchesInFirstSubmission = matchesInFirst;
+    comparison.matchesInSecondSubmissions = matchesInSecond;
 
-        const comparison = new Comparison(
-            json.first_submission_id as string,
-            json.second_submission_id as string,
-            json.match_percentage as number
-        )
-        comparison.filesOfFirstSubmission = filesOfFirst
-        comparison.filesOfSecondSubmission = filesOfSecond
-        comparison.colors = colors
-        comparison.allMatches = coloredMatches
-        comparison.matchesInFirstSubmission = matchesInFirst
-        comparison.matchesInSecondSubmissions = matchesInSecond
+    return comparison;
+  }
 
-        return comparison
-    }
-
-    private static convertToFilesByName(files: Array<Record<string, unknown>>): Record<string, SubmissionFile> {
-        return files.reduce((acc: Record<string, SubmissionFile>, val: Record<string, unknown>) => {
-            if (!acc[val.file_name as string]) {
-                acc[val.file_name as string] = {
-                    lines: [],
-                    collapsed: false
-                }
-            }
-            acc[val.file_name as string] = {
-                lines: val.lines as Array<string>,
-                collapsed: false
-            }
-            return acc;
-        }, {})
-    }
-
-    private static groupMatchesByFileName(matches: Array<Match>, index: number): Record<string, Array<MatchInSingleFile>> {
-        return matches.reduce((acc: Record<string, Array<MatchInSingleFile>>, val: Match) => {
-            const name = index === 1 ? val.firstFile as string : val.secondFile as string
-            if (!acc[name]) {
-                acc[name] = []
-            }
-            if (index === 1) {
-                const newVal: MatchInSingleFile = {
-                    start: val.startInFirst as number,
-                    end: val.endInFirst as number,
-                    linked_panel: 2,
-                    linked_file: val.secondFile as string,
-                    linked_line: val.startInSecond as number,
-                    color: val.color as string
-                }
-                acc[name].push(newVal)
-            } else {
-                const newVal: MatchInSingleFile = {
-                    start: val.startInSecond as number,
-                    end: val.endInSecond as number,
-                    linked_panel: 1,
-                    linked_file: val.firstFile as string,
-                    linked_line: val.startInFirst as number,
-                    color: val.color as string
-                }
-                acc[name].push(newVal)
-            }
-            return acc;
-        }, {})
-    }
+  private static convertToFilesByName(
+    files: Array<Record<string, unknown>>
+  ): Map<string, SubmissionFile> {
+    const map = new Map<string, SubmissionFile>();
+    files.forEach((val) => {
+      if (!map.get(val.file_name as string)) {
+        map.set(val.file_name as string, {
+          lines: [],
+          collapsed: false,
+        });
+      }
+      map.set(val.file_name as string, {
+        lines: val.lines as Array<string>,
+        collapsed: false,
+      });
+    });
+    return map;
+  }
 
-    private static generateColorsForMatches(num: number): Array<string> {
-        const colors = []
-        const hueDelta = Math.trunc(360 / (num))
+  private static groupMatchesByFileName(
+    matches: Array<Match>,
+    index: number
+  ): Map<string, Array<MatchInSingleFile>> {
+    const acc = new Map<string, Array<MatchInSingleFile>>();
+    matches.forEach((val) => {
+      const name =
+        index === 1 ? (val.firstFile as string) : (val.secondFile as string);
+      if (!acc.get(name)) {
+        acc.set(name, []);
+      }
+      if (index === 1) {
+        const newVal: MatchInSingleFile = {
+          start: val.startInFirst as number,
+          end: val.endInFirst as number,
+          linked_panel: 2,
+          linked_file: val.secondFile as string,
+          linked_line: val.startInSecond as number,
+          color: val.color as string,
+        };
+        acc.get(name)?.push(newVal);
+      } else {
+        const newVal: MatchInSingleFile = {
+          start: val.startInSecond as number,
+          end: val.endInSecond as number,
+          linked_panel: 1,
+          linked_file: val.firstFile as string,
+          linked_line: val.startInFirst as number,
+          color: val.color as string,
+        };
+        acc.get(name)?.push(newVal);
+      }
+    });
+    return acc;
+  }
 
-        for (let i = 0; i < num; i++) {
-            const hue = i * hueDelta
+  private static generateColorsForMatches(num: number): Array<string> {
+    const colors = [];
+    const hueDelta = Math.trunc(360 / num);
 
-            colors.push(`hsla(${hue}, 80%, 50%, 0.3)`)
-        }
-        return colors
-    }
+    for (let i = 0; i < num; i++) {
+      const hue = i * hueDelta;
 
-    private static mapMatch(match: Record<string, unknown>, color: string): Match {
-        return {
-            firstFile: match.first_file_name as string,
-            secondFile: match.second_file_name as string,
-            startInFirst: match.start_in_first as number,
-            endInFirst: match.end_in_first as number,
-            startInSecond: match.start_in_second as number,
-            endInSecond: match.end_in_second as number,
-            tokens: match.tokens as number,
-            color: color
-        };
+      colors.push(`hsla(${hue}, 80%, 50%, 0.3)`);
     }
+    return colors;
+  }
 
-}
+  private static mapMatch(
+    match: Record<string, unknown>,
+    color: string
+  ): Match {
+    return {
+      firstFile: match.first_file_name as string,
+      secondFile: match.second_file_name as string,
+      startInFirst: match.start_in_first as number,
+      endInFirst: match.end_in_first as number,
+      startInSecond: match.start_in_second as number,
+      endInSecond: match.end_in_second as number,
+      tokens: match.tokens as number,
+      color: color,
+    };
+  }
+}

+ 22 - 25
report-viewer/src/router/index.ts

@@ -1,36 +1,33 @@
-import {createRouter, createWebHistory, RouteRecordRaw} from "vue-router";
-import OverviewView from "@/views/OverviewView.vue"
-import ComparisonView from "@/views/ComparisonView.vue"
-import FileUploadView from "@/views/FileUploadView.vue"
+import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
+import OverviewView from "@/views/OverviewView.vue";
+import ComparisonView from "@/views/ComparisonView.vue";
+import FileUploadView from "@/views/FileUploadView.vue";
 
 /**
  * Router containing the navigation destinations.
  */
 const routes: Array<RouteRecordRaw> = [
-    {
-        path: "/",
-        name: "FileUploadView",
-        component: FileUploadView
-    },
-    {
-        path: "/overview",
-        name: "OverviewView",
-        component: OverviewView,
-    },
-    {
-        path: "/comparison",
-        name: "ComparisonView",
-        component: ComparisonView,
-        props: route => ({
-            firstId: route.query.firstId,
-            secondId: route.query.secondId
-        })
-    },
+  {
+    path: "/",
+    name: "FileUploadView",
+    component: FileUploadView,
+  },
+  {
+    path: "/overview",
+    name: "OverviewView",
+    component: OverviewView,
+  },
+  {
+    path: "/comparison",
+    name: "ComparisonView",
+    component: ComparisonView,
+    props: true,
+  },
 ];
 
 const router = createRouter({
-    history: createWebHistory("/JPlag"),
-    routes,
+  history: createWebHistory("/JPlag"),
+  routes,
 });
 
 export default router;

+ 93 - 59
report-viewer/src/store/store.ts

@@ -1,65 +1,99 @@
-import {createStore} from "vuex";
+import { createStore } from "vuex";
 
 /**
  * Local store. Stores the state of the application.
  */
-const store = createStore({
-    state() {
-        return {
-            /**
-             * The set of ids to be hidden.
-             */
-            anonymous: new Set(),
-            /**
-             * Stored files if zip mode is used. Stores the files as key - file name, value - file string
-             */
-            files: Object,
-            /**
-             * Indicates whether local mode is used.
-             */
-            local: Boolean,
-            /**
-             * Indicates whether zip mode is used.
-             */
-            zip: Boolean,
-            /**
-             * Indicates whether single file mode is used.
-             */
-            single: Boolean,
-            /**
-             * Files string if single mode is used.
-             */
-            fileString: {
-                type: String,
-                required: false
-            }
-        }
-    },
-    mutations: {
-        addAnonymous(state: any, id) {
-            for (let i = 0; i < id.length; i++) {
-                state.anonymous.add(id[i])
-            }
-        },
-        removeAnonymous(state, id) {
-            for (let i = 0; i < id.length; i++) {
-                state.anonymous.delete(id[i])
-            }
-        },
-        resetAnonymous(state) {
-            state.anonymous = new Set()
-        },
-        saveFile(state, file: Record<string, any>) {
-            state.files[file.fileName] = file.data
-        },
-        setLoadingType(state, payload: Record<string, any>) {
-            state.local = payload.local
-            state.zip = payload.zip
-            state.single = payload.single
-            state.fileString = payload.fileString
-        }
 
-    }
-})
+interface State {
+  /**
+   * The set of ids to be hidden.
+   */
+  anonymous: Set<string>;
+  /**
+   * Stored files if zip mode is used. Stores the files as key - file name, value - file string
+   */
+  files: Record<string, string>;
+  /**
+   * Indicates whether local mode is used.
+   */
+  local: boolean;
+  /**
+   * Indicates whether zip mode is used.
+   */
+  zip: boolean;
+  /**
+   * Indicates whether single file mode is used.
+   */
+  single: boolean;
+  /**
+   * Files string if single mode is used.
+   */
+  fileString: string;
+}
+
+interface File {
+  fileName: string;
+  data: string;
+}
+
+interface LoadConfiguration {
+  local: boolean;
+  zip: boolean;
+  single: boolean;
+  fileString: string;
+}
+
+const store = createStore<State>({
+  state: {
+    /**
+     * The set of ids to be hidden.
+     */
+    anonymous: new Set(),
+    /**
+     * Stored files if zip mode is used. Stores the files as key - file name, value - file string
+     */
+    files: {},
+    /**
+     * Indicates whether local mode is used.
+     */
+    local: false,
+    /**
+     * Indicates whether zip mode is used.
+     */
+    zip: false,
+    /**
+     * Indicates whether single file mode is used.
+     */
+    single: false,
+    /**
+     * Files string if single mode is used.
+     */
+    fileString: "",
+  },
+  mutations: {
+    addAnonymous(state: State, id) {
+      for (let i = 0; i < id.length; i++) {
+        state.anonymous.add(id[i]);
+      }
+    },
+    removeAnonymous(state, id) {
+      for (let i = 0; i < id.length; i++) {
+        state.anonymous.delete(id[i]);
+      }
+    },
+    resetAnonymous(state) {
+      state.anonymous = new Set();
+    },
+    saveFile(state, file: File) {
+      state.files[file.fileName] = file.data;
+    },
+    setLoadingType(state, payload: LoadConfiguration) {
+      state.local = payload.local;
+      state.zip = payload.zip;
+      state.single = payload.single;
+      state.fileString = payload.fileString;
+    },
+  },
+});
 
-export default store
+export default store;

+ 147 - 70
report-viewer/src/views/ComparisonView.vue

@@ -3,102 +3,164 @@
 -->
 <template>
   <div class="container">
-    <button id="show-button" :class="{hidden : !hideLeftPanel}" title="Show sidebar" @click="togglePanel">
-      <img alt="show" src="@/assets/double_arrow_black_24dp.svg">
+    <button
+      id="show-button"
+      :class="{ hidden: !hideLeftPanel }"
+      title="Show sidebar"
+      @click="togglePanel"
+    >
+      <img alt="show" src="@/assets/double_arrow_black_24dp.svg" />
     </button>
-    <div id="sidebar" :class="{ hidden : hideLeftPanel }">
+    <div id="sidebar" :class="{ hidden: hideLeftPanel }">
       <div class="title-section">
         <h1>JPlag Comparison</h1>
         <button id="hide-button" title="Hide sidebar" @click="togglePanel">
-          <img alt="hide" src="@/assets/keyboard_double_arrow_left_black_24dp.svg"></button>
+          <img
+            alt="hide"
+            src="@/assets/keyboard_double_arrow_left_black_24dp.svg"
+          />
+        </button>
       </div>
-      <TextInformation :anonymous="store.state.anonymous.has(firstId)" :value="firstId" label="Submission 1"/>
-      <TextInformation :anonymous="store.state.anonymous.has(secondId)" :value="secondId" label="Submission 2"/>
-      <TextInformation :value="comparison.match_percentage" label="Match %"/>
-      <MatchTable :id1="firstId" :id2="secondId" :matches="comparison.allMatches" @match-selected="showMatch"/>
+      <TextInformation
+        :anonymous="isAnonymous(firstId)"
+        :value="firstId"
+        label="Submission 1"
+      />
+      <TextInformation
+        :anonymous="store.state.anonymous.has(secondId)"
+        :value="secondId"
+        label="Submission 2"
+      />
+      <TextInformation :value="comparison.match_percentage" label="Match %" />
+      <MatchTable
+        :id1="firstId"
+        :id2="secondId"
+        :matches="comparison.allMatches"
+        @match-selected="showMatch"
+      />
     </div>
-    <FilesContainer :container-id="1" :files="filesOfFirst" :matches="comparison.matchesInFirstSubmission"
-                    files-owner="Submission 1"
-                    @toggle-collapse="toggleCollapseFirst"
-                    @line-selected="showMatchInSecond"/>
-    <FilesContainer :container-id="2" :files="filesOfSecond" :matches="comparison.matchesInSecondSubmissions"
-                    files-owner="Submission 2"
-                    @toggle-collapse="toggleCollapseSecond"
-                    @line-selected="showMatchInFirst"/>
+    <FilesContainer
+      :container-id="1"
+      :files="filesOfFirst"
+      :matches="comparison.matchesInFirstSubmission"
+      files-owner="Submission 1"
+      @toggle-collapse="toggleCollapseFirst"
+      @line-selected="showMatchInSecond"
+    />
+    <FilesContainer
+      :container-id="2"
+      :files="filesOfSecond"
+      :matches="comparison.matchesInSecondSubmissions"
+      files-owner="Submission 2"
+      @toggle-collapse="toggleCollapseSecond"
+      @line-selected="showMatchInFirst"
+    />
   </div>
 </template>
 
-<script>
-import {defineComponent, ref} from "vue";
-import {generateLineCodeLink} from "@/utils/Utils";
-import store from "@/store/store";
-import router from "@/router";
-import TextInformation from "@/components/TextInformation";
-import MatchTable from "@/components/MatchTable";
-import {ComparisonFactory} from "@/model/factories/ComparisonFactory";
-import FilesContainer from "@/components/FilesContainer";
+<script lang="ts">
+import { defineComponent, ref } from "vue";
+import { generateLineCodeLink } from "@/utils/Utils";
+import TextInformation from "@/components/TextInformation.vue";
+import MatchTable from "@/components/MatchTable.vue";
+import { ComparisonFactory } from "@/model/factories/ComparisonFactory";
+import FilesContainer from "@/components/FilesContainer.vue";
+import { useStore } from "vuex";
+import { useRouter } from "vue-router";
+import { Match } from "@/model/Match";
 
 export default defineComponent({
   name: "ComparisonView",
-  components: {FilesContainer, MatchTable, TextInformation},
+  components: { FilesContainer, MatchTable, TextInformation },
   props: {
     firstId: {
       type: String,
+      required: true,
     },
     secondId: {
       type: String,
+      required: true,
     },
   },
   setup(props) {
+    const store = useStore();
+    const router = useRouter();
     /**
      * Name of the comparison file. Comparison files should be named {ID1}-{ID2}
      * @type {string}
      */
-    const fileName1 = props.firstId.concat("-").concat(props.secondId)
-    const fileName2 = props.firstId.concat("-").concat(props.secondId)
+    const fileName1 = props.firstId.concat("-").concat(props.secondId);
+    const fileName2 = props.firstId.concat("-").concat(props.secondId);
 
     let comparison;
     //getting the comparison file based on the used mode (zip, local, single)
     if (store.state.local) {
       try {
-        // eslint-disable-next-line @typescript-eslint/no-var-requires
-        comparison = ComparisonFactory.getComparison(require(`../files/${fileName1}.json`))
+        comparison = ComparisonFactory.getComparison(
+          // eslint-disable-next-line @typescript-eslint/no-var-requires
+          require(`../files/${fileName1}.json`)
+        );
       } catch (exception) {
         try {
-          // eslint-disable-next-line @typescript-eslint/no-var-requires
-          comparison = ComparisonFactory.getComparison(require(`../files/${fileName2}.json`))
+          comparison = ComparisonFactory.getComparison(
+            // eslint-disable-next-line @typescript-eslint/no-var-requires
+            require(`../files/${fileName2}.json`)
+          );
         } catch (exception) {
-          router.back()
+          router.back();
         }
       }
     } else if (store.state.zip) {
-      if (store.state.files[fileName1.concat(".json")]) {
-        comparison = ComparisonFactory.getComparison(JSON.parse(store.state.files[fileName1.concat(".json")]))
-      } else {
-        comparison = ComparisonFactory.getComparison(JSON.parse(store.state.files[fileName2.concat(".json")]))
-      }
+      const getComparisonFileFor = (id1: string, id2: string) => {
+        const index = Object.keys(store.state.files).find(
+          (name) =>
+            name.endsWith(id1.concat("-").concat(id2).concat(".json")) ||
+            name.endsWith(id2.concat("-").concat(id1).concat(".json"))
+        );
+        return index != undefined
+          ? store.state.files[index]
+          : console.log("Could not find comparison file."); // TODO introduce error page to navigate to
+      };
 
+      let comparisonFile = getComparisonFileFor(props.firstId, props.secondId);
+      if (comparisonFile) {
+        comparison = ComparisonFactory.getComparison(
+          JSON.parse(comparisonFile)
+        );
+      }
     } else if (store.state.single) {
-      comparison = ComparisonFactory.getComparison(JSON.parse(store.state.fileString))
+      comparison = ComparisonFactory.getComparison(
+        JSON.parse(store.state.fileString)
+      );
     }
 
-    const filesOfFirst = ref(comparison.filesOfFirstSubmission)
-    const filesOfSecond = ref(comparison.filesOfSecondSubmission)
+    if (!comparison) {
+      console.warn("Could not build comparison file");
+      return;
+    }
+    const filesOfFirst = ref(comparison.filesOfFirstSubmission);
+    const filesOfSecond = ref(comparison.filesOfSecondSubmission);
 
     /**
      * Collapses a file in the first files container.
      * @param title
      */
-    const toggleCollapseFirst = (title) => {
-      filesOfFirst.value[title].collapsed = !filesOfFirst.value[title].collapsed
-    }
+    const toggleCollapseFirst = (title: string) => {
+      const file = filesOfFirst.value.get(title);
+      if (file) {
+        file.collapsed = !file.collapsed;
+      }
+    };
     /**
      * Collapses a file in the second files container.
      * @param title
      */
-    const toggleCollapseSecond = (title) => {
-      filesOfSecond.value[title].collapsed = !filesOfSecond.value[title].collapsed
-    }
+    const toggleCollapseSecond = (title: string) => {
+      const file = filesOfSecond.value.get(title);
+      if (file) {
+        file.collapsed = !file.collapsed;
+      }
+    };
     /**
      * Shows a match in the first files container
      * @param e
@@ -106,12 +168,19 @@ export default defineComponent({
      * @param file
      * @param line
      */
-    const showMatchInFirst = (e, panel, file, line) => {
-      if (!filesOfFirst.value[file].collapsed) {
-        toggleCollapseFirst(file)
+    const showMatchInFirst = (
+      e: unknown,
+      panel: number,
+      file: string,
+      line: number
+    ) => {
+      if (!filesOfFirst.value.get(file)?.collapsed) {
+        toggleCollapseFirst(file);
       }
-      document.getElementById(generateLineCodeLink(panel, file, line)).scrollIntoView()
-    }
+      document
+        .getElementById(generateLineCodeLink(panel, file, line))
+        ?.scrollIntoView();
+    };
     /**
      * Shows a match in the second files container.
      * @param e
@@ -119,24 +188,31 @@ export default defineComponent({
      * @param file
      * @param line
      */
-    const showMatchInSecond = (e, panel, file, line) => {
-      if (!filesOfSecond.value[file].collapsed) {
-        toggleCollapseSecond(file)
+    const showMatchInSecond = (
+      e: unknown,
+      panel: number,
+      file: string,
+      line: number
+    ) => {
+      if (!filesOfSecond.value.get(file)?.collapsed) {
+        toggleCollapseSecond(file);
       }
-      document.getElementById(generateLineCodeLink(panel, file, line)).scrollIntoView()
-    }
-
-    const showMatch = (e, match) => {
-      showMatchInFirst(e, 1, match.firstFile, match.startInFirst)
-      showMatchInSecond(e, 2, match.secondFile, match.startInSecond)
-    }
+      document
+        .getElementById(generateLineCodeLink(panel, file, line))
+        ?.scrollIntoView();
+    };
 
+    const showMatch = (e: unknown, match: Match) => {
+      showMatchInFirst(e, 1, match.firstFile, match.startInFirst);
+      showMatchInSecond(e, 2, match.secondFile, match.startInSecond);
+    };
 
+    const isAnonymous = (id: string) => store.state.anonymous.has(id);
     //Left panel
-    const hideLeftPanel = ref(true)
+    const hideLeftPanel = ref(true);
     const togglePanel = () => {
-      hideLeftPanel.value = !hideLeftPanel.value
-    }
+      hideLeftPanel.value = !hideLeftPanel.value;
+    };
 
     return {
       comparison,
@@ -150,11 +226,12 @@ export default defineComponent({
       showMatchInSecond,
       showMatch,
       togglePanel,
+      isAnonymous,
 
-      store
-    }
-  }
-})
+      store,
+    };
+  },
+});
 </script>
 
 <style scoped>
@@ -234,4 +311,4 @@ h1 {
 #show-button:hover img {
   display: block;
 }
-</style>
+</style>

+ 89 - 64
report-viewer/src/views/FileUploadView.vue

@@ -2,26 +2,28 @@
   Starting view of the application. Presents the options for loading a JPlag report.
 -->
 <template>
-  <div class="container" @dragover.prevent @drop.prevent>
-    <img alt="JPlag" src="@/assets/logo-nobg.png"/>
+  <div class="container" @dragover.prevent @drop.prevent="uploadFile">
+    <img alt="JPlag" src="@/assets/logo-nobg.png" />
     <h1>JPlag Report Viewer</h1>
     <h2>Select an overview or comparison file or a zip to display.</h2>
-    <div class="drop-container" @drop="uploadFile">
-      <p> Drop a .json or .zip file here</p>
+    <div class="drop-container">
+      <p>Drop a .json or .zip on this page</p>
     </div>
     <div v-if="hasLocalFile" class="local-files-container">
       <p class="local-files-text">Detected local files!</p>
-      <button class="local-files-button" @click="continueWithLocal">Continue with local files</button>
+      <button class="local-files-button" @click="continueWithLocal">
+        Continue with local files
+      </button>
     </div>
   </div>
 </template>
 
-<script>
-import {defineComponent} from "vue";
-import jszip from "jszip"
+<script lang="ts">
+import { defineComponent } from "vue";
+import jszip from "jszip";
 import router from "@/router";
 import store from "@/store/store";
-import {getFileExtension} from "@/utils/Utils";
+import { getFileExtension } from "@/utils/Utils";
 
 export default defineComponent({
   name: "FileUploadView",
@@ -29,95 +31,119 @@ export default defineComponent({
     let hasLocalFile;
     //Tries to detect local file. If no files detected, hides local mode from screen.
     try {
-      require("../files/overview.json")
-      hasLocalFile = true
+      require("../files/overview.json");
+      hasLocalFile = true;
     } catch (e) {
-      console.log(e)
-      hasLocalFile = false
+      console.log(e);
+      hasLocalFile = false;
     }
 
-
     const navigateToOverview = () => {
       router.push({
-            name: "OverviewView",
-          }
-      )
-    }
+        name: "OverviewView",
+      });
+    };
 
-    const navigateToComparisonView = (id1, id2) => {
+    const navigateToComparisonView = (firstId: string, secondId: string) => {
       router.push({
         name: "ComparisonView",
-        query: {
-          id1: id1,
-          id2: id2
-        }
-      })
-    }
+        params: {
+          firstId,
+          secondId,
+        },
+      });
+    };
     /**
      * Handles zip file on drop. It extracts the zip and saves each file in the store.
      * @param file
      */
-    const handleZipFile = (file) => {
+    const handleZipFile = (file: File) => {
       jszip.loadAsync(file).then(async (zip) => {
         for (const fileName of Object.keys(zip.files)) {
-          console.log(fileName)
-          await zip.files[fileName].async("string").then(data => {
-                store.commit("saveFile", {fileName: fileName, data: data})
-              }
-          )
+          console.log(fileName);
+          await zip.files[fileName].async("string").then((data) => {
+            store.commit("saveFile", { fileName: fileName, data: data });
+          });
         }
-        store.commit("setLoadingType", {local: false, zip: true, single: false, fileString: ""})
-        navigateToOverview()
-      })
-    }
+        store.commit("setLoadingType", {
+          local: false,
+          zip: true,
+          single: false,
+          fileString: "",
+        });
+        navigateToOverview();
+      });
+    };
     /**
      * Handles a json file on drop. It read the file and passes the file string to next window.
      * @param str
      */
-    const handleJsonFile = (str) => {
-      let json = JSON.parse(str)
+    const handleJsonFile = (str: string) => {
+      let json = JSON.parse(str);
       if (json["submission_folder_path"]) {
-        store.commit("setLoadingType", {local: false, zip: false, single: true, fileString: str})
-        navigateToOverview()
+        store.commit("setLoadingType", {
+          local: false,
+          zip: false,
+          single: true,
+          fileString: str,
+        });
+        navigateToOverview();
       } else if (json["first_submission_id"] && json["second_submission_id"]) {
-        store.commit("setLoadingType", {local: false, zip: false, single: true, fileString: str})
-        navigateToComparisonView(json["first_submission_id"], json["second_submission_id"])
+        store.commit("setLoadingType", {
+          local: false,
+          zip: false,
+          single: true,
+          fileString: str,
+        });
+        navigateToComparisonView(
+          json["first_submission_id"],
+          json["second_submission_id"]
+        );
       }
-    }
+    };
     /**
      * Handles file drop.
      * @param e
      */
-    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()
+    const uploadFile = (e: DragEvent) => {
+      let dropped = e.dataTransfer?.files;
+      if (!dropped) return; // TODO add some kind of error message
+      let files = [...dropped];
+      if (files.length > 1 || files.length === 0) return;
+      let read = new FileReader();
       read.onload = (e) => {
-        let extension = getFileExtension(files[0])
+        let extension = getFileExtension(files[0]);
         if (extension === "zip") {
-          handleZipFile(files[0])
+          handleZipFile(files[0]);
         } else if (extension === "json") {
-          handleJsonFile(e.target.result)
+          let file = e.target?.result;
+          if (!file) return;
+          if (typeof file === "string") {
+            handleJsonFile(file);
+          }
         }
-      }
-      read.readAsText(files[0])
-    }
+      };
+      read.readAsText(files[0]);
+    };
     /**
      * Handles click on Continue with local files.
      */
     const continueWithLocal = () => {
-      store.commit("setLoadingType", {local: true, zip: false, single: false, fileString: ""})
-      navigateToOverview()
-    }
+      store.commit("setLoadingType", {
+        local: true,
+        zip: false,
+        single: false,
+        fileString: "",
+      });
+      navigateToOverview();
+    };
     return {
       continueWithLocal,
       uploadFile,
-      hasLocalFile
-    }
-  }
-})
+      hasLocalFile,
+    };
+  },
+});
 </script>
 
 <style scoped>
@@ -179,11 +205,10 @@ input {
 label {
   font-weight: bold;
   font-size: larger;
-  background: #ECECEC;
+  background: #ececec;
   border-radius: 10px;
   box-shadow: #777777 2px 3px 3px;
   padding: 2%;
   margin-top: 1%;
 }
-
-</style>
+</style>

+ 135 - 73
report-viewer/src/views/OverviewView.vue

@@ -7,25 +7,51 @@
       <h1>JPlag Report</h1>
       <p class="section-title">Main Info:</p>
       <div id="basicInfo">
-        <TextInformation :has-additional-info="hasMoreSubmissionPaths" :value="submissionPathValue"
-                         additional-info-title="" label="Directory path">
-          <p v-for="path in overview.submissionFolderPath" :key="path" :title="path">{{ path }}</p>
+        <TextInformation
+          :has-additional-info="hasMoreSubmissionPaths"
+          :value="submissionPathValue"
+          additional-info-title=""
+          label="Directory path"
+        >
+          <p
+            v-for="path in overview.submissionFolderPath"
+            :key="path"
+            :title="path"
+          >
+            {{ path }}
+          </p>
         </TextInformation>
-        <TextInformation :has-additional-info="true" :value="overview.language" additional-info-title="File extensions:"
-                         label="Language">
+        <TextInformation
+          :has-additional-info="true"
+          :value="overview.language"
+          additional-info-title="File extensions:"
+          label="Language"
+        >
           <p v-for="info in overview.fileExtensions" :key="info">{{ info }}</p>
         </TextInformation>
-        <TextInformation :value="overview.matchSensitivity" label="Match Sensitivity"/>
-        <TextInformation :has-additional-info="true" :value="overview.submissionIds.length"
-                         additional-info-title="Submission IDs:"
-                         label="Submissions">
-          <IDsList :ids="overview.submissionIds" @id-sent="handleId"/>
+        <TextInformation
+          :value="overview.matchSensitivity"
+          label="Match Sensitivity"
+        />
+        <TextInformation
+          :has-additional-info="true"
+          :value="overview.submissionIds.length"
+          additional-info-title="Submission IDs:"
+          label="Submissions"
+        >
+          <IDsList :ids="overview.submissionIds" @id-sent="handleId" />
         </TextInformation>
-        <TextInformation :value="overview.dateOfExecution" label="Date of execution"/>
-        <TextInformation :value="overview.durationOfExecution" label="Duration (in ms)"/>
+        <TextInformation
+          :value="overview.dateOfExecution"
+          label="Date of execution"
+        />
+        <TextInformation
+          :value="overview.durationOfExecution"
+          label="Duration (in ms)"
+        />
       </div>
       <div id="logo-section">
-        <img id="logo" alt="JPlag" src="@/assets/logo-nobg.png">
+        <img id="logo" alt="JPlag" src="@/assets/logo-nobg.png" />
       </div>
     </div>
 
@@ -33,55 +59,88 @@
       <div id="metrics">
         <p class="section-title">Metric:</p>
         <div id="metrics-list">
-          <MetricButton v-for="(metric, i) in overview.metrics" :id="metric.metricName"
-                        :key="metric.metricName"
-                        :is-selected="selectedMetric[i]"
-                        :metric-name="metric.metricName"
-                        :metric-threshold="metric.metricThreshold"
-                        @click="selectMetric(i)"/>
+          <MetricButton
+            v-for="(metric, index) in overview.metrics"
+            :id="metric.metricName"
+            :key="metric.metricName"
+            :is-selected="selectedMetric[index]"
+            :metric-name="metric.metricName"
+            :metric-threshold="metric.metricThreshold"
+            @click="selectMetric(index)"
+          />
         </div>
       </div>
       <p class="section-title">Distribution:</p>
-      <DistributionDiagram :distribution="distributions[selectedMetricIndex]" class="full-width"/>
+      <DistributionDiagram
+        :distribution="distributions[selectedMetricIndex]"
+        class="full-width"
+      />
     </div>
     <div class="column-container" style="width: 35%">
       <p class="section-title">Top Comparisons:</p>
       <div id="comparisonsList">
-        <ComparisonsTable :clusters="overview.clusters" :top-comparisons="topComps[selectedMetricIndex]"/>
+        <ComparisonsTable
+          :clusters="overview.clusters"
+          :top-comparisons="topComps[selectedMetricIndex]"
+        />
       </div>
     </div>
   </div>
 </template>
 
-<script>
-import {defineComponent, ref} from "vue";
-import store from "@/store/store";
+<script lang="ts">
+import { computed, defineComponent, Ref, ref } from "vue";
 import router from "@/router";
-import TextInformation from "../components/TextInformation";
-import DistributionDiagram from "@/components/DistributionDiagram";
-import MetricButton from "@/components/MetricButton";
-import ComparisonsTable from "@/components/ComparisonsTable";
-import {OverviewFactory} from "@/model/factories/OverviewFactory";
-import IDsList from "@/components/IDsList";
+import TextInformation from "../components/TextInformation.vue";
+import DistributionDiagram from "@/components/DistributionDiagram.vue";
+import MetricButton from "@/components/MetricButton.vue";
+import ComparisonsTable from "@/components/ComparisonsTable.vue";
+import { OverviewFactory } from "@/model/factories/OverviewFactory";
+import IDsList from "@/components/IDsList.vue";
+import { useStore } from "vuex";
+import { Overview } from "@/model/Overview";
+import { ComparisonListElement } from "@/model/ComparisonListElement";
 
 export default defineComponent({
   name: "OverviewView",
-  components: {IDsList, ComparisonsTable, DistributionDiagram, MetricButton, TextInformation},
+  components: {
+    IDsList,
+    ComparisonsTable,
+    DistributionDiagram,
+    MetricButton,
+    TextInformation,
+  },
   setup() {
-    let overview;
-    //Gets the overview file based on the used mode (zip, local, single).
-    if (store.state.local) {
-      try {
-        // eslint-disable-next-line @typescript-eslint/no-var-requires
-        overview = OverviewFactory.getOverview(require("../files/overview.json"))
-      } catch (e) {
-        router.back()
+    const store = useStore();
+    const overviewFile = computed(() => {
+      const index = Object.keys(store.state.files).find((name) =>
+        name.endsWith("overview.json")
+      );
+      return index != undefined
+        ? store.state.files[index]
+        : console.log("Could not find overview.json"); // TODO introduce error page to navigate to
+    });
+
+    const getOverview = (): Overview => {
+      let temp!: Overview;
+      //Gets the overview file based on the used mode (zip, local, single).
+      if (store.state.local) {
+        try {
+          // eslint-disable-next-line @typescript-eslint/no-var-requires
+          temp = OverviewFactory.getOverview(require("../files/overview.json"));
+        } catch (e) {
+          router.back();
+        }
+      } else if (store.state.zip) {
+        const overviewJson = JSON.parse(overviewFile.value);
+        temp = OverviewFactory.getOverview(overviewJson);
+      } else if (store.state.single) {
+        temp = OverviewFactory.getOverview(JSON.parse(store.state.fileString));
       }
-    } else if (store.state.zip) {
-      overview = OverviewFactory.getOverview(JSON.parse(store.state.files["overview.json"]))
-    } else if (store.state.single) {
-      overview = OverviewFactory.getOverview(JSON.parse(store.state.fileString))
-    }
+      return temp;
+    };
+
+    let overview = getOverview();
 
     /**
      * Handles the selection of an Id to anonymize.
@@ -89,58 +148,63 @@ export default defineComponent({
      * If a single id is provided it hides all of the other ids except for the chosen one.
      * @param id
      */
-    const handleId = (id) => {
+    const handleId = (id: string) => {
       if (id.length === overview.submissionIds.length) {
         if (store.state.anonymous.size > 0) {
-          store.commit("resetAnonymous")
+          store.commit("resetAnonymous");
         } else {
-          store.commit("addAnonymous", id)
+          store.commit("addAnonymous", id);
         }
       } else {
         if (store.state.anonymous.has(id[0])) {
-          store.commit("removeAnonymous", id)
+          store.commit("removeAnonymous", id);
         } else {
           if (store.state.anonymous.size === 0) {
-            store.commit("addAnonymous", overview.submissionIds.filter(s => s !== id[0]))
+            store.commit(
+              "addAnonymous",
+              overview.submissionIds.filter((s) => s !== id[0])
+            );
           } else {
-            store.commit("addAnonymous", id)
+            store.commit("addAnonymous", id);
           }
         }
       }
-    }
-
+    };
 
     //Metrics
     /**
      * Current metric to display distribution and comparisons for.
      * @type {Ref<UnwrapRef<boolean[]>>}
      */
-    let selectedMetric = ref(overview.metrics.map(() => false))
+    let selectedMetric = ref(overview.metrics.map(() => false));
     /**
      * Index of current selected metric. Used to obtain information for the metric from the distribution and top
      * comparisons array.
      * @type {Ref<UnwrapRef<number>>}
      */
-    let selectedMetricIndex = ref(0)
+    let selectedMetricIndex = ref(0);
     selectedMetric.value[0] = true;
 
-    const selectMetric = (metric) => {
+    const selectMetric = (metric: number) => {
       selectedMetric.value = selectedMetric.value.map(() => {
-        return false
-      })
-      selectedMetric.value[metric] = true
-      selectedMetricIndex.value = metric
-    }
+        return false;
+      });
+      selectedMetric.value[metric] = true;
+      selectedMetricIndex.value = metric;
+    };
 
     //Distribution
-    let distributions = ref(overview.metrics.map((m) => m.distribution))
+    let distributions = ref(overview.metrics.map((m) => m.distribution));
 
     //Top Comparisons
-    let topComps = ref(overview.metrics.map((m) => m.comparisons))
+    let topComps: Ref<Array<Array<ComparisonListElement>>> = ref(
+      overview.metrics.map((m) => m.comparisons)
+    );
 
     const hasMoreSubmissionPaths = overview.submissionFolderPath.length > 1;
-    const submissionPathValue = hasMoreSubmissionPaths ? "Click arrow to see all paths"
-        : overview.submissionFolderPath[0]
+    const submissionPathValue = hasMoreSubmissionPaths
+      ? "Click arrow to see all paths"
+      : overview.submissionFolderPath[0];
 
     return {
       overview,
@@ -152,10 +216,10 @@ export default defineComponent({
       submissionPathValue,
       handleId,
       selectMetric,
-      store
-    }
-  }
-})
+      store,
+    };
+  },
+});
 </script>
 
 <style scoped>
@@ -168,9 +232,9 @@ h1 {
 hr {
   border: 0;
   height: 2px;
-  background: linear-gradient(to right, #EDF2FB, transparent, transparent);
+  background: linear-gradient(to right, #edf2fb, transparent, transparent);
   width: 100%;
-  box-shadow: #D7E3FC 0 1px;
+  box-shadow: #d7e3fc 0 1px;
 }
 
 .container {
@@ -244,6 +308,4 @@ hr {
 #logo {
   flex-shrink: 2;
 }
-
-
-</style>
+</style>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است