feat: added projects list view

This commit is contained in:
Sheen Capadngan
2024-06-12 20:18:39 +08:00
parent b06b8294e9
commit 579948ea6d
+237 -140
View File
@@ -17,10 +17,14 @@ import {
faClipboard, faClipboard,
faExclamationCircle, faExclamationCircle,
faHandPeace, faHandPeace,
faList,
faMagnifyingGlass, faMagnifyingGlass,
faNetworkWired, faNetworkWired,
faPlug, faPlug,
faPlus, faPlus,
faProjectDiagram,
faTableCells,
faTableColumns,
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";
@@ -34,6 +38,9 @@ import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { import {
Button, Button,
Checkbox, Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
FormControl, FormControl,
Input, Input,
Modal, Modal,
@@ -86,6 +93,11 @@ type ItemProps = {
link?: string; link?: string;
}; };
enum ProjectsViewMode {
GRID = "grid",
LIST = "list"
}
function copyToClipboard(id: string, setState: (value: boolean) => void) { function copyToClipboard(id: string, setState: (value: boolean) => void) {
// Get the text field // Get the text field
const copyText = document.getElementById(id) as HTMLInputElement; const copyText = document.getElementById(id) as HTMLInputElement;
@@ -309,8 +321,9 @@ const LearningItem = ({
href={link} href={link}
> >
<div <div
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : "" className={`${
} mb-3 rounded-md`} complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} mb-3 rounded-md`}
> >
<div <div
onKeyDown={() => null} onKeyDown={() => null}
@@ -321,10 +334,11 @@ const LearningItem = ({
await registerUserAction.mutateAsync(userAction); await registerUserAction.mutateAsync(userAction);
} }
}} }}
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
complete
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
} text-mineshaft-100 duration-200`} } text-mineshaft-100 duration-200`}
> >
<div className="mr-4 flex flex-row items-center"> <div className="mr-4 flex flex-row items-center">
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" /> <FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
@@ -402,8 +416,9 @@ const LearningItemSquare = ({
href={link} href={link}
> >
<div <div
className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : "" className={`${
} w-full rounded-md`} complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""
} w-full rounded-md`}
> >
<div <div
onKeyDown={() => null} onKeyDown={() => null}
@@ -414,10 +429,11 @@ const LearningItemSquare = ({
await registerUserAction.mutateAsync(userAction); await registerUserAction.mutateAsync(userAction);
} }
}} }}
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${
complete
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
} text-mineshaft-100 duration-200`} } text-mineshaft-100 duration-200`}
> >
<div className="flex w-full flex-col items-center px-6 py-4"> <div className="flex w-full flex-col items-center px-6 py-4">
<div className="flex w-full flex-row items-start justify-between"> <div className="flex w-full flex-row items-start justify-between">
@@ -431,8 +447,9 @@ const LearningItemSquare = ({
</div> </div>
)} )}
<div <div
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "font-semibold text-primary" : "" className={`text-right text-sm font-normal text-mineshaft-300 ${
}`} complete ? "font-semibold text-primary" : ""
}`}
> >
{complete ? "Complete!" : `About ${time}`} {complete ? "Complete!" : `About ${time}`}
</div> </div>
@@ -496,6 +513,9 @@ const OrganizationPage = withPermission(
const createWs = useCreateWorkspace(); const createWs = useCreateWorkspace();
const { user } = useUser(); const { user } = useUser();
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>(
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
);
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => { const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
// type check // type check
@@ -551,6 +571,90 @@ const OrganizationPage = withPermission(
const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0; const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0;
const projectsGridView = (
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{isWorkspaceLoading &&
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>
))}
{orgWorkspaces
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
.map((workspace) => (
// 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 flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</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 group-hover:border-primary-500/80 group-hover:bg-primary-800/20 group-hover:text-mineshaft-200">
Explore{" "}
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
/>
</div>
</button>
</div>
))}
</div>
);
const projectsListView = (
<div className="mt-4 w-full">
{isWorkspaceLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<div
key={`workspace-cards-loading-${i + 1}`}
className="min-w-72 group flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700"
>
<Skeleton className="w-full bg-mineshaft-600" />
</div>
))}
{orgWorkspaces
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
.map((workspace) => (
// 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 flex h-12 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700"
>
<div className="flex items-center">
<FontAwesomeIcon icon={faProjectDiagram} className="text-sm text-white" />
<div className="ml-4 truncate text-lg text-mineshaft-100">{workspace.name}</div>
</div>
<div className="text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
</div>
))}
</div>
);
return ( return (
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen"> <div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
<Head> <Head>
@@ -580,7 +684,44 @@ const OrganizationPage = withPermission(
</div> </div>
)} )}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl"> <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
<p className="mr-4 font-semibold text-white">Projects</p> <div className="flex w-full justify-between">
<p className="mr-4 font-semibold text-white">Projects</p>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faTableColumns} />}>
Display
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div className="flex flex-col space-y-1 p-1.5">
<Button
leftIcon={<FontAwesomeIcon icon={faTableCells} className="pr-2" />}
onClick={() => {
localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID);
setProjectsViewMode(ProjectsViewMode.GRID);
}}
variant="outline_bg"
className="h-10 text-left"
isFullWidth
>
Grid
</Button>
<Button
leftIcon={<FontAwesomeIcon icon={faList} className="pr-2" />}
onClick={() => {
localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST);
setProjectsViewMode(ProjectsViewMode.LIST);
}}
variant="outline_bg"
className="h-10 text-left"
isFullWidth
>
List
</Button>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="mt-6 flex w-full flex-row"> <div className="mt-6 flex w-full flex-row">
<Input <Input
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80" className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
@@ -609,52 +750,7 @@ const OrganizationPage = withPermission(
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</div> </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"> {projectsViewMode === ProjectsViewMode.LIST ? projectsListView : projectsGridView}
{isWorkspaceLoading &&
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>
))}
{orgWorkspaces
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
.map((workspace) => (
// 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 flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</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 group-hover:border-primary-500/80 group-hover:bg-primary-800/20 group-hover:text-mineshaft-200">
Explore{" "}
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
/>
</div>
</button>
</div>
))}
</div>
{isWorkspaceEmpty && ( {isWorkspaceEmpty && (
<div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300"> <div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon <FontAwesomeIcon
@@ -709,94 +805,95 @@ const OrganizationPage = withPermission(
new Date().getTime() - new Date(user?.createdAt).getTime() < new Date().getTime() - new Date(user?.createdAt).getTime() <
30 * 24 * 60 * 60 * 1000 30 * 24 * 60 * 60 * 1000
) && ( ) && (
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl"> <div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p> <p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> <div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<LearningItemSquare <LearningItemSquare
text="Watch Infisical demo" text="Watch Infisical demo"
subText="Set up Infisical in 3 min." subText="Set up Infisical in 3 min."
complete={hasUserClickedIntro} complete={hasUserClickedIntro}
icon={faHandPeace} icon={faHandPeace}
time="3 min" time="3 min"
userAction="intro_cta_clicked" userAction="intro_cta_clicked"
link="https://www.youtube.com/watch?v=PK23097-25I" link="https://www.youtube.com/watch?v=PK23097-25I"
/> />
{orgWorkspaces.length !== 0 && (
<>
<LearningItemSquare
text="Add your secrets"
subText="Drop a .env file or type your secrets."
complete={hasUserPushedSecrets}
icon={faPlus}
time="1 min"
userAction="first_time_secrets_pushed"
link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
/>
<LearningItemSquare
text="Invite your teammates"
subText="Infisical is better used as a team."
complete={usersInOrg}
icon={faUserPlus}
time="2 min"
link={`/org/${router.query.id}/members?action=invite`}
/>
</>
)}
<div className="block xl:hidden 2xl:block">
<LearningItemSquare
text="Join Infisical Slack"
subText="Have any questions? Ask us!"
complete={hasUserClickedSlack}
icon={faSlack}
time="1 min"
userAction="slack_cta_clicked"
link="https://infisical.com/slack"
/>
</div>
</div>
{orgWorkspaces.length !== 0 && ( {orgWorkspaces.length !== 0 && (
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200"> <>
<div className="mb-4 flex w-full flex-row items-center pr-4"> <LearningItemSquare
<div className="mr-4 flex w-full flex-row items-center"> text="Add your secrets"
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" /> subText="Drop a .env file or type your secrets."
{false && ( complete={hasUserPushedSecrets}
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700"> icon={faPlus}
<FontAwesomeIcon time="1 min"
icon={faCheckCircle} userAction="first_time_secrets_pushed"
className="h-5 w-5 text-4xl text-green" link={`/project/${orgWorkspaces[0]?.id}/secrets/overview`}
/> />
</div> <LearningItemSquare
)} text="Invite your teammates"
<div className="flex flex-col items-start pl-0.5"> subText="Infisical is better used as a team."
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div> complete={usersInOrg}
<div className="text-sm font-normal"> icon={faUserPlus}
Replace .env files with a more secure and efficient alternative. time="2 min"
</div> link={`/org/${router.query.id}/members?action=invite`}
/>
</>
)}
<div className="block xl:hidden 2xl:block">
<LearningItemSquare
text="Join Infisical Slack"
subText="Have any questions? Ask us!"
complete={hasUserClickedSlack}
icon={faSlack}
time="1 min"
userAction="slack_cta_clicked"
link="https://infisical.com/slack"
/>
</div>
</div>
{orgWorkspaces.length !== 0 && (
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
<div className="mb-4 flex w-full flex-row items-center pr-4">
<div className="mr-4 flex w-full flex-row items-center">
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
{false && (
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-5 w-5 text-4xl text-green"
/>
</div>
)}
<div className="flex flex-col items-start pl-0.5">
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
<div className="text-sm font-normal">
Replace .env files with a more secure and efficient alternative.
</div> </div>
</div> </div>
<div
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"
}`}
>
About 2 min
</div>
</div> </div>
<TabsObject /> <div
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />} className={`w-28 pr-4 text-right text-sm font-semibold ${
false && "text-green"
}`}
>
About 2 min
</div>
</div> </div>
)} <TabsObject />
{orgWorkspaces.length !== 0 && ( {false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
<LearningItem </div>
text="Integrate Infisical with your infrastructure" )}
subText="Connect Infisical to various 3rd party services and platforms." {orgWorkspaces.length !== 0 && (
complete={false} <LearningItem
icon={faPlug} text="Integrate Infisical with your infrastructure"
time="15 min" subText="Connect Infisical to various 3rd party services and platforms."
link="https://infisical.com/docs/integrations/overview" complete={false}
/> icon={faPlug}
)} time="15 min"
</div> link="https://infisical.com/docs/integrations/overview"
)} />
)}
</div>
)}
<Modal <Modal
isOpen={popUp.addNewWs.isOpen} isOpen={popUp.addNewWs.isOpen}
onOpenChange={(isModalOpen) => { onOpenChange={(isModalOpen) => {