Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 894e1f5

Browse files
authored
Merge branch 'main' into dk/provider-2.4.0
2 parents 899b2c7 + 9d7630b commit 894e1f5

File tree

25 files changed

+493
-438
lines changed

25 files changed

+493
-438
lines changed

site/migrate-icons.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Look for all the @mui/icons-material icons below and replace them accordinlying with the Lucide icon:
2+
3+
MUI | Lucide
4+
TaskAlt | CircleCheckBigIcon
5+
InfoOutlined | InfoIcon
6+
ErrorOutline | CircleAlertIcon
7+
8+
You should update the imports and usage.

site/src/components/BuildIcon/BuildIcon.tsx

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
import DeleteOutlined from "@mui/icons-material/DeleteOutlined";
2-
import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined";
3-
import StopOutlined from "@mui/icons-material/StopOutlined";
41
import type { WorkspaceTransition } from "api/typesGenerated";
2+
import { PlayIcon, SquareIcon, TrashIcon } from "lucide-react";
53
import type { ComponentProps } from "react";
64

7-
type SVGIcon = typeof PlayArrowOutlined;
5+
type SVGIcon = typeof PlayIcon;
86

97
type SVGIconProps = ComponentProps<SVGIcon>;
108

119
const iconByTransition: Record<WorkspaceTransition, SVGIcon> = {
12-
start: PlayArrowOutlined,
13-
stop: StopOutlined,
14-
delete: DeleteOutlined,
10+
start: PlayIcon,
11+
stop: SquareIcon,
12+
delete: TrashIcon,
1513
};
1614

