Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/configuration_file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 8 additions & 16 deletions src/actions/loadUserFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -428,24 +427,17 @@ export async function loadUrls(params: UrlParams | LoadUrlsParams) {
export async function loadUrlsWithOutcome(
params: UrlParams | LoadUrlsParams
): Promise<Pick<LoadDataSourcesOutcome, 'datasetIds' | 'hadErrors'>> {
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,
};
}

Expand Down
14 changes: 9 additions & 5 deletions src/io/import/__tests__/processingConfigInjection.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 },
Expand All @@ -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')
Expand All @@ -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);
});
Expand Down
8 changes: 4 additions & 4 deletions src/store/__tests__/remote-save-state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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 })
Expand Down
11 changes: 8 additions & 3 deletions src/utils/urlParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 6 additions & 22 deletions src/utils/urlParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
};

Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion tests/specs/automatic-layering.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion tests/specs/cine-rendering.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions tests/specs/configTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};
2 changes: 1 addition & 1 deletion tests/specs/different-direction-labelmap.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading