diff --git a/docs/configuration_file.md b/docs/configuration_file.md index 5b0eb5225..bff0f0fd6 100644 --- a/docs/configuration_file.md +++ b/docs/configuration_file.md @@ -10,10 +10,12 @@ By loading a JSON file, you can set VolView's configuration: ## Loading Configuration Files -Use the `config` URL parameter to load configuration before data files: +Include configuration files in the `urls` URL parameter. VolView recognizes +configuration JSON by its contents and applies it before displaying imported +data: ``` -https://volview.kitware.com/?config=https://example.com/config.json&urls=https://example.com/data.nrrd +https://volview.kitware.com/?urls=[https://example.com/data.nrrd,https://example.com/config.json] ``` ## View Layouts diff --git a/src/actions/loadUserFiles.ts b/src/actions/loadUserFiles.ts index eef438ff4..a798d3ebd 100644 --- a/src/actions/loadUserFiles.ts +++ b/src/actions/loadUserFiles.ts @@ -418,7 +418,6 @@ function urlsToDataSources(urls: string[], names: string[] = []): DataSource[] { type LoadUrlsParams = { urls?: string[]; names?: string[]; - config?: string[]; }; export async function loadUrls(params: UrlParams | LoadUrlsParams) { @@ -428,24 +427,17 @@ export async function loadUrls(params: UrlParams | LoadUrlsParams) { export async function loadUrlsWithOutcome( params: UrlParams | LoadUrlsParams ): Promise> { - const outcomes: LoadDataSourcesOutcome[] = []; - if (params.config) { - const configUrls = wrapInArray(params.config); - const configSources = urlsToDataSources(configUrls, []); - outcomes.push( - await loadDataSourcesWithOutcome(configSources, importDataSources) - ); + if (!params.urls) { + return { datasetIds: [], hadErrors: false }; } - if (params.urls) { - const urls = wrapInArray(params.urls); - const names = wrapInArray(params.names ?? []); - const sources = urlsToDataSources(urls, names); - outcomes.push(await loadDataSourcesWithOutcome(sources, importDataSources)); - } + const urls = wrapInArray(params.urls); + const names = wrapInArray(params.names ?? []); + const sources = urlsToDataSources(urls, names); + const outcome = await loadDataSourcesWithOutcome(sources, importDataSources); return { - datasetIds: outcomes.flatMap(({ datasetIds }) => datasetIds), - hadErrors: outcomes.some(({ hadErrors }) => hadErrors), + datasetIds: outcome.datasetIds, + hadErrors: outcome.hadErrors, }; } diff --git a/src/io/import/__tests__/processingConfigInjection.spec.ts b/src/io/import/__tests__/processingConfigInjection.spec.ts index 6f4420ead..c24dfe231 100644 --- a/src/io/import/__tests__/processingConfigInjection.spec.ts +++ b/src/io/import/__tests__/processingConfigInjection.spec.ts @@ -1,7 +1,7 @@ // Config-by-shape + origin gate, exercised through the real import pipeline. // There is no channel distinction: a provider config registers iff its origin // passes the runtime gate, no matter how it arrived -// (a `config`-role uri, a plain `urls=` file, or a dropped file). +// (a plain `urls=` file, a manifest resource, or a dropped file). import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { createPinia, setActivePinia } from 'pinia'; @@ -48,7 +48,7 @@ describe('processing config injection (config-by-shape, origin-gated)', () => { vi.restoreAllMocks(); }); - it('registers a same-origin provider from a plain urls= file (no config role)', async () => { + it('registers a same-origin provider from a plain urls= file alongside ordinary data', async () => { const [ { importDataSources }, { uriToDataSource }, @@ -59,8 +59,12 @@ describe('processing config injection (config-by-shape, origin-gated)', () => { import('@/src/processing'), ]); - // No 'config' role on the parent: registration is by shape, not channel. - const dataSource: DataSource = { + const ordinaryDataSource: DataSource = { + type: 'file', + file: jsonFile({ vertices: [[0, 0, 0]] }, 'data.json'), + fileType: 'application/json', + }; + const configDataSource: DataSource = { type: 'file', file: jsonFile( configWithProvider('/api/v1/folder/abc/volview_processing') @@ -72,7 +76,7 @@ describe('processing config injection (config-by-shape, origin-gated)', () => { ), }; - await importDataSources([dataSource]); + await importDataSources([ordinaryDataSource, configDataSource]); expect(useProcessingJobsStore().configs.size).toBe(1); }); diff --git a/src/store/__tests__/remote-save-state.spec.ts b/src/store/__tests__/remote-save-state.spec.ts index 3c8894a2b..ce2635ec4 100644 --- a/src/store/__tests__/remote-save-state.spec.ts +++ b/src/store/__tests__/remote-save-state.spec.ts @@ -75,9 +75,9 @@ describe('remote save target', () => { }); // A successful save repoints ONLY the tab's `urls=` at the returned -// `resumeUrl` so a future F5 reloads the save — no reload, and `save=`, the -// in-memory save target, and `config=` are all untouched. No `resumeUrl` (or -// an unparseable body) leaves the tab as-is. +// `resumeUrl` so a future F5 reloads the save — no reload, and `save=` plus the +// in-memory save target are untouched. No `resumeUrl` (or an unparseable body) +// leaves the tab as-is. describe('resume repoint on save', () => { beforeEach(() => { setActivePinia(createPinia()); @@ -89,7 +89,7 @@ describe('resume repoint on save', () => { window.history.replaceState(null, '', window.location.pathname); }); - it('repoints urls= to the resumeUrl and leaves the save target alone (save=/config= untouched)', async () => { + it('repoints urls= to the resumeUrl and leaves the save target alone', async () => { const resumeUrl = '/api/v1/item/session-123/volview'; vi.mocked($fetch).mockResolvedValue( new Response(JSON.stringify({ resumeUrl }), { status: 200 }) diff --git a/src/utils/urlParams.test.ts b/src/utils/urlParams.test.ts index 4bb977410..227b3ea9a 100644 --- a/src/utils/urlParams.test.ts +++ b/src/utils/urlParams.test.ts @@ -58,15 +58,20 @@ describe('normalizeUrlParams', () => { ]); }); - it('handles config and names parameters', () => { + it('handles names parameters', () => { const result = normalizeUrlParams({ - config: 'https://example.com/config.json', names: ['Image 1', 'Image 2'], }); - expect(result.config).toEqual(['https://example.com/config.json']); expect(result.names).toEqual(['Image 1', 'Image 2']); }); + it('ignores unsupported URL parameters', () => { + const result = normalizeUrlParams({ + config: 'https://example.com/config.json', + }); + expect(result).toEqual({}); + }); + it('treats relative paths as valid URLs', () => { const result = normalizeUrlParams({ urls: 'relative-path', diff --git a/src/utils/urlParams.ts b/src/utils/urlParams.ts index 01b4ca8af..aeb2bb1c0 100644 --- a/src/utils/urlParams.ts +++ b/src/utils/urlParams.ts @@ -2,15 +2,14 @@ import { UrlParams } from '@vueuse/core'; import vtkURLExtract from '@kitware/vtk.js/Common/Core/URLExtract'; import { logError } from '@/src/utils/loggers'; -// This module owns the tab's launch params (`urls=`, `names=`, `config=`, -// `save=`): both READING them at boot (readLaunchParams) and REWRITING them -// after a remote save (repointLaunchUrls). Keeping both sides here means the -// stale-`names=` interaction below stays next to the parsing it protects. +// This module owns the tab's launch params (`urls=`, `names=`, `save=`): both +// READING them at boot (readLaunchParams) and REWRITING them after a remote save +// (repointLaunchUrls). Keeping both sides here means the stale-`names=` +// interaction below stays next to the parsing it protects. type ParsedUrlParams = { urls?: string[]; names?: string[]; - config?: string[]; save?: string | string[]; }; @@ -66,21 +65,6 @@ export const normalizeUrlParams = (rawParams: UrlParams): ParsedUrlParams => { normalized.names = parseUrlArray(rawParams.names); } - if (rawParams.config) { - const configs = parseUrlArray(rawParams.config); - const validConfigs = configs.filter((url) => { - const isValid = isValidUrl(url); - if (!isValid) { - logError(new Error(`Invalid URL in config parameter: ${url}`)); - } - return isValid; - }); - - if (validConfigs.length > 0) { - normalized.config = validConfigs; - } - } - if (rawParams.save) { normalized.save = rawParams.save; } @@ -104,8 +88,8 @@ export const readLaunchParams = (): ParsedUrlParams => { // On a successful remote save the backend returns `resumeUrl` — the saved // session's load URL. Repoint ONLY the tab's `urls=` at it (so a future F5 // reloads the just-made save instead of the fresh launch manifest), via -// `history.replaceState` (no reload). `save=` and `config=` are untouched: -// every save keeps going to the launch-provided target. +// `history.replaceState` (no reload). `save=` is untouched: every save keeps +// going to the launch-provided target. export const repointLaunchUrls = (resumeUrl: string) => { const url = new URL(window.location.toString()); url.searchParams.set('urls', resumeUrl); diff --git a/tests/specs/automatic-layering.e2e.ts b/tests/specs/automatic-layering.e2e.ts index 470635287..8010fcfbf 100644 --- a/tests/specs/automatic-layering.e2e.ts +++ b/tests/specs/automatic-layering.e2e.ts @@ -15,7 +15,7 @@ describe('Automatic Layering by File Name', () => { await writeManifestToFile(config, configFileName); await volViewPage.open( - `?config=[tmp/${configFileName}]&urls=[${FETUS_DATASET.url},${FETUS_DATASET.url}]&names=[base-image.mha,base-image.layer.mha]` + `?urls=[${FETUS_DATASET.url},${FETUS_DATASET.url},tmp/${configFileName}]&names=[base-image.mha,base-image.layer.mha,${configFileName}]` ); await volViewPage.waitForViews(); diff --git a/tests/specs/cine-rendering.e2e.ts b/tests/specs/cine-rendering.e2e.ts index fe98e69d0..a27364521 100644 --- a/tests/specs/cine-rendering.e2e.ts +++ b/tests/specs/cine-rendering.e2e.ts @@ -61,7 +61,7 @@ async function openCineDatasetWithConfig(config: unknown, configName: string) { const configFileName = `${configName}-${Date.now()}.json`; await writeManifestToFile(config, configFileName); await volViewPage.open( - `?config=[tmp/${configFileName}]&urls=[tmp/${CINE_US_DATASET.name}]` + `?urls=[tmp/${CINE_US_DATASET.name},tmp/${configFileName}]` ); await volViewPage.waitForViews(); const notifications = await volViewPage.getNotificationsCount(); diff --git a/tests/specs/configTestUtils.ts b/tests/specs/configTestUtils.ts index 296d7769c..74724384d 100644 --- a/tests/specs/configTestUtils.ts +++ b/tests/specs/configTestUtils.ts @@ -109,9 +109,9 @@ export const openConfigAndDataset = async ( await writeManifestToFile(config, configFileName); await volViewPage.open( - `?config=[tmp/${configFileName}]&urls=${dataset.url}&names=${ + `?urls=[${dataset.url},tmp/${configFileName}]&names=[${ dataset.name ?? '' - }` + },${configFileName}]` ); await volViewPage.waitForViews(); }; diff --git a/tests/specs/different-direction-labelmap.e2e.ts b/tests/specs/different-direction-labelmap.e2e.ts index b755c0cf7..04c0f685a 100644 --- a/tests/specs/different-direction-labelmap.e2e.ts +++ b/tests/specs/different-direction-labelmap.e2e.ts @@ -41,7 +41,7 @@ describe('Labelmap with different direction matrix', () => { fs.unlinkSync(configFilePath); }); - const urlParams = `?urls=[tmp/${manifestFileName}]&config=[tmp/${configFileName}]`; + const urlParams = `?urls=[tmp/${manifestFileName},tmp/${configFileName}]`; await volViewPage.open(urlParams); await volViewPage.waitForViews(); const notifications = await volViewPage.getNotificationsCount();