Skip to content

Commit e53e486

Browse files
author
Laurie T. Malau
committed
[dashboard] Allow workspace renaming
Fixes #3946
1 parent 82df304 commit e53e486

File tree

1 file changed

+72
-9
lines changed

1 file changed

+72
-9
lines changed

components/dashboard/src/workspaces/WorkspaceEntry.tsx

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
import { CommitContext, Workspace, WorkspaceInfo, WorkspaceInstance, WorkspaceInstanceConditions, WorkspaceInstancePhase } from '@gitpod/gitpod-protocol';
88
import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url';
99
import moment from 'moment';
10-
import React, { useState } from 'react';
10+
import React, { useRef, useState } from 'react';
1111
import ConfirmationModal from '../components/ConfirmationModal';
12+
import Modal from '../components/Modal';
1213
import { ContextMenuEntry } from '../components/ContextMenu';
1314
import { Item, ItemField, ItemFieldContextMenu, ItemFieldIcon } from '../components/ItemsList';
1415
import PendingChangesDropdown from '../components/PendingChangesDropdown';
1516
import Tooltip from '../components/Tooltip';
1617
import { WorkspaceModel } from './workspace-model';
18+
import { getGitpodService } from '../service/service';
1719

1820
function getLabel(state: WorkspaceInstancePhase, conditions?: WorkspaceInstanceConditions) {
1921
if (conditions?.failed) {
@@ -30,10 +32,15 @@ interface Props {
3032
}
3133

3234
export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
33-
const [isModalVisible, setModalVisible] = useState(false);
35+
const [isDeleteModalVisible, setDeleteModalVisible] = useState(false);
36+
const [isRenameModalVisible, setRenameModalVisible] = useState(false);
37+
const renameInputRef = useRef<HTMLInputElement>(null);
38+
const [errorMessage, setErrorMessage] = useState('');
3439
const state: WorkspaceInstancePhase = desc.latestInstance?.status?.phase || 'stopped';
3540
const currentBranch = desc.latestInstance?.status.repo?.branch || Workspace.getBranchName(desc.workspace) || '<unknown>';
3641
const ws = desc.workspace;
42+
const [workspaceDescription, setWsDescription] = useState(ws.description);
43+
3744
const startUrl = new GitpodHostUrl(window.location.href).with({
3845
pathname: '/start/',
3946
hash: '#' + ws.id
@@ -45,7 +52,16 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
4552
{
4653
title: 'Open',
4754
href: startUrl.toString()
48-
}];
55+
},
56+
{
57+
title: 'Rename',
58+
href: "",
59+
onClick: () => {
60+
setRenameModalVisible(true);
61+
}
62+
},
63+
64+
];
4965
if (state === 'running') {
5066
menuEntries.push({
5167
title: 'Stop',
@@ -78,13 +94,41 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
7894
title: 'Delete',
7995
customFontStyle: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300',
8096
onClick: () => {
81-
setModalVisible(true);
97+
setDeleteModalVisible(true);
8298
}
8399
}
84100
);
85101
}
86102
const project = getProject(ws);
87103

104+
const updateWorkspaceDescription = async () => {
105+
// Need this check because ref is called twice
106+
// https://reactjs.org/docs/refs-and-the-dom.html#caveats-with-callback-refs
107+
if (!renameInputRef.current) {
108+
return;
109+
}
110+
111+
try {
112+
if (renameInputRef.current!.value.length === 0) {
113+
setErrorMessage('Description must not be empty.');
114+
return false;
115+
}
116+
117+
if (renameInputRef.current!.value.length > 250) {
118+
setErrorMessage('Description is too long for readability.');
119+
return false;
120+
}
121+
122+
setWsDescription(renameInputRef.current!.value);
123+
await getGitpodService().server.setWorkspaceDescription(ws.id, renameInputRef.current!.value);
124+
setErrorMessage('');
125+
setRenameModalVisible(false);
126+
} catch (error) {
127+
console.error(error);
128+
window.alert("Something went wrong. Please try renaming again.");
129+
}
130+
}
131+
88132
return <Item className="whitespace-nowrap py-6 px-6">
89133
<ItemFieldIcon>
90134
<WorkspaceStatusIndicator instance={desc?.latestInstance} />
@@ -96,7 +140,7 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
96140
</Tooltip>
97141
</ItemField>
98142
<ItemField className="w-4/12 flex flex-col">
99-
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">{ws.description}</div>
143+
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">{workspaceDescription}</div>
100144
<a href={ws.contextURL}>
101145
<div className="text-sm text-gray-400 dark:text-gray-500 overflow-ellipsis truncate hover:text-blue-600 dark:hover:text-blue-400">{ws.contextURL}</div>
102146
</a>
@@ -111,18 +155,37 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
111155
</Tooltip>
112156
</ItemField>
113157
<ItemFieldContextMenu menuEntries={menuEntries} />
114-
{isModalVisible && <ConfirmationModal
158+
{isDeleteModalVisible && <ConfirmationModal
115159
title="Delete Workspace"
116160
areYouSureText="Are you sure you want to delete this workspace?"
117161
children={{
118162
name: ws.id,
119163
description: ws.description,
120164
}}
121165
buttonText="Delete Workspace"
122-
visible={isModalVisible}
123-
onClose={() => setModalVisible(false)}
166+
visible={isDeleteModalVisible}
167+
onClose={() => setDeleteModalVisible(false)}
124168
onConfirm={() => model.deleteWorkspace(ws.id)}
125169
/>}
170+
<Modal visible={isRenameModalVisible} onClose={() => setRenameModalVisible(false)} onEnter={() => { updateWorkspaceDescription(); return isRenameModalVisible }}>
171+
<h3 className="mb-4">Rename Workspace Description</h3>
172+
<div className="border-t border-b border-gray-200 dark:border-gray-800 -mx-6 px-6 py-4 space-y-2">
173+
{errorMessage.length > 0 ?
174+
<div className="bg-gitpod-kumquat-light rounded-md p-3 text-gitpod-red text-sm mb-2">
175+
{errorMessage}
176+
</div>
177+
: null}
178+
<input className="w-full truncate" type="text" defaultValue={workspaceDescription} ref={renameInputRef} />
179+
<div className="mt-1">
180+
<p className="text-gray-500">Change the description to make it easier to go back to a workspace.</p>
181+
<p className="text-gray-500">Workspace URLs and endpoints will remain the same.</p>
182+
</div>
183+
</div>
184+
<div className="flex justify-end mt-6">
185+
<button className="secondary" onClick={() => setRenameModalVisible(false)}>Cancel</button>
186+
<button className="ml-2" type="submit" onClick={updateWorkspaceDescription}>Rename</button>
187+
</div>
188+
</Modal>
126189
</Item>;
127190
}
128191

@@ -134,7 +197,7 @@ export function getProject(ws: Workspace) {
134197
}
135198
}
136199

137-
export function WorkspaceStatusIndicator({instance}: {instance?: WorkspaceInstance}) {
200+
export function WorkspaceStatusIndicator({ instance }: { instance?: WorkspaceInstance }) {
138201
const state: WorkspaceInstancePhase = instance?.status?.phase || 'stopped';
139202
const conditions = instance?.status?.conditions;
140203
let stateClassName = 'rounded-full w-3 h-3 text-sm align-middle';

0 commit comments

Comments
 (0)