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

Set correct path for uploaded file in sim config #1054

Merged
merged 9 commits into from
Dec 14, 2023
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ORIGIN=http://localhost:3000
PUBLIC_AERIE_FILE_STORE_PREFIX=/usr/src/app/merlin_file_store/
PUBLIC_GATEWAY_CLIENT_URL=http://localhost:9000
PUBLIC_GATEWAY_SERVER_URL=http://localhost:9000
PUBLIC_HASURA_CLIENT_URL=http://localhost:8080/v1/graphql
Expand Down
19 changes: 10 additions & 9 deletions docs/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

This document provides detailed information about environment variables for Aerie UI.

| Name | Description | Type | Default |
| ------------------------------ | --------------------------------------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ORIGIN` | Url of where the UI is served from. See the [Svelte Kit Adapter Node docs][svelte-kit-adapter-node-docs]. | `string` | http://localhost |
| `PUBLIC_GATEWAY_CLIENT_URL` | Url of the Gateway as called from the client (i.e. web browser) | `string` | http://localhost:9000 |
| `PUBLIC_GATEWAY_SERVER_URL` | Url of the Gateway as called from the server (i.e. Node.js container) | `string` | http://localhost:9000 |
| `PUBLIC_HASURA_CLIENT_URL` | Url of Hasura as called from the client (i.e. web browser) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_SERVER_URL` | Url of Hasura as called from the server (i.e. Node.js container) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_WEB_SOCKET_URL` | Url of Hasura called to establish a web-socket connection from the client | `string` | ws://localhost:8080/v1/graphql |
| `PUBLIC_LOGIN_PAGE` | Set to `enabled` to turn on login page. Otherwise set to `disabled` to turn off login page. | `string` | enabled |
| Name | Description | Type | Default |
| -------------------------------- | --------------------------------------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ORIGIN` | Url of where the UI is served from. See the [Svelte Kit Adapter Node docs][svelte-kit-adapter-node-docs]. | `string` | http://localhost |
| `PUBLIC_AERIE_FILE_STORE_PREFIX` | Prefix to prepend to files uploaded through simulation configuration. | `string` | /usr/src/app/merlin_file_store/ |
| `PUBLIC_GATEWAY_CLIENT_URL` | Url of the Gateway as called from the client (i.e. web browser) | `string` | http://localhost:9000 |
| `PUBLIC_GATEWAY_SERVER_URL` | Url of the Gateway as called from the server (i.e. Node.js container) | `string` | http://localhost:9000 |
| `PUBLIC_HASURA_CLIENT_URL` | Url of Hasura as called from the client (i.e. web browser) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_SERVER_URL` | Url of Hasura as called from the server (i.e. Node.js container) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_WEB_SOCKET_URL` | Url of Hasura called to establish a web-socket connection from the client | `string` | ws://localhost:8080/v1/graphql |
| `PUBLIC_LOGIN_PAGE` | Set to `enabled` to turn on login page. Otherwise set to `disabled` to turn off login page. | `string` | enabled |
4 changes: 2 additions & 2 deletions src/components/simulation/SimulationPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
arguments: newArgumentsMap,
};

effects.updateSimulation($plan, newSimulation, user, newFiles);
effects.updateSimulation($plan, newSimulation, user, newFiles, $plan.model.parameters.parameters);
}
}

Expand All @@ -149,7 +149,7 @@
arguments: newArguments,
};

effects.updateSimulation($plan, newSimulation, user, newFiles);
effects.updateSimulation($plan, newSimulation, user, newFiles, $plan.model.parameters.parameters);
}
}

Expand Down
47 changes: 46 additions & 1 deletion src/utilities/effects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import * as Errors from '../stores/errors';
import type { User } from '../types/app';
import type { Model } from '../types/model';
import type { ArgumentsMap, ParametersMap } from '../types/parameter';
import type { Plan } from '../types/plan';
import effects from './effects';
import effects, { replacePaths } from './effects';
import * as Modals from './modal';
import * as Requests from './requests';

Expand Down Expand Up @@ -310,4 +311,48 @@ describe('Handle modal and requests in effects', () => {
);
});
});

