Skip to content

Add a remove project button on the project settings page #15316

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

Merged
merged 3 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions components/dashboard/src/contexts/FeatureFlagContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@ const FeatureFlagContextProvider: React.FC = ({ children }) => {
};

export { FeatureFlagContext, FeatureFlagContextProvider };

export const useFeatureFlags = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes it just a bit simpler to access feature flags from a component.

return useContext(FeatureFlagContext);
};
87 changes: 63 additions & 24 deletions components/dashboard/src/projects/ProjectSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* See License.AGPL.txt in the project root for license information.
*/

import { useContext, useEffect, useState } from "react";
import { useLocation } from "react-router";
import { useCallback, useContext, useEffect, useState } from "react";
import { useLocation, useHistory } from "react-router";
import { Project, ProjectSettings, Team } from "@gitpod/gitpod-protocol";
import CheckBox from "../components/CheckBox";
import { getGitpodService } from "../service/service";
Expand All @@ -17,6 +17,7 @@ import SelectWorkspaceClass from "../settings/selectClass";
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
import Alert from "../components/Alert";
import { Link } from "react-router-dom";
import { RemoveProjectModal } from "./RemoveProjectModal";

export function getProjectSettingsMenu(project?: Project, team?: Team) {
const teamOrUserSlug = !!team ? "t/" + team.slug : "projects";
Expand Down Expand Up @@ -51,8 +52,11 @@ export function ProjectSettingsPage(props: { project?: Project; children?: React
export default function () {
const { project, setProject } = useContext(ProjectContext);
const [billingMode, setBillingMode] = useState<BillingMode | undefined>(undefined);
const [showRemoveModal, setShowRemoveModal] = useState(false);
const { teams } = useContext(TeamsContext);
const team = getCurrentTeam(useLocation(), teams);
const history = useHistory();

useEffect(() => {
if (team) {
getGitpodService().server.getBillingModeForTeam(team.id).then(setBillingMode);
Expand All @@ -61,33 +65,52 @@ export default function () {
}
}, [team]);

if (!project) return null;
const updateProjectSettings = useCallback(
Copy link
Contributor Author

@selfcontained selfcontained Dec 12, 2022

Choose a reason for hiding this comment

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

Wrapped each of these functions with useCallback() to avoid creating new references for each render. The guts of each function shouldn't have changed at all.

(settings: ProjectSettings) => {
if (!project) return;

const updateProjectSettings = (settings: ProjectSettings) => {
if (!project) return;
const newSettings = { ...project.settings, ...settings };
getGitpodService().server.updateProjectPartial({ id: project.id, settings: newSettings });
setProject({ ...project, settings: newSettings });
},
[project, setProject],
);

const newSettings = { ...project.settings, ...settings };
getGitpodService().server.updateProjectPartial({ id: project.id, settings: newSettings });
setProject({ ...project, settings: newSettings });
};
const setWorkspaceClass = useCallback(
async (value: string) => {
if (!project) {
return value;
}
const before = project.settings?.workspaceClasses?.regular;
updateProjectSettings({ workspaceClasses: { ...project.settings?.workspaceClasses, regular: value } });
return before;
},
[project, updateProjectSettings],
);

const setWorkspaceClass = async (value: string) => {
if (!project) {
return value;
}
const before = project.settings?.workspaceClasses?.regular;
updateProjectSettings({ workspaceClasses: { ...project.settings?.workspaceClasses, regular: value } });
return before;
};
const setWorkspaceClassForPrebuild = useCallback(
async (value: string) => {
if (!project) {
return value;
}
const before = project.settings?.workspaceClasses?.prebuild;
updateProjectSettings({ workspaceClasses: { ...project.settings?.workspaceClasses, prebuild: value } });
return before;
},
[project, updateProjectSettings],
);

const setWorkspaceClassForPrebuild = async (value: string) => {
if (!project) {
return value;
const onProjectRemoved = useCallback(() => {
// if there's a current team, navigate to team projects
if (team) {
history.push(`/t/${team.slug}/projects`);
} else {
history.push("/projects");
}
const before = project.settings?.workspaceClasses?.prebuild;
updateProjectSettings({ workspaceClasses: { ...project.settings?.workspaceClasses, prebuild: value } });
return before;
};
}, [history, team]);

// TODO: Render a generic error screen for when an entity isn't found
if (!project) return null;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should live after all hooks, so bumped it down since I added a few useCallback() wrappers on the fns that weren't there before.


return (
<ProjectSettingsPage project={project}>
Expand Down Expand Up @@ -237,6 +260,22 @@ export default function () {
</Alert>
)}
</div>
<div>
<h3 className="mt-12">Delete Project</h3>
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: What do you think of using a different verb here to avoid causing any confusion whether this is also going to delete a project or repository on the provider?

Suggested change
<h3 className="mt-12">Delete Project</h3>
<h3 className="mt-12">Remove Project</h3>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like it. I’ll include this change in a follow up pr

<p className="text-base text-gray-500 dark:text-gray-400 pb-4">
Removing the project from this team will also remove team members access to it.
</p>
<button className="danger secondary" onClick={() => setShowRemoveModal(true)}>
Delete Project
</button>
</div>
{showRemoveModal && (
<RemoveProjectModal
project={project}
onRemoved={onProjectRemoved}
onClose={() => setShowRemoveModal(false)}
/>
)}
</ProjectSettingsPage>
);
}
48 changes: 48 additions & 0 deletions components/dashboard/src/projects/RemoveProjectModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { FunctionComponent, useCallback, useState } from "react";
import type { Project } from "@gitpod/gitpod-protocol";
import { projectsService } from "../service/public-api";
import { getGitpodService } from "../service/service";
import ConfirmationModal from "../components/ConfirmationModal";
import { useFeatureFlags } from "../contexts/FeatureFlagContext";

type RemoveProjectModalProps = {
project: Project;
onClose: () => void;
onRemoved: () => void;
};

export const RemoveProjectModal: FunctionComponent<RemoveProjectModalProps> = ({ project, onClose, onRemoved }) => {
const { usePublicApiProjectsService } = useFeatureFlags();
const [disabled, setDisabled] = useState(false);

const removeProject = useCallback(async () => {
setDisabled(true);
usePublicApiProjectsService
? await projectsService.deleteProject({ projectId: project.id })
: await getGitpodService().server.deleteProject(project.id);
setDisabled(false);
onRemoved();
}, [onRemoved, project.id, usePublicApiProjectsService]);

return (
<ConfirmationModal
title="Remove Project"
areYouSureText="Are you sure you want to remove this project from this team? Team members will also lose access to this project."
Copy link
Contributor

Choose a reason for hiding this comment

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

issue(non-blocking): This is out of the scope here, but there's a top margin here that's not needed in the modal component. Posting below how the modal would look like be ideally for visibility. However, feel free to leave this as is.

BEFORE AFTER
modal-before modal-after

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it, I'll update the confirmation modal in the follow up too.

children={{
name: project?.name ?? "",
description: project?.cloneUrl ?? "",
}}
buttonText="Remove Project"
buttonDisabled={disabled}
onClose={onClose}
onConfirm={removeProject}
visible
/>
);
};