Skip to content

Commit

Permalink
Merge branch 'main' of github.com:mrc-ide/wodin into mrc-6023
Browse files Browse the repository at this point in the history
  • Loading branch information
M-Kusumgar committed Dec 5, 2024
2 parents b0b59dc + eef2b4f commit c84f147
Show file tree
Hide file tree
Showing 29 changed files with 114 additions and 81 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,6 @@ Use the `./scripts/run-version.sh` script setting the branch references for the

## Hot Reloading

This repo has two main parts: `app/server` (Express server) and `app/static` (Vue frontend). The normal way to spin up the app without hot reloading is build frontend, copy build files into public folder in the server and run the server. The html file will then look for its javascript src `wodin.js` and css src `wodin.css` in the `app/server/public` directory and load the app from these.
This repo has two main parts: `app/server` (Express server) and `app/static` (Vue frontend). The normal way to spin up the app without hot reloading is build frontend, copy build files into public folder in the server (`npm run build --prefix=app/static` builds the frontend and copies the files to the server) and run the server (`npm run serve --prefix=app/server`). The html file will then look for its javascript src `wodin.js` and css src `wodin.css` in the `app/server/public` directory and load the app from these.

The hot reloading setup is different. The `server.js` file (from building the server) takes in a `--hot-reload` boolean option which tells it to instead look for the javascript src at `http://localhost:5173/src/wodin.ts` since `wodin.ts` is our entrypoint (`http://localhost:5173` is the default url for `vite` dev mode and it serves the files, after transpiling to javascript, based on your folder structure) and css src at `http://localhost:5173/src/scss/style.scss`.
The hot reloading setup is different. The `server.js` file (from building the server) takes in a `--hot-reload` boolean option which tells it to instead look for the javascript src at `http://localhost:5173/src/wodin.ts` since `wodin.ts` is our entrypoint (`http://localhost:5173` is the default url for `vite` dev mode and it serves the files, after transpiling to javascript, based on your folder structure) and css src at `http://localhost:5173/src/scss/style.scss`. To run it this way we run `npm run dev --prefix=app/static` to start the vite dev server and then we run `npm run serve-hot --prefix=app/server`.
7 changes: 7 additions & 0 deletions app/server/tests/server/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe("args", () => {
"--base-url", "http://example.com/wodin",
"--redis-url=redis:6379",
"--port=1234",
"--hot-reload=true",
"/testConfig"];
const args = processArgs(argv);
expect(args.path).toBe("/testConfig");
Expand All @@ -32,6 +33,7 @@ describe("args", () => {
redisUrl: "redis:6379",
port: 1234
});
expect(args.hotReload).toBe(true);
});

it("falls back on process.argv", () => {
Expand All @@ -47,4 +49,9 @@ describe("args", () => {
process.argv = ["node", "wodin", "--port=one", "somepath"];
expect(() => processArgs()).toThrow("Expected an integer for port");
});

it("requires that hot-reload is a boolean", () => {
process.argv = ["node", "wodin", "--hot-reload=T", "somepath"];
expect(() => processArgs()).toThrow("Expected a boolean for hot-reload");
});
});
1 change: 1 addition & 0 deletions app/server/tests/server/test-view.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ test }}
19 changes: 16 additions & 3 deletions app/server/tests/server/views.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import { registerViews } from "../../src/server/views";
import * as path from "path";

const { mockRender } = vi.hoisted(() => ({ mockRender: vi.fn().mockReturnValue("mustache") }));
vi.mock("mustache", () => ({ render: mockRender }));

