From dfc88d99f6d997e45fdd3839bb58cf199000dac0 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 28 Jun 2023 14:28:52 -0700 Subject: [PATCH] first draft new sidebar --- frontend/src/components/v2/Menu/Menu.tsx | 8 +- frontend/src/components/v2/Tabs/Tabs.tsx | 27 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 109 ++--- frontend/src/pages/org/[id].tsx | 433 +++++++++++++++++++ frontend/tailwind.config.js | 48 +- 5 files changed, 535 insertions(+), 90 deletions(-) create mode 100644 frontend/src/pages/org/[id].tsx diff --git a/frontend/src/components/v2/Menu/Menu.tsx b/frontend/src/components/v2/Menu/Menu.tsx index 632218020..87ef29b5c 100644 --- a/frontend/src/components/v2/Menu/Menu.tsx +++ b/frontend/src/components/v2/Menu/Menu.tsx @@ -48,7 +48,7 @@ export const MenuItem = ({ >
  • ({ > -
    +
    {/* {icon && {icon}} */} {children} diff --git a/frontend/src/components/v2/Tabs/Tabs.tsx b/frontend/src/components/v2/Tabs/Tabs.tsx index e29b5e6a9..cc7e755f6 100644 --- a/frontend/src/components/v2/Tabs/Tabs.tsx +++ b/frontend/src/components/v2/Tabs/Tabs.tsx @@ -72,12 +72,12 @@ export const TabsObject = () => { > Windows - Arch Linux - + */} { @@ -105,10 +105,10 @@ export const TabsObject = () => { .

    - +
    - )} -
    - + ))} +
    + {router.asPath.includes("project") ? { + : + + + + Overview + + + + + + + {t("nav.menu.members")} + + + + + + + Org Setting + + + + }
    -
    - {router.asPath.split("/")[1] === "home" ? ( -
    -
    -

    - -

    - Infisical Guide - progress bar -
    - ) : ( - -
    -

    - -

    - Infisical Guide - progress bar -
    - - )} +
    +
    + + Invite people +
    +
    + + Help & Support +
    diff --git a/frontend/src/pages/org/[id].tsx b/frontend/src/pages/org/[id].tsx new file mode 100644 index 000000000..34029b370 --- /dev/null +++ b/frontend/src/pages/org/[id].tsx @@ -0,0 +1,433 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { faSlack } from "@fortawesome/free-brands-svg-icons"; +import { faArrowRight, faCheckCircle, faHandPeace, faMagnifyingGlass, faNetworkWired, faPlug, faPlus, faStar, faUserPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import AddProjectMemberDialog from "@app/components/basic/dialog/AddProjectMemberDialog"; +import ProjectUsersTable from "@app/components/basic/table/ProjectUsersTable"; +import guidGenerator from "@app/components/utilities/randomId"; +import { useWorkspace } from "@app/context"; +import { Workspace } from "@app/hooks/api/workspace/types"; + +import onboardingCheck from "~/components/utilities/checks/OnboardingCheck"; +import { TabsObject } from "~/components/v2/Tabs"; + +import { + decryptAssymmetric, + encryptAssymmetric +} from "../../components/utilities/cryptography/crypto"; +import getOrganizationUsers from "../api/organization/GetOrgUsers"; +import getUser from "../api/user/getUser"; +import registerUserAction from "../api/userActions/registerUserAction"; +// import DeleteUserDialog from '@app/components/basic/dialog/DeleteUserDialog'; +import addUserToWorkspace from "../api/workspace/addUserToWorkspace"; +import getWorkspaceUsers from "../api/workspace/getWorkspaceUsers"; +import uploadKeys from "../api/workspace/uploadKeys"; + +interface UserProps { + firstName: string; + lastName: string; + email: string; + _id: string; + publicKey: string; +} + +interface MembershipProps { + deniedPermissions: any[]; + user: UserProps; + inviteEmail: string; + role: string; + status: string; + _id: string; +} + +const features = [{ + "_id": 0, + "name": "Kubernetes Operator", + "description": "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes." +}] + +type ItemProps = { + text: string; + subText: string; + complete: boolean; + icon: IconProp; + time: string; + userAction?: string; + link?: string; +}; + +const learningItem = ({ + text, + subText, + complete, + icon, + time, + userAction, + link +}: ItemProps): JSX.Element => { + if (link) { + return ( + +
    +
    null} + role="button" + tabIndex={0} + onClick={async () => { + if (userAction && userAction !== "first_time_secrets_pushed") { + await registerUserAction({ + action: userAction + }); + } + }} + className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`} + > +
    + + {complete && ( +
    + +
    + )} +
    +
    {text}
    +
    {subText}
    +
    +
    +
    + {complete ? "Complete!" : `About ${time}`} +
    + {/* {complete &&
    } */} +
    +
    +
    + ); + } + return ( +
    null} + role="button" + tabIndex={0} + onClick={async () => { + if (userAction) { + await registerUserAction({ + action: userAction + }); + } + }} + className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500" + > +
    + + {complete && ( +
    + +
    + )} +
    +
    {text}
    +
    {subText}
    +
    +
    +
    + {complete ? "Complete!" : `About ${time}`} +
    + {complete &&
    } +
    + ); +}; + +// #TODO: Update all the workspaceIds + +export default function Organization() { + const [isAddOpen, setIsAddOpen] = useState(false); + // let [isDeleteOpen, setIsDeleteOpen] = useState(false); + // let [userIdToBeDeleted, setUserIdToBeDeleted] = useState(false); + const [email, setEmail] = useState(""); + const [personalEmail, setPersonalEmail] = useState(""); + const [searchUsers, setSearchUsers] = useState(""); + + const { t } = useTranslation(); + + const router = useRouter(); + const workspaceId = router.query.id as string; + + const [userList, setUserList] = useState([]); + const [isUserListLoading, setIsUserListLoading] = useState(true); + const [orgUserList, setOrgUserList] = useState([]); + const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace(); + + useEffect(() => { + (async () => { + const user = await getUser(); + setPersonalEmail(user.email); + + // This part quiries the current users of a project + const workspaceUsers = await getWorkspaceUsers({ + workspaceId + }); + const tempUserList = workspaceUsers.map((membership: MembershipProps) => ({ + key: guidGenerator(), + firstName: membership.user?.firstName, + lastName: membership.user?.lastName, + email: membership.user?.email === null ? membership.inviteEmail : membership.user?.email, + role: membership?.role, + status: membership?.status, + userId: membership.user?._id, + membershipId: membership._id, + deniedPermissions: membership.deniedPermissions, + publicKey: membership.user?.publicKey + })); + setUserList(tempUserList); + + setIsUserListLoading(false); + + // This is needed to know wha users from an org (if any), we are able to add to a certain project + const orgUsers = await getOrganizationUsers({ + orgId: String(localStorage.getItem("orgData.id")) + }); + setOrgUserList(orgUsers); + setEmail( + orgUsers + ?.filter((membership: MembershipProps) => membership.status === "accepted") + .map((membership: MembershipProps) => membership.user.email) + .filter( + (usEmail: string) => + !tempUserList?.map((user1: UserProps) => user1.email).includes(usEmail) + )[0] + ); + })(); + }, []); + + const closeAddModal = () => { + setIsAddOpen(false); + }; + + const openAddModal = () => { + setIsAddOpen(true); + }; + + // function closeDeleteModal() { + // setIsDeleteOpen(false); + // } + + // function deleteMembership(userId) { + // deleteUserFromWorkspace(userId, router.query.id) + // } + + // function openDeleteModal() { + // setIsDeleteOpen(true); + // } + + const submitAddModal = async () => { + const result = await addUserToWorkspace(email, workspaceId); + if (result?.invitee && result?.latestKey) { + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; + + // assymmetrically decrypt symmetric key with local private key + const key = decryptAssymmetric({ + ciphertext: result.latestKey.encryptedKey, + nonce: result.latestKey.nonce, + publicKey: result.latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: key, + publicKey: result.invitee.publicKey, + privateKey: PRIVATE_KEY + }); + + uploadKeys(workspaceId, result.invitee._id, ciphertext, nonce); + } + setEmail(""); + setIsAddOpen(false); + router.rel + oad(); + }; + const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); + const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); + const [hasUserStarred, setHasUserStarred] = useState(false); + const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); + const [usersInOrg, setUsersInOrg] = useState(false); + + useEffect(() => { + onboardingCheck({ + setHasUserClickedIntro, + setHasUserClickedSlack, + setHasUserPushedSecrets, + setHasUserStarred, + setUsersInOrg + }); + }, []); + + return userList ? ( +
    + + {t("common.head-title", { title: t("settings.members.title") })} + + +
    +

    Projects

    +
    + {workspaces.map(workspace =>
    +
    {workspace.name}
    +
    {(workspace.environments?.length || 0)} environments
    + +
    Explore
    + +
    )} +
    +
    +
    +

    Onboarding Guide

    + {learningItem({ + text: "Watch a video about Infisical", + subText: "", + complete: hasUserClickedIntro, + icon: faHandPeace, + time: "3 min", + userAction: "intro_cta_clicked", + link: "https://www.youtube.com/watch?v=PK23097-25I" + })} + {learningItem({ + text: "Add your secrets", + subText: "Click to see example secrets, and add your own.", + complete: hasUserPushedSecrets, + icon: faPlus, + time: "1 min", + userAction: "first_time_secrets_pushed", + link: `/dashboard/${router.query.id}` + })} +
    +
    +
    + + {false && ( +
    + +
    + )} +
    +
    Inject secrets locally
    +
    + Replace .env files with a more secure and efficient alternative. +
    +
    +
    +
    + About 2 min +
    +
    + + {false &&
    } +
    + {learningItem({ + text: "Integrate Infisical with your infrastructure", + subText: "Connect Infisical to various 3rd party services and platforms.", + complete: false, + icon: faPlug, + time: "15 min", + link: "https://infisical.com/docs/integrations/overview" + })} + {learningItem({ + text: "Invite your teammates", + subText: "", + complete: usersInOrg, + icon: faUserPlus, + time: "2 min", + link: `/settings/org/${router.query.id}?invite` + })} + {learningItem({ + text: "Join Infisical Slack", + subText: "Have any questions? Ask us!", + complete: hasUserClickedSlack, + icon: faSlack, + time: "1 min", + userAction: "slack_cta_clicked", + link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg" + })} + {/*
    + {workspaces.map(workspace =>
    +
    {workspace.name}
    + +
    Explore
    + +
    )} +
    */} +
    +
    +

    Explore More

    +
    + {features.map(feature =>
    +
    {feature.name}
    +
    {feature.description}
    +
    +
    Setup time: 20 min
    + +
    Learn more
    + +
    +
    )} +
    +
    + membership.status === "accepted") + .map((membership: MembershipProps) => membership.user.email) + .filter( + (orgEmail) => !userList?.map((user1: UserProps) => user1.email).includes(orgEmail) + )} + setEmail={setEmail} + /> + {/* */} + {/*
    +
    + setSearchUsers(e.target.value)} + leftIcon={} + /> +
    +
    +
    +
    */} +
    + ) : ( +
    + loading animation +
    + ); +} + +Organization.requireAuth = true; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index e8bd3b56b..29811c15a 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1245,17 +1245,17 @@ module.exports = { colors: { // You can generate your own colors with this tool: https://javisperez.github.io/tailwindcolorshades/ primary: { - 50: '#fcfdf7', - 100: '#f8fcee', - 200: '#eef6d5', - 300: '#e3f1bc', - 400: '#cfe78a', - 500: '#badc58', - 600: '#a7c64f', - 700: '#8ca542', - 800: '#708435', - 900: '#5b6c2b', - DEFAULT: '#badc58' + 50: '#fffff5', + 100: '#fcfce8', + 200: '#f8faca', + 300: '#f4f7ab', + 400: '#ecf26d', + 500: '#e0ed34', + 600: '#c2d62b', + 700: '#97b31d', + 800: '#708f13', + 900: '#4d6b0b', + DEFAULT: '#e0ed34' }, grey: '#0d1117', mineshaft: { @@ -1339,7 +1339,31 @@ module.exports = { 800: '#1c7a44', 900: '#176437', DEFAULT: '#2ecc71' - } + }, + blue: { + 50: '#f2f8ff', + 100: '#e6f1ff', + 200: '#bfdbff', + 300: '#99c5ff', + 400: '#4d9aff', + 500: '#006eff', + 600: '#0063e6', + 700: '#0053bf', + 800: '#004299', + 900: '#00367d' + }, + darkblue: { + 50: '#f2f4f7', + 100: '#e6e8f0', + 200: '#bfc6d9', + 300: '#99a3c3', + 400: '#4d5f95', + 500: '#001a68', + 600: '#00175e', + 700: '#00144e', + 800: '#00103e', + 900: '#000d33' + }, }, keyframes: { type: {