diff --git a/components/dashboard/src/App.tsx b/components/dashboard/src/App.tsx index 8a8664a7cf6e52..e0d6fed8b5c04b 100644 --- a/components/dashboard/src/App.tsx +++ b/components/dashboard/src/App.tsx @@ -265,6 +265,9 @@ function App() { if (resourceOrPrebuild === "configure") { return ; } + if (resourceOrPrebuild === "workspaces") { + return ; + } if (resourceOrPrebuild === "prebuilds") { return ; } @@ -276,21 +279,28 @@ function App() { - {(teams || []).map(team => + {(teams || []).map(team => + - + { const { maybeProject, resourceOrPrebuild } = props.match.params; if (maybeProject === "projects") { return ; } + if (maybeProject === "workspaces") { + return ; + } if (maybeProject === "members") { return ; } if (resourceOrPrebuild === "configure") { return ; } + if (resourceOrPrebuild === "workspaces") { + return ; + } if (resourceOrPrebuild === "prebuilds") { return ; } diff --git a/components/dashboard/src/Menu.tsx b/components/dashboard/src/Menu.tsx index d3c6bf67326019..a05829fdc292c2 100644 --- a/components/dashboard/src/Menu.tsx +++ b/components/dashboard/src/Menu.tsx @@ -43,7 +43,7 @@ export default function Menu() { })(); const prebuildId = (() => { const resource = projectName && match?.params?.segment3; - if (resource !== "prebuilds" && resource !== "settings" && resource !== "configure") { + if (resource !== "workspaces" && resource !== "prebuilds" && resource !== "settings" && resource !== "configure") { return resource; } })(); @@ -93,6 +93,10 @@ export default function Menu() { title: 'Branches', link: `${teamOrUserSlug}/${projectName}` }, + { + title: 'Workspaces', + link: `${teamOrUserSlug}/${projectName}/workspaces` + }, { title: 'Prebuilds', link: `${teamOrUserSlug}/${projectName}/prebuilds` @@ -109,7 +113,11 @@ export default function Menu() { { title: 'Projects', link: `/t/${team.slug}/projects`, - alternatives: [`/${team.slug}`] + }, + { + title: 'Workspaces', + link: `/t/${team.slug}/workspaces`, + alternatives: [`/t/${team.slug}`] }, { title: 'Members', diff --git a/components/dashboard/src/components/Header.tsx b/components/dashboard/src/components/Header.tsx index 6eed8a66bd47d8..750e6f3037960d 100644 --- a/components/dashboard/src/components/Header.tsx +++ b/components/dashboard/src/components/Header.tsx @@ -20,7 +20,7 @@ export default function Header(p: HeaderProps) { document.title = `${p.title} — Gitpod`; }, []); return - + {typeof p.title === "string" ? ({p.title}) : p.title} {typeof p.subtitle === "string" ? ({p.subtitle}) : p.subtitle} diff --git a/components/dashboard/src/index.css b/components/dashboard/src/index.css index a1f3c0bf7169f9..08181173cee08d 100644 --- a/components/dashboard/src/index.css +++ b/components/dashboard/src/index.css @@ -59,7 +59,7 @@ } a.gp-link { - @apply underline underline-thickness-thin underline-offset-small text-gray-400 dark:text-gray-600 hover:text-gray-500 dark:hover:text-gray-500; + @apply text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500; } input[type=text], input[type=search], input[type=password], select { diff --git a/components/dashboard/src/workspaces/StartWorkspaceModal.tsx b/components/dashboard/src/workspaces/StartWorkspaceModal.tsx index 2151c24c56cf60..b9a249c84fc040 100644 --- a/components/dashboard/src/workspaces/StartWorkspaceModal.tsx +++ b/components/dashboard/src/workspaces/StartWorkspaceModal.tsx @@ -43,7 +43,7 @@ export function StartWorkspaceModal(p: StartWorkspaceModalProps) { setSelection('Recent')} /> - setSelection('Examples')} /> + {p.examples.length>0 && setSelection('Examples')} />} diff --git a/components/dashboard/src/workspaces/Workspaces.tsx b/components/dashboard/src/workspaces/Workspaces.tsx index 445853ea12aaf9..9f4adb982690b9 100644 --- a/components/dashboard/src/workspaces/Workspaces.tsx +++ b/components/dashboard/src/workspaces/Workspaces.tsx @@ -4,16 +4,19 @@ * See License-AGPL.txt in the project root for license information. */ -import React from "react"; -import { WhitelistedRepository, Workspace, WorkspaceInfo } from "@gitpod/gitpod-protocol"; +import { useContext, useEffect, useState } from "react"; +import { Project, 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 { Item, ItemField, ItemFieldContextMenu, ItemFieldIcon, ItemsList } from "../components/ItemsList"; +import { StartWorkspaceModal, WsStartEntry } from "./StartWorkspaceModal"; +import { Item, ItemField, ItemsList } from "../components/ItemsList"; +import { getCurrentTeam, TeamsContext } from "../teams/teams-context"; +import { useLocation, useRouteMatch } from "react-router"; +import { toRemoteURL } from "../projects/render-utils"; +import { useHistory } from "react-router-dom"; export interface WorkspacesProps { } @@ -24,144 +27,75 @@ export interface WorkspacesState { repos: WhitelistedRepository[]; } -export default class Workspaces extends React.Component { +export default function () { + const location = useLocation(); + const history = useHistory(); - protected workspaceModel: WorkspaceModel | undefined; + const { teams } = useContext(TeamsContext); + const team = getCurrentTeam(location, teams); + const match = useRouteMatch<{ team: string, resource: string }>("/(t/)?:team/:resource"); + const projectName = match?.params?.resource !== 'workspaces' ? match?.params?.resource : undefined; + const [projects, setProjects] = useState([]); + const [activeWorkspaces, setActiveWorkspaces] = useState([]); + const [inactiveWorkspaces, setInactiveWorkspaces] = useState([]); + const [repos, setRepos] = useState([]); + const [isTemplateModelOpen, setIsTemplateModelOpen] = useState(false); + const [workspaceModel, setWorkspaceModel] = useState(); - constructor(props: WorkspacesProps) { - super(props); - this.state = { - workspaces: [], - isTemplateModelOpen: false, - repos: [], - }; + const newProjectUrl = !!team ? `/new?team=${team.slug}` : '/new'; + const onNewProject = () => { + history.push(newProjectUrl); } - async componentDidMount() { - this.workspaceModel = new WorkspaceModel(this.setWorkspaces); - const repos = await getGitpodService().server.getFeaturedRepositories(); - this.setState({ - repos - }); - } - - protected setWorkspaces = (workspaces: WorkspaceInfo[]) => { - this.setState({ - workspaces - }); - } - - 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 <> - + useEffect(() => { + // only show example repos on the global user context + if (!team && !projectName) { + getGitpodService().server.getFeaturedRepositories().then(setRepos); + } + (async () => { + const projects = (!!team + ? await getGitpodService().server.getTeamProjects(team.id) + : await getGitpodService().server.getUserProjects()); - - - - - - { if (wsModel) wsModel.setSearch(v.target.value) }} /> - - - - - - - { if (wsModel) wsModel.limit = 50; } - }, { - title: '100', - onClick: () => { if (wsModel) wsModel.limit = 100; } - }, { - title: '200', - onClick: () => { if (wsModel) wsModel.limit = 200; } - }]} /> - - {wsModel && this.state?.workspaces.length > 0 ? - New Workspace - : null + let project: Project | undefined = undefined; + if (projectName) { + project = projects.find(p => p.name === projectName); + if (project) { + setProjects([project]); } - - {wsModel && ( - this.state?.workspaces.length > 0 || wsModel.searchTerm ? - - - - Name - Context - Pending Changes - Last Start - - - { - wsModel.active || wsModel.searchTerm ? null : - - - - - - Garbage Collection - Unpinned workspaces that have been stopped for more than 14 days will be automatically deleted. Learn more - - - } - { - this.state?.workspaces.map(e => { - return getGitpodService().server.stopWorkspace(wsId)}/> - }) - } - - : - - - - No Active Workspaces - Prefix any git repository URL with gitpod.io/# or create a new workspace for a recently used project. Learn more - - New Workspace - {wsModel.getAllFetchedWorkspaces().size > 0 ? View All Workspaces:null} - - - - - )} - ({ - title: r.name, - description: r.description || r.url, - startUrl: gitpodHostUrl.withContext(r.url).toString() - }))} - recent={wsModel && this.state?.workspaces ? - this.getRecentSuggestions() - : []} /> - >; - } + } else { + setProjects(projects); + } + let workspaceModel; + if (!!project) { + workspaceModel = new WorkspaceModel(setActiveWorkspaces, setInactiveWorkspaces, Promise.resolve([project.id]), false); + } else if (!!team) { + workspaceModel = new WorkspaceModel(setActiveWorkspaces, setInactiveWorkspaces, getGitpodService().server.getTeamProjects(team?.id).then(projects => projects.map(p => p.id)), false); + } else { + workspaceModel = new WorkspaceModel(setActiveWorkspaces, setInactiveWorkspaces, getGitpodService().server.getUserProjects().then(projects => projects.map(p => p.id)), true); + } + setWorkspaceModel(workspaceModel); + })(); + }, [teams, location]); + + const showStartWSModal = () => setIsTemplateModelOpen(true); + const hideStartWSModal = () => setIsTemplateModelOpen(false); - protected getRecentSuggestions(): WsStartEntry[] { - if (this.workspaceModel) { - const all = this.workspaceModel.getAllFetchedWorkspaces(); + const getRecentSuggestions: () => WsStartEntry[] = () => { + if (projectName || team) { + return projects.map(p => { + const remoteUrl = toRemoteURL(p.cloneUrl); + return { + title: (team ? team.name + '/' : '') + p.name, + description: remoteUrl, + startUrl: gitpodHostUrl.withContext(remoteUrl).toString() + }; + }); + } + if (workspaceModel) { + const all = workspaceModel.getAllFetchedWorkspaces(); if (all && all.size > 0) { - const index = new Map(); + const index = new Map(); for (const ws of Array.from(all.values())) { const repoUrl = Workspace.getFullRepositoryUrl(ws.workspace); if (repoUrl) { @@ -183,10 +117,106 @@ export default class Workspaces extends React.Component b.lastUse.localeCompare(a.lastUse)); + list.sort((a, b) => b.lastUse.localeCompare(a.lastUse)); return list; } } return []; } + + return <> + + + {workspaceModel?.initialized && ( + activeWorkspaces.length > 0 || inactiveWorkspaces.length > 0 || workspaceModel.searchTerm ? + <> + + + + + + { if (workspaceModel) workspaceModel.setSearch(v.target.value) }} /> + + + + + + { if (workspaceModel) workspaceModel.limit = 50; } + }, { + title: '100', + onClick: () => { if (workspaceModel) workspaceModel.limit = 100; } + }, { + title: '200', + onClick: () => { if (workspaceModel) workspaceModel.limit = 200; } + }]} /> + + { + projects.length === 0 + ? New Project + : New Workspace + } + + + + { + activeWorkspaces.map(e => { + return getGitpodService().server.stopWorkspace(wsId)} /> + }) + } + { + activeWorkspaces.length > 0 && + } + { + inactiveWorkspaces.length === 0 ? null : + + + Unpinned workspaces that have been inactive for more than 14 days will be automatically deleted. Learn more + + + } + { + inactiveWorkspaces.map(e => { + return getGitpodService().server.stopWorkspace(wsId)} /> + }) + } + + > + : + + + + {!!team && projects.length === 0 + ?<> + No Projects + This team doesn't have any projects, yet. + + New Project + + > + :<> + No Workspaces + Prefix any git repository URL with gitpod.io/# or create a new workspace for a recently used project. Learn more + + New Workspace + + >} + + + + )} + ({ + title: r.name, + description: r.description || r.url, + startUrl: gitpodHostUrl.withContext(r.url).toString() + }))} + recent={workspaceModel && activeWorkspaces ? + getRecentSuggestions() + : []} /> + >; + } diff --git a/components/dashboard/src/workspaces/workspace-model.ts b/components/dashboard/src/workspaces/workspace-model.ts index 56b01336983627..5a69d8e64fde10 100644 --- a/components/dashboard/src/workspaces/workspace-model.ts +++ b/components/dashboard/src/workspaces/workspace-model.ts @@ -13,6 +13,7 @@ export class WorkspaceModel implements Disposable, Partial { protected currentlyFetching = new Set(); protected disposables = new DisposableCollection(); protected internalLimit = 50; + public initialized = false; get limit(): number { return this.internalLimit; @@ -23,27 +24,36 @@ export class WorkspaceModel implements Disposable, Partial { this.internalRefetch(); } - constructor(protected setWorkspaces: (ws: WorkspaceInfo[]) => void) { + constructor( + protected setActiveWorkspaces: (ws: WorkspaceInfo[]) => void, + protected setInActiveWorkspaces: (ws: WorkspaceInfo[]) => void, + protected projectIds: Promise, + protected includeWithoutProject?: boolean) { this.internalRefetch(); } - protected internalRefetch() { + protected async internalRefetch(): Promise { 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)); } @@ -53,6 +63,14 @@ export class WorkspaceModel implements Disposable, Partial { } } + protected async isIncluded(info: WorkspaceInfo): Promise { + if (info.workspace.projectId) { + return (await this.projectIds).some(id => id === info.workspace.projectId); + } else { + return !!this.includeWithoutProject; + } + } + dispose(): void { this.disposables.dispose(); } @@ -69,7 +87,7 @@ export class WorkspaceModel implements Disposable, Partial { try { this.currentlyFetching.add(instance.workspaceId); const info = await getGitpodService().server.getWorkspace(instance.workspaceId); - if (info.workspace.type === 'regular') { + if (info.workspace.type === 'regular' && await this.isIncluded(info)) { this.workspaces.set(instance.workspaceId, info); this.notifyWorkpaces(); } @@ -86,16 +104,6 @@ export class WorkspaceModel implements Disposable, Partial { this.notifyWorkpaces(); } - protected internalActive = true; - get active() { - return this.internalActive; - } - set active(active: boolean) { - if (active !== this.internalActive) { - this.internalActive = active; - this.notifyWorkpaces(); - } - } searchTerm: string | undefined; setSearch(searchTerm: string) { if (searchTerm !== this.searchTerm) { @@ -123,15 +131,18 @@ export class WorkspaceModel implements Disposable, Partial { } protected notifyWorkpaces(): void { + this.initialized = true; let infos = Array.from(this.workspaces.values()); - infos = infos.filter(ws => !this.active || this.isActive(ws)); if (this.searchTerm) { infos = infos.filter(ws => (ws.workspace.description+ws.workspace.id+ws.workspace.contextURL+ws.workspace.context).toLowerCase().indexOf(this.searchTerm!.toLowerCase()) !== -1); } infos = infos.sort((a,b) => { return WorkspaceInfo.lastActiveISODate(b).localeCompare(WorkspaceInfo.lastActiveISODate(a)); }); - this.setWorkspaces(infos.slice(0, this.internalLimit)); + const activeInfo = infos.filter(ws => this.isActive(ws)); + const inActiveInfo = infos.filter(ws => !this.isActive(ws)); + this.setActiveWorkspaces(activeInfo); + this.setInActiveWorkspaces(inActiveInfo.slice(0, this.internalLimit - activeInfo.length)); } protected isActive(info: WorkspaceInfo): boolean { diff --git a/components/gitpod-db/package.json b/components/gitpod-db/package.json index 086768d0e5bc14..c7588521e3e4a5 100644 --- a/components/gitpod-db/package.json +++ b/components/gitpod-db/package.json @@ -8,6 +8,7 @@ "build:clean": "yarn clean && yarn build", "rebuild": "yarn build:clean", "build:watch": "watch 'yarn build' .", + "test": "leeway build components/gitpod-db:dbtest", "watch": "leeway exec --package .:lib --transitive-dependencies --filter-type yarn --components --parallel -- tsc -w --preserveWatchOutput", "db-test": "r(){ . $(leeway run components/gitpod-db:db-test-env); yarn db-test-run; };r", "db-test-run": "mocha --opts mocha.opts '**/*.spec.db.ts' --exclude './node_modules/**'", diff --git a/components/gitpod-db/src/typeorm/workspace-db-impl.ts b/components/gitpod-db/src/typeorm/workspace-db-impl.ts index ca2789d0cbf4ed..0a4047bcde513a 100644 --- a/components/gitpod-db/src/typeorm/workspace-db-impl.ts +++ b/components/gitpod-db/src/typeorm/workspace-db-impl.ts @@ -5,7 +5,7 @@ */ import { injectable, inject } from "inversify"; -import { Repository, EntityManager, DeepPartial, UpdateQueryBuilder } from "typeorm"; +import { Repository, EntityManager, DeepPartial, UpdateQueryBuilder, Brackets } from "typeorm"; import { MaybeWorkspace, MaybeWorkspaceInstance, WorkspaceDB, FindWorkspacesOptions, PrebuiltUpdatableAndWorkspace, WorkspaceInstanceSessionWithWorkspace, PrebuildWithWorkspace, WorkspaceAndOwner, WorkspacePortsAuthData, WorkspaceOwnerAndSoftDeleted } from "../workspace-db"; import { Workspace, WorkspaceInstance, WorkspaceInfo, WorkspaceInstanceUser, WhitelistedRepository, Snapshot, LayoutData, PrebuiltWorkspace, RunningWorkspaceInfo, PrebuiltWorkspaceUpdatable, WorkspaceAndInstance, WorkspaceType, PrebuildInfo } from "@gitpod/gitpod-protocol"; import { TypeORM } from "./typeorm"; @@ -144,13 +144,7 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB { // We need to put the subquery into the join condition (ON) here to be able to reference `ws.id` which is // not possible in a subquery on JOIN (e.g. 'LEFT JOIN (SELECT ... WHERE i.workspaceId = ws.id)') .leftJoinAndMapOne('ws.latestInstance', DBWorkspaceInstance, 'wsi', - `wsi.id = ( - SELECT i.id - FROM d_b_workspace_instance AS i - WHERE i.workspaceId = ws.id - ORDER BY i.creationTime DESC - LIMIT 1 - )` + `wsi.id = (SELECT i.id FROM d_b_workspace_instance AS i WHERE i.workspaceId = ws.id ORDER BY i.creationTime DESC LIMIT 1)` ) .leftJoin((qb) => { return qb.select('workspaceId') @@ -172,10 +166,23 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB { if (options.pinnedOnly) { qb.andWhere("ws.pinned = true"); } - if (options.projectId) { - qb.andWhere('ws.projectId = :projectId', { projectId: options.projectId }) + const projectIds = typeof options.projectId === 'string' ? [options.projectId] : options.projectId; + if (Array.isArray(projectIds)) { + if (projectIds.length === 0 && !options.includeWithoutProject) { + // user passed an empty array of projectids and also is not interested in unassigned workspaces -> no results + return []; + } + qb.andWhere(new Brackets(qb => { + if (projectIds.length > 0) { + qb.where('ws.projectId IN (:pids)', { pids: projectIds }); + if (options.includeWithoutProject) { + qb.orWhere("ws.projectId IS NULL"); + } + } else if (options.includeWithoutProject) { + qb.where("ws.projectId IS NULL"); + } + })); } - const rawResults = await qb.getMany() as any as (Workspace & { latestInstance?: WorkspaceInstance })[]; // see leftJoinAndMapOne above return rawResults.map(r => { const workspace = { ...r }; diff --git a/components/gitpod-db/src/workspace-db.spec.db.ts b/components/gitpod-db/src/workspace-db.spec.db.ts index 280c25b8614648..426a4b070253fa 100644 --- a/components/gitpod-db/src/workspace-db.spec.db.ts +++ b/components/gitpod-db/src/workspace-db.spec.db.ts @@ -25,6 +25,9 @@ import { DBWorkspaceInstance } from './typeorm/entity/db-workspace-instance'; readonly timeWs = new Date(2018, 2, 16, 10, 0, 0).toISOString(); readonly timeBefore = new Date(2018, 2, 16, 11, 5, 10).toISOString(); readonly timeAfter = new Date(2019, 2, 16, 11, 5, 10).toISOString(); + readonly userId = '12345'; + readonly projectAID = 'projectA'; + readonly projectBID = 'projectB'; readonly ws: Workspace = { id: '1', type: 'regular', @@ -34,10 +37,11 @@ import { DBWorkspaceInstance } from './typeorm/entity/db-workspace-instance'; image: '', tasks: [] }, + projectId: this.projectAID, context: { title: 'example' }, contextURL: 'example.org', description: 'blabla', - ownerId: '12345' + ownerId: this.userId }; readonly wsi1: WorkspaceInstance = { workspaceId: this.ws.id, @@ -90,10 +94,11 @@ import { DBWorkspaceInstance } from './typeorm/entity/db-workspace-instance'; image: '', tasks: [] }, + projectId: this.projectBID, context: { title: 'example' }, contextURL: 'https://github.com/gitpod-io/gitpod', description: 'Gitpod', - ownerId: '12345' + ownerId: this.userId }; readonly ws2i1: WorkspaceInstance = { workspaceId: this.ws2.id, @@ -347,5 +352,101 @@ import { DBWorkspaceInstance } from './typeorm/entity/db-workspace-instance'; expect(workspaceAndInstance.instanceId).to.eq(this.wsi1.id) }); } + + @test(timeout(10000)) + public async testFind_ByProjectIds() { + await this.db.transaction(async db => { + await Promise.all([ + db.store(this.ws), + db.storeInstance(this.wsi1), + db.storeInstance(this.wsi2), + db.store(this.ws2), + db.storeInstance(this.ws2i1), + ]); + const dbResult = await db.find({ + userId: this.userId, + includeHeadless: false, + projectId: [this.projectAID], + includeWithoutProject: false + }); + + // It should only find one workspace instance + expect(dbResult.length).to.eq(1); + + expect(dbResult[0].workspace.id).to.eq(this.ws.id); + }); + } + + @test(timeout(10000)) + public async testFind_ByProjectIds_01() { + await this.db.transaction(async db => { + await Promise.all([ + db.store(this.ws), + db.storeInstance(this.wsi1), + db.storeInstance(this.wsi2), + db.store(this.ws2), + db.storeInstance(this.ws2i1), + ]); + const dbResult = await db.find({ + userId: this.userId, + includeHeadless: false, + projectId: [this.projectBID], + includeWithoutProject: false + }); + + // It should only find one workspace instance + expect(dbResult.length).to.eq(1); + + expect(dbResult[0].workspace.id).to.eq(this.ws2.id); + }); + } + + @test(timeout(10000)) + public async testFind_ByProjectIds_02() { + await this.db.transaction(async db => { + await Promise.all([ + db.store(this.ws), + db.storeInstance(this.wsi1), + db.storeInstance(this.wsi2), + db.store(this.ws2), + db.storeInstance(this.ws2i1), + ]); + const dbResult = await db.find({ + userId: this.userId, + includeHeadless: false, + projectId: [this.projectAID, this.projectBID], + includeWithoutProject: false + }); + + expect(dbResult.length).to.eq(2); + + expect(dbResult[0].workspace.id).to.eq(this.ws.id); + expect(dbResult[1].workspace.id).to.eq(this.ws2.id); + }); + } + + @test(timeout(10000)) + public async testFind_ByProjectIds_03() { + await this.db.transaction(async db => { + await Promise.all([ + db.store(this.ws), + db.storeInstance(this.wsi1), + db.storeInstance(this.wsi2), + db.store(this.ws2), + db.storeInstance(this.ws2i1), + ]); + const dbResult = await db.find({ + userId: this.userId, + includeHeadless: false, + projectId: [], + includeWithoutProject: false + }); + + expect(dbResult.length).to.eq(0); + + // expect(dbResult[0].workspace.id).to.eq(this.ws.id); + // expect(dbResult[1].workspace.id).to.eq(this.ws2.id); + }); + } } module.exports = new WorkspaceDBSpec() diff --git a/components/gitpod-db/src/workspace-db.ts b/components/gitpod-db/src/workspace-db.ts index 95d741978c6b5f..7233e0c5324585 100644 --- a/components/gitpod-db/src/workspace-db.ts +++ b/components/gitpod-db/src/workspace-db.ts @@ -13,7 +13,8 @@ export type MaybeWorkspaceInstance = WorkspaceInstance | undefined; export interface FindWorkspacesOptions { userId: string - projectId?: string + projectId?: string | string[] + includeWithoutProject?: boolean; limit?: number searchString?: string includeHeadless?: boolean diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 29cc2f09be4a5f..abc48613d4faac 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -313,7 +313,8 @@ export namespace GitpodServer { limit?: number; searchString?: string; pinnedOnly?: boolean; - projectId?: string; + projectId?: string | string[]; + includeWithoutProject?: boolean; } export interface GetAccountStatementOptions { date?: string;
Unpinned workspaces that have been stopped for more than 14 days will be automatically deleted. Learn more