Skip to content

Commit 95c3808

Browse files
committed
Enterprise workspace list overhaul
Todo: - [ ] only enable on dedicated installs - [ ] add org setting for org-wide suggestions - [ ] possibly adopt `Recommended` badges from figma - [ ] open configurationId instead of cloneUrl if possible
1 parent c020e89 commit 95c3808

File tree

3 files changed

+181
-30
lines changed

3 files changed

+181
-30
lines changed

components/dashboard/src/components/Modal.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Props = {
2929
autoFocus?: boolean;
3030
disableFocusLock?: boolean;
3131
className?: string;
32+
containerClassName?: string;
3233
disabled?: boolean;
3334
onClose: () => void;
3435
onSubmit?: () => void | Promise<void>;
@@ -44,6 +45,7 @@ export const Modal: FC<Props> = ({
4445
autoFocus = false,
4546
disableFocusLock = false,
4647
className,
48+
containerClassName,
4749
disabled = false,
4850
onClose,
4951
onSubmit,
@@ -87,6 +89,7 @@ export const Modal: FC<Props> = ({
8789
"pointer-events-none relative",
8890
"h-dvh w-auto", // small screens
8991
"min-[576px]:mx-auto min-[576px]:mt-7 min-[576px]:h-[calc(100%-3.5rem)] min-[576px]:max-w-[500px]", // large screens
92+
containerClassName,
9093
)}
9194
>
9295
<FocusOn

components/dashboard/src/workspaces/EmptyWorkspacesContent.tsx

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,23 @@
66

77
import { LinkButton } from "@podkit/buttons/LinkButton";
88
import { Heading2, Subheading } from "@podkit/typography/Headings";
9-
import { trackVideoClick } from "../Analytics";
10-
11-
import { VideoSection } from "../onboarding/VideoSection";
9+
import { StartWorkspaceModalKeyBinding } from "../App";
1210

1311
export const EmptyWorkspacesContent = () => {
14-
const handlePlay = () => {
15-
trackVideoClick("create-new-workspace");
16-
};
17-
1812
return (
1913
<div className="app-container flex flex-col space-y-2">
2014
<div className="px-6 mt-16 flex flex-col xl:flex-row items-center justify-center gap-x-14 gap-y-10 min-h-96 min-w-96">
21-
<VideoSection
22-
metadataVideoTitle="Gitpod demo"
23-
playbackId="m01BUvCkTz7HzQKFoIcQmK00Rx5laLLoMViWBstetmvLs"
24-
poster="https://i.ytimg.com/vi_webp/1ZBN-b2cIB8/maxresdefault.webp"
25-
playerProps={{ onPlay: handlePlay, defaultHiddenCaptions: true }}
26-
className="w-[535px] rounded-xl"
27-
/>
28-
<div className="flex flex-col items-center xl:items-start justify-center">
29-
<Heading2 className="mb-4 !font-semibold !text-lg">Create your first workspace</Heading2>
15+
<div className="flex flex-col items-center text-center justify-center">
16+
<Heading2 className="!font-semibold !text-lg">No workspaces</Heading2>
3017
<Subheading className="max-w-xs xl:text-left text-center">
31-
Write code in your personal development environment that’s running in the cloud
18+
Create a new workspace to start coding
3219
</Subheading>
33-
<span className="flex flex-col space-y-4 w-fit">
34-
<LinkButton
35-
variant="secondary"
36-
className="mt-4 border !border-pk-content-invert-primary text-pk-content-secondary bg-pk-surface-secondary"
37-
href={"/new?showExamples=true"}
38-
>
39-
Try a configured demo repository
40-
</LinkButton>
20+
<div className="flex flex-col mt-4 w-fit">
4121
<LinkButton href={"/new"} className="gap-1.5">
42-
Configure your own repository
22+
New Workspace{" "}
23+
<span className="opacity-60 hidden md:inline">{StartWorkspaceModalKeyBinding}</span>
4324
</LinkButton>
44-
</span>
25+
</div>
4526
</div>
4627
</div>
4728
</div>

components/dashboard/src/workspaces/Workspaces.tsx

Lines changed: 170 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,21 @@ import { Workspace, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v
2020
import { Button } from "@podkit/buttons/Button";
2121
import { VideoCarousel } from "./VideoCarousel";
2222
import { BlogBanners } from "./BlogBanners";
23-
import { BookOpen, Code } from "lucide-react";
23+
import { Book, BookOpen, Building, Code, GraduationCap } from "lucide-react";
2424
import { ReactComponent as GitpodStrokedSVG } from "../icons/gitpod-stroked.svg";
2525
import { isGitpodIo } from "../utils";
2626
import PersonalizedContent from "./PersonalizedContent";
2727
import { 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

2939
const 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

238359
export 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+
240407
const 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
*/
252419
function workspaceActiveDate(info: Workspace): string {
253420
return info.status!.phase!.lastTransitionTime!.toDate().toISOString();

0 commit comments

Comments
 (0)