From a9faf896876b8923ee7952c476b2e91e60fade6c Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 17 Jan 2024 11:46:54 +0100 Subject: [PATCH 01/28] Fix checkbox label margin --- src/index.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.css b/src/index.css index 71434f7..2679944 100644 --- a/src/index.css +++ b/src/index.css @@ -84,7 +84,9 @@ select { .formkit-message { @apply text-red-600; } -[data-type="radio"] .formkit-inner { +[data-type="radio"] .formkit-inner, +[data-type="checkbox"] .formkit-inner + { @apply inline-block mr-2; } [data-type="radio"] .formkit-legend { From 76690b9d6e5845be9cf03e613af6ddecea008330 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 24 Jan 2024 15:11:14 +0100 Subject: [PATCH 02/28] Convert to TypeScript Squashed commit of the following: commit a367e1def91e3d231121cd03994017f3c98db198 Author: Arild Matsson Date: Wed Jan 24 15:10:24 2024 +0100 Drop JobStatusObject commit eb45cc316f0310759d532ff19c188f969a685711 Author: Arild Matsson Date: Wed Jan 24 14:18:05 2024 +0100 Drop sources route commit f6e237c4c873aeab2a4bfbba7cae2c7ac3c92c34 Author: Arild Matsson Date: Wed Jan 24 14:12:38 2024 +0100 Fix formatOptions commit 284850d6ec3ae79b8dd1a399866be4b3096a2135 Author: Arild Matsson Date: Wed Jan 24 14:07:51 2024 +0100 API types for rest commit a61ffd6e440338e67340acb3d9288954590375a4 Author: Arild Matsson Date: Wed Jan 24 11:54:13 2024 +0100 API types for /info commit 5e759ecb1a16116cb1ce263f4255f43f20dc0c77 Author: Arild Matsson Date: Tue Jan 23 16:30:09 2024 +0100 TSify config files commit c98661dfa5213a329bf778db3b7cc9cdca5a0653 Author: Arild Matsson Date: Tue Jan 23 15:55:40 2024 +0100 TSify corpus/sources dir commit aba6cf3c7d6d15e862523618fe163c03403b91f6 Author: Arild Matsson Date: Tue Jan 23 14:20:52 2024 +0100 TSify corpus/job dir commit 11e5aea44e6e0c5053dee908a871ddac02455dd4 Author: Arild Matsson Date: Tue Jan 23 13:10:56 2024 +0100 TSify corpus/exports dir commit 0f6e43c48557783dc17aed2cd714e3ad65891ab9 Author: Arild Matsson Date: Mon Jan 22 16:11:14 2024 +0100 TSify corpus/config dir commit 1a517b3b489be3c61396e9b451f190dcd9285352 Author: Arild Matsson Date: Fri Jan 19 13:50:01 2024 +0100 TSify corpus top dir commit 49c351dc3e44591c0f74ce3433799ccdcfb5f8d6 Author: Arild Matsson Date: Thu Jan 18 16:17:57 2024 +0100 TSify main files commit 2abd9ce412467feae4f6ae47cbebca1ba0fc2c70 Author: Arild Matsson Date: Thu Jan 18 16:01:37 2024 +0100 TSify user dir commit a17a6ebe0a882c8800a0e456c95ab5e55401cd05 Author: Arild Matsson Date: Thu Jan 18 14:49:57 2024 +0100 TSify spin dir commit 33c86f07519267dae03a5d3e06286de548032044 Author: Arild Matsson Date: Wed Jan 17 16:16:45 2024 +0100 TSify page dir commit a76f87150da94c4049f7e0e3aef2d12e36e05bf1 Author: Arild Matsson Date: Wed Jan 17 15:58:23 2024 +0100 TSify corpora dir commit 006ddc0f49ebaa2026fe24a7f8cf564b0ff91e75 Author: Arild Matsson Date: Wed Jan 17 15:43:00 2024 +0100 TSify droptopage composable commit 70eed929798c59541d505991e14a75cbe17da01d Author: Arild Matsson Date: Wed Jan 17 15:05:48 2024 +0100 TSify corpus store commit 867e2de0a722f95a363221ef6d2a8ca9e3cf2dbd Author: Arild Matsson Date: Wed Jan 17 15:29:47 2024 +0100 TSify util commit 125834b31735d372923b6ee9c7497251ff620b96 Author: Arild Matsson Date: Mon Jan 15 14:02:04 2024 +0100 TSify api dir commit ec4b3fca91fd6628e0128e942918d0ecc3bcce1d Author: Arild Matsson Date: Mon Jan 15 13:48:16 2024 +0100 TSify auth dir commit c2018c80134afdeaec78cdf5a5d5bd86ecc04071 Author: Arild Matsson Date: Mon Jan 15 13:20:49 2024 +0100 TSify home dir commit e50eaccd010b2be139a76a6100c3b7d50ca3f356 Author: Arild Matsson Date: Thu Jan 4 15:38:39 2024 +0100 TSify i18n dir commit 8f7831d1e1191f3af322014b08fd6971fcdf9f80 Author: Arild Matsson Date: Thu Jan 4 14:31:27 2024 +0100 TSify message dir commit 445325a599c379f7dd4f39e7305854a72ea6f3fd Author: Arild Matsson Date: Thu Jan 4 14:24:10 2024 +0100 TSify components dir commit 52f9865a49ef39c591664efdc8d9df74ec082eb5 Author: Arild Matsson Date: Thu Jan 4 13:56:13 2024 +0100 Add Typescript --- .eslintrc.js | 7 +- env.d.ts | 14 + index.d.ts | 1 + index.html | 2 +- jsconfig.json | 9 - package.json | 12 +- src/App.vue | 40 ++- src/api/api.js | 186 ----------- src/api/api.ts | 235 ++++++++++++++ src/api/api.types.ts | 127 ++++++++ ...nd.composable.js => backend.composable.ts} | 48 ++- src/api/backendInfo.composable.js | 18 - src/api/backendInfo.composable.ts | 58 ++++ src/api/{corpusConfig.js => corpusConfig.ts} | 56 +++- src/api/sparvConfig.types.ts | 58 ++++ src/auth/LoginButton.vue | 19 +- src/auth/{Login.vue => LoginView.vue} | 10 +- src/auth/{Signup.vue => SignupView.vue} | 2 +- ...{auth.composable.js => auth.composable.ts} | 26 +- src/auth/{auth.js => auth.ts} | 0 src/auth/jwtSb.js | 45 --- src/auth/jwtSb.ts | 79 +++++ src/components/ActionButton.vue | 10 +- src/components/HelpBox.vue | 10 +- src/components/PadButton.vue | 30 +- src/components/PageTitle.vue | 14 +- src/components/Panel.vue | 14 +- src/components/RouteButton.vue | 11 +- src/components/Section.vue | 14 +- src/components/UrlButton.vue | 12 +- ...composable.js => droptopage.composable.ts} | 10 +- src/corpora/CorpusButton.vue | 36 +- src/corpora/CreateCorpus.vue | 91 +++--- src/corpora/{Library.vue => LibraryView.vue} | 54 +-- ...ra.composable.js => corpora.composable.ts} | 6 +- src/corpus/CorpusDelete.vue | 8 +- .../{Overview.vue => CorpusOverview.vue} | 44 ++- src/corpus/CorpusStateHelp.vue | 2 +- src/corpus/CorpusStateMessage.vue | 38 +-- src/corpus/{Corpus.vue => CorpusView.vue} | 34 +- .../config/{Config.vue => ConfigPanel.vue} | 20 +- src/corpus/config/CorpusConfiguration.vue | 164 ++++++---- src/corpus/config/CorpusMetadata.vue | 64 ++-- .../{Metadata.vue => MetadataPanel.vue} | 26 +- ...fig.composable.js => config.composable.ts} | 15 +- src/corpus/corpus.composable.js | 46 --- src/corpus/corpus.composable.ts | 40 +++ ...posable.js => corpusIdParam.composable.ts} | 2 +- ...omposable.js => corpusState.composable.ts} | 43 ++- ...mposable.js => createCorpus.composable.ts} | 58 ++-- src/corpus/deleteCorpus.composable.ts | 31 ++ src/corpus/exports/CorpusResult.vue | 46 ++- .../exports/{Exports.vue => ExportsPanel.vue} | 109 +++---- src/corpus/exports/ToolPanel.vue | 24 +- ...ts.composable.js => exports.composable.ts} | 8 +- src/corpus/job/JobStatus.vue | 74 ++--- src/corpus/job/JobStatusMessage.vue | 44 ++- src/corpus/job/ProgressBar.vue | 33 +- .../{job.composable.js => job.composable.ts} | 78 +---- src/corpus/sources/CorpusSources.vue | 12 - .../{Filedrop.vue => FileDropArea.vue} | 22 +- src/corpus/sources/SourceText.vue | 15 +- src/corpus/sources/SourceUpload.vue | 112 +++---- .../sources/{Source.vue => SourceView.vue} | 83 ++--- .../sources/{Sources.vue => SourcesPanel.vue} | 56 ++-- src/corpus/sources/UploadSizeLimits.vue | 12 +- ...es.composable.js => sources.composable.ts} | 22 +- src/{fontawesome.js => fontawesome.ts} | 0 src/{formkit.js => formkit.ts} | 10 +- src/home/{Home.test.js => Home.test.ts} | 4 +- src/home/HomeIllustration.vue | 23 -- src/home/HomeNews.vue | 6 +- src/home/{Home.vue => HomeView.vue} | 30 +- src/i18n/{i18n.js => i18n.ts} | 0 ...ale.composable.js => locale.composable.ts} | 17 +- src/{main.js => main.ts} | 0 src/message/MessageToasts.vue | 2 +- ....composable.js => messenger.composable.ts} | 23 +- src/page/AppHeader.vue | 12 +- src/page/Breadcrumb.vue | 6 +- src/page/MinkLogo.vue | 8 +- src/page/NotFound.vue | 10 +- ...itle.composable.js => title.composable.ts} | 12 +- src/{router.js => router.ts} | 59 ++-- src/spin/PendingContent.vue | 19 +- src/spin/{Spinner.vue => SpinIndicator.vue} | 0 ...{spin.composable.js => spin.composable.ts} | 36 +- .../{corpus.store.js => corpus.store.ts} | 27 +- src/user/AdminModeBanner.vue | 2 +- src/user/AdminModeSwitcher.vue | 20 +- src/user/{User.vue => UserView.vue} | 38 +-- ...dmin.composable.js => admin.composable.ts} | 3 +- src/util.js | 82 ----- src/{util.test.js => util.test.ts} | 92 +++++- src/util.ts | 123 +++++++ src/util.types.ts | 1 + tailwind.config.js => tailwind.config.ts | 4 +- tsconfig.json | 13 + vite.config.js => vite.config.ts | 5 +- yarn.lock | 307 +++++++++++++++++- 100 files changed, 2273 insertions(+), 1470 deletions(-) create mode 100644 env.d.ts create mode 100644 index.d.ts delete mode 100644 jsconfig.json delete mode 100644 src/api/api.js create mode 100644 src/api/api.ts create mode 100644 src/api/api.types.ts rename src/api/{backend.composable.js => backend.composable.ts} (72%) delete mode 100644 src/api/backendInfo.composable.js create mode 100644 src/api/backendInfo.composable.ts rename src/api/{corpusConfig.js => corpusConfig.ts} (72%) create mode 100644 src/api/sparvConfig.types.ts rename src/auth/{Login.vue => LoginView.vue} (90%) rename src/auth/{Signup.vue => SignupView.vue} (97%) rename src/auth/{auth.composable.js => auth.composable.ts} (77%) rename src/auth/{auth.js => auth.ts} (100%) delete mode 100644 src/auth/jwtSb.js create mode 100644 src/auth/jwtSb.ts rename src/components/{droptopage.composable.js => droptopage.composable.ts} (76%) rename src/corpora/{Library.vue => LibraryView.vue} (95%) rename src/corpora/{corpora.composable.js => corpora.composable.ts} (91%) rename src/corpus/{Overview.vue => CorpusOverview.vue} (84%) rename src/corpus/{Corpus.vue => CorpusView.vue} (97%) rename src/corpus/config/{Config.vue => ConfigPanel.vue} (98%) rename src/corpus/config/{Metadata.vue => MetadataPanel.vue} (97%) rename src/corpus/config/{config.composable.js => config.composable.ts} (72%) delete mode 100644 src/corpus/corpus.composable.js create mode 100644 src/corpus/corpus.composable.ts rename src/corpus/{corpusIdParam.composable.js => corpusIdParam.composable.ts} (73%) rename src/corpus/{corpusState.composable.js => corpusState.composable.ts} (73%) rename src/corpus/{createCorpus.composable.js => createCorpus.composable.ts} (59%) create mode 100644 src/corpus/deleteCorpus.composable.ts rename src/corpus/exports/{Exports.vue => ExportsPanel.vue} (89%) rename src/corpus/exports/{exports.composable.js => exports.composable.ts} (84%) rename src/corpus/job/{job.composable.js => job.composable.ts} (55%) delete mode 100644 src/corpus/sources/CorpusSources.vue rename src/corpus/sources/{Filedrop.vue => FileDropArea.vue} (79%) rename src/corpus/sources/{Source.vue => SourceView.vue} (77%) rename src/corpus/sources/{Sources.vue => SourcesPanel.vue} (85%) rename src/corpus/sources/{sources.composable.js => sources.composable.ts} (65%) rename src/{fontawesome.js => fontawesome.ts} (100%) rename src/{formkit.js => formkit.ts} (73%) rename src/home/{Home.test.js => Home.test.ts} (84%) delete mode 100644 src/home/HomeIllustration.vue rename src/home/{Home.vue => HomeView.vue} (90%) rename src/i18n/{i18n.js => i18n.ts} (100%) rename src/i18n/{locale.composable.js => locale.composable.ts} (79%) rename src/{main.js => main.ts} (100%) rename src/message/{messenger.composable.js => messenger.composable.ts} (81%) rename src/page/{title.composable.js => title.composable.ts} (64%) rename src/{router.js => router.ts} (62%) rename src/spin/{Spinner.vue => SpinIndicator.vue} (100%) rename src/spin/{spin.composable.js => spin.composable.ts} (61%) rename src/store/{corpus.store.js => corpus.store.ts} (66%) rename src/user/{User.vue => UserView.vue} (93%) rename src/user/{admin.composable.js => admin.composable.ts} (90%) delete mode 100644 src/util.js rename src/{util.test.js => util.test.ts} (55%) create mode 100644 src/util.ts create mode 100644 src/util.types.ts rename tailwind.config.js => tailwind.config.ts (89%) create mode 100644 tsconfig.json rename vite.config.js => vite.config.ts (91%) diff --git a/.eslintrc.js b/.eslintrc.js index 3691132..deeec58 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,12 @@ module.exports = { node: true, "vue/setup-compiler-macros": true, }, - extends: ["eslint:recommended", "plugin:vue/vue3-recommended", "prettier"], + extends: [ + "eslint:recommended", + "plugin:vue/vue3-recommended", + "prettier", + "@vue/eslint-config-typescript", + ], rules: { // https://eslint.vuejs.org/user-guide/#does-not-work-well-with-script-setup "vue/script-setup-uses-vars": "error", diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..4a2ffe3 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,14 @@ +/// + +interface ImportMetaEnv { + readonly VITE_BACKEND_URL: string; + readonly VITE_AUTH_URL: string; + readonly VITE_KORP_URL: string; + readonly VITE_STRIX_URL: string; + readonly VITE_MATOMO_URL: string; + readonly VITE_MATOMO_ID?: number; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..d9f966a --- /dev/null +++ b/index.d.ts @@ -0,0 +1 @@ +declare module "vue-matomo"; diff --git a/index.html b/index.html index 1d0741b..ddd0708 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,6 @@
- + diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 8da7df9..0000000 --- a/jsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "include": ["./src/**/*"], - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - } -} diff --git a/package.json b/package.json index 56d4d48..ef9eb65 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "license": "MIT", "scripts": { "dev": "vite", - "build": "vite build", + "build": "yarn typecheck && vite build", "serve": "vite preview", + "typecheck": "vue-tsc --noEmit", "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src", "format": "prettier . --write '!.yarn' --ignore-path .gitignore", "test": "vitest", @@ -22,6 +23,7 @@ "@fortawesome/vue-fontawesome": "^3.0.1", "@modyfi/vite-plugin-yaml": "^1.0.4", "@vitejs/plugin-vue": "^5", + "@vue/tsconfig": "^0.5.1", "@vueuse/core": "^9.6.0", "autoprefixer": "^10.4.7", "axios": "^0.21.4", @@ -32,15 +34,21 @@ "pinia": "^2.0.28", "rollup-plugin-visualizer": "^5.6.0", "tailwindcss": "^3.1.4", + "typescript": "^5.3.3", "vite": "^4.0.5", "vite-plugin-rewrite-all": "1.0.1 || ^1.0.3", "vue": "^3.4", "vue-i18n": "9", "vue-matomo": "^4.2.0", - "vue-router": "4" + "vue-router": "4", + "vue-tsc": "^1.8.27" }, "devDependencies": { "@testing-library/vue": "^6.6.1", + "@types/js-yaml": "^4.0.9", + "@types/lodash": "^4.14.202", + "@types/node": "^20.10.6", + "@vue/eslint-config-typescript": "^12.0.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-vue": "^8.0", "happy-dom": "^7.7.0", diff --git a/src/App.vue b/src/App.vue index d088e6b..f85262c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,20 +1,4 @@ - - - - + diff --git a/src/api/api.js b/src/api/api.js deleted file mode 100644 index 50fcec7..0000000 --- a/src/api/api.js +++ /dev/null @@ -1,186 +0,0 @@ -import Axios from "axios"; -import { ensureTrailingSlash } from "@/util"; -import pick from "lodash/pick"; - -/** Mink backend API client */ -class MinkApi { - /** Creates the client instance */ - constructor() { - this.axios = Axios.create({ - baseURL: ensureTrailingSlash(import.meta.env.VITE_BACKEND_URL), - withCredentials: true, - }); - } - - /** Sets a JWT token which is then used to authenticate API requests. */ - setJwt(jwt) { - this.jwt = jwt; - this.axios.defaults.headers["Authorization"] = jwt - ? `Bearer ${jwt}` - : undefined; - } - - async getInfo() { - const response = await this.axios.get("info"); - return pick(response.data, [ - "status_codes", - "importer_modules", - "file_size_limits", - "recommended_file_size", - ]); - } - - async listCorpora() { - const response = await this.axios.get("list-corpora"); - return response.data.corpora; - } - - async createCorpus() { - const response = await this.axios.post("create-corpus"); - return response.data.corpus_id; - } - - async removeCorpus(corpusId) { - const response = await this.axios.delete("remove-corpus", { - params: { corpus_id: corpusId }, - }); - return response.data; - } - - async uploadConfig(corpusId, config) { - const configFile = new File([config], "config.yaml", { type: "text/yaml" }); - const formData = new FormData(); - formData.append("files[]", configFile); - const response = await this.axios.put("upload-config", formData, { - params: { corpus_id: corpusId }, - }); - return response.data; - } - - async downloadSourceFile(corpusId, filename, binary = false) { - const response = await this.axios.get("download-sources", { - params: { corpus_id: corpusId, file: filename, zip: false }, - responseType: binary ? "arraybuffer" : "text", - }); - return response.data; - } - - async downloadSourceText(corpusId, filename) { - const response = await this.axios.get("download-source-text", { - params: { corpus_id: corpusId, file: filename }, - }); - return response.data; - } - - async uploadSources(corpusId, files) { - const formData = new FormData(); - [...files].forEach((file) => formData.append("files[]", file)); - const response = await this.axios.put("upload-sources", formData, { - params: { corpus_id: corpusId }, - }); - return response.data; - } - - async removeSource(corpusId, name) { - const response = await this.axios.delete("remove-sources", { - params: { corpus_id: corpusId, remove: name }, - }); - return response.data; - } - - async downloadConfig(corpusId) { - const response = await this.axios.get("download-config", { - params: { corpus_id: corpusId }, - }); - return response.data; - } - - /** - * @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Process-Corpus/operation/resourceinfo - * - * An info record has {resource: ..., job: ...}. - * - * If corpus_id is given, the response is an info record. If not, it contains - * `resources` which is a list of info records. - */ - async resourceInfo(corpusId) { - const response = await this.axios.get("resource-info", { - params: { corpus_id: corpusId }, - }); - return response.data; - } - - async runSparv(corpusId) { - const response = await this.axios - .put("run-sparv", null, { params: { corpus_id: corpusId } }) - // Errors are okay. - .catch((reason) => reason.response); - return response.data; - } - - async abortJob(corpusId) { - const response = await this.axios.post("abort-job", null, { - params: { corpus_id: corpusId }, - }); - return response.data; - } - - async listExports(corpusId) { - const response = await this.axios.get("list-exports", { - params: { corpus_id: corpusId }, - }); - return response.data.contents; - } - - async downloadExports(corpusId) { - const response = await this.axios.get("download-exports", { - params: { corpus_id: corpusId }, - responseType: "blob", - }); - return response.data; - } - - async downloadExportFile(corpusId, path) { - const response = await this.axios.get("download-exports", { - params: { - corpus_id: corpusId, - file: path, - zip: false, - }, - responseType: "blob", - }); - return response.data; - } - - async installKorp(corpusId) { - const response = await this.axios.put("install-korp", null, { - params: { corpus_id: corpusId }, - }); - return response.data; - } - - async installStrix(corpusId) { - const response = await this.axios.put("install-strix", null, { - params: { corpus_id: corpusId }, - }); - return response.data; - } - - async adminModeStatus() { - const response = await this.axios.get("admin-mode-status"); - return response.data.admin_mode_status; - } - - async adminModeOn() { - const response = await this.axios.post("admin-mode-on"); - return response.data; - } - - async adminModeOff() { - const response = await this.axios.post("admin-mode-off"); - return response.data; - } -} - -/** API client singleton instance. */ -export const api = new MinkApi(); diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..9559300 --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,235 @@ +import Axios, { type AxiosInstance } from "axios"; +import { ensureTrailingSlash } from "@/util"; +import type { + MinkResponse, + InfoData, + ListCorporaData, + CreateCorpusData, + ResourceInfoAllData, + ResourceInfoOneData, + JobState, + JobType, + ListExportsData, + AdminModeStatusData, +} from "./api.types"; + +/** Mink backend API client */ +class MinkApi { + /** An instance of the Axios HTTP client. */ + axios: AxiosInstance; + + /** A JWT token used to authenticate API requests. */ + jwt: string | undefined; + + /** Creates the client instance */ + constructor() { + this.axios = Axios.create({ + baseURL: ensureTrailingSlash(import.meta.env.VITE_BACKEND_URL), + withCredentials: true, + }); + } + + /** Sets a JWT token which is then used to authenticate API requests. */ + setJwt(jwt?: string) { + this.jwt = jwt; + this.axios.defaults.headers["Authorization"] = jwt + ? `Bearer ${jwt}` + : undefined; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Documentation/operation/APIinfo */ + async getInfo() { + const response = await this.axios.get>("info"); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Corpora/operation/listcorpora */ + async listCorpora() { + const response = await this.axios.get>( + "list-corpora" + ); + return response.data.corpora; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Corpora/operation/createcorpus */ + async createCorpus() { + const response = await this.axios.post>( + "create-corpus" + ); + return response.data.corpus_id; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Corpora/operation/removecorpus */ + async removeCorpus(corpusId: string) { + const response = await this.axios.delete("remove-corpus", { + params: { corpus_id: corpusId }, + }); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Config/operation/uploadconfig */ + async uploadConfig(corpusId: string, config: string) { + const configFile = new File([config], "config.yaml", { type: "text/yaml" }); + const formData = new FormData(); + formData.append("files[]", configFile); + const response = await this.axios.put( + "upload-config", + formData, + { params: { corpus_id: corpusId } } + ); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Sources/operation/downloadsources */ + async downloadSources(corpusId: string, filename: string, binary = false) { + const response = await this.axios.get("download-sources", { + params: { corpus_id: corpusId, file: filename, zip: false }, + responseType: binary ? "arraybuffer" : "text", + }); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Exports/operation/downloadsourcetext */ + async downloadSourceText(corpusId: string, filename: string) { + const response = await this.axios.get("download-source-text", { + params: { corpus_id: corpusId, file: filename }, + }); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Sources/operation/uploadsources */ + async uploadSources(corpusId: string, files: FileList) { + const formData = new FormData(); + [...files].forEach((file) => formData.append("files[]", file)); + const response = await this.axios.put( + "upload-sources", + formData, + { params: { corpus_id: corpusId } } + ); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Sources/operation/removesources */ + async removeSource(corpusId: string, name: string) { + const response = await this.axios.delete("remove-sources", { + params: { corpus_id: corpusId, remove: name }, + }); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Config/operation/downloadconfig */ + async downloadConfig(corpusId: string) { + const response = await this.axios.get("download-config", { + params: { corpus_id: corpusId }, + }); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Process-Corpus/operation/resourceinfo */ + async resourceInfoAll() { + const response = await this.axios.get>( + "resource-info" + ); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Process-Corpus/operation/resourceinfo */ + async resourceInfoOne(corpusId: string) { + const response = await this.axios.get>( + "resource-info", + { params: { corpus_id: corpusId } } + ); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Process-Corpus/operation/runSparv */ + async runSparv(corpusId: string) { + const response = await this.axios + .put>("run-sparv", null, { + params: { corpus_id: corpusId }, + }) + // Errors are okay. + .catch((reason) => reason.response); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Process-Corpus/operation/abortjob */ + async abortJob(corpusId: string) { + const response = await this.axios.post< + MinkResponse> + >("abort-job", null, { + params: { corpus_id: corpusId }, + }); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Exports/operation/listexports */ + async listExports(corpusId: string) { + const response = await this.axios.get>( + "list-exports", + { params: { corpus_id: corpusId } } + ); + return response.data.contents; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Exports/operation/downloadexports */ + async downloadExports(corpusId: string) { + const response = await this.axios.get("download-exports", { + params: { corpus_id: corpusId }, + responseType: "blob", + }); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Manage-Exports/operation/downloadexports */ + async downloadExportFile(corpusId: string, path: string) { + const response = await this.axios.get("download-exports", { + params: { corpus_id: corpusId, file: path, zip: false }, + responseType: "blob", + }); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Process-Corpus/operation/installinKorp */ + async installKorp(corpusId: string) { + const response = await this.axios.put>( + "install-korp", + null, + { params: { corpus_id: corpusId } } + ); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Process-Corpus/operation/installinStrix */ + async installStrix(corpusId: string) { + const response = await this.axios.put>( + "install-strix", + null, + { params: { corpus_id: corpusId } } + ); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Admin-Mode/operation/adminmodestatus */ + async adminModeStatus() { + const response = await this.axios.get>( + "admin-mode-status" + ); + return response.data.admin_mode_status; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Admin-Mode/operation/adminmodeon */ + async adminModeOn() { + const response = await this.axios.post("admin-mode-on"); + return response.data; + } + + /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Admin-Mode/operation/adminmodeoff */ + async adminModeOff() { + const response = await this.axios.post("admin-mode-off"); + return response.data; + } +} + +/** API client singleton instance. */ +export const api = new MinkApi(); diff --git a/src/api/api.types.ts b/src/api/api.types.ts new file mode 100644 index 0000000..b8ae8d4 --- /dev/null +++ b/src/api/api.types.ts @@ -0,0 +1,127 @@ +import type { ByLang } from "@/util.types"; + +/** Properties common to most backend responses */ +export type MinkResponse = T & { + status: "success" | "error"; + return_code: string; + message: string; +}; + +/** Data in the info response */ +export type InfoData = { + status_codes: InfoDataSection<{ + name: JobState; + description: string; + }>; + importer_modules: InfoDataSection<{ + file_extension: `.${string}`; + importer: string; + }>; + file_size_limits: InfoDataSection<{ + name: "max_content_length" | "max_file_length" | "max_corpus_length"; + description: string; + value: number; + }>; + recommended_file_size: InfoDataSection<{ + name: "max_file_length" | "min_file_length"; + description: string; + value: number; + }>; +}; + +/** A structure for a section in the backend info response */ +type InfoDataSection = { + info: string; + data: T[]; +}; + +/** Data in the list-corpora response */ +export type ListCorporaData = { + corpora: string[]; +}; + +/** Data in the create-corpus response */ +export type CreateCorpusData = { + corpus_id: string; +}; + +/** Data in the resource-info response, if no corpus_id param is given */ +export type ResourceInfoAllData = { + resources: MinkResponse[]; +}; + +/** Data in the resource-info response, if the corpus_id param is given */ +export type ResourceInfoOneData = ResourceInfo; + +/** Data about a resource and its job status */ +export type ResourceInfo = { + resource: ResourceData; + job: CorpusStatus; +}; + +/** Basic data about a resource */ +export type ResourceData = { + type: "corpus"; + id: string; + public_id: string; + name: ByLang; + source_files: FileMeta[]; +}; + +/** Job status for a resource */ +// There's more but we're not using everything. +export type CorpusStatus = { + current_process: JobType | null; + status: Record; + warnings: string; + errors: string; + sparv_output: string; + installed_korp: boolean; + installed_strix: boolean; + /** ISO 8601 date */ + started: string | null; + /** ISO 8601 date */ + last_run_started: string | ""; + /** ISO 8601 date */ + last_run_ended: string | ""; + /** Queue number, starting at 1 */ + priority: number | ""; + /** Percentage of job completion, if running */ + progress: `${number}%` | ""; +}; + +/** File metadata */ +export type FileMeta = { + /** ISO 8601 date (with timezone offset) of last modification */ + last_modified: string; + /** Filename */ + name: string; + /** File path, relative to some directory */ + path: string; + /** File size (bytes) */ + size: number; + /** MIME type e.g. "text/xml" */ + type: string; +}; + +/** Indicates a job type that the backend can do */ +export type JobType = "sparv" | "korp" | "strix" | "sync2storage"; + +/** The states a job can have */ +export type JobState = + | "none" // "Process does not exist" + | "waiting" // "Waiting to be processed" + | "running" // "Process is running" + | "done" // "Process has finished" + | "error" // "An error occurred in the process" + | "aborted"; // "Process was aborted by the user" + +/** Data in the list-exports response */ +export type ListExportsData = { + contents: FileMeta[]; +}; + +/** Data in the admin-mode-status response */ +export type AdminModeStatusData = { + admin_mode_status: boolean; +}; diff --git a/src/api/backend.composable.js b/src/api/backend.composable.ts similarity index 72% rename from src/api/backend.composable.js rename to src/api/backend.composable.ts index c1ae424..a71313a 100644 --- a/src/api/backend.composable.js +++ b/src/api/backend.composable.ts @@ -1,11 +1,7 @@ -import { reactive } from "vue"; import { useI18n } from "vue-i18n"; import { api } from "./api"; import useSpin from "@/spin/spin.composable"; -const info = reactive({}); -api.getInfo().then((values) => Object.assign(info, values)); - /** Wraps API endpoints with Spin. */ export default function useMinkBackend() { const { spin } = useSpin(); @@ -17,98 +13,100 @@ export default function useMinkBackend() { const createCorpus = () => spin(api.createCorpus(), t("corpus.creating"), "create"); - const deleteCorpus = (corpusId) => + const deleteCorpus = (corpusId: string) => spin( api.removeCorpus(corpusId), t("corpus.deleting"), `corpus/${corpusId}` ); - const loadConfig = (corpusId) => + const loadConfig = (corpusId: string) => spin( api.downloadConfig(corpusId), t("config.loading"), `corpus/${corpusId}/config` ); - const saveConfig = (corpusId, configYaml) => + const saveConfig = (corpusId: string, configYaml: string) => spin( api.uploadConfig(corpusId, configYaml), t("corpus.configuring"), `corpus/${corpusId}/config` ); - const downloadSource = (corpusId, filename, binary) => + const downloadSource = (corpusId: string, filename: string, binary = false) => spin( - api.downloadSourceFile(corpusId, filename, binary), + api.downloadSources(corpusId, filename, binary), t("source.downloading"), `corpus/${corpusId}/sources/${filename}` ); - const downloadPlaintext = (corpusId, filename) => + const downloadPlaintext = (corpusId: string, filename: string) => spin( api.downloadSourceText(corpusId, filename), t("source.downloading_plain"), `corpus/${corpusId}/sources/${filename}` ); - const uploadSources = (corpusId, files) => + const uploadSources = (corpusId: string, files: FileList) => spin( api.uploadSources(corpusId, files), t("source.uploading", files.length), `corpus/${corpusId}/sources` ); - const deleteSource = (corpusId, filename) => + const deleteSource = (corpusId: string, filename: string) => spin( api.removeSource(corpusId, filename), t("source.deleting"), `corpus/${corpusId}/sources` ); - /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Process-Corpus/operation/resourceinfo */ - const resourceInfo = (corpusId) => + const resourceInfoAll = () => + spin(api.resourceInfoAll(), t("resource.loading"), "corpora"); + + const resourceInfoOne = (corpusId: string) => spin( - api.resourceInfo(corpusId), + api.resourceInfoOne(corpusId), t("resource.loading"), - corpusId ? `corpus/${corpusId}/job` : "corpora" + `corpus/${corpusId}/job` ); - const runJob = (corpusId) => + const runJob = (corpusId: string) => spin(api.runSparv(corpusId), t("job.starting"), `corpus/${corpusId}/job`); - const installKorp = (corpusId) => + const installKorp = (corpusId: string) => spin( api.installKorp(corpusId), t("job.installing"), `corpus/${corpusId}/job` ); - const installStrix = (corpusId) => + const installStrix = (corpusId: string) => spin( api.installStrix(corpusId), t("job.installing"), `corpus/${corpusId}/job` ); - const abortJob = (corpusId) => + const abortJob = (corpusId: string) => spin(api.abortJob(corpusId), t("job.aborting"), `corpus/${corpusId}/job`); - const loadExports = (corpusId) => + const loadExports = (corpusId: string) => spin( api.listExports(corpusId), t("exports.loading"), `corpus/${corpusId}/exports` ); - const downloadExports = (corpusId) => + const downloadExports = (corpusId: string) => spin( api.downloadExports(corpusId), t("exports.downloading"), `corpus/${corpusId}/exports` ); - const downloadExportFiles = (corpusId, filename) => + const downloadExportFiles = (corpusId: string, filename: string) => spin( api.downloadExportFile(corpusId, filename), t("exports.downloading"), @@ -124,7 +122,6 @@ export default function useMinkBackend() { spin(api.adminModeOff(), "Disabling admin mode", "admin-mode"); return { - info, loadCorpusIds, createCorpus, deleteCorpus, @@ -134,7 +131,8 @@ export default function useMinkBackend() { downloadPlaintext, uploadSources, deleteSource, - resourceInfo, + resourceInfoAll, + resourceInfoOne, runJob, installKorp, installStrix, diff --git a/src/api/backendInfo.composable.js b/src/api/backendInfo.composable.js deleted file mode 100644 index 047b3d1..0000000 --- a/src/api/backendInfo.composable.js +++ /dev/null @@ -1,18 +0,0 @@ -import { computed, reactive } from "vue"; -import { api } from "./api"; - -const info = reactive({}); -api.getInfo().then((values) => Object.assign(info, values)); - -/** Wraps API endpoints with Spin. */ -export default function useMinkBackendInfo() { - const hasInfo = computed(() => Object.keys(info).length); - - const findInfo = (field, name) => - info[field]?.data.find((item) => item.name == name).value; - return { - info, - hasInfo, - findInfo, - }; -} diff --git a/src/api/backendInfo.composable.ts b/src/api/backendInfo.composable.ts new file mode 100644 index 0000000..3799185 --- /dev/null +++ b/src/api/backendInfo.composable.ts @@ -0,0 +1,58 @@ +import { computed, readonly, ref } from "vue"; +import { api } from "./api"; +import type { InfoData } from "./api.types"; +import { keyBy, objsToDict } from "@/util"; + +export type Info = { + status_codes: Record; + importer_modules: Record; + file_size_limits: Record< + InfoData["file_size_limits"]["data"][0]["name"], + { + description: string; + value: number; + } + >; + recommended_file_size: Record< + InfoData["recommended_file_size"]["data"][0]["name"], + { + description: string; + value: number; + } + >; +}; + +const info = ref(); +api.getInfo().then((original: InfoData) => { + const status_codes = objsToDict( + original.status_codes.data, + "name", + "description" + ); + const importer_modules = objsToDict( + original.importer_modules.data, + "file_extension", + "importer" + ); + const file_size_limits = keyBy(original.file_size_limits.data, "name"); + const recommended_file_size = keyBy( + original.recommended_file_size.data, + "name" + ); + info.value = { + status_codes, + importer_modules, + file_size_limits, + recommended_file_size, + }; +}); + +/** Wraps API endpoints with Spin. */ +export default function useMinkBackendInfo() { + const hasInfo = computed(() => !!Object.keys(info).length); + + return { + info: readonly(info), + hasInfo, + }; +} diff --git a/src/api/corpusConfig.js b/src/api/corpusConfig.ts similarity index 72% rename from src/api/corpusConfig.js rename to src/api/corpusConfig.ts index 31645ec..722d3dc 100644 --- a/src/api/corpusConfig.js +++ b/src/api/corpusConfig.ts @@ -1,6 +1,23 @@ const yaml = import("js-yaml").then((m) => m.default); -const FORMATS = { +import type { ByLang } from "@/util.types"; +import type { ConfigSentenceSegmenter, SparvConfig } from "./sparvConfig.types"; + +export type FileFormat = "txt" | "xml" | "odt" | "docx" | "pdf"; + +/** Frontend-internal format of a Sparv config. */ +export type ConfigOptions = { + format?: FileFormat; + name: ByLang; + description?: ByLang; + textAnnotation?: string; + sentenceSegmenter?: ConfigSentenceSegmenter; + datetimeFrom?: string; + datetimeTo?: string; + enableNer?: boolean; +}; + +const FORMATS: Record = { txt: "text_import:parse", xml: "xml_import:parse", odt: "odt_import:parse", @@ -10,9 +27,9 @@ const FORMATS = { export const FORMATS_EXT = Object.keys(FORMATS); -export const SEGMENTERS = ["linebreaks"]; +export const SEGMENTERS: ConfigSentenceSegmenter[] = ["linebreaks"]; -export async function makeConfig(id, options) { +export async function makeConfig(id: string, options: ConfigOptions) { const { format, name, @@ -23,7 +40,8 @@ export async function makeConfig(id, options) { datetimeTo, enableNer, } = options; - const config = { + + const config: Partial = { metadata: { id, name, @@ -31,6 +49,10 @@ export async function makeConfig(id, options) { }, }; + if (!format) { + throw new TypeError("File format must be set."); + } + config.import = { importer: FORMATS[format], }; @@ -106,7 +128,7 @@ export async function makeConfig(id, options) { }, }, ]; - config.export.annotations.push( + config.export.annotations!.push( ":dateformat.datefrom", ":dateformat.dateto", ":dateformat.timefrom", @@ -116,7 +138,7 @@ export async function makeConfig(id, options) { // Enable named entity recognition. if (enableNer) { - config.export.annotations.push( + config.export.annotations!.push( "swener.ne", "swener.ne:swener.name", "swener.ne:swener.ex", @@ -125,38 +147,38 @@ export async function makeConfig(id, options) { ); } - return (await yaml).dump(config); + return (await yaml).dump(config as SparvConfig); } -export function emptyConfig() { +export function emptyConfig(): ConfigOptions { return { name: { swe: "", eng: "" }, description: { swe: "", eng: "" }, }; } -export async function parseConfig(configYaml) { - const config = (await yaml).load(configYaml); +export async function parseConfig(configYaml: string) { + const config = (await yaml).load(configYaml) as SparvConfig; return { name: config.metadata?.name, description: config.metadata?.description, - format: Object.keys(FORMATS).find( - (ext) => FORMATS[ext] == config.import?.importer + format: (Object.keys(FORMATS) as FileFormat[]).find( + (ext) => FORMATS[ext as FileFormat] == config.import?.importer ), textAnnotation: config.import?.text_annotation, sentenceSegmenter: config.segment?.sentence_segmenter, datetimeFrom: config.custom_annotations?.find( - (a) => a.params.out == ":misc.datefrom" - ).params.value, + (a: any) => a.params.out == ":misc.datefrom" + )?.params.value, datetimeTo: config.custom_annotations?.find( - (a) => a.params.out == ":misc.dateto" - ).params.value, + (a: any) => a.params.out == ":misc.dateto" + )?.params.value, enableNer: config.export?.annotations?.includes("swener.ne"), }; } /** Check if the config looks ready to run. May throw anything. */ -export function validateConfig(config) { +export function validateConfig(config: ConfigOptions) { if (!config.format) { throw new TypeError("Format missing"); } diff --git a/src/api/sparvConfig.types.ts b/src/api/sparvConfig.types.ts new file mode 100644 index 0000000..e9aaec7 --- /dev/null +++ b/src/api/sparvConfig.types.ts @@ -0,0 +1,58 @@ +import type { ByLang } from "@/util.types"; + +/** Models a Sparv config file */ +export type SparvConfig = { + metadata: ConfigMetadata; + import: ConfigImport; + segment: ConfigSegment; + export: ConfigExport; + dateformat?: ConfigDateformat; + custom_annotations?: ConfigCustomAnnotations[]; + korp?: ConfigKorp; + sbx_strix?: ConfigStrix; +}; + +type ConfigMetadata = { + id: string; + name: ByLang; + description?: ByLang; +}; + +type ConfigImport = { + importer: string; + text_annotation?: string; +}; + +type ConfigSegment = { + sentence_segmenter?: ConfigSentenceSegmenter; +}; + +export type ConfigSentenceSegmenter = "linebreaks"; + +type ConfigExport = { + annotations?: string[]; + source_annotations?: string[]; +}; + +type ConfigDateformat = { + datetime_from?: string; + datetime_to?: string; + datetime_informat?: string; +}; + +type ConfigCustomAnnotations = { + annotator: string; + params: { + out: string; + chunk: string; + value: string; + }; +}; + +type ConfigKorp = { + protected?: boolean; +}; + +type ConfigStrix = { + modes?: Array<{ name: string }>; +}; diff --git a/src/auth/LoginButton.vue b/src/auth/LoginButton.vue index c18a717..ca7758b 100644 --- a/src/auth/LoginButton.vue +++ b/src/auth/LoginButton.vue @@ -1,17 +1,18 @@ + + - - diff --git a/src/auth/Login.vue b/src/auth/LoginView.vue similarity index 90% rename from src/auth/Login.vue rename to src/auth/LoginView.vue index 9e730c1..25392ec 100644 --- a/src/auth/Login.vue +++ b/src/auth/LoginView.vue @@ -1,10 +1,10 @@ + + - - diff --git a/src/auth/Signup.vue b/src/auth/SignupView.vue similarity index 97% rename from src/auth/Signup.vue rename to src/auth/SignupView.vue index 9db9a61..930b02e 100644 --- a/src/auth/Signup.vue +++ b/src/auth/SignupView.vue @@ -1,4 +1,4 @@ - + - - diff --git a/src/components/HelpBox.vue b/src/components/HelpBox.vue index 705e8d8..d92d1fa 100644 --- a/src/components/HelpBox.vue +++ b/src/components/HelpBox.vue @@ -1,9 +1,7 @@ -