describe('replacePaths', () => {
it('should find and replace all matching paths in sim config', async () => {
const modelParameters: ParametersMap = {
parameter0: { order: 0, schema: { type: 'int' } },
parameter1: { order: 1, schema: { type: 'path' } },
parameter2: { order: 2, schema: { items: { x: { type: 'boolean' }, y: { type: 'path' } }, type: 'struct' } },
parameter3: {
order: 3,
schema: { items: { type: 'variant', variants: [{ key: 'A', label: 'A' }] }, type: 'series' },
},
parameter4: { order: 4, schema: { items: { type: 'path' }, type: 'series' } },
};
const simArgs: ArgumentsMap = {
parameter0: 1,
parameter1: 'abcdefg',
parameter2: {
x: true,
y: 'hijklmnop',
},
parameter3: ['A'],
parameter4: ['qrstuvwxyz', 'zyxwvut'],
};
const filenames = {
abcdefg: 'path1',
hijklmnop: 'path2',
qrstuvwxyz: 'path3',
zyxwvut: 'path4',
};

const res = replacePaths(modelParameters, simArgs, filenames);

expect(res).toEqual({
parameter0: 1,
parameter1: 'path1',
parameter2: {
x: true,
y: 'path2',
},
parameter3: ['A'],
parameter4: ['path3', 'path4'],
});
});
});
});
90 changes: 84 additions & 6 deletions src/utilities/effects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { env } from '$env/dynamic/public';
import type { CommandDictionary as AmpcsCommandDictionary } from '@nasa-jpl/aerie-ampcs';
import { get } from 'svelte/store';
import { SearchParameters } from '../enums/searchParameters';
Expand Down Expand Up @@ -54,10 +55,13 @@ import type { Extension, ExtensionPayload } from '../types/extension';
import type { Model, ModelInsertInput, ModelSchema, ModelSlim } from '../types/model';
import type { DslTypeScriptResponse, TypeScriptFile } from '../types/monaco';
import type {
Argument,
ArgumentsMap,
EffectiveArguments,
Parameter,
ParameterValidationError,
ParameterValidationResponse,
ParametersMap,
} from '../types/parameter';
import type {
PermissibleQueriesMap,
Expand Down Expand Up @@ -93,6 +97,7 @@ import type {
SchedulingSpecGoalInsertInput,
SchedulingSpecInsertInput,
} from '../types/scheduling';
import type { ValueSchema } from '../types/schema';
import type {
CommandDictionary,
GetSeqJsonResponse,
Expand Down Expand Up @@ -4107,18 +4112,41 @@ const effects = {
simulationSetInput: Simulation,
user: User | null,
newFiles: File[] = [],
modelParameters: ParametersMap | null = null,
): Promise<void> {
try {
if (!queryPermissions.UPDATE_SIMULATION(user, plan)) {
throwPermissionError('update this simulation');
}

const ids = await effects.uploadFiles(newFiles, user);
const original_filename_to_id: Record<string, number> = {};
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
if (id !== null) {
original_filename_to_id[newFiles[i].name] = id;
}
}

// The aerie gateway mangles the names of uploaded files to ensure uniqueness.
// Here, we use the ids of the files we just uploaded to look up the generated filenames
const generatedFilenames: Record<string, string> = {};
for (const newFile of newFiles) {
const id = original_filename_to_id[newFile.name];
const response = (await reqHasura<[{ name: string }]>(gql.GET_UPLOADED_FILENAME, { id }, user))[
'uploaded_file'
];
if (response !== null) {
generatedFilenames[newFile.name] = `${env.PUBLIC_AERIE_FILE_STORE_PREFIX}${response[0]['name']}`;
}
}

const data = await reqHasura<Pick<Simulation, 'id'>>(
gql.UPDATE_SIMULATION,
{
id: simulationSetInput.id,
simulation: {
arguments: simulationSetInput.arguments,
arguments: replacePaths(modelParameters, simulationSetInput.arguments, generatedFilenames),
simulation_end_time: simulationSetInput?.simulation_end_time ?? null,
simulation_start_time: simulationSetInput?.simulation_start_time ?? null,
simulation_template_id: simulationSetInput?.template?.id ?? null,
Expand All @@ -4127,7 +4155,6 @@ const effects = {
user,
);
if (data.updateSimulation !== null) {
await effects.uploadFiles(newFiles, user);
showSuccessToast('Simulation Updated Successfully');
} else {
throw Error(`Unable to update simulation with ID: "${simulationSetInput.id}"`);
Expand Down Expand Up @@ -4266,15 +4293,16 @@ const effects = {
}
},

async uploadFiles(files: File[], user: User | null): Promise<boolean> {
async uploadFiles(files: File[], user: User | null): Promise<(number | null)[]> {
try {
const ids = [];
for (const file of files) {
await effects.uploadFile(file, user);
ids.push(await effects.uploadFile(file, user));
}
return true;
return ids;
} catch (e) {
catchError(e as Error);
return false;
return [];
}
},

Expand Down Expand Up @@ -4359,4 +4387,54 @@ const effects = {
},
};

/**
* Traverses the given simulation arguments and does a "find and replace", replacing any paths that match the keys of `pathsToReplace` with the corresponding values.
*
* @param modelParameters The type definitions of the mission model parameters. Used to determine which parameters have type 'path'.
* @param simArgs The full simulation arguments, which are assumed to conform to the above type definition.
* @param pathsToReplace A map from old paths to new paths. Any occurrences of old paths in simArgs will be replaced with new paths.
* @returns
*/
export function replacePaths(
modelParameters: ParametersMap | null,
simArgs: ArgumentsMap,
pathsToReplace: Record<string, string>,
): ArgumentsMap {
if (modelParameters === null) {
return simArgs;
}
const result: ArgumentsMap = {};
for (const parameterName in modelParameters) {
const parameter: Parameter = modelParameters[parameterName];
const arg: Argument = simArgs[parameterName];
if (arg !== undefined) {
result[parameterName] = replacePathsHelper(parameter.schema, arg, pathsToReplace);
}
}
return result;
}

function replacePathsHelper(schema: ValueSchema, arg: Argument, pathsToReplace: Record<string, string>) {
switch (schema.type) {
case 'path':
if (arg in pathsToReplace) {
return pathsToReplace[arg];
} else {
return arg;
}
case 'struct':
return (function () {
const res: Argument = {};
for (const key in schema.items) {
res[key] = replacePathsHelper(schema.items[key], arg[key], pathsToReplace);
}
return res;
})();
case 'series':
return arg.map((x: Argument) => replacePathsHelper(schema.items, x, pathsToReplace));
default:
return arg;
}
}

export default effects;
8 changes: 8 additions & 0 deletions src/utilities/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,14 @@ const gql = {
}
`,

GET_UPLOADED_FILENAME: `#graphql
query GetUploadedFileName($id: Int!) {
uploaded_file(where: { id: { _eq: $id }}) {
name
}
}
`,

GET_USER_SEQUENCE: `#graphql
query GetUserSequence($id: Int!) {
userSequence: user_sequence_by_pk(id: $id) {
Expand Down
Loading