Skip to content

Commit

Permalink
Upload progress bar, fix #45
Browse files Browse the repository at this point in the history
  • Loading branch information
arildm committed Feb 28, 2024
1 parent 3858807 commit d5036fb
Show file tree
Hide file tree
Showing 11 changed files with 83 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ As this project is a user-facing application, the places in the semantic version
### Added

- Basic functionality for adding and removing metadata resources [#145](https://github.com/spraakbanken/mink-frontend/issues/145)
- Progress bar for source file uploads [#45](https://github.com/spraakbanken/mink-frontend/issues/45)
- Some dynamic imports to reduce main chunk size

### Changed
Expand Down
12 changes: 10 additions & 2 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
ListExportsData,
AdminModeStatusData,
CreateMetadataData,
ProgressHandler,
} from "./api.types";

/** Mink backend API client */
Expand Down Expand Up @@ -112,13 +113,20 @@ class MinkApi {
}

/** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Sources/operation/uploadsources */
async uploadSources(corpusId: string, files: FileList) {
async uploadSources(
corpusId: string,
files: FileList,
onProgress?: ProgressHandler,
) {
const formData = new FormData();
[...files].forEach((file) => formData.append("files[]", file));
const response = await this.axios.put<MinkResponse>(
"upload-sources",
formData,
{ params: { corpus_id: corpusId } },
{
params: { corpus_id: corpusId },
onUploadProgress: onProgress,
},
);
return response.data;
}
Expand Down
3 changes: 3 additions & 0 deletions src/api/api.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ByLang } from "@/util.types";
import type { AxiosProgressEvent } from "axios";

/** Properties common to most backend responses */
export type MinkResponse<T extends { [k: string]: any } = {}> = T & {
Expand Down Expand Up @@ -132,3 +133,5 @@ export type ListExportsData = {
export type AdminModeStatusData = {
admin_mode_status: boolean;
};

export type ProgressHandler = (progressEvent: AxiosProgressEvent) => void;
9 changes: 7 additions & 2 deletions src/api/backend.composable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useI18n } from "vue-i18n";
import api from "./api";
import useSpin from "@/spin/spin.composable";
import type { ProgressHandler } from "./api.types";

/** Wraps API endpoints with Spin. */
export default function useMinkBackend() {
Expand Down Expand Up @@ -54,9 +55,13 @@ export default function useMinkBackend() {
`corpus/${corpusId}/sources/${filename}`,
);

const uploadSources = (corpusId: string, files: FileList) =>
const uploadSources = (
corpusId: string,
files: FileList,
onProgress?: ProgressHandler,
) =>
spin(
api.uploadSources(corpusId, files),
api.uploadSources(corpusId, files, onProgress),
t("source.uploading", files.length),
`corpus/${corpusId}/sources`,
);
Expand Down
32 changes: 29 additions & 3 deletions src/components/FileUpload.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
<script setup lang="ts">
import { ref } from "vue";
import type { AxiosProgressEvent } from "axios";
import useMessenger from "@/message/messenger.composable";
import FileDropArea from "@/components/FileDropArea.vue";
import FileDropArea from "./FileDropArea.vue";
import ProgressBar from "./ProgressBar.vue";
import type { ProgressHandler } from "@/api/api.types";
const props = defineProps<{
fileHandler: (files: FileList) => Promise<void>;
/**
* A function that presumably uploads given file(s) to the appropriate API.
*/
fileHandler: (files: FileList, onProgress: ProgressHandler) => Promise<void>;
primary?: boolean;
accept?: string;
multiple?: boolean;
showProgress?: boolean;
}>();
const { clear } = useMessenger();
const progress = ref<number>();
/** Call upload function when using the <input> element. */
async function handleFileInput(event: Event) {
const fileInput = event.target as HTMLInputElement;
if (!fileInput.files) {
Expand All @@ -22,9 +32,19 @@ async function handleFileInput(event: Event) {
fileInput.value = "";
}
/** Call upload function. */
async function handleUpload(files: FileList) {
clear();
await props.fileHandler(files);
try {
await props.fileHandler(files, onProgress);
} finally {
progress.value = undefined;
}
}
/** Report upload progress. */
function onProgress(progressEvent: AxiosProgressEvent) {
progress.value = progressEvent.progress;
}
</script>

Expand Down Expand Up @@ -74,6 +94,12 @@ async function handleUpload(files: FileList) {
@change="handleFileInput"
/>

<ProgressBar
v-if="showProgress && progress !== undefined"
:percent="progress * 100"
class="w-60"
/>

<slot />
</div>
</div>
Expand Down
File renamed without changes.
10 changes: 7 additions & 3 deletions src/corpus/createCorpus.composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type ConfigOptions,
} from "@/api/corpusConfig";
import type { AxiosError } from "axios";
import type { MinkResponse } from "@/api/api.types";
import type { MinkResponse, ProgressHandler } from "@/api/api.types";
import useCreateResource from "@/resource/createResource.composable";

export default function useCreateCorpus() {
Expand Down Expand Up @@ -69,8 +69,12 @@ export default function useCreateCorpus() {
}

// Like the `uploadSources` in `sources.composable.ts` but takes `corpusId` as argument.
async function uploadSources(files: FileList, corpusId: string) {
await mink.uploadSources(corpusId, files);
async function uploadSources(
files: FileList,
corpusId: string,
onProgress?: ProgressHandler,
) {
await mink.uploadSources(corpusId, files, onProgress);
const info = await mink.resourceInfoOne(corpusId).catch(alertError);
if (!info) return;
resourceStore.corpora[corpusId].sources = info.resource.source_files;
Expand Down
2 changes: 1 addition & 1 deletion src/corpus/job/JobStatus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { useCorpusState } from "@/corpus/corpusState.composable";
import ActionButton from "@/components/ActionButton.vue";
import TerminalOutput from "@/components/TerminalOutput.vue";
import useJob from "./job.composable";
import ProgressBar from "./ProgressBar.vue";
import JobStatusMessage from "./JobStatusMessage.vue";
import ProgressBar from "@/components/ProgressBar.vue";
const corpusId = useCorpusIdParam();
const { runJob, abortJob, jobStatus, jobState, isJobRunning } =
Expand Down
10 changes: 8 additions & 2 deletions src/corpus/sources/SourceUpload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import UploadSizeLimits from "./UploadSizeLimits.vue";
import useConfig from "../config/config.composable";
import type { FileFormat } from "@/api/corpusConfig";
import FileUpload from "@/components/FileUpload.vue";
import type { ProgressHandler } from "@/api/api.types";
const props = defineProps<{
fileHandler?: (files: FileList) => Promise<void>;
Expand All @@ -18,12 +19,16 @@ const corpusId = useCorpusIdParam();
const { uploadSources, extensions } = useSources(corpusId);
const { config, uploadConfig } = useConfig(corpusId);
const { alertError } = useMessenger();
const extensionsAccept = computed(() =>
extensions.value?.map((ext) => `.${ext}`).join(),
);
async function defaultFileHandler(files: FileList) {
const requests = [uploadSources(files).catch(alertError)];
async function defaultFileHandler(
files: FileList,
onProgress: ProgressHandler,
) {
const requests = [uploadSources(files, onProgress).catch(alertError)];
// Also update format setting in config if needed
const format = getFilenameExtension(files[0]?.name) as FileFormat;
Expand All @@ -42,6 +47,7 @@ const fileHandler = props.fileHandler || defaultFileHandler;
:primary="primary"
:accept="extensionsAccept"
multiple
show-progress
>
<UploadSizeLimits />
</FileUpload>
Expand Down
25 changes: 14 additions & 11 deletions src/corpus/sources/SourcesPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ const totalSize = computed(() =>
</script>

<template>
<div class="flex flex-wrap gap-x-8">
<span>{{ $t("files", sources.length) }}, {{ filesize(totalSize) }}</span>
<span v-if="info">
{{ $t("source.limit.corpus.recommended") }}:
{{ filesize(info.recommended_file_size.min_file_length.value) }}
</span>
<span v-if="info">
{{ $t("source.limit.corpus.max") }}:
{{ filesize(info.file_size_limits.max_corpus_length.value) }}
</span>
</div>
<PendingContent :on="`corpus/${corpusId}/sources`">
<div class="flex flex-wrap gap-x-8">
<span>{{ $t("files", sources.length) }}, {{ filesize(totalSize) }}</span>
<span v-if="info">
{{ $t("source.limit.corpus.recommended") }}:
{{ filesize(info.recommended_file_size.min_file_length.value) }}
</span>
<span v-if="info">
{{ $t("source.limit.corpus.max") }}:
{{ filesize(info.file_size_limits.max_corpus_length.value) }}
</span>
</div>
<table v-if="sources.length" class="w-full mt-4 striped">
<thead>
<tr>
Expand Down Expand Up @@ -64,6 +64,9 @@ const totalSize = computed(() =>
</tr>
</tbody>
</table>
</PendingContent>

<PendingContent :on="`corpus/${corpusId}/sources`" blocking>
<SourceUpload :primary="isEmpty" />
</PendingContent>
</template>
6 changes: 3 additions & 3 deletions src/corpus/sources/sources.composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useMinkBackend from "@/api/backend.composable";
import { useResourceStore } from "@/store/resource.store";
import useMessenger from "@/message/messenger.composable";
import { getFilenameExtension } from "@/util";
import type { FileMeta } from "@/api/api.types";
import type { FileMeta, ProgressHandler } from "@/api/api.types";

export default function useSources(corpusId: string) {
const resourceStore = useResourceStore();
Expand All @@ -29,8 +29,8 @@ export default function useSources(corpusId: string) {
return mink.downloadPlaintext(corpusId, source.name).catch(alertError);
}

async function uploadSources(files: FileList) {
await mink.uploadSources(corpusId, files);
async function uploadSources(files: FileList, onProgress?: ProgressHandler) {
await mink.uploadSources(corpusId, files, onProgress);
loadSources();
}

Expand Down

0 comments on commit d5036fb

Please sign in to comment.