diff --git a/.github/workflows/test-build-push.yml b/.github/workflows/test-build-push.yml index 738967a3..69fe016e 100644 --- a/.github/workflows/test-build-push.yml +++ b/.github/workflows/test-build-push.yml @@ -2,6 +2,9 @@ name: Test, Build and Publish on: push: + branches: [ "main", "develop" ] + tags: [ "v*.*.*" ] + pull_request: workflow_dispatch: inputs: dockerTag: @@ -80,7 +83,7 @@ jobs: build-and-push-image: name: Build and Push Docker Image runs-on: ubuntu-latest - if: contains(fromJSON('["main", "develop", "redlink"]'), github.ref_name) || github.event.inputs.dockerTag != '' + if: contains(fromJSON('["main", "develop"]'), github.ref_name) || startsWith(github.ref, 'refs/tags/v') || github.event.inputs.dockerTag != '' needs: - test permissions: @@ -108,6 +111,10 @@ jobs: type=raw,value=1.0.${{ github.run_number }}.{{sha}} # tag with branch-name type=ref,event=branch,enable=${{ github.event.inputs.dockerTag == '' }} + # tags from git tag (e.g. v1.0.0 -> 1.0.0, 1.0, 1) + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} # latest-tag on the default-branch (main) type=raw,value=latest,enable={{is_default_branch}} # a manually provided tag diff --git a/.gitignore b/.gitignore index 2f60fe36..46170fa3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ more-configuration-api-client-ts* ConfiguratorAPI.yaml src/generated-sources tests/coverage +.env diff --git a/Dockerfile b/Dockerfile index 35f5bb70..608eab01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,10 @@ RUN npm ci COPY . . ARG VITE_GIT_BRANCH ARG VITE_GIT_REVISION +ARG BACKEND_URL=https://studymanager.platform-test.more.redlink.io/ +ARG KEYCLOAK_URL=https://auth.more.redlink.io +ARG KEYCLOAK_REALM=Auth-Client-Test +ARG KEYCLOAK_CLIENTID=oauth2-pkce-client RUN npm run package:quick # production stage @@ -15,6 +19,13 @@ COPY --from=build-stage /app/dist /usr/share/nginx/html COPY docker/nginx/*.conf.template /etc/nginx/templates/ EXPOSE 80 -ENV MORE_BACKEND_URL=https://studymanager.more.redlink.io/ +ARG BACKEND_URL=https://studymanager.platform-test.more.redlink.io/ +ENV BACKEND_URL=$BACKEND_URL +ARG KEYCLOAK_URL=https://auth.more.redlink.io +ENV KEYCLOAK_URL=$KEYCLOAK_URL +ARG KEYCLOAK_REALM=Auth-Client-Test +ENV KEYCLOAK_REALM=$KEYCLOAK_REALM +ARG KEYCLOAK_CLIENTID=oauth2-pkce-client +ENV KEYCLOAK_CLIENTID=$KEYCLOAK_CLIENTID CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index b6953ec3..5d633f56 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,27 @@ # MORE / Front-End + The Management & Monitoring Backend (MMB) is the front-end to manage and monitor the back-end. The [frontend`s architecture](docs/adr) is based upon an Architectural Decision Records (ADR). Mainly, Vue 3, vite, TS, tailwindCSS and PrimeVue are used. To have a consistent code style and quality, we use Eslint in combination with prettier. # Setup + ``` nvm use # Optional, let nvm use the needed version npm i # install all dependencies npm run generate:api # generate needed sourced from BE, for generation as in the pipeline, java 17 must be set in your path npm run dev # start local development server ``` -See [nvm](https://github.com/nvm-sh/nvm) for more. The local front-end can be visited on http://localhost:3000, the [gateway](https://github.com/MORE-Platform/more-data-gateway), [back-end](https://github.com/MORE-Platform/more-studymanager-backend) with their dependencies must also run locally. See `vite.config.ts` for the required port for localhost. + +See [nvm](https://github.com/nvm-sh/nvm) for more. The local front-end can be visited on http://localhost:3000, +the [gateway](https://github.com/MORE-Platform/more-data-gateway), [back-end](https://github.com/MORE-Platform/more-studymanager-backend) +with their dependencies must also run locally. See `vite.config.ts` for the required port for localhost. ## Intellij, Webstorm ESLint configuration -Set the EsLint settings for Webstorm in Preferences --> Languages & Frameworks --> Javascript --> Code Quality Tools --> ESLint + +Set the EsLint settings for Webstorm in Preferences --> Languages & Frameworks --> Javascript --> Code Quality Tools --> +ESLint - Automatic ESLint config check - File extensions to check: `{**/*,*}.{js,ts,html,vue,json}` @@ -23,6 +30,7 @@ Set the EsLint settings for Webstorm in Preferences --> Languages & Frameworks - **Currently not working because of Intellij IDEs..., use `npm run lint:fix` for now** # Scripts + - `npm i`: install dependencies / node modules - `npm run`: shows all possible `npm` run commands including generation, development, linting, testing, and building @@ -53,11 +61,11 @@ First-party plugins needed for Tailwind UI: - [tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin) - [tailwindcss/aspect-ratio](https://github.com/tailwindlabs/tailwindcss-aspect-ratio) - -The template uses Vue 3 ` + + diff --git a/src/components/ObservationList.vue b/src/components/ObservationList.vue index cec67210..056b54b9 100644 --- a/src/components/ObservationList.vue +++ b/src/components/ObservationList.vue @@ -4,12 +4,13 @@ Prevention -- A research institute of the Ludwig Boltzmann Gesellschaft, Oesterreichische Vereinigung zur Foerderung der wissenschaftlichen Forschung). Licensed under the Elastic License 2.0. */ @@ -481,14 +546,14 @@ Licensed under the Elastic License 2.0. */ > @@ -497,7 +562,7 @@ Licensed under the Elastic License 2.0. */ - diff --git a/src/components/StudyApplicationManager.vue b/src/components/StudyApplicationManager.vue new file mode 100644 index 00000000..f53f2d65 --- /dev/null +++ b/src/components/StudyApplicationManager.vue @@ -0,0 +1,80 @@ +/* Copyright LBI-DHP and/or licensed to LBI-DHP under one or more contributor +license agreements (LBI-DHP: Ludwig Boltzmann Institute for Digital Health and +Prevention -- A research institute of the Ludwig Boltzmann Gesellschaft, +Oesterreichische Vereinigung zur Foerderung der wissenschaftlichen Forschung). +Licensed under the Elastic License 2.0. */ + + + diff --git a/src/components/StudyCollaboratorList.vue b/src/components/StudyCollaboratorList.vue index 8e5a0253..38101c62 100644 --- a/src/components/StudyCollaboratorList.vue +++ b/src/components/StudyCollaboratorList.vue @@ -26,7 +26,7 @@ Licensed under the Elastic License 2.0. */ import { useErrorHandling } from '../composable/useErrorHandling'; import DeleteMoreTableRowDialog from './dialog/DeleteMoreTableRowDialog.vue'; import { useUserStore } from '../stores/userStore'; - import Dropdown from 'primevue/dropdown'; + import DropdownPanelWithSearch from '@/components/shared/DropdownPanelWithSearch.vue'; const dialog = useDialog(); const { t } = useI18n(); @@ -269,7 +269,7 @@ Licensed under the Elastic License 2.0. */ data: { collaborator: { name: options.label, - institution: options.institution, + institution: options.description, uid: options.value, } as MoreTableCollaboratorItem, placeholder: t('global.placeholder.chooseRole'), @@ -311,7 +311,7 @@ Licensed under the Elastic License 2.0. */ ({ label: u.name, value: u.uid, - institution: u.institution, + description: u.institution, }) as MoreTableCollaboratorItemOption, ); }); @@ -342,47 +342,15 @@ Licensed under the Elastic License 2.0. */ > diff --git a/src/components/StudyGroupList.vue b/src/components/StudyGroupList.vue index b2b46d40..0c54a061 100644 --- a/src/components/StudyGroupList.vue +++ b/src/components/StudyGroupList.vue @@ -8,15 +8,16 @@ Licensed under the Elastic License 2.0. */ import { MoreStudyGroupTableMap, MoreTableAction, - MoreTableColumn, MoreTableChoice, + MoreTableColumn, MoreTableFieldType, - MoreTableRowActionResult, - } from '../models/MoreTableModel'; - import { UnitEnum, StudyGroup, StudyRole, StudyStatus } from '@gs'; + MoreTableRowActionResult + } from '@/models/MoreTableModel'; + import { StudyGroup, StudyRole, StudyStatus } from '@gs'; + import { DurationUnitEnum } from '@gs/models/duration'; import MoreTable from './shared/MoreTable.vue'; import ConfirmDialog from 'primevue/confirmdialog'; - import { useStudyGroupStore } from '../stores/studyGroupStore'; + import { useStudyGroupStore } from '@/stores/studyGroupStore'; import { useI18n } from 'vue-i18n'; import { useDialog } from 'primevue/usedialog'; import DeleteMoreTableRowDialog from './dialog/DeleteMoreTableRowDialog.vue'; @@ -48,15 +49,15 @@ Licensed under the Elastic License 2.0. */ }, { label: t('scheduler.preview.unit.MINUTE'), - value: UnitEnum.Minute, + value: DurationUnitEnum.Minute, }, { label: t('scheduler.preview.unit.HOUR'), - value: UnitEnum.Hour, + value: DurationUnitEnum.Hour, }, { label: t('scheduler.preview.unit.DAY'), - value: UnitEnum.Day, + value: DurationUnitEnum.Day, }, ]; diff --git a/src/components/StudyList.vue b/src/components/StudyList.vue index 50f7c266..9c8b4836 100644 --- a/src/components/StudyList.vue +++ b/src/components/StudyList.vue @@ -23,12 +23,11 @@ Licensed under the Elastic License 2.0. */ import { useStudyStore } from '../stores/studyStore'; import { useI18n } from 'vue-i18n'; import DeleteStudyDialog from './dialog/DeleteStudyDialog.vue'; - import { reactive } from 'vue'; - import AlertMsg from './shared/AlertMsg.vue'; import FileUpload, { FileUploadUploaderEvent } from 'primevue/fileupload'; import Button from 'primevue/button'; import { DownloadData } from '../models/DataDownloadModel'; import { useToastService } from '../composable/toastService'; + import { useToast } from 'primevue/usetoast'; const studyStore = useStudyStore(); const router = useRouter(); @@ -36,27 +35,13 @@ Licensed under the Elastic License 2.0. */ const loader = useLoader(); const { t } = useI18n(); const { showErrorToast } = useToastService(); + const toast = useToast(); const sortOptions: MoreTableSortOptions = { sortField: 'studyId', sortOrder: -1, }; - const alert = reactive({ - message: '', - showMessage: false, - }); - - function setAlertMessage(message: string): void { - alert.message = message; - alert.showMessage = true; - } - - function clearAlertMessage(): void { - alert.message = ''; - alert.showMessage = false; - } - const studyColumns: MoreTableColumn[] = [ { field: 'studyId', header: t('study.props.studyId'), sortable: true }, { @@ -256,8 +241,14 @@ Licensed under the Elastic License 2.0. */ ): void { if (studyId) { const studyUrl = `${location.host}/studies/${studyId}`; - navigator.clipboard.writeText(studyUrl); - setAlertMessage(t('study.dialog.msg.urlCopied', { studyId, title })); + navigator.clipboard.writeText(studyUrl).then(() => { + toast.add({ + severity: 'success', + summary: t('global.labels.success'), + detail: t('study.dialog.msg.urlCopied', { studyId, title }), + life: 2000, + }); + }); } } @@ -339,13 +330,5 @@ Licensed under the Elastic License 2.0. */ - diff --git a/src/components/TimelineList.vue b/src/components/TimelineList.vue index 6b1de3b8..cced58ec 100644 --- a/src/components/TimelineList.vue +++ b/src/components/TimelineList.vue @@ -11,13 +11,15 @@ Licensed under the Elastic License 2.0. */ import Calendar from 'primevue/calendar'; import DynamicDialog from 'primevue/dynamicdialog'; import { useDialog } from 'primevue/usedialog'; - import Dropdown, { DropdownChangeEvent } from 'primevue/dropdown'; import MultiSelect from 'primevue/multiselect'; import Button from 'primevue/button'; + import Dropdown from 'primevue/dropdown'; + import { DropdownChangeEvent } from 'primevue/dropdown'; import { ComponentFactory, InterventionTimelineEvent, ListComponentsComponentTypeEnum, + ObservationGroup, ObservationTimelineEvent, Participant, StudyGroup, @@ -48,13 +50,19 @@ Licensed under the Elastic License 2.0. */ const props = defineProps({ studyId: { type: Number, required: true }, studyGroups: { type: Array as PropType>, required: true }, + observationGroups: { + type: Array as PropType>, + required: true, + }, }); const vueCalLocale = locale.value.split('-')[0]; const filterRelativeStartDate = ref(); const filterStudyGroup = ref(); + const filterObservationGroup = ref(); const filterParticipant = ref(); const filterObservationAndIntervention = ref(); + const showFullTimeline = ref(false); // We differ between EventDetail (with more data for the dialog) and Event (prop for the VueCal component) const timelineEventsList: Ref = ref([]); const eventToEventDetailMapper: Record = {}; @@ -97,6 +105,23 @@ Licensed under the Elastic License 2.0. */ ), ); + const observationGroupOptions: Ref = ref([ + { + label: t('global.placeholder.entireStudy'), + value: undefined, + } as DropdownOption, + ]); + + observationGroupOptions.value.push( + ...props.observationGroups.map( + (observationGroup) => + ({ + label: observationGroup.title, + value: observationGroup.observationGroupId?.toString(), + }) as DropdownOption, + ), + ); + const events: ComputedRef = computed(() => { return timelineEventsList.value.filter((event: Event) => { const eventDetail = getEventDetailByEventCid(event.cId); @@ -275,37 +300,41 @@ Licensed under the Elastic License 2.0. */ return translation; } - function onStudyGroupFilterChange(e: DropdownChangeEvent): void { - const filteredOptions: DropdownOption[] = []; - const filteredParticipants: Participant[] = []; + type ParticipantGroupField = 'studyGroupId' | 'observationGroupId'; - filteredOptions.push({ - label: t('participants.placeholder.allParticipants'), - value: undefined, - } as DropdownOption); + function rebuildParticipantOptionsByGroup( + field: ParticipantGroupField, + selectedId?: string | undefined, // kommt aus Dropdown als string + ): void { + const id = selectedId ? parseInt(selectedId) : undefined; - if (e.value) { - filteredParticipants.push( - ...participantsList.filter( - (participant) => participant.studyGroupId === parseInt(e.value), - ), - ); - } else { - filteredParticipants.push(...participantsList); - } + const filteredParticipants = id + ? participantsList.filter((p) => (p as any)[field] === id) + : participantsList; - filteredOptions.push( + participantOptions.value = [ + { + label: t('participants.placeholder.allParticipants'), + value: undefined, + } as DropdownOption, ...filteredParticipants.map( - (filteredParticipant) => + (p) => ({ - label: filteredParticipant.alias, - value: filteredParticipant.participantId?.toString(), + label: p.alias, + value: p.participantId?.toString(), }) as DropdownOption, ), - ); + ]; - participantOptions.value = filteredOptions; - listTimeline(); + // reset falls ungΓΌltig + if ( + filterParticipant.value && + !filteredParticipants.some( + (p) => p.participantId?.toString() === filterParticipant.value, + ) + ) { + filterParticipant.value = undefined; + } } function onParticipantFilterChange(e: DropdownChangeEvent): void { @@ -313,6 +342,18 @@ Licensed under the Elastic License 2.0. */ listTimeline(); } + function onObservationGroupFilterChange(e: DropdownChangeEvent): void { + filterObservationGroup.value = e.value; + rebuildParticipantOptionsByGroup('observationGroupId', e.value); + listTimeline(); + } + + function onStudyGroupFilterChange(e: DropdownChangeEvent): void { + filterStudyGroup.value = e.value; + rebuildParticipantOptionsByGroup('studyGroupId', e.value); + listTimeline(); + } + function onEventClick(vueCalEvent: VueCalEvent, e: MouseEvent): void { const eventDetail: EventDetail | undefined = getEventDetailByEventCid( vueCalEvent.cId, @@ -353,6 +394,7 @@ Licensed under the Elastic License 2.0. */ function clearAllFilters(): void { filterRelativeStartDate.value = undefined; filterStudyGroup.value = undefined; + filterObservationGroup.value = undefined; filterParticipant.value = undefined; filterObservationAndIntervention.value = []; listTimeline(); @@ -421,6 +463,7 @@ Licensed under the Elastic License 2.0. */ props.studyId, filterParticipant.value, filterStudyGroup.value, + filterObservationGroup.value, filterRelativeStartDate.value, studyStartDate, studyEndDate, @@ -474,69 +517,106 @@ Licensed under the Elastic License 2.0. */ - diff --git a/src/components/dialog/ObservationTypeDialog.vue b/src/components/dialog/ObservationTypeDialog.vue new file mode 100644 index 00000000..bb45dcc2 --- /dev/null +++ b/src/components/dialog/ObservationTypeDialog.vue @@ -0,0 +1,465 @@ +/* Copyright LBI-DHP and/or licensed to LBI-DHP under one or more contributor +license agreements (LBI-DHP: Ludwig Boltzmann Institute for Digital Health and +Prevention -- A research institute of the Ludwig Boltzmann Gesellschaft, +Oesterreichische Vereinigung zur Foerderung der wissenschaftlichen Forschung). +Licensed under the Elastic License 2.0. */ + + + + + diff --git a/src/components/dialog/ParticipantDetailsDialog.vue b/src/components/dialog/ParticipantDetailsDialog.vue new file mode 100644 index 00000000..e94a0624 --- /dev/null +++ b/src/components/dialog/ParticipantDetailsDialog.vue @@ -0,0 +1,249 @@ + + + diff --git a/src/components/dialog/ParticipantInfoDialog.vue b/src/components/dialog/ParticipantInfoDialog.vue new file mode 100644 index 00000000..f564c2ab --- /dev/null +++ b/src/components/dialog/ParticipantInfoDialog.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/src/components/dialog/QrCodeDialog.vue b/src/components/dialog/QrCodeDialog.vue index 5b5a8d75..e23510a8 100644 --- a/src/components/dialog/QrCodeDialog.vue +++ b/src/components/dialog/QrCodeDialog.vue @@ -4,16 +4,17 @@ Prevention -- A research institute of the Ludwig Boltzmann Gesellschaft, Oesterreichische Vereinigung zur Foerderung der wissenschaftlichen Forschung). Licensed under the Elastic License 2.0. */ + + diff --git a/src/components/shared/ExclamationIcon.vue b/src/components/shared/ExclamationIcon.vue new file mode 100644 index 00000000..d659746b --- /dev/null +++ b/src/components/shared/ExclamationIcon.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/shared/Footer.vue b/src/components/shared/Footer.vue index ebb0853e..487d2dec 100644 --- a/src/components/shared/Footer.vue +++ b/src/components/shared/Footer.vue @@ -45,7 +45,7 @@ Licensed under the Elastic License 2.0. */ {{ uiConfig.title }} {{ $t('global.footer.aboutMore') }} - diff --git a/src/components/shared/MoreTabNav.vue b/src/components/shared/MoreTabNav.vue index 1145e527..074a03e7 100644 --- a/src/components/shared/MoreTabNav.vue +++ b/src/components/shared/MoreTabNav.vue @@ -124,7 +124,7 @@ Licensed under the Elastic License 2.0. */ draggable: false, }, onClose: () => { - console.log('closed access dialog'); + console.info('closed access dialog'); }, }); } @@ -207,7 +207,7 @@ Licensed under the Elastic License 2.0. */ - diff --git a/src/components/shared/RelativeScheduler.vue b/src/components/shared/RelativeScheduler.vue index b40495f0..da067470 100644 --- a/src/components/shared/RelativeScheduler.vue +++ b/src/components/shared/RelativeScheduler.vue @@ -1,30 +1,35 @@ - diff --git a/src/components/subComponents/AbsoluteSchedulerRepetition.vue b/src/components/subComponents/AbsoluteSchedulerRepetition.vue index 7ccd3c1c..b6e03669 100644 --- a/src/components/subComponents/AbsoluteSchedulerRepetition.vue +++ b/src/components/subComponents/AbsoluteSchedulerRepetition.vue @@ -118,31 +118,31 @@ const rruleWeekdayOptions = [ { label: t('scheduler.weekday.short.monday'), - value: t('scheduler.weekday.short.monday'), + value: 'MO', }, { label: t('scheduler.weekday.short.tuesday'), - value: t('scheduler.weekday.short.tuesday'), + value: 'TU', }, { label: t('scheduler.weekday.short.wednesday'), - value: t('scheduler.weekday.short.wednesday'), + value: 'WE', }, { label: t('scheduler.weekday.short.thursday'), - value: t('scheduler.weekday.short.thursday'), + value: 'TH', }, { label: t('scheduler.weekday.short.friday'), - value: t('scheduler.weekday.short.friday'), + value: 'FR', }, { label: t('scheduler.weekday.short.saturday'), - value: t('scheduler.weekday.short.saturday'), + value: 'SA', }, { label: t('scheduler.weekday.short.sunday'), - value: t('scheduler.weekday.short.sunday'), + value: 'SO', }, ]; @@ -234,26 +234,23 @@ - diff --git a/src/components/subComponents/CronScheduleInfo.vue b/src/components/subComponents/CronScheduleInfo.vue index e055c750..81ba945c 100644 --- a/src/components/subComponents/CronScheduleInfo.vue +++ b/src/components/subComponents/CronScheduleInfo.vue @@ -5,6 +5,7 @@ Oesterreichische Vereinigung zur Foerderung der wissenschaftlichen Forschung). Licensed under the Elastic License 2.0. */ - + diff --git a/src/components/subComponents/DatapointList.vue b/src/components/subComponents/DatapointList.vue index d3dfee09..ca66b67b 100644 --- a/src/components/subComponents/DatapointList.vue +++ b/src/components/subComponents/DatapointList.vue @@ -186,8 +186,8 @@ Licensed under the Elastic License 2.0. */ - diff --git a/src/components/subComponents/RandomizationView.vue b/src/components/subComponents/RandomizationView.vue new file mode 100644 index 00000000..d2a13249 --- /dev/null +++ b/src/components/subComponents/RandomizationView.vue @@ -0,0 +1,49 @@ + + diff --git a/src/components/subComponents/SchedulerInfoBlock.vue b/src/components/subComponents/SchedulerInfoBlock.vue index d305fc7c..8c71f8f0 100644 --- a/src/components/subComponents/SchedulerInfoBlock.vue +++ b/src/components/subComponents/SchedulerInfoBlock.vue @@ -181,10 +181,10 @@ Licensed under the Elastic License 2.0. */ -