@@ -3,6 +3,7 @@ import Star from "@mui/icons-material/Star";
3
3
import Checkbox from "@mui/material/Checkbox" ;
4
4
import Skeleton from "@mui/material/Skeleton" ;
5
5
import { templateVersion } from "api/queries/templates" ;
6
+ import { apiKey } from "api/queries/users" ;
6
7
import {
7
8
cancelBuild ,
8
9
deleteWorkspace ,
@@ -19,6 +20,8 @@ import { Avatar } from "components/Avatar/Avatar";
19
20
import { AvatarData } from "components/Avatar/AvatarData" ;
20
21
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton" ;
21
22
import { Button } from "components/Button/Button" ;
23
+ import { VSCodeIcon } from "components/Icons/VSCodeIcon" ;
24
+ import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon" ;
22
25
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip" ;
23
26
import { Spinner } from "components/Spinner/Spinner" ;
24
27
import { Stack } from "components/Stack/Stack" ;
@@ -49,7 +52,17 @@ import dayjs from "dayjs";
49
52
import relativeTime from "dayjs/plugin/relativeTime" ;
50
53
import { useAuthenticated } from "hooks" ;
51
54
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" ;
53
66
import { useDashboard } from "modules/dashboard/useDashboard" ;
54
67
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus" ;
55
68
import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge" ;
@@ -59,6 +72,7 @@ import {
59
72
useWorkspaceUpdate ,
60
73
} from "modules/workspaces/WorkspaceUpdateDialogs" ;
61
74
import { abilitiesByWorkspaceStatus } from "modules/workspaces/actions" ;
75
+ import type React from "react" ;
62
76
import {
63
77
type FC ,
64
78
type PropsWithChildren ,
@@ -534,6 +548,10 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
534
548
return (
535
549
< TableCell >
536
550
< div className = "flex gap-1 justify-end" >
551
+ { workspace . latest_build . status === "running" && (
552
+ < WorkspaceApps workspace = { workspace } />
553
+ ) }
554
+
537
555
{ abilities . actions . includes ( "start" ) && (
538
556
< PrimaryAction
539
557
onClick = { ( ) => startWorkspaceMutation . mutate ( { } ) }
@@ -557,18 +575,6 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
557
575
</ >
558
576
) }
559
577
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
-
572
578
{ abilities . canCancel && (
573
579
< PrimaryAction
574
580
onClick = { cancelBuildMutation . mutate }
@@ -594,9 +600,9 @@ const WorkspaceActionsCell: FC<WorkspaceActionsCellProps> = ({
594
600
} ;
595
601
596
602
type PrimaryActionProps = PropsWithChildren < {
597
- onClick : ( ) => void ;
598
- isLoading : boolean ;
599
603
label : string ;
604
+ isLoading ?: boolean ;
605
+ onClick : ( ) => void ;
600
606
} > ;
601
607
602
608
const PrimaryAction : FC < PrimaryActionProps > = ( {
@@ -626,3 +632,127 @@ const PrimaryAction: FC<PrimaryActionProps> = ({
626
632
</ TooltipProvider >
627
633
) ;
628
634
} ;
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