@@ -3,6 +3,7 @@ import Star from "@mui/icons-material/Star";
33import Checkbox from "@mui/material/Checkbox" ;
44import Skeleton from "@mui/material/Skeleton" ;
55import { templateVersion } from "api/queries/templates" ;
6+ import { apiKey } from "api/queries/users" ;
67import {
78 cancelBuild ,
89 deleteWorkspace ,
@@ -19,6 +20,8 @@ import { Avatar } from "components/Avatar/Avatar";
1920import { AvatarData } from "components/Avatar/AvatarData" ;
2021import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton" ;
2122import { Button } from "components/Button/Button" ;
23+ import { VSCodeIcon } from "components/Icons/VSCodeIcon" ;
24+ import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon" ;
2225import { InfoTooltip } from "components/InfoTooltip/InfoTooltip" ;
2326import { Spinner } from "components/Spinner/Spinner" ;
2427import { Stack } from "components/Stack/Stack" ;
@@ -49,7 +52,17 @@ import dayjs from "dayjs";
4952import relativeTime from "dayjs/plugin/relativeTime" ;
5053import { useAuthenticated } from "hooks" ;
5154import { 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" ;
5366import { useDashboard } from "modules/dashboard/useDashboard" ;
5467import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus" ;
5568import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge" ;
@@ -59,6 +72,7 @@ import {
5972 useWorkspaceUpdate ,
6073} from "modules/workspaces/WorkspaceUpdateDialogs" ;
6174import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions" ;
75+ import type React from "react" ;
6276import {
6377 type FC ,
6478 type PropsWithChildren ,
@@ -534,6 +548,10 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
534548 return (
535549 < TableCell >
536550 < div className = "flex gap-1 justify-end" >
551+ { workspace . latest_build . status === "running" && (
552+ < WorkspaceApps workspace = { 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
596602type PrimaryActionProps = PropsWithChildren < {
597- onClick : ( ) => void ;
598- isLoading : boolean ;
599603 label : string ;
604+ isLoading ?: boolean ;
605+ onClick : ( ) => void ;
600606} > ;
601607
602608const PrimaryAction : FC < PrimaryActionProps > = ( {
@@ -626,3 +632,127 @@ const PrimaryAction: FC<PrimaryActionProps> = ({
626632 </ TooltipProvider >
627633 ) ;
628634} ;
635+
636+ type WorkspaceAppsProps = {
637+ workspace : Workspace ;
638+ } ;
639+
640+ const WorkspaceApps : FC < WorkspaceAppsProps > = ( { workspace } ) => {
641+ const { data : apiKeyRes } = useQuery ( apiKey ( ) ) ;
642+ const token = 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+ const agent = workspace . latest_build . resources
654+ . filter ( ( r ) => ! r . hide )
655+ . at ( 0 )
656+ ?. agents ?. at ( 0 ) ;
657+ if ( ! agent ) {
658+ return null ;
659+ }
660+
661+ const buttons : 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+ const href = 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+ return buttons ;
720+ } ;
721+
722+ type AppLinkProps = PropsWithChildren < {
723+ label : string ;
724+ href : string ;
725+ isLoading ?: boolean ;
726+ onClick ?: ( e : React . MouseEvent < HTMLAnchorElement > ) => void ;
727+ } > ;
728+
729+ const AppLink : FC < AppLinkProps > = ( {
730+ href,
731+ isLoading,
732+ label,
733+ children,
734+ onClick,
735+ } ) => {
736+ return (
737+ < TooltipProvider >
738+ < Tooltip >
739+ < TooltipTrigger asChild >
740+ < Button variant = "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+ < span className = "sr-only" > { label } </ span >
751+ </ a >
752+ </ Button >
753+ </ TooltipTrigger >
754+ < TooltipContent > { label } </ TooltipContent >
755+ </ Tooltip >
756+ </ TooltipProvider >
757+ ) ;
758+ } ;
0 commit comments