Skip to content
Open
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: 6 additions & 0 deletions docs/ViewersConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ viewers:

```

#### Per-user override of the Neuroglancer URL

When a deployment overrides Neuroglancer's URL with `instance_template_url` (for example, to point at an internal Neuroglancer instance), individual users can still switch their own links back to the manifest default (the external Neuroglancer at `neuroglancer-demo.appspot.com`) from **Preferences → Neuroglancer**.

This choice is a per-user preference (`viewerUrlSources`); it does not change the deployment configuration and only affects the user who sets it. The option only appears when the deployment actually overrides the Neuroglancer URL — if the configured URL already equals the manifest default, there is nothing to switch between and no control is shown.

### Add a custom viewer

To add a new viewer, create a capability manifest YAML file, host it at a URL, and reference it in the config:
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/__tests__/unitTests/viewerUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, test } from 'vitest';

import { resolveViewerTemplate } from '@/utils/viewerUrl';
import type { ValidViewer } from '@/contexts/ViewersContext';

const viewer = {
key: 'neuroglancer',
displayName: 'Neuroglancer',
urlTemplate: 'https://ng.internal.example.org/#!',
manifestTemplateUrl: 'https://neuroglancer-demo.appspot.com/#!',
logoPath: '',
label: 'View in Neuroglancer',
manifest: {} as ValidViewer['manifest']
} satisfies ValidViewer;

describe('resolveViewerTemplate', () => {
test('defaults to the configured template when no source is set', () => {
expect(resolveViewerTemplate(viewer, undefined)).toBe(viewer.urlTemplate);
expect(resolveViewerTemplate(viewer, 'configured')).toBe(
viewer.urlTemplate
);
});

test('uses the manifest default when source is "manifest"', () => {
expect(resolveViewerTemplate(viewer, 'manifest')).toBe(
viewer.manifestTemplateUrl
);
});

test('falls back to the configured template when manifest default is empty', () => {
const noManifest = { ...viewer, manifestTemplateUrl: '' };
expect(resolveViewerTemplate(noManifest, 'manifest')).toBe(
noManifest.urlTemplate
);
});

test('uses the custom URL when provided', () => {
expect(
resolveViewerTemplate(viewer, { custom: 'https://custom.example/#!' })
).toBe('https://custom.example/#!');
});

test('falls back to the configured template for an empty custom URL', () => {
expect(resolveViewerTemplate(viewer, { custom: '' })).toBe(
viewer.urlTemplate
);
});
});
3 changes: 3 additions & 0 deletions frontend/src/components/NGLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import FgDialog from '@/components/ui/Dialogs/FgDialog';
import { useNGLinkContext } from '@/contexts/NGLinkContext';
import type { CreateNGLinkPayload, NGLink } from '@/queries/ngLinkQueries';
import { copyToClipboard } from '@/utils/copyText';
import { useDefaultNeuroglancerBaseUrl } from '@/hooks/useDefaultNeuroglancerBaseUrl';
import FgButton from './designSystem/atoms/FgButton';

export default function NGLinks() {
Expand All @@ -22,6 +23,7 @@ export default function NGLinks() {
const [showDialog, setShowDialog] = useState(false);
const [editItem, setEditItem] = useState<NGLink | undefined>(undefined);
const [deleteItem, setDeleteItem] = useState<NGLink | undefined>(undefined);
const defaultBaseUrl = useDefaultNeuroglancerBaseUrl();

const handleOpenCreate = () => {
setEditItem(undefined);
Expand Down Expand Up @@ -134,6 +136,7 @@ export default function NGLinks() {
</div>
{showDialog ? (
<NGLinkDialog
defaultBaseUrl={defaultBaseUrl}
editItem={editItem}
onClose={handleClose}
onCreate={handleCreate}
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@ export default function Preferences() {
<Typography className="font-semibold mb-4" type="lead">
Data links
</Typography>
<div className="flex flex-col gap-4 pl-4 max-w-md">
<div className="pl-4 max-w-md">
<DataLinkOptions />
</div>
</div>

<div>
<Typography className="font-semibold mb-4" type="lead">
Neuroglancer links
</Typography>
<div className="pl-4 max-w-md">
<NeuroglancerOptions />
</div>
</div>
Expand Down
34 changes: 25 additions & 9 deletions frontend/src/components/ui/Dialogs/NGLinkDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
validateJsonState,
constructNeuroglancerUrl
} from '@/utils';
import { DEFAULT_NEUROGLANCER_BASE_URL } from '@/hooks/useDefaultNeuroglancerBaseUrl';
import FgButton from '@/components/designSystem/atoms/FgButton';
import FgFormField from '@/components/designSystem/molecules/FgFormField';
import FgInput from '@/components/designSystem/atoms/formElements/FgInput';
Expand All @@ -27,17 +28,22 @@ type NGLinkDialogProps = {
readonly onCreate?: (payload: CreateNGLinkPayload) => Promise<void>;
readonly onUpdate?: (payload: UpdateNGLinkPayload) => Promise<void>;
readonly editItem?: NGLink;
/**
* Base URL a new short link defaults to, resolved from the deployment config
* and the user's Neuroglancer URL preference by the parent. Falls back to the
* external default when not provided.
*/
readonly defaultBaseUrl?: string;
};

const DEFAULT_BASE_URL = 'https://neuroglancer-demo.appspot.com/';

export default function NGLinkDialog({
open,
pending,
onClose,
onCreate,
onUpdate,
editItem
editItem,
defaultBaseUrl = DEFAULT_NEUROGLANCER_BASE_URL
}: NGLinkDialogProps) {
const isEditMode = !!editItem;
const urlInputRef = useRef<HTMLInputElement>(null);
Expand All @@ -46,7 +52,7 @@ export default function NGLinkDialog({
const [inputMode, setInputMode] = useState<'url' | 'state'>('url');
const [neuroglancerUrl, setNeuroglancerUrl] = useState('');
const [stateJson, setStateJson] = useState('');
const [baseUrl, setBaseUrl] = useState(DEFAULT_BASE_URL);
const [baseUrl, setBaseUrl] = useState(defaultBaseUrl);
const [shortName, setShortName] = useState('');
const [title, setTitle] = useState('');
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -78,12 +84,22 @@ export default function NGLinkDialog({
setShortName('');
setTitle('');
setStateJson('');
setBaseUrl(DEFAULT_BASE_URL);
setUrlValidationError(null);
setStateValidationError(null);
}
}, [editItem]);

// When creating a new link, keep the base URL aligned with the resolved
// default as it becomes available (viewers finishing loading) or changes
// (preference update). Owns the create-mode base URL so the reset above
// stays keyed on editItem alone and never clears a half-filled form. Does
// not run in edit mode, so existing links keep their stored base URL.
useEffect(() => {
if (!editItem) {
setBaseUrl(defaultBaseUrl);
}
}, [defaultBaseUrl, editItem]);

// Auto-select the URL text once after it's populated in edit mode
useEffect(() => {
if (shouldAutoSelectUrl.current && urlInputRef.current) {
Expand Down Expand Up @@ -131,7 +147,7 @@ export default function NGLinkDialog({
if (!isEditMode) {
setNeuroglancerUrl('');
setStateJson('');
setBaseUrl(DEFAULT_BASE_URL);
setBaseUrl(defaultBaseUrl);
}
setUrlValidationError(null);
setStateValidationError(null);
Expand Down Expand Up @@ -172,7 +188,7 @@ export default function NGLinkDialog({
setInputMode('url');
setNeuroglancerUrl('');
setStateJson('');
setBaseUrl(DEFAULT_BASE_URL);
setBaseUrl(defaultBaseUrl);
setShortName('');
setTitle('');
onClose();
Expand Down Expand Up @@ -294,7 +310,7 @@ export default function NGLinkDialog({
autoFocus
id="neuroglancer-url"
onChange={handleUrlChange}
placeholder="https://neuroglancer-demo.appspot.com/#!{...}"
placeholder={`${defaultBaseUrl}#!{...}`}
ref={urlInputRef}
size="lg"
type="text"
Expand Down Expand Up @@ -328,7 +344,7 @@ export default function NGLinkDialog({
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setBaseUrl(e.target.value)
}
placeholder="https://neuroglancer-demo.appspot.com/"
placeholder={defaultBaseUrl}
size="lg"
type="text"
value={baseUrl}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function DataLinkOptions({
};

return (
<>
<div className="space-y-4">
<FgSwitch
checked={automaticOption.checked}
id={automaticOption.id}
Expand All @@ -83,6 +83,6 @@ export default function DataLinkOptions({
showState
/>
{hideSubpathMode ? null : <SubpathModeOptions />}
</>
</div>
);
}
Loading
Loading