Skip to content
Merged
10 changes: 7 additions & 3 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: ui
on:
push:
branches:
- '*'
- '**'
tags:
- v*
pull_request:
Expand Down Expand Up @@ -60,9 +60,12 @@ jobs:
name: nextjs-build
path: build/

- name: Create environment variable with the commit id
- name: Create environment variables (commit id + sanitized branch)
run: |
echo "DOCKER_TAG=${GITHUB_SHA}" >> $GITHUB_ENV
SAFE_BRANCH="${{ steps.branch-name.outputs.current_branch }}"
# Docker tags forbid '/' — replace with '-' so slash-containing branches build
echo "SAFE_BRANCH=${SAFE_BRANCH//\//-}" >> $GITHUB_ENV

- name: Expose the commit id
id: exposeValue
Expand All @@ -86,7 +89,7 @@ jobs:
with:
push: true
context: .
tags: ${{ env.DOCKER_IMAGE_REPOSITORY }}/${{ env.DOCKER_IMAGE_NAME }}:${{ steps.branch-name.outputs.current_branch }}, ${{ env.DOCKER_IMAGE_REPOSITORY }}/${{ env.DOCKER_IMAGE_NAME }}:${{ env.DOCKER_TAG }}
tags: ${{ env.DOCKER_IMAGE_REPOSITORY }}/${{ env.DOCKER_IMAGE_NAME }}:${{ env.SAFE_BRANCH }}, ${{ env.DOCKER_IMAGE_REPOSITORY }}/${{ env.DOCKER_IMAGE_NAME }}:${{ env.DOCKER_TAG }}
file: ${{ env.DOCKER_FILE}}
platforms: linux/amd64,linux/arm64

Expand All @@ -108,6 +111,7 @@ jobs:
name: 'Scan vulnerabilities in the image'
needs: [push]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
Expand Down
34 changes: 34 additions & 0 deletions src/model-catalog-api/custom-apis/model-configuration-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,40 @@ export class CustomModelConfigurationSetupApi extends DefaultReduxApi<
super(ModelConfigurationSetupApi, user, config);
}

/** Override get to restore isOptional on hasInput items from the raw API response.
* The generated ModelConfigurationSetupFromJSON strips isOptional because it is not
* in the v1.8.0 OpenAPI schema used to generate the client.
*/
public get: ActionThunk<Promise<ModelConfigurationSetup>, MCActionAdd> =
(uri: string) => (dispatch) => {
const id: string = this._getIdFromUri(uri);
const rawReq = this._api.modelconfigurationsetupsIdGetRaw({
username: this._username,
id,
});
return rawReq.then(async (apiResponse) => {
const [rawJson, typed] = await Promise.all([
apiResponse.raw.clone().json() as Promise<any>,
apiResponse.value(),
]);
// Restore isOptional on hasInput items from the raw JSON
if (typed.hasInput && rawJson.hasInput) {
typed.hasInput = typed.hasInput.map((item: any, i: number) => ({
...item,
isOptional: !!(rawJson.hasInput[i]?.isOptional),
}));
}
if (this._redux) {
dispatch({
type: MODEL_CATALOG_ADD,
kind: this.getName(),
payload: this._idReducer({}, typed),
});
}
return typed;
});
};

private simplePost: ActionThunk<
Promise<ModelConfigurationSetup>,
MCActionAdd
Expand Down
36 changes: 36 additions & 0 deletions src/model-catalog-api/custom-apis/model-configuration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IdMap } from "app/reducers";
import {
MCActionAdd,
MODEL_CATALOG_ADD,
ActionThunk,
} from "../actions";
import { Configuration, BaseAPI, TapisApp } from "@mintproject/modelcatalog_client";
Expand All @@ -25,6 +26,41 @@ export class CustomModelConfigurationApi extends DefaultReduxApi<
super(ModelConfigurationApi, user, config);
}

/** Override get to restore isOptional on hasInput items from the raw API response.
* The generated ModelConfigurationFromJSON strips isOptional because it is not in
* the v1.8.0 OpenAPI schema used to generate the client. We fetch the raw JSON
* alongside the typed object and merge the flag back before dispatching to Redux.
*/
public get: ActionThunk<Promise<ModelConfiguration>, MCActionAdd> =
(uri: string) => (dispatch) => {
const id: string = this._getIdFromUri(uri);
const rawReq = this._api.modelconfigurationsIdGetRaw({
username: this._username,
id,
});
return rawReq.then(async (apiResponse) => {
const [rawJson, typed] = await Promise.all([
apiResponse.raw.clone().json() as Promise<any>,
apiResponse.value(),
]);
// Restore isOptional on hasInput items from the raw JSON
if (typed.hasInput && rawJson.hasInput) {
typed.hasInput = typed.hasInput.map((item: any, i: number) => ({
...item,
isOptional: !!(rawJson.hasInput[i]?.isOptional),
}));
}
if (this._redux) {
dispatch({
type: MODEL_CATALOG_ADD,
kind: this.getName(),
payload: this._idReducer({}, typed),
});
}
return typed;
});
};

