diff --git a/components/CommonActionForm.vue b/components/CommonActionForm.vue index 8d76634794b94b52c1c7544b5d5497eabbde0bb2..c8af422a0c6472a9798332141aa03780f2aac578 100644 --- a/components/CommonActionForm.vue +++ b/components/CommonActionForm.vue @@ -29,7 +29,7 @@ implied. See the Licence for the specific language governing permissions and limitations under the Licence. --> <template> - <v-container> + <div> <v-row> <v-col cols="12" md="12"> <v-textarea @@ -82,7 +82,7 @@ permissions and limitations under the Licence. /> </v-col> </v-row> - </v-container> + </div> </template> <script lang="ts"> @@ -94,7 +94,7 @@ import { Component, Prop, Vue } from 'nuxt-property-decorator' import { Attachment } from '@/models/Attachment' import { Contact } from '@/models/Contact' -import { GenericAction } from '@/models/GenericAction' +import { IActionCommonDetails, ActionCommonDetails } from '@/models/ActionCommonDetails' /** * A class component for a set of common form fields for actions @@ -108,15 +108,14 @@ export default class CommonActionForm extends Vue { private contactIsValid = true /** - * a GenericAction + * an IActionCommonDetails like object */ @Prop({ - default: new GenericAction(), required: true, type: Object }) // @ts-ignore - readonly value!: GenericAction + readonly value!: IActionCommonDetails /** * a list of available attachments @@ -159,7 +158,7 @@ export default class CommonActionForm extends Vue { * @fires CommonActionForm#input */ set description (value: string) { - const actionCopy = GenericAction.createFromObject(this.value) + const actionCopy = ActionCommonDetails.createFromObject(this.value) actionCopy.description = value /** * descriptionChange event @@ -180,7 +179,7 @@ export default class CommonActionForm extends Vue { * @fires CommonActionForm#input */ set contact (value: Contact | null) { - const actionCopy = GenericAction.createFromObject(this.value) + const actionCopy = ActionCommonDetails.createFromObject(this.value) actionCopy.contact = value || null /** * contactChange event @@ -201,7 +200,7 @@ export default class CommonActionForm extends Vue { * @fires CommonActionForm#input */ set actionAttachments (value: Attachment[]) { - const actionCopy = GenericAction.createFromObject(this.value) + const actionCopy = ActionCommonDetails.createFromObject(this.value) actionCopy.attachments = value /** * attachments event diff --git a/components/GenericActionForm.vue b/components/GenericActionForm.vue index 986dc3f11c5797daa05a2622829420a754b786ad..6571e76da8276eca819d5931b67224c82af87b42 100644 --- a/components/GenericActionForm.vue +++ b/components/GenericActionForm.vue @@ -30,7 +30,7 @@ implied. See the Licence for the specific language governing permissions and limitations under the Licence. --> <template> - <v-container> + <div> <v-form ref="datesForm" v-model="datesAreValid" @@ -38,7 +38,7 @@ permissions and limitations under the Licence. > <v-row> <v-col cols="12" md="6"> - <date-time-picker + <DateTimePicker :value="actionCopy.beginDate" label="Start date" placeholder="e.g. 2000-01-31 12:00" @@ -47,7 +47,7 @@ permissions and limitations under the Licence. /> </v-col> <v-col cols="12" md="6"> - <date-time-picker + <DateTimePicker :value="actionCopy.endDate" label="End date" placeholder="e.g. 2001-01-31 12:00" @@ -59,11 +59,12 @@ permissions and limitations under the Licence. </v-form> <CommonActionForm ref="commonForm" - v-model="action" + :value="actionCopy" :attachments="attachments" :rules="[rules.contactNotNull]" + @input="updateCommonFields" /> - </v-container> + </div> </template> <script lang="ts"> @@ -77,6 +78,7 @@ import { DateTime } from 'luxon' import { Attachment } from '@/models/Attachment' import { GenericAction } from '@/models/GenericAction' +import { ActionCommonDetails } from '@/models/ActionCommonDetails' import CommonActionForm from '@/components/CommonActionForm.vue' import DateTimePicker from '@/components/DateTimePicker.vue' @@ -87,8 +89,8 @@ import DateTimePicker from '@/components/DateTimePicker.vue' */ @Component({ components: { - DateTimePicker, - CommonActionForm + CommonActionForm, + DateTimePicker } }) // @ts-ignore @@ -129,14 +131,6 @@ export default class GenericActionForm extends Vue { this.createActionCopy(this.value) } - get action (): GenericAction { - return this.actionCopy - } - - set action (value: GenericAction) { - this.$emit('input', value) - } - /** * sets the start date and validates start- and enddate * @@ -163,6 +157,13 @@ export default class GenericActionForm extends Vue { this.$emit('input', this.actionCopy) } + updateCommonFields (action: ActionCommonDetails) { + this.actionCopy.description = action.description + this.actionCopy.contact = action.contact + this.actionCopy.attachments = action.attachments.map((a: Attachment) => Attachment.createFromObject(a)) + this.$emit('input', this.actionCopy) + } + /** * validates the form based on its rules * @@ -174,13 +175,10 @@ export default class GenericActionForm extends Vue { /** * a rule to validate the start date * - * @param {string} v - a string representation of the date as supplied by the datepicker component * @return {boolean | string} whether the date is valid or an error message */ - validateInputForStartDate (v: string): boolean | string { - // NOTE: as the internals of the DatePicker component work with strings, - // the validation functions should expect strings, too - if (v === null || v === '') { + validateInputForStartDate (): boolean | string { + if (!this.actionCopy.beginDate) { return true } if (!this.actionCopy.endDate) { @@ -195,13 +193,10 @@ export default class GenericActionForm extends Vue { /** * a rule to validate the end date * - * @param {string} v - a string representation of the date as supplied by the datepicker component * @return {boolean | string} whether the date is valid or an error message */ - validateInputForEndDate (v: string): boolean | string { - // NOTE: as the internals of the DatePicker component work with strings, - // the validation functions should expect strings, too - if (v === null || v === '') { + validateInputForEndDate (): boolean | string { + if (!this.actionCopy.endDate) { return true } if (!this.actionCopy.beginDate) { diff --git a/components/SoftwareUpdateActionCard.vue b/components/SoftwareUpdateActionCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..b3c0addb5fe90af3b81fc54eec4f4dc9fbbcb647 --- /dev/null +++ b/components/SoftwareUpdateActionCard.vue @@ -0,0 +1,182 @@ +<!-- +Web client of the Sensor Management System software developed within the +Helmholtz DataHub Initiative by GFZ and UFZ. + +Copyright (C) 2020, 2021 +- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) +- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) +- Helmholtz Centre Potsdam - GFZ German Research Centre for + Geosciences (GFZ, https://www.gfz-potsdam.de) + +Parts of this program were developed within the context of the +following publicly funded projects or measures: +- Helmholtz Earth and Environment DataHub + (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + +Licensed under the HEESIL, Version 1.0 or - as soon they will be +approved by the "Community" - subsequent versions of the HEESIL +(the "Licence"). + +You may not use this work except in compliance with the Licence. + +You may obtain a copy of the Licence at: +https://gitext.gfz-potsdam.de/software/heesil + +Unless required by applicable law or agreed to in writing, software +distributed under the Licence is distributed on an "AS IS" basis, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the Licence for the specific language governing +permissions and limitations under the Licence. +--> +<template> + <v-card> + <v-card-subtitle class="pb-0"> + <v-row no-gutters> + <v-col> + {{ value.updateDate | toUtcDate }} + </v-col> + <v-col + align-self="end" + class="text-right" + > + <slot name="menu" /> + </v-col> + </v-row> + </v-card-subtitle> + <v-card-title class="pt-0"> + {{ updateName }} + </v-card-title> + <v-card-subtitle class="pb-1"> + <v-row + no-gutters + > + <v-col> + {{ value.contact.toString() }} + </v-col> + <v-col + align-self="end" + class="text-right" + > + <slot name="actions" /> + <v-btn + icon + @click.stop.prevent="toggleVisibility()" + > + <v-icon>{{ isVisible() ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon> + </v-btn> + </v-col> + </v-row> + </v-card-subtitle> + <v-expand-transition> + <div + v-show="isVisible(value.id)" + > + <v-card-text + class="grey lighten-5 text--primary pt-2" + > + <v-row dense> + <v-col cols="12" md="4"> + <label> + Version + </label> + {{ value.version }} + </v-col> + <v-col cols="12" md="4"> + <label> + Repository + </label> + <!-- eslint-disable-next-line vue/no-v-html --> + <span v-html="repositoryLink" /> + </v-col> + </v-row> + <label>Description</label> + {{ value.description }} + </v-card-text> + </div> + </v-expand-transition> + </v-card> +</template> + +<script lang="ts"> +/** + * @file provides a component for a Software Update Action card + * @author <marc.hanisch@gfz-potsdam.de> + */ +import { Component, Prop, Vue } from 'nuxt-property-decorator' + +import { dateToDateTimeString } from '@/utils/dateHelper' +import { protocolsInUrl } from '@/utils/urlHelpers' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' + +/** + * A class component for Software Update Action card + * @extends Vue + */ +@Component({ + filters: { + toUtcDate: dateToDateTimeString + } +}) +// @ts-ignore +export default class SoftwareUpdateActionCard extends Vue { + private showDetails: boolean = false + + /** + * a SoftwareUpdateAction + */ + @Prop({ + default: () => new SoftwareUpdateAction(), + required: true, + type: Object + }) + // @ts-ignore + readonly value!: SoftwareUpdateAction + + /** + * whether the card expansion is shown or not + * + * @return {boolean} whether the card expansion is shown or not + */ + isVisible (): boolean { + return this.showDetails + } + + /** + * toggles the shown state of the card expansion + * + */ + toggleVisibility (): void { + this.showDetails = !this.showDetails + } + + /** + * returns an URL as an link + * + * All characters except 0-9, a-z, :, / and . are removed from the link to + * prevent xss attacks. If the URL doesn't start with a known protocol, it + * won't be wrapped. + * + * @return {string} the url wrapped in an HTML link element + */ + get repositoryLink (): string { + // eslint-disable-next-line no-useless-escape + const url = this.value.repositoryUrl.replace(/[^a-zA-Z0-9:\/.-]/g, '') + if (protocolsInUrl(['https', 'http', 'ftp', 'ftps', 'sftp', 'dav', 'davs'], url)) { + return '<a href="' + url + '" target="_blank">' + url + '</a>' + } + return url + } + + /** + * returns the name of the update + * + * @returns {string} the update name + */ + get updateName (): string { + if (this.value.softwareTypeName.toLowerCase() === 'others') { + return 'Device Software Update' + } + return this.value.softwareTypeName + ' Update' + } +} +</script> diff --git a/components/SoftwareUpdateActionForm.vue b/components/SoftwareUpdateActionForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..e2f5ac913d2732ccf4c8880b4e2beef6b9180aaa --- /dev/null +++ b/components/SoftwareUpdateActionForm.vue @@ -0,0 +1,277 @@ +<!-- +Web client of the Sensor Management System software developed within the +Helmholtz DataHub Initiative by GFZ and UFZ. + +Copyright (C) 2020, 2021 +- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) +- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) +- Helmholtz Centre Potsdam - GFZ German Research Centre for + Geosciences (GFZ, https://www.gfz-potsdam.de) + +Parts of this program were developed within the context of the +following publicly funded projects or measures: +- Helmholtz Earth and Environment DataHub + (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + +Licensed under the HEESIL, Version 1.0 or - as soon they will be +approved by the "Community" - subsequent versions of the HEESIL +(the "Licence"). + +You may not use this work except in compliance with the Licence. + +You may obtain a copy of the Licence at: +https://gitext.gfz-potsdam.de/software/heesil + +Unless required by applicable law or agreed to in writing, software +distributed under the Licence is distributed on an "AS IS" basis, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the Licence for the specific language governing +permissions and limitations under the Licence. +--> +<template> + <div> + <v-row> + <v-col cols="12" md="6"> + <v-form + ref="datesForm" + v-model="datesAreValid" + @submit.prevent + > + <DateTimePicker + :value="actionCopy.updateDate" + label="Update date" + :rules="[rules.dateNotNull]" + @input="setUpdateDateAndValidate" + /> + </v-form> + </v-col> + <v-col cols="12" md="6"> + <v-form + ref="softwareTypeForm" + @submit.prevent + > + <v-select + :value="actionCopy.softwareTypeUrl" + :items="softwareTypes" + clearable + :item-text="(x) => x.name" + :item-value="(x) => x.uri" + label="Software type" + :rules="[rules.softwareTypeNotNull]" + @input="setSoftwareType" + /> + </v-form> + </v-col> + </v-row> + <v-row> + <v-col cols="12" md="6"> + <v-text-field + :value="actionCopy.version" + label="Version" + placeholder="1.2.3" + @input="setVersion" + /> + </v-col> + </v-row> + <v-row> + <v-col cols="12"> + <v-form + ref="repositoryUrlForm" + @submit.prevent + > + <v-text-field + :value="actionCopy.repositoryUrl" + label="Repository URL" + placeholder="https://github.com/" + :rules="[rules.isUrl]" + @input="setRepositoryUrl" + /> + </v-form> + </v-col> + </v-row> + <CommonActionForm + ref="commonForm" + :value="actionCopy" + :attachments="attachments" + :rules="[rules.contactNotNull]" + @input="updateCommonFields" + /> + </div> +</template> + +<script lang="ts"> +/** + * @file provides a component for a Software Update Action form + * @author <marc.hanisch@gfz-potsdam.de> + */ +import { Component, Prop, Vue, Watch } from 'nuxt-property-decorator' + +import { DateTime } from 'luxon' + +import { Attachment } from '@/models/Attachment' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' +import { SoftwareType } from '@/models/SoftwareType' +import { ActionCommonDetails } from '@/models/ActionCommonDetails' + +import { protocolsInUrl } from '@/utils/urlHelpers' + +import CommonActionForm from '@/components/CommonActionForm.vue' +import DateTimePicker from '@/components/DateTimePicker.vue' + +/** + * A class component for a form for Generic Device Actions + * @extends Vue + */ +@Component({ + components: { + CommonActionForm, + DateTimePicker + } +}) +// @ts-ignore +export default class SoftwareUpdateActionForm extends Vue { + private actionCopy: SoftwareUpdateAction = new SoftwareUpdateAction() + private datesAreValid: boolean = true + private rules: Object = { + dateNotNull: this.mustBeProvided('Update date'), + contactNotNull: this.mustBeProvided('Contact'), + softwareTypeNotNull: this.mustBeProvided('Software type'), + isUrl: this.isUrl + } + + private softwareTypes: SoftwareType[] = [] + + /** + * a SoftwareUpdateAction + */ + @Prop({ + default: () => new SoftwareUpdateAction(), + required: true, + type: Object + }) + // @ts-ignore + readonly value!: SoftwareUpdateAction + + /** + * a list of available attachments + */ + @Prop({ + default: () => [], + required: false, + type: Array + }) + // @ts-ignore + readonly attachments!: Attachment[] + + async fetch (): Promise<any> { + await this.fetchSoftwareTypes() + } + + async fetchSoftwareTypes (): Promise<any> { + this.softwareTypes = await this.$api.softwareTypes.findAllPaginated() + } + + created () { + // create a copy of the original value on which all operations will be applied + this.createActionCopy(this.value) + } + + /** + * sets the update date and validates + * + * @param {DateTime | null} aDate - the update date + */ + setUpdateDateAndValidate (aDate: DateTime | null) { + this.actionCopy.updateDate = aDate + this.checkValidationOfDates() + this.$emit('input', this.actionCopy) + } + + setSoftwareType (anUri: string | null) { + let aSoftwareType: SoftwareType | undefined + if (anUri) { + aSoftwareType = this.softwareTypes.find(i => i.uri === anUri) + } + this.actionCopy.softwareTypeUrl = aSoftwareType?.uri || '' + this.actionCopy.softwareTypeName = aSoftwareType?.name || '' + this.$emit('input', this.actionCopy) + } + + setVersion (version: string) { + this.actionCopy.version = version + this.$emit('input', this.actionCopy) + } + + setRepositoryUrl (repositoryUrl: string) { + this.actionCopy.repositoryUrl = repositoryUrl + this.$emit('input', this.actionCopy) + } + + updateCommonFields (action: ActionCommonDetails) { + this.actionCopy.description = action.description + this.actionCopy.contact = action.contact + this.actionCopy.attachments = action.attachments.map((a: Attachment) => Attachment.createFromObject(a)) + this.$emit('input', this.actionCopy) + } + + /** + * validates the form based on its rules + * + */ + checkValidationOfDates () { + return (this.$refs.datesForm as Vue & { validate: () => boolean }).validate() + } + + /** + * a rule to check that an field is non-empty + * + * @param {string} fieldname - the (human readable) label of the field + * @return {(v: any) => boolean | string} a function that checks whether the field is valid or an error message + */ + mustBeProvided (fieldname: string): (v: any) => boolean | string { + const innerFunc: (v: any) => boolean | string = function (v: any) { + if (v == null || v === '') { + return fieldname + ' must be provided' + } + return true + } + return innerFunc + } + + /** + * checks whether the link is an valid URL or not + * + * to be honest, it just checks whether the string starts with http(s):// or similar + * + * @param {string} link - the link to validate + * @returns {boolean | string} true when valid, otherwise an error message + */ + isUrl (link: string): boolean | string { + if (!protocolsInUrl(['http', 'https', 'ftp', 'ftps', 'sftp', 'ssh', 'dav', 'davs'], link)) { + return 'The link is not a valid URL.' + } + return true + } + + /** + * checks if the form is valid + * + */ + isValid (): boolean { + return this.checkValidationOfDates() && + (this.$refs.commonForm as Vue & { isValid: () => boolean }).isValid() && + (this.$refs.softwareTypeForm as Vue & { validate: () => boolean }).validate() && + (this.$refs.repositoryUrlForm as Vue & { validate: () => boolean }).validate() + } + + createActionCopy (action: SoftwareUpdateAction): void { + this.actionCopy = SoftwareUpdateAction.createFromObject(action) + } + + @Watch('value', { immediate: true, deep: true }) + // @ts-ignore + onValueChanged (val: SoftwareUpdateAction) { + this.createActionCopy(val) + } +} +</script> diff --git a/modelUtils/Compareables.ts b/modelUtils/Compareables.ts index bbd815b9ab2209127103ec9587adc93525bd302a..2d260efd66c44396a14aaf57d3b907b6f416a869 100644 --- a/modelUtils/Compareables.ts +++ b/modelUtils/Compareables.ts @@ -9,7 +9,7 @@ export interface IDateCompareable { } export function isDateCompareable (i: any): i is IDateCompareable { - if (i && typeof i === 'object' && Object.prototype.hasOwnProperty.call(i, 'date') && DateTime.isDateTime(i.date)) { + if (i && typeof i === 'object' && 'date' in i && DateTime.isDateTime(i.date)) { return true } return false diff --git a/models/ActionCommonDetails.ts b/models/ActionCommonDetails.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3949f6df593ffba4b4dd13c4bdac0927439fa9a --- /dev/null +++ b/models/ActionCommonDetails.ts @@ -0,0 +1,95 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2020, 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { Attachment } from '@/models/Attachment' +import { Contact } from '@/models/Contact' + +export interface IActionCommonDetails { + id: string | null + description: string + contact: Contact | null + attachments: Attachment[] +} + +/** + * a very unspecific Action class + * + * this class in mainly used for inheritance in derived classes + * + * @implements IActionCommonDetails + */ +export class ActionCommonDetails implements IActionCommonDetails { + private _id: string | null = null + private _description: string = '' + private _contact: Contact | null = null + private _attachments: Attachment[] = [] + + get id (): string | null { + return this._id + } + + set id (id: string | null) { + this._id = id + } + + get description (): string { + return this._description + } + + set description (description: string) { + this._description = description + } + + get contact (): Contact | null { + return this._contact + } + + set contact (contact: Contact | null) { + this._contact = contact + } + + get attachments (): Attachment[] { + return this._attachments + } + + set attachments (attachments: Attachment[]) { + this._attachments = attachments + } + + static createFromObject (value: IActionCommonDetails): ActionCommonDetails { + const action = new ActionCommonDetails() + action.id = value.id + action.description = value.description + action.contact = value.contact ? Contact.createFromObject(value.contact) : null + action.attachments = value.attachments.map(a => Attachment.createFromObject(a)) + return action + } +} diff --git a/models/GenericAction.ts b/models/GenericAction.ts index 4d144df1fa2bf1e8c0560974cdf92adc2c1074fe..a7892d8877472446282acd9c3500a056e56dbeb2 100644 --- a/models/GenericAction.ts +++ b/models/GenericAction.ts @@ -32,27 +32,21 @@ import { DateTime } from 'luxon' import { Attachment } from '@/models/Attachment' import { Contact } from '@/models/Contact' -import { IAction } from '@/models/Action' +import { IActionCommonDetails, ActionCommonDetails } from '@/models/ActionCommonDetails' import { IDateCompareable } from '@/modelUtils/Compareables' -export interface IGenericAction extends IAction { +export interface IGenericAction extends IActionCommonDetails { actionTypeName: string actionTypeUrl: string beginDate: DateTime | null endDate: DateTime | null - contact: Contact | null - attachments: Attachment[] } -export class GenericAction implements IGenericAction, IDateCompareable { - private _id: string | null = null - private _description: string = '' +export class GenericAction extends ActionCommonDetails implements IGenericAction, IDateCompareable { private _actionTypeName: string = '' private _actionTypeUrl: string = '' private _beginDate: DateTime | null = null private _endDate: DateTime | null = null - private _contact: Contact | null = null - private _attachments: Attachment[] = [] /** * returns an empty instance @@ -77,29 +71,13 @@ export class GenericAction implements IGenericAction, IDateCompareable { action.description = someObject.description action.actionTypeName = someObject.actionTypeName action.actionTypeUrl = someObject.actionTypeUrl - action.beginDate = someObject.beginDate ? someObject.beginDate : null - action.endDate = someObject.endDate ? someObject.endDate : null + action.beginDate = someObject.beginDate + action.endDate = someObject.endDate action.contact = someObject.contact ? Contact.createFromObject(someObject.contact) : null action.attachments = someObject.attachments.map(i => Attachment.createFromObject(i)) return action } - get id (): string | null { - return this._id - } - - set id (id: string | null) { - this._id = id - } - - get description (): string { - return this._description - } - - set description (description: string) { - this._description = description - } - get actionTypeUrl (): string { return this._actionTypeUrl } @@ -132,22 +110,6 @@ export class GenericAction implements IGenericAction, IDateCompareable { this._endDate = date } - get contact (): Contact | null { - return this._contact - } - - set contact (contact: Contact | null) { - this._contact = contact - } - - get attachments (): Attachment[] { - return this._attachments - } - - set attachments (attachments: Attachment[]) { - this._attachments = attachments - } - get isGenericAction (): boolean { return true } diff --git a/models/SoftwareType.ts b/models/SoftwareType.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f23ccb684c6cb2dba3a4e376ecc7493ef4921e1 --- /dev/null +++ b/models/SoftwareType.ts @@ -0,0 +1,100 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2020, 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +export interface ISoftwareType { + id: string + name: string + uri: string + definition: string +} + +export class SoftwareType implements ISoftwareType { + private _id: string = '' + private _name: string = '' + private _uri: string = '' + private _definition: string = '' + + get id (): string { + return this._id + } + + set id (newId: string) { + this._id = newId + } + + get name (): string { + return this._name + } + + set name (newName: string) { + this._name = newName + } + + get uri (): string { + return this._uri + } + + set uri (newUri: string) { + this._uri = newUri + } + + get definition (): string { + return this._definition + } + + set definition (newDefinition: string) { + this._definition = newDefinition + } + + toString (): string { + return this._name + } + + static createWithData (id: string, name: string, uri: string, definition: string): SoftwareType { + const result = new SoftwareType() + result.id = id + result.name = name + result.uri = uri + result.definition = definition + return result + } + + static createFromObject (someObject: ISoftwareType): SoftwareType { + const newObject = new SoftwareType() + + newObject.id = someObject.id + newObject.name = someObject.name + newObject.uri = someObject.uri + newObject.definition = someObject.definition + + return newObject + } +} diff --git a/models/SoftwareUpdateAction.ts b/models/SoftwareUpdateAction.ts new file mode 100644 index 0000000000000000000000000000000000000000..1948a44ef0374384cf9cf6c0295363569b2d19a0 --- /dev/null +++ b/models/SoftwareUpdateAction.ts @@ -0,0 +1,131 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2020, 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { DateTime } from 'luxon' +import { Attachment } from '@/models/Attachment' +import { Contact } from '@/models/Contact' +import { IActionCommonDetails, ActionCommonDetails } from '@/models/ActionCommonDetails' +import { IDateCompareable } from '@/modelUtils/Compareables' + +export interface ISoftwareUpdateAction extends IActionCommonDetails { + softwareTypeName: string + softwareTypeUrl: string + updateDate: DateTime | null + version: string + repositoryUrl: string +} + +export class SoftwareUpdateAction extends ActionCommonDetails implements ISoftwareUpdateAction, IDateCompareable { + private _softwareTypeName: string = '' + private _softwareTypeUrl: string = '' + private _updateDate: DateTime | null = null + private _version: string = '' + private _repositoryUrl: string = '' + + /** + * returns an empty instance + * + * @static + * @return {SoftwareUpdateAction} an empty instance + */ + static createEmpty (): SoftwareUpdateAction { + return new SoftwareUpdateAction() + } + + /** + * creates an instance from an existing ISoftwareUpdateAction-like object + * + * @static + * @param {ISoftwareUpdateAction} someObject - an ISoftwareUpdateAction like object + * @return {SoftwareUpdateAction} a cloned instance of the original object + */ + static createFromObject (someObject: ISoftwareUpdateAction) : SoftwareUpdateAction { + const action = new SoftwareUpdateAction() + action.id = someObject.id + action.softwareTypeName = someObject.softwareTypeName + action.softwareTypeUrl = someObject.softwareTypeUrl + action.updateDate = someObject.updateDate + action.version = someObject.version + action.repositoryUrl = someObject.repositoryUrl + action.description = someObject.description + action.contact = someObject.contact ? Contact.createFromObject(someObject.contact) : null + action.attachments = someObject.attachments.map(i => Attachment.createFromObject(i)) + return action + } + + get softwareTypeUrl (): string { + return this._softwareTypeUrl + } + + set softwareTypeUrl (softwareTypeUrl: string) { + this._softwareTypeUrl = softwareTypeUrl + } + + get softwareTypeName (): string { + return this._softwareTypeName + } + + set softwareTypeName (softwareTypeName: string) { + this._softwareTypeName = softwareTypeName + } + + get updateDate (): DateTime | null { + return this._updateDate + } + + set updateDate (date: DateTime | null) { + this._updateDate = date + } + + get version (): string { + return this._version + } + + set version (version: string) { + this._version = version + } + + get repositoryUrl (): string { + return this._repositoryUrl + } + + set repositoryUrl (repositoryUrl: string) { + this._repositoryUrl = repositoryUrl + } + + get isSoftwareUpdateAction (): boolean { + return true + } + + get date (): DateTime | null { + return this.updateDate + } +} diff --git a/pages/configurations/_id.vue b/pages/configurations/_id.vue index 6fba723e1e50cb2e2c931993b90faa484287cc5b..fe6e8e18e0f677d1f5183711e5f38fc3dd53c914 100644 --- a/pages/configurations/_id.vue +++ b/pages/configurations/_id.vue @@ -353,7 +353,7 @@ export default class ConfigurationsIdPage extends Vue { } validateInputForStartDate (v: string): boolean | string { - if (v === null || v === '') { + if (v === null || v === '' || this.configuration.startDate === null) { return true } if (!this.configuration.endDate) { @@ -366,7 +366,7 @@ export default class ConfigurationsIdPage extends Vue { } validateInputForEndDate (v: string): boolean | string { - if (v === null || v === '') { + if (v === null || v === '' || this.configuration.endDate === null) { return true } if (!this.configuration.startDate) { diff --git a/pages/devices/_deviceId/actions.vue b/pages/devices/_deviceId/actions.vue index da0005ceaac806a893b78490893989b3b9b257a5..07d1c7c3d7338a77532183355ce1e6aba907fc7a 100644 --- a/pages/devices/_deviceId/actions.vue +++ b/pages/devices/_deviceId/actions.vue @@ -56,11 +56,9 @@ permissions and limitations under the Licence. <template v-else-if="isEditActionPage" > - <!-- the currently edited action is passed as a property to the - `edit.vue` page to avoid reloading of the action --> <NuxtChild - :value="editedAction" @input="$fetch" + @showload="showload" @showsave="showsave" /> </template> @@ -77,7 +75,10 @@ permissions and limitations under the Licence. v-if="action.isGenericAction" v-model="actions[index]" > - <template #menu> + <template + v-if="isLoggedIn" + #menu + > <v-menu close-on-click close-on-content-click @@ -105,7 +106,7 @@ permissions and limitations under the Licence. <v-list-item :disabled="!isLoggedIn" dense - @click="showDeleteDialog(action.id)" + @click="showDeleteDialog(action)" > <v-list-item-content> <v-list-item-title @@ -127,7 +128,8 @@ permissions and limitations under the Licence. </template> <template #actions> <v-btn - :to="'/devices/' + deviceId + '/actions/' + action.id + '/edit'" + v-if="isLoggedIn" + :to="'/devices/' + deviceId + '/actions/generic-device-actions/' + action.id + '/edit'" color="primary" text @click.stop.prevent @@ -137,65 +139,74 @@ permissions and limitations under the Licence. </template> </GenericActionCard> - <template v-if="action.isDeviceSoftwareUpdateAction"> - <v-card> - <v-card-subtitle class="pb-0"> - {{ action.updateDate | toUtcDate }} - </v-card-subtitle> - <v-card-title class="pt-0"> - {{ action.softwareTypeName }} update - </v-card-title> - <v-card-subtitle> - <v-row - no-gutters - > - <v-col - cols="11" - > - {{ action.contact.toString() }} - </v-col> - <v-col - align-self="end" - class="text-right" + <SoftwareUpdateActionCard + v-if="action.isSoftwareUpdateAction" + v-model="actions[index]" + > + <template + v-if="isLoggedIn" + #menu + > + <v-menu + close-on-click + close-on-content-click + offset-x + left + z-index="999" + > + <template #activator="{ on }"> + <v-btn + data-role="property-menu" + icon + small + v-on="on" > - <v-btn - icon - @click.stop.prevent="showActionItem(action.id)" + <v-icon + dense + small > - <v-icon>{{ isActionItemShown(action.id) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon> - </v-btn> - </v-col> - </v-row> - </v-card-subtitle> - <v-expand-transition> - <v-card-text - v-show="isActionItemShown(action.id)" - class="text--primary" - > - <v-row dense> - <v-col cols="12" md="4"> - <label> - Version - </label> - {{ action.version }} - </v-col> - <v-col cols="12" md="4"> - <label> - Repository - </label> - {{ action.repositoryUrl }} - </v-col> - </v-row> - <v-row dense> - <v-col> - <label>Description</label> - {{ action.description }} - </v-col> - </v-row> - </v-card-text> - </v-expand-transition> - </v-card> - </template> + mdi-dots-vertical + </v-icon> + </v-btn> + </template> + + <v-list> + <v-list-item + :disabled="!isLoggedIn" + dense + @click="showDeleteDialog(action)" + > + <v-list-item-content> + <v-list-item-title + :class="isLoggedIn ? 'red--text' : 'grey--text'" + > + <v-icon + left + small + :color="isLoggedIn ? 'red' : 'grey'" + > + mdi-delete + </v-icon> + Delete + </v-list-item-title> + </v-list-item-content> + </v-list-item> + </v-list> + </v-menu> + </template> + <template #actions> + <v-btn + v-if="isLoggedIn" + :to="'/devices/' + deviceId + '/actions/software-update-actions/' + action.id + '/edit'" + color="primary" + text + @click.stop.prevent + > + Edit + </v-btn> + </template> + </SoftwareUpdateActionCard> + <template v-if="action.isDeviceCalibrationAction"> <v-card> <v-card-subtitle class="pb-0"> @@ -348,7 +359,11 @@ permissions and limitations under the Licence. </template> </v-timeline-item> </v-timeline> - <v-dialog v-model="hasActionIdToDelete" max-width="290"> + <v-dialog + v-model="hasActionToDelete" + max-width="290" + @click:outside="hideDeleteDialog" + > <v-card> <v-card-title class="headline"> Delete action @@ -367,7 +382,7 @@ permissions and limitations under the Licence. <v-btn color="error" text - @click="deleteActionAndCloseDialog(actionIdToDelete)" + @click="deleteAction()" > <v-icon left> mdi-delete @@ -384,13 +399,16 @@ permissions and limitations under the Licence. import { Component, Vue } from 'nuxt-property-decorator' import ProgressIndicator from '@/components/ProgressIndicator.vue' import GenericActionCard from '@/components/GenericActionCard.vue' +import SoftwareUpdateActionCard from '@/components/SoftwareUpdateActionCard.vue' import { DateTime } from 'luxon' +import { Attachment } from '@/models/Attachment' import { Contact } from '@/models/Contact' import { DeviceProperty } from '@/models/DeviceProperty' -import { IAction } from '@/models/Action' +import { IActionCommonDetails } from '@/models/ActionCommonDetails' import { GenericAction } from '@/models/GenericAction' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' import { DateComparator, isDateCompareable } from '@/modelUtils/Compareables' const toUtcDate = (dt: DateTime) => { @@ -401,49 +419,7 @@ interface IColoredAction { getColor (): string } -class DeviceSoftwareUpdateAction implements IAction, IColoredAction { - public id: string - public softwareTypeName: string - public softwareTypeUri: string - public updateDate: DateTime - public version: string - public repositoryUrl: string - public description: string - public contact: Contact - constructor ( - id: string, - softwareTypeName: string, - softwareTypeUri: string, - updateDate: DateTime, - version: string, - repositoryUrl: string, - description: string, - contact: Contact - ) { - this.id = id - this.softwareTypeName = softwareTypeName - this.softwareTypeUri = softwareTypeUri - this.updateDate = updateDate - this.version = version - this.repositoryUrl = repositoryUrl - this.description = description - this.contact = contact - } - - getId (): string { - return 'software-' + this.id - } - - get isDeviceSoftwareUpdateAction (): boolean { - return true - } - - getColor (): string { - return 'yellow' - } -} - -class DeviceCalibrationAction implements IAction, IColoredAction { +class DeviceCalibrationAction implements IActionCommonDetails, IColoredAction { public id: string public description: string public currentCalibrationDate: DateTime @@ -452,6 +428,7 @@ class DeviceCalibrationAction implements IAction, IColoredAction { public value: string public deviceProperties: DeviceProperty[] public contact: Contact + public attachments: Attachment[] constructor ( id: string, description: string, @@ -460,7 +437,8 @@ class DeviceCalibrationAction implements IAction, IColoredAction { formula: string, value: string, deviceProperties: DeviceProperty[], - contact: Contact + contact: Contact, + attachments: Attachment[] ) { this.id = id this.description = description @@ -470,6 +448,7 @@ class DeviceCalibrationAction implements IAction, IColoredAction { this.value = value this.deviceProperties = deviceProperties this.contact = contact + this.attachments = attachments } getId (): string { @@ -495,6 +474,7 @@ class DeviceMountAction { public beginDate: DateTime public description: string public contact: Contact + public attachments: Attachment[] constructor ( id: string, configurationName: string, @@ -504,7 +484,8 @@ class DeviceMountAction { offsetZ: number, beginDate: DateTime, description: string, - contact: Contact + contact: Contact, + attachments: Attachment[] ) { this.id = id this.configurationName = configurationName @@ -515,6 +496,7 @@ class DeviceMountAction { this.beginDate = beginDate this.description = description this.contact = contact + this.attachments = attachments } getId (): string { @@ -530,24 +512,27 @@ class DeviceMountAction { } } -class DeviceUnmountAction implements IAction, IColoredAction { +class DeviceUnmountAction implements IActionCommonDetails, IColoredAction { public id: string public configurationName: string public endDate: DateTime public description: string public contact: Contact + public attachments: Attachment[] constructor ( id: string, configurationName: string, endDate: DateTime, description: string, - contact: Contact + contact: Contact, + attachments: Attachment[] ) { this.id = id this.configurationName = configurationName this.endDate = endDate this.description = description this.contact = contact + this.attachments = attachments } getId (): string { @@ -564,18 +549,27 @@ class DeviceUnmountAction implements IAction, IColoredAction { } /** - * extend the original interface by adding the getColor() method + * extend the original interfaces by adding the getColor() method */ declare module '@/models/GenericAction' { - export interface GenericAction extends IColoredAction { + interface GenericAction extends IColoredAction { } } GenericAction.prototype.getColor = (): string => 'blue' +declare module '@/models/SoftwareUpdateAction' { + interface SoftwareUpdateAction extends IColoredAction { + } +} +SoftwareUpdateAction.prototype.getColor = (): string => 'yellow' + +type ActionDeleteMethod = (id: string) => Promise<void> + @Component({ components: { ProgressIndicator, - GenericActionCard + GenericActionCard, + SoftwareUpdateActionCard }, filters: { toUtcDate @@ -585,10 +579,10 @@ export default class DeviceActionsPage extends Vue { private isLoading: boolean = false private isSaving: boolean = false - private actions: IAction[] = [] + private actions: IActionCommonDetails[] = [] private searchResultItemsShown: { [id: string]: boolean } = {} - private actionIdToDelete: string = '' + private actionToDelete: IActionCommonDetails | null = null async fetch () { const contact1 = Contact.createFromObject({ @@ -605,16 +599,6 @@ export default class DeviceActionsPage extends Vue { email: 'cam.paign@gfz-potsdam.de', website: '' }) - const deviceSoftwareUpdateAction = new DeviceSoftwareUpdateAction( - 'X2', - 'Firmware', - 'softwaretypes/firmware', - DateTime.fromISO('2021-03-30T08:10:00Z'), - '1.0.34', - 'git.gfz-potsdam.de/sensor-management-system/firmware', - 'The 1.0.34 firmware version for the device', - contact1 - ) const devProp1 = new DeviceProperty() devProp1.label = 'Wind direction' const devProp2 = new DeviceProperty() @@ -627,7 +611,8 @@ export default class DeviceActionsPage extends Vue { 'f(x) = x', '100', [devProp1, devProp2], - contact2 + contact2, + [] ) const deviceMountAction1 = new DeviceMountAction( 'X4', @@ -638,35 +623,39 @@ export default class DeviceActionsPage extends Vue { 2, DateTime.fromISO('2021-03-30T12:00:00Z'), 'Mounted Measurement ABC', - contact1 + contact1, + [] ) const deviceUnmountAction1 = new DeviceUnmountAction( 'X5', 'Measurement ABC', DateTime.fromISO('2022-03-30T12:00:00Z'), 'Unmounted Measurement ABC', - contact1 + contact1, + [] ) this.actions = [ - deviceSoftwareUpdateAction, deviceCalibrationAction1, deviceMountAction1, deviceUnmountAction1 ] - await Promise.all([this.fetchGenericActions()]) + await Promise.all([ + this.fetchGenericActions(), + this.fetchSoftwareUpdateActions() + ]) // sort the actions const comparator = new DateComparator() - this.actions.sort((i: IAction, j: IAction): number => { + this.actions.sort((i: IActionCommonDetails, j: IActionCommonDetails): number => { if (isDateCompareable(i) && isDateCompareable(j)) { // multiply result with -1 to get descending order return comparator.compare(i, j) * -1 } if (isDateCompareable(i)) { - return 1 + return -1 } if (isDateCompareable(j)) { - return -1 + return 1 } return 0 }) @@ -677,6 +666,11 @@ export default class DeviceActionsPage extends Vue { actions.forEach((action: GenericAction) => this.actions.push(action)) } + async fetchSoftwareUpdateActions (): Promise<void> { + const actions: SoftwareUpdateAction[] = await this.$api.devices.findRelatedSoftwareUpdateActions(this.deviceId) + actions.forEach((action: SoftwareUpdateAction) => this.actions.push(action)) + } + get isInProgress (): boolean { return this.isLoading || this.isSaving } @@ -700,7 +694,7 @@ export default class DeviceActionsPage extends Vue { get isEditActionPage (): boolean { // eslint-disable-next-line no-useless-escape - const editUrl = '^\/devices\/' + this.deviceId + '\/actions\/([0-9]+)\/edit$' + const editUrl = '^\/devices\/' + this.deviceId + '\/actions\/[a-zA-Z-]+\/[0-9]+\/edit$' return !!this.$route.path.match(editUrl) } @@ -718,45 +712,61 @@ export default class DeviceActionsPage extends Vue { return this.$route.params.actionId } - /** - * Returns the action object from the list of actions that is currently edited - * - * When the `/edit` route for an action is called, the `edit.vue` page is - * included via a `NuxtChild` component. To avoid of loading the action again - * in this page, we return it from this method to pass it as a property to - * the `NuxtChild` component - * - * Calls {@link DeviceActionsPage.actionId} to get the currently edited - * action from the route. - * - * @return {IAction | undefined} the found action, otherwise undefined - */ - get editedAction (): IAction | undefined { - if (!this.actionId) { - return - } - return this.actions.find(action => action.id === this.actionId) - } - - get hasActionIdToDelete (): boolean { - return !!this.actionIdToDelete + get hasActionToDelete (): boolean { + return this.actionToDelete !== null } showsave (isSaving: boolean) { this.isSaving = isSaving } - showDeleteDialog (id: string) { - this.actionIdToDelete = id + showload (isLoading: boolean) { + this.isLoading = isLoading + } + + showDeleteDialog (action: IActionCommonDetails) { + this.actionToDelete = action } hideDeleteDialog (): void { - this.actionIdToDelete = '' + this.actionToDelete = null + } + + /** + * deletes the action and closes the delete dialog + * + * calls {@link DeviceActionsPage#deleteActionAndCloseDialog} with the appropriate API method + * + * @throws {TypeError} - throws an Error if the type of action is not supported + */ + deleteAction (): void { + if (!this.actionToDelete) { + return + } + if (!this.actionToDelete.id) { + return + } + switch (true) { + case 'isGenericAction' in this.actionToDelete: + this.deleteActionAndCloseDialog(this.actionToDelete.id, this.$api.genericDeviceActions.deleteById.bind(this.$api.genericDeviceActions)) + break + case 'isSoftwareUpdateAction' in this.actionToDelete: + this.deleteActionAndCloseDialog(this.actionToDelete.id, this.$api.deviceSoftwareUpdateActions.deleteById.bind(this.$api.deviceSoftwareUpdateActions)) + break + default: + throw new TypeError('deleting the action type is not supported.') + } } - deleteActionAndCloseDialog (id: string) { + /** + * deletes the action and closes the delete dialog + * + * @param {string} id - the id of the action to delete + * @param {ActionDeleteMethod} actionDeleteMethod - an API method to delete an action + */ + deleteActionAndCloseDialog (id: string, actionDeleteMethod: ActionDeleteMethod): void { this.isSaving = true - this.$api.genericDeviceActions.deleteById(id).then(() => { + actionDeleteMethod(id).then(() => { this.$fetch() this.$store.commit('snackbar/setSuccess', 'Action deleted') }).catch((_error) => { @@ -767,11 +777,11 @@ export default class DeviceActionsPage extends Vue { }) } - getActionType (action: IAction): string { + getActionType (action: IActionCommonDetails): string { switch (true) { case 'isGenericAction' in action: return 'generic-action' - case 'isDeviceSoftwareUpdateAction' in action: + case 'isSoftwareUpdateAction' in action: return 'software-update-action' case 'isDeviceCalibrationAction' in action: return 'device-calibration-action' @@ -780,7 +790,7 @@ export default class DeviceActionsPage extends Vue { } } - getActionTypeIterationKey (action: IAction): string { + getActionTypeIterationKey (action: IActionCommonDetails): string { return this.getActionType(action) + '-' + action.id } } diff --git a/pages/devices/_deviceId/actions/_actionId/edit.vue b/pages/devices/_deviceId/actions/generic-device-actions/_actionId/edit.vue similarity index 77% rename from pages/devices/_deviceId/actions/_actionId/edit.vue rename to pages/devices/_deviceId/actions/generic-device-actions/_actionId/edit.vue index 52eda9234c14fbb53b215299ce25df719d992deb..8d0e1e89d6a25ddc0964f363e303898229ba23ec 100644 --- a/pages/devices/_deviceId/actions/_actionId/edit.vue +++ b/pages/devices/_deviceId/actions/generic-device-actions/_actionId/edit.vue @@ -51,19 +51,21 @@ permissions and limitations under the Licence. apply </v-btn> </v-card-actions> + <!-- just to be consistent with the new mask, we show the selected action type as an disabled v-select here --> <v-select - :value="valueCopy.actionTypeName" - :items="[valueCopy.actionTypeName]" + :value="action.actionTypeName" + :items="[action.actionTypeName]" :item-text="(x) => x" disabled label="Action Type" /> <GenericActionForm ref="genericDeviceActionForm" - v-model="valueCopy" + v-model="action" :attachments="attachments" /> + <v-card-actions> <v-spacer /> <v-btn @@ -89,7 +91,7 @@ permissions and limitations under the Licence. </template> <script lang="ts"> -import { Component, Vue, Prop, Watch } from 'nuxt-property-decorator' +import { Component, Vue } from 'nuxt-property-decorator' import { Attachment } from '@/models/Attachment' import { GenericAction } from '@/models/GenericAction' @@ -99,27 +101,33 @@ import GenericActionForm from '@/components/GenericActionForm.vue' @Component({ components: { GenericActionForm - } + }, + scrollToTop: true }) -export default class DeviceActionEditPage extends Vue { - private valueCopy: GenericAction = new GenericAction() +export default class GenericDeviceActionEditPage extends Vue { + private action: GenericAction = new GenericAction() private attachments: Attachment[] = [] + private _isLoading: boolean = false private _isSaving: boolean = false - @Prop({ - default: () => new GenericAction(), - required: true, - type: Object - }) - readonly value!: GenericAction + async fetch (): Promise<any> { + this.isLoading = true + await Promise.all([ + this.fetchAttachments(), + this.fetchAction() + ]) + this.isLoading = false + } - created () { - if (this.value) { - this.valueCopy = GenericAction.createFromObject(this.value) + async fetchAction (): Promise<any> { + try { + this.action = await this.$api.genericDeviceActions.findById(this.actionId) + } catch (_) { + this.$store.commit('snackbar/setError', 'Failed to fetch action') } } - async fetch (): Promise<any> { + async fetchAttachments (): Promise<any> { try { this.attachments = await this.$api.devices.findRelatedDeviceAttachments(this.deviceId) } catch (_) { @@ -131,10 +139,23 @@ export default class DeviceActionEditPage extends Vue { return this.$route.params.deviceId } + get actionId (): string { + return this.$route.params.actionId + } + get isLoggedIn (): boolean { return this.$store.getters['oidc/isAuthenticated'] } + get isLoading (): boolean { + return this.$data._isLoading + } + + set isLoading (value: boolean) { + this.$data._isLoading = value + this.$emit('showload', value) + } + get isSaving (): boolean { return this.$data._isSaving } @@ -150,7 +171,7 @@ export default class DeviceActionEditPage extends Vue { return } this.isSaving = true - this.$api.genericDeviceActions.update(this.deviceId, this.valueCopy).then((action: GenericAction) => { + this.$api.genericDeviceActions.update(this.deviceId, this.action).then((action: GenericAction) => { this.$router.push('/devices/' + this.deviceId + '/actions', () => this.$emit('input', action)) }).catch(() => { this.$store.commit('snackbar/setError', 'Failed to save the action') @@ -158,11 +179,5 @@ export default class DeviceActionEditPage extends Vue { this.isSaving = false }) } - - @Watch('value', { immediate: true, deep: true }) - // @ts-ignore - onValueChanged (val: GenericAction) { - this.valueCopy = GenericAction.createFromObject(val) - } } </script> diff --git a/pages/devices/_deviceId/actions/new.vue b/pages/devices/_deviceId/actions/new.vue index dc3320194fb0d05b31aaf341c5b66539dfb91b29..e192833f34f1e6d11907dc3c608916f087199741 100644 --- a/pages/devices/_deviceId/actions/new.vue +++ b/pages/devices/_deviceId/actions/new.vue @@ -58,7 +58,7 @@ permissions and limitations under the Licence. v-else-if="softwareUpdateChosen" color="green" small - @click="addDeviceSoftwareUpdateAction" + @click="addSoftwareUpdateAction" > Add </v-btn> @@ -132,46 +132,19 @@ permissions and limitations under the Licence. </v-row> </v-form> </v-card-text> + <!-- softwareUpdate --> <v-card-text v-if="softwareUpdateChosen" > - <v-form - ref="datesForm" - v-model="datesAreValid" - @submit.prevent - > - <v-row> - <v-col cols="12" md="6"> - <DatePicker - :value="startDate" - label="Date" - :rules="[rules.startDate, rules.updateDateNotNull]" - @input="setStartDateAndValidate" - /> - </v-col> - </v-row> - </v-form> - <v-form - ref="softwareTypeForm" - v-model="softwareTypeIsValid" - @submit.prevent - > - <v-row> - <v-col cols="12" md="6"> - <v-select :items="softwareTypes" clearable :item-text="(x) => x.name" label="Software type" :rules="[rules.softwareTypeNotNull]" /> - </v-col> - </v-row> - </v-form> - <v-row> - <v-col cols="12" md="3"> - <v-text-field label="Version" placeholder="1.2.3" /> - </v-col> - <v-col cols="12" md="9"> - <v-text-field label="Repository URL" placeholder="https://github.com/" /> - </v-col> - </v-row> + <SoftwareUpdateActionForm + ref="softwareUpdateActionForm" + v-model="softwareUpdateAction" + :attachments="attachments" + /> </v-card-text> + + <!-- genericAction --> <v-card-text v-if="genericActionChosen" > @@ -181,9 +154,13 @@ permissions and limitations under the Licence. :attachments="attachments" /> </v-card-text> - <!-- action type independent --> + + <!-- Action type independent + TODO: can be removed once all Action classes are implemented and + derive from ActionCommonDetails. Then the CommonActionForm component can + be used for all Action types. --> <v-card-text - v-if="chosenKindOfAction && !genericActionChosen" + v-if="chosenKindOfAction && !genericActionChosen && !softwareUpdateChosen" > <v-row> <v-col cols="12" md="12"> @@ -255,7 +232,7 @@ permissions and limitations under the Licence. v-else-if="softwareUpdateChosen" color="green" small - @click="addDeviceSoftwareUpdateAction" + @click="addSoftwareUpdateAction" > Add </v-btn> @@ -281,6 +258,7 @@ import { Contact } from '@/models/Contact' import { Attachment } from '@/models/Attachment' import { DeviceProperty } from '@/models/DeviceProperty' import { GenericAction } from '@/models/GenericAction' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' import { IActionType, ActionType } from '@/models/ActionType' import { ACTION_TYPE_API_FILTER_DEVICE } from '@/services/cv/ActionTypeApi' @@ -288,6 +266,7 @@ import { ACTION_TYPE_API_FILTER_DEVICE } from '@/services/cv/ActionTypeApi' import { dateToString, stringToDate } from '@/utils/dateHelper' import GenericActionForm from '@/components/GenericActionForm.vue' +import SoftwareUpdateActionForm from '@/components/SoftwareUpdateActionForm.vue' import DatePicker from '@/components/DatePicker.vue' const KIND_OF_ACTION_TYPE_DEVICE_CALIBRATION = 'device_calibration' @@ -303,6 +282,7 @@ type IOptionsForActionType = Pick<IActionType, 'id' | 'name' | 'uri'> & { @Component({ components: { GenericActionForm, + SoftwareUpdateActionForm, DatePicker } }) @@ -359,6 +339,7 @@ export default class ActionAddPage extends Vue { private endDate: DateTime | null = null private genericDeviceAction: GenericAction = new GenericAction() + private softwareUpdateAction: SoftwareUpdateAction = new SoftwareUpdateAction() private _isSaving: boolean = false @@ -417,6 +398,9 @@ export default class ActionAddPage extends Vue { this.genericDeviceAction.actionTypeName = newValue?.name || '' this.genericDeviceAction.actionTypeUrl = newValue?.uri || '' } + if (this.softwareUpdateChosen) { + this.softwareUpdateAction = new SoftwareUpdateAction() + } } } @@ -543,17 +527,26 @@ export default class ActionAddPage extends Vue { this.$store.commit('snackbar/setError', 'Not implemented yet') } - addDeviceSoftwareUpdateAction () { - if (!(this.$refs.datesForm as Vue & { validate: () => boolean }).validate()) { + addSoftwareUpdateAction () { + if (!this.isLoggedIn) { return } - if (!(this.$refs.softwareTypeForm as Vue & { validate: () => boolean }).validate()) { + if (!this.softwareUpdateAction) { return } - if (!(this.$refs.contactForm as Vue & { validate: () => boolean}).validate()) { + if (!(this.$refs.softwareUpdateActionForm as Vue & { isValid: () => boolean }).isValid()) { + this.isSaving = false + this.$store.commit('snackbar/setError', 'Please correct the errors') return } - this.$store.commit('snackbar/setError', 'Not implemented yet') + this.isSaving = true + this.$api.deviceSoftwareUpdateActions.add(this.deviceId, this.softwareUpdateAction).then((action: SoftwareUpdateAction) => { + this.$router.push('/devices/' + this.deviceId + '/actions', () => this.$emit('input', action)) + }).catch(() => { + this.$store.commit('snackbar/setError', 'Failed to save the action') + }).finally(() => { + this.isSaving = false + }) } addGenericAction () { diff --git a/pages/devices/_deviceId/actions/software-update-actions/_actionId/edit.vue b/pages/devices/_deviceId/actions/software-update-actions/_actionId/edit.vue new file mode 100644 index 0000000000000000000000000000000000000000..86a5d0201411f1ebbd186d9d51a3630bc3ac26fb --- /dev/null +++ b/pages/devices/_deviceId/actions/software-update-actions/_actionId/edit.vue @@ -0,0 +1,175 @@ +<!-- +Web client of the Sensor Management System software developed within the +Helmholtz DataHub Initiative by GFZ and UFZ. + +Copyright (C) 2020, 2021 +- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) +- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) +- Helmholtz Centre Potsdam - GFZ German Research Centre for + Geosciences (GFZ, https://www.gfz-potsdam.de) + +Parts of this program were developed within the context of the +following publicly funded projects or measures: +- Helmholtz Earth and Environment DataHub + (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + +Licensed under the HEESIL, Version 1.0 or - as soon they will be +approved by the "Community" - subsequent versions of the HEESIL +(the "Licence"). + +You may not use this work except in compliance with the Licence. + +You may obtain a copy of the Licence at: +https://gitext.gfz-potsdam.de/software/heesil + +Unless required by applicable law or agreed to in writing, software +distributed under the Licence is distributed on an "AS IS" basis, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the Licence for the specific language governing +permissions and limitations under the Licence. +--> +<template> + <div> + <v-card-actions> + <v-spacer /> + <v-btn + v-if="isLoggedIn" + small + text + nuxt + :to="'/devices/' + deviceId + '/actions'" + > + cancel + </v-btn> + <v-btn + v-if="isLoggedIn" + color="green" + small + :disabled="isSaving" + @click="save" + > + apply + </v-btn> + </v-card-actions> + + <SoftwareUpdateActionForm + ref="softwareUpdateActionForm" + v-model="action" + :attachments="attachments" + /> + + <v-card-actions> + <v-spacer /> + <v-btn + v-if="isLoggedIn" + small + text + nuxt + :to="'/devices/' + deviceId + '/actions'" + > + cancel + </v-btn> + <v-btn + v-if="isLoggedIn" + color="green" + small + :disabled="isSaving" + @click="save" + > + apply + </v-btn> + </v-card-actions> + </div> +</template> + +<script lang="ts"> +import { Component, Vue } from 'nuxt-property-decorator' + +import { Attachment } from '@/models/Attachment' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' + +import SoftwareUpdateActionForm from '@/components/SoftwareUpdateActionForm.vue' + +@Component({ + components: { + SoftwareUpdateActionForm + }, + scrollToTop: true +}) +export default class DeviceSoftwareUpdateActionEditPage extends Vue { + private action: SoftwareUpdateAction = new SoftwareUpdateAction() + private attachments: Attachment[] = [] + private _isLoading: boolean = false + private _isSaving: boolean = false + + async fetch (): Promise<any> { + this.isLoading = true + await Promise.all([ + this.fetchAttachments(), + this.fetchAction() + ]) + this.isLoading = false + } + + async fetchAction (): Promise<any> { + try { + this.action = await this.$api.deviceSoftwareUpdateActions.findById(this.actionId) + } catch (_) { + this.$store.commit('snackbar/setError', 'Failed to fetch action') + } + } + + async fetchAttachments (): Promise<any> { + try { + this.attachments = await this.$api.devices.findRelatedDeviceAttachments(this.deviceId) + } catch (_) { + this.$store.commit('snackbar/setError', 'Failed to fetch attachments') + } + } + + get deviceId (): string { + return this.$route.params.deviceId + } + + get actionId (): string { + return this.$route.params.actionId + } + + get isLoggedIn (): boolean { + return this.$store.getters['oidc/isAuthenticated'] + } + + get isLoading (): boolean { + return this.$data._isLoading + } + + set isLoading (value: boolean) { + this.$data._isLoading = value + this.$emit('showload', value) + } + + get isSaving (): boolean { + return this.$data._isSaving + } + + set isSaving (value: boolean) { + this.$data._isSaving = value + this.$emit('showsave', value) + } + + save (): void { + if (!(this.$refs.softwareUpdateActionForm as Vue & { isValid: () => boolean }).isValid()) { + this.$store.commit('snackbar/setError', 'Please correct the errors') + return + } + this.isSaving = true + this.$api.deviceSoftwareUpdateActions.update(this.deviceId, this.action).then((action: SoftwareUpdateAction) => { + this.$router.push('/devices/' + this.deviceId + '/actions', () => this.$emit('input', action)) + }).catch(() => { + this.$store.commit('snackbar/setError', 'Failed to save the action') + }).finally(() => { + this.isSaving = false + }) + } +} +</script> diff --git a/serializers/jsonapi/ActionAttachmentSerializer.ts b/serializers/jsonapi/ActionAttachmentSerializer.ts new file mode 100644 index 0000000000000000000000000000000000000000..1edf4795021acc37d4a72ab02a9ea96c937b0048 --- /dev/null +++ b/serializers/jsonapi/ActionAttachmentSerializer.ts @@ -0,0 +1,199 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { Attachment } from '@/models/Attachment' + +import { + IJsonApiEntityWithOptionalId, + IJsonApiEntityWithOptionalAttributes, + IJsonApiRelationships, + IJsonApiEntityWithoutDetails, + IJsonApiEntityWithoutDetailsDataDictList +} from '@/serializers/jsonapi/JsonApiTypes' + +import { IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer' +import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer' +import { PlatformAttachmentSerializer } from '@/serializers/jsonapi/PlatformAttachmentSerializer' + +export interface IActionAttachmentSerializer { + attachmentSerializer: IAttachmentSerializer + getActionTypeName (): string + getActionAttachmentTypeName (): string + getActionAttachmentTypeNamePlural (): string + getAttachmentTypeName (): string + convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId + convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[] +} + +export abstract class AbstractActionAttachmentSerializer implements IActionAttachmentSerializer { + abstract get attachmentSerializer (): IAttachmentSerializer + abstract getActionTypeName (): string + abstract getActionAttachmentTypeName (): string + abstract getActionAttachmentTypeNamePlural (): string + abstract getAttachmentTypeName (): string + + convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId { + /** + * 2021-05-07 mha: + * We build the relation to the action by hand instead of using the + * GenericActionSerializer, to avoid circular references. We also + * build the relation to the attachment by hand instead of using the + * DeviceAttachmentSerializer, which uses 'device_attachment' as its + * property whereas we need 'attachment' as the property and + * 'device_attachment' as the type. + */ + const entityType = this.getActionAttachmentTypeName() + const actionType = this.getActionTypeName() + const attachmentType = this.getAttachmentTypeName() + const data: IJsonApiEntityWithOptionalId = { + type: entityType, + attributes: {}, + relationships: { + action: { + data: { + type: actionType, + id: actionId + } + }, + attachment: { + data: { + type: attachmentType, + id: attachment.id || '' + } + } + } + } + return data + } + + convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[] { + const actionAttachmentIds = [] + const entityType = this.getActionAttachmentTypeName() + const typePlural = this.getActionAttachmentTypeNamePlural() + if (relationships[typePlural]) { + const attachmentObject = relationships[typePlural] as IJsonApiEntityWithoutDetailsDataDictList + if (attachmentObject.data && (attachmentObject.data as IJsonApiEntityWithoutDetails[]).length > 0) { + for (const relationShipAttachmentData of attachmentObject.data as IJsonApiEntityWithoutDetails[]) { + const actionAttachmentId = relationShipAttachmentData.id + actionAttachmentIds.push(actionAttachmentId) + } + } + } + + const attachmentIds = [] + if (included && included.length > 0) { + for (const includedEntry of included) { + if (includedEntry.type === entityType) { + const actionAttachmentId = includedEntry.id + if (actionAttachmentIds.includes(actionAttachmentId)) { + if ((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails | undefined)?.id) { + attachmentIds.push((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails).id) + } + } + } + } + } + + const attachmentType = this.getAttachmentTypeName() + const attachments: Attachment[] = [] + if (included && included.length > 0) { + for (const includedEntry of included) { + if (includedEntry.type === attachmentType) { + const attachmentId = includedEntry.id + if (attachmentIds.includes(attachmentId)) { + const attachment = this.attachmentSerializer.convertJsonApiDataToModel(includedEntry) + attachments.push(attachment) + } + } + } + } + + return attachments + } +} + +export class GenericDeviceActionAttachmentSerializer extends AbstractActionAttachmentSerializer { + private _attachmentSerializer: IAttachmentSerializer + + constructor () { + super() + this._attachmentSerializer = new DeviceAttachmentSerializer() + } + + getActionTypeName (): string { + return 'generic_device_action' + } + + getActionAttachmentTypeName (): string { + return 'generic_device_action_attachment' + } + + getActionAttachmentTypeNamePlural (): string { + return this.getActionAttachmentTypeName() + 's' + } + + getAttachmentTypeName (): string { + return 'device_attachment' + } + + get attachmentSerializer (): IAttachmentSerializer { + return this._attachmentSerializer + } +} + +export class GenericPlatformActionAttachmentSerializer extends AbstractActionAttachmentSerializer { + private _attachmentSerializer: IAttachmentSerializer + + constructor () { + super() + this._attachmentSerializer = new PlatformAttachmentSerializer() + } + + getActionTypeName (): string { + return 'generic_platform_action' + } + + getActionAttachmentTypeName (): string { + return 'generic_platform_action_attachment' + } + + getActionAttachmentTypeNamePlural (): string { + return this.getActionAttachmentTypeName() + 's' + } + + getAttachmentTypeName (): string { + return 'platform_attachment' + } + + get attachmentSerializer (): IAttachmentSerializer { + return this._attachmentSerializer + } +} diff --git a/serializers/jsonapi/AttachmentSerializer.ts b/serializers/jsonapi/AttachmentSerializer.ts index 0f757b7d3921c6a79f263841e5a1fb1f6b4de0eb..549090cfe22039d7c57423b9ac56971b6f7c59f3 100644 --- a/serializers/jsonapi/AttachmentSerializer.ts +++ b/serializers/jsonapi/AttachmentSerializer.ts @@ -53,7 +53,13 @@ export interface IAttachmentsAndMissing { missing: IMissingAttachmentData } -export class AttachmentSerializer { +export interface IAttachmentSerializer { + convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): Attachment + convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes): Attachment + convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): Attachment[] +} + +export class AttachmentSerializer implements IAttachmentSerializer { convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): Attachment { const data = jsonApiObject.data return this.convertJsonApiDataToModel(data) diff --git a/serializers/jsonapi/DeviceAttachmentSerializer.ts b/serializers/jsonapi/DeviceAttachmentSerializer.ts index d63a2d7e6f07676b4d7f071c9f2227c866dfb7c5..3031abd289813fd6ef860bf4796a30245d277e03 100644 --- a/serializers/jsonapi/DeviceAttachmentSerializer.ts +++ b/serializers/jsonapi/DeviceAttachmentSerializer.ts @@ -43,9 +43,9 @@ import { IJsonApiEntityWithOptionalAttributes } from '@/serializers/jsonapi/JsonApiTypes' -import { IAttachmentsAndMissing } from '@/serializers/jsonapi/AttachmentSerializer' +import { IAttachmentsAndMissing, IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer' -export class DeviceAttachmentSerializer { +export class DeviceAttachmentSerializer implements IAttachmentSerializer { convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): Attachment { const data = jsonApiObject.data return this.convertJsonApiDataToModel(data) diff --git a/serializers/jsonapi/GenericActionAttachmentSerializer.ts b/serializers/jsonapi/GenericActionAttachmentSerializer.ts index 2500a9899523dbc447cf23c6a7bf7591aca39cd6..a65dd551642249c304e93d26edede4d4cfc0c473 100644 --- a/serializers/jsonapi/GenericActionAttachmentSerializer.ts +++ b/serializers/jsonapi/GenericActionAttachmentSerializer.ts @@ -29,118 +29,54 @@ * implied. See the Licence for the specific language governing * permissions and limitations under the Licence. */ -import { Attachment } from '@/models/Attachment' - -import { - IJsonApiEntityWithOptionalId, - IJsonApiEntityWithOptionalAttributes, - IJsonApiRelationships, - IJsonApiEntityWithoutDetails, - IJsonApiEntityWithoutDetailsDataDictList -} from '@/serializers/jsonapi/JsonApiTypes' - +import { IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer' +import { AbstractActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer' import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer' +import { PlatformAttachmentSerializer } from '@/serializers/jsonapi/PlatformAttachmentSerializer' -export interface IGenericActionAttachmentSerializer { - targetType: string - convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId - convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[] - getActionTypeName (): string - getActionAttachmentTypeName (): string - getActionAttachmentTypeNamePlural (): string - getAttachmentTypeName (): string -} +export class GenericDeviceActionAttachmentSerializer extends AbstractActionAttachmentSerializer { + private _attachmentSerializer: IAttachmentSerializer -export abstract class AbstractGenericActionAttachmentSerializer implements IGenericActionAttachmentSerializer { - private attachmentSerializer: DeviceAttachmentSerializer = new DeviceAttachmentSerializer() + constructor () { + super() + this._attachmentSerializer = new DeviceAttachmentSerializer() + } - abstract get targetType (): string + getActionTypeName (): string { + return 'generic_device_action' + } - convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId { - /** - * 2021-05-07 mha: - * We build the relation to the action by hand instead of using the - * GenericActionSerializer, to avoid circular references. We also - * build the relation to the attachment by hand instead of using the - * DeviceAttachmentSerializer, which uses 'device_attachment' as its - * property whereas we need 'attachment' as the property and - * 'device_attachment' as the type. - */ - const entityType = this.getActionAttachmentTypeName() - const actionType = this.getActionTypeName() - const attachmentType = this.getAttachmentTypeName() - const data: IJsonApiEntityWithOptionalId = { - type: entityType, - attributes: {}, - relationships: { - action: { - data: { - type: actionType, - id: actionId - } - }, - attachment: { - data: { - type: attachmentType, - id: attachment.id || '' - } - } - } - } - return data + getActionAttachmentTypeName (): string { + return 'generic_device_action_attachment' } - convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[] { - const actionAttachmentIds = [] - const entityType = this.getActionAttachmentTypeName() - const typePlural = this.getActionAttachmentTypeNamePlural() - if (relationships[typePlural]) { - const attachmentObject = relationships[typePlural] as IJsonApiEntityWithoutDetailsDataDictList - if (attachmentObject.data && (attachmentObject.data as IJsonApiEntityWithoutDetails[]).length > 0) { - for (const relationShipAttachmentData of attachmentObject.data as IJsonApiEntityWithoutDetails[]) { - const actionAttachmentId = relationShipAttachmentData.id - actionAttachmentIds.push(actionAttachmentId) - } - } - } + getActionAttachmentTypeNamePlural (): string { + return this.getActionAttachmentTypeName() + 's' + } - const attachmentIds = [] - if (included && included.length > 0) { - for (const includedEntry of included) { - if (includedEntry.type === entityType) { - const actionAttachmentId = includedEntry.id - if (actionAttachmentIds.includes(actionAttachmentId)) { - if ((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails | undefined)?.id) { - attachmentIds.push((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails).id) - } - } - } - } - } + getAttachmentTypeName (): string { + return 'device_attachment' + } + + get attachmentSerializer (): IAttachmentSerializer { + return this._attachmentSerializer + } +} - const attachmentType = this.getAttachmentTypeName() - const attachments: Attachment[] = [] - if (included && included.length > 0) { - for (const includedEntry of included) { - if (includedEntry.type === attachmentType) { - const attachmentId = includedEntry.id - if (attachmentIds.includes(attachmentId)) { - const attachment = this.attachmentSerializer.convertJsonApiDataToModel(includedEntry) - attachments.push(attachment) - } - } - } - } +export class GenericPlatformActionAttachmentSerializer extends AbstractActionAttachmentSerializer { + private _attachmentSerializer: IAttachmentSerializer - return attachments + constructor () { + super() + this._attachmentSerializer = new PlatformAttachmentSerializer() } getActionTypeName (): string { - return 'generic_' + this.targetType + '_action' + return 'generic_platform_action' } getActionAttachmentTypeName (): string { - return 'generic_' + this.targetType + '_action_attachment' + return 'generic_platform_action_attachment' } getActionAttachmentTypeNamePlural (): string { @@ -148,18 +84,10 @@ export abstract class AbstractGenericActionAttachmentSerializer implements IGene } getAttachmentTypeName (): string { - return this.targetType + '_attachment' + return 'platform_attachment' } -} - -export class GenericDeviceActionAttachmentSerializer extends AbstractGenericActionAttachmentSerializer { - get targetType (): string { - return 'device' - } -} -export class GenericPlatformActionAttachmentSerializer extends AbstractGenericActionAttachmentSerializer { - get targetType (): string { - return 'platform' + get attachmentSerializer (): IAttachmentSerializer { + return this._attachmentSerializer } } diff --git a/serializers/jsonapi/GenericActionSerializer.ts b/serializers/jsonapi/GenericActionSerializer.ts index 2360bb662a76eb40c8884a06b2b6bbdd63b2423c..3ded4a183f20cefc23ece836acc6b13004028384 100644 --- a/serializers/jsonapi/GenericActionSerializer.ts +++ b/serializers/jsonapi/GenericActionSerializer.ts @@ -47,8 +47,9 @@ import { IContactAndMissing } from '@/serializers/jsonapi/ContactSerializer' +import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer' + import { - IGenericActionAttachmentSerializer, GenericDeviceActionAttachmentSerializer, GenericPlatformActionAttachmentSerializer } from '@/serializers/jsonapi/GenericActionAttachmentSerializer' @@ -58,7 +59,7 @@ export interface IMissingGenericActionData { } export interface IGenericActionsAndMissing { - genericDeviceActions: GenericAction[] + genericActions: GenericAction[] missing: IMissingGenericActionData } @@ -69,7 +70,7 @@ export interface IGenericActionAttachmentRelation { export interface IGenericActionSerializer { targetType: string - attachmentSerializer: IGenericActionAttachmentSerializer + attachmentSerializer: IActionAttachmentSerializer convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): GenericAction convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes, included: IJsonApiEntityWithOptionalAttributes[]): GenericAction convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): GenericAction[] @@ -77,7 +78,7 @@ export interface IGenericActionSerializer { convertModelToJsonApiRelationshipObject (action: IGenericAction): IJsonApiRelationships convertModelToTupleWithIdAndType (action: IGenericAction): IJsonApiEntityWithoutDetails convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionsAndMissing - convertJsonApiIncludedGenericActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[] + convertJsonApiIncludedActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[] getActionTypeName (): string getActionTypeNamePlural (): string getActionAttachmentTypeName (): string @@ -87,7 +88,7 @@ export abstract class AbstractGenericActionSerializer implements IGenericActionS private contactSerializer: ContactSerializer = new ContactSerializer() abstract get targetType (): string - abstract get attachmentSerializer (): IGenericActionAttachmentSerializer + abstract get attachmentSerializer (): IActionAttachmentSerializer convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): GenericAction { const data = jsonApiObject.data @@ -218,14 +219,14 @@ export abstract class AbstractGenericActionSerializer implements IGenericActionS } return { - genericDeviceActions: actions, + genericActions: actions, missing: { ids: missingDataForActionIds } } } - convertJsonApiIncludedGenericActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[] { + convertJsonApiIncludedActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[] { const linkedAttachments: IGenericActionAttachmentRelation[] = [] const type = this.getActionAttachmentTypeName() included.forEach((i) => { @@ -261,35 +262,35 @@ export abstract class AbstractGenericActionSerializer implements IGenericActionS } export class GenericDeviceActionSerializer extends AbstractGenericActionSerializer { - private _attachmentSerializer: IGenericActionAttachmentSerializer + private _attachmentSerializer: IActionAttachmentSerializer + + constructor () { + super() + this._attachmentSerializer = new GenericDeviceActionAttachmentSerializer() + } get targetType (): string { return 'device' } - get attachmentSerializer (): IGenericActionAttachmentSerializer { + get attachmentSerializer (): IActionAttachmentSerializer { return this._attachmentSerializer } +} + +export class GenericPlatformActionSerializer extends AbstractGenericActionSerializer { + private _attachmentSerializer: IActionAttachmentSerializer constructor () { super() - this._attachmentSerializer = new GenericDeviceActionAttachmentSerializer() + this._attachmentSerializer = new GenericPlatformActionAttachmentSerializer() } -} - -export class GenericPlatformActionSerializer extends AbstractGenericActionSerializer { - private _attachmentSerializer: IGenericActionAttachmentSerializer get targetType (): string { return 'platform' } - get attachmentSerializer (): IGenericActionAttachmentSerializer { + get attachmentSerializer (): IActionAttachmentSerializer { return this._attachmentSerializer } - - constructor () { - super() - this._attachmentSerializer = new GenericPlatformActionAttachmentSerializer() - } } diff --git a/serializers/jsonapi/PlatformAttachmentSerializer.ts b/serializers/jsonapi/PlatformAttachmentSerializer.ts index fc369b968796e2c3c20221fa257dc89c47b9a473..b4574abe40e8a74ff5c485b9b703165f1f53cd48 100644 --- a/serializers/jsonapi/PlatformAttachmentSerializer.ts +++ b/serializers/jsonapi/PlatformAttachmentSerializer.ts @@ -43,9 +43,9 @@ import { IJsonApiTypedEntityWithoutDetailsDataDictList, IJsonApiRelationships } from '@/serializers/jsonapi/JsonApiTypes' -import { IAttachmentsAndMissing } from '@/serializers/jsonapi/AttachmentSerializer' +import { IAttachmentsAndMissing, IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer' -export class PlatformAttachmentSerializer { +export class PlatformAttachmentSerializer implements IAttachmentSerializer { convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): Attachment { const data = jsonApiObject.data return this.convertJsonApiDataToModel(data) diff --git a/models/Action.ts b/serializers/jsonapi/SoftwareTypeSerializer.ts similarity index 64% rename from models/Action.ts rename to serializers/jsonapi/SoftwareTypeSerializer.ts index 4d90f32857ac2ac4a11f33edb5331d48c5c42de6..91ebb98a8ce3c4e8cecd47a594e1d428043690e6 100644 --- a/models/Action.ts +++ b/serializers/jsonapi/SoftwareTypeSerializer.ts @@ -29,10 +29,24 @@ * implied. See the Licence for the specific language governing * permissions and limitations under the Licence. */ -import { Contact } from '@/models/Contact' +import { SoftwareType } from '@/models/SoftwareType' -export interface IAction { - id: string | null - description: string - contact: Contact | null +import { + IJsonApiEntityListEnvelope, + IJsonApiEntity +} from '@/serializers/jsonapi/JsonApiTypes' + +export class SoftwareTypeSerializer { + convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): SoftwareType[] { + return jsonApiObjectList.data.map(this.convertJsonApiDataToModel.bind(this)) + } + + convertJsonApiDataToModel (jsonApiData: IJsonApiEntity): SoftwareType { + const id = jsonApiData.id.toString() + const name = jsonApiData.attributes.term + const url = jsonApiData.links?.self || '' + const definition = jsonApiData.attributes.definition + + return SoftwareType.createWithData(id, name, url, definition) + } } diff --git a/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.ts b/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5a156c8abc2c6396da81587a47c8ec395c7bfea --- /dev/null +++ b/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.ts @@ -0,0 +1,93 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer' +import { AbstractActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer' +import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer' +import { PlatformAttachmentSerializer } from '@/serializers/jsonapi/PlatformAttachmentSerializer' + +export class DeviceSoftwareUpdateActionAttachmentSerializer extends AbstractActionAttachmentSerializer { + private _attachmentSerializer: IAttachmentSerializer + + constructor () { + super() + this._attachmentSerializer = new DeviceAttachmentSerializer() + } + + getActionTypeName (): string { + return 'device_software_update_action' + } + + getActionAttachmentTypeName (): string { + return 'device_software_update_action_attachment' + } + + getActionAttachmentTypeNamePlural (): string { + return this.getActionAttachmentTypeName() + 's' + } + + getAttachmentTypeName (): string { + return 'device_attachment' + } + + get attachmentSerializer (): IAttachmentSerializer { + return this._attachmentSerializer + } +} + +export class PlatformSoftwareUpdateActionAttachmentSerializer extends AbstractActionAttachmentSerializer { + private _attachmentSerializer: IAttachmentSerializer + + constructor () { + super() + this._attachmentSerializer = new PlatformAttachmentSerializer() + } + + getActionTypeName (): string { + return 'platform_software_update_action' + } + + getActionAttachmentTypeName (): string { + return 'platform_software_update_action_attachment' + } + + getActionAttachmentTypeNamePlural (): string { + return this.getActionAttachmentTypeName() + 's' + } + + getAttachmentTypeName (): string { + return 'platform_attachment' + } + + get attachmentSerializer (): IAttachmentSerializer { + return this._attachmentSerializer + } +} diff --git a/serializers/jsonapi/SoftwareUpdateActionSerializer.ts b/serializers/jsonapi/SoftwareUpdateActionSerializer.ts new file mode 100644 index 0000000000000000000000000000000000000000..e2202479c19d9574a8455030768bc3245add0b45 --- /dev/null +++ b/serializers/jsonapi/SoftwareUpdateActionSerializer.ts @@ -0,0 +1,298 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { DateTime } from 'luxon' +import { SoftwareUpdateAction, ISoftwareUpdateAction } from '@/models/SoftwareUpdateAction' +import { Attachment } from '@/models/Attachment' +import { + IJsonApiEntityEnvelope, + IJsonApiEntityListEnvelope, + IJsonApiEntityWithOptionalId, + IJsonApiEntityWithOptionalAttributes, + IJsonApiEntityWithoutDetails, + IJsonApiEntityWithoutDetailsDataDictList, + IJsonApiRelationships +} from '@/serializers/jsonapi/JsonApiTypes' + +import { + ContactSerializer, + IContactAndMissing +} from '@/serializers/jsonapi/ContactSerializer' + +import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer' + +import { + DeviceSoftwareUpdateActionAttachmentSerializer, + PlatformSoftwareUpdateActionAttachmentSerializer +} from '@/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer' + +export interface IMissingSoftwareUpdateActionData { + ids: string[] +} + +export interface ISoftwareUpdateActionsAndMissing { + softwareUpdateActions: SoftwareUpdateAction[] + missing: IMissingSoftwareUpdateActionData +} + +export interface ISoftwareUpdateActionAttachmentRelation { + softwareUpdateActionAttachmentId: string + attachmentId: string +} + +export interface ISoftwareUpdateActionSerializer { + targetType: string + attachmentSerializer: IActionAttachmentSerializer + convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): SoftwareUpdateAction + convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes, included: IJsonApiEntityWithOptionalAttributes[]): SoftwareUpdateAction + convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): SoftwareUpdateAction[] + convertModelToJsonApiData (action: SoftwareUpdateAction, deviceOrPlatformId: string): IJsonApiEntityWithOptionalId + convertModelToJsonApiRelationshipObject (action: ISoftwareUpdateAction): IJsonApiRelationships + convertModelToTupleWithIdAndType (action: ISoftwareUpdateAction): IJsonApiEntityWithoutDetails + convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): ISoftwareUpdateActionsAndMissing + convertJsonApiIncludedActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): ISoftwareUpdateActionAttachmentRelation[] + getActionTypeName (): string + getActionTypeNamePlural (): string + getActionAttachmentTypeName (): string +} + +export abstract class AbstractSoftwareUpdateActionSerializer implements ISoftwareUpdateActionSerializer { + private contactSerializer: ContactSerializer = new ContactSerializer() + + abstract get targetType (): string + abstract get attachmentSerializer (): IActionAttachmentSerializer + + convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): SoftwareUpdateAction { + const data = jsonApiObject.data + const included = jsonApiObject.included || [] + return this.convertJsonApiDataToModel(data, included) + } + + convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes, included: IJsonApiEntityWithOptionalAttributes[]): SoftwareUpdateAction { + const attributes = jsonApiData.attributes + const newEntry = SoftwareUpdateAction.createEmpty() + + newEntry.id = jsonApiData.id.toString() + if (attributes) { + newEntry.description = attributes.description || '' + newEntry.softwareTypeName = attributes.software_type_name || '' + newEntry.softwareTypeUrl = attributes.software_type_uri || '' + newEntry.updateDate = attributes.update_date ? DateTime.fromISO(attributes.update_date, { zone: 'UTC' }) : null + newEntry.version = attributes.version || '' + newEntry.repositoryUrl = attributes.repository_url || '' + } + + const relationships = jsonApiData.relationships || {} + + const contactWithMissing: IContactAndMissing = this.contactSerializer.convertJsonApiRelationshipsSingleModel(relationships, included) + if (contactWithMissing.contact) { + newEntry.contact = contactWithMissing.contact + } + + const attachments: Attachment[] = this.attachmentSerializer.convertJsonApiRelationshipsModelList(relationships, included) + if (attachments.length) { + newEntry.attachments = attachments + } + + return newEntry + } + + convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): SoftwareUpdateAction[] { + const included = jsonApiObjectList.included || [] + return jsonApiObjectList.data.map((model) => { + return this.convertJsonApiDataToModel(model, included) + }) + } + + convertModelToJsonApiData (action: SoftwareUpdateAction, deviceOrPlatformId: string): IJsonApiEntityWithOptionalId { + const data: IJsonApiEntityWithOptionalId = { + type: this.getActionTypeName(), + attributes: { + description: action.description, + software_type_name: action.softwareTypeName, + software_type_uri: action.softwareTypeUrl, + update_date: action.updateDate != null ? action.updateDate.setZone('UTC').toISO() : null, + version: action.version, + repository_url: action.repositoryUrl + }, + relationships: { + [this.targetType]: { + data: { + type: this.targetType, + id: deviceOrPlatformId + } + } + } + } + if (action.id) { + data.id = action.id + } + if (action.contact && action.contact.id) { + const contactRelationship = this.contactSerializer.convertModelToJsonApiRelationshipObject(action.contact) + data.relationships = { + ...data.relationships, + ...contactRelationship + } + } + // Note: Attachments are not included and must be send to the backend with + // a relation to the action after this action was saved + return data + } + + convertModelToJsonApiRelationshipObject (action: ISoftwareUpdateAction): IJsonApiRelationships { + return { + [this.getActionTypeName()]: { + data: this.convertModelToTupleWithIdAndType(action) + } + } + } + + convertModelToTupleWithIdAndType (action: ISoftwareUpdateAction): IJsonApiEntityWithoutDetails { + return { + id: action.id || '', + type: this.getActionTypeName() + } + } + + convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): ISoftwareUpdateActionsAndMissing { + const actionIds = [] + const type = this.getActionTypeName() + const typePlural = this.getActionTypeNamePlural() + if (relationships[typePlural]) { + const actionObject = relationships[typePlural] as IJsonApiEntityWithoutDetailsDataDictList + if (actionObject.data && (actionObject.data as IJsonApiEntityWithoutDetails[]).length > 0) { + for (const relationShipActionData of actionObject.data) { + const actionId = relationShipActionData.id + actionIds.push(actionId) + } + } + } + + const possibleActions: { [key: string]: SoftwareUpdateAction } = {} + if (included && included.length > 0) { + for (const includedEntry of included) { + if (includedEntry.type === type) { + const actionId = includedEntry.id + if (actionIds.includes(actionId)) { + const action = this.convertJsonApiDataToModel(includedEntry, []) + possibleActions[actionId] = action + } + } + } + } + + const actions = [] + const missingDataForActionIds = [] + + for (const actionId of actionIds) { + if (possibleActions[actionId]) { + actions.push(possibleActions[actionId]) + } else { + missingDataForActionIds.push(actionId) + } + } + + return { + softwareUpdateActions: actions, + missing: { + ids: missingDataForActionIds + } + } + } + + convertJsonApiIncludedActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): ISoftwareUpdateActionAttachmentRelation[] { + const linkedAttachments: ISoftwareUpdateActionAttachmentRelation[] = [] + const type = this.getActionAttachmentTypeName() + included.forEach((i) => { + if (!i.id) { + return + } + if (i.type !== type) { + return + } + if (!i.relationships?.attachment || !i.relationships?.attachment.data || !(i.relationships?.attachment.data as IJsonApiEntityWithoutDetails).id) { + return + } + const attachmentId: string = (i.relationships.attachment.data as IJsonApiEntityWithoutDetails).id + linkedAttachments.push({ + softwareUpdateActionAttachmentId: i.id, + attachmentId + }) + }) + return linkedAttachments + } + + getActionTypeName (): string { + return this.targetType + '_software_update_action' + } + + getActionTypeNamePlural (): string { + return this.getActionTypeName() + 's' + } + + getActionAttachmentTypeName (): string { + return this.targetType + '_software_update_action_attachment' + } +} + +export class DeviceSoftwareUpdateActionSerializer extends AbstractSoftwareUpdateActionSerializer { + private _attachmentSerializer: IActionAttachmentSerializer + + constructor () { + super() + this._attachmentSerializer = new DeviceSoftwareUpdateActionAttachmentSerializer() + } + + get targetType (): string { + return 'device' + } + + get attachmentSerializer (): IActionAttachmentSerializer { + return this._attachmentSerializer + } +} + +export class PlatformSoftwareUpdateActionSerializer extends AbstractSoftwareUpdateActionSerializer { + private _attachmentSerializer: IActionAttachmentSerializer + + constructor () { + super() + this._attachmentSerializer = new PlatformSoftwareUpdateActionAttachmentSerializer() + } + + get targetType (): string { + return 'platform' + } + + get attachmentSerializer (): IActionAttachmentSerializer { + return this._attachmentSerializer + } +} diff --git a/services/Api.ts b/services/Api.ts index 551e38f1a6e40d1dd7b065e61f0e6ce4af9ddacd..bb33f4a475b7d0162678d0471a171124d50c82e3 100644 --- a/services/Api.ts +++ b/services/Api.ts @@ -41,7 +41,10 @@ import { CustomfieldsApi } from '@/services/sms/CustomfieldsApi' import { DeviceAttachmentApi } from '@/services/sms/DeviceAttachmentApi' import { PlatformAttachmentApi } from '@/services/sms/PlatformAttachmentApi' import { GenericDeviceActionApi } from '@/services/sms/GenericDeviceActionApi' -import { GenericDeviceActionAttachmentApi } from '@/services/sms/GenericDeviceActionAttachmentApi' +import { GenericDeviceActionAttachmentApi, GenericPlatformActionAttachmentApi } from '@/services/sms/GenericActionAttachmentApi' +import { DeviceSoftwareUpdateActionApi } from '@/services/sms/DeviceSoftwareUpdateActionApi' +import { PlatformSoftwareUpdateActionApi } from '@/services/sms/PlatformSoftwareUpdateActionApi' +import { DeviceSoftwareUpdateActionAttachmentApi, PlatformSoftwareUpdateActionAttachmentApi } from '@/services/sms/SoftwareUpdateActionAttachmentApi' import { CompartmentApi } from '@/services/cv/CompartmentApi' import { DeviceTypeApi } from '@/services/cv/DeviceTypeApi' @@ -53,6 +56,7 @@ import { StatusApi } from '@/services/cv/StatusApi' import { UnitApi } from '@/services/cv/UnitApi' import { MeasuredQuantityUnitApi } from '@/services/cv/MeasuredQuantityUnitApi' import { ActionTypeApi } from '@/services/cv/ActionTypeApi' +import { SoftwareTypeApi } from '@/services/cv/SoftwareTypeApi' import { ProjectApi } from '@/services/project/ProjectApi' @@ -71,6 +75,11 @@ export class Api { private readonly _devicePropertyApi: DevicePropertyApi private readonly _genericDeviceActionApi: GenericDeviceActionApi private readonly _genericDeviceActionAttachmentApi: GenericDeviceActionAttachmentApi + private readonly _genericPlatformActionAttachmentApi: GenericPlatformActionAttachmentApi + private readonly _deviceSoftwareUpdateActionApi: DeviceSoftwareUpdateActionApi + private readonly _deviceSoftwareUpdateActionAttachmentApi: DeviceSoftwareUpdateActionAttachmentApi + private readonly _platformSoftwareUpdateActionApi: PlatformSoftwareUpdateActionApi + private readonly _platformSoftwareUpdateActionAttachmentApi: PlatformSoftwareUpdateActionAttachmentApi private readonly _manufacturerApi: ManufacturerApi private readonly _platformTypeApi: PlatformTypeApi @@ -82,6 +91,7 @@ export class Api { private readonly _unitApi: UnitApi private readonly _measuredQuantityUnitApi: MeasuredQuantityUnitApi private readonly _actionTypeApi: ActionTypeApi + private readonly _softwareTypeApi: SoftwareTypeApi private readonly _projectApi: ProjectApi @@ -138,6 +148,28 @@ export class Api { this._genericDeviceActionAttachmentApi ) + this._genericPlatformActionAttachmentApi = new GenericPlatformActionAttachmentApi( + this.createAxios(smsBaseUrl, '/generic-platform-action-attachments', smsConfig, getIdToken) + ) + + this._deviceSoftwareUpdateActionAttachmentApi = new DeviceSoftwareUpdateActionAttachmentApi( + this.createAxios(smsBaseUrl, '/device-software-update-action-attachments', smsConfig, getIdToken) + ) + + this._deviceSoftwareUpdateActionApi = new DeviceSoftwareUpdateActionApi( + this.createAxios(smsBaseUrl, '/device-software-update-actions', smsConfig, getIdToken), + this._deviceSoftwareUpdateActionAttachmentApi + ) + + this._platformSoftwareUpdateActionAttachmentApi = new PlatformSoftwareUpdateActionAttachmentApi( + this.createAxios(smsBaseUrl, '/platform-software-update-action-attachments', smsConfig, getIdToken) + ) + + this._platformSoftwareUpdateActionApi = new PlatformSoftwareUpdateActionApi( + this.createAxios(smsBaseUrl, '/platform-software-update-actions', smsConfig, getIdToken), + this._platformSoftwareUpdateActionAttachmentApi + ) + // and here we can set settings for all the cv api calls const cvConfig: AxiosRequestConfig = { headers: { @@ -177,6 +209,9 @@ export class Api { this._actionTypeApi = new ActionTypeApi( this.createAxios(cvBaseUrl, '/actiontypes/', cvConfig) ) + this._softwareTypeApi = new SoftwareTypeApi( + this.createAxios(cvBaseUrl, '/softwaretypes/', cvConfig) + ) this._projectApi = new ProjectApi() } @@ -245,6 +280,22 @@ export class Api { return this._genericDeviceActionAttachmentApi } + get deviceSoftwareUpdateActions (): DeviceSoftwareUpdateActionApi { + return this._deviceSoftwareUpdateActionApi + } + + get deviceSoftwareUpdateActionAttachments (): DeviceSoftwareUpdateActionAttachmentApi { + return this._deviceSoftwareUpdateActionAttachmentApi + } + + get platformSoftwareUpdateActions (): PlatformSoftwareUpdateActionApi { + return this._platformSoftwareUpdateActionApi + } + + get platformSoftwareUpdateActionAttachments (): PlatformSoftwareUpdateActionAttachmentApi { + return this._platformSoftwareUpdateActionAttachmentApi + } + get contacts (): ContactApi { return this._contactApi } @@ -289,6 +340,10 @@ export class Api { return this._actionTypeApi } + get softwareTypes (): SoftwareTypeApi { + return this._softwareTypeApi + } + get projects (): ProjectApi { return this._projectApi } diff --git a/services/cv/SoftwareTypeApi.ts b/services/cv/SoftwareTypeApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..769729a8b8b566c045222fc2fea1874c433241f5 --- /dev/null +++ b/services/cv/SoftwareTypeApi.ts @@ -0,0 +1,155 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2020 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { AxiosInstance } from 'axios' + +import { SoftwareType } from '@/models/SoftwareType' +import { SoftwareTypeSerializer } from '@/serializers/jsonapi/SoftwareTypeSerializer' +import { CVApi } from '@/services/cv/CVApi' + +import { IPaginationLoader } from '@/utils/PaginatedLoader' + +export class SoftwareTypeApi extends CVApi<SoftwareType> { + private serializer: SoftwareTypeSerializer + + constructor (axiosInstance: AxiosInstance) { + super(axiosInstance) + this.serializer = new SoftwareTypeSerializer() + } + + newSearchBuilder (): SoftwareTypeSearchBuilder { + return new SoftwareTypeSearchBuilder(this.axiosApi, this.serializer) + } + + findAll (): Promise<SoftwareType[]> { + return this.newSearchBuilder().build().findMatchingAsList().then(data => SoftwareTypeApi.sort(data)) + } + + findAllPaginated (pageSize: number = 100): Promise<SoftwareType[]> { + return this.newSearchBuilder().build().findMatchingAsPaginationLoader(pageSize).then(loader => this.loadPaginated(loader)).then(data => SoftwareTypeApi.sort(data)) + } + + static sort (softwareTypes: SoftwareType[]): SoftwareType[] { + const softwareTypesCopy: SoftwareType[] = [...softwareTypes] + // sort alphabetical + softwareTypesCopy.sort((a, b) => { + if (a.name < b.name) { + return -1 + } + if (a.name > b.name) { + return 1 + } + return 0 + }) + // move 'Others' to the end + const othersIndex: number = softwareTypesCopy.findIndex(i => i.name.toLowerCase() === 'others') + if (othersIndex > -1) { + const other: SoftwareType = softwareTypesCopy.splice(othersIndex, 1)[0] + softwareTypesCopy.push(other) + } + return softwareTypesCopy + } +} + +export class SoftwareTypeSearchBuilder { + private axiosApi: AxiosInstance + private serializer: SoftwareTypeSerializer + + constructor (axiosApi: AxiosInstance, serializer: SoftwareTypeSerializer) { + this.axiosApi = axiosApi + this.serializer = serializer + } + + build (): SoftwareTypeSearcher { + return new SoftwareTypeSearcher(this.axiosApi, this.serializer) + } +} + +export class SoftwareTypeSearcher { + private axiosApi: AxiosInstance + private serializer: SoftwareTypeSerializer + + constructor (axiosApi: AxiosInstance, serializer: SoftwareTypeSerializer) { + this.axiosApi = axiosApi + this.serializer = serializer + } + + private findAllOnPage (page: number, pageSize: number): Promise<IPaginationLoader<SoftwareType>> { + const params: { [idx: string]: any } = { + 'page[size]': pageSize, + 'page[number]': page, + 'filter[status.iexact]': 'ACCEPTED', + sort: 'term' + } + return this.axiosApi.get( + '', + { + params + } + ).then((rawResponse) => { + const response = rawResponse.data + const elements: SoftwareType[] = this.serializer.convertJsonApiObjectListToModelList(response) + const totalCount = response.meta.count + + let funToLoadNext = null + if (response.meta.pagination.page < response.meta.pagination.pages) { + funToLoadNext = () => this.findAllOnPage(page + 1, pageSize) + } + + return { + elements, + totalCount, + funToLoadNext + } + }) + } + + findMatchingAsList (): Promise<SoftwareType[]> { + const params: { [idx: string]: any } = { + 'page[size]': 10000, + 'filter[status.iexact]': 'ACCEPTED', + sort: 'term' + } + return this.axiosApi.get( + '', + { + params + } + ).then((rawResponse) => { + const response = rawResponse.data + return this.serializer.convertJsonApiObjectListToModelList(response) + }) + } + + findMatchingAsPaginationLoader (pageSize: number): Promise<IPaginationLoader<SoftwareType>> { + return this.findAllOnPage(1, pageSize) + } +} diff --git a/services/sms/GenericDeviceActionAttachmentApi.ts b/services/sms/ActionAttachmentApi.ts similarity index 81% rename from services/sms/GenericDeviceActionAttachmentApi.ts rename to services/sms/ActionAttachmentApi.ts index b812717b3f39c75822eedf5d13a083103ec917d8..2792e80d7243fbddbb65753c7936f64ad4fd41e8 100644 --- a/services/sms/GenericDeviceActionAttachmentApi.ts +++ b/services/sms/ActionAttachmentApi.ts @@ -32,15 +32,21 @@ import { AxiosInstance } from 'axios' import { Attachment } from '@/models/Attachment' -import { IGenericActionAttachmentSerializer, GenericDeviceActionAttachmentSerializer } from '@/serializers/jsonapi/GenericActionAttachmentSerializer' +import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer' -export class GenericDeviceActionAttachmentApi { +export interface IActionAttachmentApi { + serializer: IActionAttachmentSerializer + add (actionId: string, attachment: Attachment): Promise<any> + delete (id: string): Promise<void> +} + +export abstract class AbstractActionAttachmentApi implements IActionAttachmentApi { private axiosApi: AxiosInstance - private serializer: IGenericActionAttachmentSerializer + + abstract get serializer (): IActionAttachmentSerializer constructor (axiosInstance: AxiosInstance) { this.axiosApi = axiosInstance - this.serializer = new GenericDeviceActionAttachmentSerializer() } async add (actionId: string, attachment: Attachment): Promise<any> { diff --git a/services/sms/DeviceApi.ts b/services/sms/DeviceApi.ts index 6447ea70494bb84a1262efb59f4cf2a9487a23a0..954c54902fed4e23d2dce651fe70ebda20075847 100644 --- a/services/sms/DeviceApi.ts +++ b/services/sms/DeviceApi.ts @@ -40,12 +40,14 @@ import { DeviceType } from '@/models/DeviceType' import { Manufacturer } from '@/models/Manufacturer' import { Status } from '@/models/Status' import { GenericAction } from '@/models/GenericAction' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' import { ContactSerializer } from '@/serializers/jsonapi/ContactSerializer' import { CustomTextFieldSerializer } from '@/serializers/jsonapi/CustomTextFieldSerializer' import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer' import { DevicePropertySerializer } from '@/serializers/jsonapi/DevicePropertySerializer' import { GenericDeviceActionSerializer } from '@/serializers/jsonapi/GenericActionSerializer' +import { DeviceSoftwareUpdateActionSerializer } from '@/serializers/jsonapi/SoftwareUpdateActionSerializer' import { IFlaskJSONAPIFilter } from '@/utils/JSONApiInterfaces' @@ -211,6 +213,20 @@ export class DeviceApi { return new GenericDeviceActionSerializer().convertJsonApiObjectListToModelList(rawServerResponse.data) }) } + + findRelatedSoftwareUpdateActions (deviceId: string): Promise<SoftwareUpdateAction[]> { + const url = deviceId + '/device-software-update-actions' + const params = { + 'page[size]': 10000, + include: [ + 'contact', + 'device_software_update_action_attachments.attachment' + ].join(',') + } + return this.axiosApi.get(url, { params }).then((rawServerResponse) => { + return new DeviceSoftwareUpdateActionSerializer().convertJsonApiObjectListToModelList(rawServerResponse.data) + }) + } } export class DeviceSearchBuilder { diff --git a/services/sms/DeviceSoftwareUpdateActionApi.ts b/services/sms/DeviceSoftwareUpdateActionApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..a91647bf95a4a8b49aee677b1c6e815876e15bd7 --- /dev/null +++ b/services/sms/DeviceSoftwareUpdateActionApi.ts @@ -0,0 +1,146 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { AxiosInstance } from 'axios' + +import { Attachment } from '@/models/Attachment' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' +import { DeviceSoftwareUpdateActionAttachmentApi } from '@/services/sms/SoftwareUpdateActionAttachmentApi' +import { ISoftwareUpdateActionSerializer, DeviceSoftwareUpdateActionSerializer } from '@/serializers/jsonapi/SoftwareUpdateActionSerializer' + +export class DeviceSoftwareUpdateActionApi { + private axiosApi: AxiosInstance + private serializer: ISoftwareUpdateActionSerializer + private attachmentApi: DeviceSoftwareUpdateActionAttachmentApi + + constructor (axiosInstance: AxiosInstance, attachmentApi: DeviceSoftwareUpdateActionAttachmentApi) { + this.axiosApi = axiosInstance + this.serializer = new DeviceSoftwareUpdateActionSerializer() + this.attachmentApi = attachmentApi + } + + async findById (id: string): Promise<SoftwareUpdateAction> { + const response = await this.axiosApi.get(id, { + params: { + include: [ + 'contact', + 'device_software_update_action_attachments.attachment' + ].join(',') + } + }) + const data = response.data + return this.serializer.convertJsonApiObjectToModel(data) + } + + deleteById (id: string): Promise<void> { + return this.axiosApi.delete<string, void>(id) + } + + async add (deviceId: string, action: SoftwareUpdateAction): Promise<SoftwareUpdateAction> { + const url = '' + const data = this.serializer.convertModelToJsonApiData(action, deviceId) + const response = await this.axiosApi.post(url, { data }) + const savedAction = this.serializer.convertJsonApiObjectToModel(response.data) + // save every attachment as an ActionAttachment + if (savedAction.id) { + const promises = action.attachments.map((attachment: Attachment) => this.attachmentApi.add(savedAction.id as string, attachment)) + await Promise.all(promises) + } + return savedAction + } + + async update (deviceId: string, action: SoftwareUpdateAction): Promise<SoftwareUpdateAction> { + if (!action.id) { + throw new Error('no id for the SoftwareUpdateAction') + } + // load the stored action to get a list of the device action attachments before the update + const attRawResponse = await this.axiosApi.get(action.id, { + params: { + include: [ + 'device_software_update_action_attachments.attachment' + ].join(',') + } + }) + const attResponseData = attRawResponse.data + const included = attResponseData.included + + // get the relations between attachments and device action attachments + const linkedAttachments: { [attachmentId: string]: string } = {} + if (included) { + const relations = this.serializer.convertJsonApiIncludedActionAttachmentsToIdList(included) + // convert to object to gain faster access to its members + relations.forEach((rel) => { + linkedAttachments[rel.attachmentId] = rel.softwareUpdateActionAttachmentId + }) + } + + // update the action + const data = this.serializer.convertModelToJsonApiData(action, deviceId) + const actionResponse = await this.axiosApi.patch(action.id, { data }) + + // find new attachments + const newAttachments: Attachment[] = [] + action.attachments.forEach((attachment: Attachment) => { + if (attachment.id && linkedAttachments[attachment.id]) { + return + } + newAttachments.push(attachment) + }) + + // find deleted attachments + const deviceActionAttachmentsToDelete: string[] = [] + for (const attachmentId in linkedAttachments) { + if (action.attachments.find((i: Attachment) => i.id === attachmentId)) { + continue + } + deviceActionAttachmentsToDelete.push(linkedAttachments[attachmentId]) + } + + // when there are no new attachments, newPromises is empty, which is okay + const newPromises = newAttachments.map((attachment: Attachment) => this.attachmentApi.add(action.id as string, attachment)) + // when there are no deleted attachments, deletedPromises is empty, which is okay + const deletedPromises = deviceActionAttachmentsToDelete.map((id: string) => this.attachmentApi.delete(id)) + await Promise.all([...deletedPromises, ...newPromises]) + + return this.serializer.convertJsonApiObjectToModel(actionResponse.data) + } + + findRelatedActionAttachments (actionId: string): Promise<SoftwareUpdateAction[]> { + const url = actionId + '/device-software-update-action-attachments' + const params = { + 'page[size]': 10000, + include: 'attachment' + } + return this.axiosApi.get(url, { params }).then((rawServerResponse) => { + return this.serializer.convertJsonApiObjectListToModelList(rawServerResponse.data) + }) + } +} diff --git a/services/sms/GenericActionAttachmentApi.ts b/services/sms/GenericActionAttachmentApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbb17b28511d05ecb31676cdf0924970cec03648 --- /dev/null +++ b/services/sms/GenericActionAttachmentApi.ts @@ -0,0 +1,62 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { AxiosInstance } from 'axios' + +import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer' +import { GenericDeviceActionAttachmentSerializer, GenericPlatformActionAttachmentSerializer } from '@/serializers/jsonapi/GenericActionAttachmentSerializer' +import { AbstractActionAttachmentApi } from '@/services/sms/ActionAttachmentApi' + +export class GenericDeviceActionAttachmentApi extends AbstractActionAttachmentApi { + private _serializer: IActionAttachmentSerializer + + constructor (axiosInstance: AxiosInstance) { + super(axiosInstance) + this._serializer = new GenericDeviceActionAttachmentSerializer() + } + + get serializer (): IActionAttachmentSerializer { + return this._serializer + } +} + +export class GenericPlatformActionAttachmentApi extends AbstractActionAttachmentApi { + private _serializer: IActionAttachmentSerializer + + constructor (axiosInstance: AxiosInstance) { + super(axiosInstance) + this._serializer = new GenericPlatformActionAttachmentSerializer() + } + + get serializer (): IActionAttachmentSerializer { + return this._serializer + } +} diff --git a/services/sms/GenericDeviceActionApi.ts b/services/sms/GenericDeviceActionApi.ts index 4faf5eb067408d8b836f2b9d53b93b26de93619e..9a8a6724e12b3beaa90f5395374c93318589c206 100644 --- a/services/sms/GenericDeviceActionApi.ts +++ b/services/sms/GenericDeviceActionApi.ts @@ -33,7 +33,7 @@ import { AxiosInstance } from 'axios' import { Attachment } from '@/models/Attachment' import { GenericAction } from '@/models/GenericAction' -import { GenericDeviceActionAttachmentApi } from '@/services/sms/GenericDeviceActionAttachmentApi' +import { GenericDeviceActionAttachmentApi } from '@/services/sms/GenericActionAttachmentApi' import { IGenericActionSerializer, GenericDeviceActionSerializer } from '@/serializers/jsonapi/GenericActionSerializer' export class GenericDeviceActionApi { @@ -95,7 +95,7 @@ export class GenericDeviceActionApi { // get the relations between attachments and generic device action attachments const linkedAttachments: { [attachmentId: string]: string } = {} if (included) { - const relations = this.serializer.convertJsonApiIncludedGenericActionAttachmentsToIdList(included) + const relations = this.serializer.convertJsonApiIncludedActionAttachmentsToIdList(included) // convert to object to gain faster access to its members relations.forEach((rel) => { linkedAttachments[rel.attachmentId] = rel.genericActionAttachmentId diff --git a/services/sms/PlatformSoftwareUpdateActionApi.ts b/services/sms/PlatformSoftwareUpdateActionApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..81b5b4fe76c664e5354c30e1672411fc8cb3d798 --- /dev/null +++ b/services/sms/PlatformSoftwareUpdateActionApi.ts @@ -0,0 +1,146 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { AxiosInstance } from 'axios' + +import { Attachment } from '@/models/Attachment' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' +import { PlatformSoftwareUpdateActionAttachmentApi } from '@/services/sms/SoftwareUpdateActionAttachmentApi' +import { ISoftwareUpdateActionSerializer, PlatformSoftwareUpdateActionSerializer } from '@/serializers/jsonapi/SoftwareUpdateActionSerializer' + +export class PlatformSoftwareUpdateActionApi { + private axiosApi: AxiosInstance + private serializer: ISoftwareUpdateActionSerializer + private attachmentApi: PlatformSoftwareUpdateActionAttachmentApi + + constructor (axiosInstance: AxiosInstance, attachmentApi: PlatformSoftwareUpdateActionAttachmentApi) { + this.axiosApi = axiosInstance + this.serializer = new PlatformSoftwareUpdateActionSerializer() + this.attachmentApi = attachmentApi + } + + async findById (id: string): Promise<SoftwareUpdateAction> { + const response = await this.axiosApi.get(id, { + params: { + include: [ + 'contact', + 'platform_software_update_action_attachments.attachment' + ].join(',') + } + }) + const data = response.data + return this.serializer.convertJsonApiObjectToModel(data) + } + + deleteById (id: string): Promise<void> { + return this.axiosApi.delete<string, void>(id) + } + + async add (platformId: string, action: SoftwareUpdateAction): Promise<SoftwareUpdateAction> { + const url = '' + const data = this.serializer.convertModelToJsonApiData(action, platformId) + const response = await this.axiosApi.post(url, { data }) + const savedAction = this.serializer.convertJsonApiObjectToModel(response.data) + // save every attachment as an ActionAttachment + if (savedAction.id) { + const promises = action.attachments.map((attachment: Attachment) => this.attachmentApi.add(savedAction.id as string, attachment)) + await Promise.all(promises) + } + return savedAction + } + + async update (platformId: string, action: SoftwareUpdateAction): Promise<SoftwareUpdateAction> { + if (!action.id) { + throw new Error('no id for the SoftwareUpdateAction') + } + // load the stored action to get a list of the platform action attachments before the update + const attRawResponse = await this.axiosApi.get(action.id, { + params: { + include: [ + 'platform_software_update_action_attachments.attachment' + ].join(',') + } + }) + const attResponseData = attRawResponse.data + const included = attResponseData.included + + // get the relations between attachments and platform action attachments + const linkedAttachments: { [attachmentId: string]: string } = {} + if (included) { + const relations = this.serializer.convertJsonApiIncludedActionAttachmentsToIdList(included) + // convert to object to gain faster access to its members + relations.forEach((rel) => { + linkedAttachments[rel.attachmentId] = rel.softwareUpdateActionAttachmentId + }) + } + + // update the action + const data = this.serializer.convertModelToJsonApiData(action, platformId) + const actionResponse = await this.axiosApi.patch(action.id, { data }) + + // find new attachments + const newAttachments: Attachment[] = [] + action.attachments.forEach((attachment: Attachment) => { + if (attachment.id && linkedAttachments[attachment.id]) { + return + } + newAttachments.push(attachment) + }) + + // find deleted attachments + const platformActionAttachmentsToDelete: string[] = [] + for (const attachmentId in linkedAttachments) { + if (action.attachments.find((i: Attachment) => i.id === attachmentId)) { + continue + } + platformActionAttachmentsToDelete.push(linkedAttachments[attachmentId]) + } + + // when there are no new attachments, newPromises is empty, which is okay + const newPromises = newAttachments.map((attachment: Attachment) => this.attachmentApi.add(action.id as string, attachment)) + // when there are no deleted attachments, deletedPromises is empty, which is okay + const deletedPromises = platformActionAttachmentsToDelete.map((id: string) => this.attachmentApi.delete(id)) + await Promise.all([...deletedPromises, ...newPromises]) + + return this.serializer.convertJsonApiObjectToModel(actionResponse.data) + } + + findRelatedActionAttachments (actionId: string): Promise<SoftwareUpdateAction[]> { + const url = actionId + '/platform-software-update-action-attachments' + const params = { + 'page[size]': 10000, + include: 'attachment' + } + return this.axiosApi.get(url, { params }).then((rawServerResponse) => { + return this.serializer.convertJsonApiObjectListToModelList(rawServerResponse.data) + }) + } +} diff --git a/services/sms/SoftwareUpdateActionAttachmentApi.ts b/services/sms/SoftwareUpdateActionAttachmentApi.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf9e1347dda4da262a2c74b7d0b6c3354c9be5e0 --- /dev/null +++ b/services/sms/SoftwareUpdateActionAttachmentApi.ts @@ -0,0 +1,62 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { AxiosInstance } from 'axios' + +import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer' +import { DeviceSoftwareUpdateActionAttachmentSerializer, PlatformSoftwareUpdateActionAttachmentSerializer } from '@/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer' +import { AbstractActionAttachmentApi } from '@/services/sms/ActionAttachmentApi' + +export class DeviceSoftwareUpdateActionAttachmentApi extends AbstractActionAttachmentApi { + private _serializer: IActionAttachmentSerializer + + constructor (axiosInstance: AxiosInstance) { + super(axiosInstance) + this._serializer = new DeviceSoftwareUpdateActionAttachmentSerializer() + } + + get serializer (): IActionAttachmentSerializer { + return this._serializer + } +} + +export class PlatformSoftwareUpdateActionAttachmentApi extends AbstractActionAttachmentApi { + private _serializer: IActionAttachmentSerializer + + constructor (axiosInstance: AxiosInstance) { + super(axiosInstance) + this._serializer = new PlatformSoftwareUpdateActionAttachmentSerializer() + } + + get serializer (): IActionAttachmentSerializer { + return this._serializer + } +} diff --git a/test/models/GenericAction.test.ts b/test/models/GenericAction.test.ts index e54200bf21b1f07899c59b1914d5709a53a14b82..ce47e0e4eaba98be5f2d02c9ad0ce8674f103e09 100644 --- a/test/models/GenericAction.test.ts +++ b/test/models/GenericAction.test.ts @@ -67,6 +67,7 @@ describe('GenericAction', () => { expect(action).toHaveProperty('actionTypeUrl', 'https://foo/bar') expect(action.beginDate).toBe(date1) expect(action.endDate).toBe(date2) + expect(action.date).toBe(date1) expect(action.contact).toStrictEqual(contact) expect(action.attachments).toContainEqual(attachment) expect(action.isGenericAction).toBeTruthy() diff --git a/test/models/SoftwareUpdateAction.test.ts b/test/models/SoftwareUpdateAction.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8880bd80f52b44d8d7c97ee407d7a8fc8b6a8d8f --- /dev/null +++ b/test/models/SoftwareUpdateAction.test.ts @@ -0,0 +1,76 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2020, 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { DateTime } from 'luxon' +import { Attachment } from '@/models/Attachment' +import { Contact } from '@/models/Contact' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' + +describe('SoftwareUpdateAction', () => { + test('create a SoftwareUpdateAction from an object', () => { + const date1 = DateTime.fromISO('2021-05-27') + + const attachment = new Attachment() + attachment.id = '1' + attachment.label = 'an attachment' + attachment.url = 'https://foo/baz' + + const contact = new Contact() + contact.givenName = 'Homer' + contact.familyName = 'Simpson' + contact.email = 'homer.simpson@springfield.com' + + const action = SoftwareUpdateAction.createFromObject({ + id: '1', + description: 'This is a software update action description', + softwareTypeName: 'Software Update', + softwareTypeUrl: 'https://foo/bar', + updateDate: date1, + version: '1.03', + repositoryUrl: 'https://git.gfz-potsdam.de/sensor-system-management/frontend', + contact, + attachments: [attachment] + }) + + expect(typeof action).toBe('object') + expect(action).toHaveProperty('id', '1') + expect(action).toHaveProperty('description', 'This is a software update action description') + expect(action).toHaveProperty('softwareTypeName', 'Software Update') + expect(action).toHaveProperty('softwareTypeUrl', 'https://foo/bar') + expect(action).toHaveProperty('version', '1.03') + expect(action).toHaveProperty('repositoryUrl', 'https://git.gfz-potsdam.de/sensor-system-management/frontend') + expect(action.updateDate).toBe(date1) + expect(action.date).toBe(date1) + expect(action.contact).toStrictEqual(contact) + expect(action.attachments).toContainEqual(attachment) + expect(action.isSoftwareUpdateAction).toBeTruthy() + }) +}) diff --git a/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts b/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts index 951aeca4efde212c219eb0fdddd4b7a6e4b3f583..0fa289805dcc7ca27916fcfd7926825614e86a1b 100644 --- a/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts +++ b/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts @@ -208,10 +208,6 @@ describe('GenericActionAttachmentSerializer', () => { } describe('constructing and types', () => { - it('should return \'device\' as its type', () => { - const serializer = new GenericDeviceActionAttachmentSerializer() - expect(serializer.targetType).toEqual('device') - }) it('should return a correct action type name', () => { const serializer = new GenericDeviceActionAttachmentSerializer() expect(serializer.getActionTypeName()).toEqual('generic_device_action') @@ -228,6 +224,10 @@ describe('GenericActionAttachmentSerializer', () => { const serializer = new GenericDeviceActionAttachmentSerializer() expect(serializer.getAttachmentTypeName()).toEqual('device_attachment') }) + it('should return an attachment serializer', () => { + const serializer = new GenericDeviceActionAttachmentSerializer() + expect(typeof serializer.attachmentSerializer).toBe('object') + }) }) describe('#convertModelToJsonApiData', () => { it('should return a JSON API object from an attachment and an action id', () => { @@ -289,10 +289,6 @@ describe('GenericActionAttachmentSerializer', () => { }) describe('GenericPlatformActionAttachmentSerializer', () => { describe('constructing and types', () => { - it('should return \'platform\' as its type', () => { - const serializer = new GenericPlatformActionAttachmentSerializer() - expect(serializer.targetType).toEqual('platform') - }) it('should return a correct action type name', () => { const serializer = new GenericPlatformActionAttachmentSerializer() expect(serializer.getActionTypeName()).toEqual('generic_platform_action') @@ -309,6 +305,10 @@ describe('GenericActionAttachmentSerializer', () => { const serializer = new GenericPlatformActionAttachmentSerializer() expect(serializer.getAttachmentTypeName()).toEqual('platform_attachment') }) + it('should return an attachment serializer', () => { + const serializer = new GenericPlatformActionAttachmentSerializer() + expect(typeof serializer.attachmentSerializer).toBe('object') + }) }) }) }) diff --git a/test/serializers/jsonapi/GenericActionSerializer.test.ts b/test/serializers/jsonapi/GenericActionSerializer.test.ts index 44c487d53120d5ebc0fe102a852cf86f4f2e591a..572d4b4be8c7aef898317159886bfd54c93fdbbf 100644 --- a/test/serializers/jsonapi/GenericActionSerializer.test.ts +++ b/test/serializers/jsonapi/GenericActionSerializer.test.ts @@ -787,9 +787,9 @@ describe('GenericActionSerializer', () => { const actionList = serializer.convertJsonApiRelationshipsModelList(relationships as IJsonApiRelationships, included as IJsonApiEntityWithOptionalAttributes[]) - expect(actionList).toHaveProperty('genericDeviceActions') - expect(actionList.genericDeviceActions).toContainEqual(expectedAction1) - expect(actionList.genericDeviceActions).toContainEqual(expectedAction2) + expect(actionList).toHaveProperty('genericActions') + expect(actionList.genericActions).toContainEqual(expectedAction1) + expect(actionList.genericActions).toContainEqual(expectedAction2) }) }) describe('#convertJsonApiObjectListToModelList', () => { @@ -913,7 +913,7 @@ describe('GenericActionSerializer', () => { expect(apiRelationship).toEqual(expectedRelationship) }) }) - describe('#convertJsonApiIncludedGenericActionAttachmentsToIdList', () => { + describe('#convertJsonApiIncludedActionAttachmentsToIdList', () => { it('should return a list of generic_device_action_attachment ids / attachment ids mappings', () => { const expectedMappings = [ { @@ -923,7 +923,7 @@ describe('GenericActionSerializer', () => { ] const serializer = new GenericDeviceActionSerializer() const data = getExampleObjectResponseWithIncludedActionAttachments() - const mappings = serializer.convertJsonApiIncludedGenericActionAttachmentsToIdList(data.included as IJsonApiEntityWithOptionalAttributes[]) + const mappings = serializer.convertJsonApiIncludedActionAttachmentsToIdList(data.included as IJsonApiEntityWithOptionalAttributes[]) expect(mappings).toEqual(expectedMappings) }) diff --git a/test/serializers/jsonapi/SoftwareTypeSerializer.test.ts b/test/serializers/jsonapi/SoftwareTypeSerializer.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..88dbc2f7b5411bb996a88c03e8c0c5aea1add5df --- /dev/null +++ b/test/serializers/jsonapi/SoftwareTypeSerializer.test.ts @@ -0,0 +1,104 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2020, 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { SoftwareType } from '@/models/SoftwareType' +import { SoftwareTypeSerializer } from '@/serializers/jsonapi/SoftwareTypeSerializer' + +describe('SoftwareTypeSerializer', () => { + describe('#convertJsonApiObjectListToModelList', () => { + it('should convert a list of two elements to a model list', () => { + const jsonApiObjectList: any = { + data: [{ + attributes: { + term: 'Software', + definition: '', + provenance: null, + provenance_uri: null, + category: null, + note: null, + status: 'ACCEPTED' + }, + id: '1', + links: { + self: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/softwaretypes/1/' + }, + relationships: {}, + type: 'SoftwareType' + }, { + attributes: { + term: 'Firmware', + definition: '', + provenance: null, + provenance_uri: null, + category: null, + note: null, + status: 'ACCEPTED' + }, + id: '2', + links: { + self: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/softwaretypes/2/' + }, + relationships: {}, + type: 'SoftwareType' + }], + included: [], + jsonapi: { + version: '1.0' + }, + meta: { + count: 2 + } + } + + const expectedSoftwareType1 = SoftwareType.createFromObject({ + id: '1', + name: 'Software', + definition: '', + uri: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/softwaretypes/1/' + }) + const expectedSoftwareType2 = SoftwareType.createFromObject({ + id: '2', + name: 'Firmware', + definition: '', + uri: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/softwaretypes/2/' + }) + + const serializer = new SoftwareTypeSerializer() + + const softwaretypes = serializer.convertJsonApiObjectListToModelList(jsonApiObjectList) + + expect(Array.isArray(softwaretypes)).toBeTruthy() + expect(softwaretypes.length).toEqual(2) + expect(softwaretypes[0]).toEqual(expectedSoftwareType1) + expect(softwaretypes[1]).toEqual(expectedSoftwareType2) + }) + }) +}) diff --git a/test/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.test.ts b/test/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f70e0aecbc97fcc039a759641920e967d20bd1c --- /dev/null +++ b/test/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.test.ts @@ -0,0 +1,313 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2020, 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { Attachment } from '@/models/Attachment' + +import { + DeviceSoftwareUpdateActionAttachmentSerializer, + PlatformSoftwareUpdateActionAttachmentSerializer +} from '@/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer' + +import { + IJsonApiEntityEnvelope, + IJsonApiEntityWithOptionalId, + IJsonApiEntityWithOptionalAttributes, + IJsonApiRelationships +} from '@/serializers/jsonapi/JsonApiTypes' + +describe('SoftwareUpdateActionAttachmentSerializer', () => { + describe('DeviceSoftwareUpdateActionAttachmentSerializer', () => { + function getExampleObjectResponse (): IJsonApiEntityEnvelope { + return { + data: { + type: 'device_software_update_action', + relationships: { + device_software_update_action_attachments: { + links: { + related: '/rdm/svm-api/v1/device-software-update-actions/9/relationships/device-software-update-action-attachments' + }, + data: [ + { + type: 'device_software_update_action_attachment', + id: '6' + }, + { + type: 'device_software_update_action_attachment', + id: '7' + } + ] + }, + device: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/9/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + }, + contact: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/9/relationships/contact', + related: '/rdm/svm-api/v1/contacts/14' + }, + data: { + type: 'contact', + id: '14' + } + } + }, + attributes: { + software_type_uri: 'https://cv/firmware', + software_type_name: 'Firmware', + update_date: '2021-05-21T00:00:00', + description: 'dfdfdf', + version: '1.42', + repository_url: 'https://myrepo.de' + }, + id: '9', + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/9' + } + }, + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/9' + }, + included: [ + { + type: 'device_software_update_action_attachment', + relationships: { + attachment: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/6/relationships/attachment', + related: '/rdm/svm-api/v1/device-attachments/51' + }, + data: { + type: 'device_attachment', + id: '51' + } + }, + action: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/6/relationships/action', + related: '/rdm/svm-api/v1/device-software-update-actions/9' + } + } + }, + id: '6', + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/6' + } + }, + { + type: 'device_attachment', + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-attachments/51/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + } + }, + attributes: { + url: 'https://foo.de', + label: 'Foo.de' + }, + id: '51', + links: { + self: '/rdm/svm-api/v1/device-attachments/51' + } + }, + { + type: 'device_software_update_action_attachment', + relationships: { + attachment: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/7/relationships/attachment', + related: '/rdm/svm-api/v1/device-attachments/52' + }, + data: { + type: 'device_attachment', + id: '52' + } + }, + action: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/7/relationships/action', + related: '/rdm/svm-api/v1/device-software-update-actions/9' + } + } + }, + id: '7', + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/7' + } + }, + { + type: 'device_attachment', + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-attachments/52/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + } + }, + attributes: { + url: 'https://bar.baz', + label: 'Bar.baz' + }, + id: '52', + links: { + self: '/rdm/svm-api/v1/device-attachments/52' + } + } + ], + jsonapi: { + version: '1.0' + } + } + } + + describe('constructing and types', () => { + it('should return a correct action type name', () => { + const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer() + expect(serializer.getActionTypeName()).toEqual('device_software_update_action') + }) + it('should return a correct action attachment type name', () => { + const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer() + expect(serializer.getActionAttachmentTypeName()).toEqual('device_software_update_action_attachment') + }) + it('should return a the plural form of the action attachment type name', () => { + const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer() + expect(serializer.getActionAttachmentTypeNamePlural()).toEqual('device_software_update_action_attachments') + }) + it('should return a correct attachment type name', () => { + const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer() + expect(serializer.getAttachmentTypeName()).toEqual('device_attachment') + }) + it('should return an attachment serializer', () => { + const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer() + expect(typeof serializer.attachmentSerializer).toBe('object') + }) + }) + describe('#convertModelToJsonApiData', () => { + it('should return a JSON API object from an attachment and an action id', () => { + const attachment = Attachment.createFromObject({ + id: '1', + label: 'Foo', + url: 'https://bar.baz' + }) + const actionId = '2' + + const expectedApiModel: IJsonApiEntityWithOptionalId = { + type: 'device_software_update_action_attachment', + attributes: {}, + relationships: { + action: { + data: { + type: 'device_software_update_action', + id: '2' + } + }, + attachment: { + data: { + type: 'device_attachment', + id: '1' + } + } + } + } + + const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer() + const apiModel = serializer.convertModelToJsonApiData(attachment, actionId) + + expect(apiModel).toEqual(expectedApiModel) + }) + }) + describe('#convertJsonApiRelationshipsModelList', () => { + it('should return a serialized list of attachments from an list of included API entities', () => { + const attachment1 = Attachment.createFromObject({ + id: '51', + label: 'Foo.de', + url: 'https://foo.de' + }) + const attachment2 = Attachment.createFromObject({ + id: '52', + label: 'Bar.baz', + url: 'https://bar.baz' + }) + + const response = getExampleObjectResponse() + const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer() + + const attachmentList = serializer.convertJsonApiRelationshipsModelList(response.data.relationships as IJsonApiRelationships, response.included as IJsonApiEntityWithOptionalAttributes[]) + + expect(attachmentList).toHaveLength(2) + expect(attachmentList).toContainEqual(attachment1) + expect(attachmentList).toContainEqual(attachment2) + }) + }) + }) + describe('PlatformSoftwareUpdateActionAttachmentSerializer', () => { + describe('constructing and types', () => { + it('should return a correct action type name', () => { + const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer() + expect(serializer.getActionTypeName()).toEqual('platform_software_update_action') + }) + it('should return a correct action attachment type name', () => { + const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer() + expect(serializer.getActionAttachmentTypeName()).toEqual('platform_software_update_action_attachment') + }) + it('should return a the plural form of the action attachment type name', () => { + const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer() + expect(serializer.getActionAttachmentTypeNamePlural()).toEqual('platform_software_update_action_attachments') + }) + it('should return a correct attachment type name', () => { + const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer() + expect(serializer.getAttachmentTypeName()).toEqual('platform_attachment') + }) + it('should return an attachment serializer', () => { + const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer() + expect(typeof serializer.attachmentSerializer).toBe('object') + }) + }) + }) +}) diff --git a/test/serializers/jsonapi/SoftwareUpdateActionSerializer.test.ts b/test/serializers/jsonapi/SoftwareUpdateActionSerializer.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ac37023b669484762dba88b9bf4084c2fb7192f --- /dev/null +++ b/test/serializers/jsonapi/SoftwareUpdateActionSerializer.test.ts @@ -0,0 +1,1229 @@ +/** + * @license + * Web client of the Sensor Management System software developed within + * the Helmholtz DataHub Initiative by GFZ and UFZ. + * + * Copyright (C) 2020, 2021 + * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de) + * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de) + * - Helmholtz Centre Potsdam - GFZ German Research Centre for + * Geosciences (GFZ, https://www.gfz-potsdam.de) + * + * Parts of this program were developed within the context of the + * following publicly funded projects or measures: + * - Helmholtz Earth and Environment DataHub + * (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095) + * + * Licensed under the HEESIL, Version 1.0 or - as soon they will be + * approved by the "Community" - subsequent versions of the HEESIL + * (the "Licence"). + * + * You may not use this work except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://gitext.gfz-potsdam.de/software/heesil + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the Licence for the specific language governing + * permissions and limitations under the Licence. + */ +import { DateTime } from 'luxon' + +import { Attachment } from '@/models/Attachment' +import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction' +import { Contact } from '@/models/Contact' + +import { + DeviceSoftwareUpdateActionSerializer, + PlatformSoftwareUpdateActionSerializer +} from '@/serializers/jsonapi/SoftwareUpdateActionSerializer' + +import { + IJsonApiEntityEnvelope, + IJsonApiEntityListEnvelope, + IJsonApiEntityWithOptionalId, + IJsonApiEntityWithOptionalAttributes, + IJsonApiRelationships +} from '@/serializers/jsonapi/JsonApiTypes' + +describe('SoftwareUpdateActionSerializer', () => { + function getExampleObjectResponse (): IJsonApiEntityEnvelope { + return { + data: { + type: 'device_software_update_action', + attributes: { + software_type_name: 'Program', + version: 'fe23f4afc12f234sd', + software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/', + repository_url: 'https://foo/bar', + update_date: '2021-07-01T00:00:00', + description: 'Test', + created_at: '2021-06-14T14:47:53.554867', + updated_at: null + }, + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + }, + contact: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/contact', + related: '/rdm/svm-api/v1/contacts/14' + }, + data: { + type: 'contact', + id: '14' + } + }, + device_software_update_action_attachments: { + links: { + related: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/device-software-update-action-attachments' + }, + data: [ + + ] + } + }, + id: '3', + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/3' + } + }, + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/3' + }, + included: [ + { + type: 'contact', + attributes: { + family_name: 'Hanisch', + given_name: 'Marc', + website: '', + email: 'marc.hanisch@gfz-potsdam.de' + }, + relationships: { + devices: { + links: { + related: '/rdm/svm-api/v1/contacts/14/relationships/devices' + }, + data: [ + { + type: 'device', + id: '250' + } + ] + }, + configurations: { + links: { + related: '/rdm/svm-api/v1/contacts/14/relationships/configurations' + }, + data: [ + + ] + }, + platforms: { + links: { + related: '/rdm/svm-api/v1/contacts/14/relationships/platforms' + }, + data: [ + + ] + }, + user: { + links: { + self: '/rdm/svm-api/v1/contacts/14/relationships/user' + }, + data: { + type: 'user', + id: '6' + } + } + }, + id: '14', + links: { + self: '/rdm/svm-api/v1/contacts/14' + } + } + ], + jsonapi: { + version: '1.0' + } + } + } + + function getExampleObjectListResponse (): IJsonApiEntityListEnvelope { + return { + data: [ + { + type: 'device_software_update_action', + id: '2', + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + }, + contact: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/contact', + related: '/rdm/svm-api/v1/contacts/3' + }, + data: { + type: 'contact', + id: '3' + } + }, + device_software_update_action_attachments: { + links: { + related: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device-software-update-action-attachments' + }, + data: [ + { + type: 'device_software_update_action_attachment', + id: '1' + }, + { + type: 'device_software_update_action_attachment', + id: '2' + }, + { + type: 'device_software_update_action_attachment', + id: '3' + } + ] + } + }, + attributes: { + software_type_name: 'Firmware', + repository_url: 'https://foo/bar/baz', + description: 'Some simple description!!!', + update_date: '2021-06-30T00:00:00', + software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/', + version: '1.9', + created_at: '2021-06-14T12:18:06.531875' + }, + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2' + } + }, + { + type: 'device_software_update_action', + id: '1', + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/1/relationships/device', + related: '/rdm/svm-api/v1/devices/256' + }, + data: { + type: 'device', + id: '256' + } + }, + contact: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/1/relationships/contact', + related: '/rdm/svm-api/v1/contacts/14' + }, + data: { + type: 'contact', + id: '14' + } + }, + device_software_update_action_attachments: { + links: { + related: '/rdm/svm-api/v1/device-software-update-actions/1/relationships/device-software-update-action-attachments' + }, + data: [ + + ] + } + }, + attributes: { + software_type_name: 'Firmware', + repository_url: 'https://foo.bar', + description: 'Some description', + update_date: '2021-06-03T00:00:00', + software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/', + version: '1.3' + }, + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/1' + } + } + ], + links: { + self: 'http://rz-vm64.gfz-potsdam.de:5000/rdm/svm-api/v1/device-software-update-actions?include=contact%2Cdevice_software_update_action_attachments.attachment' + }, + included: [ + { + type: 'contact', + id: '3', + attributes: { + family_name: 'Brinckmann', + given_name: 'Nils', + email: 'nils.brinckmann@gfz-potsdam.de', + website: 'https://www.gfz-potsdam.de/staff/nils-brinckmann/' + }, + relationships: { + user: { + links: { + self: '/rdm/svm-api/v1/contacts/3/relationships/user' + }, + data: { + type: 'user', + id: '3' + } + } + }, + links: { + self: '/rdm/svm-api/v1/contacts/3' + } + }, + { + type: 'device_software_update_action_attachment', + relationships: { + action: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/1/relationships/action', + related: '/rdm/svm-api/v1/device-software-update-action-attachments/2' + }, + data: { + type: 'device_software_update_action', + id: '2' + } + }, + attachment: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/1/relationships/attachment', + related: '/rdm/svm-api/v1/device-attachments/53' + }, + data: { + type: 'device_attachment', + id: '53' + } + } + }, + id: '1', + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/1' + } + }, + { + type: 'device_attachment', + id: '53', + attributes: { + label: 'GFZ', + url: 'https://www.gfz-potsdam.de' + }, + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-attachments/53/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + } + }, + links: { + self: '/rdm/svm-api/v1/device-attachments/53' + } + }, + { + type: 'device_software_update_action_attachment', + relationships: { + action: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/2/relationships/action', + related: '/rdm/svm-api/v1/device-software-update-action-attachments/2' + }, + data: { + type: 'device_software_update_action', + id: '2' + } + }, + attachment: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/2/relationships/attachment', + related: '/rdm/svm-api/v1/device-attachments/52' + }, + data: { + type: 'device_attachment', + id: '52' + } + } + }, + id: '2', + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/2' + } + }, + { + type: 'device_attachment', + id: '52', + attributes: { + label: 'Bar.baz', + url: 'https://bar.baz' + }, + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-attachments/52/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + } + }, + links: { + self: '/rdm/svm-api/v1/device-attachments/52' + } + }, + { + type: 'device_software_update_action_attachment', + relationships: { + action: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/3/relationships/action', + related: '/rdm/svm-api/v1/device-software-update-action-attachments/2' + }, + data: { + type: 'device_software_update_action', + id: '2' + } + }, + attachment: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/3/relationships/attachment', + related: '/rdm/svm-api/v1/device-attachments/51' + }, + data: { + type: 'device_attachment', + id: '51' + } + } + }, + id: '3', + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/3' + } + }, + { + type: 'device_attachment', + id: '51', + attributes: { + label: 'Foo.de', + url: 'https://foo.de' + }, + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-attachments/51/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + } + }, + links: { + self: '/rdm/svm-api/v1/device-attachments/51' + } + }, + { + type: 'contact', + id: '14', + attributes: { + family_name: 'Hanisch', + given_name: 'Marc', + email: 'marc.hanisch@gfz-potsdam.de', + website: '' + }, + relationships: { + user: { + links: { + self: '/rdm/svm-api/v1/contacts/14/relationships/user' + }, + data: { + type: 'user', + id: '6' + } + } + }, + links: { + self: '/rdm/svm-api/v1/contacts/14' + } + } + ], + meta: { + count: 2 + }, + jsonapi: { + version: '1.0' + } + } + } + + function getExampleDeviceResponse (): IJsonApiEntityEnvelope { + return { + data: { + type: 'device', + attributes: { + persistent_identifier: null, + manufacturer_name: 'OTT Hydromet GmbH', + model: 'SM1', + updated_at: '2021-04-26T09:03:01.944689', + device_type_name: 'Frequency/Time Domain Reflectometer (FTDR)(Soil moisture and temperature)', + long_name: 'Adcon SM 1 soil moisture / temperature sensor', + short_name: 'Adcon SM1 soil moisture / temperature sensor FTDR Zeitlow 1', + website: 'http://www.adcon.com', + status_name: 'In Use', + manufacturer_uri: 'OTT Hydromet GmbH', + created_at: '2021-01-18T07:07:24.360000', + serial_number: '', + device_type_uri: '', + dual_use: false, + description: '', + status_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/equipmentstatus/2/', + inventory_number: '' + }, + relationships: { + device_properties: { + links: { + related: '/rdm/svm-api/v1/devices/204/relationships/device-properties' + }, + data: [ + { + type: 'device_property', + id: '150' + } + ] + }, + contacts: { + links: { + related: '/rdm/svm-api/v1/devices/204/relationships/contacts' + }, + data: [ + { + type: 'contact', + id: '10' + }, + { + type: 'contact', + id: '21' + } + ] + }, + device_software_update_actions: { + links: { + related: '/rdm/svm-api/v1/devices/204/relationships/device-software-update-actions' + }, + data: [ + { + type: 'device_software_update_action', + id: '2' + }, + { + type: 'device_software_update_action', + id: '3' + } + ] + }, + device_attachments: { + links: { + related: '/rdm/svm-api/v1/devices/204/relationships/device-attachments' + }, + data: [ + { + type: 'device_attachment', + id: '51' + }, + { + type: 'device_attachment', + id: '52' + }, + { + type: 'device_attachment', + id: '53' + } + ] + } + }, + id: '204', + links: { + self: '/rdm/svm-api/v1/devices/204' + } + }, + links: { + self: '/rdm/svm-api/v1/devices/204' + }, + included: [ + { + type: 'device_software_update_action', + id: '2', + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + }, + contact: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/contact', + related: '/rdm/svm-api/v1/contacts/3' + }, + data: { + type: 'contact', + id: '3' + } + }, + device_software_update_action_attachments: { + links: { + related: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device-software-update-action-attachments' + }, + data: [ + { + type: 'device_software_update_action_attachment', + id: '1' + }, + { + type: 'device_software_update_action_attachment', + id: '2' + }, + { + type: 'device_software_update_action_attachment', + id: '3' + } + ] + } + }, + attributes: { + software_type_name: 'Firmware', + repository_url: 'https://foo/bar/baz', + description: 'Some simple description!!!', + updated_at: '2021-06-14T14:37:42.105091', + update_date: '2021-06-30T00:00:00', + software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/', + version: '1.9', + created_at: '2021-06-14T12:18:06.531875' + }, + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2' + } + }, + { + type: 'device_software_update_action', + id: '3', + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + }, + contact: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/contact', + related: '/rdm/svm-api/v1/contacts/14' + }, + data: { + type: 'contact', + id: '14' + } + }, + device_software_update_action_attachments: { + links: { + related: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/device-software-update-action-attachments' + }, + data: [ + + ] + } + }, + attributes: { + software_type_name: 'Program', + repository_url: '', + description: 'Test', + updated_at: null, + update_date: '2021-07-01T00:00:00', + software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/', + version: 'fe23f4afc12f234sd', + created_at: '2021-06-14T14:47:53.554867' + }, + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/3' + } + } + ], + jsonapi: { + version: '1.0' + } + } + } + + function getExampleObjectResponseWithIncludedActionAttachments (): IJsonApiEntityEnvelope { + return { + data: { + type: 'device_software_update_action', + id: '2', + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + }, + contact: { + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/contact', + related: '/rdm/svm-api/v1/contacts/3' + }, + data: { + type: 'contact', + id: '3' + } + }, + device_software_update_action_attachments: { + links: { + related: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device-software-update-action-attachments' + }, + data: [ + { + type: 'device_software_update_action_attachment', + id: '1' + }, + { + type: 'device_software_update_action_attachment', + id: '2' + }, + { + type: 'device_software_update_action_attachment', + id: '3' + } + ] + } + }, + attributes: { + software_type_name: 'Firmware', + repository_url: 'https://foo/bar/baz', + description: 'Some simple description!!!', + updated_at: '2021-06-14T14:37:42.105091', + update_date: '2021-06-30T00:00:00', + software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/', + version: '1.9', + created_at: '2021-06-14T12:18:06.531875' + }, + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2' + } + }, + links: { + self: '/rdm/svm-api/v1/device-software-update-actions/2' + }, + included: [ + { + type: 'contact', + id: '3', + attributes: { + family_name: 'Brinckmann', + given_name: 'Nils', + email: 'nils.brinckmann@gfz-potsdam.de', + website: 'https://www.gfz-potsdam.de/staff/nils-brinckmann/' + }, + relationships: { + user: { + links: { + self: '/rdm/svm-api/v1/contacts/3/relationships/user' + }, + data: { + type: 'user', + id: '3' + } + } + }, + links: { + self: '/rdm/svm-api/v1/contacts/3' + } + }, + { + type: 'device_software_update_action_attachment', + relationships: { + action: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/1/relationships/action', + related: '/rdm/svm-api/v1/device-software-update-action-attachments/2' + }, + data: { + type: 'device_software_update_action', + id: '2' + } + }, + attachment: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/1/relationships/attachment', + related: '/rdm/svm-api/v1/device-attachments/53' + }, + data: { + type: 'device_attachment', + id: '53' + } + } + }, + id: '1', + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/1' + } + }, + { + type: 'device_attachment', + id: '53', + attributes: { + label: 'GFZ', + url: 'https://www.gfz-potsdam.de' + }, + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-attachments/53/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + } + }, + links: { + self: '/rdm/svm-api/v1/device-attachments/53' + } + }, + { + type: 'device_software_update_action_attachment', + relationships: { + action: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/2/relationships/action', + related: '/rdm/svm-api/v1/device-software-update-action-attachments/2' + }, + data: { + type: 'device_software_update_action', + id: '2' + } + }, + attachment: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/2/relationships/attachment', + related: '/rdm/svm-api/v1/device-attachments/52' + }, + data: { + type: 'device_attachment', + id: '52' + } + } + }, + id: '2', + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/2' + } + }, + { + type: 'device_attachment', + id: '52', + attributes: { + label: 'Bar.baz', + url: 'https://bar.baz' + }, + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-attachments/52/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + } + }, + links: { + self: '/rdm/svm-api/v1/device-attachments/52' + } + }, + { + type: 'device_software_update_action_attachment', + relationships: { + action: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/3/relationships/action', + related: '/rdm/svm-api/v1/device-software-update-action-attachments/2' + }, + data: { + type: 'device_software_update_action', + id: '2' + } + }, + attachment: { + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/3/relationships/attachment', + related: '/rdm/svm-api/v1/device-attachments/51' + }, + data: { + type: 'device_attachment', + id: '51' + } + } + }, + id: '3', + links: { + self: '/rdm/svm-api/v1/device-software-update-action-attachments/3' + } + }, + { + type: 'device_attachment', + id: '51', + attributes: { + label: 'Foo.de', + url: 'https://foo.de' + }, + relationships: { + device: { + links: { + self: '/rdm/svm-api/v1/device-attachments/51/relationships/device', + related: '/rdm/svm-api/v1/devices/204' + }, + data: { + type: 'device', + id: '204' + } + } + }, + links: { + self: '/rdm/svm-api/v1/device-attachments/51' + } + } + ], + jsonapi: { + version: '1.0' + } + } + } + + describe('DeviceSoftwareUpdateActionSerializer', () => { + describe('constructing and types', () => { + it('should return \'device\' as its type', () => { + const serializer = new DeviceSoftwareUpdateActionSerializer() + expect(serializer.targetType).toEqual('device') + }) + it('should return a correct action type name', () => { + const serializer = new DeviceSoftwareUpdateActionSerializer() + expect(serializer.getActionTypeName()).toEqual('device_software_update_action') + }) + it('should return a the plural form of the action type name', () => { + const serializer = new DeviceSoftwareUpdateActionSerializer() + expect(serializer.getActionTypeNamePlural()).toEqual('device_software_update_actions') + }) + it('should return a correction action attachment type name', () => { + const serializer = new DeviceSoftwareUpdateActionSerializer() + expect(serializer.getActionAttachmentTypeName()).toEqual('device_software_update_action_attachment') + }) + }) + describe('#convertJsonApiObjectToModel', () => { + it('should return a serialized software update action from an API response', () => { + const contact = Contact.createFromObject({ + id: '14', + givenName: 'Marc', + familyName: 'Hanisch', + email: 'marc.hanisch@gfz-potsdam.de', + website: '' + }) + const expectedAction = new SoftwareUpdateAction() + expectedAction.id = '3' + expectedAction.description = 'Test' + expectedAction.softwareTypeName = 'Program' + expectedAction.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/' + expectedAction.updateDate = DateTime.fromISO('2021-07-01T00:00:00', { zone: 'UTC' }) + expectedAction.contact = contact + expectedAction.version = 'fe23f4afc12f234sd' + expectedAction.repositoryUrl = 'https://foo/bar' + + const serializer = new DeviceSoftwareUpdateActionSerializer() + const action = serializer.convertJsonApiObjectToModel(getExampleObjectResponse()) + + expect(action).toEqual(expectedAction) + }) + }) + describe('#convertJsonApiDataToModel', () => { + it('should return a serialized software update action from an API response object', () => { + const contact = Contact.createFromObject({ + id: '14', + givenName: 'Marc', + familyName: 'Hanisch', + email: 'marc.hanisch@gfz-potsdam.de', + website: '' + }) + const expectedAction = new SoftwareUpdateAction() + expectedAction.id = '3' + expectedAction.description = 'Test' + expectedAction.softwareTypeName = 'Program' + expectedAction.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/' + expectedAction.updateDate = DateTime.fromISO('2021-07-01T00:00:00', { zone: 'UTC' }) + expectedAction.contact = contact + expectedAction.version = 'fe23f4afc12f234sd' + expectedAction.repositoryUrl = 'https://foo/bar' + + const serializer = new DeviceSoftwareUpdateActionSerializer() + const data = getExampleObjectResponse().data + const included = getExampleObjectResponse().included + const action = serializer.convertJsonApiDataToModel(data, included as IJsonApiEntityWithOptionalAttributes[]) + + expect(action).toEqual(expectedAction) + }) + }) + describe('#convertJsonApiRelationshipsModelList', () => { + it('should return a serialized list of software update actions from an list of included API entities', () => { + const serializer = new DeviceSoftwareUpdateActionSerializer() + const response = getExampleDeviceResponse() + + const relationships = response.data.relationships + const included = response.included + + const expectedAction1 = new SoftwareUpdateAction() + expectedAction1.id = '2' + expectedAction1.description = 'Some simple description!!!' + expectedAction1.softwareTypeName = 'Firmware' + expectedAction1.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/' + expectedAction1.updateDate = DateTime.fromISO('2021-06-30T00:00:00', { zone: 'UTC' }) + expectedAction1.version = '1.9' + expectedAction1.repositoryUrl = 'https://foo/bar/baz' + + const expectedAction2 = new SoftwareUpdateAction() + expectedAction2.id = '3' + expectedAction2.description = 'Test' + expectedAction2.softwareTypeName = 'Program' + expectedAction2.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/' + expectedAction2.updateDate = DateTime.fromISO('2021-07-01T00:00:00', { zone: 'UTC' }) + expectedAction2.version = 'fe23f4afc12f234sd' + expectedAction2.repositoryUrl = '' + + const actionList = serializer.convertJsonApiRelationshipsModelList(relationships as IJsonApiRelationships, included as IJsonApiEntityWithOptionalAttributes[]) + + expect(actionList).toHaveProperty('softwareUpdateActions') + expect(actionList.softwareUpdateActions).toContainEqual(expectedAction1) + expect(actionList.softwareUpdateActions).toContainEqual(expectedAction2) + }) + }) + describe('#convertJsonApiObjectListToModelList', () => { + it('should return a list of serialized software update actions from an API response', () => { + const contact1 = Contact.createFromObject({ + id: '14', + givenName: 'Marc', + familyName: 'Hanisch', + email: 'marc.hanisch@gfz-potsdam.de', + website: '' + }) + const contact2 = Contact.createFromObject({ + id: '3', + givenName: 'Nils', + familyName: 'Brinckmann', + email: 'nils.brinckmann@gfz-potsdam.de', + website: 'https://www.gfz-potsdam.de/staff/nils-brinckmann/' + }) + const attachment1 = Attachment.createFromObject({ + id: '51', + label: 'Foo.de', + url: 'https://foo.de' + }) + const attachment2 = Attachment.createFromObject({ + id: '52', + label: 'Bar.baz', + url: 'https://bar.baz' + }) + const attachment3 = Attachment.createFromObject({ + id: '53', + label: 'GFZ', + url: 'https://www.gfz-potsdam.de' + }) + + const expectedAction1 = new SoftwareUpdateAction() + expectedAction1.id = '2' + expectedAction1.description = 'Some simple description!!!' + expectedAction1.softwareTypeName = 'Firmware' + expectedAction1.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/' + expectedAction1.updateDate = DateTime.fromISO('2021-06-30T00:00:00', { zone: 'UTC' }) + expectedAction1.version = '1.9' + expectedAction1.repositoryUrl = 'https://foo/bar/baz' + expectedAction1.contact = contact2 + expectedAction1.attachments = [ + attachment3, + attachment2, + attachment1 + ] + + const expectedAction2 = new SoftwareUpdateAction() + expectedAction2.id = '1' + expectedAction2.description = 'Some description' + expectedAction2.softwareTypeName = 'Firmware' + expectedAction2.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/' + expectedAction2.updateDate = DateTime.fromISO('2021-06-03T00:00:00', { zone: 'UTC' }) + expectedAction2.version = '1.3' + expectedAction2.repositoryUrl = 'https://foo.bar' + expectedAction2.contact = contact1 + + const serializer = new DeviceSoftwareUpdateActionSerializer() + const actionList = serializer.convertJsonApiObjectListToModelList(getExampleObjectListResponse()) + + expect(actionList).toContainEqual(expectedAction1) + expect(actionList).toContainEqual(expectedAction2) + }) + }) + describe('#convertModelToJsonApiData', () => { + it('should return a JSON API representation from a software update action model', () => { + const contact = Contact.createFromObject({ + id: '14', + givenName: 'Marc', + familyName: 'Hanisch', + email: 'marc.hanisch@gfz-potsdam.de', + website: '' + }) + + const action = new SoftwareUpdateAction() + action.id = '7' + action.description = 'Bla' + action.softwareTypeName = 'Firmware' + action.softwareTypeUrl = 'https://foo/bar' + action.updateDate = DateTime.fromISO('2021-05-23T00:00:00', { zone: 'UTC' }) + action.version = '10.2' + action.repositoryUrl = 'https://git.gfz-potsdam.de/sms/frontend' + action.contact = contact + + const expectedApiModel: IJsonApiEntityWithOptionalId = { + type: 'device_software_update_action', + id: '7', + attributes: { + description: 'Bla', + software_type_name: 'Firmware', + software_type_uri: 'https://foo/bar', + update_date: '2021-05-23T00:00:00.000Z', + version: '10.2', + repository_url: 'https://git.gfz-potsdam.de/sms/frontend' + }, + relationships: { + device: { + data: { + type: 'device', + id: '204' + } + }, + contact: { + data: { + type: 'contact', + id: '14' + } + } + } + } + + const serializer = new DeviceSoftwareUpdateActionSerializer() + const apiModel = serializer.convertModelToJsonApiData(action, '204') + + expect(apiModel).toEqual(expectedApiModel) + }) + }) + describe('#convertModelToJsonApiRelationshipObject', () => { + it('should return a JSON API relationships object from a software update action model', () => { + const action = new SoftwareUpdateAction() + action.id = '7' + + const expectedRelationship: IJsonApiRelationships = { + device_software_update_action: { + data: { + id: '7', + type: 'device_software_update_action' + } + } + } + const serializer = new DeviceSoftwareUpdateActionSerializer() + const apiRelationship = serializer.convertModelToJsonApiRelationshipObject(action) + + expect(apiRelationship).toEqual(expectedRelationship) + }) + }) + describe('#convertJsonApiIncludedActionAttachmentsToIdList', () => { + it('should return a list of device_software_update_action_attachment ids / attachment ids mappings', () => { + const expectedMappings = [ + { + softwareUpdateActionAttachmentId: '1', + attachmentId: '53' + }, + { + softwareUpdateActionAttachmentId: '2', + attachmentId: '52' + }, + { + softwareUpdateActionAttachmentId: '3', + attachmentId: '51' + } + ] + const serializer = new DeviceSoftwareUpdateActionSerializer() + const data = getExampleObjectResponseWithIncludedActionAttachments() + const mappings = serializer.convertJsonApiIncludedActionAttachmentsToIdList(data.included as IJsonApiEntityWithOptionalAttributes[]) + + expect(mappings).toEqual(expectedMappings) + }) + }) + }) + + describe('PlatformSoftwareUpdateActionSerializer', () => { + describe('constructing and types', () => { + it('should return \'platform\' as its type', () => { + const serializer = new PlatformSoftwareUpdateActionSerializer() + expect(serializer.targetType).toEqual('platform') + }) + it('should return a correct action type name', () => { + const serializer = new PlatformSoftwareUpdateActionSerializer() + expect(serializer.getActionTypeName()).toEqual('platform_software_update_action') + }) + it('should return a the plural form of the action type name', () => { + const serializer = new PlatformSoftwareUpdateActionSerializer() + expect(serializer.getActionTypeNamePlural()).toEqual('platform_software_update_actions') + }) + it('should return a correction action attachment type name', () => { + const serializer = new PlatformSoftwareUpdateActionSerializer() + expect(serializer.getActionAttachmentTypeName()).toEqual('platform_software_update_action_attachment') + }) + }) + }) +}) diff --git a/test/utils/urlHelper.test.ts b/test/utils/urlHelper.test.ts index 1cd86c3660de45cef6ea6f3687a9bedc16b8cd72..d793d5f1d86f6d6182f1b1d683b4bf381c43d3b0 100644 --- a/test/utils/urlHelper.test.ts +++ b/test/utils/urlHelper.test.ts @@ -29,7 +29,7 @@ * implied. See the Licence for the specific language governing * permissions and limitations under the Licence. */ -import { removeBaseUrl, removeFirstSlash, removeTrailingSlash, toRouterPath } from '@/utils/urlHelpers' +import { protocolsInUrl, removeBaseUrl, removeFirstSlash, removeTrailingSlash, toRouterPath } from '@/utils/urlHelpers' describe('removeBaseUrl', () => { it('should return the url if the base url is undefined', () => { @@ -109,3 +109,17 @@ describe('toRouterPath', () => { expect(result).toEqual('/logout-callback') }) }) +describe('protocolsInUrl', () => { + it('should return true with the correct protocols', () => { + const allowedProtocols = ['http', 'https'] + const url = 'http://www.heise.de' + const urlSecure = 'https://www.heise.de' + expect(protocolsInUrl(allowedProtocols, url)).toBeTruthy() + expect(protocolsInUrl(allowedProtocols, urlSecure)).toBeTruthy() + }) + it('should return false with a protocol that is not supported', () => { + const allowedProtocols = ['http', 'https'] + const url = 'ftp://www.heise.de' + expect(protocolsInUrl(allowedProtocols, url)).toBeFalsy() + }) +}) diff --git a/utils/urlHelpers.ts b/utils/urlHelpers.ts index ba937c2dbc093d203d544a16ee5305c704dbaee9..e97435b1a80cadd42bffcd67a4d99fe3eb559804 100644 --- a/utils/urlHelpers.ts +++ b/utils/urlHelpers.ts @@ -68,3 +68,21 @@ export function toRouterPath (callbackUri: string, routeBase = '/') { } return null } + +/** + * checks whether the url contains the given protocols + * + * to be honest, it just checks whether the string starts with http(s):// or similar + * + * @param {string[]} allowedProtocols - the protocols to check + * @param {string} url - the url to check + * @returns {boolean | string} true when protocols are in the url, otherwise false + */ +export function protocolsInUrl (allowedProtocols: string[], url: string) { + const protocols = allowedProtocols.join('|') + const urlRegExp = new RegExp('^(' + protocols + ')://.+$', 'i') + if (url && !url.match(urlRegExp)) { + return false + } + return true +}