From dca916e43cce5913a4e42c5a43aab96a42d53a3e Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 2 Jan 2024 16:59:40 +0100 Subject: [PATCH 01/20] Defer loadExports to quicken Library page --- src/corpus/corpus.composable.js | 3 --- src/corpus/exports/CorpusResult.vue | 11 +++++++++-- src/corpus/exports/Exports.vue | 2 ++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/corpus/corpus.composable.js b/src/corpus/corpus.composable.js index 30e56b4..2ccd194 100644 --- a/src/corpus/corpus.composable.js +++ b/src/corpus/corpus.composable.js @@ -4,7 +4,6 @@ import { useCorpusStore } from "@/store/corpus.store"; import useMessenger from "@/message/messenger.composable"; import useCorpora from "@/corpora/corpora.composable"; import useConfig from "./config/config.composable"; -import useExports from "./exports/exports.composable"; /** Let data be refreshed initially, but skip subsequent load calls. */ const isCorpusFresh = {}; @@ -16,7 +15,6 @@ export default function useCorpus(corpusId) { const { loadCorpora } = useCorpora(); const { alertError } = useMessenger(); const { loadConfig } = useConfig(corpusId); - const { loadExports } = useExports(corpusId); async function loadCorpus(force = false) { // Make sure the corpus has an entry in the store. @@ -28,7 +26,6 @@ export default function useCorpus(corpusId) { // Load all essential info about the corpus. await Promise.all([ loadConfig(), // - loadExports(), loadResourceInfo(), ]); diff --git a/src/corpus/exports/CorpusResult.vue b/src/corpus/exports/CorpusResult.vue index 5b5afcf..e7462c7 100644 --- a/src/corpus/exports/CorpusResult.vue +++ b/src/corpus/exports/CorpusResult.vue @@ -61,8 +61,15 @@ import useLocale from "@/i18n/locale.composable"; const corpusId = useCorpusIdParam(); const { filesize } = useLocale(); -const { exports, downloadResult, downloadResultFile, getDownloadFilename } = - useExports(corpusId); +const { + loadExports, + exports, + downloadResult, + downloadResultFile, + getDownloadFilename, +} = useExports(corpusId); + +loadExports(); diff --git a/src/corpus/exports/Exports.vue b/src/corpus/exports/Exports.vue index 7f4e252..89adfb9 100644 --- a/src/corpus/exports/Exports.vue +++ b/src/corpus/exports/Exports.vue @@ -89,6 +89,8 @@ const canInstall = computed( !isInstallPending.value ); +loadExports(); + async function korpInstall() { isInstallPending.value = true; await installKorp(); From f7c73799dd67b01bdeb5948205d5b879808f5e21 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 2 Jan 2024 17:34:27 +0100 Subject: [PATCH 02/20] Request resource-info once for all resources --- CHANGELOG.md | 4 ++++ src/api/api.js | 9 ++++++- src/api/backend.composable.js | 6 ++--- src/corpora/corpora.composable.js | 40 ++++++++++++++++++++++++++----- src/corpus/corpus.composable.js | 18 ++++---------- src/store/corpus.store.js | 8 +++++++ src/user/admin.composable.js | 6 ++--- 7 files changed, 65 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd6f3f..832a803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ As this project is a user-facing application, the places in the semantic version ## [Unreleased] +### Changed + +- Request `resource-info` once for all resources + ## [1.1.0] (2024-01-02) ### Added diff --git a/src/api/api.js b/src/api/api.js index 6bd1c72..aa22c77 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -95,7 +95,14 @@ class MinkApi { return response.data; } - /** @see https://ws.spraakbanken.gu.se/ws/mink/api-doc#tag/Process-Corpus/operation/resourceinfo */ + /** + * @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 + * `jobs` which is a list of info records. + */ async resourceInfo(corpusId) { const response = await this.axios.get("resource-info", { params: { corpus_id: corpusId }, diff --git a/src/api/backend.composable.js b/src/api/backend.composable.js index 2b10f0f..186ad9b 100644 --- a/src/api/backend.composable.js +++ b/src/api/backend.composable.js @@ -11,7 +11,7 @@ export default function useMinkBackend() { const { spin } = useSpin(); const { t } = useI18n(); - const loadCorpora = () => + const loadCorpusIds = () => spin(api.listCorpora(), t("corpus.list.loading"), "corpora"); const createCorpus = () => @@ -71,7 +71,7 @@ export default function useMinkBackend() { spin( api.resourceInfo(corpusId), t("resource.loading"), - `corpus/${corpusId}/job` + corpusId ? `corpus/${corpusId}/job` : "corpora" ); const runJob = (corpusId) => @@ -123,7 +123,7 @@ export default function useMinkBackend() { return { info, - loadCorpora, + loadCorpusIds, createCorpus, deleteCorpus, loadConfig, diff --git a/src/corpora/corpora.composable.js b/src/corpora/corpora.composable.js index 7f7edfb..d55ab96 100644 --- a/src/corpora/corpora.composable.js +++ b/src/corpora/corpora.composable.js @@ -13,17 +13,14 @@ export default function useCorpora() { const { alertError } = useMessenger(); const mink = useMinkBackend(); - async function doLoadCorpora() { - const corpusIds = await mink.loadCorpora().catch(alertError); - corpusStore.setCorpusIds(corpusIds); - } - async function loadCorpora(force = false) { // Skip if already loaded. if (isCorporaFresh && !force) return; // Store the pending request in module scope, so simultaneous calls will await the same promise. - if (!loadPromise) loadPromise = doLoadCorpora(); + if (!loadPromise) + // loadCorpusIds has less information, but it is faster and will update UI sooner. + loadPromise = Promise.all([loadCorpusIds(), loadResourceInfo()]); await loadPromise; // Unset the promise slot to allow any future, forced calls. @@ -32,7 +29,38 @@ export default function useCorpora() { isCorporaFresh = true; } + /** Load corpus ids and update store to match. */ + async function loadCorpusIds() { + const corpusIds = await mink.loadCorpusIds().catch(alertError); + corpusStore.setCorpusIds(corpusIds); + } + + /** Load and store data about all the user's resources. */ + async function loadResourceInfo() { + const data = await mink.resourceInfo().catch(alertError); + const corpora = {}; + for (const info of data.jobs) { + // TODO Patch instead, to avoid removing config/exports? + corpora[info.resource.id] = { + name: info.resource.name, + sources: info.resource.source_files, + status: info.job, + }; + } + // Overwrite the whole value, so removed items are dropped. + corpusStore.setCorpora(corpora); + } + + /** Signal that info needs to be reloaded, and fetch ids. */ + async function refreshCorpora() { + isCorporaFresh = false; + await loadCorpusIds(); + } + return { loadCorpora, + loadCorpusIds, + loadResourceInfo, + refreshCorpora, }; } diff --git a/src/corpus/corpus.composable.js b/src/corpus/corpus.composable.js index 2ccd194..348dbaf 100644 --- a/src/corpus/corpus.composable.js +++ b/src/corpus/corpus.composable.js @@ -23,24 +23,16 @@ export default function useCorpus(corpusId) { return; } - // Load all essential info about the corpus. - await Promise.all([ - loadConfig(), // - loadResourceInfo(), - ]); + // Load remaining essential info about the corpus. + // Skip if removed. + if (corpusId in corpusStore.corpora) { + await loadConfig(); + } // Remember to skip loading next time. isCorpusFresh[corpusId] = true; } - /** Load job status and source files in the same request. */ - async function loadResourceInfo() { - const info = await mink.resourceInfo(corpusId).catch(alertError); - corpusStore.corpora[corpusId].name = info.resource.name; - corpusStore.corpora[corpusId].sources = info.resource.source_files; - corpusStore.corpora[corpusId].status = info.job; - } - async function deleteCorpus(corpusId_ = corpusId) { // Delete corpus in the backend. await mink.deleteCorpus(corpusId_).catch(alertError); diff --git a/src/store/corpus.store.js b/src/store/corpus.store.js index 151b63f..f8024e8 100644 --- a/src/store/corpus.store.js +++ b/src/store/corpus.store.js @@ -15,6 +15,13 @@ export const useCorpusStore = defineStore("corpus", () => { setKeys(corpora, corpusIds, {}); } + function setCorpora(corporaNew) { + setKeys(corpora, Object.keys(corporaNew)); + for (const id in corporaNew) { + corpora[id] = corporaNew[id]; + } + } + function removeCorpus(corpusId) { delete corpora[corpusId]; } @@ -24,6 +31,7 @@ export const useCorpusStore = defineStore("corpus", () => { return { corpora, setCorpusIds, + setCorpora, removeCorpus, hasCorpora, }; diff --git a/src/user/admin.composable.js b/src/user/admin.composable.js index 3f732e4..38d62bc 100644 --- a/src/user/admin.composable.js +++ b/src/user/admin.composable.js @@ -8,20 +8,20 @@ const adminModeRef = ref(false); export default function useAdmin() { const { canUserAdmin } = useAuth(); - const { loadCorpora } = useCorpora(); + const { refreshCorpora } = useCorpora(); const mink = useMinkBackend(); const { alertError } = useMessenger(); async function enableAdminMode() { await mink.enableAdminMode().catch(alertError); adminModeRef.value = true; - await loadCorpora(true); + await refreshCorpora(); } async function disableAdminMode() { await mink.disableAdminMode().catch(alertError); adminModeRef.value = false; - await loadCorpora(true); + await refreshCorpora(); } return { From 5d6e2727a7e30162c15cc6ef6676c8516e6d0c3b Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 3 Jan 2024 14:03:22 +0100 Subject: [PATCH 03/20] Use new key name "resources" in resource-info response --- src/api/api.js | 2 +- src/corpora/corpora.composable.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/api.js b/src/api/api.js index aa22c77..a28ef5f 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -101,7 +101,7 @@ class MinkApi { * An info record has {resource: ..., job: ...}. * * If corpus_id is given, the response is an info record. If not, it contains - * `jobs` which is a list of info records. + * `resources` which is a list of info records. */ async resourceInfo(corpusId) { const response = await this.axios.get("resource-info", { diff --git a/src/corpora/corpora.composable.js b/src/corpora/corpora.composable.js index d55ab96..d795fa4 100644 --- a/src/corpora/corpora.composable.js +++ b/src/corpora/corpora.composable.js @@ -39,7 +39,7 @@ export default function useCorpora() { async function loadResourceInfo() { const data = await mink.resourceInfo().catch(alertError); const corpora = {}; - for (const info of data.jobs) { + for (const info of data.resources) { // TODO Patch instead, to avoid removing config/exports? corpora[info.resource.id] = { name: info.resource.name, From 9838bf6a1745924331b4588568d7946bc4d8e514 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 3 Jan 2024 14:16:25 +0100 Subject: [PATCH 04/20] Patch info records --- src/corpora/corpora.composable.js | 12 +----------- src/store/corpus.store.js | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/corpora/corpora.composable.js b/src/corpora/corpora.composable.js index d795fa4..2a3447a 100644 --- a/src/corpora/corpora.composable.js +++ b/src/corpora/corpora.composable.js @@ -38,17 +38,7 @@ export default function useCorpora() { /** Load and store data about all the user's resources. */ async function loadResourceInfo() { const data = await mink.resourceInfo().catch(alertError); - const corpora = {}; - for (const info of data.resources) { - // TODO Patch instead, to avoid removing config/exports? - corpora[info.resource.id] = { - name: info.resource.name, - sources: info.resource.source_files, - status: info.job, - }; - } - // Overwrite the whole value, so removed items are dropped. - corpusStore.setCorpora(corpora); + corpusStore.setCorpora(data.resources); } /** Signal that info needs to be reloaded, and fetch ids. */ diff --git a/src/store/corpus.store.js b/src/store/corpus.store.js index f8024e8..55ea1e2 100644 --- a/src/store/corpus.store.js +++ b/src/store/corpus.store.js @@ -15,11 +15,20 @@ export const useCorpusStore = defineStore("corpus", () => { setKeys(corpora, corpusIds, {}); } - function setCorpora(corporaNew) { - setKeys(corpora, Object.keys(corporaNew)); - for (const id in corporaNew) { - corpora[id] = corporaNew[id]; + function setCorpora(infos) { + for (const infoNew of infos) { + // Patch any existing record, otherwise create a new one. + const info = + infoNew.resource.id in corpora ? corpora[infoNew.resource.id] : {}; + info.name = infoNew.resource.name; + info.sources = infoNew.resource.source_files; + info.status = infoNew.job; + corpora[infoNew.resource.id] = info; } + + // Drop old keys. + const ids = infos.map((info) => info.resource.id); + setKeys(corpora, ids); } function removeCorpus(corpusId) { From 8677bc9287f4a140729e3348bcc26d06d4d4f2e6 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 3 Jan 2024 15:14:14 +0100 Subject: [PATCH 05/20] Check admin mode, fix #144 --- CHANGELOG.md | 4 ++++ src/api/api.js | 5 +++++ src/api/backend.composable.js | 3 +++ src/user/AdminModeBanner.vue | 10 ++++++++-- src/user/admin.composable.js | 5 +++++ 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 832a803..a6da0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ As this project is a user-facing application, the places in the semantic version ## [Unreleased] +### Added + +- Check if admin mode is enabled on load + ### Changed - Request `resource-info` once for all resources diff --git a/src/api/api.js b/src/api/api.js index a28ef5f..50fcec7 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -166,6 +166,11 @@ class MinkApi { 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; diff --git a/src/api/backend.composable.js b/src/api/backend.composable.js index 186ad9b..c1ae424 100644 --- a/src/api/backend.composable.js +++ b/src/api/backend.composable.js @@ -115,6 +115,8 @@ export default function useMinkBackend() { `corpus/${corpusId}/exports` ); + const checkAdminMode = () => spin(api.adminModeStatus(), null, "admin-mode"); + const enableAdminMode = () => spin(api.adminModeOn(), "Enabling admin mode", "admin-mode"); @@ -140,6 +142,7 @@ export default function useMinkBackend() { loadExports, downloadExports, downloadExportFiles, + checkAdminMode, enableAdminMode, disableAdminMode, }; diff --git a/src/user/AdminModeBanner.vue b/src/user/AdminModeBanner.vue index dd2a4c9..1cf176c 100644 --- a/src/user/AdminModeBanner.vue +++ b/src/user/AdminModeBanner.vue @@ -1,8 +1,14 @@ diff --git a/src/components/UrlButton.vue b/src/components/UrlButton.vue new file mode 100644 index 0000000..c23c62d --- /dev/null +++ b/src/components/UrlButton.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/corpus/exports/Exports.vue b/src/corpus/exports/Exports.vue index 89adfb9..45a2cc3 100644 --- a/src/corpus/exports/Exports.vue +++ b/src/corpus/exports/Exports.vue @@ -40,7 +40,6 @@ - {{ getDownloadFilename() }} diff --git a/src/corpus/exports/ToolPanel.vue b/src/corpus/exports/ToolPanel.vue index c113a0e..b44a972 100644 --- a/src/corpus/exports/ToolPanel.vue +++ b/src/corpus/exports/ToolPanel.vue @@ -1,5 +1,6 @@ diff --git a/src/components/UrlButton.vue b/src/components/UrlButton.vue index e40105a..00a0612 100644 --- a/src/components/UrlButton.vue +++ b/src/components/UrlButton.vue @@ -16,16 +16,3 @@ defineProps({ - - diff --git a/src/index.css b/src/index.css index d217812..43fea47 100644 --- a/src/index.css +++ b/src/index.css @@ -99,21 +99,34 @@ select { @apply p-1 px-2 rounded shadow bg-zinc-100 text-gray-700 cursor-pointer hover:drop-shadow-md hover:brightness-110 transition-all; @apply dark:bg-zinc-700 dark:text-zinc-300; } - .mink-button.mink-primary:not(.disabled) { + .mink-primary:not(.disabled) { @apply bg-blue-400 shadow-blue-700 text-white; } - .mink-button.mink-danger:not(.disabled) { + .mink-danger:not(.disabled) { @apply bg-red-400 shadow-red-700 text-white; @apply dark:bg-red-700; } - .mink-button.mink-success:not(.disabled) { + .mink-success:not(.disabled) { @apply bg-green-300 shadow-green-600 text-green-900; } - .mink-button.mink-warning:not(.disabled) { + .mink-warning:not(.disabled) { @apply bg-amber-300 shadow-amber-600 text-amber-900; } + .mink-button.disabled { @apply bg-slate-100 text-slate-400 cursor-not-allowed hover:drop-shadow-none hover:brightness-100; @apply dark:bg-slate-600 dark:text-gray-400; } + + .mute { + @apply text-inherit border-0 shadow-none; + } + .mute:not(:hover) { + @apply !bg-transparent !text-inherit; + } + + .slim { + @apply p-0 px-1; + } + } From 95c89336e557c226043b413a89e19a197f8be155 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 3 Jan 2024 18:25:06 +0100 Subject: [PATCH 11/20] ActionButton use