private simplePost: ActionThunk<Promise<ModelConfiguration>, MCActionAdd> =
this.post;

Expand Down
1 change: 1 addition & 0 deletions src/model-catalog-api/model-catalog-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Configuration,
ConfigurationParameters,
} from "@mintproject/modelcatalog_client";
import "./sdk-patches";
import { UserCatalog } from "./user-catalog";
import { MINT_PREFERENCES } from "config";

Expand Down
34 changes: 34 additions & 0 deletions src/model-catalog-api/sdk-patches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Patch the SDK's models index so internal API call sites use the patched
// serializer/deserializer. The internal .js modules access functions via
// `require("../models").DatasetSpecificationToJSON`, so we mutate that exact
// object. Patching the top-level package index does NOT propagate, because the
// top-level uses __export copies.
const models = require("@mintproject/modelcatalog_client/dist/models");

const originalToJSON = models.DatasetSpecificationToJSON;
const originalFromJSONTyped = models.DatasetSpecificationFromJSONTyped;
const originalFromJSON = models.DatasetSpecificationFromJSON;

function patchedToJSON(value: any): any {
const out = originalToJSON(value);
if (out && value && value.isOptional !== undefined && value.isOptional !== null) {
out.isOptional = value.isOptional;
}
return out;
}

function patchedFromJSONTyped(json: any, ignoreDiscriminator: boolean): any {
const ds = originalFromJSONTyped(json, ignoreDiscriminator);
if (ds && json && json.isOptional !== undefined && json.isOptional !== null) {
ds.isOptional = json.isOptional;
}
return ds;
}

function patchedFromJSON(json: any): any {
return patchedFromJSONTyped(json, false);
}

