Skip to content

Commit f313efb

Browse files
committed
[t&p] add workspaces to teams
fixes #4921
1 parent 95d432f commit f313efb

File tree

7 files changed

+177
-153
lines changed

7 files changed

+177
-153
lines changed

components/dashboard/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,16 @@ function App() {
212212
</Route>
213213
{(teams || []).map(team => <Route path={`/${team.slug}`}>
214214
<Route exact path={`/${team.slug}`}>
215-
<Redirect to={`/${team.slug}/projects`} />
215+
<Redirect to={`/${team.slug}/workspaces`} />
216216
</Route>
217217
<Route exact path={`/${team.slug}/:maybeProject/:resourceOrPrebuild?`} render={(props) => {
218218
const { maybeProject, resourceOrPrebuild } = props.match.params;
219219
if (maybeProject === "projects") {
220220
return <Projects />;
221221
}
222+
if (maybeProject === "workspaces") {
223+
return <Workspaces />;
224+
}
222225
if (maybeProject === "members") {
223226
return <Members />;
224227
}

components/dashboard/src/Menu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export default function Menu() {
101101
{
102102
title: 'Projects',
103103
link: `/${team.slug}/projects`,
104+
},
105+
{
106+
title: 'Workspaces',
107+
link: `/${team.slug}/workspaces`,
104108
alternatives: [`/${team.slug}`]
105109
},
106110
{

components/dashboard/src/workspaces/Workspaces.tsx

Lines changed: 127 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import React from "react";
7+
import { useContext, useEffect, useState } from "react";
88
import { WhitelistedRepository, Workspace, WorkspaceInfo } from "@gitpod/gitpod-protocol";
99
import Header from "../components/Header";
1010
import DropDown from "../components/DropDown";
1111
import exclamation from "../images/exclamation.svg";
1212
import { WorkspaceModel } from "./workspace-model";
1313
import { WorkspaceEntry } from "./WorkspaceEntry";
1414
import { getGitpodService, gitpodHostUrl } from "../service/service";
15-
import {StartWorkspaceModal, WsStartEntry} from "./StartWorkspaceModal";
15+
import { StartWorkspaceModal, WsStartEntry } from "./StartWorkspaceModal";
1616
import { Item, ItemField, ItemFieldContextMenu, ItemFieldIcon, ItemsList } from "../components/ItemsList";
17+
import { getCurrentTeam, TeamsContext } from "../teams/teams-context";
18+
import { useLocation } from "react-router";
1719

1820
export interface WorkspacesProps {
1921
}
@@ -24,144 +26,36 @@ export interface WorkspacesState {
2426
repos: WhitelistedRepository[];
2527
}
2628

27-
export default class Workspaces extends React.Component<WorkspacesProps, WorkspacesState> {
29+
export default function () {
30+
const location = useLocation();
2831

29-
protected workspaceModel: WorkspaceModel | undefined;
32+
const { teams } = useContext(TeamsContext);
33+
const team = getCurrentTeam(location, teams);
34+
const [workspaces, setWorkspaces] = useState<WorkspaceInfo[]>([]);
35+
const [repos, setRepos] = useState<WhitelistedRepository[]>([]);
36+
const [isTemplateModelOpen, setIsTemplateModelOpen] = useState<boolean>(false);
37+
const [workspaceModel, setWorkspaceModel] = useState<WorkspaceModel>();
3038

31-
constructor(props: WorkspacesProps) {
32-
super(props);
33-
this.state = {
34-
workspaces: [],
35-
isTemplateModelOpen: false,
36-
repos: [],
37-
};
38-
}
39-
40-
async componentDidMount() {
41-
this.workspaceModel = new WorkspaceModel(this.setWorkspaces);
42-
const repos = await getGitpodService().server.getFeaturedRepositories();
43-
this.setState({
44-
repos
45-
});
46-
}
39+
const updateWorkspaces = async () => {
40+
getGitpodService().server.getFeaturedRepositories().then(setRepos);
41+
const workspaceModel = !!team ?
42+
new WorkspaceModel(setWorkspaces, getGitpodService().server.getTeamProjects(team?.id).then(projects => projects.map(p => p.id)), false) :
43+
new WorkspaceModel(setWorkspaces, getGitpodService().server.getUserProjects().then(projects => projects.map(p => p.id)), true);
4744

48-
protected setWorkspaces = (workspaces: WorkspaceInfo[]) => {
49-
this.setState({
50-
workspaces
51-
});
45+
setWorkspaceModel(workspaceModel);
5246
}
47+
useEffect(() => {
48+
updateWorkspaces();
49+
}, [teams, location]);
5350

54-
protected showStartWSModal = () => this.setState({
55-
isTemplateModelOpen: true
56-
});
57-
58-
protected hideStartWSModal = () => this.setState({
59-
isTemplateModelOpen: false
60-
});
61-
62-
render() {
63-
const wsModel = this.workspaceModel;
64-
const onActive = () => wsModel!.active = true;
65-
const onAll = () => wsModel!.active = false;
66-
return <>
67-
<Header title="Workspaces" subtitle="Manage recent and stopped workspaces." />
51+
const showStartWSModal = () => setIsTemplateModelOpen(true);
52+
const hideStartWSModal = () => setIsTemplateModelOpen(false);
6853

69-
<div className="lg:px-28 px-10 pt-8 flex">
70-
<div className="flex">
71-
<div className="py-4">
72-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" width="16" height="16"><path fill="#A8A29E" d="M6 2a4 4 0 100 8 4 4 0 000-8zM0 6a6 6 0 1110.89 3.477l4.817 4.816a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 010 6z"/></svg>
73-
</div>
74-
<input type="search" placeholder="Search Workspaces" onChange={(v) => { if (wsModel) wsModel.setSearch(v.target.value) }} />
75-
</div>
76-
<div className="flex-1" />
77-
<div className="py-3">
78-
<DropDown prefix="Filter: " contextMenuWidth="w-32" activeEntry={wsModel?.active ? 'Active' : 'All'} entries={[{
79-
title: 'Active',
80-
onClick: onActive
81-
}, {
82-
title: 'All',
83-
onClick: onAll
84-
}]} />
85-
</div>
86-
<div className="py-3 pl-3">
87-
<DropDown prefix="Limit: " contextMenuWidth="w-32" activeEntry={wsModel ? wsModel?.limit+'' : undefined} entries={[{
88-
title: '50',
89-
onClick: () => { if (wsModel) wsModel.limit = 50; }
90-
}, {
91-
title: '100',
92-
onClick: () => { if (wsModel) wsModel.limit = 100; }
93-
}, {
94-
title: '200',
95-
onClick: () => { if (wsModel) wsModel.limit = 200; }
96-
}]} />
97-
</div>
98-
{wsModel && this.state?.workspaces.length > 0 ?
99-
<button onClick={this.showStartWSModal} className="ml-2">New Workspace</button>
100-
: null
101-
}
102-
</div>
103-
{wsModel && (
104-
this.state?.workspaces.length > 0 || wsModel.searchTerm ?
105-
<ItemsList className="lg:px-28 px-10">
106-
<Item header={true} className="px-6">
107-
<ItemFieldIcon />
108-
<ItemField className="w-3/12">Name</ItemField>
109-
<ItemField className="w-4/12">Context</ItemField>
110-
<ItemField className="w-2/12">Pending Changes</ItemField>
111-
<ItemField className="w-2/12">Last Start</ItemField>
112-
<ItemFieldContextMenu />
113-
</Item>
114-
{
115-
wsModel.active || wsModel.searchTerm ? null :
116-
<Item className="w-full bg-gitpod-kumquat-light py-6 px-6">
117-
<ItemFieldIcon>
118-
<img src={exclamation} alt="Exclamation Mark" className="m-auto" />
119-
</ItemFieldIcon>
120-
<ItemField className=" flex flex-col">
121-
<div className="text-gitpod-red font-semibold">Garbage Collection</div>
122-
<p className="text-gray-500">Unpinned workspaces that have been stopped for more than 14 days will be automatically deleted. <a className="gp-link" href="https://www.gitpod.io/docs/life-of-workspace/#garbage-collection">Learn more</a></p>
123-
</ItemField>
124-
</Item>
125-
}
126-
{
127-
this.state?.workspaces.map(e => {
128-
return <WorkspaceEntry key={e.workspace.id} desc={e} model={wsModel} stopWorkspace={wsId => getGitpodService().server.stopWorkspace(wsId)}/>
129-
})
130-
}
131-
</ItemsList>
132-
:
133-
<div className="lg:px-28 px-10 flex flex-col space-y-2">
134-
<div className="px-6 py-3 flex justify-between space-x-2 text-gray-400 border-t border-gray-200 dark:border-gray-800 h-96">
135-
<div className="flex flex-col items-center w-96 m-auto">
136-
<h3 className="text-center pb-3 text-gray-500 dark:text-gray-400">No Active Workspaces</h3>
137-
<div className="text-center pb-6 text-gray-500">Prefix any git repository URL with gitpod.io/# or create a new workspace for a recently used project. <a className="gp-link" href="https://www.gitpod.io/docs/getting-started/">Learn more</a></div>
138-
<span>
139-
<button onClick={this.showStartWSModal}>New Workspace</button>
140-
{wsModel.getAllFetchedWorkspaces().size > 0 ? <button className="secondary ml-2" onClick={onAll}>View All Workspaces</button>:null}
141-
</span>
142-
</div>
143-
</div>
144-
</div>
145-
)}
146-
<StartWorkspaceModal
147-
onClose={this.hideStartWSModal}
148-
visible={!!this.state?.isTemplateModelOpen}
149-
examples={this.state?.repos && this.state.repos.map(r => ({
150-
title: r.name,
151-
description: r.description || r.url,
152-
startUrl: gitpodHostUrl.withContext(r.url).toString()
153-
}))}
154-
recent={wsModel && this.state?.workspaces ?
155-
this.getRecentSuggestions()
156-
: []} />
157-
</>;
158-
}
159-
160-
protected getRecentSuggestions(): WsStartEntry[] {
161-
if (this.workspaceModel) {
162-
const all = this.workspaceModel.getAllFetchedWorkspaces();
54+
const getRecentSuggestions: () => WsStartEntry[] = () => {
55+
if (workspaceModel) {
56+
const all = workspaceModel.getAllFetchedWorkspaces();
16357
if (all && all.size > 0) {
164-
const index = new Map<string, WsStartEntry & {lastUse: string}>();
58+
const index = new Map<string, WsStartEntry & { lastUse: string }>();
16559
for (const ws of Array.from(all.values())) {
16660
const repoUrl = Workspace.getFullRepositoryUrl(ws.workspace);
16761
if (repoUrl) {
@@ -183,10 +77,109 @@ export default class Workspaces extends React.Component<WorkspacesProps, Workspa
18377
}
18478
}
18579
const list = Array.from(index.values());
186-
list.sort((a,b) => b.lastUse.localeCompare(a.lastUse));
80+
list.sort((a, b) => b.lastUse.localeCompare(a.lastUse));
18781
return list;
18882
}
18983
}
19084
return [];
19185
}
86+
if (!workspaceModel) {
87+
return <div />
88+
}
89+
90+
const onActive = () => workspaceModel.active = true;
91+
const onAll = () => workspaceModel.active = false;
92+
return <>
93+
<Header title="Workspaces" subtitle="Manage recent and stopped workspaces." />
94+
95+
<div className="lg:px-28 px-10 pt-8 flex">
96+
<div className="flex">
97+
<div className="py-4">
98+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" width="16" height="16"><path fill="#A8A29E" d="M6 2a4 4 0 100 8 4 4 0 000-8zM0 6a6 6 0 1110.89 3.477l4.817 4.816a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 010 6z" /></svg>
99+
</div>
100+
<input type="search" placeholder="Search Workspaces" onChange={(v) => { if (workspaceModel) workspaceModel.setSearch(v.target.value) }} />
101+
</div>
102+
<div className="flex-1" />
103+
<div className="py-3">
104+
<DropDown prefix="Filter: " contextMenuWidth="w-32" activeEntry={workspaceModel?.active ? 'Active' : 'All'} entries={[{
105+
title: 'Active',
106+
onClick: onActive
107+
}, {
108+
title: 'All',
109+
onClick: onAll
110+
}]} />
111+
</div>
112+
<div className="py-3 pl-3">
113+
<DropDown prefix="Limit: " contextMenuWidth="w-32" activeEntry={workspaceModel ? workspaceModel?.limit + '' : undefined} entries={[{
114+
title: '50',
115+
onClick: () => { if (workspaceModel) workspaceModel.limit = 50; }
116+
}, {
117+
title: '100',
118+
onClick: () => { if (workspaceModel) workspaceModel.limit = 100; }
119+
}, {
120+
title: '200',
121+
onClick: () => { if (workspaceModel) workspaceModel.limit = 200; }
122+
}]} />
123+
</div>
124+
{workspaceModel && workspaces.length > 0 ?
125+
<button onClick={showStartWSModal} className="ml-2">New Workspace</button>
126+
: null
127+
}
128+
</div>
129+
{workspaceModel && (
130+
workspaces.length > 0 || workspaceModel.searchTerm ?
131+
<ItemsList className="lg:px-28 px-10">
132+
<Item header={true} className="px-6">
133+
<ItemFieldIcon />
134+
<ItemField className="w-3/12">Name</ItemField>
135+
<ItemField className="w-4/12">Context</ItemField>
136+
<ItemField className="w-2/12">Pending Changes</ItemField>
137+
<ItemField className="w-2/12">Last Start</ItemField>
138+
<ItemFieldContextMenu />
139+
</Item>
140+
{
141+
workspaceModel.active || workspaceModel.searchTerm ? null :
142+
<Item className="w-full bg-gitpod-kumquat-light py-6 px-6">
143+
<ItemFieldIcon>
144+
<img src={exclamation} alt="Exclamation Mark" className="m-auto" />
145+
</ItemFieldIcon>
146+
<ItemField className=" flex flex-col">
147+
<div className="text-gitpod-red font-semibold">Garbage Collection</div>
148+
<p className="text-gray-500">Unpinned workspaces that have been stopped for more than 14 days will be automatically deleted. <a className="gp-link" href="https://www.gitpod.io/docs/life-of-workspace/#garbage-collection">Learn more</a></p>
149+
</ItemField>
150+
</Item>
151+
}
152+
{
153+
workspaces.map(e => {
154+
return <WorkspaceEntry key={e.workspace.id} desc={e} model={workspaceModel} stopWorkspace={wsId => getGitpodService().server.stopWorkspace(wsId)} />
155+
})
156+
}
157+
</ItemsList>
158+
:
159+
<div className="lg:px-28 px-10 flex flex-col space-y-2">
160+
<div className="px-6 py-3 flex justify-between space-x-2 text-gray-400 border-t border-gray-200 dark:border-gray-800 h-96">
161+
<div className="flex flex-col items-center w-96 m-auto">
162+
<h3 className="text-center pb-3 text-gray-500 dark:text-gray-400">No Active Workspaces</h3>
163+
<div className="text-center pb-6 text-gray-500">Prefix any git repository URL with gitpod.io/# or create a new workspace for a recently used project. <a className="gp-link" href="https://www.gitpod.io/docs/getting-started/">Learn more</a></div>
164+
<span>
165+
<button onClick={showStartWSModal}>New Workspace</button>
166+
{workspaceModel.getAllFetchedWorkspaces().size > 0 ? <button className="secondary ml-2" onClick={onAll}>View All Workspaces</button> : null}
167+
</span>
168+
</div>
169+
</div>
170+
</div>
171+
)}
172+
<StartWorkspaceModal
173+
onClose={hideStartWSModal}
174+
visible={!!isTemplateModelOpen}
175+
examples={repos && repos.map(r => ({
176+
title: r.name,
177+
description: r.description || r.url,
178+
startUrl: gitpodHostUrl.withContext(r.url).toString()
179+
}))}
180+
recent={workspaceModel && workspaces ?
181+
getRecentSuggestions()
182+
: []} />
183+
</>;
184+
192185
}

components/dashboard/src/workspaces/workspace-model.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,34 @@ export class WorkspaceModel implements Disposable, Partial<GitpodClient> {
2323
this.internalRefetch();
2424
}
2525

26-
constructor(protected setWorkspaces: (ws: WorkspaceInfo[]) => void) {
26+
constructor(protected setWorkspaces: (ws: WorkspaceInfo[]) => void,
27+
protected projectIds: Promise<string[]>,
28+
protected includeWithoutProject?: boolean) {
2729
this.internalRefetch();
2830
}
2931

30-
protected internalRefetch() {
32+
protected async internalRefetch(): Promise<void> {
3133
this.disposables.dispose();
3234
this.disposables = new DisposableCollection();
33-
getGitpodService().server.getWorkspaces({
34-
limit: this.internalLimit
35-
}).then( infos => {
36-
this.updateMap(infos);
37-
// Additional fetch pinned workspaces
38-
// see also: https://github.com/gitpod-io/gitpod/issues/4488
35+
const [infos, pinned] = await Promise.all([
36+
getGitpodService().server.getWorkspaces({
37+
limit: this.internalLimit,
38+
projectId: await this.projectIds,
39+
includeWithoutProject: !!this.includeWithoutProject
40+
}),
3941
getGitpodService().server.getWorkspaces({
4042
limit: this.internalLimit,
4143
pinnedOnly: true,
42-
}).then(infos => {
43-
this.updateMap(infos);
44-
this.notifyWorkpaces();
45-
});
46-
});
44+
projectId: await this.projectIds,
45+
includeWithoutProject: !!this.includeWithoutProject
46+
})
47+
]);
48+
49+
this.updateMap(infos);
50+
// Additional fetch pinned workspaces
51+
// see also: https://github.com/gitpod-io/gitpod/issues/4488
52+
this.updateMap(pinned);
53+
this.notifyWorkpaces();
4754
this.disposables.push(getGitpodService().registerClient(this));
4855
}
4956

0 commit comments

Comments
 (0)