Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dashboard] Allow workspace renaming #5695

Merged
merged 2 commits into from
Sep 16, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 72 additions & 9 deletions components/dashboard/src/workspaces/WorkspaceEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
import { CommitContext, Workspace, WorkspaceInfo, WorkspaceInstance, WorkspaceInstanceConditions, WorkspaceInstancePhase } from '@gitpod/gitpod-protocol';
import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url';
import moment from 'moment';
import React, { useState } from 'react';
import React, { useRef, useState } from 'react';
import ConfirmationModal from '../components/ConfirmationModal';
import Modal from '../components/Modal';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Good to see the modal component in action again! ➿

import { ContextMenuEntry } from '../components/ContextMenu';
import { Item, ItemField, ItemFieldContextMenu, ItemFieldIcon } from '../components/ItemsList';
import PendingChangesDropdown from '../components/PendingChangesDropdown';
import Tooltip from '../components/Tooltip';
import { WorkspaceModel } from './workspace-model';
import { getGitpodService } from '../service/service';

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

export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
const [isModalVisible, setModalVisible] = useState(false);
const [isDeleteModalVisible, setDeleteModalVisible] = useState(false);
const [isRenameModalVisible, setRenameModalVisible] = useState(false);
const renameInputRef = useRef<HTMLInputElement>(null);
const [errorMessage, setErrorMessage] = useState('');
const state: WorkspaceInstancePhase = desc.latestInstance?.status?.phase || 'stopped';
const currentBranch = desc.latestInstance?.status.repo?.branch || Workspace.getBranchName(desc.workspace) || '<unknown>';
const ws = desc.workspace;
const [workspaceDescription, setWsDescription] = useState(ws.description);

const startUrl = new GitpodHostUrl(window.location.href).with({
pathname: '/start/',
hash: '#' + ws.id
Expand All @@ -45,7 +52,16 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
{
title: 'Open',
href: startUrl.toString()
}];
},
{
title: 'Rename',
href: "",
onClick: () => {
setRenameModalVisible(true);
}
},

];
if (state === 'running') {
menuEntries.push({
title: 'Stop',
Expand Down Expand Up @@ -78,13 +94,41 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
title: 'Delete',
customFontStyle: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300',
onClick: () => {
setModalVisible(true);
setDeleteModalVisible(true);
}
}
);
}
const project = getProject(ws);

const updateWorkspaceDescription = async () => {
// Need this check because ref is called twice
// https://reactjs.org/docs/refs-and-the-dom.html#caveats-with-callback-refs
if (!renameInputRef.current) {
return;
}

try {
if (renameInputRef.current!.value.length === 0) {
setErrorMessage('Description cannot not be empty.');
return false;
}

if (renameInputRef.current!.value.length > 250) {
setErrorMessage('Description is too long for readability.');
return false;
}

setWsDescription(renameInputRef.current!.value);
await getGitpodService().server.setWorkspaceDescription(ws.id, renameInputRef.current!.value);
setErrorMessage('');
setRenameModalVisible(false);
} catch (error) {
console.error(error);
window.alert("Something went wrong. Please try renaming again.");
}
}

return <Item className="whitespace-nowrap py-6 px-6">
<ItemFieldIcon>
<WorkspaceStatusIndicator instance={desc?.latestInstance} />
Expand All @@ -96,7 +140,7 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
</Tooltip>
</ItemField>
<ItemField className="w-4/12 flex flex-col">
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">{ws.description}</div>
<div className="text-gray-500 dark:text-gray-400 overflow-ellipsis truncate">{workspaceDescription}</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: Thinking of this PR and since this was a feature requested from some community members would it be interesting to measure how much people use this feature? Maybe have a self-serve graph of how many of the existing workspaces are renamed per day or week? Maybe also how many times people clicked the rename option in the dropdown option? It could be interesting to also see if the usage of this feature decreases after #3594. What do you think? Feel free to drop the idea if this would increase the scope of these changes. 💭

Cc @jakobhero

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi: Discussed this briefly with @svenefftinge earlier in the product sync meeting and we decided that adding tracking events here is a low priority given that the renaming functionality will be hidden behind the more actions dropdown. Up to you @laushinka and @jakobhero to decide if it's worth adding any tracking events within the scope of this PR. 🏓

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that it would be useful to keep track but that we could do it in a separate issue if @jakobhero also thinks it's worth to track 🙏🏽

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey there!👋 all clicks on buttons, anchors or clickable divs in the dashboard will be automatically tracked with the project & teams tracking PR i am currently working on. this means that we will see how many people clicked on the rename button, but now how many renames were successfully performed (which probably would be best tracked on the server). i think having visibility on how many users clicked on the button to open the modal is a good start to see how important this is to users and sufficient for now, wdyt?

<a href={ws.contextURL}>
<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>
</a>
Expand All @@ -111,18 +155,37 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
</Tooltip>
</ItemField>
<ItemFieldContextMenu menuEntries={menuEntries} />
{isModalVisible && <ConfirmationModal
{isDeleteModalVisible && <ConfirmationModal
title="Delete Workspace"
areYouSureText="Are you sure you want to delete this workspace?"
children={{
name: ws.id,
description: ws.description,
}}
buttonText="Delete Workspace"
visible={isModalVisible}
onClose={() => setModalVisible(false)}
visible={isDeleteModalVisible}
onClose={() => setDeleteModalVisible(false)}
onConfirm={() => model.deleteWorkspace(ws.id)}
/>}
<Modal visible={isRenameModalVisible} onClose={() => setRenameModalVisible(false)} onEnter={() => { updateWorkspaceDescription(); return isRenameModalVisible }}>
<h3 className="mb-4">Rename Workspace Description</h3>
<div className="border-t border-b border-gray-200 dark:border-gray-800 -mx-6 px-6 py-4 space-y-2">
{errorMessage.length > 0 ?
<div className="bg-gitpod-kumquat-light rounded-md p-3 text-gitpod-red text-sm mb-2">
{errorMessage}
</div>
: null}
<input className="w-full truncate" type="text" defaultValue={workspaceDescription} ref={renameInputRef} />
<div className="mt-1">
<p className="text-gray-500">Change the description to make it easier to go back to a workspace.</p>
<p className="text-gray-500">Workspace URLs and endpoints will remain the same.</p>
</div>
</div>
<div className="flex justify-end mt-6">
<button className="secondary" onClick={() => setRenameModalVisible(false)}>Cancel</button>
<button className="ml-2" type="submit" onClick={updateWorkspaceDescription}>Update Description</button>
</div>
</Modal>
</Item>;
}

Expand All @@ -134,7 +197,7 @@ export function getProject(ws: Workspace) {
}
}

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