Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert parameter metadata into a front-end form #23

Merged
merged 21 commits into from
Sep 9, 2024

Conversation

david-mears-2
Copy link
Contributor

@david-mears-2 david-mears-2 commented Sep 3, 2024

In this PR we go as far as using the metadata response from the R API (which contains information about the model parameters) to render a form. This front-end component (ParameterForm.vue) can currently cope with two types of parameterType: select, and globeSelect, both of which result in the same kind of form input. The resulting form input is rendered depending on the number and length of the options either as a select input or a set of radio buttons (stylised to look like a row of buttons).

This form can't yet be submitted - that's for the next PR.

We could have put metadata info into a pinia AppStore or some such, but since it's only used in one page (two components) for now, I haven't bothered with it yet; but I'm bearing in mind that this might be the simpler way forward. I will probably do that in the next PR too. This AppStore could also hold the 'versions' API response.

Comment on lines -1 to +9
import type { EventHandler, EventHandlerRequest } from "h3";
import type { EventHandler, H3Event } from "h3";
import type { ApiResponse } from "@/types/daedalusApiResponseTypes";

export const defineEventHandlerWithErrors = <T extends EventHandlerRequest, D>(
handler: EventHandler<T, D>,
): EventHandler<T, D> =>
defineEventHandler<T>(async (event) => {
const response = await handler(event) as ApiResponse;
// A wrapper for Nuxt's defineEventHandler that handles errors from the R API.
export const defineRApiEventHandler = (
callback: (event: H3Event) => Promise<ApiResponse>,
): EventHandler =>
defineEventHandler(async (event) => {
const response = await callback(event) as ApiResponse;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes were motivated by this comment: #10 (comment)

Copy link

codecov bot commented Sep 3, 2024

Codecov Report

Attention: Patch coverage is 68.77637% with 74 lines in your changes missing coverage. Please review.

Project coverage is 58.70%. Comparing base (bee45e8) to head (29a6f0f).
Report is 22 commits behind head on main.

Files with missing lines Patch % Lines
modules/coreui.ts 0.00% 45 Missing ⚠️
components/ParameterIcon.vue 68.96% 9 Missing ⚠️
server/api/metadata.get.ts 0.00% 7 Missing and 1 partial ⚠️
server/utils/defineRApiEventHandler.ts 0.00% 4 Missing and 1 partial ⚠️
server/api/versions.get.ts 0.00% 4 Missing ⚠️
components/ParameterForm.vue 97.27% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #23      +/-   ##
==========================================
+ Coverage   54.02%   58.70%   +4.67%     
==========================================
  Files          22       26       +4     
  Lines         509      724     +215     
  Branches       35       63      +28     
==========================================
+ Hits          275      425     +150     
- Misses        226      290      +64     
- Partials        8        9       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@david-mears-2 david-mears-2 force-pushed the jidea-57-non-country-inputs-front-end branch 5 times, most recently from 8511426 to c092e14 Compare September 3, 2024 12:10
This was to avoid having a handler fed into a another defineEventHandler which
would wrap the first EventHandler in a second EventHandler. Although the docs pointed
us in the direction of doing such a double-wrapping, we found this way to avoid doing so.

A downside of this is that endpoints are forced to use a specific function (e.g.
'defineEventHandler' or in this case 'defineCachedEventHandler') since they all route through
the wrapper's choice of function - this means if we wanted to use e.g. defineLazyEventHandler
from h3 in some endpoint, it (probably?) couldn't use our custom wrapper, unless we introduce some
switch statement that lets endpoints specify which EventHandler wrapping function to use. I am aware
of the following wrapping function choices:
- defineEventHandler
- defineCachedEventHandler
- defineLazyEventHandler
Cacheing was too complicated - there was no easy way to purge the cache for testing
different responses from the same endpoint within a test suite.
@david-mears-2 david-mears-2 force-pushed the jidea-57-non-country-inputs-front-end branch from fcf2714 to 40bcb92 Compare September 3, 2024 12:37
@david-mears-2 david-mears-2 force-pushed the jidea-57-non-country-inputs-front-end branch from 40bcb92 to 0ee4b83 Compare September 3, 2024 12:49
Copy link
Contributor

@EmmaLRussell EmmaLRussell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Couple of minor comments. That coreui eccentricity is a bit concerning...

case "vaccine":
return "cilIndustry";
case "pathogen":
return "cilBug";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug is cute but I wonder if it's going to make people immersed in disease response think about vector-borne illness particularly. And maybe cute isn't really appropriate anyway!.. Not sure - see what other people think!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also - arguably the preferred icon should be provided as part of the metadata too.. It is a presentation detail, but there could be a layer of abstraction from API icon type to front end icon type.

Copy link
Contributor Author

@david-mears-2 david-mears-2 Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arguably the preferred icon should be provided as part of the metadata too

I'm not sure I see the benefit. If there is an abstraction layer, we'd still end up with the web app having a dict look up.

Copy link
Contributor Author

@david-mears-2 david-mears-2 Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You make a good point about the bug icon, I wasn't sure myself. I will look for a little virus icon that matches our style.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New icon:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The colour's a little off, making my eye twitch, but I hope people can live with it for now because I couldn't quickly fix it (when you have inline svg you can change a fill attribute, but that doesn't seem to work when it's an svg loaded in an img tag)

Comment on lines 73 to 80
<CFormSelect
:id="globeParameter.id"
v-model="formData[globeParameter.id]"
:data-test="`${globeParameter.id}-select`"
:label="globeParameter.label"
:aria-label="globeParameter.label"
:options="selectOptions(globeParameter)"
:size="largeScreen ? 'lg' : ''"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because there's a bit of repetition here with the select parameter types, I wonder if it would be as well to just have a single v-for for both select and globe parameter types. That way you'll also be able to include the drop-down for the globe parameter in the position returned by the metadata, which would be more flexible I think. It doesn't necessarily feel the most natural to have the country parameter last (or first).

(And if we're going to honour the parameter order defined by the metadata it should be the same v-for for numeric parameters too eventually.)

Would mean slightly rejigging the logic for getting the icons etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point


const props = defineProps<{
globeParameter: Parameter | undefined
metaData: MetaData | undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
metaData: MetaData | undefined
metadata: Metadata | undefined

🙏

}

return accumulator;
}, {} as { [key: string]: any }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}, {} as { [key: string]: any }),
}, {} as { [key: string]: string | number }),

