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

Commit6ac1bd8

Browse files
feat: display builtin apps on workspaces table (#17695)
Related to#17311<img width="1624" alt="Screenshot 2025-05-06 at 16 20 40"src="https://github.com/user-attachments/assets/932f6034-9f8a-45d7-bf8d-d330dcca683d"/>
1 parent9fe5b71 commit6ac1bd8

File tree

4 files changed

+214
-54
lines changed

4 files changed

+214
-54
lines changed

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
typeGetVSCodeHrefParams={
2+
owner:string;
3+
workspace:string;
4+
token:string;
5+
agent?:string;
6+
folder?:string;
7+
};
8+
9+
exportconstgetVSCodeHref=(
10+
app:"vscode"|"vscode-insiders",
11+
{ owner, workspace, token, agent, folder}:GetVSCodeHrefParams,
12+
)=>{
13+
constquery=newURLSearchParams({
14+
owner,
15+
workspace,
16+
url:location.origin,
17+
token,
18+
openRecent:"true",
19+
});
20+
if(agent){
21+
query.set("agent",agent);
22+
}
23+
if(folder){
24+
query.set("folder",folder);
25+
}
26+
return`${app}://coder.coder-remote/open?${query}`;
27+
};
28+
29+
typeGetTerminalHrefParams={
30+
username:string;
31+
workspace:string;
32+
agent?:string;
33+
container?:string;
34+
};
35+
36+
exportconstgetTerminalHref=({
37+
username,
38+
workspace,
39+
agent,
40+
container,
41+
}:GetTerminalHrefParams)=>{
42+
constparams=newURLSearchParams();
43+
if(container){
44+
params.append("container",container);
45+
}
46+
// Always use the primary for the terminal link. This is a relative link.
47+
return`/@${username}/${workspace}${
48+
agent ?`.${agent}` :""
49+
}/terminal?${params}`;
50+
};
51+
52+
exportconstopenAppInNewWindow=(name:string,href:string)=>{
53+
window.open(href,"_blank","width=900,height=600");
54+
};

‎site/src/modules/resources/TerminalLink/TerminalLink.tsx‎

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import{TerminalIcon}from"components/Icons/TerminalIcon";
2+
import{getTerminalHref,openAppInNewWindow}from"modules/apps/apps";
23
importtype{FC,MouseEvent}from"react";
3-
import{generateRandomString}from"utils/random";
44
import{AgentButton}from"../AgentButton";
55
import{DisplayAppNameMap}from"../AppLink/AppLink";
66

7-
constLanguage={
8-
terminalTitle:(identifier:string):string=>`Terminal -${identifier}`,
9-
};
10-
117
exportinterfaceTerminalLinkProps{
128
workspaceName:string;
139
agentName?:string;
@@ -28,26 +24,20 @@ export const TerminalLink: FC<TerminalLinkProps> = ({
2824
workspaceName,
2925
containerName,
3026
})=>{
31-
constparams=newURLSearchParams();
32-
if(containerName){
33-
params.append("container",containerName);
34-
}
35-
// Always use the primary for the terminal link. This is a relative link.
36-
consthref=`/@${userName}/${workspaceName}${
37-
agentName ?`.${agentName}` :""
38-
}/terminal?${params.toString()}`;
27+
consthref=getTerminalHref({
28+
username:userName,
29+
workspace:workspaceName,
30+
agent:agentName,
31+
container:containerName,
32+
});
3933

4034
return(
4135
<AgentButtonasChild>
4236
<a
4337
href={href}
4438
onClick={(event:MouseEvent<HTMLElement>)=>{
4539
event.preventDefault();
46-
window.open(
47-
href,
48-
Language.terminalTitle(generateRandomString(12)),
49-
"width=900,height=600",
50-
);
40+
openAppInNewWindow("Terminal",href);
5141
}}
5242
>
5343
<TerminalIcon/>

‎site/src/modules/resources/VSCodeDesktopButton/VSCodeDesktopButton.tsx‎

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { DisplayApp } from "api/typesGenerated";
55
import{VSCodeIcon}from"components/Icons/VSCodeIcon";
66
import{VSCodeInsidersIcon}from"components/Icons/VSCodeInsidersIcon";
77
import{ChevronDownIcon}from"lucide-react";
8+
import{getVSCodeHref}from"modules/apps/apps";
89
import{typeFC,useRef,useState}from"react";
910
import{AgentButton}from"../AgentButton";
1011
import{DisplayAppNameMap}from"../AppLink/AppLink";
@@ -118,21 +119,13 @@ const VSCodeButton: FC<VSCodeDesktopButtonProps> = ({
118119
setLoading(true);
119120
API.getApiKey()
120121
.then(({ key})=>{
121-
constquery=newURLSearchParams({
122+
location.href=getVSCodeHref("vscode",{
122123
owner:userName,
123124
workspace:workspaceName,
124-
url:location.origin,
125125
token:key,
126-
openRecent:"true",
126+
agent:agentName,
127+
folder:folderPath,
127128
});
128-
if(agentName){
129-
query.set("agent",agentName);
130-
}
131-
if(folderPath){
132-
query.set("folder",folderPath);
133-
}
134-
135-
location.href=`vscode://coder.coder-remote/open?${query.toString()}`;
136129
})
137130
.catch((ex)=>{
138131
console.error(ex);
@@ -163,20 +156,13 @@ const VSCodeInsidersButton: FC<VSCodeDesktopButtonProps> = ({
163156
setLoading(true);
164157
API.getApiKey()
165158
.then(({ key})=>{
166-
constquery=newURLSearchParams({
159+
location.href=getVSCodeHref("vscode-insiders",{
167160
owner:userName,
168161
workspace:workspaceName,
169-
url:location.origin,
170162
token:key,
163+
agent:agentName,
164+
folder:folderPath,
171165
});
172-
if(agentName){
173-
query.set("agent",agentName);
174-
}
175-
if(folderPath){
176-
query.set("folder",folderPath);
177-
}
178-
179-
location.href=`vscode-insiders://coder.coder-remote/open?${query.toString()}`;
180166
})
181167
.catch((ex)=>{
182168
console.error(ex);

‎site/src/pages/WorkspacesPage/WorkspacesTable.tsx‎

Lines changed: 145 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Star from "@mui/icons-material/Star";
33
importCheckboxfrom"@mui/material/Checkbox";
44
importSkeletonfrom"@mui/material/Skeleton";
55
import{templateVersion}from"api/queries/templates";
6+
import{apiKey}from"api/queries/users";
67
import{
78
cancelBuild,
89
deleteWorkspace,
@@ -19,6 +20,8 @@ import { Avatar } from "components/Avatar/Avatar";
1920
import{AvatarData}from"components/Avatar/AvatarData";
2021
import{AvatarDataSkeleton}from"components/Avatar/AvatarDataSkeleton";
2122
import{Button}from"components/Button/Button";
23+
import{VSCodeIcon}from"components/Icons/VSCodeIcon";
24+
import{VSCodeInsidersIcon}from"components/Icons/VSCodeInsidersIcon";
2225
import{InfoTooltip}from"components/InfoTooltip/InfoTooltip";
2326
import{Spinner}from"components/Spinner/Spinner";
2427
import{Stack}from"components/Stack/Stack";
@@ -49,7 +52,17 @@ import dayjs from "dayjs";
4952
importrelativeTimefrom"dayjs/plugin/relativeTime";
5053
import{useAuthenticated}from"hooks";
5154
import{useClickableTableRow}from"hooks/useClickableTableRow";
52-
import{BanIcon,PlayIcon,RefreshCcwIcon,SquareIcon}from"lucide-react";
55+
import{
56+
BanIcon,
57+
PlayIcon,
58+
RefreshCcwIcon,
59+
SquareTerminalIcon,
60+
}from"lucide-react";
61+
import{
62+
getTerminalHref,
63+
getVSCodeHref,
64+
openAppInNewWindow,
65+
}from"modules/apps/apps";
5366
import{useDashboard}from"modules/dashboard/useDashboard";
5467
import{WorkspaceAppStatus}from"modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
5568
import{WorkspaceDormantBadge}from"modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge";
@@ -59,6 +72,7 @@ import {
5972
useWorkspaceUpdate,
6073
}from"modules/workspaces/WorkspaceUpdateDialogs";
6174
import{abilitiesByWorkspaceStatus}from"modules/workspaces/actions";
75+
importtypeReactfrom"react";
6276
import{
6377
typeFC,
6478
typePropsWithChildren,
@@ -534,6 +548,10 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
534548
return(
535549
<TableCell>
536550
<divclassName="flex gap-1 justify-end">
551+
{workspace.latest_build.status==="running"&&(
552+
<WorkspaceAppsworkspace={workspace}/>
553+
)}
554+
537555
{abilities.actions.includes("start")&&(
538556
<PrimaryAction
539557
onClick={()=>startWorkspaceMutation.mutate({})}
@@ -557,18 +575,6 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
557575
</>
558576
)}
559577

560-
{abilities.actions.includes("stop")&&(
561-
<PrimaryAction
562-
onClick={()=>{
563-
stopWorkspaceMutation.mutate({});
564-
}}
565-
isLoading={stopWorkspaceMutation.isLoading}
566-
label="Stop workspace"
567-
>
568-
<SquareIcon/>
569-
</PrimaryAction>
570-
)}
571-
572578
{abilities.canCancel&&(
573579
<PrimaryAction
574580
onClick={cancelBuildMutation.mutate}
@@ -594,9 +600,9 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
594600
};
595601

596602
typePrimaryActionProps=PropsWithChildren<{
597-
onClick:()=>void;
598-
isLoading:boolean;
599603
label:string;
604+
isLoading?:boolean;
605+
onClick:()=>void;
600606
}>;
601607

602608
constPrimaryAction:FC<PrimaryActionProps>=({
@@ -626,3 +632,127 @@ const PrimaryAction: FC<PrimaryActionProps> = ({
626632
</TooltipProvider>
627633
);
628634
};
635+
636+
typeWorkspaceAppsProps={
637+
workspace:Workspace;
638+
};
639+
640+
constWorkspaceApps:FC<WorkspaceAppsProps>=({ workspace})=>{
641+
const{data:apiKeyRes}=useQuery(apiKey());
642+
consttoken=apiKeyRes?.key;
643+
644+
/**
645+
* Coder is pretty flexible and allows an enormous variety of use cases, such
646+
* as having multiple resources with many agents, but they are not common. The
647+
* most common scenario is to have one single compute resource with one single
648+
* agent containing all the apps. Lets test this getting the apps for the
649+
* first resource, and first agent - they are sorted to return the compute
650+
* resource first - and see what customers and ourselves, using dogfood, think
651+
* about that.
652+
*/
653+
constagent=workspace.latest_build.resources
654+
.filter((r)=>!r.hide)
655+
.at(0)
656+
?.agents?.at(0);
657+
if(!agent){
658+
returnnull;
659+
}
660+
661+
constbuttons:ReactNode[]=[];
662+
663+
if(agent.display_apps.includes("vscode")){
664+
buttons.push(
665+
<AppLink
666+
isLoading={!token}
667+
label="Open VSCode"
668+
href={getVSCodeHref("vscode",{
669+
owner:workspace.owner_name,
670+
workspace:workspace.name,
671+
agent:agent.name,
672+
token:apiKeyRes?.key??"",
673+
folder:agent.expanded_directory,
674+
})}
675+
>
676+
<VSCodeIcon/>
677+
</AppLink>,
678+
);
679+
}
680+
681+
if(agent.display_apps.includes("vscode_insiders")){
682+
buttons.push(
683+
<AppLink
684+
label="Open VSCode Insiders"
685+
isLoading={!token}
686+
href={getVSCodeHref("vscode-insiders",{
687+
owner:workspace.owner_name,
688+
workspace:workspace.name,
689+
agent:agent.name,
690+
token:apiKeyRes?.key??"",
691+
folder:agent.expanded_directory,
692+
})}
693+
>
694+
<VSCodeInsidersIcon/>
695+
</AppLink>,
696+
);
697+
}
698+
699+
if(agent.display_apps.includes("web_terminal")){
700+
consthref=getTerminalHref({
701+
username:workspace.owner_name,
702+
workspace:workspace.name,
703+
agent:agent.name,
704+
});
705+
buttons.push(
706+
<AppLink
707+
href={href}
708+
onClick={(e)=>{
709+
e.preventDefault();
710+
openAppInNewWindow("Terminal",href);
711+
}}
712+
label="Open Terminal"
713+
>
714+
<SquareTerminalIcon/>
715+
</AppLink>,
716+
);
717+
}
718+
719+
returnbuttons;
720+
};
721+
722+
typeAppLinkProps=PropsWithChildren<{
723+
label:string;
724+
href:string;
725+
isLoading?:boolean;
726+
onClick?:(e:React.MouseEvent<HTMLAnchorElement>)=>void;
727+
}>;
728+
729+
constAppLink:FC<AppLinkProps>=({
730+
href,
731+
isLoading,
732+
label,
733+
children,
734+
onClick,
735+
})=>{
736+
return(
737+
<TooltipProvider>
738+
<Tooltip>
739+
<TooltipTriggerasChild>
740+
<Buttonvariant="outline"size="icon-lg"asChild>
741+
<a
742+
className={isLoading ?"animate-pulse" :""}
743+
href={href}
744+
onClick={(e)=>{
745+
e.stopPropagation();
746+
onClick?.(e);
747+
}}
748+
>
749+
{children}
750+
<spanclassName="sr-only">{label}</span>
751+
</a>
752+
</Button>
753+
</TooltipTrigger>
754+
<TooltipContent>{label}</TooltipContent>
755+
</Tooltip>
756+
</TooltipProvider>
757+
);
758+
};

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp