Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
arildm committed Sep 18, 2023
2 parents 41c82f6 + 2edc47a commit 157b81b
Show file tree
Hide file tree
Showing 18 changed files with 351 additions and 168 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
BASE=/mink/
VITE_BACKEND_URL=https://ws.spraakbanken.gu.se/ws/mink/
VITE_AUTH_URL=https://sp.spraakbanken.gu.se/auth/
VITE_KORP_URL=https://spraakbanken.gu.se/korp/
VITE_STRIX_URL=https://spraakbanken.gu.se/strix/
14 changes: 13 additions & 1 deletion src/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ class MinkApi {
return response.data;
}

async checkStatusAll() {
const response = await this.axios.get("check-status");
return response.data.jobs;
}

async runSparv(corpusId) {
const response = await this.axios
.put("run-sparv", null, { params: { corpus_id: corpusId } })
Expand Down Expand Up @@ -154,13 +159,20 @@ class MinkApi {
return response.data;
}

async installCorpus(corpusId) {
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 adminModeOn() {
const response = await this.axios.post("admin-mode-on");
return response.data;
Expand Down
18 changes: 15 additions & 3 deletions src/api/backend.composable.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,22 @@ export default function useMinkBackend() {
const loadJob = (corpusId) =>
spin(api.checkStatus(corpusId), t("job.loading"), `corpus/${corpusId}/job`);

const loadJobs = () =>
spin(api.checkStatusAll(), t("job.loading"), `corpora`);

const runJob = (corpusId) =>
spin(api.runSparv(corpusId), t("job.starting"), `corpus/${corpusId}/job`);

const install = (corpusId) =>
const installKorp = (corpusId) =>
spin(
api.installKorp(corpusId),
t("job.installing"),
`corpus/${corpusId}/job`
);

const installStrix = (corpusId) =>
spin(
api.installCorpus(corpusId),
api.installStrix(corpusId),
t("job.installing"),
`corpus/${corpusId}/job`
);
Expand Down Expand Up @@ -129,8 +139,10 @@ export default function useMinkBackend() {
uploadSources,
deleteSource,
loadJob,
loadJobs,
runJob,
install,
installKorp,
installStrix,
abortJob,
loadExports,
downloadExports,
Expand Down
1 change: 1 addition & 0 deletions src/api/corpusConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export async function makeConfig(id, options) {
"<text>:readability.ovix",
"<text>:readability.nk",
"<text>:misc.source",
"<text>:misc.id as _id",
],
};

Expand Down
10 changes: 5 additions & 5 deletions src/auth/auth.composable.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ export function useAuth() {

const isAuthenticated = computed(() => !!jwt.value);
const payload = computed(() => decodeJwt(jwt.value)?.payload);
const canUserAdmin = computed(() =>
canAdmin(payload.value, "other", "mink-app")
const canUserAdmin = computed(
() => payload.value && canAdmin(payload.value, "other", "mink-app")
);
const canUserWrite = computed(() =>
canWrite(payload.value, "other", "mink-app")
const canUserWrite = computed(
() => payload.value && canWrite(payload.value, "other", "mink-app")
);

/** If not authenticated, redirect to the login page. */
Expand Down Expand Up @@ -66,7 +66,7 @@ export function useAuth() {

// Schedule next request shortly before expiration time.
clearTimeout(refreshTimer);
if (payload.value) {
if (payload.value && payload.value.exp) {
const timeoutMs = (payload.value.exp - 10) * 1000 - Date.now();
refreshTimer = setTimeout(refreshJwt, timeoutMs);
}
Expand Down
7 changes: 4 additions & 3 deletions src/auth/auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { pathJoin } from "@/util";

export const AUTH_BASE = "https://sp.spraakbanken.gu.se/auth";
export const AUTH_BASE = import.meta.env.VITE_AUTH_URL;

export function getLoginUrl(redirectLocation = "") {
// Prepend redirect location with Mink base url.
Expand All @@ -9,11 +9,12 @@ export function getLoginUrl(redirectLocation = "") {
import.meta.env.BASE_URL,
redirectLocation
);
return `${AUTH_BASE}/login?redirect=${redirectLocation}`;
return AUTH_BASE + `login?redirect=${redirectLocation}`;
}

export async function checkLogin() {
const response = await fetch(`${AUTH_BASE}/jwt`, { credentials: "include" });
const url = import.meta.env.VITE_JWT_URL || AUTH_BASE + "jwt";
const response = await fetch(url, { credentials: "include" });
if (response.ok) return await response.text();
return false;
}
Expand Down
23 changes: 20 additions & 3 deletions src/auth/jwtSb.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,31 @@ export function decodeJwt(jwt) {
};
}

export function assertValidPayload(payload) {
const isValid =
payload &&
payload.scope &&
payload.levels &&
payload.levels.ADMIN &&
payload.levels.WRITE &&
payload.levels.READ;

if (!isValid) {
throw new TypeError("Malformed jwt payload: " + JSON.stringify(payload));
}
}

export function canAdmin(payload, resourceType, resourceName) {
return payload?.scope[resourceType]?.[resourceName] >= payload?.levels.ADMIN;
assertValidPayload(payload);
return payload.scope[resourceType]?.[resourceName] >= payload.levels.ADMIN;
}

export function canWrite(payload, resourceType, resourceName) {
return payload?.scope[resourceType]?.[resourceName] >= payload?.levels.WRITE;
assertValidPayload(payload);
return payload.scope[resourceType]?.[resourceName] >= payload.levels.WRITE;
}

export function canRead(payload, resourceType, resourceName) {
return payload?.scope[resourceType]?.[resourceName] >= payload?.levels.READ;
assertValidPayload(payload);
return payload.scope[resourceType]?.[resourceName] >= payload.levels.READ;
}
5 changes: 5 additions & 0 deletions src/corpora/CreateCorpus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
validate="required"
/>

<HelpBox v-if="value.format === 'pdf'" important>
<icon :icon="['far', 'lightbulb']" class="mr-1" />
{{ $t("config.format.note.pdf") }}
</HelpBox>

<FormKit
v-if="value.format === 'xml'"
name="textAnnotation"
Expand Down
104 changes: 25 additions & 79 deletions src/corpora/corpora.composable.js
Original file line number Diff line number Diff line change
@@ -1,101 +1,47 @@
import { useRouter } from "vue-router";
import { useAuth } from "@/auth/auth.composable";
import { emptyConfig } from "@/api/corpusConfig";
import useConfig from "@/corpus/config/config.composable";
import useSources from "@/corpus/sources/sources.composable";
import useMessenger from "@/message/messenger.composable";
import useMinkBackend from "@/api/backend.composable";
import useCorpus from "@/corpus/corpus.composable";
import { useCorpusStore } from "@/store/corpus.store";
import useMessenger from "@/message/messenger.composable";

/** Let corpus list be refreshed initially, but skip subsequent load calls. */
let isCorporaFresh = false;

/** Use this module-scope variable for the request, so that simultaneous calls don't procude multiple requests. */
let loadPromise = null;

export default function useCorpora() {
const corpusStore = useCorpusStore();
const router = useRouter();
const { refreshJwt } = useAuth();
const { deleteCorpus } = useCorpus();
const { uploadConfig } = useConfig();
const { uploadSources } = useSources();
const { alert, alertError } = useMessenger();
const { alertError } = useMessenger();
const mink = useMinkBackend();

async function loadCorpora(force = false) {
if (isCorporaFresh && !force) return;
const corpora = await mink.loadCorpora().catch(alertError);
corpusStore.setCorpusIds(corpora);
isCorporaFresh = true;
}

async function createCorpus() {
const corpusId = await mink.createCorpus().catch(alertError);
// Have the new corpus included in further API calls.
await refreshJwt();
// Adding the new id to store may trigger API calls, so do it after updating the JWT.
corpusStore.corpora[corpusId] = corpusStore.corpora[corpusId] || {};
return corpusId;
}

async function createFromUpload(files) {
const corpusId = await createCorpus().catch(alertError);
if (!corpusId) return;

const results = await Promise.allSettled([
uploadSources(files, corpusId),
uploadConfig(emptyConfig(), corpusId),
]);

const rejectedResults = results.filter(
(result) => result.status != "fulfilled"
);
if (rejectedResults.length) {
// Display error message(s).
rejectedResults.forEach((result) => alertError(result.reason));
// Discard the empty corpus.
await deleteCorpus(corpusId).catch(alertError);
return;
// Store the pending request in module scope, so simultaneous calls will await the same promise.
if (!loadPromise) {
loadPromise = mink
.loadCorpora()
.catch(alertError)
.then((corpora) => corpusStore.setCorpusIds(corpora));

// This request can take some time, so better not await it.
mink
.loadJobs()
.catch(alertError)
.then((jobs) =>
jobs.forEach((job) => {
corpusStore.corpora[job.corpus_id].status = job;
corpusStore.corpora[job.corpus_id].sources = job.available_files;
})
);
}

router.push(`/corpus/${corpusId}`);
}

async function createFromConfig(name, description, format, textAnnotation) {
const config = {
name: { swe: name, eng: name },
description: { swe: description, eng: description },
format,
textAnnotation,
};
await loadPromise;

// Create an empty corpus. If it fails, abort.
let corpusId;
try {
corpusId = await createCorpus().catch(alertError);
} catch (e) {
alertError(e);
return;
}

// Upload the basic config.
try {
await uploadConfig(config, corpusId);
// Show the created corpus.
router.push(`/corpus/${corpusId}`);
return corpusId;
} catch (e) {
// If creating the config fails, there's a TypeError.
if (e.name == "TypeError") alert(e.message, "error");
// Otherwise it's probably a backend error when saving.
else alertError(e);
// Discard the empty corpus.
await deleteCorpus(corpusId).catch(alertError);
}
loadPromise = null;
isCorporaFresh = true;
}

return {
loadCorpora,
createFromUpload,
createFromConfig,
};
}
3 changes: 0 additions & 3 deletions src/corpus/Corpus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,19 @@ import { computed } from "vue";
import { useAuth } from "@/auth/auth.composable";
import { useCorpusStore } from "@/store/corpus.store";
import useCorpusIdParam from "./corpusIdParam.composable";
import useCorpora from "@/corpora/corpora.composable";
import useCorpus from "./corpus.composable.js";
import useConfig from "./config/config.composable";
import PageTitle from "@/components/PageTitle.vue";
const corpusStore = useCorpusStore();
const { requireAuthentication, isAuthenticated } = useAuth();
const corpusId = useCorpusIdParam();
const { loadCorpora } = useCorpora();
const { loadCorpus } = useCorpus(corpusId);
const { corpusName } = useConfig(corpusId);
const corpus = computed(() => corpusStore.corpora[corpusId]);
requireAuthentication(async () => {
await loadCorpora();
await loadCorpus();
});
</script>
Expand Down
8 changes: 7 additions & 1 deletion src/corpus/config/CorpusConfiguration.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
:help="$t('config.format.help')"
/>

<HelpBox v-if="value.format === 'pdf'" important>
<icon :icon="['far', 'lightbulb']" class="mr-1" />
{{ $t("config.format.note.pdf") }}
</HelpBox>

<FormKit
v-if="value.format === 'xml'"
name="textAnnotation"
Expand Down Expand Up @@ -114,7 +119,8 @@ const formatOptions = computed(() =>
);
const selectedFormat = computed(() => {
return extensions.value.includes(config.value.format)
return !extensions.value.length ||
extensions.value.includes(config.value.format)
? config.value.format
: undefined;
});
Expand Down
12 changes: 4 additions & 8 deletions src/corpus/corpus.composable.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { useAuth } from "@/auth/auth.composable";
import useMinkBackend from "@/api/backend.composable";
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";
import useJob from "./job/job.composable";
import useSources from "./sources/sources.composable";

/** Let data be refreshed initially, but skip subsequent load calls. */
const isCorpusFresh = {};
Expand All @@ -14,22 +13,19 @@ export default function useCorpus(corpusId) {
const corpusStore = useCorpusStore();
const { refreshJwt } = useAuth();
const mink = useMinkBackend();
const { loadCorpora } = useCorpora();
const { alertError } = useMessenger();
const { loadConfig } = useConfig(corpusId);
const { loadExports } = useExports(corpusId);
const { loadJob } = useJob(corpusId);
const { loadSources } = useSources(corpusId);

async function loadCorpus(force = false) {
const isLoaded = Object.keys(corpusStore.corpora[corpusId]).length;
if (isLoaded && isCorpusFresh[corpusId] && !force) {
await loadCorpora();
if (isCorpusFresh[corpusId] && !force) {
return;
}
await Promise.all([
loadConfig(), //
loadExports(),
loadJob(),
loadSources(),
]);
isCorpusFresh[corpusId] = true;
}
Expand Down
Loading

0 comments on commit 157b81b

Please sign in to comment.