Skip to content

Commit

Permalink
[t&p] add workspaces to teams
Browse files Browse the repository at this point in the history
fixes #4921
  • Loading branch information
svenefftinge committed Sep 21, 2021
1 parent 1a91123 commit 1f014cb
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 156 deletions.
9 changes: 6 additions & 3 deletions components/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,15 +225,18 @@ function App() {
<Route exact path="/teams/new" component={NewTeam} />
<Route exact path="/teams/join" component={JoinTeam} />
</Route>
{(teams || []).map(team => <Route path={`/t/${team.slug}`}>
<Route exact path={`/t/${team.slug}`}>
<Redirect to={`/t/${team.slug}/projects`} />
{(teams || []).map(team => <Route path={`/${team.slug}`}>
<Route exact path={`/${team.slug}`}>
<Redirect to={`/${team.slug}/workspaces`} />
</Route>
<Route exact path={`/t/${team.slug}/:maybeProject/:resourceOrPrebuild?`} render={(props) => {
const { maybeProject, resourceOrPrebuild } = props.match.params;
if (maybeProject === "projects") {
return <Projects />;
}
if (maybeProject === "workspaces") {
return <Workspaces />;
}
if (maybeProject === "members") {
return <Members />;
}
Expand Down
6 changes: 5 additions & 1 deletion components/dashboard/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ export default function Menu() {
return [
{
title: 'Projects',
link: `/t/${team.slug}/projects`,
link: `/${team.slug}/projects`,
},
{
title: 'Workspaces',
link: `/${team.slug}/workspaces`,
alternatives: [`/${team.slug}`]
},
{
Expand Down
261 changes: 127 additions & 134 deletions components/dashboard/src/workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
* See License-AGPL.txt in the project root for license information.
*/

import React from "react";
import { useContext, useEffect, useState } from "react";
import { WhitelistedRepository, Workspace, WorkspaceInfo } from "@gitpod/gitpod-protocol";
import Header from "../components/Header";
import DropDown from "../components/DropDown";
import exclamation from "../images/exclamation.svg";
import { WorkspaceModel } from "./workspace-model";
import { WorkspaceEntry } from "./WorkspaceEntry";
import { getGitpodService, gitpodHostUrl } from "../service/service";
import {StartWorkspaceModal, WsStartEntry} from "./StartWorkspaceModal";
import { StartWorkspaceModal, WsStartEntry } from "./StartWorkspaceModal";
import { Item, ItemField, ItemFieldContextMenu, ItemFieldIcon, ItemsList } from "../components/ItemsList";
import { getCurrentTeam, TeamsContext } from "../teams/teams-context";
import { useLocation } from "react-router";

export interface WorkspacesProps {
}
Expand All @@ -24,144 +26,36 @@ export interface WorkspacesState {
repos: WhitelistedRepository[];
}

export default class Workspaces extends React.Component<WorkspacesProps, WorkspacesState> {
export default function () {
const location = useLocation();

protected workspaceModel: WorkspaceModel | undefined;
const { teams } = useContext(TeamsContext);
const team = getCurrentTeam(location, teams);
const [workspaces, setWorkspaces] = useState<WorkspaceInfo[]>([]);
const [repos, setRepos] = useState<WhitelistedRepository[]>([]);
const [isTemplateModelOpen, setIsTemplateModelOpen] = useState<boolean>(false);
const [workspaceModel, setWorkspaceModel] = useState<WorkspaceModel>();

constructor(props: WorkspacesProps) {
super(props);
this.state = {
workspaces: [],
isTemplateModelOpen: false,
repos: [],
};
}

async componentDidMount() {
this.workspaceModel = new WorkspaceModel(this.setWorkspaces);
const repos = await getGitpodService().server.getFeaturedRepositories();
this.setState({
repos
});
}
const updateWorkspaces = async () => {
getGitpodService().server.getFeaturedRepositories().then(setRepos);
const workspaceModel = !!team ?
new WorkspaceModel(setWorkspaces, getGitpodService().server.getTeamProjects(team?.id).then(projects => projects.map(p => p.id)), false) :
new WorkspaceModel(setWorkspaces, getGitpodService().server.getUserProjects().then(projects => projects.map(p => p.id)), true);

protected setWorkspaces = (workspaces: WorkspaceInfo[]) => {
this.setState({
workspaces
});
setWorkspaceModel(workspaceModel);
}
useEffect(() => {
updateWorkspaces();
}, [teams, location]);

protected showStartWSModal = () => this.setState({
isTemplateModelOpen: true
});

protected hideStartWSModal = () => this.setState({
isTemplateModelOpen: false
});

render() {
const wsModel = this.workspaceModel;
const onActive = () => wsModel!.active = true;
const onAll = () => wsModel!.active = false;
return <>
<Header title="Workspaces" subtitle="Manage recent and stopped workspaces." />
const showStartWSModal = () => setIsTemplateModelOpen(true);
const hideStartWSModal = () => setIsTemplateModelOpen(false);

<div className="lg:px-28 px-10 pt-8 flex">
<div className="flex">
<div className="py-4">
<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>
</div>
<input type="search" placeholder="Search Workspaces" onChange={(v) => { if (wsModel) wsModel.setSearch(v.target.value) }} />
</div>
<div className="flex-1" />
<div className="py-3">
<DropDown prefix="Filter: " contextMenuWidth="w-32" activeEntry={wsModel?.active ? 'Active' : 'All'} entries={[{
title: 'Active',
onClick: onActive
}, {
title: 'All',
onClick: onAll
}]} />
</div>
<div className="py-3 pl-3">
<DropDown prefix="Limit: " contextMenuWidth="w-32" activeEntry={wsModel ? wsModel?.limit+'' : undefined} entries={[{
title: '50',
onClick: () => { if (wsModel) wsModel.limit = 50; }
}, {
title: '100',
onClick: () => { if (wsModel) wsModel.limit = 100; }
}, {
title: '200',
onClick: () => { if (wsModel) wsModel.limit = 200; }
}]} />
</div>
{wsModel && this.state?.workspaces.length > 0 ?
<button onClick={this.showStartWSModal} className="ml-2">New Workspace</button>
: null
}
</div>
{wsModel && (
this.state?.workspaces.length > 0 || wsModel.searchTerm ?
<ItemsList className="lg:px-28 px-10">
<Item header={true} className="px-6">
<ItemFieldIcon />
<ItemField className="w-3/12">Name</ItemField>
<ItemField className="w-4/12">Context</ItemField>
<ItemField className="w-2/12">Pending Changes</ItemField>
<ItemField className="w-2/12">Last Start</ItemField>
<ItemFieldContextMenu />
</Item>
{
wsModel.active || wsModel.searchTerm ? null :
<Item className="w-full bg-gitpod-kumquat-light py-6 px-6">
<ItemFieldIcon>
<img src={exclamation} alt="Exclamation Mark" className="m-auto" />
</ItemFieldIcon>
<ItemField className=" flex flex-col">
<div className="text-gitpod-red font-semibold">Garbage Collection</div>
<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>
</ItemField>
</Item>
}
{
this.state?.workspaces.map(e => {
return <WorkspaceEntry key={e.workspace.id} desc={e} model={wsModel} stopWorkspace={wsId => getGitpodService().server.stopWorkspace(wsId)}/>
})
}
</ItemsList>
:
<div className="lg:px-28 px-10 flex flex-col space-y-2">
<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">
<div className="flex flex-col items-center w-96 m-auto">
<h3 className="text-center pb-3 text-gray-500 dark:text-gray-400">No Active Workspaces</h3>
<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>
<span>
<button onClick={this.showStartWSModal}>New Workspace</button>
{wsModel.getAllFetchedWorkspaces().size > 0 ? <button className="secondary ml-2" onClick={onAll}>View All Workspaces</button>:null}
</span>
</div>
</div>
</div>
)}
<StartWorkspaceModal
onClose={this.hideStartWSModal}
visible={!!this.state?.isTemplateModelOpen}
examples={this.state?.repos && this.state.repos.map(r => ({
title: r.name,
description: r.description || r.url,
startUrl: gitpodHostUrl.withContext(r.url).toString()
}))}
recent={wsModel && this.state?.workspaces ?
this.getRecentSuggestions()
: []} />
</>;
}

protected getRecentSuggestions(): WsStartEntry[] {
if (this.workspaceModel) {
const all = this.workspaceModel.getAllFetchedWorkspaces();
const getRecentSuggestions: () => WsStartEntry[] = () => {
if (workspaceModel) {
const all = workspaceModel.getAllFetchedWorkspaces();
if (all && all.size > 0) {
const index = new Map<string, WsStartEntry & {lastUse: string}>();
const index = new Map<string, WsStartEntry & { lastUse: string }>();
for (const ws of Array.from(all.values())) {
const repoUrl = Workspace.getFullRepositoryUrl(ws.workspace);
if (repoUrl) {
Expand All @@ -183,10 +77,109 @@ export default class Workspaces extends React.Component<WorkspacesProps, Workspa
}
}
const list = Array.from(index.values());
list.sort((a,b) => b.lastUse.localeCompare(a.lastUse));
list.sort((a, b) => b.lastUse.localeCompare(a.lastUse));
return list;
}
}
return [];
}
if (!workspaceModel) {
return <div />
}

const onActive = () => workspaceModel.active = true;
const onAll = () => workspaceModel.active = false;
return <>
<Header title="Workspaces" subtitle="Manage recent and stopped workspaces." />

<div className="lg:px-28 px-10 pt-8 flex">
<div className="flex">
<div className="py-4">
<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>
</div>
<input type="search" placeholder="Search Workspaces" onChange={(v) => { if (workspaceModel) workspaceModel.setSearch(v.target.value) }} />
</div>
<div className="flex-1" />
<div className="py-3">
<DropDown prefix="Filter: " contextMenuWidth="w-32" activeEntry={workspaceModel?.active ? 'Active' : 'All'} entries={[{
title: 'Active',
onClick: onActive
}, {
title: 'All',
onClick: onAll
}]} />
</div>
<div className="py-3 pl-3">
<DropDown prefix="Limit: " contextMenuWidth="w-32" activeEntry={workspaceModel ? workspaceModel?.limit + '' : undefined} entries={[{
title: '50',
onClick: () => { if (workspaceModel) workspaceModel.limit = 50; }
}, {
title: '100',
onClick: () => { if (workspaceModel) workspaceModel.limit = 100; }
}, {
title: '200',
onClick: () => { if (workspaceModel) workspaceModel.limit = 200; }
}]} />
</div>
{workspaceModel && workspaces.length > 0 ?
<button onClick={showStartWSModal} className="ml-2">New Workspace</button>
: null
}
</div>
{workspaceModel && (
workspaces.length > 0 || workspaceModel.searchTerm ?
<ItemsList className="lg:px-28 px-10">
<Item header={true} className="px-6">
<ItemFieldIcon />
<ItemField className="w-3/12">Name</ItemField>
<ItemField className="w-4/12">Context</ItemField>
<ItemField className="w-2/12">Pending Changes</ItemField>
<ItemField className="w-2/12">Last Start</ItemField>
<ItemFieldContextMenu />
</Item>
{
workspaceModel.active || workspaceModel.searchTerm ? null :
<Item className="w-full bg-gitpod-kumquat-light py-6 px-6">
<ItemFieldIcon>
<img src={exclamation} alt="Exclamation Mark" className="m-auto" />
</ItemFieldIcon>
<ItemField className=" flex flex-col">
<div className="text-gitpod-red font-semibold">Garbage Collection</div>
<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>
</ItemField>
</Item>
}
{
workspaces.map(e => {
return <WorkspaceEntry key={e.workspace.id} desc={e} model={workspaceModel} stopWorkspace={wsId => getGitpodService().server.stopWorkspace(wsId)} />
})
}
</ItemsList>
:
<div className="lg:px-28 px-10 flex flex-col space-y-2">
<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">
<div className="flex flex-col items-center w-96 m-auto">
<h3 className="text-center pb-3 text-gray-500 dark:text-gray-400">No Active Workspaces</h3>
<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>
<span>
<button onClick={showStartWSModal}>New Workspace</button>
{workspaceModel.getAllFetchedWorkspaces().size > 0 ? <button className="secondary ml-2" onClick={onAll}>View All Workspaces</button> : null}
</span>
</div>
</div>
</div>
)}
<StartWorkspaceModal
onClose={hideStartWSModal}
visible={!!isTemplateModelOpen}
examples={repos && repos.map(r => ({
title: r.name,
description: r.description || r.url,
startUrl: gitpodHostUrl.withContext(r.url).toString()
}))}
recent={workspaceModel && workspaces ?
getRecentSuggestions()
: []} />
</>;

}
33 changes: 20 additions & 13 deletions components/dashboard/src/workspaces/workspace-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,34 @@ export class WorkspaceModel implements Disposable, Partial<GitpodClient> {
this.internalRefetch();
}

constructor(protected setWorkspaces: (ws: WorkspaceInfo[]) => void) {
constructor(protected setWorkspaces: (ws: WorkspaceInfo[]) => void,
protected projectIds: Promise<string[]>,
protected includeWithoutProject?: boolean) {
this.internalRefetch();
}

protected internalRefetch() {
protected async internalRefetch(): Promise<void> {
this.disposables.dispose();
this.disposables = new DisposableCollection();
getGitpodService().server.getWorkspaces({
limit: this.internalLimit
}).then( infos => {
this.updateMap(infos);
// Additional fetch pinned workspaces
// see also: https://github.com/gitpod-io/gitpod/issues/4488
const [infos, pinned] = await Promise.all([
getGitpodService().server.getWorkspaces({
limit: this.internalLimit,
projectId: await this.projectIds,
includeWithoutProject: !!this.includeWithoutProject
}),
getGitpodService().server.getWorkspaces({
limit: this.internalLimit,
pinnedOnly: true,
}).then(infos => {
this.updateMap(infos);
this.notifyWorkpaces();
});
});
projectId: await this.projectIds,
includeWithoutProject: !!this.includeWithoutProject
})
]);

this.updateMap(infos);
// Additional fetch pinned workspaces
// see also: https://github.com/gitpod-io/gitpod/issues/4488
this.updateMap(pinned);
this.notifyWorkpaces();
this.disposables.push(getGitpodService().registerClient(this));
}

Expand Down
Loading

0 comments on commit 1f014cb

Please sign in to comment.