describe("views", () => {
it("register views sets app values and registers helper", () => {
it("register views sets app values and registers engine", () => {
const mockApp = {
set: vi.fn(),
engine: vi.fn()
} as any;
registerViews(mockApp, "/testRoot");
};
registerViews(mockApp as any, "/testRoot");

expect(mockApp.engine).toBeCalledTimes(1);
expect(mockApp.engine.mock.calls[0][0]).toBe("mustache");
const engineFn = mockApp.engine.mock.calls[0][1];
const mockCallback = vi.fn();

engineFn(path.resolve(__dirname, "./test-view.mustache"), {}, mockCallback);
expect(mockCallback.mock.calls[0][0]).toBe(null);
expect(mockCallback.mock.calls[0][1]).toBe("mustache");

engineFn(path.resolve(__dirname, "./wrong-file-path.mustache"), {}, mockCallback);
expect((mockCallback.mock.calls[1][0] as Error).message).toContain("no such file");

expect(mockApp.set).toBeCalledTimes(2);
expect(mockApp.set.mock.calls[0][0]).toBe("view engine");
Expand Down
9 changes: 6 additions & 3 deletions app/static/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@ export default [
},

{
files: ["**/*.ts", "**/*.vue"],
files: ["src/**/*.ts", "src/**/*.vue"],
rules: {
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error",
"vue/no-setup-props-destructure": "error",
}
},

{
files: ["**/tests/**/*.ts"],
rules: {
"vitest/expect-expect": "off"
"vitest/expect-expect": "off",
"@typescript-eslint/no-explicit-any": "off",
}
},

Expand Down
26 changes: 16 additions & 10 deletions app/static/src/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@ export interface ResponseWithType<T> extends ResponseSuccess {
data: T;
}

export function isAPIError(object: any): object is WodinError {
return typeof object.error === "string" && (object.details === undefined || typeof object.details === "string");
function isObject<K extends string>(object: unknown): object is Record<K, unknown> {
return typeof object === "object" && !Array.isArray(object) && object !== null;
}

export function isAPIResponseFailure(object: any): object is ResponseFailure {
return (
export function isAPIError(object: unknown): object is WodinError {
return isObject<keyof WodinError>(object) && typeof object.error === "string" &&
(object.detail === undefined || object.detail === null || typeof object.detail === "string");
}

export function isAPIResponseFailure(object: unknown): object is ResponseFailure {
return !!(
object &&
isObject<keyof ResponseFailure>(object) &&
object.status === "failure" &&
Array.isArray(object.errors) &&
object.errors.every((e: any) => isAPIError(e))
object.errors.every(e => isAPIError(e))
);
}

Expand All @@ -38,12 +44,12 @@ export interface API<S, E> {
type OnError = (failure: ResponseFailure) => void;
type OnSuccess = (success: ResponseSuccess) => void;

export class APIService<S extends string, E extends string> implements API<S, E> {
export class APIService<S extends string, E extends string, State> implements API<S, E> {
private readonly _commit: Commit;

private readonly _baseUrl: string;

constructor(context: AppCtx) {
constructor(context: AppCtx<State>) {
this._commit = context.commit;
this._baseUrl = (context.rootState as AppState).baseUrl!;
}
Expand Down Expand Up @@ -104,7 +110,7 @@ export class APIService<S extends string, E extends string> implements API<S, E>
};

withSuccess = (type: S, root = false) => {
this._onSuccess = (data: any) => {
this._onSuccess = (data: unknown) => {
const finalData = this._freezeResponse ? freezer.deepFreeze(data) : data;
try {
this._commit(type, finalData, { root });
Expand Down Expand Up @@ -203,7 +209,7 @@ export class APIService<S extends string, E extends string> implements API<S, E>
return this._handleAxiosResponse(axios.get(fullUrl));
}

async post<T>(url: string, body: any, contentType = "application/json"): Promise<void | ResponseWithType<T>> {
async post<T>(url: string, body: unknown, contentType = "application/json"): Promise<void | ResponseWithType<T>> {
this._verifyHandlers(url);
if (STATIC_BUILD) return this._overridePostRequestsStaticBuild(url);
const headers = { "Content-Type": contentType };
Expand All @@ -212,4 +218,4 @@ export class APIService<S extends string, E extends string> implements API<S, E>
}
}

export const api = <S extends string, E extends string>(ctx: AppCtx): APIService<S, E> => new APIService<S, E>(ctx);
export const api = <S extends string, E extends string, State>(ctx: AppCtx<State>): APIService<S, E, State> => new APIService<S, E, State>(ctx);
1 change: 1 addition & 0 deletions app/static/src/components/VerticalCollapse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default defineComponent({
VueFeather
},
setup(props) {
// eslint-disable-next-line vue/no-setup-props-destructure
const collapsed = ref(props.collapsedDefault);
const toggleCollapse = () => {
Expand Down
9 changes: 5 additions & 4 deletions app/static/src/components/WodinPlot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
AxisType,
Layout,
Config,
LayoutAxis
LayoutAxis,
PlotlyHTMLElement
} from "plotly.js-basic-dist-min";
import { WodinPlotData, fadePlotStyle, margin, config } from "../plot";
import WodinPlotDataSummary from "./WodinPlotDataSummary.vue";
Expand All @@ -46,7 +47,7 @@ export default defineComponent({
// Only used as an indicator that redraw is required when this changes - the data to display is calculated by
// plotData function using these solutions
redrawWatches: {
type: Array as PropType<any[]>,
type: Array as PropType<unknown[]>,
required: true
},
recalculateOnRelayout: {
Expand Down Expand Up @@ -83,7 +84,7 @@ export default defineComponent({
const startTime = 0;
const plot = ref<null | HTMLElement>(null); // Picks up the element with 'plot' ref in the template
const plot = ref<null | PlotlyHTMLElement>(null); // Picks up the element with 'plot' ref in the template
const baseData = ref<WodinPlotData>([]);
const nPoints = 1000; // TODO: appropriate value could be derived from width of element
Expand All @@ -101,7 +102,7 @@ export default defineComponent({
const lastYAxisFromZoom: Ref<Partial<LayoutAxis> | null> = ref(null);
const commitYAxisRange = () => {
const plotLayout = (plot.value as any).layout;
const plotLayout = plot.value!.layout;
const yRange = plotLayout.yaxis?.range;
if (plotLayout) {
if (props.fitPlot) {
Expand Down
1 change: 1 addition & 0 deletions app/static/src/components/WodinSession.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default defineComponent({
!!store.state.config && !!store.state.sessions.sessionsMetadata);
// These props won't change as provided by server
// eslint-disable-next-line vue/no-setup-props-destructure
const { appName, baseUrl, loadSessionId, appsPath, enableI18n, defaultLanguage } = props;
store.dispatch(AppStateAction.InitialiseApp, {
Expand Down
1 change: 1 addition & 0 deletions app/static/src/components/WodinTabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default defineComponent({
},
emits: ["tabSelected"],
setup(props, { emit }) {
// eslint-disable-next-line vue/no-setup-props-destructure
const selectedTabName = ref(props.initSelectedTab || props.tabNames[0]);
const tabSelected = (tabName: string) => {
Expand Down
1 change: 1 addition & 0 deletions app/static/src/components/graphConfig/GraphConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default defineComponent({
},
setup(props, { emit }) {
const store = useStore();
// eslint-disable-next-line vue/no-setup-props-destructure
const { startDrag, endDrag, onDrop, removeVariable } = SelectVariables(store, emit, false, props.graphIndex);
const selectedVariables = computed<string[]>(
() => store.state.graphs.config[props.graphIndex].selectedVariables
Expand Down
7 changes: 4 additions & 3 deletions app/static/src/components/options/ParameterSetView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
</template>

<script lang="ts">
import { computed, defineComponent, nextTick, onBeforeUnmount, PropType, ref, watch } from "vue";
import { computed, defineComponent, nextTick, onBeforeUnmount, PropType, Ref, ref, watch } from "vue";
import { useStore } from "vuex";
import VueFeather from "vue-feather";
import { ParameterSet } from "../../store/run/state";
Expand Down Expand Up @@ -172,6 +172,7 @@ export default defineComponent({
const paramNameInput = ref(null);
const editDisplayName = ref(false);
// eslint-disable-next-line vue/no-setup-props-destructure
const newDisplayName = ref(props.parameterSet.displayName);
const editDisplayNameOn = () => {
editDisplayName.value = true;
Expand All @@ -189,9 +190,9 @@ export default defineComponent({
editDisplayName.value = false;
}
};
const saveButton = ref<HTMLButtonElement | null>(null);
const saveButton = ref<Ref<InstanceType<typeof VueFeather>> | null>(null);
const cancelEditDisplayName = (event: FocusEvent) => {
if (event.relatedTarget && event.relatedTarget === (saveButton.value as any).$el) return;
if (event.relatedTarget && event.relatedTarget === saveButton.value!.$el) return;
store.commit(`run/${RunMutation.TurnOffDisplayNameError}`, props.parameterSet.name);
newDisplayName.value = props.parameterSet.displayName;
editDisplayName.value = false;
Expand Down
1 change: 1 addition & 0 deletions app/static/src/components/options/SensitivityOptions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export default defineComponent({
);
const showOptions = computed(() => allSettings.value.length && !!allSettings.value[0].parameterToVary);
// eslint-disable-next-line vue/no-setup-props-destructure
const compileModelMessage = userMessages.sensitivity.compileRequiredForOptions(props.multiSensitivity);
const editOpen = ref(false);
const editSettingsIdx = ref<number | null>(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default defineComponent({
},
setup(props) {
const store = useStore();
// eslint-disable-next-line vue/no-setup-props-destructure
const { canDownloadSummary, downloading, downloadSummaryUserFileName, downloadSummary } = baseSensitivity(
store,
props.multiSensitivity
Expand Down
6 changes: 3 additions & 3 deletions app/static/src/csvUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface ParseError {
message: string;
}

export class CSVUpload<S extends string, E extends string> {
export class CSVUpload<S extends string, E extends string, State> {
private readonly _commit: Commit;

private _onError: OnError | null = null;
Expand All @@ -23,7 +23,7 @@ export class CSVUpload<S extends string, E extends string> {

private _postSuccess: PostSuccess | null = null;

constructor(context: AppCtx) {
constructor(context: AppCtx<State>) {
this._commit = context.commit;
}

Expand Down Expand Up @@ -100,4 +100,4 @@ export class CSVUpload<S extends string, E extends string> {
};
}

export const csvUpload = <S extends string, E extends string>(ctx: AppCtx): CSVUpload<S, E> => new CSVUpload<S, E>(ctx);
export const csvUpload = <S extends string, E extends string, State>(ctx: AppCtx<State>): CSVUpload<S, E, State> => new CSVUpload<S, E, State>(ctx);
13 changes: 7 additions & 6 deletions app/static/src/directives/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface ToolTipSettings {
delayMs?: number;
}

type TooltipWithConfig = Tooltip & { _config: Tooltip.Options }

export default {
mounted(el: HTMLElement, binding: DirectiveBinding<string | ToolTipSettings>): void {
const { value } = binding;
Expand Down Expand Up @@ -39,13 +41,13 @@ export default {
beforeUpdate(el: HTMLElement, binding: DirectiveBinding<string | ToolTipSettings>): void {
const { value } = binding;

let tooltip = Tooltip.getInstance(el);
let tooltip = Tooltip.getInstance(el) as TooltipWithConfig;

if (typeof value !== "string" && tooltip) {
const variant = value?.variant || "text";
const oldCustomClass = variant === "text" ? "" : `tooltip-${variant}`;

const isVariantSame = (tooltip as any)._config.customClass === oldCustomClass;
const isVariantSame = tooltip._config.customClass === oldCustomClass;
if (!isVariantSame) {
tooltip.dispose();

Expand All @@ -56,16 +58,15 @@ export default {
customClass: variant === "text" ? "" : `tooltip-${variant}`,
animation: false,
delay: { show: value?.delayMs || 0, hide: 0 }
});
}) as TooltipWithConfig;
}
}

const content = typeof value === "string" ? value : value?.content || "";

if (tooltip) {
const configuredTooltip = tooltip as any;
configuredTooltip._config.title = content;
const { trigger } = configuredTooltip._config;
tooltip._config.title = content;
const { trigger } = tooltip._config;
if (trigger === "manual") {
if (!content) {
tooltip.hide();
Expand Down
6 changes: 3 additions & 3 deletions app/static/src/excel/wodinExcelDownload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import { AppState } from "../store/appState/state";
import { AppCtx } from "../types/utilTypes";
import { ErrorsMutation } from "../store/errors/mutations";

export abstract class WodinExcelDownload {
export abstract class WodinExcelDownload<State> {
private readonly _fileName: string;

protected readonly _state: AppState;

protected readonly _rootGetters: any;
protected readonly _rootGetters: AppCtx<State>["rootGetters"];

protected readonly _commit: Commit;

protected readonly _workbook: XLSX.WorkBook;

constructor(context: AppCtx, fileName: string) {
constructor(context: AppCtx<State>, fileName: string) {
this._state = context.rootState;
this._rootGetters = context.rootGetters;
this._commit = context.commit;
Expand Down
4 changes: 2 additions & 2 deletions app/static/src/excel/wodinModelOutputDownload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import { FitState } from "../store/fit/state";
import { FitDataGetter } from "../store/fitData/getters";
import { GraphsGetter } from "../store/graphs/getters";

export class WodinModelOutputDownload extends WodinExcelDownload {
export class WodinModelOutputDownload<State> extends WodinExcelDownload<State> {
private readonly _points: number;

constructor(context: AppCtx, fileName: string, points: number) {
constructor(context: AppCtx<State>, fileName: string, points: number) {
super(context, fileName);
this._points = points;
}
Expand Down
2 changes: 1 addition & 1 deletion app/static/src/excel/wodinSensitivitySummaryDownload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const extremeSummarySheets: ExtremeSummarySheetSettings[] = [
{ name: "TimeAtMin", extremePrefix: SensitivityPlotExtremePrefix.time, extreme: SensitivityPlotExtreme.Min },
{ name: "TimeAtMax", extremePrefix: SensitivityPlotExtremePrefix.time, extreme: SensitivityPlotExtreme.Max }
];
export class WodinSensitivitySummaryDownload extends WodinExcelDownload {
export class WodinSensitivitySummaryDownload<State> extends WodinExcelDownload<State> {
private _addSummarySheetFromOdinSeriesSet = (data: OdinUserTypeSeriesSet, sheetName: string) => {
const sheetData = data.x.map((x: OdinUserType, index: number) => {
return {
Expand Down
2 changes: 1 addition & 1 deletion app/static/src/store/appState/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function immediateUploadState(context: ActionContext<AppState, AppState>)
const { appName, appsPath, sessionId } = state;

commit(AppStateMutation.SetStateUploadInProgress, true);
await api<AppStateMutation, ErrorsMutation>(context)
await api<AppStateMutation, ErrorsMutation, AppState>(context)
.withSuccess(AppStateMutation.SetPersisted)
.withError(ErrorsMutation.AddError)
.post(`/${appsPath}/${appName}/sessions/${sessionId}`, serialiseState(state));
Expand Down
Loading

0 comments on commit c84f147

Please sign in to comment.