@@ -20,11 +20,21 @@ import { Workspace, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v
2020import { Button } from "@podkit/buttons/Button" ;
2121import { VideoCarousel } from "./VideoCarousel" ;
2222import { BlogBanners } from "./BlogBanners" ;
23- import { BookOpen , Code } from "lucide-react" ;
23+ import { Book , BookOpen , Building , Code , GraduationCap } from "lucide-react" ;
2424import { ReactComponent as GitpodStrokedSVG } from "../icons/gitpod-stroked.svg" ;
2525import { isGitpodIo } from "../utils" ;
2626import PersonalizedContent from "./PersonalizedContent" ;
2727import { useListenToWorkspacesWSMessages as useListenToWorkspacesStatusUpdates } from "../data/workspaces/listen-to-workspace-ws-messages" ;
28+ import { Subheading } from "@podkit/typography/Headings" ;
29+ import { useCurrentOrg } from "../data/organizations/orgs-query" ;
30+ import { Link } from "react-router-dom" ;
31+ import { useOrgSettingsQuery } from "../data/organizations/org-settings-query" ;
32+ import Modal , { ModalBaseFooter , ModalBody , ModalHeader } from "../components/Modal" ;
33+ import { VideoSection } from "../onboarding/VideoSection" ;
34+ import { trackVideoClick } from "../Analytics" ;
35+ import { useSuggestedRepositories } from "../data/git-providers/suggested-repositories-query" ;
36+ import { useUserLoader } from "../hooks/use-user-loader" ;
37+ import { cn } from "@podkit/lib/cn" ;
2838
2939const WorkspacesPage : FunctionComponent = ( ) => {
3040 const [ limit , setLimit ] = useState ( 50 ) ;
@@ -36,6 +46,11 @@ const WorkspacesPage: FunctionComponent = () => {
3646 const deleteInactiveWorkspaces = useDeleteInactiveWorkspacesMutation ( ) ;
3747 useListenToWorkspacesStatusUpdates ( ) ;
3848
49+ const { data : org } = useCurrentOrg ( ) ;
50+ const { data : orgSettings } = useOrgSettingsQuery ( ) ;
51+
52+ const { user } = useUserLoader ( ) ;
53+
3954 const { toast } = useToast ( ) ;
4055
4156 // Sort workspaces into active/inactive groups
@@ -54,6 +69,25 @@ const WorkspacesPage: FunctionComponent = () => {
5469 } ;
5570 } , [ data , limit ] ) ;
5671
72+ const handlePlay = ( ) => {
73+ trackVideoClick ( "create-new-workspace" ) ;
74+ } ;
75+
76+ const { data : suggestedRepos } = useSuggestedRepositories ( { excludeConfigurations : false } ) ;
77+
78+ const recentRepos = useMemo ( ( ) => {
79+ return (
80+ suggestedRepos
81+ ?. filter ( ( repo ) => {
82+ const autostartMatch = user ?. workspaceAutostartOptions . find ( ( option ) => {
83+ return option . cloneUrl . includes ( repo . url ) ;
84+ } ) ;
85+ return autostartMatch ;
86+ } )
87+ . slice ( 0 , 3 ) ?? [ ]
88+ ) ;
89+ } , [ suggestedRepos , user ] ) ;
90+
5791 const { filteredActiveWorkspaces, filteredInactiveWorkspaces } = useMemo ( ( ) => {
5892 const filteredActiveWorkspaces = activeWorkspaces . filter (
5993 ( info ) =>
@@ -90,9 +124,96 @@ const WorkspacesPage: FunctionComponent = () => {
90124 } catch ( e ) { }
91125 } , [ deleteInactiveWorkspaces , inactiveWorkspaces , toast ] ) ;
92126
127+ const [ isVideoModalVisible , setVideoModalVisible ] = useState ( false ) ;
128+ const handleVideoModalClose = useCallback ( ( ) => {
129+ setVideoModalVisible ( false ) ;
130+ } , [ ] ) ;
131+
93132 return (
94133 < >
95- < Header title = "Workspaces" subtitle = "Manage recent and stopped workspaces." />
134+ < Header
135+ title = "Workspaces"
136+ subtitle = "Manage, start and stop your personal development environments in the cloud."
137+ />
138+
139+ < Subheading className = "font-semibold text-pk-content-primary mt-4 mb-2 lg:px-28 px-4" >
140+ Getting started
141+ </ Subheading >
142+
143+ < div className = "flex flex-wrap gap-5 lg:px-28 px-4" >
144+ < Card onClick = { ( ) => setVideoModalVisible ( true ) } >
145+ < GraduationCap className = "flex-shrink-0" size = { 24 } />
146+ < div >
147+ < CardTitle > Learn how Gitpod works</ CardTitle >
148+ < CardDescription >
149+ We’ve put together resources for you to get the most our of Gitpod.
150+ </ CardDescription >
151+ </ div >
152+ </ Card >
153+ { orgSettings ?. onboardingSettings ?. internalLink ? (
154+ < Card href = { orgSettings . onboardingSettings . internalLink } isLinkExternal >
155+ < Building className = "flex-shrink-0" size = { 24 } />
156+ < div >
157+ < CardTitle > Learn more about Gitpod at { org ?. name } </ CardTitle >
158+ < CardDescription >
159+ Read through the internal Gitpod landing page of your organization.
160+ </ CardDescription >
161+ </ div >
162+ </ Card >
163+ ) : (
164+ < Card href = { "/new?showExamples=true" } >
165+ < Code className = "flex-shrink-0" size = { 24 } />
166+ < div >
167+ < CardTitle > Open a sample repository</ CardTitle >
168+ < CardDescription > Explore a sample repository to quickly experience Gitpod.</ CardDescription >
169+ </ div >
170+ </ Card >
171+ ) }
172+ < Card href = "https://www.gitpod.io/docs/introduction" isLinkExternal >
173+ < Book className = "flex-shrink-0" size = { 24 } />
174+ < div >
175+ < CardTitle > Visit the docs</ CardTitle >
176+ < CardDescription > We have extensive documentation to help if you get stuck.</ CardDescription >
177+ </ div >
178+ </ Card >
179+ </ div >
180+
181+ < Subheading className = "font-semibold text-pk-content-primary pt-8 mb-2 lg:px-28 px-4" > Suggested</ Subheading >
182+
183+ < div className = "flex flex-wrap gap-5 lg:px-28 px-4" >
184+ { recentRepos . map ( ( repo ) => (
185+ < Card key = { repo . url } href = { `/new#${ repo . url } ` } className = "border-[#D79A45] border" >
186+ < div >
187+ < CardTitle > { repo . configurationName || repo . repoName } </ CardTitle >
188+ < CardDescription > { repo . url } </ CardDescription >
189+ </ div >
190+ </ Card >
191+ ) ) }
192+ </ div >
193+
194+ < Modal
195+ visible = { isVideoModalVisible }
196+ onClose = { handleVideoModalClose }
197+ containerClassName = "min-[576px]:max-w-[600px]"
198+ >
199+ < ModalHeader > Demo video</ ModalHeader >
200+ < ModalBody >
201+ < div className = "flex flex-row items-center justify-center" >
202+ < VideoSection
203+ metadataVideoTitle = "Gitpod demo"
204+ playbackId = "m01BUvCkTz7HzQKFoIcQmK00Rx5laLLoMViWBstetmvLs"
205+ poster = "https://i.ytimg.com/vi_webp/1ZBN-b2cIB8/maxresdefault.webp"
206+ playerProps = { { onPlay : handlePlay , defaultHiddenCaptions : true } }
207+ className = "w-[535px] rounded-xl"
208+ />
209+ </ div >
210+ </ ModalBody >
211+ < ModalBaseFooter >
212+ < Button variant = "secondary" onClick = { handleVideoModalClose } >
213+ Close
214+ </ Button >
215+ </ ModalBaseFooter >
216+ </ Modal >
96217
97218 { deleteModalVisible && (
98219 < ConfirmationModal
@@ -237,6 +358,52 @@ const WorkspacesPage: FunctionComponent = () => {
237358
238359export default WorkspacesPage ;
239360
361+ const CardTitle = ( { children } : { children : React . ReactNode } ) => {
362+ return < span className = "text-lg font-semibold text-pk-content-primary" > { children } </ span > ;
363+ } ;
364+ const CardDescription = ( { children } : { children : React . ReactNode } ) => {
365+ return < p className = "text-pk-content-secondary" > { children } </ p > ;
366+ } ;
367+ type CardProps = {
368+ children : React . ReactNode ;
369+ href ?: string ;
370+ isLinkExternal ?: boolean ;
371+ className ?: string ;
372+ onClick ?: ( ) => void ;
373+ } ;
374+ const Card = ( { children, href, isLinkExternal, className : classNameFromProps , onClick } : CardProps ) => {
375+ const className = cn (
376+ "bg-pk-surface-secondary flex gap-3 py-4 px-5 flex-grow basis-[300px] sm:basis-[45%] lg:basis-[30%] rounded-xl max-w-[400px] text-left" ,
377+ classNameFromProps ,
378+ ) ;
379+
380+ if ( href && isLinkExternal ) {
381+ return (
382+ < a href = { href } className = { className } target = "_blank" rel = "noreferrer" >
383+ { children }
384+ </ a >
385+ ) ;
386+ }
387+
388+ if ( href ) {
389+ return (
390+ < Link to = { href } className = { className } >
391+ { children }
392+ </ Link >
393+ ) ;
394+ }
395+
396+ if ( onClick ) {
397+ return (
398+ < button className = { className } onClick = { onClick } >
399+ { children }
400+ </ button >
401+ ) ;
402+ }
403+
404+ return < div className = { className } > { children } </ div > ;
405+ } ;
406+
240407const sortWorkspaces = ( a : Workspace , b : Workspace ) => {
241408 const result = workspaceActiveDate ( b ) . localeCompare ( workspaceActiveDate ( a ) ) ;
242409 if ( result === 0 ) {
@@ -247,7 +414,7 @@ const sortWorkspaces = (a: Workspace, b: Workspace) => {
247414} ;
248415
249416/**
250- * Given a WorkspaceInfo, return a ISO string of the last related activitiy
417+ * Given a WorkspaceInfo, return a ISO string of the last related activity
251418 */
252419function workspaceActiveDate ( info : Workspace ) : string {
253420 return info . status ! . phase ! . lastTransitionTime ! . toDate ( ) . toISOString ( ) ;
0 commit comments