Cluster: Label the instance switcher by base-path segment

This commit is contained in:
Michael Mayer 2026-06-03 19:22:29 +00:00
parent 586739b0c3
commit 92fc028d06
2 changed files with 52 additions and 2 deletions

View file

@ -39,6 +39,31 @@ export const InstanceIdentityKeys = [InstanceUrlKey, InstanceTitleKey];
// safeWindow returns the browser window if available, else null.
const safeWindow = () => (typeof window === "undefined" ? null : window);
// basePathSegment returns the last non-empty path segment of a URL (e.g.
// "https://app.example.com/i/pro-1/" -> "pro-1"), or "" when the URL is invalid
// or root-pathed.
function basePathSegment(url) {
if (typeof url !== "string" || url === "") {
return "";
}
let pathname;
try {
pathname = new URL(url, safeWindow()?.location?.origin || "http://localhost/").pathname;
} catch {
return "";
}
const segments = pathname.split("/").filter((s) => s !== "");
return segments.length ? segments[segments.length - 1] : "";
}
// instanceLabel derives a distinctive switcher label for an instance: its base
// path segment when present (same-origin instances differ only by path and
// commonly share a generic title like "PhotoPrism"), else the recorded title,
// else the URL.
export function instanceLabel(url, title) {
return basePathSegment(url) || title || url || "";
}
// persistInstanceIdentity records this instance's SiteUrl and display title in
// the given (namespaced) store, so other instances on the same origin can list
// it in the navigation instance switcher. No-op without a URL or usable store.
@ -105,7 +130,7 @@ export function listReachableInstances(options) {
instances.push({
namespace,
url,
title: store.getItem(prefix + InstanceTitleKey) || url,
title: instanceLabel(url, store.getItem(prefix + InstanceTitleKey)),
});
});
});

View file

@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import StorageShim from "node-storage-shim";
import { buildNamespace, createNamespacedStorage } from "common/storage";
import { persistInstanceIdentity, listReachableInstances, InstanceIdentityKeys } from "common/instances";
import { persistInstanceIdentity, listReachableInstances, instanceLabel, InstanceIdentityKeys } from "common/instances";
// seedInstance writes a peer instance's session token and identity into a shared store.
const seedInstance = (store, namespace, { token = "tok", url, title } = {}) => {
@ -50,6 +50,23 @@ describe("common/instances", () => {
});
});
describe("instanceLabel", () => {
it("prefers the last base-path segment", () => {
expect(instanceLabel("https://app.example.com/i/pro-1/", "PhotoPrism")).toBe("pro-1");
expect(instanceLabel("https://app.example.com/instance/foo", "PhotoPrism")).toBe("foo");
});
it("falls back to the title for root-pathed URLs", () => {
expect(instanceLabel("https://pro-1.example.com/", "Pro One")).toBe("Pro One");
});
it("falls back to the url when no title and no path segment", () => {
expect(instanceLabel("https://pro-1.example.com/", "")).toBe("https://pro-1.example.com/");
});
it("returns empty for invalid input", () => {
expect(instanceLabel("", "")).toBe("");
expect(instanceLabel(null, null)).toBe("");
});
});
describe("listReachableInstances", () => {
it("returns peer instances excluding the current namespace", () => {
const store = new StorageShim();
@ -66,6 +83,14 @@ describe("common/instances", () => {
const result = listReachableInstances({ currentNamespace: "ns-pro-1", storage: store });
expect(result[0].title).toBe("https://pro-2.example.com/");
});
it("labels same-origin path-based instances by their distinctive base-path segment", () => {
// Both share the generic title "PhotoPrism"; the path segment disambiguates.
const store = new StorageShim();
seedInstance(store, "ns-pro-1", { url: "https://app.example.com/i/pro-1/", title: "PhotoPrism" });
seedInstance(store, "ns-pro-2", { url: "https://app.example.com/i/pro-2/", title: "PhotoPrism" });
const result = listReachableInstances({ currentNamespace: "ns-pro-1", storage: store });
expect(result[0]).toMatchObject({ url: "https://app.example.com/i/pro-2/", title: "pro-2" });
});
it("ignores namespaces that have a session but no recorded identity", () => {
const store = new StorageShim();
seedInstance(store, "ns-pro-1", { url: "https://pro-1.example.com/" });