diff --git a/.github/workflows/files/progpedia.zip b/.github/workflows/files/progpedia.zip new file mode 100644 index 000000000..310d196fa Binary files /dev/null and b/.github/workflows/files/progpedia.zip differ diff --git a/.github/workflows/report-viewer-demo.yml b/.github/workflows/report-viewer-demo.yml new file mode 100644 index 000000000..6158b40ef --- /dev/null +++ b/.github/workflows/report-viewer-demo.yml @@ -0,0 +1,113 @@ +name: Report Viewer Demo Deployment + +on: + workflow_dispatch: # Use this to dispatch from the Actions Tab + push: + branches: + - main + +jobs: + build-jar: + runs-on: ubuntu-latest + + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: 'temurin' + + - name: Build Assembly + run: mvn clean package assembly:single + + - name: Upload Assembly + uses: actions/upload-artifact@v3 + with: + name: "JPlag" + path: "cli/target/jplag-*-jar-with-dependencies.jar" + + + run-example: + needs: build-jar + runs-on: ubuntu-latest + + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: 'temurin' + + - name: Get JAR + uses: actions/download-artifact@v3 + with: + name: JPlag + + - name: Copy and unzip submissions + run: unzip ./.github/workflows/files/progpedia.zip + + - name: Rename jar + run: mv *.jar ./jplag.jar + + - name: Run JPlag + run: java -jar jplag.jar ACCEPTED -bc base -r example + + - name: Upload Result + uses: actions/upload-artifact@v3 + with: + name: "Result" + path: "example.zip" + + + build-and-deploy: + needs: run-example + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Set version of Report Viewer + shell: bash + run: | + VERSION=$(grep "" pom.xml | grep -oPm1 "(?<=)[^-|<]+") + MAJOR=$(echo $VERSION | cut -d '.' -f 1) + MINOR=$(echo $VERSION | cut -d '.' -f 2) + PATCH=$(echo $VERSION | cut -d '.' -f 3) + json=$(cat report-viewer/src/version.json) + json=$(echo "$json" | jq --arg MAJOR "$MAJOR" --arg MINOR "$MINOR" --arg PATCH "$PATCH" '.report_viewer_version |= { "major": $MAJOR | tonumber, "minor": $MINOR | tonumber, "patch": $PATCH | tonumber }') + echo "$json" > report-viewer/src/version.json + echo "Version of Report Viewer:" + cat report-viewer/src/version.json + + - name: Download Results + uses: actions/download-artifact@v3 + with: + name: Result + path: report-viewer/public + + - name: Install and Build 🔧 + working-directory: report-viewer + run: | + npm install + npm run build-demo + + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@v4.5.0 + with: + branch: gh-pages + folder: report-viewer/dist + repository-name: JPlag/Demo + token: ${{ secrets.SDQ_DEV_DEPLOY_TOKEN }} + clean: true + single-commit: true + diff --git a/cli/src/main/java/de/jplag/cli/CLI.java b/cli/src/main/java/de/jplag/cli/CLI.java index 9507a9a80..be94fa083 100644 --- a/cli/src/main/java/de/jplag/cli/CLI.java +++ b/cli/src/main/java/de/jplag/cli/CLI.java @@ -78,6 +78,8 @@ public static void main(String[] args) { JPlagResult result = JPlag.run(options); ReportObjectFactory reportObjectFactory = new ReportObjectFactory(); reportObjectFactory.createAndSaveReport(result, cli.getResultFolder()); + + OutputFileGenerator.generateCsvOutput(result, new File(cli.getResultFolder()), cli.options); } } catch (ExitException exception) { logger.error(exception.getMessage()); // do not pass exception here to keep log clean diff --git a/cli/src/main/java/de/jplag/cli/CliOptions.java b/cli/src/main/java/de/jplag/cli/CliOptions.java index 44d04f1da..da249342c 100644 --- a/cli/src/main/java/de/jplag/cli/CliOptions.java +++ b/cli/src/main/java/de/jplag/cli/CliOptions.java @@ -88,6 +88,9 @@ public static class Advanced { "--similarity-threshold"}, description = "Comparison similarity threshold [0.0-1.0]: All comparisons above this threshold will " + "be saved (default: ${DEFAULT-VALUE})%n") public double similarityThreshold = JPlagOptions.DEFAULT_SIMILARITY_THRESHOLD; + + @Option(names = "--csv-export", description = "If present, a csv export will be generated in addition to the zip file.") + public boolean csvExport = false; } public static class Clustering { diff --git a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java new file mode 100644 index 000000000..028361346 --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java @@ -0,0 +1,36 @@ +package de.jplag.cli; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.jplag.JPlagResult; +import de.jplag.csv.comparisons.CsvComparisonOutput; + +public final class OutputFileGenerator { + private static final Logger logger = LoggerFactory.getLogger(OutputFileGenerator.class); + + private OutputFileGenerator() { + // Prevents default constructor + } + + /** + * Exports the given result as csvs, if the csvExport is activated in the options. Both a full and an anonymized version + * will be written. + * @param result The result to export + * @param outputRoot The root folder for the output + * @param options The cli options + */ + public static void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options) { + if (options.advanced.csvExport) { + try { + CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), false, outputRoot, "results"); + CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), true, outputRoot, "results-anonymous"); + } catch (IOException e) { + logger.warn("Could not write csv results", e); + } + } + } +} diff --git a/core/src/main/java/de/jplag/csv/CsvDataMapper.java b/core/src/main/java/de/jplag/csv/CsvDataMapper.java new file mode 100644 index 000000000..67844a6d4 --- /dev/null +++ b/core/src/main/java/de/jplag/csv/CsvDataMapper.java @@ -0,0 +1,22 @@ +package de.jplag.csv; + +import java.util.Optional; + +/** + * Provides mappings for csv rows and optionally names for the columns. Needs to always return the same number of + * columns. + * @param The type of data that is mapped + */ +public interface CsvDataMapper { + /** + * Provides the cell values for one row + * @param value The original object + * @return The cell values + */ + String[] provideData(T value); + + /** + * @return The names of the columns if present + */ + Optional getTitleRow(); +} diff --git a/core/src/main/java/de/jplag/csv/CsvPrinter.java b/core/src/main/java/de/jplag/csv/CsvPrinter.java new file mode 100644 index 000000000..089eb4fe8 --- /dev/null +++ b/core/src/main/java/de/jplag/csv/CsvPrinter.java @@ -0,0 +1,144 @@ +package de.jplag.csv; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import de.jplag.util.FileUtils; + +/** + * Prints a csv according to the specification in + * .... If you need to deviate from this + * definition slightly you can modify the line end and separator characters. + * @param + */ +public class CsvPrinter { + private static final char DEFAULT_SEPARATOR = ','; + private static final String DEFAULT_LINE_END = "\r\n"; // not System.lineSeparator(), because of csv specification + private static final char LITERAL = '"'; + + private final CsvDataMapper dataSource; + private final List data; + + private char separator; + private String lineEnd; + + /** + * @param dataSource The data source used to map the given object to rows. + */ + public CsvPrinter(CsvDataMapper dataSource) { + this.dataSource = dataSource; + this.data = new ArrayList<>(); + + this.separator = DEFAULT_SEPARATOR; + this.lineEnd = DEFAULT_LINE_END; + } + + /** + * Adds a new row to this csv + * @param value the value to add + */ + public void addRow(T value) { + this.data.add(this.dataSource.provideData(value)); + } + + /** + * Adds multiple rows to this csv + * @param values The values to add + */ + public void addRows(Collection values) { + values.forEach(this::addRow); + } + + /** + * Changes the separator between cells + * @param separator The new separator + */ + public void setSeparator(char separator) { + this.separator = separator; + } + + /** + * Sets the string to separate lines with + * @param lineEnd the new line end + */ + public void setLineEnd(String lineEnd) { + this.lineEnd = lineEnd; + } + + /** + * Prints this csv with all current data to a file + * @param file The file to write + * @throws IOException on io errors + */ + public void printToFile(File file) throws IOException { + try (Writer writer = FileUtils.openFileWriter(file)) { + this.printCsv(writer); + } + } + + public String printToString() throws IOException { + String csv; + + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + try (Writer writer = new OutputStreamWriter(outputStream)) { + this.printCsv(writer); + } + + csv = outputStream.toString(); + } + + return csv; + } + + private void printCsv(Writer writer) throws IOException { + this.writeTitleRow(writer); + + for (String[] datum : this.data) { + this.printRow(writer, datum); + } + } + + private void writeTitleRow(Writer writer) throws IOException { + Optional titleRow = this.dataSource.getTitleRow(); + if (titleRow.isPresent()) { + this.printRow(writer, titleRow.get()); + } + } + + private void printRow(Writer writer, String[] data) throws IOException { + Iterator dataIterator = Arrays.stream(data).iterator(); + + if (dataIterator.hasNext()) { + printCell(writer, dataIterator.next()); + } + + while (dataIterator.hasNext()) { + writer.write(this.separator); + printCell(writer, dataIterator.next()); + } + + writer.write(this.lineEnd); + } + + private void printCell(Writer writer, String cellValue) throws IOException { + boolean literalsNeeded = cellValue.contains(String.valueOf(LITERAL)); + String actualValue = cellValue; + if (literalsNeeded) { + writer.write(LITERAL); + actualValue = actualValue.replace("\"", "\"\""); + } + writer.write(actualValue); + if (literalsNeeded) { + writer.write(LITERAL); + } + } +} diff --git a/core/src/main/java/de/jplag/csv/CsvValue.java b/core/src/main/java/de/jplag/csv/CsvValue.java new file mode 100644 index 000000000..973b60f0f --- /dev/null +++ b/core/src/main/java/de/jplag/csv/CsvValue.java @@ -0,0 +1,18 @@ +package de.jplag.csv; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used with {@link ReflectiveCsvDataMapper} to identify fields and methods, that should be used for the csv. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface CsvValue { + /** + * The index of the csv field. Has to be used as the compiler sometimes changes the order of fields/methods + */ + int value(); +} diff --git a/core/src/main/java/de/jplag/csv/HardcodedCsvDataMapper.java b/core/src/main/java/de/jplag/csv/HardcodedCsvDataMapper.java new file mode 100644 index 000000000..89b78edde --- /dev/null +++ b/core/src/main/java/de/jplag/csv/HardcodedCsvDataMapper.java @@ -0,0 +1,58 @@ +package de.jplag.csv; + +import java.util.Optional; +import java.util.function.Function; + +/** + * Can be used to hardcode mappings to csv. Uses the given function to map values. + * @param The mapped type. + */ +public class HardcodedCsvDataMapper implements CsvDataMapper { + private final Function mappingFunction; + private final int columnCount; + + private String[] titles; + + /** + * @param columnCount The number of columns + * @param mappingFunction The function returning the column values. Must return as many values as specified in + * columnCount + */ + public HardcodedCsvDataMapper(int columnCount, Function mappingFunction) { + this.mappingFunction = mappingFunction; + this.columnCount = columnCount; + this.titles = null; + } + + /** + * @param columnCount The number of columns + * @param mappingFunction The function returning the column values. Must return as many values as specified in + * columnCount + * @param titles The titles for the csv + */ + public HardcodedCsvDataMapper(int columnCount, Function mappingFunction, String[] titles) { + this(columnCount, mappingFunction); + this.titles = titles; + } + + @Override + public String[] provideData(T value) { + Object[] values = this.mappingFunction.apply(value); + + if (values.length != this.columnCount) { + throw new IllegalStateException("You need to return the appropriate number of columns"); + } + + String[] data = new String[this.columnCount]; + for (int i = 0; i < this.columnCount; i++) { + data[i] = String.valueOf(values[i]); + } + + return data; + } + + @Override + public Optional getTitleRow() { + return Optional.ofNullable(this.titles); + } +} diff --git a/core/src/main/java/de/jplag/csv/ReflectiveCsvDataMapper.java b/core/src/main/java/de/jplag/csv/ReflectiveCsvDataMapper.java new file mode 100644 index 000000000..1a442521f --- /dev/null +++ b/core/src/main/java/de/jplag/csv/ReflectiveCsvDataMapper.java @@ -0,0 +1,89 @@ +package de.jplag.csv; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import org.apache.commons.math3.util.Pair; + +/** + * Mapped data automatically based on the exposed fields and methods. + * @param The mapped type. Mark included methods and fields with @{@link CsvValue} + */ +public class ReflectiveCsvDataMapper implements CsvDataMapper { + private final List>> values; + private String[] titles; + + /** + * @param type The mapped type. + */ + public ReflectiveCsvDataMapper(Class type) { + this.values = new ArrayList<>(); + + for (Field field : type.getFields()) { + if (field.getAnnotation(CsvValue.class) != null) { + this.values.add(new Pair<>(field.getAnnotation(CsvValue.class).value(), field::get)); + } + } + + for (Method method : type.getMethods()) { + if (method.getAnnotation(CsvValue.class) != null) { + if (method.getParameters().length != 0) { + throw new IllegalStateException( + String.format("Method %s in %s must not have parameters to be a csv value", method.getName(), type.getName())); + } + if (method.getReturnType().equals(Void.class)) { + throw new IllegalStateException( + String.format("Method %s in %s must not return void to be a csv value", method.getName(), type.getName())); + } + + this.values.add(new Pair<>(method.getAnnotation(CsvValue.class).value(), method::invoke)); + } + } + + this.values.sort(Comparator.comparing(Pair::getKey)); + this.titles = null; + } + + /** + * @param type The mapped type + * @param titles The titles for the csv. Must be as many as @{@link CsvValue} annotation in the given type. + */ + public ReflectiveCsvDataMapper(Class type, String[] titles) { + this(type); + + if (this.values.size() != titles.length) { + throw new IllegalArgumentException("Csv data must have the same number of tiles and values per row."); + } + + this.titles = titles; + } + + @Override + public String[] provideData(T value) { + String[] data = new String[this.values.size()]; + + for (int i = 0; i < data.length; i++) { + try { + data[i] = String.valueOf(this.values.get(i).getValue().get(value)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + + return data; + } + + @Override + public Optional getTitleRow() { + return Optional.ofNullable(this.titles); + } + + private interface GetterFunction { + Object get(T instance) throws IllegalAccessException, InvocationTargetException; + } +} diff --git a/core/src/main/java/de/jplag/csv/comparisons/CsvComparisonData.java b/core/src/main/java/de/jplag/csv/comparisons/CsvComparisonData.java new file mode 100644 index 000000000..b275d840a --- /dev/null +++ b/core/src/main/java/de/jplag/csv/comparisons/CsvComparisonData.java @@ -0,0 +1,14 @@ +package de.jplag.csv.comparisons; + +import de.jplag.csv.CsvValue; + +/** + * Comparison data for writing to a csv. + * @param firstSubmissionName The name of the first submission + * @param secondSubmissionName The name of the second submission + * @param averageSimilarity The average similarity + * @param maxSimilarity The maximum similarity + */ +public record CsvComparisonData(@CsvValue(1) String firstSubmissionName, @CsvValue(2) String secondSubmissionName, + @CsvValue(3) double averageSimilarity, @CsvValue(4) double maxSimilarity) { +} diff --git a/core/src/main/java/de/jplag/csv/comparisons/CsvComparisonOutput.java b/core/src/main/java/de/jplag/csv/comparisons/CsvComparisonOutput.java new file mode 100644 index 000000000..013ee2f7b --- /dev/null +++ b/core/src/main/java/de/jplag/csv/comparisons/CsvComparisonOutput.java @@ -0,0 +1,61 @@ +package de.jplag.csv.comparisons; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import de.jplag.JPlagComparison; +import de.jplag.csv.CsvDataMapper; +import de.jplag.csv.CsvPrinter; +import de.jplag.csv.HardcodedCsvDataMapper; +import de.jplag.csv.ReflectiveCsvDataMapper; +import de.jplag.options.SimilarityMetric; + +/** + * Frontend for writing the result comparisons as a csv. + */ +public class CsvComparisonOutput { + private static final String[] titles = new String[] {"submissionName1", "submissionName2", "averageSimilarity", "maxSimilarity"}; + + private CsvComparisonOutput() { + } + + /** + * Writes the comparisons as a csv + * @param comparisons The list of comparisons + * @param anonymize If true only random ids will be printed and an additional file will contain the actual names + * @param directory The directory to write into + * @param fileName The base name for the file without ".csv" + */ + public static void writeCsvResults(List comparisons, boolean anonymize, File directory, String fileName) throws IOException { + NameMapper mapper = new NameMapper.IdentityMapper(); + directory.mkdirs(); + + if (anonymize) { + mapper = new NameMapperIncrementalIds(); + } + + CsvDataMapper dataMapper = new ReflectiveCsvDataMapper<>(CsvComparisonData.class, titles); + CsvPrinter printer = new CsvPrinter<>(dataMapper); + + for (JPlagComparison comparison : comparisons) { + double average = SimilarityMetric.AVG.applyAsDouble(comparison); + double max = SimilarityMetric.MAX.applyAsDouble(comparison); + String firstName = mapper.map(comparison.firstSubmission().getName()); + String secondName = mapper.map(comparison.secondSubmission().getName()); + printer.addRow(new CsvComparisonData(firstName, secondName, average, max)); + } + + printer.printToFile(new File(directory, fileName + ".csv")); + + if (anonymize) { + List> nameMap = mapper.getNameMap(); + CsvDataMapper> namesMapMapper = new HardcodedCsvDataMapper<>(2, it -> new String[] {it.getValue(), it.getKey()}, + new String[] {"id", "realName"}); + CsvPrinter> namesPrinter = new CsvPrinter<>(namesMapMapper); + namesPrinter.addRows(nameMap); + namesPrinter.printToFile(new File(directory, fileName + "-names.csv")); + } + } +} diff --git a/core/src/main/java/de/jplag/csv/comparisons/NameMapper.java b/core/src/main/java/de/jplag/csv/comparisons/NameMapper.java new file mode 100644 index 000000000..d514eef5d --- /dev/null +++ b/core/src/main/java/de/jplag/csv/comparisons/NameMapper.java @@ -0,0 +1,37 @@ +package de.jplag.csv.comparisons; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Maps the names of submissions for csv printing. Used to anonymize data if needed. + */ +public interface NameMapper { + /** + * Maps the original name to the one that should be printed. + * @param original The original name + * @return The name for printing + */ + String map(String original); + + /** + * @return The list of mappings. + */ + List> getNameMap(); + + /** + * Simple implementation, that does not change the names. + */ + class IdentityMapper implements NameMapper { + @Override + public String map(String original) { + return original; + } + + @Override + public List> getNameMap() { + return Collections.emptyList(); + } + } +} diff --git a/core/src/main/java/de/jplag/csv/comparisons/NameMapperIncrementalIds.java b/core/src/main/java/de/jplag/csv/comparisons/NameMapperIncrementalIds.java new file mode 100644 index 000000000..c3e0aaa29 --- /dev/null +++ b/core/src/main/java/de/jplag/csv/comparisons/NameMapperIncrementalIds.java @@ -0,0 +1,42 @@ +package de.jplag.csv.comparisons; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Maps the real names of submissions to incremental ids. The ids will be in order of the queried new names. + */ +public class NameMapperIncrementalIds implements NameMapper { + private final Map map; + private int nextId; + + /** + * New instance + */ + public NameMapperIncrementalIds() { + this.map = new HashMap<>(); + this.nextId = 0; + } + + private String newId() { + String id = String.valueOf(this.nextId++); + + if (this.map.containsKey(id)) { + return newId(); + } + + return id; + } + + @Override + public String map(String original) { + this.map.computeIfAbsent(original, ignore -> this.newId()); + return this.map.get(original); + } + + @Override + public List> getNameMap() { + return this.map.entrySet().stream().toList(); + } +} diff --git a/core/src/test/java/de/jplag/csv/CsvPrinterTest.java b/core/src/test/java/de/jplag/csv/CsvPrinterTest.java new file mode 100644 index 000000000..4a2673274 --- /dev/null +++ b/core/src/test/java/de/jplag/csv/CsvPrinterTest.java @@ -0,0 +1,35 @@ +package de.jplag.csv; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class CsvPrinterTest { + private static final String EXPECTED_CSV_TEXT = "1,test1\r\n2,\"test2,\"\"x\"\"\"\r\n"; + private static final List TEST_ITEMS = List.of(new CsvTestItem(1, "test1"), new CsvTestItem(2, "test2,\"x\"")); + + @Test + void testPrintWithReflectiveMapper() throws IOException { + CsvDataMapper mapper = new ReflectiveCsvDataMapper<>(CsvTestItem.class); + CsvPrinter printer = new CsvPrinter<>(mapper); + + printer.addRows(TEST_ITEMS); + + Assertions.assertEquals(EXPECTED_CSV_TEXT, printer.printToString()); + } + + @Test + void testPrintWithHardcodedMapper() throws IOException { + CsvDataMapper mapper = new HardcodedCsvDataMapper<>(2, item -> new Object[] {item.number(), item.text()}); + CsvPrinter printer = new CsvPrinter<>(mapper); + + printer.addRows(TEST_ITEMS); + + Assertions.assertEquals(EXPECTED_CSV_TEXT, printer.printToString()); + } + + private record CsvTestItem(@CsvValue(1) int number, @CsvValue(2) String text) { + } +} diff --git a/report-viewer/package-lock.json b/report-viewer/package-lock.json index d52ab3fa1..e8dd9d98f 100644 --- a/report-viewer/package-lock.json +++ b/report-viewer/package-lock.json @@ -27,9 +27,9 @@ }, "devDependencies": { "@playwright/test": "^1.40.1", - "@rushstack/eslint-patch": "^1.6.1", + "@rushstack/eslint-patch": "^1.7.0", "@types/jsdom": "^21.1.6", - "@types/node": "^18.19.4", + "@types/node": "^18.19.8", "@vitejs/plugin-vue": "^5.0.3", "@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-typescript": "^12.0.0", @@ -37,16 +37,16 @@ "@vue/tsconfig": "^0.5.1", "autoprefixer": "^10.4.16", "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.19.2", + "eslint-plugin-vue": "^9.20.1", "husky": "^8.0.0", "jsdom": "^23.2.0", "lint-staged": "^15.2.0", "npm-run-all": "^4.1.5", "postcss": "^8.4.31", "prettier": "^3.1.1", - "prettier-plugin-tailwindcss": "^0.5.9", + "prettier-plugin-tailwindcss": "^0.5.11", "tailwindcss": "^3.4.0", - "typescript": "^5.3.2", + "typescript": "^5.3.3", "vite": "^5.0.11", "vitest": "^1.1.1", "vue-tsc": "^1.8.25" @@ -928,9 +928,9 @@ ] }, "node_modules/@rushstack/eslint-patch": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz", - "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.0.tgz", + "integrity": "sha512-Jh4t/593gxs0lJZ/z3NnasKlplXT2f+4y/LZYuaKZW5KAaiVFL/fThhs+17EbUd53jUVJ0QudYCBGbN/psvaqg==", "dev": true }, "node_modules/@sinclair/typebox": { @@ -973,9 +973,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.19.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.4.tgz", - "integrity": "sha512-xNzlUhzoHotIsnFoXmJB+yWmBvFZgKCI9TtPIEdYIMM1KWfwuY8zh7wvc1u1OAXlC7dlf6mZVx/s+Y5KfFz19A==", + "version": "18.19.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz", + "integrity": "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -2766,9 +2766,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.19.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.19.2.tgz", - "integrity": "sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA==", + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.20.1.tgz", + "integrity": "sha512-GyCs8K3lkEvoyC1VV97GJhP1SvqsKCiWGHnbn0gVUYiUhaH2+nB+Dv1uekv1THFMPbBfYxukrzQdltw950k+LQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -2776,7 +2776,7 @@ "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.13", "semver": "^7.5.4", - "vue-eslint-parser": "^9.3.1", + "vue-eslint-parser": "^9.4.0", "xml-name-validator": "^4.0.0" }, "engines": { @@ -5381,9 +5381,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.9.tgz", - "integrity": "sha512-9x3t1s2Cjbut2QiP+O0mDqV3gLXTe2CgRlQDgucopVkUdw26sQi53p/q4qvGxMLBDfk/dcTV57Aa/zYwz9l8Ew==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.11.tgz", + "integrity": "sha512-AvI/DNyMctyyxGOjyePgi/gqj5hJYClZ1avtQvLlqMT3uDZkRbi4HhGUpok3DRzv9z7Lti85Kdj3s3/1CeNI0w==", "dev": true, "engines": { "node": ">=14.21.3" @@ -6633,9 +6633,9 @@ } }, "node_modules/typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -7661,9 +7661,9 @@ } }, "node_modules/vue-eslint-parser": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz", - "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.0.tgz", + "integrity": "sha512-7KsNBb6gHFA75BtneJsoK/dbZ281whUIwFYdQxA68QrCrGMXYzUMbPDHGcOQ0OocIVKrWSKWXZ4mL7tonCXoUw==", "dev": true, "dependencies": { "debug": "^4.3.4", diff --git a/report-viewer/package.json b/report-viewer/package.json index 4c6ff70c3..c1952bc34 100644 --- a/report-viewer/package.json +++ b/report-viewer/package.json @@ -12,6 +12,7 @@ "build-only": "vite build", "build-prod": "run-p type-check && vite build --mode prod", "build-dev": "run-p type-check && vite build --mode dev", + "build-demo": "run-p type-check && vite build --mode demo", "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore --max-warnings 0", "format": "prettier --write src/", @@ -37,9 +38,9 @@ }, "devDependencies": { "@playwright/test": "^1.40.1", - "@rushstack/eslint-patch": "^1.6.1", + "@rushstack/eslint-patch": "^1.7.0", "@types/jsdom": "^21.1.6", - "@types/node": "^18.19.4", + "@types/node": "^18.19.8", "@vitejs/plugin-vue": "^5.0.3", "@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-typescript": "^12.0.0", @@ -47,16 +48,16 @@ "@vue/tsconfig": "^0.5.1", "autoprefixer": "^10.4.16", "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.19.2", + "eslint-plugin-vue": "^9.20.1", "husky": "^8.0.0", "jsdom": "^23.2.0", "lint-staged": "^15.2.0", "npm-run-all": "^4.1.5", "postcss": "^8.4.31", "prettier": "^3.1.1", - "prettier-plugin-tailwindcss": "^0.5.9", + "prettier-plugin-tailwindcss": "^0.5.11", "tailwindcss": "^3.4.0", - "typescript": "^5.3.2", + "typescript": "^5.3.3", "vite": "^5.0.11", "vitest": "^1.1.1", "vue-tsc": "^1.8.25" diff --git a/report-viewer/src/components/ClusterGraph.vue b/report-viewer/src/components/ClusterGraph.vue index 7912a1a67..a2b6d1a69 100644 --- a/report-viewer/src/components/ClusterGraph.vue +++ b/report-viewer/src/components/ClusterGraph.vue @@ -49,11 +49,7 @@ Chart.register(GraphController) Chart.register(GraphChart) const keys = computed(() => Array.from(props.cluster.members.keys())) -const labels = computed(() => - Array.from(keys.value).map((m) => - store().state.anonymous.has(m) ? 'Hidden' : store().submissionDisplayName(m) ?? m - ) -) +const labels = computed(() => Array.from(keys.value).map((m) => store().getDisplayName(m))) const edges = computed(() => { const edges: { source: number; target: number }[] = [] props.cluster.members.forEach((member1, key1) => { diff --git a/report-viewer/src/components/ClusterRadarChart.vue b/report-viewer/src/components/ClusterRadarChart.vue index 8def4ff28..dbe8136b1 100644 --- a/report-viewer/src/components/ClusterRadarChart.vue +++ b/report-viewer/src/components/ClusterRadarChart.vue @@ -13,7 +13,7 @@ @selectionChanged="(value) => (idOfShownSubmission = value)" />
- +

