mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Cluster: Label the instance switcher by base-path segment
This commit is contained in:
parent
586739b0c3
commit
92fc028d06
2 changed files with 52 additions and 2 deletions
|
|
@ -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)),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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/" });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue