Skip to content

Commit

Permalink
Merge pull request #225 from DigitalSlideArchive/ui-imageData_table
Browse files Browse the repository at this point in the history
UI image data table
  • Loading branch information
marySalvi authored Jul 19, 2024
2 parents 1c79dbd + 02dac49 commit 4fb1905
Show file tree
Hide file tree
Showing 21 changed files with 442 additions and 172 deletions.
3 changes: 3 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
<title>ImageDePHI</title>
</head>
<body class="bg-base-300 min-h-screen">
Expand Down
19 changes: 19 additions & 0 deletions client/public/thumbnailPlaceholder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 15 additions & 13 deletions client/src/HomePage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { imageRedactionPlan } from "./store/imageStore";
import MenuSteps from "./components/MenuSteps.vue";
import FileBrowser from "./components/FileBrowser.vue";
import ImageList from "./components/ImageList.vue";
import ImageDataTable from "./components/ImageDataDisplay.vue";
const inputModal = ref(null);
const outputModal = ref(null);
const redactionModal = ref();
const redacting = ref(false);
const redactionComplete = ref(false);
const showImageList = ref(false);
const showImageTable = ref(false);
const progress = ref({
count: 0,
max: imageRedactionPlan.value.total,
Expand Down Expand Up @@ -45,7 +45,7 @@ const redact_images = async () => {
redacting.value = false;
redactionComplete.value = true;
redactionModal.value.close();
showImageList.value = false;
showImageTable.value = false;
}
};
</script>
Expand Down Expand Up @@ -82,21 +82,23 @@ const redact_images = async () => {
ref="inputModal"
:modal-id="'inputDirectory'"
:title="'Input Directory'"
@update-image-list="showImageList = true"
@update-image-list="showImageTable = true"
/>
<FileBrowser
ref="outputModal"
:modal-id="'outputDirectory'"
:title="'Output Directory'"
/>
<button
type="submit"
class="btn btn-wide bg-accent m-auto text-white"
:disabled="redacting"
@click="redact_images()"
>
De-phi Images
</button>
<div class="p-4 w-full">
<button
type="submit"
class="btn btn-block bg-accent text-white uppercase rounded-lg"
:disabled="redacting"
@click="redact_images()"
>
De-phi Images
</button>
</div>
</div>
</div>
</div>
Expand All @@ -121,7 +123,7 @@ const redact_images = async () => {
</div>
</div>
</dialog>
<ImageList v-if="showImageList" />
<ImageDataTable v-if="imageRedactionPlan.total && showImageTable" />
<div v-if="redactionComplete" class="card">
<div class="card-body">
<h2 class="card-title">Redaction Complete</h2>
Expand Down
11 changes: 11 additions & 0 deletions client/src/api/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ export async function redactImages(
);
return response;
}

export async function getImages(path: string, imageKey: string) {
const response = await fetch(
`${basePath}/image/?file_name=${path}&image_key=${imageKey}`,
{
method: "GET",
mode: "cors",
},
);
return response;
}
Binary file removed client/src/assets/logo.png
Binary file not shown.
4 changes: 2 additions & 2 deletions client/src/components/FileBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const updateDirectories = async (currentDirectory?: string) => {
updateDirectories();
const updateImageData = async (directory: string) => {
imageRedactionPlan.value = await getRedactionPlan(directory, 10, 0, true);
imageRedactionPlan.value = await getRedactionPlan(directory, 50, 0, true);
};
const closeModal = () => {
Expand All @@ -60,7 +60,7 @@ const updateSelectedDirectories = (path: string) => {
{{ title }}
</h2>
<button
class="btn bg-primary float-right text-white"
class="btn bg-primary float-right text-white uppercase"
type="button"
@click="
$emit('update-image-list'),
Expand Down
69 changes: 69 additions & 0 deletions client/src/components/ImageDataDisplay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { imageRedactionPlan } from "../store/imageStore";
import { getImages, getRedactionPlan } from "../api/rest";
import { selectedDirectories } from "../store/directoryStore";
import ImageDataTable from "./ImageDataTable.vue";
import InfiniteScroller from "./InfiniteScroller.vue";
const limit = ref(50);
const offset = ref(1);
const loadImagePlan = async () => {
const newPlan = await getRedactionPlan(
selectedDirectories.value.inputDirectory,
limit.value,
offset.value,
false,
);
imageRedactionPlan.value.data = {
...imageRedactionPlan.value.data,
...newPlan.data,
};
getThumbnail(newPlan.data);
++offset.value;
};
const usedColumns = computed(() => imageRedactionPlan.value.tags);
const getThumbnail = async (
imagedict: Record<string, Record<string, string>>,
) => {
Object.keys(imagedict).forEach(async (image) => {
const response = await getImages(
selectedDirectories.value.inputDirectory + "/" + image,
"thumbnail",
);
if (response.status >= 400) {
imageRedactionPlan.value.data[image].thumbnail =
"/thumbnailPlaceholder.svg";
return;
}
if (response.body) {
const reader = response.body.getReader();
const chunks = [];
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const blob = new Blob(chunks);
const url = URL.createObjectURL(blob);
imageRedactionPlan.value.data[image].thumbnail = url;
}
});
};
getThumbnail(imageRedactionPlan.value.data);
</script>

<template>
<div class="card m-4 pb-4 rounded">
<InfiniteScroller @infinite-scroll="loadImagePlan">
<ImageDataTable
:used-columns="usedColumns"
:image-redaction-plan="imageRedactionPlan"
/>
</InfiniteScroller>
</div>
</template>
130 changes: 104 additions & 26 deletions client/src/components/ImageDataTable.vue
Original file line number Diff line number Diff line change
@@ -1,44 +1,122 @@
<script setup lang="ts">
const props = defineProps({
imageData: {
defineProps({
imageRedactionPlan: {
type: Object,
required: true,
},
});
const sortedData = Object.entries(props.imageData).sort(([keyA], [keyB]) => {
if (keyA === "missing_tags") {
return -1;
} else if (keyB === "missing_tags") {
return 1;
} else {
return 0;
}
usedColumns: {
type: Array<string>,
required: true,
},
});
</script>

<template>
<table class="table table-auto table-xs w-full">
<div v-if="!usedColumns" class="m-auto flex justify-center">
Loading.. <span class="loading loading-bars loading-md"></span>
</div>
<table v-if="usedColumns" class="table table-auto text-center bg-base-100">
<thead>
<tr>
<th v-for="tag in sortedData" :key="tag[0]">
{{ tag[0] }}
<tr class="text-base">
<th class="bg-neutral text-white py-5 px-6">Image File Name</th>
<th class="bg-gray-600 text-white py-5 px-10">Image</th>
<th class="bg-gray-600 text-white">Redaction Status</th>
<th
v-if="Object.keys(imageRedactionPlan.data).includes('missing_tags')"
class="bg-gray-600 text-white p-4"
>
Missing Tags
</th>
<th
v-for="tag in usedColumns"
:key="tag"
class="bg-gray-600 text-white py-5 px-6"
>
{{ tag }}
</th>
</tr>
</thead>
<tbody>
<tr>
<template v-for="rule in sortedData" :key="rule[0]">
<td v-if="rule[0] === 'missing_tags'">
<span v-for="(tag, id) in rule[1][0]" :key="id">
{{ id }} : {{ tag }}
<tbody class="text-base bg-base-100">
<tr
v-for="(image, index) in imageRedactionPlan.data"
:key="index"
class="h-[125px]"
>
<th>{{ index }}</th>
<td>
<img :src="image.thumbnail" class="w-20 h-20" />
</td>
<td>
<div
v-if="image.missing_tags"
class="tooltip tooltip-right"
:data-tip="`${image.missing_tags.length} tag(s) missing redaction rules.`"
>
<i class="ri-error-warning-fill text-red-600 text-xl"></i>
<div v-for="(obj, pos) in image.missing_tags" :key="pos">
<span v-for="(value, key) in obj" :key="key">
{{ key }}: {{ value }}
</span>
</div>
</div>

<div
v-else
class="tooltip tooltip-right"
:data-tip="`No missing redaction rules.`"
>
<i class="ri-checkbox-circle-fill text-green-600 text-xl"></i>
</div>
</td>
<template v-for="tag in usedColumns" :key="tag">
<td class="text-ellipsis overflow-hidden max-w-[300px]">
<span
v-if="image[tag]"
:class="
image[tag].action === 'delete'
? 'line-through text-accent font-bold decoration-2'
: ''
"
>
<span v-if="image[tag].binary">
{{ image[tag].binary.bytes }} bytes
<span>
{{ image[tag].binary.value.slice(0, 32) }}
</span>
</span>
<span v-else>
{{
typeof image[tag].value === "object"
? image[tag].value.join(", ")
: image[tag].value
}}
</span>
</span>
</td>
<td v-else>
{{ rule[1] }}
</td>
</template>
</tr>
</tbody>
</table>
</template>
<style scoped>
thead th:first-child {
position: sticky;
left: 0;
z-index: 2;
background-color: #201c35;
}
thead th {
position: sticky;
top: 0;
z-index: 1;
}
tbody th {
position: relative;
}
tbody th:first-child {
position: sticky;
left: 0;
z-index: 1;
background-color: #ffffff;
}
</style>
Loading

0 comments on commit 4fb1905

Please sign in to comment.