Merge pull request #2040 from Infisical/feat/mark-projects-as-favourite

feat: allow org members to mark projects as favorites
This commit is contained in:
Sheen Capadngan
2024-07-01 22:38:36 +08:00
committed by GitHub
9 changed files with 439 additions and 84 deletions
@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.OrgMembership, "projectFavorites"))) {
await knex.schema.alterTable(TableName.OrgMembership, (tb) => {
tb.specificType("projectFavorites", "text[]");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.OrgMembership, "projectFavorites")) {
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
t.dropColumn("projectFavorites");
});
}
}
+2 -1
View File
@@ -16,7 +16,8 @@ export const OrgMembershipsSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
userId: z.string().uuid().nullable().optional(), userId: z.string().uuid().nullable().optional(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
roleId: z.string().uuid().nullable().optional() roleId: z.string().uuid().nullable().optional(),
projectFavorites: z.string().array().nullable().optional()
}); });
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>; export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
+3 -1
View File
@@ -415,8 +415,10 @@ export const registerRoutes = async (
userAliasDAL, userAliasDAL,
orgMembershipDAL, orgMembershipDAL,
tokenService, tokenService,
smtpService smtpService,
projectMembershipDAL
}); });
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL }); const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
const passwordService = authPaswordServiceFactory({ const passwordService = authPaswordServiceFactory({
tokenService, tokenService,
+45 -1
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { authRateLimit, readLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -90,4 +90,48 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
return res.redirect(`${appCfg.SITE_URL}/login`); return res.redirect(`${appCfg.SITE_URL}/login`);
} }
}); });
server.route({
method: "GET",
url: "/me/project-favorites",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
orgId: z.string().trim()
}),
response: {
200: z.object({
projectFavorites: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.user.getUserProjectFavorites(req.permission.id, req.query.orgId);
}
});
server.route({
method: "PUT",
url: "/me/project-favorites",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
orgId: z.string().trim(),
projectFavorites: z.string().array()
})
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
return server.services.user.updateUserProjectFavorites(
req.permission.id,
req.body.orgId,
req.body.projectFavorites
);
}
});
}; };
+52 -2
View File
@@ -8,6 +8,7 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { AuthMethod } from "../auth/auth-type"; import { AuthMethod } from "../auth/auth-type";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TUserDALFactory } from "./user-dal"; import { TUserDALFactory } from "./user-dal";
type TUserServiceFactoryDep = { type TUserServiceFactoryDep = {
@@ -26,8 +27,9 @@ type TUserServiceFactoryDep = {
| "delete" | "delete"
>; >;
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">; userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">; tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
}; };
@@ -37,6 +39,7 @@ export const userServiceFactory = ({
userDAL, userDAL,
userAliasDAL, userAliasDAL,
orgMembershipDAL, orgMembershipDAL,
projectMembershipDAL,
tokenService, tokenService,
smtpService smtpService
}: TUserServiceFactoryDep) => { }: TUserServiceFactoryDep) => {
@@ -247,6 +250,51 @@ export const userServiceFactory = ({
return privateKey; return privateKey;
}; };
const getUserProjectFavorites = async (userId: string, orgId: string) => {
const orgMembership = await orgMembershipDAL.findOne({
userId,
orgId
});
if (!orgMembership) {
throw new BadRequestError({
message: "User does not belong in the organization."
});
}
return { projectFavorites: orgMembership.projectFavorites || [] };
};
const updateUserProjectFavorites = async (userId: string, orgId: string, projectIds: string[]) => {
const orgMembership = await orgMembershipDAL.findOne({
userId,
orgId
});
if (!orgMembership) {
throw new BadRequestError({
message: "User does not belong in the organization."
});
}
const matchingUserProjectMemberships = await projectMembershipDAL.find({
userId,
$in: {
projectId: projectIds
}
});
const memberProjectFavorites = matchingUserProjectMemberships.map(
(projectMembership) => projectMembership.projectId
);
const updatedOrgMembership = await orgMembershipDAL.updateById(orgMembership.id, {
projectFavorites: memberProjectFavorites
});
return updatedOrgMembership.projectFavorites;
};
return { return {
sendEmailVerificationCode, sendEmailVerificationCode,
verifyEmailVerificationCode, verifyEmailVerificationCode,
@@ -258,6 +306,8 @@ export const userServiceFactory = ({
createUserAction, createUserAction,
getUserAction, getUserAction,
unlockUser, unlockUser,
getUserPrivateKey getUserPrivateKey,
getUserProjectFavorites,
updateUserProjectFavorites
}; };
}; };
+24
View File
@@ -7,6 +7,7 @@ import {
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace/queries";
import { userKeys } from "./queries";
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types"; import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
export const useAddUserToWsE2EE = () => { export const useAddUserToWsE2EE = () => {
@@ -88,3 +89,26 @@ export const useVerifyEmailVerificationCode = () => {
} }
}); });
}; };
export const useUpdateUserProjectFavorites = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
orgId,
projectFavorites
}: {
orgId: string;
projectFavorites: string[];
}) => {
await apiRequest.put("/api/v1/user/me/project-favorites", {
orgId,
projectFavorites
});
return {};
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(userKeys.userProjectFavorites(orgId));
}
});
};
+16
View File
@@ -22,11 +22,13 @@ export const userKeys = {
getUser: ["user"] as const, getUser: ["user"] as const,
getPrivateKey: ["user"] as const, getPrivateKey: ["user"] as const,
userAction: ["user-action"] as const, userAction: ["user-action"] as const,
userProjectFavorites: (orgId: string) => [{ orgId }, "user-project-favorites"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"], getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const, myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const, myAPIKeys: ["api-keys"] as const,
myAPIKeysV2: ["api-keys-v2"] as const, myAPIKeysV2: ["api-keys-v2"] as const,
mySessions: ["sessions"] as const, mySessions: ["sessions"] as const,
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
}; };
@@ -74,6 +76,14 @@ export const fetchUserAction = async (action: string) => {
return data.userAction || ""; return data.userAction || "";
}; };
export const fetchUserProjectFavorites = async (orgId: string) => {
const { data } = await apiRequest.get<{ projectFavorites: string[] }>(
`/api/v1/user/me/project-favorites?orgId=${orgId}`
);
return data.projectFavorites;
};
export const useRenameUser = () => { export const useRenameUser = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -122,6 +132,12 @@ export const fetchOrgUsers = async (orgId: string) => {
return data.users; return data.users;
}; };
export const useGetUserProjectFavorites = (orgId: string) =>
useQuery({
queryKey: userKeys.userProjectFavorites(orgId),
queryFn: () => fetchUserProjectFavorites(orgId)
});
export const useGetOrgUsers = (orgId: string) => export const useGetOrgUsers = (orgId: string) =>
useQuery({ useQuery({
queryKey: userKeys.getOrgUsers(orgId), queryKey: userKeys.getOrgUsers(orgId),
+89 -10
View File
@@ -12,6 +12,7 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import { faStar } from "@fortawesome/free-regular-svg-icons";
import { import {
faAngleDown, faAngleDown,
faArrowLeft, faArrowLeft,
@@ -23,7 +24,8 @@ import {
faInfo, faInfo,
faMobile, faMobile,
faPlus, faPlus,
faQuestion faQuestion,
faStar as faSolidStar
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
@@ -72,6 +74,9 @@ import {
useRegisterUserAction, useRegisterUserAction,
useSelectOrganization useSelectOrganization
} from "@app/hooks/api"; } from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { navigateUserToOrg } from "@app/views/Login/Login.utils"; import { navigateUserToOrg } from "@app/views/Login/Login.utils";
import { CreateOrgModal } from "@app/views/Org/components"; import { CreateOrgModal } from "@app/views/Org/components";
@@ -122,6 +127,20 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { workspaces, currentWorkspace } = useWorkspace(); const { workspaces, currentWorkspace } = useWorkspace();
const { orgs, currentOrg } = useOrganization(); const { orgs, currentOrg } = useOrganization();
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
const workspacesWithFaveProp = useMemo(
() =>
workspaces
.map((w): Workspace & { isFavorite: boolean } => ({
...w,
isFavorite: Boolean(projectFavorites?.includes(w.id))
}))
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)),
[workspaces, projectFavorites]
);
const { user } = useUser(); const { user } = useUser();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
@@ -271,6 +290,38 @@ export const AppLayout = ({ children }: LayoutProps) => {
} }
}; };
const addProjectToFavorites = async (projectId: string) => {
try {
if (currentOrg?.id) {
await updateUserProjectFavorites({
orgId: currentOrg?.id,
projectFavorites: [...(projectFavorites || []), projectId]
});
}
} catch (err) {
createNotification({
text: "Failed to add project to favorites.",
type: "error"
});
}
};
const removeProjectFromFavorites = async (projectId: string) => {
try {
if (currentOrg?.id) {
await updateUserProjectFavorites({
orgId: currentOrg?.id,
projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
});
}
} catch (err) {
createNotification({
text: "Failed to remove project from favorites.",
type: "error"
});
}
};
return ( return (
<> <>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex"> <div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
@@ -451,19 +502,47 @@ export const AppLayout = ({ children }: LayoutProps) => {
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700" dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
> >
<div className="no-scrollbar::-webkit-scrollbar h-full no-scrollbar"> <div className="no-scrollbar::-webkit-scrollbar h-full no-scrollbar">
{workspaces {workspacesWithFaveProp
.filter((ws) => ws.orgId === currentOrg?.id) .filter((ws) => ws.orgId === currentOrg?.id)
.map(({ id, name }) => ( .map(({ id, name, isFavorite }) => (
<SelectItem <div
key={`ws-layout-list-${id}`}
value={id}
className={twMerge( className={twMerge(
currentWorkspace?.id === id && "bg-mineshaft-600", "mb-1 grid grid-cols-7 rounded-md hover:bg-mineshaft-500",
"truncate" id === currentWorkspace?.id && "bg-mineshaft-500"
)} )}
key={id}
> >
{name} <div className="col-span-6">
</SelectItem> <SelectItem
key={`ws-layout-list-${id}`}
value={id}
className="transition-none data-[highlighted]:bg-mineshaft-500"
>
{name}
</SelectItem>
</div>
<div className="col-span-1 flex items-center">
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
className="text-sm text-mineshaft-300 hover:text-mineshaft-400"
onClick={(e) => {
e.stopPropagation();
removeProjectFromFavorites(id);
}}
/>
) : (
<FontAwesomeIcon
icon={faStar}
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={(e) => {
e.stopPropagation();
addProjectToFavorites(id);
}}
/>
)}
</div>
</div>
))} ))}
</div> </div>
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" /> <hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
+189 -69
View File
@@ -1,6 +1,6 @@
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex // REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
@@ -8,7 +8,7 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { faFolderOpen } from "@fortawesome/free-regular-svg-icons"; import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
import { import {
faArrowRight, faArrowRight,
faArrowUpRightFromSquare, faArrowUpRightFromSquare,
@@ -24,6 +24,7 @@ import {
faNetworkWired, faNetworkWired,
faPlug, faPlug,
faPlus, faPlus,
faStar as faSolidStar,
faUserPlus faUserPlus
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -62,6 +63,9 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; // import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
const features = [ const features = [
@@ -485,7 +489,11 @@ const OrganizationPage = withPermission(
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const routerOrgId = String(router.query.id); const routerOrgId = String(router.query.id);
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || []; const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
const { data: projectFavorites, isLoading: isProjectFavoritesLoading } =
useGetUserProjectFavorites(currentOrg?.id!);
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading;
const addUsersToProject = useAddUserToWsNonE2EE(); const addUsersToProject = useAddUserToWsNonE2EE();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -570,56 +578,187 @@ const OrganizationPage = withPermission(
ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()) ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())
); );
const projectsGridView = ( const { workspacesWithFaveProp, favoriteWorkspaces, nonFavoriteWorkspaces } = useMemo(() => {
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> const workspacesWithFav = filteredWorkspaces
{isWorkspaceLoading && .map((w): Workspace & { isFavorite: boolean } => ({
Array.apply(0, Array(3)).map((_x, i) => ( ...w,
<div isFavorite: Boolean(projectFavorites?.includes(w.id))
key={`workspace-cards-loading-${i + 1}`} }))
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4" .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite));
>
<div className="mt-0 text-lg text-mineshaft-100"> const favWorkspaces = workspacesWithFav.filter((w) => w.isFavorite);
<Skeleton className="w-3/4 bg-mineshaft-600" /> const nonFavWorkspaces = workspacesWithFav.filter((w) => !w.isFavorite);
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300"> return {
<Skeleton className="w-1/2 bg-mineshaft-600" /> workspacesWithFaveProp: workspacesWithFav,
</div> favoriteWorkspaces: favWorkspaces,
<div className="flex justify-end"> nonFavoriteWorkspaces: nonFavWorkspaces
<Skeleton className="w-1/2 bg-mineshaft-600" /> };
</div> }, [filteredWorkspaces, projectFavorites]);
</div>
))} const addProjectToFavorites = async (projectId: string) => {
{filteredWorkspaces.map((workspace) => ( try {
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events if (currentOrg?.id) {
<div await updateUserProjectFavorites({
onClick={() => { orgId: currentOrg?.id,
router.push(`/project/${workspace.id}/secrets/overview`); projectFavorites: [...(projectFavorites || []), projectId]
localStorage.setItem("projectData.id", workspace.id); });
}} }
key={workspace.id} } catch (err) {
className="min-w-72 group flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4" createNotification({
> text: "Failed to add project to favorites.",
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div> type: "error"
<div className="mt-0 pb-6 text-sm text-mineshaft-300"> });
{workspace.environments?.length || 0} environments }
</div> };
<button type="button">
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all group-hover:border-primary-500/80 group-hover:bg-primary-800/20 group-hover:text-mineshaft-200"> const removeProjectFromFavorites = async (projectId: string) => {
Explore{" "} try {
<FontAwesomeIcon if (currentOrg?.id) {
icon={faArrowRight} await updateUserProjectFavorites({
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0" orgId: currentOrg?.id,
/> projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
</div> });
</button> }
} catch (err) {
createNotification({
text: "Failed to remove project from favorites.",
type: "error"
});
}
};
const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
onClick={() => {
router.push(`/project/${workspace.id}/secrets/overview`);
localStorage.setItem("projectData.id", workspace.id);
}}
key={workspace.id}
className="min-w-72 flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="flex flex-row justify-between">
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
className="text-sm text-mineshaft-300 hover:text-mineshaft-400"
onClick={(e) => {
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
}}
/>
) : (
<FontAwesomeIcon
icon={faStar}
className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={(e) => {
e.stopPropagation();
addProjectToFavorites(workspace.id);
}}
/>
)}
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
<button type="button">
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
Explore{" "}
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
/>
</div> </div>
))} </button>
</div> </div>
); );
const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
onClick={() => {
router.push(`/project/${workspace.id}/secrets/overview`);
localStorage.setItem("projectData.id", workspace.id);
}}
key={workspace.id}
className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
index === 0 && "rounded-t-md"
} ${index === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`}
>
<div className="flex items-center sm:col-span-3 lg:col-span-4">
<FontAwesomeIcon icon={faFileShield} className="text-sm text-primary/70" />
<div className="ml-5 truncate text-sm text-mineshaft-100">{workspace.name}</div>
</div>
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
<div className="text-center text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
{isFavorite ? (
<FontAwesomeIcon
icon={faSolidStar}
className="ml-6 text-sm text-mineshaft-300 hover:text-mineshaft-400"
onClick={(e) => {
e.stopPropagation();
removeProjectFromFavorites(workspace.id);
}}
/>
) : (
<FontAwesomeIcon
icon={faStar}
className="ml-6 text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={(e) => {
e.stopPropagation();
addProjectToFavorites(workspace.id);
}}
/>
)}
</div>
</div>
);
const projectsGridView = (
<>
{favoriteWorkspaces.length > 0 && (
<>
<p className="mt-6 text-xl font-semibold text-white">Favorites</p>
<div
className={`b grid w-full grid-cols-1 gap-4 ${
nonFavoriteWorkspaces.length > 0 && "border-b border-mineshaft-600"
} py-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`}
>
{favoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, true))}
</div>
</>
)}
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{isProjectViewLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<div
key={`workspace-cards-loading-${i + 1}`}
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">
<Skeleton className="w-3/4 bg-mineshaft-600" />
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
<div className="flex justify-end">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
</div>
))}
{!isProjectViewLoading &&
nonFavoriteWorkspaces.map((workspace) => renderProjectGridItem(workspace, false))}
</div>
</>
);
const projectsListView = ( const projectsListView = (
<div className="mt-4 w-full rounded-md"> <div className="mt-4 w-full rounded-md">
{isWorkspaceLoading && {isProjectViewLoading &&
Array.apply(0, Array(3)).map((_x, i) => ( Array.apply(0, Array(3)).map((_x, i) => (
<div <div
key={`workspace-cards-loading-${i + 1}`} key={`workspace-cards-loading-${i + 1}`}
@@ -630,29 +769,10 @@ const OrganizationPage = withPermission(
<Skeleton className="w-full bg-mineshaft-600" /> <Skeleton className="w-full bg-mineshaft-600" />
</div> </div>
))} ))}
{filteredWorkspaces.map((workspace, ind) => ( {!isProjectViewLoading &&
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events workspacesWithFaveProp.map((workspace, ind) =>
<div renderProjectListItem(workspace, workspace.isFavorite, ind)
onClick={() => { )}
router.push(`/project/${workspace.id}/secrets/overview`);
localStorage.setItem("projectData.id", workspace.id);
}}
key={workspace.id}
className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
ind === 0 && "rounded-t-md"
} ${ind === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`}
>
<div className="flex items-center sm:col-span-3 lg:col-span-4">
<FontAwesomeIcon icon={faFileShield} className="text-sm text-primary/70" />
<div className="ml-5 truncate text-sm text-mineshaft-100">{workspace.name}</div>
</div>
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
<div className="text-center text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
</div>
</div>
))}
</div> </div>
); );