Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitf897981

Browse files
chore: extract app access logic for reuse (#17724)
We are starting to add app links in many places in the UI, and to makeit consistent, this PR extracts the most core logic into themodules/apps for reuse.Related to#17311
1 parent2696926 commitf897981

File tree

10 files changed

+457
-397
lines changed

10 files changed

+457
-397
lines changed

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

Lines changed: 119 additions & 0 deletions
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+
constexternalApp={
11+
...MockWorkspaceApp,
12+
external:true,
13+
url:"https://example.com",
14+
};
15+
consthref=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+
constexternalApp={
26+
...MockWorkspaceApp,
27+
external:true,
28+
url:`vscode://example.com?token=${SESSION_TOKEN_PLACEHOLDER}`,
29+
};
30+
consthref=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+
constexternalApp={
42+
...MockWorkspaceApp,
43+
external:true,
44+
url:`https://example.com?token=${SESSION_TOKEN_PLACEHOLDER}`,
45+
};
46+
consthref=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+
constapp={
58+
...MockWorkspaceApp,
59+
subdomain:false,
60+
};
61+
consthref=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+
constapp={
74+
...MockWorkspaceApp,
75+
command:"ls -la",
76+
};
77+
consthref=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+
constapp={
90+
...MockWorkspaceApp,
91+
subdomain:true,
92+
subdomain_name:"hellocoder",
93+
};
94+
consthref=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+
constapp={
105+
...MockWorkspaceApp,
106+
subdomain:true,
107+
subdomain_name:undefined,
108+
};
109+
consthref=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

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
importtype{
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+
exportconstSESSION_TOKEN_PLACEHOLDER="$SESSION_TOKEN";
12+
113
typeGetVSCodeHrefParams={
214
owner:string;
315
workspace:string;
@@ -49,6 +61,73 @@ export const getTerminalHref = ({
4961
}/terminal?${params}`;
5062
};
5163

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

‎site/src/modules/apps/useAppLink.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import{apiKey}from"api/queries/users";
2+
importtype{
3+
Workspace,
4+
WorkspaceAgent,
5+
WorkspaceApp,
6+
}from"api/typesGenerated";
7+
import{displayError}from"components/GlobalSnackbar/utils";
8+
import{useProxy}from"contexts/ProxyContext";
9+
importtypeReactfrom"react";
10+
import{useQuery}from"react-query";
11+
import{
12+
getAppHref,
13+
isExternalApp,
14+
needsSessionToken,
15+
openAppInNewWindow,
16+
}from"./apps";
17+
18+
typeUseAppLinkParams={
19+
workspace:Workspace;
20+
agent:WorkspaceAgent;
21+
};
22+
23+
exportconstuseAppLink=(
24+
app:WorkspaceApp,
25+
{ agent, workspace}:UseAppLinkParams,
26+
)=>{
27+
constlabel=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+
consthref=getAppHref(app,{
35+
agent,
36+
workspace,
37+
token:apiKeyResponse?.key??"",
38+
path:proxy.preferredPathAppURL,
39+
host:proxy.preferredWildcardHostname,
40+
});
41+
42+
constonClick=(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+
constopenAppExternallyFailedTimeout=500;
52+
constopenAppExternallyFailed=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)

[8]ページ先頭

©2009-2025 Movatter.jp