1715
export const BuildIcon = (

site/src/components/InputGroup/InputGroup.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,9 @@ export const InputGroup: FC<HTMLProps<HTMLDivElement>> = (props) => {
2525
zIndex: 2,
2626
},
2727

28-
"& > *:first-child": {
28+
"& > *:first-of-type": {
2929
borderTopRightRadius: 0,
3030
borderBottomRightRadius: 0,
31-
32-
"&.MuiFormControl-root .MuiInputBase-root": {
33-
borderTopRightRadius: 0,
34-
borderBottomRightRadius: 0,
35-
},
3631
},
3732

3833
"& > *:last-child": {
@@ -45,7 +40,7 @@ export const InputGroup: FC<HTMLProps<HTMLDivElement>> = (props) => {
4540
},
4641
},
4742

48-
"& > *:not(:first-child):not(:last-child)": {
43+
"& > *:not(:first-of-type):not(:last-child)": {
4944
borderRadius: 0,
5045

5146
"&.MuiFormControl-root .MuiInputBase-root": {

site/src/components/Markdown/Markdown.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -348,19 +348,19 @@ const MarkdownGfmAlert: FC<MarkdownGfmAlertProps> = ({
348348
"[&_p]:m-0 [&_p]:mb-2",
349349

350350
alertType === "important" &&
351-
"border-highlight-purple [&_p:first-child]:text-highlight-purple",
351+
"border-highlight-purple [&_p:first-of-type]:text-highlight-purple",
352352

353353
alertType === "warning" &&
354-
"border-border-warning [&_p:first-child]:text-border-warning",
354+
"border-border-warning [&_p:first-of-type]:text-border-warning",
355355

356356
alertType === "note" &&
357-
"border-highlight-sky [&_p:first-child]:text-highlight-sky",
357+
"border-highlight-sky [&_p:first-of-type]:text-highlight-sky",
358358

359359
alertType === "tip" &&
360-
"border-highlight-green [&_p:first-child]:text-highlight-green",
360+
"border-highlight-green [&_p:first-of-type]:text-highlight-green",
361361

362362
alertType === "caution" &&
363-
"border-highlight-red [&_p:first-child]:text-highlight-red",
363+
"border-highlight-red [&_p:first-of-type]:text-highlight-red",
364364
)}
365365
>
366366
<p className="font-bold">

site/src/components/Table/Table.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ export const TableBody = React.forwardRef<
3636
<tbody
3737
ref={ref}
3838
className={cn(
39-
"[&>tr:first-child>td]:border-t [&>tr>td:first-child]:border-l",
39+
"[&>tr:first-of-type>td]:border-t [&>tr>td:first-of-type]:border-l",
4040
"[&>tr:last-child>td]:border-b [&>tr>td:last-child]:border-r",
41-
"[&>tr:first-child>td:first-child]:rounded-tl-md [&>tr:first-child>td:last-child]:rounded-tr-md",
42-
"[&>tr:last-child>td:first-child]:rounded-bl-md [&>tr:last-child>td:last-child]:rounded-br-md",
41+
"[&>tr:first-of-type>td:first-of-type]:rounded-tl-md [&>tr:first-of-type>td:last-child]:rounded-tr-md",
42+
"[&>tr:last-child>td:first-of-type]:rounded-bl-md [&>tr:last-child>td:last-child]:rounded-br-md",
4343
className,
4444
)}
4545
{...props}

site/src/modules/apps/apps.test.ts

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
MockWorkspace,
3+
MockWorkspaceAgent,
4+
MockWorkspaceApp,
5+
} from "testHelpers/entities";
6+
import { SESSION_TOKEN_PLACEHOLDER, getAppHref } from "./apps";
7+
8+
describe("getAppHref", () => {
9+
it("returns the URL without changes when external app has regular URL", () => {
10+
const externalApp = {
11+
...MockWorkspaceApp,
12+
external: true,
13+
url: "https://example.com",
14+
};
15+
const href = getAppHref(externalApp, {
16+
host: "*.apps-host.tld",
17+
path: "/path-base",
18+
agent: MockWorkspaceAgent,
19+
workspace: MockWorkspace,
20+
});
21+
expect(href).toBe(externalApp.url);
22+
});
23+
24+
it("returns the URL with the session token replaced when external app needs session token", () => {
25+
const externalApp = {
26+
...MockWorkspaceApp,
27+
external: true,
28+
url: `vscode://example.com?token=${SESSION_TOKEN_PLACEHOLDER}`,
29+
};
30+
const href = getAppHref(externalApp, {
31+
host: "*.apps-host.tld",
32+
path: "/path-base",
33+
agent: MockWorkspaceAgent,
34+
workspace: MockWorkspace,
35+
token: "user-session-token",
36+
});
37+
expect(href).toBe("vscode://example.com?token=user-session-token");
38+
});
39+
40+
it("doesn't return the URL with the session token replaced when using the HTTP protocol", () => {
41+
const externalApp = {
42+
...MockWorkspaceApp,
43+
external: true,
44+
url: `https://example.com?token=${SESSION_TOKEN_PLACEHOLDER}`,
45+
};
46+
const href = getAppHref(externalApp, {
47+
host: "*.apps-host.tld",
48+
path: "/path-base",
49+
agent: MockWorkspaceAgent,
50+
workspace: MockWorkspace,
51+
token: "user-session-token",
52+
});
53+
expect(href).toBe(externalApp.url);
54+
});
55+
56+
it("returns a path when app doesn't use a subdomain", () => {
57+
const app = {
58+
...MockWorkspaceApp,
59+
subdomain: false,
60+
};
61+
const href = getAppHref(app, {
62+
host: "*.apps-host.tld",
63+
agent: MockWorkspaceAgent,
64+
workspace: MockWorkspace,
65+
path: "/path-base",
66+
});
67+
expect(href).toBe(
68+
`/path-base/@${MockWorkspace.owner_name}/Test-Workspace.a-workspace-agent/apps/${app.slug}/`,
69+
);
70+
});
71+
72+
it("includes the command in the URL when app has a command", () => {
73+
const app = {
74+
...MockWorkspaceApp,
75+
command: "ls -la",
76+
};
77+
const href = getAppHref(app, {
78+
host: "*.apps-host.tld",
79+
agent: MockWorkspaceAgent,
80+
workspace: MockWorkspace,
81+
path: "",
82+
});
83+
expect(href).toBe(
84+
`/@${MockWorkspace.owner_name}/Test-Workspace.a-workspace-agent/terminal?command=ls%20-la`,
85+
);
86+
});
87+
88+
it("uses the subdomain when app has a subdomain", () => {
89+
const app = {
90+
...MockWorkspaceApp,
91+
subdomain: true,
92+
subdomain_name: "hellocoder",
93+
};
94+
const href = getAppHref(app, {
95+
host: "*.apps-host.tld",
96+
agent: MockWorkspaceAgent,
97+
workspace: MockWorkspace,
98+
path: "/path-base",
99+
});
100+
expect(href).toBe("http://hellocoder.apps-host.tld/");
101+
});
102+
103+
it("returns a path when app has a subdomain but no subdomain name", () => {
104+
const app = {
105+
...MockWorkspaceApp,
106+
subdomain: true,
107+
subdomain_name: undefined,
108+
};
109+
const href = getAppHref(app, {
110+
host: "*.apps-host.tld",
111+
agent: MockWorkspaceAgent,
112+
workspace: MockWorkspace,
113+
path: "/path-base",
114+
});
115+
expect(href).toBe(
116+
`/path-base/@${MockWorkspace.owner_name}/Test-Workspace.a-workspace-agent/apps/${app.slug}/`,
117+
);
118+
});
119+
});

site/src/modules/apps/apps.ts

+74-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
import type {
2+
Workspace,
3+
WorkspaceAgent,
4+
WorkspaceApp,
5+
} from "api/typesGenerated";
6+
7+
// This is a magic undocumented string that is replaced
8+
// with a brand-new session token from the backend.
9+
// This only exists for external URLs, and should only
10+
// be used internally, and is highly subject to break.
11+
export const SESSION_TOKEN_PLACEHOLDER = "$SESSION_TOKEN";
12+
113
type GetVSCodeHrefParams = {
214
owner: string;
315
workspace: string;
@@ -49,6 +61,67 @@ export const getTerminalHref = ({
4961
}/terminal?${params}`;
5062
};
5163

52-
export const openAppInNewWindow = (name: string, href: string) => {
64+
export const openAppInNewWindow = (href: string) => {
5365
window.open(href, "_blank", "width=900,height=600");
5466
};
67+
68+
export type GetAppHrefParams = {
69+
path: string;
70+
host: string;
71+
workspace: Workspace;
72+
agent: WorkspaceAgent;
73+
token?: string;
74+
};
75+
76+
export const getAppHref = (
77+
app: WorkspaceApp,
78+
{ path, token, workspace, agent, host }: GetAppHrefParams,
79+
): string => {
80+
if (isExternalApp(app)) {
81+
return needsSessionToken(app)
82+
? app.url.replaceAll(SESSION_TOKEN_PLACEHOLDER, token ?? "")
83+
: app.url;
84+
}
85+
86+
if (app.command) {
87+
// Terminal links are relative. The terminal page knows how
88+
// to select the correct workspace proxy for the websocket
89+
// connection.
90+
return `/@${workspace.owner_name}/${workspace.name}.${
91+
agent.name
92+
}/terminal?command=${encodeURIComponent(app.command)}`;
93+
}
94+
95+
if (host && app.subdomain && app.subdomain_name) {
96+
const baseUrl = `${window.location.protocol}//${host.replace(/\*/g, app.subdomain_name)}`;
97+
const url = new URL(baseUrl);
98+
url.pathname = "/";
99+
return url.toString();
100+
}
101+
102+
// The backend redirects if the trailing slash isn't included, so we add it
103+
// here to avoid extra roundtrips.
104+
return `${path}/@${workspace.owner_name}/${workspace.name}.${
105+
agent.name
106+
}/apps/${encodeURIComponent(app.slug)}/`;
107+
};
108+
109+
type ExternalWorkspaceApp = WorkspaceApp & {
110+
external: true;
111+
url: string;
112+
};
113+
114+
export const isExternalApp = (
115+
app: WorkspaceApp,
116+
): app is ExternalWorkspaceApp => {
117+
return app.external && app.url !== undefined;
118+
};
119+
120+
export const needsSessionToken = (app: ExternalWorkspaceApp) => {
121+
// HTTP links should never need the session token, since Cookies
122+
// handle sharing it when you access the Coder Dashboard. We should
123+
// never be forwarding the bare session token to other domains!
124+
const isHttp = app.url.startsWith("http");
125+
const requiresSessionToken = app.url.includes(SESSION_TOKEN_PLACEHOLDER);
126+
return requiresSessionToken && !isHttp;
127+
};

site/src/modules/apps/useAppLink.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { apiKey } from "api/queries/users";
2+
import type {
3+
Workspace,
4+
WorkspaceAgent,
5+
WorkspaceApp,
6+
} from "api/typesGenerated";
7+
import { displayError } from "components/GlobalSnackbar/utils";
8+
import { useProxy } from "contexts/ProxyContext";
9+
import type React from "react";
10+
import { useQuery } from "react-query";
11+
import {
12+
getAppHref,
13+
isExternalApp,
14+
needsSessionToken,
15+
openAppInNewWindow,
16+
} from "./apps";
17+
18+
type UseAppLinkParams = {
19+
workspace: Workspace;
20+
agent: WorkspaceAgent;
21+
};
22+
23+
export const useAppLink = (
24+
app: WorkspaceApp,
25+
{ agent, workspace }: UseAppLinkParams,
26+
) => {
27+
const label = app.display_name ?? app.slug;
28+
const { proxy } = useProxy();
29+
const { data: apiKeyResponse } = useQuery({
30+
...apiKey(),
31+
enabled: isExternalApp(app) && needsSessionToken(app),
32+
});
33+
34+
const href = getAppHref(app, {
35+
agent,
36+
workspace,
37+
token: apiKeyResponse?.key,
38+
path: proxy.preferredPathAppURL,
39+
host: proxy.preferredWildcardHostname,
40+
});
41+
42+
const onClick = (e: React.MouseEvent) => {
43+
if (!e.currentTarget.getAttribute("href")) {
44+
return;
45+
}
46+
47+
if (app.external) {
48+
// When browser recognizes the protocol and is able to navigate to the app,
49+
// it will blur away, and will stop the timer. Otherwise,
50+
// an error message will be displayed.
51+
const openAppExternallyFailedTimeout = 500;
52+
const openAppExternallyFailed = setTimeout(() => {
53+
displayError(`${label} must be installed first.`);
54+
}, openAppExternallyFailedTimeout);
55+
window.addEventListener("blur", () => {
56+
clearTimeout(openAppExternallyFailed);
57+
});
58+
}
59+
60+
switch (app.open_in) {
61+
case "slim-window": {
62+
e.preventDefault();
63+
openAppInNewWindow(href);
64+
return;
65+
}
66+
}
67+
};
68+
69+
return {
70+
href,
71+
onClick,
72+
label,
73+
hasToken: !!apiKeyResponse?.key,
74+
};
75+
};

0 commit comments

Comments
 (0)