models.DatasetSpecificationToJSON = patchedToJSON;
models.DatasetSpecificationFromJSONTyped = patchedFromJSONTyped;
models.DatasetSpecificationFromJSON = patchedFromJSON;
47 changes: 45 additions & 2 deletions src/screens/models/configure/resources/dataset-specification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ModelCatalogResource } from "./resource";
import { ModelCatalogResource, Action } from "./resource";
import { property, html, customElement, css } from "lit-element";
import { connect } from "pwa-helpers/connect-mixin";
import { store } from "app/store";
Expand Down Expand Up @@ -84,6 +84,11 @@ export class ModelCatalogDatasetSpecification extends connect(store)(
private sampleResources: IdMap<ModelCatalogSampleResource> = {};
private sampleCollections: IdMap<ModelCatalogSampleCollection> = {};
private _vpDisplayer: IdMap<ModelCatalogVariablePresentation> = {};
// isOptional is a junction column on modelcatalog_configuration_input, not on the
// DatasetSpecification entity. The base setResources() refetches each row by id,
// and that entity GET has no isOptional. Stash the inline value from the parent
// ModelConfiguration payload and re-apply it onto _loadedResources before render.
private _junctionOverlay: { [id: string]: { isOptional?: boolean } } = {};
@property({ type: String }) private _fileType: "resource" | "collection" =
"resource";

Expand All @@ -92,6 +97,26 @@ export class ModelCatalogDatasetSpecification extends connect(store)(
this.colspan = 4;
}

public setResources(r: DatasetSpecification[]) {
this._junctionOverlay = {};
(r || []).forEach((it: any) => {
if (it && it.id) {
this._junctionOverlay[it.id] = { isOptional: !!it.isOptional };
}
});
super.setResources(r);
}

public requestUpdate(name?: PropertyKey, oldValue?: unknown) {
Object.keys(this._junctionOverlay || {}).forEach((id: string) => {
const lr = (this as any)._loadedResources[id];
if (lr && (lr as any).isOptional === undefined) {
(lr as any).isOptional = this._junctionOverlay[id].isOptional;
}
});
return super.requestUpdate(name as any, oldValue);
}

constructor() {
super();
this._inputVariablePresentation = new ModelCatalogVariablePresentation();
Expand Down Expand Up @@ -198,6 +223,23 @@ export class ModelCatalogDatasetSpecification extends connect(store)(
>(.${r.hasFormat})</span
>`
: ""}
${this._action === Action.EDIT_OR_ADD
? html`<label style="font-size: 0.75em; margin-left: 8px; cursor: pointer; color: #555; display: inline-flex; align-items: center; gap: 3px;" title="Optional inputs are skipped during execution if no dataset is bound">
<input type="checkbox"
style="margin: 0;"
.checked="${!!(r as any).isOptional}"
@change="${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
(r as any).isOptional = checked;
this._junctionOverlay[r.id] = { isOptional: checked };
this.requestUpdate();
}}"
/>
optional
</label>`
: (r as any).isOptional
? html`<span style="font-size: 0.7em; margin-left: 8px; padding: 1px 6px; border-radius: 3px; background: #f0f0f0; color: #666;" title="Optional input — skipped during execution if no dataset is bound">optional</span>`
: ""}
</td>
<td>
<b>${r.description ? r.description[0] : ""}</b>
Expand Down Expand Up @@ -403,7 +445,8 @@ export class ModelCatalogDatasetSpecification extends connect(store)(
"If no variables are associated with an input, we will not be able to search dataset candidates in the MINT data catalog when using this model"
)
) {
return DatasetSpecificationFromJSON(jsonRes);
const result = DatasetSpecificationFromJSON(jsonRes);
return result;
}
} else {
// Show errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export class ModelCatalogModelConfigurationSetup extends connect(store)(
}

this._inputParameter.setResources(r.hasParameter);
// hasInput items carry isOptional from the raw API response (see CustomModelConfigurationSetupApi.get)
this._inputDSInput.setResources(r.hasInput);
this._inputDSOutput.setResources(r.hasOutput);
this._inputSourceCode.setResources(r.hasSourceCode);
Expand Down Expand Up @@ -458,6 +459,7 @@ export class ModelCatalogModelConfigurationSetup extends connect(store)(
${this._inputParameter}

<wl-title level="4" style="margin-top:1em"> Files: </wl-title>
<div style="font-size: 0.8em; color: #666; margin-bottom: 4px;">Inputs marked as optional can be skipped when no dataset is bound for execution.</div>
${this._inputDSInput}

<wl-title level="3" style="margin-top:1em"> Output files: </wl-title>
Expand Down Expand Up @@ -686,6 +688,7 @@ ${edResource && edResource.hasUsageNotes
${this._inputParameter}

<wl-title level="4" style="margin-top:1em"> Files: </wl-title>
<div style="font-size: 0.8em; color: #666; margin-bottom: 4px;">Use the "optional" checkbox on each input to mark it skippable. Save the configuration to persist.</div>
${this._inputDSInput}

<wl-title level="3" style="margin-top:1em"> Output files: </wl-title>
Expand Down Expand Up @@ -830,7 +833,8 @@ ${edResource && edResource.hasUsageNotes
hasOutputTimeInterval: this._inputTimeInterval.getResources(),
hasGrid: this._inputGrid.getResources(),
hasParameter: this._inputParameter.getResources(),
hasInput: this._inputDSInput.getResources(),
// getResources() returns hasInput items with isOptional preserved from API or form edits
hasInput: this._inputDSInput.getResources().map((ds: any) => ({ ...ds, isOptional: ds.isOptional ?? false })),
hasOutput: this._inputDSOutput.getResources(),
hasSourceCode: this._inputSourceCode.getResources(),
hasConstraint: this._inputConstraint.getResources(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export class ModelCatalogModelConfiguration extends connect(store)(
}

this._inputParameter.setResources(r.hasParameter);
// hasInput items carry isOptional from the raw API response (see CustomModelConfigurationApi.get)
this._inputDSInput.setResources(r.hasInput);
this._inputDSOutput.setResources(r.hasOutput);
this._inputSourceCode.setResources(r.hasSourceCode);
Expand Down Expand Up @@ -435,6 +436,7 @@ export class ModelCatalogModelConfiguration extends connect(store)(
${this._inputParameter}

<wl-title level="4" style="margin-top:1em"> Files: </wl-title>
<div style="font-size: 0.8em; color: #666; margin-bottom: 4px;">Inputs marked as optional can be skipped when no dataset is bound for execution.</div>
${this._inputDSInput}

<wl-title level="3" style="margin-top:1em"> Output files: </wl-title>
Expand Down Expand Up @@ -689,6 +691,7 @@ ${edResource && edResource.hasUsageNotes
${this._inputParameter}

<wl-title level="4" style="margin-top:1em"> Files: </wl-title>
<div style="font-size: 0.8em; color: #666; margin-bottom: 4px;">Use the "optional" checkbox on each input to mark it skippable. Save the configuration to persist.</div>
${this._inputDSInput}

<wl-title level="3" style="margin-top:1em"> Output files: </wl-title>
Expand Down Expand Up @@ -825,7 +828,8 @@ ${edResource && edResource.hasUsageNotes
hasGrid: this._inputGrid.getResources(),
// this ones are temporal resources
hasParameter: this._inputParameter.getResources(),
hasInput: this._inputDSInput.getResources(),
// getResources() returns hasInput items with isOptional preserved from API or form edits
hasInput: this._inputDSInput.getResources().map((ds: any) => ({ ...ds, isOptional: ds.isOptional ?? false })),
hasOutput: this._inputDSOutput.getResources(),
hasSourceCode: this._inputSourceCode.getResources(),
hasConstraint: this._inputConstraint.getResources(),
Expand Down
Loading