From f23c738d669f51efed32ee9d1717e7e208196c78 Mon Sep 17 00:00:00 2001 From: ml bonhomme <bonhomme@teklia.com> Date: Wed, 23 Aug 2023 11:00:42 +0000 Subject: [PATCH] Piniaize the model store --- package-lock.json | 128 +++++------ package.json | 2 +- src/api/model.ts | 12 +- src/components/Model/ModelPicker.vue | 44 ++-- src/components/Model/Selection.vue | 10 +- src/components/Model/Versions/DeleteModal.vue | 25 ++- src/components/Model/Versions/List.vue | 31 +-- .../Model/Versions/ModelVersionPicker.vue | 46 ++-- src/components/Model/Versions/Row.vue | 17 +- src/helpers/index.ts | 2 +- src/store/index.js | 9 +- src/store/model.js | 85 ------- src/stores/index.ts | 1 + src/stores/model.ts | 72 ++++++ src/types/index.ts | 15 +- src/types/model.ts | 14 ++ src/views/Model/Create.vue | 52 +++-- src/views/Model/List.vue | 39 ++-- src/views/Model/Model.vue | 17 +- src/views/Model/Version.vue | 18 +- tests/unit/store/auth.spec.js | 5 - tests/unit/store/elements.spec.js | 3 +- tests/unit/store/index.spec.js | 2 - tests/unit/store/model.spec.js | 212 ------------------ tests/unit/stores/model.spec.js | 156 +++++++++++++ 25 files changed, 497 insertions(+), 520 deletions(-) delete mode 100644 src/store/model.js create mode 100644 src/stores/model.ts delete mode 100644 tests/unit/store/model.spec.js create mode 100644 tests/unit/stores/model.spec.js diff --git a/package-lock.json b/package-lock.json index 2eb80eead..8c6385249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "arkindex", - "version": "1.5.1-alpha1", + "version": "1.5.1-beta1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "arkindex", - "version": "1.5.1-alpha1", + "version": "1.5.1-beta1", "license": "MIT", "dependencies": { "@sentry/integrations": "^7.16.0", "@sentry/vue": "^7.16.0", "ansi-to-html": "^0.7.2", - "axios": "^1.1.3", + "axios": "^1.4.0", "bulma": "^0.9.3", "bulma-switch": "^2.0.0", "bulma-tooltip": "^3.0.2", @@ -3602,6 +3602,26 @@ } } }, + "node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15": { + "name": "vue-loader", + "version": "15.10.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz", + "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", + "dev": true, + "dependencies": { + "@vue/component-compiler-utils": "^3.1.0", + "hash-sum": "^1.0.2", + "loader-utils": "^1.1.0", + "vue-hot-reload-api": "^2.3.0", + "vue-style-loader": "^4.1.0" + } + }, + "node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, "node_modules/@vue/cli-shared-utils": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz", @@ -4012,38 +4032,6 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz", "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==" }, - "node_modules/@vue/vue-loader-v15": { - "name": "vue-loader", - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz", - "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", - "dev": true, - "dependencies": { - "@vue/component-compiler-utils": "^3.1.0", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" - }, - "peerDependencies": { - "css-loader": "*", - "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "cache-loader": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/@vue/vue-loader-v15/node_modules/hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", - "dev": true - }, "node_modules/@vue/web-component-wrapper": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz", @@ -4518,6 +4506,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-3.3.2.tgz", "integrity": "sha512-L5TiS8E2Hn/Yz7SSnWIVbZw0ZfEIXZCa5VUiVxD9P53JvSrf4aStvsFDlGWPvpIdCR+aly2CfoB79B9/JjKFqg==", + "deprecated": "The `apollo-datasource` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", "dev": true, "dependencies": { "@apollo/utils.keyvaluecache": "^1.0.1", @@ -4531,6 +4520,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-3.3.3.tgz", "integrity": "sha512-L3+DdClhLMaRZWVmMbBcwl4Ic77CnEBPXLW53F7hkYhkaZD88ivbCVB1w/x5gunO6ZHrdzhjq0FHmTsBvPo7aQ==", + "deprecated": "The `apollo-reporting-protobuf` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/usage-reporting-protobuf` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", "dev": true, "dependencies": { "@apollo/protobufjs": "1.2.6" @@ -4540,6 +4530,7 @@ "version": "3.11.1", "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-3.11.1.tgz", "integrity": "sha512-t/eCKrRFK1lYZlc5pHD99iG7Np7CEm3SmbDiONA7fckR3EaB/pdsEdIkIwQ5QBBpT5JLp/nwvrZRVwhaWmaRvw==", + "deprecated": "The `apollo-server-core` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", "dev": true, "dependencies": { "@apollo/utils.keyvaluecache": "^1.0.1", @@ -4586,6 +4577,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-4.2.1.tgz", "integrity": "sha512-vm/7c7ld+zFMxibzqZ7SSa5tBENc4B0uye9LTfjJwGoQFY5xsUPH5FpO5j0bMUDZ8YYNbrF9SNtzc5Cngcr90g==", + "deprecated": "The `apollo-server-env` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/utils.fetcher` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", "dev": true, "dependencies": { "node-fetch": "^2.6.7" @@ -4598,6 +4590,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-3.3.1.tgz", "integrity": "sha512-xnZJ5QWs6FixHICXHxUfm+ZWqqxrNuPlQ+kj5m6RtEgIpekOPssH/SD9gf2B4HuWV0QozorrygwZnux8POvyPA==", + "deprecated": "The `apollo-server-errors` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", "dev": true, "engines": { "node": ">=12.0" @@ -4610,6 +4603,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-3.10.0.tgz", "integrity": "sha512-ww3tZq9I/x3Oxtux8xlHAZcSB0NNQ17lRlY6yCLk1F+jCzdcjuj0x8XNg0GdTrMowt5v43o786bU9VYKD5OVnA==", + "deprecated": "The `apollo-server-express` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", "dev": true, "dependencies": { "@types/accepts": "^1.3.5", @@ -4636,6 +4630,7 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-3.7.1.tgz", "integrity": "sha512-g3vJStmQtQvjGI289UkLMfThmOEOddpVgHLHT2bNj0sCD/bbisj4xKbBHETqaURokteqSWyyd4RDTUe0wAUDNQ==", + "deprecated": "The `apollo-server-plugin-base` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", "dev": true, "dependencies": { "apollo-server-types": "^3.7.1" @@ -4651,6 +4646,7 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-3.7.1.tgz", "integrity": "sha512-aE9RDVplmkaOj/OduNmGa+0a1B5RIWI0o3zC1zLvBTVWMKTpo0ifVf11TyMkLCY+T7cnZqVqwyShziOyC3FyUw==", + "deprecated": "The `apollo-server-types` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", "dev": true, "dependencies": { "@apollo/utils.keyvaluecache": "^1.0.1", @@ -4913,9 +4909,9 @@ } }, "node_modules/axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -6262,6 +6258,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", "dev": true, "dependencies": { "bluebird": "^3.1.1" @@ -16201,6 +16198,7 @@ "version": "2.2.16", "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, "dependencies": { "nanoid": "^2.1.0" @@ -16531,7 +16529,8 @@ "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, "node_modules/spdx-correct": { "version": "3.1.1", @@ -18370,6 +18369,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", "dev": true, "dependencies": { "browser-process-hrtime": "^1.0.0" @@ -22131,6 +22131,29 @@ "webpack-merge": "^5.7.3", "webpack-virtual-modules": "^0.4.2", "whatwg-fetch": "^3.6.2" + }, + "dependencies": { + "@vue/vue-loader-v15": { + "version": "npm:vue-loader@15.10.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz", + "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", + "dev": true, + "requires": { + "@vue/component-compiler-utils": "^3.1.0", + "hash-sum": "^1.0.2", + "loader-utils": "^1.1.0", + "vue-hot-reload-api": "^2.3.0", + "vue-style-loader": "^4.1.0" + }, + "dependencies": { + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + } + } + } } }, "@vue/cli-shared-utils": { @@ -22456,27 +22479,6 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz", "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==" }, - "@vue/vue-loader-v15": { - "version": "npm:vue-loader@15.10.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz", - "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", - "dev": true, - "requires": { - "@vue/component-compiler-utils": "^3.1.0", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" - }, - "dependencies": { - "hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", - "dev": true - } - } - }, "@vue/web-component-wrapper": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz", @@ -23141,9 +23143,9 @@ } }, "axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", diff --git a/package.json b/package.json index 402e57505..38b1e9630 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@sentry/integrations": "^7.16.0", "@sentry/vue": "^7.16.0", "ansi-to-html": "^0.7.2", - "axios": "^1.1.3", + "axios": "^1.4.0", "bulma": "^0.9.3", "bulma-switch": "^2.0.0", "bulma-tooltip": "^3.0.2", diff --git a/src/api/model.ts b/src/api/model.ts index 4f422878e..3d4fb3282 100644 --- a/src/api/model.ts +++ b/src/api/model.ts @@ -1,13 +1,13 @@ import axios from 'axios' -import { ModelVersion, PageNumberPagination, UUID } from '@/types' -import { Model } from '@/types/model' +import { PageNumberPagination, UUID } from '@/types' +import { Model, ModelVersion } from '@/types/model' import { PageNumberPaginationParameters, unique } from '.' -type CreateModelPayload = Pick<Model, 'name' | 'description'> +export type CreateModelPayload = Pick<Model, 'name'> & Partial<Pick<Model, 'description'>> export const createModel = unique(async (params: CreateModelPayload): Promise<Model> => (await axios.post('/models/', params)).data) -interface ModelListParameters extends PageNumberPaginationParameters { +export interface ModelListParameters extends PageNumberPaginationParameters { /** * Restrict to models declared as compatible with a worker ID */ @@ -19,11 +19,11 @@ interface ModelListParameters extends PageNumberPaginationParameters { name?: string } -export const listModels = unique(async (params: ModelListParameters): Promise<PageNumberPagination<Model[]>> => (await axios.get('/models/', { params })).data) +export const listModels = unique(async (params: ModelListParameters): Promise<PageNumberPagination<Model>> => (await axios.get('/models/', { params })).data) export const retrieveModel = unique(async (id: UUID): Promise<Model> => (await axios.get(`/model/${id}/`)).data) -export const listModelVersions = unique(async (modelId: UUID, params: PageNumberPaginationParameters): Promise<ModelVersion[]> => (await axios.get(`/model/${modelId}/versions/`, { params })).data) +export const listModelVersions = unique(async (modelId: UUID, params: PageNumberPaginationParameters): Promise<PageNumberPagination<ModelVersion>> => (await axios.get(`/model/${modelId}/versions/`, { params })).data) export const deleteModelVersion = unique(async (id: UUID) => (await axios.delete(`/modelversion/${id}/`))) diff --git a/src/components/Model/ModelPicker.vue b/src/components/Model/ModelPicker.vue index 36e8b22cf..a0da34592 100644 --- a/src/components/Model/ModelPicker.vue +++ b/src/components/Model/ModelPicker.vue @@ -106,18 +106,22 @@ </div> </template> -<script> -import { mapActions, mapMutations } from 'vuex' -import Paginator from '@/components/Paginator.vue' +<script lang="ts"> +import { defineComponent, PropType } from 'vue' +import { mapActions } from 'pinia' + +import { ModelListParameters } from '@/api' import { errorParser } from '@/helpers' -import Modal from '@/components/Modal' -import CreateModel from '@/views/Model/Create.vue' import { truncateMixin } from '@/mixins' +import { useModelStore, useNotificationStore } from '@/stores' +import { Model } from '@/types/model' +import { PageNumberPagination, UUID } from '@/types' + +import Paginator from '@/components/Paginator.vue' +import Modal from '@/components/Modal.vue' +import CreateModel from '@/views/Model/Create.vue' -/* - * A component allowing to pick a specific model - */ -export default { +export default defineComponent({ mixins: [ truncateMixin ], @@ -126,10 +130,14 @@ export default { Modal, CreateModel }, - emits: ['update:modelValue'], + emits: { + 'update:modelValue' (value: Model | null) { + return value === null || value.id !== undefined + } + }, props: { modelValue: { - type: Object, + type: Object as PropType<Model | null>, default: null }, placeholder: { @@ -137,7 +145,7 @@ export default { default: 'Pick a model…' }, compatibleWorkerId: { - type: String, + type: String as PropType<UUID>, default: '' } }, @@ -145,18 +153,18 @@ export default { opened: false, loading: false, page: 1, - modelsPage: null, + modelsPage: null as PageNumberPagination<Model> | null, nameFilter: '', creationModal: false, allModels: false }), methods: { - ...mapMutations('notifications', ['notify']), - ...mapActions('model', ['listModels']), + ...mapActions(useNotificationStore, ['notify']), + ...mapActions(useModelStore, ['listModels']), async updateModelsPage () { this.loading = true try { - const payload = { page: this.page } + const payload: ModelListParameters = { page: this.page } if (this.nameFilter) payload.name = this.nameFilter if (this.compatibleWorkerId && !this.allModels) payload.compatible_worker = this.compatibleWorkerId this.modelsPage = await this.listModels(payload) @@ -171,7 +179,7 @@ export default { if (this.page === 1) return this.updateModelsPage() this.page = 1 }, - selectModel (model) { + selectModel (model: Model) { this.$emit('update:modelValue', model) // Automatically close modals upon selection or creation this.opened = false @@ -192,7 +200,7 @@ export default { handler: 'updateModelsPage' } } -} +}) </script> <style scoped lang="scss"> diff --git a/src/components/Model/Selection.vue b/src/components/Model/Selection.vue index 43a210428..dc7401dd8 100644 --- a/src/components/Model/Selection.vue +++ b/src/components/Model/Selection.vue @@ -33,9 +33,11 @@ </template> <script> -import { mapState, mapActions } from 'vuex' +import { mapState as mapVuexState } from 'vuex' +import { mapState, mapActions } from 'pinia' import Modal from '@/components/Modal.vue' import ModelList from '@/views/Model/List' +import { useModelStore } from '@/stores' export default { components: { @@ -68,8 +70,8 @@ export default { openModal: false }), computed: { - ...mapState('process', ['processWorkerRuns']), - ...mapState('model', ['models', 'modelVersions']), + ...mapVuexState('process', ['processWorkerRuns']), + ...mapState(useModelStore, ['models', 'modelVersions']), needModel () { return this.modelNeeded && !this.selectedModelVersionId }, @@ -99,7 +101,7 @@ export default { } }, methods: { - ...mapActions('model', ['getModelVersion', 'retrieveModel']) + ...mapActions(useModelStore, ['getModelVersion', 'retrieveModel']) }, watch: { selectedModelVersionId: { diff --git a/src/components/Model/Versions/DeleteModal.vue b/src/components/Model/Versions/DeleteModal.vue index 029bbf999..7f09e6ad6 100644 --- a/src/components/Model/Versions/DeleteModal.vue +++ b/src/components/Model/Versions/DeleteModal.vue @@ -2,7 +2,7 @@ <span> <button class="button is-danger is-small" - :disabled="!canDelete || null" + :disabled="!canDelete || undefined" v-on:click.prevent="open" :title="canDelete ? 'Delete this model version' : 'You are not allowed to delete this version.'" > @@ -23,7 +23,7 @@ <button class="button is-danger" :class="{ 'is-loading': loading }" - :disabled="loading || !canDelete || null" + :disabled="loading || !canDelete || undefined" v-on:click.prevent="performDelete" > Delete @@ -33,18 +33,21 @@ </span> </template> -<script> +<script lang="ts"> +import { PropType, defineComponent } from 'vue' import Modal from '@/components/Modal.vue' -import { mapState, mapActions, mapMutations } from 'vuex' +import { mapState, mapActions } from 'pinia' +import { useModelStore, useNotificationStore } from '@/stores' +import { ModelVersion } from '@/types/model' -export default { +export default defineComponent({ components: { Modal }, props: { // The model version to delete. version: { - type: Object, + type: Object as PropType<ModelVersion>, required: true } }, @@ -53,7 +56,7 @@ export default { loading: false }), computed: { - ...mapState('model', ['models']), + ...mapState(useModelStore, ['models']), modelName () { return this.models[this.version.model_id].name }, @@ -62,8 +65,8 @@ export default { } }, methods: { - ...mapActions('model', ['deleteModelVersion']), - ...mapMutations('notifications', ['notify']), + ...mapActions(useModelStore, ['deleteModelVersion']), + ...mapActions(useNotificationStore, ['notify']), open () { this.opened = this.canDelete }, @@ -71,12 +74,12 @@ export default { if (!this.canDelete || this.loading) return this.loading = true try { - await this.deleteModelVersion({ versionId: this.version.id }) + await this.deleteModelVersion(this.version.id) this.opened = false } finally { this.loading = false } } } -} +}) </script> diff --git a/src/components/Model/Versions/List.vue b/src/components/Model/Versions/List.vue index 1d32515a3..046ae1bce 100644 --- a/src/components/Model/Versions/List.vue +++ b/src/components/Model/Versions/List.vue @@ -34,13 +34,18 @@ </div> </template> -<script> -import { mapActions, mapMutations, mapState } from 'vuex' +<script lang="ts"> +import { defineComponent } from 'vue' +import { mapActions, mapState } from 'pinia' import { ensureArray, errorParser } from '@/helpers' import Paginator from '@/components/Paginator.vue' -import Row from './Row' +import Row from './Row.vue' +import { useModelStore } from '@/stores/model' +import { useNotificationStore } from '@/stores' +import { ModelVersion } from '@/types/model' +import { PageNumberPagination } from '@/types' -export default { +export default defineComponent({ components: { Paginator, Row @@ -61,30 +66,26 @@ export default { } }, data: () => ({ - versionsError: null, - versionsPage: null, + versionsError: null as string | null, + versionsPage: null as PageNumberPagination<ModelVersion> | null, loading: false, page: 1 }), computed: { - ...mapState('model', ['modelVersions']), + ...mapState(useModelStore, ['modelVersions']), modelVersionCount () { return ensureArray(this.modelVersions).filter(version => version.model_id === this.modelId).length } }, methods: { - ...mapMutations('notifications', ['notify']), - ...mapActions('model', ['listModelVersions']), + ...mapActions(useNotificationStore, ['notify']), + ...mapActions(useModelStore, ['listModelVersions']), async fetchVersions () { this.versionsPage = null this.versionsError = null this.loading = true - const payload = { - modelId: this.modelId, - page: this.page - } try { - this.versionsPage = await this.listModelVersions(payload) + this.versionsPage = await this.listModelVersions(this.modelId, this.page) } catch (err) { this.versionsError = errorParser(err) this.notify({ type: 'error', text: `An error occurred listing model versions: ${this.versionsError}` }) @@ -104,5 +105,5 @@ export default { } } } -} +}) </script> diff --git a/src/components/Model/Versions/ModelVersionPicker.vue b/src/components/Model/Versions/ModelVersionPicker.vue index c6a4c6555..f72f46243 100644 --- a/src/components/Model/Versions/ModelVersionPicker.vue +++ b/src/components/Model/Versions/ModelVersionPicker.vue @@ -45,31 +45,37 @@ </div> </template> -<script> -import { mapActions, mapMutations } from 'vuex' -import Row from './Row' -import Paginator from '@/components/Paginator.vue' +<script lang="ts"> +import { defineComponent, PropType } from 'vue' +import { mapActions } from 'pinia' + import { errorParser } from '@/helpers' -import Modal from '@/components/Modal' +import { useModelStore, useNotificationStore } from '@/stores' +import { PageNumberPagination, UUID } from '@/types' +import { ModelVersion } from '@/types/model' + +import Paginator from '@/components/Paginator.vue' +import Modal from '@/components/Modal.vue' +import Row from './Row.vue' -/* - * A component allowing to pick a specific model - */ -export default { +export default defineComponent({ components: { Row, Paginator, Modal }, - emits: ['update:modelValue'], + emits: { + 'update:modelValue' (value: ModelVersion | null) { + return value === null || value.id !== undefined + } + }, props: { modelId: { type: String, required: true }, - // The worker version object modelValue: { - type: Object, + type: Object as PropType<ModelVersion | null>, default: null }, placeholder: { @@ -81,9 +87,7 @@ export default { opened: false, loading: false, page: 1, - versionsPage: null, - nameFilter: '', - selectedVersion: null + versionsPage: null as PageNumberPagination<ModelVersion> | null }), computed: { shortId () { @@ -91,26 +95,26 @@ export default { } }, methods: { - ...mapMutations('notifications', ['notify']), - ...mapActions('model', ['listModelVersions']), + ...mapActions(useNotificationStore, ['notify']), + ...mapActions(useModelStore, ['listModelVersions']), async updateVersionsPage () { this.loading = true try { - this.versionsPage = await this.listModelVersions({ modelId: this.modelId, page: this.page }) + this.versionsPage = await this.listModelVersions(this.modelId, this.page) } catch (err) { this.notify({ type: 'error', text: `An error occurred listing model versions: ${errorParser(err)}` }) } finally { this.loading = false } }, - selectVersion (version) { + selectVersion (version: ModelVersion) { this.$emit('update:modelValue', version) } }, watch: { modelId: { immediate: true, - handler (newValue) { + handler (newValue: UUID) { // The ModelVersion gets deselected if the new model ID does not match it if (this.modelValue?.model_id !== newValue) this.$emit('update:modelValue', null) this.updateVersionsPage() @@ -120,5 +124,5 @@ export default { handler: 'updateVersionsPage' } } -} +}) </script> diff --git a/src/components/Model/Versions/Row.vue b/src/components/Model/Versions/Row.vue index 10af370f5..10853dc91 100644 --- a/src/components/Model/Versions/Row.vue +++ b/src/components/Model/Versions/Row.vue @@ -50,9 +50,16 @@ </template> <script> -import { mapState, mapActions, mapMutations } from 'vuex' +import { + mapState as mapVuexState, + mapActions as mapVuexActions +} from 'vuex' +import { mapState, mapActions } from 'pinia' + import { MODEL_VERSION_STATE_COLORS } from '@/config' import { ago } from '@/helpers/text' +import { useModelStore, useNotificationStore } from '@/stores' + import ItemId from '@/components/ItemId.vue' import DeleteModal from './DeleteModal' @@ -96,8 +103,8 @@ export default { loading: false }), computed: { - ...mapState('model', ['models']), - ...mapState('process', ['processWorkerRuns']), + ...mapState(useModelStore, ['models']), + ...mapVuexState('process', ['processWorkerRuns']), createdDate () { if (!this.version) return return new Date(this.version.created) @@ -128,8 +135,8 @@ export default { } }, methods: { - ...mapMutations('notifications', ['notify']), - ...mapActions('process', ['updateWorkerRun']), + ...mapActions(useNotificationStore, ['notify']), + ...mapVuexActions('process', ['updateWorkerRun']), async copyId () { if (!this.version.id) return try { diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 36e521d18..db75a7483 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -65,7 +65,7 @@ export function removeEmptyStrings<T extends string> (obj: Record<string, T> = { * Falsy values (false, NaN, undefined, null, '', 0, …) will return []. * Objects become an array of their values, and other primitives become [value]. */ -export function ensureArray<T> (value: T | ArrayLike<T>): T[] { +export function ensureArray<T> (value: T | ArrayLike<T> | { [key: string | number | symbol]: T }): T[] { if (!value) return [] if (Array.isArray(value)) return value if (typeof value === 'object') return Object.values(value) diff --git a/src/store/index.js b/src/store/index.js index ad64f75a3..023bea41e 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,7 +5,8 @@ import { useImageStore, useIngestStore, useJobsStore, - useSearchStore + useSearchStore, + useModelStore } from '@/stores' /** @@ -20,7 +21,6 @@ const moduleNames = [ 'elements', 'entity', 'files', - 'model', 'navigation', 'notifications', 'oauth', @@ -41,7 +41,10 @@ export const piniaStores = [ useImageStore, useIngestStore, useJobsStore, - useSearchStore + useSearchStore, + useFolderPickerStore, + useJobsStore, + useModelStore ] export const actions = { diff --git a/src/store/model.js b/src/store/model.js deleted file mode 100644 index 0ed90d91d..000000000 --- a/src/store/model.js +++ /dev/null @@ -1,85 +0,0 @@ -import { assign } from 'lodash' -import * as api from '@/api' -import { errorParser } from '@/helpers' - -export const initialState = () => ({ - /* - * Stores ML models and their versions. - * { [modelId]: model } - */ - models: {}, - // { [modelVersionId]: modelVersion } - modelVersions: {} -}) - -export const mutations = { - setModels (state, models) { - state.models = { - ...state.models, - ...Object.fromEntries(models.map(model => [model.id, model])) - } - }, - - setModelVersions (state, versions) { - state.modelVersions = { - ...state.modelVersions, - ...Object.fromEntries(versions.map(v => [v.id, v])) - } - }, - - removeModelVersion (state, versionId) { - const newModelVersions = { ...state.modelVersions } - delete newModelVersions[versionId] - state.modelVersions = newModelVersions - }, - - reset (state) { - assign(state, initialState()) - } -} - -export const actions = { - async createModel ({ commit }, params) { - const resp = await api.createModel(params) - commit('setModels', [resp]) - return resp - }, - - async listModels ({ commit }, params) { - const resp = await api.listModels(params) - commit('setModels', resp.results) - return resp - }, - - async listModelVersions ({ commit }, { modelId, page = 1 }) { - const resp = await api.listModelVersions(modelId, { page }) - commit('setModelVersions', resp.results) - return resp - }, - - async retrieveModel ({ commit }, modelId) { - const resp = await api.retrieveModel(modelId) - commit('setModels', [resp]) - }, - - async getModelVersion ({ commit }, modelVersionId) { - const resp = await api.retrieveModelVersion(modelVersionId) - commit('setModelVersions', [resp]) - }, - - async deleteModelVersion ({ commit }, { versionId }) { - try { - await api.deleteModelVersion(versionId) - commit('removeModelVersion', versionId) - } catch (err) { - commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true }) - } - } -} - -export default { - namespaced: true, - state: initialState(), - mutations, - actions -} diff --git a/src/stores/index.ts b/src/stores/index.ts index e666647a0..4680fcb24 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -3,5 +3,6 @@ export { useFolderPickerStore } from './folderpicker' export { useImageStore } from './image' export { useIngestStore } from './ingest' export { useJobsStore } from './jobs' +export { useModelStore } from './model' export { useNotificationStore } from './notification' export { useSearchStore } from './search' diff --git a/src/stores/model.ts b/src/stores/model.ts new file mode 100644 index 000000000..e38c9ba63 --- /dev/null +++ b/src/stores/model.ts @@ -0,0 +1,72 @@ +import { defineStore } from 'pinia' +import { errorParser } from '@/helpers' +import { UUID } from '@/types' +import { Model, ModelVersion } from '@/types/model' +import { + CreateModelPayload, + ModelListParameters, + createModel, + listModels, + listModelVersions, + retrieveModel, + retrieveModelVersion, + deleteModelVersion +} from '@/api' +import { useNotificationStore } from '.' + +interface State { + models: { [id: UUID]: Model } + modelVersions: { [id: UUID]: ModelVersion } +} + +export const useModelStore = defineStore('model', { + state: (): State => ({ + models: {}, + modelVersions: {} + }), + actions: { + async createModel (params: CreateModelPayload) { + const model = await createModel(params) + this.models[model.id] = model + return model + }, + + async listModels (params: ModelListParameters) { + const resp = await listModels(params) + this.models = { + ...this.models, + ...Object.fromEntries(resp.results.map(model => [model.id, model])) + } + return resp + }, + + async listModelVersions (modelId: UUID, page = 1) { + const resp = await listModelVersions(modelId, { page }) + this.modelVersions = { + ...this.modelVersions, + ...Object.fromEntries(resp.results.map(modelVersion => [modelVersion.id, modelVersion])) + } + return resp + }, + + async retrieveModel (modelId: UUID) { + const model = await retrieveModel(modelId) + this.models[model.id] = model + }, + + async getModelVersion (modelVersionId: UUID) { + const modelVersion = await retrieveModelVersion(modelVersionId) + this.modelVersions[modelVersion.id] = modelVersion + }, + + async deleteModelVersion (versionId: UUID) { + try { + await deleteModelVersion(versionId) + delete this.modelVersions[versionId] + } catch (err) { + const notificationStore = useNotificationStore() + notificationStore.notify({ type: 'error', text: errorParser(err) }) + } + } + } +}) \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 7873b2a5c..495204901 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ import { METADATA_TYPES, PROCESS_STATES, PROCESS_MODES, DATASET_STATES, CLASSIFICATION_STATES } from '@/config' import { S3FileStatus, GitRefType, WorkerVersionGPUUsage, WorkerVersionState, ProcessActivityState, ModelVersionState } from '@/enums' +import { ModelVersionLight } from './model' /** * A universally unique identifier, using the format @@ -339,25 +340,13 @@ export interface WorkerConfiguration { archived: boolean } -export interface ModelVersion { - id: UUID - model: { - id: UUID - name: string - } - tag: string | null - state: ModelVersionState - size: number - configuration: object -} - export interface WorkerRun { id: UUID parents: UUID[] worker_version: WorkerVersion process: TrainingProcess configuration: WorkerConfiguration | null - model_version: ModelVersion | null + model_version: ModelVersionLight | null } export interface Dataset { diff --git a/src/types/model.ts b/src/types/model.ts index 54e011461..2939c7874 100644 --- a/src/types/model.ts +++ b/src/types/model.ts @@ -1,6 +1,20 @@ import { ModelVersionState } from '@/enums' import { Right, UUID } from '.' +export interface ModelLight { + id: UUID + name: string +} + +export interface ModelVersionLight { + id: UUID + model: ModelLight + tag: string | null + state: ModelVersionState + size: number + configuration: object +} + export interface Model { id: UUID name: string diff --git a/src/views/Model/Create.vue b/src/views/Model/Create.vue index 7c70e60d1..f921f6463 100644 --- a/src/views/Model/Create.vue +++ b/src/views/Model/Create.vue @@ -13,9 +13,9 @@ <div class="control"> <input class="input is-fullwidth" - :disabled="loading || null" + :disabled="loading || undefined" type="text" - v-model="fields.name" + v-model.trim="fields.name" /> </div> <p v-if="errors.name" class="help is-danger">{{ errors.name }}</p> @@ -26,9 +26,9 @@ <div class="control"> <input class="input is-fullwidth" - :disabled="loading || null" + :disabled="loading || undefined" type="text" - v-model="fields.description" + v-model.trim="fields.description" /> </div> <p v-if="errors.description" class="help is-danger">{{ errors.description }}</p> @@ -40,7 +40,7 @@ type="submit" class="button is-primary is-fullwidth" v-on:click="create" - :disabled="!allowCreate || null" + :disabled="!allowCreate || undefined" :title="allowCreate ? 'Create a new model' : 'A name is required to create a new model'" > <i v-if="loading" class="loader"></i> @@ -52,15 +52,23 @@ </main> </template> -<script> -import { mapMutations } from 'vuex' -import { corporaMixin } from '@/mixins.js' +<script lang="ts"> +import { isAxiosError } from 'axios' +import { defineComponent } from 'vue' +import { mapActions } from 'pinia' + +import { CreateModelPayload } from '@/api' +import { corporaMixin } from '@/mixins' import { errorParser } from '@/helpers' +import { useModelStore, useNotificationStore } from '@/stores' +import { Model } from '@/types/model' -export default { - emits: [ - 'create-model' - ], +export default defineComponent({ + emits: { + 'create-model' (value: Model) { + return value.id !== undefined + } + }, mixins: [ corporaMixin ], @@ -69,8 +77,8 @@ export default { fields: { name: '', description: '' - }, - errors: {} + } as CreateModelPayload, + errors: {} as Record<string, unknown> }), computed: { mainView () { @@ -78,29 +86,29 @@ export default { return this.$route.name === 'model-create' }, allowCreate () { - return this.fields.name && !this.loading + return Boolean(this.fields.name) && !this.loading } }, methods: { - ...mapMutations('notifications', ['notify']), + ...mapActions(useNotificationStore, ['notify']), async create () { if (!this.allowCreate) return this.loading = true this.errors = {} try { - const payload = { name: this.fields.name } - if (this.fields.description.trim()) payload.description = this.fields.description - const resp = await this.$store.dispatch('model/createModel', payload) + const payload: CreateModelPayload = { name: this.fields.name } + if (this.fields.description?.trim()) payload.description = this.fields.description + const model = await useModelStore().createModel(payload) this.notify({ type: 'success', text: `Model "${this.fields.name}" created successfully.` }) - this.$emit('create-model', resp) + this.$emit('create-model', model) this.fields.name = this.fields.description = '' } catch (err) { - if (err?.response?.data) this.errors = err.response.data + if (isAxiosError(err) && err.response?.data) this.errors = err.response.data this.notify({ type: 'error', text: errorParser(err) }) } finally { this.loading = false } } } -} +}) </script> diff --git a/src/views/Model/List.vue b/src/views/Model/List.vue index 9fab55b06..80e8f4ead 100644 --- a/src/views/Model/List.vue +++ b/src/views/Model/List.vue @@ -91,28 +91,35 @@ </main> </template> -<script> -import { mapActions, mapMutations } from 'vuex' -import Model from '@/components/Model' -import Paginator from '@/components/Paginator.vue' +<script lang="ts"> +import { defineComponent } from 'vue' +import { mapActions } from 'pinia' + +import { ModelListParameters } from '@/api' import { errorParser } from '@/helpers' import { truncateMixin } from '@/mixins' +import { useModelStore, useNotificationStore } from '@/stores' +import { Model } from '@/types/model' + +import Paginator from '@/components/Paginator.vue' +import ModelComponent from '@/components/Model/Model.vue' +import { PageNumberPagination } from '@/types' -export default { +export default defineComponent({ mixins: [ truncateMixin ], components: { Paginator, - Model + Model: ModelComponent }, props: { + /** + * If a process ID is defined, this component allows to select a model + * and add one of its versions to a WorkerRun. + * Otherwise, list models with their versions and members. + */ processId: { - /** - * If a process ID is defined, this component allows to select a model - * and add one of its versions to a WorkerRun. - * Otherwise, list models with their versions and members. - */ type: String, default: '' }, @@ -141,7 +148,7 @@ export default { }, data: () => ({ loading: false, - modelsPage: null, + modelsPage: null as PageNumberPagination<Model> | null, // ID of the selected model selectedModel: null, page: 1, @@ -149,13 +156,13 @@ export default { allModels: false }), methods: { - ...mapMutations('notifications', ['notify']), - ...mapActions('model', ['listModels']), + ...mapActions(useNotificationStore, ['notify']), + ...mapActions(useModelStore, ['listModels']), async updateModelsPage () { this.loading = true try { this.selectedModel = null - const payload = { page: this.page } + const payload: ModelListParameters = { page: this.page } if (this.nameFilter) payload.name = this.nameFilter if (this.compatibleWorkerId && !this.allModels) payload.compatible_worker = this.compatibleWorkerId this.modelsPage = await this.listModels(payload) @@ -181,7 +188,7 @@ export default { handler: 'updateModelsPage' } } -} +}) </script> <style lang="scss" scoped> diff --git a/src/views/Model/Model.vue b/src/views/Model/Model.vue index cb05fc7f8..d5ed328d4 100644 --- a/src/views/Model/Model.vue +++ b/src/views/Model/Model.vue @@ -46,11 +46,12 @@ <script lang="ts"> import { defineComponent } from 'vue' -import { mapState, mapMutations, mapActions } from 'vuex' +import { mapState, mapActions } from 'pinia' import { UUID_REGEX } from '@/config' import { ago, errorParser } from '@/helpers' import { UUID } from '@/types' import Model from '@/components/Model' +import { useModelStore, useNotificationStore } from '@/stores' export default defineComponent({ props: { @@ -68,22 +69,22 @@ export default defineComponent({ error: null as string | null }), computed: { - ...mapState('model', ['models']), + ...mapState(useModelStore, ['models']), model () { return this.models[this.modelId] }, - creationDate () { - if (!this.model) return + creationDate (): string | null { + if (!this.model) return null return ago(new Date(this.model.created)) }, - updateDate () { - if (!this.model) return + updateDate (): string | null { + if (!this.model) return null return ago(new Date(this.model.updated)) } }, methods: { - ...mapMutations('notifications', ['notify']), - ...mapActions('model', ['retrieveModel']), + ...mapActions(useNotificationStore, ['notify']), + ...mapActions(useModelStore, ['retrieveModel']), async loadModel (modelId: UUID) { this.loading = true try { diff --git a/src/views/Model/Version.vue b/src/views/Model/Version.vue index a46b5a071..d07cf9498 100644 --- a/src/views/Model/Version.vue +++ b/src/views/Model/Version.vue @@ -48,14 +48,14 @@ <div class="field"> <label class="label">Size</label> <div class="control"> - <p :title="version.size">{{ size }}</p> + <p :title="version.size.toString()">{{ size }}</p> </div> </div> <div class="field"> <label class="label">Configuration</label> <div class="control"> - <pre v-if="model.configuration">{{ model.configuration }}</pre> + <pre v-if="version.configuration">{{ version.configuration }}</pre> <template v-else>—</template> </div> </div> @@ -67,11 +67,14 @@ <script lang="ts"> import { defineComponent } from 'vue' -import { mapActions, mapState } from 'vuex' +import { mapActions, mapState } from 'pinia' import { MODEL_VERSION_STATE_COLORS, UUID_REGEX } from '@/config' import { errorParser, formatBytes } from '@/helpers' import { truncateMixin } from '@/mixins' import ItemId from '@/components/ItemId.vue' +import { useModelStore } from '@/stores' +import { UUID } from '@/types' +import { ModelVersion } from '@/types/model' export default defineComponent({ mixins: [ @@ -91,7 +94,7 @@ export default defineComponent({ error: null as string | null }), computed: { - ...mapState('model', ['models', 'modelVersions']), + ...mapState(useModelStore, ['models', 'modelVersions']), model () { if (!this.version) return null return this.models[this.version.model_id] @@ -105,17 +108,16 @@ export default defineComponent({ }, stateClass () { if (!this.version) return null - // @ts-expect-error Requires migrating the models store to Pinia to get a ModelVersion type return MODEL_VERSION_STATE_COLORS[this.version.state] } }, methods: { - ...mapActions('model', ['getModelVersion', 'retrieveModel']) + ...mapActions(useModelStore, ['getModelVersion', 'retrieveModel']) }, watch: { versionId: { immediate: true, - async handler (newValue) { + async handler (newValue: UUID) { try { await this.getModelVersion(newValue) } catch (err) { @@ -125,7 +127,7 @@ export default defineComponent({ }, version: { immediate: true, - async handler (newValue) { + async handler (newValue: ModelVersion) { if (!newValue?.model_id) return try { await this.retrieveModel(newValue.model_id) diff --git a/tests/unit/store/auth.spec.js b/tests/unit/store/auth.spec.js index 1b1eca3d3..2399196e8 100644 --- a/tests/unit/store/auth.spec.js +++ b/tests/unit/store/auth.spec.js @@ -118,7 +118,6 @@ describe('auth', () => { { mutation: 'elements/reset' }, { mutation: 'entity/reset' }, { mutation: 'files/reset' }, - { mutation: 'model/reset' }, { mutation: 'navigation/reset' }, { mutation: 'notifications/reset' }, { mutation: 'oauth/reset' }, @@ -154,7 +153,6 @@ describe('auth', () => { { mutation: 'elements/reset' }, { mutation: 'entity/reset' }, { mutation: 'files/reset' }, - { mutation: 'model/reset' }, { mutation: 'navigation/reset' }, { mutation: 'notifications/reset' }, { mutation: 'oauth/reset' }, @@ -188,7 +186,6 @@ describe('auth', () => { { mutation: 'elements/reset' }, { mutation: 'entity/reset' }, { mutation: 'files/reset' }, - { mutation: 'model/reset' }, { mutation: 'navigation/reset' }, { mutation: 'notifications/reset' }, { mutation: 'oauth/reset' }, @@ -224,7 +221,6 @@ describe('auth', () => { { mutation: 'elements/reset' }, { mutation: 'entity/reset' }, { mutation: 'files/reset' }, - { mutation: 'model/reset' }, { mutation: 'navigation/reset' }, { mutation: 'notifications/reset' }, { mutation: 'oauth/reset' }, @@ -256,7 +252,6 @@ describe('auth', () => { { mutation: 'elements/reset' }, { mutation: 'entity/reset' }, { mutation: 'files/reset' }, - { mutation: 'model/reset' }, { mutation: 'navigation/reset' }, { mutation: 'notifications/reset' }, { mutation: 'oauth/reset' }, diff --git a/tests/unit/store/elements.spec.js b/tests/unit/store/elements.spec.js index 0520bcedd..dcd5cef89 100644 --- a/tests/unit/store/elements.spec.js +++ b/tests/unit/store/elements.spec.js @@ -830,7 +830,8 @@ describe('elements', () => { await store.dispatch('elements/nextChildren', { id: 'element1', max: Infinity }) assert.strictEqual(mock.history.get.length, 1) - assert.deepStrictEqual(mock.history.get[0].headers, { + assert.deepStrictEqual({ ...mock.history.get[0].headers }, { + Accept: 'application/json, text/plain, */*', 'Cache-Control': 'no-cache', 'If-Modified-Since': 'Wed, 21 Oct 2015 07:28:00 GMT' }) diff --git a/tests/unit/store/index.spec.js b/tests/unit/store/index.spec.js index 539bec1c4..32a0dbba3 100644 --- a/tests/unit/store/index.spec.js +++ b/tests/unit/store/index.spec.js @@ -44,7 +44,6 @@ describe('store', () => { { mutation: 'elements/reset' }, { mutation: 'entity/reset' }, { mutation: 'files/reset' }, - { mutation: 'model/reset' }, { mutation: 'navigation/reset' }, { mutation: 'notifications/reset' }, { mutation: 'oauth/reset' }, @@ -65,7 +64,6 @@ describe('store', () => { require('./elements.spec.js') require('./entity.spec.js') require('./files.spec.js') - require('./model.spec.js') require('./navigation.spec.js') require('./oauth.spec.js') require('./ponos.spec.js') diff --git a/tests/unit/store/model.spec.js b/tests/unit/store/model.spec.js deleted file mode 100644 index e5140b20c..000000000 --- a/tests/unit/store/model.spec.js +++ /dev/null @@ -1,212 +0,0 @@ -import { assert } from 'chai' -import axios from 'axios' -import { pick } from 'lodash' -import { mutations } from '@/store/model.js' -import { modelsSample, modelVersionsSample } from '../samples.js' -import store from './index.spec.js' -import { FakeAxios } from '../testhelpers.js' - -describe('model', () => { - describe('mutations', () => { - describe('setModels', () => { - it('sets a Model', () => { - const state = { - models: { model_0: { id: 'model_0' } } - } - mutations.setModels(state, modelsSample.results) - assert.deepStrictEqual(state, { - models: { - model_0: { id: 'model_0' }, - ...Object.fromEntries(modelsSample.results.map(model => [model.id, model])) - } - }) - }) - }) - - describe('setModelVersions', () => { - it('sets a Model Version', () => { - const state = { - modelVersions: { model_version_0: { id: 'model_version_0' } } - } - mutations.setModelVersions(state, modelVersionsSample.results) - assert.deepStrictEqual(state, { - modelVersions: { - model_version_0: { id: 'model_version_0' }, - ...Object.fromEntries(modelVersionsSample.results.map(v => [v.id, v])) - } - }) - }) - }) - - describe('removeModelVersion', () => { - it('removes a Model Version', () => { - const state = { - modelVersions: { model_version_0: { id: 'model_version_0' } } - } - mutations.removeModelVersion(state, 'model_version_0') - assert.deepStrictEqual(state, { - modelVersions: {} - }) - }) - }) - }) - - describe('actions', () => { - let mock - - before('Setting up Axios mock', () => { - mock = new FakeAxios(axios) - }) - - afterEach(() => { - // Remove any handlers, but leave mocking in place - mock.reset() - store.reset() - }) - - after('Removing Axios mock', () => { - // Remove mocking entirely - mock.restore() - }) - - describe('createModel', () => { - it('creates a model', async () => { - store.state.model.models = { model_0: { id: 'model_0' } } - const attrs = { name: 'test', description: 'A' } - mock.onPost('/models/').reply(201, { id: 'model_1', ...attrs }) - - await store.dispatch('model/createModel', attrs) - - assert.deepStrictEqual(store.history, [ - { action: 'model/createModel', payload: attrs }, - { mutation: 'model/setModels', payload: [{ id: 'model_1', ...attrs }] } - ]) - assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'data'])), [ - { - method: 'post', - url: '/models/', - data: attrs - } - ]) - assert.deepStrictEqual(store.state.model.models, { - model_0: { id: 'model_0' }, - model_1: { id: 'model_1', ...attrs } - }) - }) - }) - - describe('listModels', () => { - it('lists stored models with filters', async () => { - const reply = { - count: 1, - results: [ - { id: 'model1' } - ] - } - mock.onGet('/models/').reply(200, reply) - await store.dispatch('model/listModels', { name: '1' }) - assert.deepStrictEqual(store.history, [ - { action: 'model/listModels', payload: { name: '1' } }, - { mutation: 'model/setModels', payload: reply.results } - ]) - assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'params'])), [ - { - method: 'get', - url: '/models/', - params: { name: '1' } - } - ]) - assert.deepStrictEqual(store.state.model.models, { model1: { id: 'model1' } }) - }) - }) - - describe('retrieveModel', () => { - it('retrieves a model', async () => { - const model = { id: 'model1' } - mock.onGet('/model/model1/').reply(200, model) - - await store.dispatch('model/retrieveModel', 'model1') - - assert.deepStrictEqual(store.history, [ - { - action: 'model/retrieveModel', - payload: 'model1' - }, - { - mutation: 'model/setModels', - payload: [model] - } - ]) - assert.deepStrictEqual(store.state.model.models, { - model1: model - }) - }) - }) - - describe('listModelVersions', () => { - it('lists stored model versions', async () => { - const reply = { - count: 1, - results: [ - { id: 'model_version1' } - ] - } - mock.onGet('/model/model1/versions/').reply(200, reply) - await store.dispatch('model/listModelVersions', { modelId: 'model1', page: '1' }) - assert.deepStrictEqual(store.history, [ - { action: 'model/listModelVersions', payload: { modelId: 'model1', page: '1' } }, - { mutation: 'model/setModelVersions', payload: reply.results } - ]) - assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'params'])), [ - { - method: 'get', - url: '/model/model1/versions/', - params: { page: '1' } - }]) - assert.deepStrictEqual(store.state.model.modelVersions, { model_version1: { id: 'model_version1' } }) - }) - }) - - describe('getModelVersion', () => { - it('retrieves a model version', async () => { - const version = { id: 'model_version1' } - mock.onGet('/modelversion/model_version1/').reply(200, version) - - await store.dispatch('model/getModelVersion', 'model_version1') - - assert.deepStrictEqual(store.history, [ - { - action: 'model/getModelVersion', - payload: 'model_version1' - }, - { - mutation: 'model/setModelVersions', - payload: [version] - } - ]) - assert.deepStrictEqual(store.state.model.modelVersions, { - model_version1: version - }) - }) - }) - - describe('deleteModelVersions', () => { - it('deletes a model version', async () => { - store.state.model.modelVersions = { model_version_0: { id: 'model_version_0' } } - - mock.onDelete('/modelversion/model_version_0/').reply(204) - await store.dispatch('model/deleteModelVersion', { versionId: 'model_version_0' }) - assert.deepStrictEqual(store.history, [ - { action: 'model/deleteModelVersion', payload: { versionId: 'model_version_0' } }, - { mutation: 'model/removeModelVersion', payload: 'model_version_0' } - ]) - assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [ - { - method: 'delete', - url: '/modelversion/model_version_0/' - }]) - assert.deepStrictEqual(store.state.model.modelVersions, {}) - }) - }) - }) -}) diff --git a/tests/unit/stores/model.spec.js b/tests/unit/stores/model.spec.js new file mode 100644 index 000000000..052fffcc6 --- /dev/null +++ b/tests/unit/stores/model.spec.js @@ -0,0 +1,156 @@ +import axios from 'axios' +import { assert } from 'chai' +import { pick } from 'lodash' +import { createPinia, setActivePinia } from 'pinia' +import { useModelStore, useNotificationStore } from '@/stores' +import { FakeAxios } from '../testhelpers' + +describe('model', () => { + describe('actions', () => { + let mock, store, notificationStore + + before('Setting up Axios mock', () => { + mock = new FakeAxios(axios) + setActivePinia(createPinia()) + store = useModelStore() + notificationStore = useNotificationStore() + }) + + afterEach(() => { + // Remove any handlers, but leave mocking in place + mock.reset() + store.$reset() + notificationStore.$reset() + }) + + after('Removing Axios mock', () => { + // Remove mocking entirely + mock.restore() + }) + + describe('createModel', () => { + it('creates a model', async () => { + store.models = { model_0: { id: 'model_0' } } + const attrs = { name: 'test', description: 'A' } + mock.onPost('/models/').reply(201, { id: 'model_1', ...attrs }) + + await store.createModel(attrs) + + assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'data'])), [ + { + method: 'post', + url: '/models/', + data: attrs + } + ]) + assert.deepStrictEqual(store.models, { + model_0: { id: 'model_0' }, + model_1: { id: 'model_1', ...attrs } + }) + }) + }) + + describe('listModels', () => { + it('lists stored models with filters', async () => { + const reply = { + count: 1, + results: [ + { id: 'model1' } + ] + } + mock.onGet('/models/').reply(200, reply) + await store.listModels({ name: '1' }) + assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'params'])), [ + { + method: 'get', + url: '/models/', + params: { name: '1' } + } + ]) + assert.deepStrictEqual(store.models, { model1: { id: 'model1' } }) + }) + }) + + describe('retrieveModel', () => { + it('retrieves a model', async () => { + const model = { id: 'model1' } + mock.onGet('/model/model1/').reply(200, model) + + await store.retrieveModel('model1') + + assert.deepStrictEqual(store.models, { + model1: model + }) + }) + }) + + describe('listModelVersions', () => { + it('lists stored model versions', async () => { + const reply = { + count: 1, + results: [ + { id: 'model_version1' } + ] + } + mock.onGet('/model/model1/versions/').reply(200, reply) + await store.listModelVersions('model1', 1) + assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url', 'params'])), [ + { + method: 'get', + url: '/model/model1/versions/', + params: { page: 1 } + }]) + assert.deepStrictEqual(store.modelVersions, { model_version1: { id: 'model_version1' } }) + }) + }) + + describe('getModelVersion', () => { + it('retrieves a model version', async () => { + const version = { id: 'model_version1' } + mock.onGet('/modelversion/model_version1/').reply(200, version) + + await store.getModelVersion('model_version1') + + assert.deepStrictEqual(store.modelVersions, { + model_version1: version + }) + }) + }) + + describe('deleteModelVersions', () => { + it('deletes a model version', async () => { + store.modelVersions = { model_version_0: { id: 'model_version_0' } } + + mock.onDelete('/modelversion/model_version_0/').reply(204) + await store.deleteModelVersion('model_version_0') + assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [ + { + method: 'delete', + url: '/modelversion/model_version_0/' + }]) + assert.deepStrictEqual(store.modelVersions, {}) + }) + + it('handles errors', async () => { + store.modelVersions = { model_version_0: { id: 'model_version_0' } } + mock.onDelete('/modelversion/model_version_0/').reply(500) + + await store.deleteModelVersion('model_version_0') + + assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [ + { + method: 'delete', + url: '/modelversion/model_version_0/' + }]) + assert.deepStrictEqual(store.modelVersions, { model_version_0: { id: 'model_version_0' } }) + assert.deepStrictEqual(notificationStore.notifications, [ + { + id: 0, + type: 'error', + text: 'Request failed with status code 500' + } + ]) + }) + }) + }) +}) -- GitLab