Copy link
Contributor Author

@david-mears-2 david-mears-2 Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out one of the 3rd-party components we're using, the radio button one, dictates that the type signature for the 'v-model' prop -- (which it translates into 'modelValue' behind the scenes!) -- is string | boolean | unknown[] | undefined. Notice it does not allow number.

From this, I will omit the Array type because we expect not to have multi-selects here.

I think I will keep the number type around as suggested, so it can be used by numeric inputs and we can do calculations with the value; and I'll just cast all values into strings when they are passed into the radio button v-model prop.

}>();

const formData = ref(
// Create a new object with keys set to the id values of the metaData.parameters array of objects, and all values set to refs with default values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than these individual refs, how about using reactive

Copy link
Contributor Author

@david-mears-2 david-mears-2 Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to work:

const formData = ref(
  props.metadata?.parameters.reduce((acc, { id, defaultOption, options }) => {
    acc[id] = defaultOption || options[0].id;
    return acc;
  }, {} as { [key: string]: string | number }),
);

And swapping out ref/reactive still passes all tests. So we can avoid having individual refs as I had before while still wrapping the whole thing in a ref() instead of a reactive(). Personally, I prefer the explicitness of having to type .value, which reminds me when I'm using something that's reactive.

Comment on lines 179 to 181
const eachOptionIsASingleWord = parameter.options.filter((option) => {
return option.label.includes(" ");
}).length === 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const eachOptionIsASingleWord = parameter.options.filter((option) => {
return option.label.includes(" ");
}).length === 0;
const eachOptionIsASingleWord = !parameter.options.some((option) => {
return option.label.includes(" ");
});

Comment on lines +190 to +199
const largeScreen = ref(true);
const breakpoint = 992; // CoreUI's "lg" breakpoint
const setFieldSizes = () => {
if (window.innerWidth < breakpoint) {
largeScreen.value = false;
} else {
largeScreen.value = true;
}
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is repeated in SideBar - should really only be implemented in one place and ideally called in one place and then made available to all components. Could be put in the store and made reactive (in another ticket!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add this to the app-store plan!

@@ -7,6 +7,7 @@ ENV NODE_ENV=production
WORKDIR /src

COPY . .
RUN npm install -g npm@latest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did out of date npm cause problems in this ticket? Could update the base image if you want to use a more recent npm. Oh, is this related to rollup issue you describe below? Either way could maybe put a comment here because I'm a still a bit confused!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it was related, but it was something I did while trying to debug the rollup issue. While working on that, I updated my local npm version, which then put my machine 1 minor version ahead of the one used in the Docker container. It seemed like a good idea to use the latest npm available in both places?

package.json Outdated
Comment on lines 74 to 80
"3": "the installation section of https://pinia.vuejs.org/ssr/nuxt.html",
"4": "==============================",
"5": "@rollup/rollup-linux-x64-gnu is an optional dependency as a fix for the issue that Rollup",
"6": "describes here: https://github.com/rollup/rollup/blob/f83b3151e93253a45f5b8ccb9ccb2e04214bc490/native.js#L59",
"7": "and which occurred for us when doing an installation with npm on Docker on CI. Their suggested fix",
"8": "does not work for our use case, because removing package-lock.json prevents the use of `npm ci`, so instead",
"9": "we use the solution suggested here: https://github.com/vitejs/vite/discussions/15532#discussioncomment-10192839"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh, I find this trick for putting comments into package.json pretty unreadable. Given that the dependencies etc being referred to aren't actually in the vicinity of the relevant section of package.json anyway, I'm not sure there's much benefit in keeping them here. How about moving them to a section in the README file dedicated to npm dependency notes? That way you could make all these urls into working links!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds better - they're pretty fiddly to write as well because of the line numbers!

ordered: boolean
options: Array<ParameterOption>
}
export interface MetaData {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think MetaData needs a hump in the middle... 🐫

This helps us to preserve the order of parameters in the metadata.
We found the documentation and behavior of the CFormSelect component too
confusing to be worth any benefit. In particular, it did
not seem to honour the initial v-model value, so we had to manually
set the 'selected' attribute for each option (which results in a
recalculation of options every time one is selected) in order to get the
correct default value to be honoured - and even then, it was confusingly
slow or flakey about it.
@david-mears-2 david-mears-2 force-pushed the jidea-57-non-country-inputs-front-end branch from 6a756bb to 529fefc Compare September 5, 2024 17:03
Copy link
Contributor

@EmmaLRussell EmmaLRussell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good! Are we supposed to include an attribution for that wikimedia svg?

@david-mears-2
Copy link
Contributor Author

Are we supposed to include an attribution for that wikimedia svg?

Well caught. Yes we are, "in any reasonable manner". I shall add a note to the ticket for the acknowledgements page (that sounds like less work than adapting a new svg with a more licentious license).

https://commons.wikimedia.org/wiki/File:Coronavirus_icon.svg

@EmmaLRussell
Copy link
Contributor

Are we supposed to include an attribution for that wikimedia svg?

Well caught. Yes we are, "in any reasonable manner". I shall add a note to the ticket for the acknowledgements page (that sounds like less work than adapting a new svg with a more licentious license).

https://commons.wikimedia.org/wiki/File:Coronavirus_icon.svg

Thanks, sounds good

@david-mears-2 david-mears-2 merged commit 45014aa into main Sep 9, 2024
6 checks passed
@david-mears-2 david-mears-2 deleted the jidea-57-non-country-inputs-front-end branch September 9, 2024 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants