Skip to content

Commit

Permalink
Rename and delete projects (#880)
Browse files Browse the repository at this point in the history
* Side menu: selector title changed to project. Changed section headers and made it Slack + Discord

* Added project settings page. Renaming is working

* Added project.deletedAt column

* Deleting projects WIP

* Allow clearing the project from the session

* Don’t load projects that are deleted

* Improved the project selection logic

* Clear the session project id when deleting a project

* Deleting the last project in an org now works. The new project page doesn’t have the sidebar anymore.

* Removed false code comment

* Added the JobRun index back in, using Prisma. Otherwise it tries to remove it on each migration

* Only return environments where the project isn’t deleted. This will block API calls

* Do the project deletedAt check in code, not SQL

* Made it clearer what the code does with a comment and applied the same logic to the public api key
  • Loading branch information
matt-aitken committed Feb 5, 2024
1 parent a9ff324 commit 29edcd3
Show file tree
Hide file tree
Showing 16 changed files with 542 additions and 73 deletions.
45 changes: 22 additions & 23 deletions apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
ArrowRightOnRectangleIcon,
ChartBarIcon,
CursorArrowRaysIcon,
EllipsisHorizontalIcon,
ShieldCheckIcon,
} from "@heroicons/react/20/solid";
import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
Expand All @@ -15,6 +14,7 @@ import { useFeatures } from "~/hooks/useFeatures";
import { MatchedOrganization } from "~/hooks/useOrganizations";
import { MatchedProject } from "~/hooks/useProject";
import { User } from "~/models/user.server";
import { useV3Enabled } from "~/root";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { cn } from "~/utils/cn";
import {
Expand All @@ -33,6 +33,7 @@ import {
projectHttpEndpointsPath,
projectPath,
projectRunsPath,
projectSettingsPath,
projectSetupPath,
projectTriggersPath,
} from "~/utils/pathBuilder";
Expand All @@ -56,9 +57,8 @@ import {
PopoverSectionHeader,
} from "../primitives/Popover";
import { StepNumber } from "../primitives/StepNumber";
import { MenuCount, SideMenuItem } from "./SideMenuItem";
import { SideMenuHeader } from "./SideMenuHeader";
import { useV3Enabled } from "~/root";
import { MenuCount, SideMenuItem } from "./SideMenuItem";

type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
type SideMenuProject = Pick<
Expand Down Expand Up @@ -106,19 +106,15 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
showHeaderDivider ? " border-border" : "border-transparent"
)}
>
<ProjectSelector
organization={organization}
organizations={organizations}
project={project}
/>
<ProjectSelector organizations={organizations} project={project} />
<UserMenu user={user} />
</div>
<div
className="h-full overflow-hidden overflow-y-auto pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
ref={borderRef}
>
<div className="mb-6 flex flex-col gap-1 px-1">
<SideMenuHeader title={project.name || "No project found"}>
<SideMenuHeader title={"Project"}>
<PopoverMenuItem
to={projectSetupPath(organization, project)}
title="Framework setup"
Expand Down Expand Up @@ -168,9 +164,16 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
to={projectEnvironmentsPath(organization, project)}
data-action="environments & api keys"
/>
<SideMenuItem
name="Project settings"
icon="settings"
iconColor="text-teal-500"
to={projectSettingsPath(organization, project)}
data-action="project-settings"
/>
</div>
<div className="mb-1 flex flex-col gap-1 px-1">
<SideMenuHeader title={organization.title}>
<SideMenuHeader title={"Organization"}>
<PopoverMenuItem to={newProjectPath(organization)} title="New Project" icon="plus" />
<PopoverMenuItem
to={inviteTeamMemberPath(organization)}
Expand Down Expand Up @@ -209,7 +212,7 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
</div>
</div>
<div className="flex flex-col gap-1 border-t border-border p-1">
{currentPlan?.subscription?.isPaying === true ? (
{currentPlan?.subscription?.isPaying === true && (
<Dialog>
<DialogTrigger asChild>
<Button
Expand Down Expand Up @@ -265,16 +268,14 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
</div>
</DialogContent>
</Dialog>
) : (
<SideMenuItem
name="Join our Discord"
icon={DiscordIcon}
to="https://trigger.dev/discord"
data-action="join our discord"
target="_blank"
/>
)}

<SideMenuItem
name="Join our Discord"
icon={DiscordIcon}
to="https://trigger.dev/discord"
data-action="join our discord"
target="_blank"
/>
<SideMenuItem
name="Documentation"
icon="docs"
Expand Down Expand Up @@ -317,11 +318,9 @@ export function SideMenu({ user, project, organization, organizations }: SideMen

function ProjectSelector({
project,
organization,
organizations,
}: {
project: SideMenuProject;
organization: MatchedOrganization;
organizations: MatchedOrganization[];
}) {
const [isOrgMenuOpen, setOrgMenuOpen] = useState(false);
Expand All @@ -339,7 +338,7 @@ function ProjectSelector({
className="h-7 w-full justify-between overflow-hidden py-1 pl-2"
>
<LogoIcon className="relative -top-px mr-2 h-4 w-4 min-w-[1rem]" />
<span className="truncate">{organization.title ?? "Select an organization"}</span>
<span className="truncate">{project.name ?? "Select a project"}</span>
</PopoverArrowTrigger>
<PopoverContent
className="min-w-[16rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
Expand Down
25 changes: 8 additions & 17 deletions apps/webapp/app/models/runtimeEnvironment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export async function findEnvironmentByApiKey(apiKey: string) {
},
});

//don't return deleted projects
if (environment?.project.deletedAt !== null) {
return null;
}

return environment;
}

Expand All @@ -28,24 +33,10 @@ export async function findEnvironmentByPublicApiKey(apiKey: string) {
},
});

return environment;
}

export async function getEnvironmentForOrganization(organizationSlug: string, slug: string) {
const organization = await prisma.organization.findUnique({
where: {
slug: organizationSlug,
},
include: {
environments: true,
},
});

if (!organization) {
return;
//don't return deleted projects
if (environment?.project.deletedAt !== null) {
return null;
}

const environment = organization.environments.find((environment) => environment.slug === slug);

return environment;
}
56 changes: 45 additions & 11 deletions apps/webapp/app/presenters/OrganizationsPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { PrismaClient } from "@trigger.dev/database";
import { redirect } from "remix-typedjson";
import { prisma } from "~/db.server";
import {
clearCurrentProjectId,
commitCurrentProjectSession,
getCurrentProjectId,
setCurrentProjectId,
} from "~/services/currentProject.server";
import { logger } from "~/services/logger.server";
import { newProjectPath } from "~/utils/pathBuilder";
import { ProjectPresenter } from "./ProjectPresenter.server";
import { redirectWithErrorMessage } from "~/models/message.server";
import { match } from "assert";

export class OrganizationsPresenter {
#prismaClient: PrismaClient;
Expand Down Expand Up @@ -54,7 +57,11 @@ export class OrganizationsPresenter {
});

if (!project) {
throw new Response("Project not found", { status: 404 });
throw redirectWithErrorMessage(
newProjectPath({ slug: organizationSlug }),
request,
"No projects found in organization"
);
}

return { organizations, organization, project };
Expand All @@ -75,16 +82,21 @@ export class OrganizationsPresenter {

//no project in session, let's set one
if (!sessionProjectId) {
//no session id and no project slug so we need to select the best project
if (!projectSlug) {
const bestProject = await this.#selectBestProjectForOrganization(organizationSlug, userId);
const bestProject = await this.#selectBestProjectForOrganization(
organizationSlug,
userId,
request
);
const session = await setCurrentProjectId(bestProject.id, request);
throw redirect(request.url, {
headers: { "Set-Cookie": await commitCurrentProjectSession(session) },
});
}

//use the project param to find the project
const project = await prisma.project.findFirst({
//get all the projects
const projects = await prisma.project.findMany({
select: {
id: true,
slug: true,
Expand All @@ -93,21 +105,37 @@ export class OrganizationsPresenter {
organization: {
slug: organizationSlug,
},
deletedAt: null,
slug: projectSlug,
},
orderBy: {
updatedAt: "desc",
},
});

if (!project) {
throw redirect(newProjectPath({ slug: organizationSlug }));
if (projects.length === 0) {
throw redirectWithErrorMessage(
newProjectPath({ slug: organizationSlug }),
request,
"No projects in this organization"
);
}

const session = await setCurrentProjectId(project.id, request);
//try get the project which matches the URL
let matchingProject = projects.find((p) => p.slug === projectSlug);

//if there's no matching project, just use the most recently updated one
if (!matchingProject) {
matchingProject = projects[0];
}

//set the session
const session = await setCurrentProjectId(matchingProject.id, request);
throw redirect(request.url, {
headers: { "Set-Cookie": await commitCurrentProjectSession(session) },
});
}

//no project slug, so just return the session id
if (!projectSlug) {
return sessionProjectId;
}
Expand All @@ -123,6 +151,7 @@ export class OrganizationsPresenter {
organization: {
slug: organizationSlug,
},
deletedAt: null,
},
});

Expand Down Expand Up @@ -150,6 +179,7 @@ export class OrganizationsPresenter {
title: true,
runsEnabled: true,
projects: {
where: { deletedAt: null },
select: {
id: true,
slug: true,
Expand Down Expand Up @@ -202,13 +232,18 @@ export class OrganizationsPresenter {
});
}

async #selectBestProjectForOrganization(organizationSlug: string, userId: string) {
async #selectBestProjectForOrganization(
organizationSlug: string,
userId: string,
request: Request
) {
const projects = await this.#prismaClient.project.findMany({
select: {
id: true,
slug: true,
},
where: {
deletedAt: null,
organization: {
slug: organizationSlug,
members: { some: { userId } },
Expand All @@ -223,8 +258,7 @@ export class OrganizationsPresenter {
});

if (projects.length === 0) {
logger.info("Didn't find a project in this org", { organizationSlug, projects });
throw new Response("Not Found", { status: 404 });
throw redirect(newProjectPath({ slug: organizationSlug }), request);
}

return projects[0];
Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/presenters/ProjectPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class ProjectPresenter {
organizationId: true,
createdAt: true,
updatedAt: true,
deletedAt: true,
_count: {
select: {
sources: {
Expand Down Expand Up @@ -53,7 +54,7 @@ export class ProjectPresenter {
},
},
},
where: { id, organization: { members: { some: { userId } } } },
where: { id, deletedAt: null, organization: { members: { some: { userId } } } },
});

if (!project) {
Expand All @@ -67,6 +68,7 @@ export class ProjectPresenter {
organizationId: project.organizationId,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
deletedAt: project.deletedAt,
hasInactiveExternalTriggers: project._count.sources > 0,
jobCount: project._count.jobs,
httpEndpointCount: project._count.httpEndpoints,
Expand Down

0 comments on commit 29edcd3

Please sign in to comment.