Skip to content

Commit b1cca19

Browse files
committed
Adding a common url builder function to support absolute urls for files specified in the json map.
1 parent 069436a commit b1cca19

File tree

7 files changed

+203
-15
lines changed

7 files changed

+203
-15
lines changed

src/components/BeamlineSelect.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FileContext, executeAction } from "@diamondlightsource/cs-web-lib";
77
import { CHANGE_BEAMLINE } from "../store";
88
import { Tooltip } from "@mui/material";
99
import { BeamlineTreeStateContext } from "../App";
10+
import { buildFullyQualifiedUrl } from "../utils/urlUtils";
1011

1112
const MenuItem = styled(MuiMenuItem)({
1213
"&.Mui-disabled": {
@@ -32,9 +33,10 @@ export default function BeamlineSelect() {
3233
location: "main",
3334
description: undefined,
3435
file: {
35-
path:
36-
state.beamlines[event.target.value].host +
37-
state.beamlines[event.target.value].topLevelScreen,
36+
path: buildFullyQualifiedUrl(
37+
state.beamlines[event.target.value].host,
38+
state.beamlines[event.target.value].topLevelScreen
39+
),
3840
macros: {},
3941
defaultProtocol: "ca"
4042
}

src/components/ScreenTreeView.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { TreeViewBaseItem, TreeViewItemId } from "@mui/x-tree-view";
44
import { executeAction, FileContext } from "@diamondlightsource/cs-web-lib";
55
import { BeamlineTreeStateContext } from "../App";
66
import { MenuContext } from "../routes/MainPage";
7+
import { buildFullyQualifiedUrl } from "../utils/urlUtils";
78

89
export default function ScreenTreeView() {
910
const { state } = useContext(BeamlineTreeStateContext);
@@ -16,9 +17,13 @@ export default function ScreenTreeView() {
1617
};
1718

1819
const handleClick = (itemId: string) => {
19-
const newScreen =
20-
state.beamlines[state.currentBeamline].host +
21-
state.beamlines[state.currentBeamline].filePathIds[itemId].file;
20+
const fileMetadata =
21+
state.beamlines[state.currentBeamline].filePathIds[itemId];
22+
const newScreen = buildFullyQualifiedUrl(
23+
state.beamlines[state.currentBeamline].host,
24+
fileMetadata.file
25+
);
26+
2227
executeAction(
2328
{
2429
type: "OPEN_PAGE",

src/components/SynopticBreadcrumbs.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Breadcrumbs, Link } from "@mui/material";
55
import { executeAction, FileContext } from "@diamondlightsource/cs-web-lib";
66
import { BeamlineStateProperties } from "../store";
77
import { BeamlineTreeStateContext } from "../App";
8+
import { buildFullyQualifiedUrl } from "../utils/urlUtils";
89

910
export const SynopticBreadcrumbs = () => {
1011
const { state } = useContext(BeamlineTreeStateContext);
@@ -44,12 +45,14 @@ const handleClick =
4445
.split("/")
4546
.at(-1) as string;
4647

47-
const filepath =
48-
Object.values(currentBeamlineState.filePathIds).find(
49-
x => x.urlId === urlId
50-
)?.file ?? "";
48+
const fileMetadata = Object.values(currentBeamlineState.filePathIds).find(
49+
x => x.urlId === urlId
50+
);
51+
const newScreen = buildFullyQualifiedUrl(
52+
currentBeamlineState.host,
53+
fileMetadata?.file
54+
);
5155

52-
const newScreen = currentBeamlineState.host + filepath;
5356
executeAction(
5457
{
5558
type: "OPEN_PAGE",

src/routes/EditorPage.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { parseScreenTree } from "../utils/parser";
1111
import { executeAction, FileContext } from "@diamondlightsource/cs-web-lib";
1212
import Editor from "../components/Editor";
1313
import { useParams } from "react-router-dom";
14+
import { buildFullyQualifiedUrl } from "../utils/urlUtils";
1415

1516
/**
1617
* Displays a mock editor page with palette and Phoebus
@@ -63,7 +64,10 @@ export function EditorPage() {
6364
const newBeamlineState = newBeamlines[params.beamline];
6465
const newScreen = params.screenUrlId
6566
? newBeamlineState.filePathIds[params.screenUrlId].file
66-
: newBeamlineState.host + newBeamlineState.topLevelScreen;
67+
: buildFullyQualifiedUrl(
68+
newBeamlineState.host,
69+
newBeamlineState.topLevelScreen
70+
);
6771
executeAction(
6872
{
6973
type: "OPEN_PAGE",

src/routes/MainPage.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { RotatingLines } from "react-loader-spinner";
1616
import { SynopticBreadcrumbs } from "../components/SynopticBreadcrumbs";
1717
import { BeamlineTreeStateContext } from "../App";
1818
import { useParams } from "react-router-dom";
19+
import { buildFullyQualifiedUrl } from "../utils/urlUtils";
1920

2021
export const MenuContext = createContext<{
2122
menuOpen: boolean;
@@ -54,7 +55,7 @@ export function MainPage() {
5455
for (const [beamline, item] of Object.entries(newBeamlines)) {
5556
try {
5657
const [tree, fileIDs, firstFile] = await parseScreenTree(
57-
item.host + item.entryPoint
58+
buildFullyQualifiedUrl(item.host, item.entryPoint)
5859
);
5960
item.screenTree = tree;
6061
item.filePathIds = fileIDs;
@@ -82,8 +83,10 @@ export function MainPage() {
8283
x => x.urlId === params.screenUrlId
8384
)?.file;
8485

85-
const newScreen =
86-
newBeamlineState.host + (filepath ?? newBeamlineState.topLevelScreen);
86+
const newScreen = buildFullyQualifiedUrl(
87+
newBeamlineState.host,
88+
filepath ?? newBeamlineState.topLevelScreen
89+
);
8790

8891
executeAction(
8992
{

src/tests/utils/urlUtils.test.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildFullyQualifiedUrl,
4+
isFullyQualifiedUrl
5+
} from "../../utils/urlUtils";
6+
7+
describe("urlUtils", () => {
8+
describe("isFullyQualifiedUrl", () => {
9+
it("should return true when url is valid", async () => {
10+
const result = isFullyQualifiedUrl("https://diamond.ac.uk:4000/path1");
11+
expect(result).toEqual(true);
12+
});
13+
14+
it("should return false when url is undefined", async () => {
15+
const result = isFullyQualifiedUrl(undefined);
16+
expect(result).toEqual(false);
17+
});
18+
19+
it("should return false when url is invalid", async () => {
20+
const result = isFullyQualifiedUrl("abcde1234.ac.uk");
21+
expect(result).toEqual(false);
22+
});
23+
});
24+
25+
describe("buildFullyQualifiedUrl", () => {
26+
it("should use base url when the joined path args don't make a fully qualified URL", async () => {
27+
const baseUrl = "http://diamond.ac.uk";
28+
const args = ["path1"];
29+
30+
const result = buildFullyQualifiedUrl(baseUrl, ...args);
31+
32+
expect(result).toEqual("http://diamond.ac.uk/path1");
33+
});
34+
35+
it("should return the default base url when the path args not specified", async () => {
36+
const baseUrl = "http://diamond.ac.uk";
37+
38+
const result = buildFullyQualifiedUrl(baseUrl);
39+
40+
expect(result).toEqual("http://diamond.ac.uk/");
41+
});
42+
43+
it("should return the default base url when the only path arg is undefined", async () => {
44+
const baseUrl = "http://diamond.ac.uk";
45+
46+
const result = buildFullyQualifiedUrl(baseUrl, undefined);
47+
48+
expect(result).toEqual("http://diamond.ac.uk/");
49+
});
50+
51+
it("url encodes the path string", async () => {
52+
const baseUrl = "http://diamond.ac.uk";
53+
const args = ["path 1"];
54+
55+
const result = buildFullyQualifiedUrl(baseUrl, ...args);
56+
57+
expect(result).toEqual("http://diamond.ac.uk/path%201");
58+
});
59+
60+
it("removes trailing and leading slash", async () => {
61+
const baseUrl = "http://diamond.ac.uk/";
62+
const args = ["/path1/"];
63+
64+
const result = buildFullyQualifiedUrl(baseUrl, ...args);
65+
66+
expect(result).toEqual("http://diamond.ac.uk/path1");
67+
});
68+
69+
it("Joins multiple path args with / separator", async () => {
70+
const baseUrl = "http://diamond.ac.uk";
71+
const args = ["/path1/", "/path2", "path3/"];
72+
73+
const result = buildFullyQualifiedUrl(baseUrl, ...args);
74+
75+
expect(result).toEqual("http://diamond.ac.uk/path1/path2/path3");
76+
});
77+
78+
it("Joins multiple path args with / separator, ignores filename component of host path", async () => {
79+
const baseUrl = "http://diamond.ac.uk/path0/filename";
80+
const args = ["/path1/"];
81+
82+
const result = buildFullyQualifiedUrl(baseUrl, ...args);
83+
84+
expect(result).toEqual("http://diamond.ac.uk/path0/path1");
85+
});
86+
87+
it("Joins multiple path args with / separator, trailing slash on the url", async () => {
88+
const baseUrl = "http://diamond.ac.uk/path0/path1/";
89+
const args = ["/path10/", "/path20"];
90+
91+
const result = buildFullyQualifiedUrl(baseUrl, ...args);
92+
93+
expect(result).toEqual("http://diamond.ac.uk/path0/path1/path10/path20");
94+
});
95+
96+
it("ignores default base url if the args start with a fully qualified url", async () => {
97+
const baseUrl = "http://diamond.ac.uk";
98+
const args = ["http://ral.ac.uk/", "path1", "path2"];
99+
100+
const result = buildFullyQualifiedUrl(baseUrl, ...args);
101+
102+
expect(result).toEqual("http://ral.ac.uk/path1/path2");
103+
});
104+
105+
it("ignores default base url if the args start with a fully qualified url and even if the base Url contains a path", async () => {
106+
const baseUrl = "https://diamond.ac.uk/path0";
107+
const args = ["https://ral.ac.uk:4000/", "path1", "path2"];
108+
109+
const result = buildFullyQualifiedUrl(baseUrl, ...args);
110+
111+
expect(result).toEqual("https://ral.ac.uk:4000/path1/path2");
112+
});
113+
114+
it("ignores default base url if the args start with a fully qualified url, when base url is invalid", async () => {
115+
const args = ["http://ral.ac.uk/", "path1", "path2"];
116+
117+
const result = buildFullyQualifiedUrl("abcd", ...args);
118+
119+
expect(result).toEqual("http://ral.ac.uk/path1/path2");
120+
});
121+
122+
it("ignores default base url if the args start with a fully qualified url, when base url is undefined", async () => {
123+
const args = ["http://ral.ac.uk/", "path1", "path2"];
124+
125+
const result = buildFullyQualifiedUrl(undefined, ...args);
126+
127+
expect(result).toEqual("http://ral.ac.uk/path1/path2");
128+
});
129+
130+
it("Throws if default base url is invalid and args don't form a fully qualified url", async () => {
131+
const args = ["path1", "path2"];
132+
133+
expect(() => buildFullyQualifiedUrl("abcd", ...args)).toThrow(
134+
"Invalid base URL: abcd and args do not form a valid URL"
135+
);
136+
});
137+
});
138+
});

src/utils/urlUtils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export const isFullyQualifiedUrl = (url: string): boolean => {
2+
try {
3+
const parsed = new URL(url);
4+
return parsed.protocol === "https:" || parsed.protocol === "http:";
5+
} catch {
6+
return false;
7+
}
8+
};
9+
10+
export const buildFullyQualifiedUrl = (
11+
defaultBaseHost: string,
12+
...args: (string | undefined)[]
13+
) => {
14+
const path =
15+
args
16+
?.filter(s => s != null && s !== "")
17+
.map(s => s?.replace(/\/+$/, "").replace(/^\/+/, ""))
18+
.join("/") ?? "";
19+
20+
if (isFullyQualifiedUrl(path)) {
21+
const parsedUrl = new URL(path);
22+
return parsedUrl.toString();
23+
}
24+
25+
if (isFullyQualifiedUrl(defaultBaseHost)) {
26+
const parsedUrl = new URL(path, defaultBaseHost);
27+
return parsedUrl.toString();
28+
}
29+
30+
throw new Error(
31+
`Invalid base URL: ${defaultBaseHost} and args do not form a valid URL`
32+
);
33+
};

0 commit comments

Comments
 (0)