@@ -56,6 +56,7 @@ import { Chart, registerables } from 'chart.js' import ChartDataLabels from 'chartjs-plugin-datalabels' import DropDownSelector from './DropDownSelector.vue' import { graphColors } from '@/utils/ColorUtils' +import { store } from '@/stores/store' Chart.register(...registerables) Chart.register(ChartDataLabels) @@ -81,25 +82,21 @@ const idOfShownSubmission = ref(selectedOptions.value.length > 0 ? selectedOptio const memberCount = computed(() => props.cluster.members.size) -/** - * @param member The member to create the labels for. - * @returns The labels for the member. - */ -function createLabelsFor(member: string) { +const labels = computed(() => { let matchedWith = new Array() - props.cluster.members.get(member)?.forEach((m) => matchedWith.push(m.matchedWith)) - return matchedWith -} + props.cluster.members + .get(idOfShownSubmission.value) + ?.forEach((m) => matchedWith.push(m.matchedWith)) + return matchedWith.map((m) => store().getDisplayName(m)) +}) -/** - * @param member The member to create the data set for. - * @returns The data set for the member. - */ -function createDataSetFor(member: string) { +const dataSet = computed(() => { let data = new Array() - props.cluster.members.get(member)?.forEach((m) => data.push(+(m.similarity * 100).toFixed(2))) + props.cluster.members + .get(idOfShownSubmission.value) + ?.forEach((m) => data.push(+(m.similarity * 100).toFixed(2))) return data -} +}) const radarChartStyle = { fill: true, @@ -137,16 +134,14 @@ const radarChartOptions = computed(() => { const chartData: Ref> = computed(() => { return { - labels: createLabelsFor(idOfShownSubmission.value), + labels: labels.value, datasets: [ { ...radarChartStyle, - label: idOfShownSubmission.value, - data: createDataSetFor(idOfShownSubmission.value) + label: store().getDisplayName(idOfShownSubmission.value), + data: dataSet.value } ] } }) - -const options = ref(radarChartOptions) diff --git a/report-viewer/src/components/DropDownSelector.vue b/report-viewer/src/components/DropDownSelector.vue index 1e0946ffd..1d0c67ab9 100644 --- a/report-viewer/src/components/DropDownSelector.vue +++ b/report-viewer/src/components/DropDownSelector.vue @@ -8,8 +8,8 @@ @change="$emit('selectionChanged', selectedOption)" class="m-0 w-full cursor-pointer bg-interactable-light dark:bg-interactable-dark" > - @@ -17,6 +17,7 @@