Merge remote-tracking branch 'refs/remotes/origin/main' into circleci-integration-branch

This commit is contained in:
Aashish-Upadhyay-101
2023-02-09 13:16:43 +05:45
185 changed files with 6888 additions and 1750 deletions
+31 -32
View File
@@ -187,10 +187,10 @@ const Layout = ({ children }: LayoutProps) => {
});
const userWorkspaces = orgUserProjects;
if (
userWorkspaces.length === 0 &&
router.asPath !== '/noprojects' &&
!router.asPath.includes('home') &&
!router.asPath.includes('settings') ||
(userWorkspaces.length === 0 &&
router.asPath !== '/noprojects' &&
!router.asPath.includes('home') &&
!router.asPath.includes('settings')) ||
router.asPath === '/dashboard/undefined'
) {
router.push('/noprojects');
@@ -199,13 +199,13 @@ const Layout = ({ children }: LayoutProps) => {
.split('/')
[router.asPath.split('/').length - 1].split('?')[0];
if (!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId)) {
if (!['callback', 'create', 'authorize'].includes(intendedWorkspaceId)) {
localStorage.setItem('projectData.id', intendedWorkspaceId);
}
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) &&
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
@@ -246,16 +246,15 @@ const Layout = ({ children }: LayoutProps) => {
return (
<>
<div className="fixed w-full md:block flex flex-col h-screen">
<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js" defer />
<div className="flex h-screen w-full flex-col overflow-x-hidden">
<NavBarDashboard />
<div className="flex flex-col md:flex-row flex-1">
<aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen">
<nav className="flex flex-col justify-between items-between h-full">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
<aside className="w-full border-r border-mineshaft-500 bg-bunker-600 md:w-60">
<nav className="items-between flex h-full flex-col justify-between">
{/* <div className="py-6"></div> */}
<div>
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
<div className="mt-6 mb-6 flex h-20 w-full flex-col items-center justify-center bg-bunker-600 px-4">
<div className="ml-1 mb-1 self-start text-xs font-semibold tracking-wide text-gray-400">
{t('nav:menu.project')}
</div>
{Object.keys(workspaceMapping).length > 0 ? (
@@ -279,29 +278,29 @@ const Layout = ({ children }: LayoutProps) => {
<ul>
{Object.keys(workspaceMapping).length > 0 &&
menuItems.map(({ href, title, emoji }) => (
<li className="mt-0.5 mx-2" key={title}>
<li className="mx-2 mt-0.5" key={title}>
{router.asPath.split('/')[1] === href.split('/')[1] &&
(['project', 'billing', 'org', 'personal'].includes(
router.asPath.split('/')[2]
)
? router.asPath.split('/')[2] === href.split('/')[2]
: true) ? (
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
{emoji}
</p>
{title}
</div>
) : router.asPath === '/noprojects' ? (
<div className="flex p-2.5 text-white text-sm rounded">
<p className="w-10 flex items-center justify-center text-lg">{emoji}</p>
<div className="flex rounded p-2.5 text-sm text-white">
<p className="flex w-10 items-center justify-center text-lg">{emoji}</p>
{title}
</div>
) : (
<Link href={href}>
<div className="flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5">
<p className="w-10 flex items-center justify-center text-lg">
<div className="flex cursor-pointer rounded p-2.5 text-sm text-white hover:bg-primary-50/5">
<p className="flex w-10 items-center justify-center text-lg">
{emoji}
</p>
{title}
@@ -312,11 +311,11 @@ const Layout = ({ children }: LayoutProps) => {
))}
</ul>
</div>
<div className="w-full mt-40 mb-4 px-2">
<div className="mt-40 mb-4 w-full px-2">
{router.asPath.split('/')[1] === 'home' ? (
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} />
</p>
Infisical Guide
@@ -336,8 +335,8 @@ const Layout = ({ children }: LayoutProps) => {
</div>
) : (
<Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
<div className="relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max">
<p className="w-10 flex items-center justify-center text-lg">
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
<p className="flex w-10 items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} />
</p>
Infisical Guide
@@ -369,12 +368,12 @@ const Layout = ({ children }: LayoutProps) => {
error={error}
loading={loading}
/>
<main className="flex-1 bg-bunker-800">{children}</main>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800">{children}</main>
</div>
</div>
<div className="md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
<FontAwesomeIcon icon={faMobile} className="text-gray-300 text-7xl mb-8" />
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t('common:no-mobile')} `}
</p>
</div>
+1 -1
View File
@@ -58,7 +58,7 @@ const ListBox = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<Listbox.Options className="border border-mineshaft-700 z-[70] p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm no-scrollbar no-scrollbar::-webkit-scrollbar">
{data.map((person, personIdx) => (
<Listbox.Option
key={`${person}.${personIdx + 1}`}
@@ -36,7 +36,7 @@ const AddWorkspaceDialog = ({
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={closeModal}>
<Dialog as="div" className="relative z-[100]" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@@ -49,7 +49,7 @@ const AddWorkspaceDialog = ({
<div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto z-50">
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
@@ -60,7 +60,7 @@ const AddWorkspaceDialog = ({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
@@ -69,7 +69,7 @@ const AddWorkspaceDialog = ({
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
This project will contain your environmental variables.
This project will contain your secrets and configs.
</p>
</div>
<div className="max-h-28 mt-4">
@@ -15,7 +15,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => {}}>
<Dialog as="div" className="relative z-[80]" onClose={() => {}}>
<div className="fixed inset-0 overflow-y-auto">
<Transition.Child
as={Fragment}
@@ -1,120 +0,0 @@
import { Fragment, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
import Button from "../buttons/Button";
import InputField from "../InputField";
interface IntegrationOption {
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
image: string;
isAvailable: boolean;
name: string;
slug: string;
type: string;
}
type Props = {
isOpen: boolean;
closeModal: () => void;
selectedIntegrationOption: IntegrationOption | null
handleIntegrationOption: (arg:{
integrationOption: IntegrationOption,
accessToken?: string;
})=>void;
};
const IntegrationAccessTokenDialog = ({
isOpen,
closeModal,
selectedIntegrationOption,
handleIntegrationOption
}:Props) => {
const [accessToken, setAccessToken] = useState('');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const submit = async () => {
try {
if (selectedIntegrationOption && accessToken !== '') {
handleIntegrationOption({
integrationOption: selectedIntegrationOption,
accessToken
});
closeModal();
setAccessToken('');
}
} catch (err) {
console.log(err);
}
}
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={() => {
closeModal();
}}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
>
{`Enter your ${selectedIntegrationOption?.name} API Key`}
</Dialog.Title>
<div className="mt-2 mb-2">
<p className="text-sm text-gray-500">
{`This integration requires you to obtain an API key from ${selectedIntegrationOption?.name ?? ''} and store it with Infisical. `}
You can learn how to do this <a target="_blank" rel="noreferrer" className="text-primary cursor-pointer underline underline-offset-2" href="https://infisical.com/docs/integrations/cloud/render">here</a>.
</p>
</div>
<div className="mt-4 max-w-full">
<InputField
label="API Key"
onChangeHandler={setAccessToken}
type="varName"
value={accessToken}
placeholder=""
isRequired
/>
<div className="mt-4 max-w-[5.5rem]">
<Button
onButtonPressed={submit}
color="mineshaft"
text="Connect"
size="md"
/>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
}
export default IntegrationAccessTokenDialog;
@@ -41,7 +41,6 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onEnvCreateCB = async (env: Env) => {
try {
await onCreateEnv(env);
@@ -71,25 +70,25 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
return (
<>
<div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full">
<p className="text-xl font-semibold mb-3">Project Environments</p>
<p className="text-base text-gray-400 mb-4">
<div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Project Environments</p>
<p className="mb-4 text-base text-gray-400">
Choose which environments will show up in your dashboard like development, staging,
production
</p>
<p className="text-sm mr-1 text-gray-500 self-start">
<p className="mr-1 self-start text-sm text-gray-500">
Note: the text in slugs shows how these environmant should be accessed in CLI.
</p>
</div>
<div className="w-48">
<Button
text="Add New Env"
text="Add New Environment"
onButtonPressed={() => {
if (plan !== plans.starter || host !== "https://app.infisical.com") {
handlePopUpOpen('createUpdateEnv')
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
handlePopUpOpen('createUpdateEnv');
} else {
handlePopUpOpen('upgradePlan')
handlePopUpOpen('upgradePlan');
}
}}
color="mineshaft"
@@ -98,32 +97,30 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
/>
</div>
</div>
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
<table className="w-full my-1">
<div className="table-container relative mb-6 mt-1 w-full rounded-md border border-mineshaft-700 bg-bunker">
<div className="absolute h-12 w-full rounded-t-md bg-white/5" />
<table className="my-1 w-full">
<thead className="text-bunker-300">
<tr>
<th className="text-left pl-6 pt-2.5 pb-2">Name</th>
<th className="text-left pl-6 pt-2.5 pb-2">Slug</th>
<th className="pl-6 pt-2.5 pb-2 text-left">Name</th>
<th className="pl-6 pt-2.5 pb-2 text-left">Slug</th>
<th aria-label="buttons" />
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data.map(({ name, slug }) => (
<tr key={name} className="bg-bunker-800 hover:bg-bunker-800/5 duration-100">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{name}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">{slug}</td>
<td className="py-2 border-mineshaft-700 border-t flex justify-end">
<div className="hover:opacity-100 duration-200 flex items-center mr-2">
<tr key={name} className="bg-bunker-800 duration-100 hover:bg-bunker-800/5">
<td className="border-t border-mineshaft-700 py-2 pl-6 text-gray-300">{name}</td>
<td className="border-t border-mineshaft-700 py-2 pl-6 text-gray-300">{slug}</td>
<td className="flex justify-end border-t border-mineshaft-700 py-2">
<div className="mr-2 flex items-center duration-200 hover:opacity-100">
<Button
onButtonPressed={() => {
if (plan !== plans.starter || host !== "https://app.infisical.com") {
handlePopUpOpen('createUpdateEnv', { name, slug })
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
handlePopUpOpen('createUpdateEnv', { name, slug });
} else {
handlePopUpOpen('upgradePlan')
handlePopUpOpen('upgradePlan');
}
}}
color="mineshaft"
@@ -131,13 +128,13 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
icon={faPencil}
/>
</div>
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center mr-6">
<div className="mr-6 flex items-center opacity-50 duration-200 hover:opacity-100">
<Button
onButtonPressed={() => {
if (plan !== plans.starter || host !== "https://app.infisical.com") {
handlePopUpOpen('deleteEnv', { name, slug })
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
handlePopUpOpen('deleteEnv', { name, slug });
} else {
handlePopUpOpen('upgradePlan')
handlePopUpOpen('upgradePlan');
}
}}
color="red"
@@ -150,8 +147,8 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
))
) : (
<tr>
<td colSpan={4} className="text-center pt-7 pb-4 text-bunker-400">
No environmants found
<td colSpan={4} className="pt-7 pb-4 text-center text-bunker-400">
No environments found
</td>
</tr>
)}
@@ -174,7 +171,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
onCreateSubmit={onEnvCreateCB}
onEditSubmit={onEnvUpdateCB}
/>
<UpgradePlanModal
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onClose={() => handlePopUpClose('upgradePlan')}
text="You can add custom environments if you switch to Infisical's Team plan."
@@ -0,0 +1,63 @@
import { Fragment } from 'react';
import { useRouter } from 'next/router';
import { faCheckSquare, faPlus, faSquare } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
import { Tag } from 'public/data/frequentInterfaces';
/**
* This is the menu that is used to add more tags to a secret
* @param {object} obj
* @param {Tag[]} obj.allTags - all available tags for a vertain project
* @param {Tag[]} obj.currentTags - currently selected tags for a certain secret
* @param {function} obj.modifyTags - modify tags for a certain secret
* @param {Tag[]} obj.position - currently selected tags for a certain secret
*/
const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], position: number) => void; position: number; }) => {
const router = useRouter();
return (
<Menu as="div" className="ml-2 relative inline-block text-left">
<Menu.Button
as="div"
className="flex justify-center items-center font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
<div className='bg-mineshaft/30 cursor-pointer rounded-sm text-sm text-mineshaft-200/50 hover:bg-mineshaft/70 duration-200 flex items-center'>
<FontAwesomeIcon icon={faPlus} className="p-[0.28rem]"/>
{currentTags?.length > 2 && <span className='pr-2'>{currentTags.length - 2}</span>}
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-[90] text-sm drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-mineshaft-600 border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-1 space-y-1">
{allTags?.map((tag) => { return (
<Menu.Item key={tag._id}>
<button
type="button"
className={`${currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? "opacity-30 cursor-default" : "hover:bg-mineshaft-700"} w-full text-left bg-mineshaft-800 px-2 py-0.5 text-bunker-200 rounded-sm flex items-center`}
onClick={() => {if (!currentTags?.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), position)}}}
>
{currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? <FontAwesomeIcon icon={faCheckSquare} className="text-xs mr-2 text-primary"/> : <FontAwesomeIcon icon={faSquare} className="text-xs mr-2"/>} {tag.name}
</button>
</Menu.Item>
)})}
<button
type="button"
className='w-full text-left bg-mineshaft-800 hover:bg-primary hover:text-black duration-200 px-2 py-0.5 text-bunker-200 rounded-sm'
onClick={() => router.push(`/settings/project/${String(router.query.id)}`)}
>
<FontAwesomeIcon icon={faPlus} className="mr-2 text-xs" />Add more tags
</button>
</Menu.Items>
</Transition>
</Menu>
);
};
export default AddTagsMenu;
@@ -0,0 +1,88 @@
import { SetStateAction, useEffect, useState } from 'react';
import Image from 'next/image';
import { WorkspaceEnv } from '@app/hooks/api/types';
import getSecretsForProject from '../utilities/secrets/getSecretsForProject';
import { Modal, ModalContent } from '../v2';
interface Secrets {
label: string;
secret: string;
}
interface CompareSecretsModalProps {
compareModal: boolean;
setCompareModal: React.Dispatch<SetStateAction<boolean>>;
selectedEnv: WorkspaceEnv;
workspaceEnvs: WorkspaceEnv[];
workspaceId: string;
currentSecret: {
key: string;
value: string;
};
}
const CompareSecretsModal = ({
compareModal,
setCompareModal,
selectedEnv,
workspaceEnvs,
workspaceId,
currentSecret
}: CompareSecretsModalProps) => {
const [secrets, setSecrets] = useState<Secrets[]>([]);
const getEnvSecrets = async () => {
const workspaceEnvironments = workspaceEnvs?.filter((env) => env !== selectedEnv);
const newSecrets = await Promise.all(
workspaceEnvironments.map(async (env) => {
// #TODO: optimize this query somehow...
const allSecrets = await getSecretsForProject({ env: env.slug, workspaceId });
const secret =
allSecrets.find((item) => item.key === currentSecret.key)?.value ?? 'Not found';
return { label: env.name, secret };
})
);
setSecrets([{ label: selectedEnv.name, secret: currentSecret.value }, ...newSecrets]);
};
useEffect(() => {
if (compareModal) {
(async () => {
await getEnvSecrets();
})();
}
}, [compareModal]);
return (
<Modal isOpen={compareModal} onOpenChange={setCompareModal}>
<ModalContent title={currentSecret?.key} onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="space-y-4">
{secrets.length === 0 ? (
<div className="flex items-center bg-bunker-900 justify-center h-full py-4 rounded-md">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
/>
</div>
) : (
secrets.map((item) => (
<div key={`${currentSecret.key}${item.label}`} className="space-y-0.5">
<p className="text-sm text-bunker-300">{item.label}</p>
<input
defaultValue={item.secret}
className="h-no-capture border border-mineshaft-500 text-md min-w-16 no-scrollbar::-webkit-scrollbar peer z-10 w-full rounded-md bg-bunker-800 px-2 py-1.5 font-mono text-gray-400 caret-white outline-none duration-200 no-scrollbar focus:ring-2 focus:ring-primary/50 "
readOnly
/>
</div>
))
)}
</div>
</ModalContent>
</Modal>
);
};
export default CompareSecretsModal;
@@ -0,0 +1,42 @@
import { Button, Modal, ModalContent } from '../v2';
type Props = {
isOpen: boolean;
onClose: () => void;
onOverwriteConfirm: (preserve: 'old' | 'new') => void;
duplicateKeys: string[];
};
const ConfirmEnvOverwriteModal = ({
isOpen,
onClose,
duplicateKeys,
onOverwriteConfirm
}: Props): JSX.Element => {
return (
<Modal isOpen={isOpen}>
<ModalContent
title="Duplicate Secrets"
footerContent={
<div className="flex items-center gap-4">
<Button colorSchema="primary" onClick={() => onOverwriteConfirm('old')}>
Keep Old
</Button>
<Button colorSchema="danger" onClick={() => onOverwriteConfirm('new')}>
Overwrite
</Button>
</div>
}
onClose={onClose}
>
<div className="flex flex-col gap-2">
<p className='text-gray-400'>Your file contains the following duplicate secrets:</p>
<p className="text-sm text-gray-500">{duplicateKeys.join(', ')}</p>
<p className='text-md text-gray-400'>Are you sure you want to overwrite these secrets?</p>
</div>
</ModalContent>
</Modal>
);
};
export default ConfirmEnvOverwriteModal;
@@ -1,8 +1,9 @@
import { memo, SyntheticEvent, useRef } from 'react';
import { faCircle } from '@fortawesome/free-solid-svg-icons';
import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../utilities/randomId';
import { HoverObject } from '../v2/HoverCard';
const REGEX = /([$]{.*?})/g;
@@ -10,10 +11,13 @@ interface DashboardInputFieldProps {
position: number;
onChangeHandler: (value: string, position: number) => void;
value: string | undefined;
type: 'varName' | 'value';
type: 'varName' | 'value' | 'comment';
blurred?: boolean;
isDuplicate?: boolean;
override?: boolean;
isCapitalized?: boolean;
overrideEnabled?: boolean;
modifyValueOverride?: (value: string | undefined, position: number) => void;
isSideBarOpen?: boolean;
}
/**
@@ -26,6 +30,8 @@ interface DashboardInputFieldProps {
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
* @param {boolean} obj.isDuplicate - if the key name is duplicated
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
*
*
* @returns
*/
@@ -36,7 +42,10 @@ const DashboardInputField = ({
value,
blurred,
isDuplicate,
override
isCapitalized,
overrideEnabled,
modifyValueOverride,
isSideBarOpen
}: DashboardInputFieldProps) => {
const ref = useRef<HTMLDivElement | null>(null);
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
@@ -51,41 +60,97 @@ const DashboardInputField = ({
const error = startsWithNumber || isDuplicate;
return (
<div className="flex-col w-full">
<div className={`relative flex-col w-full h-10 ${
error && value !== '' ? 'bg-red/[0.15]' : ''
} ${
isSideBarOpen && 'bg-mineshaft-700 duration-200'
}`}>
<div
className={`group relative flex flex-col justify-center w-full border ${
error ? 'border-red' : 'border-mineshaft-500'
} rounded-md`}
className={`group relative flex flex-col justify-center items-center h-full ${
error ? 'w-max' : 'w-full'
}`}
>
<input
onChange={(e) => onChangeHandler(e.target.value.toUpperCase(), position)}
onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, position)}
type={type}
value={value}
className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${
error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${
error ? 'text-red-600 focus:text-red-500' : 'text-bunker-300 focus:text-bunker-100'
} duration-200`}
spellCheck="false"
/>
</div>
{startsWithNumber && (
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
Should not start with a number
</p>
<div className='absolute right-2 top-2 text-red z-50'>
<HoverObject
text="Secret names should not start with a number"
icon={faExclamationCircle}
color="red"
/>
</div>
)}
{isDuplicate && !startsWithNumber && (
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
Secret names should be unique
</p>
{isDuplicate && value !== '' && !startsWithNumber && (
<div className='absolute right-2 top-2 text-red z-50'>
<HoverObject
text="Secret names should be unique"
icon={faExclamationCircle}
color="red"
/>
</div>
)}
{!error && <div className={`absolute right-0 top-0 text-red z-50 ${
overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700'
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
<button type="button" onClick={() => {
if (modifyValueOverride) {
if (overrideEnabled === false) {
modifyValueOverride('', position);
} else {
modifyValueOverride(undefined, position);
}
}
}}>
<HoverObject
text={overrideEnabled ? 'This secret is overriden with your personal value' : 'You can override this secret with a personal value'}
icon={faLayerGroup}
color={overrideEnabled ? 'primary' : 'bunker-400'}
/>
</button>
</div>}
</div>
);
}
if (type === 'comment') {
const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== '';
const error = startsWithNumber || isDuplicate;
return (
<div className={`relative flex-col w-full h-10 ${
isSideBarOpen && 'bg-mineshaft-700 duration-200'
}`}>
<div
className={`group relative flex flex-col justify-center items-center ${
error ? 'w-max' : 'w-full'
}`}
>
<input
onChange={(e) => onChangeHandler(e.target.value, position)}
type={type}
value={value}
className='z-10 peer font-mono ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
spellCheck="false"
placeholder='–'
/>
</div>
</div>
);
}
if (type === 'value') {
return (
<div className="flex-col w-full">
<div className="group relative whitespace-pre flex flex-col justify-center w-full border border-mineshaft-500 rounded-md">
{override === true && (
<div className="bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md">
<div className="group relative whitespace-pre flex flex-col justify-center w-full">
{overrideEnabled === true && (
<div className="bg-primary-500 rounded-sm absolute top-[0.1rem] right-[0.1rem] z-0 w-min text-xxs px-1 text-black opacity-80">
Override enabled
</div>
)}
@@ -95,19 +160,19 @@ const DashboardInputField = ({
onScroll={syncScroll}
className={`${
blurred
? 'text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent'
? 'text-transparent focus:text-transparent active:text-transparent'
: ''
} z-10 peer font-mono ph-no-capture bg-transparent rounded-md caret-white text-transparent text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
} z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
spellCheck="false"
/>
<div
ref={ref}
className={`${
blurred && !override
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
blurred && !overrideEnabled
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400 duration-200'
: ''
} ${override ? 'text-primary-300' : 'text-gray-400'}
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
} ${overrideEnabled ? 'text-primary-300' : 'text-gray-400'}
absolute flex flex-row whitespace-pre font-mono z-0 ${blurred ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
>
{value?.split(REGEX).map((word, id) => {
if (word.match(REGEX) !== null) {
@@ -137,7 +202,9 @@ const DashboardInputField = ({
})}
</div>
{blurred && (
<div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full rounded-md text-gray-400/50 text-clip">
<div className={`absolute flex flex-row justify-between items-center z-0 peer pr-2 ${
isSideBarOpen ? 'bg-mineshaft-700 duration-200' : 'bg-mineshaft-800'
} peer-active:hidden peer-focus:hidden group-hover:bg-white/[0.00] duration-100 h-10 w-full text-bunker-400 text-clip`}>
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
{value?.split('').map(() => (
<FontAwesomeIcon
@@ -146,7 +213,9 @@ const DashboardInputField = ({
icon={faCircle}
/>
))}
{value?.split('').length === 0 && <span className='text-bunker-400/80'>EMPTY</span>}
</div>
<div className='invisible group-hover:visible cursor-pointer'><FontAwesomeIcon icon={faEye} /></div>
</div>
)}
</div>
@@ -163,8 +232,10 @@ function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInput
prev.type === next.type &&
prev.position === next.position &&
prev.blurred === next.blurred &&
prev.override === next.override &&
prev.isDuplicate === next.isDuplicate
prev.isCapitalized === next.isCapitalized &&
prev.overrideEnabled === next.overrideEnabled &&
prev.isDuplicate === next.isDuplicate &&
prev.isSideBarOpen === next.isSideBarOpen
);
}
@@ -1,26 +1,42 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '../basic/buttons/Button';
import { DeleteEnvVar } from '../basic/dialog/DeleteEnvVar';
type Props = {
onSubmit: () => void
onSubmit: () => void;
isPlain?: boolean;
}
export const DeleteActionButton = ({ onSubmit }: Props) => {
export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false)
return (
<div className="bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2">
<Button
<div className={`${
!isPlain
? 'bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2'
: 'cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'}`}>
{isPlain
? <div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => setOpen(true)}
className="invisible group-hover:visible"
>
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5" icon={faXmark} />
</div>
: <Button
text={String(t("Delete"))}
// onButtonPressed={onSubmit}
color="red"
size="md"
onButtonPressed={() => setOpen(true)}
/>
/>}
<DeleteEnvVar
isOpen={open}
onClose={() => {
@@ -31,7 +31,7 @@ const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: strin
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
<Menu.Items className="absolute z-[90] drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
<Menu.Item>
<Button
color="mineshaft"
@@ -64,7 +64,8 @@ const DropZone = ({
key,
value: keyPairs[key as keyof typeof keyPairs].value,
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
type: 'shared'
type: 'shared',
tags: []
}));
break;
}
@@ -86,7 +87,8 @@ const DropZone = ({
key,
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
comment,
type: 'shared'
type: 'shared',
tags: []
};
});
break;
@@ -152,7 +154,7 @@ const DropZone = ({
</div>
) : keysExist ? (
<div
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-4 mx-auto mt-1 py-8 px-2"
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
+116 -37
View File
@@ -1,21 +1,51 @@
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
import { faEllipsis, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
import AddTagsMenu from './AddTagsMenu';
import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton';
interface KeyPairProps {
keyPair: SecretDataProps;
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void;
modifyTags: (value: Tag[], position: number) => void;
isBlurred: boolean;
isDuplicate: boolean;
toggleSidebar: (id: string) => void;
sidebarSecretId: string;
isSnapshot: boolean;
isCapitalized: boolean;
deleteRow?: (props: DeleteRowFunctionProps) => void;
tags: Tag[];
togglePITSidebar?: (value: boolean) => void;
}
export interface DeleteRowFunctionProps {
ids: string[];
secretName: string;
}
const colors = [
'bg-[#f1c40f]/40',
'bg-[#cb1c8d]/40',
'bg-[#badc58]/40',
'bg-[#ff5400]/40',
'bg-[#00bbf9]/40'
]
const colorsText = [
'text-[#fcf0c3]/70',
'text-[#f2c6e3]/70',
'text-[#eef6d5]/70',
'text-[#ffddcc]/70',
'text-[#f0fffd]/70'
]
/**
* This component represent a single row for an environemnt variable on the dashboard
* @param {object} obj
@@ -23,11 +53,16 @@ interface KeyPairProps {
* @param {function} obj.modifyKey - modify the key of a certain environment variable
* @param {function} obj.modifyValue - modify the value of a certain environment variable
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
* @param {function} obj.modifyComment - modify the comment of a certain environment variable
* @param {function} obj.modifyTags - modify the tags of a certain environment variable
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @param {function} obj.togglePITSidebar - open or close the Point-in-time recovery sidebar
* @param {Tag[]} obj.tags - tags for a certain secret
* @returns
*/
const KeyPair = ({
@@ -35,66 +70,110 @@ const KeyPair = ({
modifyKey,
modifyValue,
modifyValueOverride,
modifyComment,
modifyTags,
isBlurred,
isDuplicate,
toggleSidebar,
sidebarSecretId,
isSnapshot
}: KeyPairProps) => (
isCapitalized,
isSnapshot,
deleteRow,
togglePITSidebar,
tags
}: KeyPairProps) => {
const tagData = (tags.map((tag, index) => {return {
...tag,
color: colors[index%colors.length],
colorText: colorsText[index%colorsText.length]
}}));
return (
<div
className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && 'pointer-events-none'} ${
keyPair.id === sidebarSecretId && 'bg-mineshaft-500 duration-200'
} rounded-md`}
className={`group flex flex-col items-center border-b border-mineshaft-500 hover:bg-white/[0.03] duration-100 ${isSnapshot && 'pointer-events-none'} ${
keyPair.id === sidebarSecretId && 'bg-mineshaft-700 duration-200'
}`}
>
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
{keyPair.valueOverride && (
<div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
<div className="w-1 h-1 rounded-full bg-primary z-40" />
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
This secret is overriden
</span>
</div>
)}
<div className="min-w-xl w-96">
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
<div className='text-bunker-400 text-xs flex items-center justify-center w-14 h-10 cursor-default'>{keyPair.pos + 1}</div>
<div className="flex items-center max-h-16">
<DashboardInputField
isCapitalized = {isCapitalized}
onChangeHandler={modifyKey}
type="varName"
position={keyPair.pos}
value={keyPair.key}
isDuplicate={isDuplicate}
overrideEnabled={keyPair.valueOverride !== undefined}
modifyValueOverride={modifyValueOverride}
isSideBarOpen={keyPair.id === sidebarSecretId}
/>
</div>
</div>
<div className="w-full min-w-xl">
<div className="w-5/12 border-r border-mineshaft-600">
<div
className={`flex min-w-xl items-center ${
!isSnapshot && 'pr-1.5'
} rounded-lg mt-4 md:mt-0 max-h-10`}
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
>
<DashboardInputField
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue}
onChangeHandler={keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue}
type="value"
position={keyPair.pos}
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value}
blurred={isBlurred}
override={Boolean(keyPair.valueOverride)}
overrideEnabled={keyPair.valueOverride !== undefined}
isSideBarOpen={keyPair.id === sidebarSecretId}
/>
</div>
</div>
{!isSnapshot && (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => toggleSidebar(keyPair.id)}
className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"
>
<FontAwesomeIcon className="text-gray-300 px-2.5 text-lg mt-0.5" icon={faEllipsis} />
<div className="w-2/12 border-r border-mineshaft-600">
<div className="flex items-center max-h-16">
<DashboardInputField
onChangeHandler={modifyComment}
type="comment"
position={keyPair.pos}
value={keyPair.comment}
isDuplicate={isDuplicate}
isSideBarOpen={keyPair.id === sidebarSecretId}
/>
</div>
)}
</div>
<div className="w-2/12 h-10 flex items-center overflow-visible overflow-r-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
<div className="flex items-center max-h-16">
{keyPair.tags?.map((tag, index) => (
index < 2 && <div key={keyPair.pos} className={`ml-2 px-1.5 ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}>
<span className='mb-0.5 cursor-default'>{tag.name}</span>
<FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/>
</div>
))}
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} position={keyPair.pos} />
</div>
</div>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => {
if (togglePITSidebar) {
togglePITSidebar(false);
}
toggleSidebar(keyPair.id)
}}
className={`cursor-pointer w-[1.5rem] h-[2.35rem] ml-auto group-hover:bg-mineshaft-700 z-50 rounded-md invisible group-hover:visible flex flex-row justify-center items-center ${isSnapshot ?? 'invisible'}`}
>
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
</div>
<div className={`group-hover:bg-mineshaft-700 z-50 ${isSnapshot ?? 'invisible'}`}>
<DeleteActionButton
onSubmit={() => { if (deleteRow) {
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
}}}
isPlain
/>
</div>
</div>
</div>
);
)};
export default KeyPair;
+76 -43
View File
@@ -2,14 +2,16 @@
import { useState } from 'react';
import Image from 'next/image';
import { useTranslation } from 'next-i18next';
import { faX } from '@fortawesome/free-solid-svg-icons';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import SecretVersionList from '@app/ee/components/SecretVersionList';
import { WorkspaceEnv } from '@app/hooks/api/types';
import Button from '../basic/buttons/Button';
import Toggle from '../basic/Toggle';
import CommentField from './CommentField';
import CompareSecretsModal from './CompareSecretsModal';
import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton';
import GenerateSecretMenu from './GenerateSecretMenu';
@@ -40,6 +42,9 @@ interface SideBarProps {
sharedToHide: string[];
setSharedToHide: (values: string[]) => void;
deleteRow: (props: DeleteRowFunctionProps) => void;
workspaceEnvs: WorkspaceEnv[];
selectedEnv: WorkspaceEnv;
workspaceId: string;
}
/**
@@ -63,15 +68,19 @@ const SideBar = ({
modifyComment,
buttonReady,
savePush,
deleteRow
deleteRow,
workspaceEnvs,
selectedEnv,
workspaceId
}: SideBarProps) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoading, setIsLoading] = useState(false);
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined);
const [overrideEnabled, setOverrideEnabled] = useState(data[0]?.valueOverride !== undefined);
const [compareModal, setCompareModal] = useState(false);
const { t } = useTranslation();
return (
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between">
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-[28rem] sticky top-0 right-0 z-[70] shadow-xl flex flex-col justify-between">
{isLoading ? (
<div className="flex items-center justify-center h-full">
<Image
@@ -92,19 +101,21 @@ const SideBar = ({
className="p-1"
onClick={() => toggleSidebar('None')}
>
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
</div>
</div>
<div className="mt-4 px-4 pointer-events-none">
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.key')}</p>
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
position={data[0]?.pos}
value={data[0]?.key}
isDuplicate={false}
blurred={false}
/>
<div className='rounded-md border overflow-hidden border-mineshaft-600 bg-white/5'>
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
position={data[0]?.pos}
value={data[0]?.key}
isDuplicate={false}
blurred={false}
/>
</div>
</div>
{(data[0]?.value || data[0]?.value === "") ? (
<div
@@ -113,14 +124,16 @@ const SideBar = ({
} duration-200`}
>
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.value')}</p>
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
position={data[0].pos}
value={data[0]?.value}
isDuplicate={false}
blurred
/>
<div className='rounded-md border overflow-hidden border-mineshaft-600 bg-white/5'>
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
position={data[0].pos}
value={data[0]?.value}
isDuplicate={false}
blurred
/>
</div>
<div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50">
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
</div>
@@ -150,14 +163,16 @@ const SideBar = ({
!overrideEnabled && 'opacity-40 pointer-events-none'
} duration-200`}
>
<DashboardInputField
onChangeHandler={modifyValueOverride}
type="value"
position={data[0]?.pos}
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
isDuplicate={false}
blurred
/>
<div className='rounded-md border overflow-hidden border-mineshaft-600 bg-white/5'>
<DashboardInputField
onChangeHandler={modifyValueOverride}
type="value"
position={data[0]?.pos}
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
isDuplicate={false}
blurred
/>
</div>
<div className="absolute right-[0.57rem] top-[0.3rem] z-50">
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
</div>
@@ -171,20 +186,38 @@ const SideBar = ({
/>
</div>
)}
<div className="flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]">
<Button
text={String(t('common:save-changes'))}
onButtonPressed={savePush}
color="primary"
size="md"
active={buttonReady}
textDisabled="Saved"
/>
<DeleteActionButton
onSubmit={() =>
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
}
/>
<div className="mt-full mt-4 mb-4 flex max-w-sm flex-col justify-start space-y-2 px-4">
<div>
<Button
text="Compare secret across environments"
color="mineshaft"
size="md"
onButtonPressed={() => setCompareModal(true)}
/>
<CompareSecretsModal
compareModal={compareModal}
setCompareModal={setCompareModal}
currentSecret={{ key: data[0]?.key, value: data[0]?.value }}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv}
workspaceId={workspaceId}
/>
</div>
<div className="flex">
<Button
text={String(t('common:save-changes'))}
onButtonPressed={savePush}
color="primary"
size="md"
active={buttonReady}
textDisabled="Saved"
/>
<DeleteActionButton
onSubmit={() =>
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
}
/>
</div>
</div>
</div>
);
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// TODO: This needs to be moved from public folder
import { contextNetlifyMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
import { contextNetlifyMapping, integrationSlugNameMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox';
@@ -52,6 +52,7 @@ const IntegrationTile = ({
environments = [],
handleDeleteIntegration
}: Props) => {
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration?.environment) || {
@@ -71,7 +72,14 @@ const IntegrationTile = ({
});
setApps(tempApps);
setIntegrationApp(integration?.app ? integration.app : tempApps[0].name);
if (integration?.app) {
setIntegrationApp(integration.app);
} else if (tempApps.length > 0) {
setIntegrationApp(tempApps[0].name)
} else {
setIntegrationApp('');
}
switch (integration.integration) {
case 'vercel':
@@ -174,7 +182,7 @@ const IntegrationTile = ({
return <div />;
};
if (!integrationApp || apps.length === 0) return <div />;
if (!integrationApp) return <div />;
return (
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-white/5 p-6">
@@ -199,9 +207,10 @@ const IntegrationTile = ({
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
</div>
<div className="mr-2">
<p className="mb-2 text-xs font-semibold text-gray-400">INTEGRATION</p>
<div className="rounded-md bg-white/[.07] py-2.5 pl-4 pr-10 text-sm font-semibold text-gray-300">
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
{integrationSlugNameMapping[integration.integration]}
</div>
</div>
<div className="mr-2">
@@ -93,7 +93,7 @@ export default function Navbar() {
};
return (
<div className="absolute flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
<div className="m-auto flex justify-start items-center mx-4">
<div className="flex flex-row items-center">
<div className="flex justify-center py-4">
@@ -3,8 +3,8 @@ import { useRouter } from 'next/router';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useWorkspace } from '@app/context';
import getOrganization from '@app/pages/api/organization/GetOrg';
import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
/**
* This is the component at the top of almost every page.
@@ -23,9 +23,9 @@ export default function NavHeader({
isProjectRelated?: boolean;
}): JSX.Element {
const [orgName, setOrgName] = useState('');
const [workspaceName, setWorkspaceName] = useState('');
const router = useRouter();
const projectId = String(router.query.id);
const { currentWorkspace } = useWorkspace();
useEffect(() => {
(async () => {
@@ -34,29 +34,24 @@ export default function NavHeader({
orgId: orgId || ''
});
setOrgName(org.name);
const workspace = await getProjectInfo({
projectId
});
setWorkspaceName(workspace.name);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]);
return (
<div className="pt-20 ml-6 flex flex-row items-center">
<div className="bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2">
<div className="ml-6 flex flex-row items-center pt-8">
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
{orgName?.charAt(0)}
</div>
<div className="text-primary text-sm font-semibold">{orgName}</div>
<div className="text-sm font-semibold text-primary">{orgName}</div>
{isProjectRelated && (
<>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
<div className="font-semibold text-primary text-sm">{workspaceName}</div>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
<div className="text-sm font-semibold text-primary">{currentWorkspace?.name}</div>
</>
)}
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
<div className="text-gray-400 text-sm">{pageName}</div>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
<div className="text-sm text-gray-400">{pageName}</div>
</div>
);
}
@@ -137,8 +137,9 @@ const attemptLogin = async (
// eslint-disable-next-line no-template-curly-in-string
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
comment: 'This is an example of secret referencing.',
id: ''
comment: 'Secret referencing example',
id: '',
tags: []
},
{
pos: 1,
@@ -146,8 +147,9 @@ const attemptLogin = async (
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
id: ''
'Override secrets with personal value',
id: '',
tags: []
},
{
pos: 2,
@@ -155,8 +157,9 @@ const attemptLogin = async (
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
id: ''
'Another secret override',
id: '',
tags: []
},
{
pos: 3,
@@ -164,7 +167,8 @@ const attemptLogin = async (
value: 'user1234',
valueOverride: 'user1234',
comment: '',
id: ''
id: '',
tags: []
},
{
pos: 4,
@@ -172,7 +176,8 @@ const attemptLogin = async (
value: 'example_password',
valueOverride: 'example_password',
comment: '',
id: ''
id: '',
tags: []
},
{
pos: 5,
@@ -180,7 +185,8 @@ const attemptLogin = async (
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
id: ''
id: '',
tags: []
},
{
pos: 6,
@@ -188,7 +194,8 @@ const attemptLogin = async (
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
id: ''
id: '',
tags: []
}
];
const secrets = await encryptSecrets({
@@ -1,6 +1,6 @@
import crypto from 'crypto';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import { SecretDataProps, Tag } from 'public/data/frequentInterfaces';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -20,6 +20,7 @@ interface EncryptedSecretProps {
secretValueIV: string;
secretValueTag: string;
type: 'personal' | 'shared';
tags: Tag[];
}
/**
@@ -105,7 +106,8 @@ const encryptSecrets = async ({
type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared'
: 'personal'
: 'personal',
tags: secret.tags
};
return result;
@@ -1,3 +1,5 @@
import { Tag } from 'public/data/frequentInterfaces';
import getSecrets from '@app/pages/api/files/GetSecrets';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
@@ -17,6 +19,7 @@ interface EncryptedSecretProps {
secretValueIV: string;
secretValueTag: string;
type: 'personal' | 'shared';
tags: Tag[];
}
interface SecretProps {
@@ -25,12 +28,13 @@ interface SecretProps {
type: 'personal' | 'shared';
comment: string;
id: string;
tags: Tag[];
}
interface FunctionProps {
env: string;
setIsKeyAvailable: any;
setData: any;
setIsKeyAvailable?: any;
setData?: any;
workspaceId: string;
}
@@ -58,7 +62,9 @@ const getSecretsForProject = async ({
const latestKey = await getLatestFileKey({ workspaceId });
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
if (typeof setIsKeyAvailable === 'function') {
setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
}
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
@@ -105,7 +111,8 @@ const getSecretsForProject = async ({
key: plainTextKey,
value: plainTextValue,
type: secret.type,
comment: plainTextComment
comment: plainTextComment,
tags: secret.tags
});
});
}
@@ -128,10 +135,16 @@ const getSecretsForProject = async ({
)[0]?.value,
comment: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.comment
)[0]?.comment,
tags: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.tags
}));
setData(result);
if (typeof setData === 'function') {
setData(result);
}
return result;
} catch (error) {
console.log('Something went wrong during accessing or decripting secrets.');
+14 -12
View File
@@ -18,14 +18,15 @@ const buttonVariants = cva(
'font-inter font-medium',
'cursor-pointer',
'inline-flex items-center justify-center',
'relative'
'relative',
'whitespace-nowrap'
],
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red']
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
},
variant: {
solid: '',
@@ -33,7 +34,7 @@ const buttonVariants = cva(
plain: ''
},
isDisabled: {
true: 'bg-opacity-70 cursor-not-allowed',
true: 'bg-mineshaft opacity-40 cursor-not-allowed',
false: ''
},
isFullWidth: {
@@ -45,9 +46,9 @@ const buttonVariants = cva(
false: ''
},
size: {
xs: ['text-xs', 'py-1', 'px-2'],
sm: ['text-sm', 'py-2', 'px-4'],
md: ['text-md', 'py-2', 'px-6'],
xs: ['text-xs', 'py-1', 'px-1'],
sm: ['text-sm', 'py-2', 'px-2'],
md: ['text-md', 'py-2', 'px-4'],
lg: ['text-lg', 'py-2', 'px-8']
}
},
@@ -75,7 +76,7 @@ const buttonVariants = cva(
{
colorSchema: 'secondary',
variant: 'plain',
className: 'text-mineshaft'
className: 'text-mineshaft-300'
},
{
colorSchema: 'danger',
@@ -101,7 +102,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
children,
isDisabled = false,
className = '',
size = 'md',
size = 'sm',
variant = 'solid',
isFullWidth,
isRounded = true,
@@ -144,9 +145,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
)}
<span
className={twMerge(
'transition-all shrink-0 cursor-pointer',
'shrink-0 cursor-pointer transition-all',
loadingToggleClass,
size === 'xs' ? 'mr-1' : 'mr-2'
leftIcon && 'ml-2',
size === 'xs' ? 'mr-1' : 'mr-3'
)}
>
{leftIcon}
@@ -154,9 +156,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
<span className={twMerge('transition-all', loadingToggleClass)}>{children}</span>
<span
className={twMerge(
'transition-all shrink-0 cursor-pointer',
'shrink-0 cursor-pointer transition-all',
loadingToggleClass,
size === 'xs' ? 'ml-1' : 'ml-2'
size === 'xs' ? 'ml-1' : 'ml-3'
)}
>
{rightIcon}
+3 -3
View File
@@ -8,7 +8,7 @@ export type CardTitleProps = {
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
<div className={twMerge('p-6 pb-4 font-sans text-xl font-medium', className)}>
<div className={twMerge('px-6 py-4 font-sans text-xl font-medium', className)}>
{children}
{subTitle && <p className="py-1 text-sm font-normal text-gray-400">{subTitle}</p>}
</div>
@@ -29,7 +29,7 @@ export type CardBodyProps = {
};
export const CardBody = ({ children, className }: CardBodyProps) => (
<div className={twMerge('p-6 pt-0', className)}>{children}</div>
<div className={twMerge('px-6 pb-6 pt-0', className)}>{children}</div>
);
export type CardProps = {
@@ -47,7 +47,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(
<div
ref={ref}
className={twMerge(
'flex flex-col w-full font-inter text-gray-200 bg-mineshaft-700 shadow-md',
'flex w-full flex-col bg-mineshaft-800 font-inter text-gray-200 shadow-md',
isFullHeight && 'h-full',
isRounded && 'rounded-md',
isPlain && 'shadow-none',
@@ -0,0 +1,89 @@
import { useEffect, useState } from 'react';
import { useToggle } from '@app/hooks';
import { Button } from '../Button';
import { FormControl } from '../FormControl';
import { Input } from '../Input';
import { Modal, ModalContent } from '../Modal';
type Props = {
isOpen?: boolean;
onClose?: () => void;
onChange?: (isOpen: boolean) => void;
deleteKey: string;
title: string;
onDeleteApproved: () => Promise<void>;
};
export const DeleteActionModal = ({
isOpen,
onClose,
onChange,
deleteKey,
onDeleteApproved,
title
}: Props): JSX.Element => {
const [inputData, setInputData] = useState('');
const [isLoading, setIsLoading] = useToggle();
useEffect(() => {
setInputData('');
}, [isOpen]);
const onDelete = async () => {
setIsLoading.on();
try {
await onDeleteApproved();
} catch {
setIsLoading.off();
} finally {
setIsLoading.off();
}
};
return (
<Modal
isOpen={isOpen}
onOpenChange={(isOpenState) => {
setInputData('');
if (onChange) onChange(isOpenState);
}}
>
<ModalContent
title={title}
subTitle="This action is irreversible!"
footerContent={
<div className="flex items-center">
<Button
className="mr-4"
colorSchema="danger"
isDisabled={!(deleteKey === inputData) || isLoading}
onClick={onDelete}
isLoading={isLoading}
>
Delete
</Button>
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel
</Button>
</div>
}
onClose={onClose}
>
<form>
<FormControl
label={
<div className="pb-2 text-sm">
Type <span className="font-bold">{deleteKey}</span> to delete the resource
</div>
}
className="mb-4"
>
<Input value={inputData} onChange={(e) => setInputData(e.target.value)} />
</FormControl>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export { DeleteActionModal } from './DeleteActionModal';
@@ -11,7 +11,7 @@ export type FormLabelProps = {
};
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
<Label.Root className="text-mineshaft-300 text-sm font-medium block mb-1 ml-0.5" htmlFor={id}>
<Label.Root className="mb-1 ml-0.5 block text-sm font-medium text-mineshaft-300" htmlFor={id}>
{label}
{isRequired && <span className="ml-1 text-red">*</span>}
</Label.Root>
@@ -25,7 +25,7 @@ export type FormHelperTextProps = {
export const FormHelperText = ({ isError, text }: FormHelperTextProps) => (
<div
className={twMerge(
'text-xs font-inter flex items-center opacity-90 text-mineshaft-300 mt-2',
'mt-2 flex items-center font-inter text-xs text-mineshaft-300 opacity-90',
isError && 'text-red-600'
)}
>
@@ -46,6 +46,7 @@ export type FormControlProps = {
helperText?: ReactNode;
errorText?: ReactNode;
children: JSX.Element;
className?: string;
};
export const FormControl = ({
@@ -55,10 +56,11 @@ export const FormControl = ({
helperText,
errorText,
id,
isError
isError,
className
}: FormControlProps): JSX.Element => {
return (
<div>
<div className={twMerge('mb-4', className)}>
{typeof label === 'string' ? (
<FormLabel label={label} isRequired={isRequired} id={id} />
) : (
@@ -0,0 +1,42 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as HoverCard from '@radix-ui/react-hover-card';
type Props = {
text: string;
icon: IconProp;
color: string;
};
export type HoverCardProps = Props;
export const HoverObject = ({
text,
icon,
color
}: Props): JSX.Element => (
<HoverCard.Root openDelay={50}>
<HoverCard.Trigger asChild>
<a
className="ImageTrigger z-20"
>
<FontAwesomeIcon icon={icon} className={`text-${color}`} />
</a>
</HoverCard.Trigger>
<HoverCard.Portal>
<HoverCard.Content className="HoverCardContent z-[50]" sideOffset={5}>
<div className='bg-bunker-700 border border-mineshaft-600 p-2 rounded-md drop-shadow-xl text-bunker-300'>
<div style={{ display: 'flex', flexDirection: 'column', gap: 15 }}>
<div>
<div className="Text bold">{text}</div>
</div>
</div>
</div>
<HoverCard.Arrow className="border-mineshaft-600" />
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
);
HoverObject.displayName = 'HoverCard';
@@ -0,0 +1,2 @@
export type { HoverCardProps } from './HoverCard';
export { HoverObject } from './HoverCard';
@@ -14,7 +14,7 @@ const iconButtonVariants = cva(
[
'button',
'transition-all',
'font-inter font-medium',
'font-inter font-medium user-select-none',
'cursor-pointer',
'inline-flex items-center justify-center',
'relative'
@@ -22,9 +22,9 @@ const iconButtonVariants = cva(
{
variants: {
colorSchema: {
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
danger: ['bg-red', 'text-white', 'border-red']
primary: ['bg-primary', 'text-black', 'border-primary hover:opacity-80'],
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-bunker-400'],
danger: ['bg-[#973939] hover:bg-red', 'text-white', 'border-red']
},
variant: {
solid: '',
@@ -40,10 +40,10 @@ const iconButtonVariants = cva(
false: ''
},
size: {
xs: ['text-xs', 'py-1', 'px-2'],
sm: ['text-sm', 'py-2', 'px-3'],
md: ['text-md', 'py-3', 'px-4'],
lg: ['text-lg', 'py-3', 'px-6']
xs: ['text-xs', 'py-2', 'px-2'],
sm: ['text-sm', 'py-3', 'px-3'],
md: ['text-md', 'py-4', 'px-4'],
lg: ['text-lg', 'py-6', 'px-6']
}
},
compoundVariants: [
@@ -80,7 +80,7 @@ const iconButtonVariants = cva(
{
colorSchema: ['danger', 'primary', 'secondary'],
variant: ['plain'],
className: 'bg-transparent py-1 px-1'
className: 'bg-transparent py-1 px-1 text-bunker-300'
}
]
}
@@ -97,7 +97,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
ariaLabel,
isDisabled = false,
className,
size = 'md',
size = 'sm',
variant = 'solid',
isRounded = true,
colorSchema = 'primary',
+7 -4
View File
@@ -8,10 +8,11 @@ type Props = {
isRequired?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
isDisabled?: boolean;
};
const inputVariants = cva(
'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50',
'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2',
{
variants: {
size: {
@@ -46,7 +47,7 @@ const inputParentContainerVariants = cva('inline-flex font-inter items-center bo
},
isError: {
true: 'border-red',
false: 'border-mineshaft-400'
false: 'border-mineshaft-500'
},
isFullWidth: {
true: 'w-full',
@@ -70,6 +71,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
className,
isRounded = true,
isFullWidth = true,
isDisabled,
isError = false,
isRequired,
leftIcon,
@@ -87,9 +89,10 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
{...props}
required={isRequired}
ref={ref}
disabled={isDisabled}
className={twMerge(
leftIcon ? 'pl-9' : 'pl-4',
rightIcon ? 'pr-9' : 'pr-4',
leftIcon ? 'pl-9' : 'pl-2.5',
rightIcon ? 'pr-9' : 'pr-2.5',
inputVariants({ className, isError, size, isRounded, variant })
)}
/>
+11 -7
View File
@@ -11,33 +11,34 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
title?: ReactNode;
subTitle?: string;
footerContent?: ReactNode;
onClose?: () => void;
};
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
({ children, title, subTitle, className, footerContent, ...props }, forwardedRef) => (
({ children, title, subTitle, className, footerContent, onClose, ...props }, forwardedRef) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className="fixed inset-0 w-full h-full animate-fadeIn"
style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}
className="fixed inset-0 h-full w-full animate-fadeIn z-[70]"
style={{ backgroundColor: 'rgba(0, 0, 0, 0.7)' }}
/>
<DialogPrimitive.Content {...props} ref={forwardedRef}>
<Card
isRounded
className={twMerge(
'fixed max-w-md animate-popIn top-1/2 left-1/2 -translate-y-2/4 -translate-x-2/4',
'fixed top-1/2 left-1/2 max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
className
)}
>
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
<CardBody>{children}</CardBody>
{footerContent && <CardFooter>{footerContent}</CardFooter>}
<DialogPrimitive.Close aria-label="Close" asChild>
<DialogPrimitive.Close aria-label="Close" asChild onClick={onClose}>
<IconButton
variant="plain"
ariaLabel="close"
className="absolute top-2.5 right-2.5 text-white hover:bg-gray-600 rounded"
className="absolute top-3 right-3 rounded text-white hover:bg-gray-600"
>
<FontAwesomeIcon icon={faTimes} size="sm" className="cursor-pointer" />
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
</IconButton>
</DialogPrimitive.Close>
</Card>
@@ -55,3 +56,6 @@ export const Modal = ({ isOpen, ...props }: ModalProps) => (
export const ModalTrigger = DialogPrimitive.Trigger;
export type ModalTriggerProps = DialogPrimitive.DialogTriggerProps;
export const ModalClose = DialogPrimitive.Close;
export type ModalCloseProps = DialogPrimitive.DialogCloseProps;
+2 -2
View File
@@ -1,2 +1,2 @@
export type { ModalContentProps, ModalProps, ModalTriggerProps } from './Modal';
export { Modal, ModalContent, ModalTrigger } from './Modal';
export type { ModalCloseProps, ModalContentProps, ModalProps, ModalTriggerProps } from './Modal';
export { Modal, ModalClose, ModalContent, ModalTrigger } from './Modal';
+20 -14
View File
@@ -10,34 +10,40 @@ type Props = {
children: ReactNode;
placeholder?: string;
className?: string;
dropdownContainerClassName?: string;
isLoading?: boolean;
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
({ children, placeholder, className, isLoading, ...props }, ref): JSX.Element => {
(
{ children, placeholder, className, isLoading, dropdownContainerClassName, ...props },
ref
): JSX.Element => {
return (
<SelectPrimitive.Root {...props}>
<SelectPrimitive.Trigger
ref={ref}
className={twMerge(
`inline-flex items-center justify-between data-[placeholder]:text-gray-500
px-3 py-2 font-inter text-sm text-bunker-200 font-normal rounded-md bg-mineshaft-800 outline-none`,
`inline-flex items-center justify-between rounded-md
bg-bunker-800 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-gray-500`,
className
)}
>
<SelectPrimitive.Value placeholder={placeholder} />
{!props.disabled && <SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon icon={faChevronDown} size="sm" />
</SelectPrimitive.Icon>}
{!props.disabled && (
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon icon={faChevronDown} size="sm" />
</SelectPrimitive.Icon>
)}
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
// position="popper"
sideOffset={4}
className="overflow-hidden text-bunker-100 rounded-md shadow-md font-inter bg-mineshaft-800"
style={{ width: 'var(--radix-select-trigger-width) + 6' }}
className={twMerge(
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
dropdownContainerClassName
)}
>
<SelectPrimitive.ScrollUpButton>
<FontAwesomeIcon icon={faChevronUp} size="sm" />
@@ -75,11 +81,11 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
<SelectPrimitive.Item
{...props}
className={twMerge(
`text-sm rounded-sm transition-all hover:bg-mineshaft-500
flex items-center pl-10 pr-4 py-2 cursor-pointer rounded-md
select-none outline-none relative`,
`relative flex cursor-pointer
select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
outline-none transition-all hover:bg-mineshaft-500`,
isSelected && 'bg-primary',
isDisabled && 'text-gray-600 hover:bg-transparent cursor-not-allowed hover:text-gray-600',
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-gray-600',
className
)}
ref={forwardedRef}
+17 -13
View File
@@ -1,4 +1,4 @@
import { ReactNode } from 'react';
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react';
import { twMerge } from 'tailwind-merge';
export type TableContainerProps = {
@@ -14,7 +14,7 @@ export const TableContainer = ({
}: TableContainerProps): JSX.Element => (
<div
className={twMerge(
'overflow-x-auto font-inter shadow-md relative border border-solid border-mineshaft-700',
'relative w-full overflow-x-auto border border-solid border-mineshaft-700 font-inter shadow-md',
isRounded && 'rounded-md',
className
)}
@@ -32,7 +32,7 @@ export type TableProps = {
export const Table = ({ children, className }: TableProps): JSX.Element => (
<table
className={twMerge(
'bg-bunker-800 p-2 roun text-gray-300 w-full text-sm text-left rounded-md',
'w-full rounded rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
className
)}
>
@@ -47,7 +47,7 @@ export type THeadProps = {
};
export const THead = ({ children, className }: THeadProps): JSX.Element => (
<thead className={twMerge('text-xs bg-bunker text-bunker-300 uppercase', className)}>
<thead className={twMerge('bg-bunker text-xs uppercase text-bunker-300', className)}>
{children}
</thead>
);
@@ -56,20 +56,22 @@ export const THead = ({ children, className }: THeadProps): JSX.Element => (
export type TrProps = {
children: ReactNode;
className?: string;
};
} & HTMLAttributes<HTMLTableRowElement>;
export const Tr = ({ children, className }: TrProps): JSX.Element => (
<tr className={twMerge('border border-solid border-mineshaft-700', className)}>{children}</tr>
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
<tr className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)} {...props}>
{children}
</tr>
);
// table head columns
export type ThProps = {
children: ReactNode;
children?: ReactNode;
className?: string;
};
export const Th = ({ children, className }: ThProps): JSX.Element => (
<th className={twMerge('px-6 py-3 font-medium', className)}>{children}</th>
<th className={twMerge('px-5 pt-4 pb-3.5 font-medium font-semibold bg-bunker-500', className)}>{children}</th>
);
// table body
@@ -84,10 +86,12 @@ export const TBody = ({ children, className }: TBodyProps): JSX.Element => (
// table body columns
export type TdProps = {
children: ReactNode;
children?: ReactNode;
className?: string;
};
} & TdHTMLAttributes<HTMLTableCellElement>;
export const Td = ({ children, className }: TdProps): JSX.Element => (
<td className={twMerge('text-left px-6 py-3', className)}>{children}</td>
export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
<td className={twMerge('px-5 py-2 text-left', className)} {...props}>
{children}
</td>
);
@@ -0,0 +1,36 @@
import Link from 'next/link';
import { Button } from '../Button';
import { Modal, ModalClose, ModalContent } from '../Modal';
type Props = {
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
text: string;
};
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title="Unleash Infisical's Full Power"
footerContent={[
<Link
href={`/settings/billing/${localStorage.getItem('projectData.id') as string}`}
key="upgrade-plan"
>
<Button className="mr-4">Upgrade Plan</Button>
</Link>,
<ModalClose asChild key="upgrade-plan-cancel">
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
]}
>
<p className="mb-4 text-bunker-300">{text}</p>
<p className="font-medium text-bunker-300">
Upgrade and get access to this, as well as to other powerful enhancements.
</p>
</ModalContent>
</Modal>
);
@@ -0,0 +1 @@
export { UpgradePlanModal } from './UpgradePlanModal';
+2
View File
@@ -1,6 +1,7 @@
export * from './Button';
export * from './Card';
export * from './Checkbox';
export * from './DeleteActionModal';
export * from './Dropdown';
export * from './FormControl';
export * from './IconButton';
@@ -12,3 +13,4 @@ export * from './Spinner';
export * from './Switch';
export * from './Table';
export * from './TextArea';
export * from './UpgradePlanModal';
+24 -4
View File
@@ -16,11 +16,31 @@ export const publicPaths = [
`/terms`,
`/subprocessors`,
`/verify-email`,
`/password-reset`,
`/password-reset`
];
export const languageMap = {
en: "English",
ko: "한국어",
fr: "Français",
en: 'English',
ko: '한국어',
fr: 'Français'
};
interface Mapping {
[key: string]: string;
}
const plansDev: Mapping = {
starter: 'prod_Mb4ATFT5QAHoPM',
team: 'prod_NEpD2WMXUS2eDn',
professional: 'prod_Mb4CetZ2jE7jdl',
enterprise: 'licence_key_required'
};
const plansProd: Mapping = {
starter: 'prod_Mb8oR5XNwyFTul',
team: 'prod_NEp7fAB3UJWK6A',
professional: 'prod_Mb8pUIpA0OUi5N',
enterprise: 'licence_key_required'
};
export const plans = plansProd || plansDev;
@@ -0,0 +1,47 @@
import { createContext, ReactNode, useContext, useMemo } from 'react';
import { useGetOrgSubscription } from '@app/hooks/api';
import { GetSubscriptionPlan } from '@app/hooks/api/types';
import { useWorkspace } from '../WorkspaceContext';
// import { Subscription } from '@app/hooks/api/workspace/types';
type TSubscriptionContext = {
subscription?: GetSubscriptionPlan;
subscriptionPlan: string;
isLoading: boolean;
};
const SubscriptionContext = createContext<TSubscriptionContext | null>(null);
type Props = {
children: ReactNode;
};
export const SubscriptionProvider = ({ children }: Props): JSX.Element => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetOrgSubscription({
orgID: currentWorkspace?.organization || ''
});
// memorize the workspace details for the context
const value = useMemo<TSubscriptionContext>(
() => ({
subscription: data,
subscriptionPlan: data?.data?.[0]?.plan?.product || '',
isLoading
}),
[data, isLoading]
);
return <SubscriptionContext.Provider value={value}>{children}</SubscriptionContext.Provider>;
};
export const useSubscription = () => {
const ctx = useContext(SubscriptionContext);
if (!ctx) {
throw new Error('useSubscription has to be used within <SubscriptionContext.Provider>');
}
return ctx;
};
@@ -0,0 +1 @@
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
@@ -0,0 +1,43 @@
import { createContext, ReactNode, useContext, useMemo } from 'react';
import { useRouter } from 'next/router';
import { useGetUserWorkspaces } from '@app/hooks/api';
import { Workspace } from '@app/hooks/api/workspace/types';
type TWorkspaceContext = {
workspaces: Workspace[];
currentWorkspace?: Workspace;
isLoading: boolean;
};
const WorkspaceContext = createContext<TWorkspaceContext | null>(null);
type Props = {
children: ReactNode;
};
export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
const { data: ws, isLoading } = useGetUserWorkspaces();
const router = useRouter();
const workspaceId = router.query.id;
// memorize the workspace details for the context
const value = useMemo<TWorkspaceContext>(() => {
return {
workspaces: ws || [],
currentWorkspace: (ws || []).find(({ _id: id }) => id === workspaceId),
isLoading
};
}, [ws, workspaceId, isLoading]);
return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>;
};
export const useWorkspace = () => {
const ctx = useContext(WorkspaceContext);
if (!ctx) {
throw new Error('useWorkspace has to be used within <WorkspaceContext.Provider>');
}
return ctx;
};
@@ -0,0 +1 @@
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
+2
View File
@@ -0,0 +1,2 @@
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
@@ -3,8 +3,9 @@ import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faX } from '@fortawesome/free-solid-svg-icons';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button';
import {
@@ -48,6 +49,7 @@ interface EncrypetedSecretVersionListProps {
secretKeyTag: string;
environment: string;
type: 'personal' | 'shared';
tags: Tag[];
}
/**
@@ -106,6 +108,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
pos,
type: encryptedSecretVersion.type,
environment: encryptedSecretVersion.environment,
tags: encryptedSecretVersion.tags,
key: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
iv: encryptedSecretVersion.secretKeyIV,
@@ -134,6 +137,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
environment: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].environment,
tags: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].tags,
value: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0]?.value,
@@ -155,7 +161,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
<div
className={`absolute border-l border-mineshaft-500 ${
isLoading ? 'bg-bunker-800' : 'bg-bunker'
} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
} fixed h-full w-[28rem] right-0 z-[70] shadow-xl flex flex-col justify-between sticky top-0`}
>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
@@ -177,7 +183,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
className="p-1"
onClick={() => toggleSidebar(false)}
>
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
</div>
</div>
<div className="flex flex-col px-2 py-2 overflow-y-auto h-[92vh]">
@@ -89,7 +89,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
/>
</div>
) : (
<div className="h-48 overflow-y-auto overflow-x-none">
<div className="h-48 overflow-y-auto overflow-x-none dark:[color-scheme:dark]">
{secretVersions ? (
secretVersions
?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
+6 -1
View File
@@ -1 +1,6 @@
export { useGetAuthToken } from './auth';
export * from './auth';
export * from './keys';
export * from './serviceTokens';
export * from './subscriptions';
export * from './tags';
export * from './workspace';
@@ -0,0 +1,3 @@
export {
useGetIntegrationAuthApps,
useGetIntegrationAuthById} from './queries';
@@ -0,0 +1,38 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
App,
IntegrationAuth} from './types';
const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuth'] as const,
getIntegrationAuthApps: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthApps'] as const,
}
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(`/api/v1/integration-auth/${integrationAuthId}`);
return data.integrationAuth;
}
const fetchIntegrationAuthApps = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ apps: App[] }>(`/api/v1/integration-auth/${integrationAuthId}/apps`);
return data.apps;
}
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
enabled: true
});
}
export const useGetIntegrationAuthApps = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId),
queryFn: () => fetchIntegrationAuthApps(integrationAuthId),
enabled: true
});
}
@@ -0,0 +1,13 @@
export type IntegrationAuth = {
_id: string;
workspace: string;
integration: string;
teamId?: string;
accountId?: string;
}
export type App = {
name: string;
appId?: string;
owner?: string;
}
+1
View File
@@ -0,0 +1 @@
export { useGetUserWsKey } from './queries';
+24
View File
@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { UserWsKeyPair } from './types';
const encKeyKeys = {
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
};
const fetchUserWsKey = async (workspaceID: string) => {
const { data } = await apiRequest.get<{ latestKey: UserWsKeyPair }>(
`/api/v1/key/${workspaceID}/latest`
);
return data.latestKey;
};
export const useGetUserWsKey = (workspaceID: string) =>
useQuery({
queryKey: encKeyKeys.getUserWorkspaceKey(workspaceID),
queryFn: () => fetchUserWsKey(workspaceID),
enabled: Boolean(workspaceID)
});
+22
View File
@@ -0,0 +1,22 @@
export type UserWsKeyPair = {
_id: string;
encryptedKey: string;
nonce: string;
sender: Sender;
receiver: string;
workspace: string;
createdAt: string;
updatedAt: string;
__v: number;
};
export type Sender = {
_id: string;
email: string;
createdAt: string;
updatedAt: string;
__v: number;
firstName: string;
lastName: string;
publicKey: string;
};
@@ -0,0 +1 @@
export { useCreateServiceToken, useDeleteServiceToken, useGetUserWsServiceTokens } from './queries';
@@ -0,0 +1,60 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import {
CreateServiceTokenDTO,
CreateServiceTokenRes,
DeleteServiceTokenRes,
ServiceToken
} from './types';
const serviceTokenKeys = {
getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, 'service-tokens'] as const
};
const fetchWorkspaceServiceTokens = async (workspaceID: string) => {
const { data } = await apiRequest.get<{ serviceTokenData: ServiceToken[] }>(
`/api/v2/workspace/${workspaceID}/service-token-data`
);
return data.serviceTokenData;
};
type UseGetWorkspaceServiceTokensProps = { workspaceID: string };
export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServiceTokensProps) =>
useQuery({
queryKey: serviceTokenKeys.getAllWorkspaceServiceToken(workspaceID),
queryFn: () => fetchWorkspaceServiceTokens(workspaceID),
enabled: Boolean(workspaceID)
});
// mutation
export const useCreateServiceToken = () => {
const queryClient = useQueryClient();
return useMutation<CreateServiceTokenRes, {}, CreateServiceTokenDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.post('/api/v2/service-token/', body);
return data;
},
onSuccess: ({ serviceTokenData: { workspace } }) => {
queryClient.invalidateQueries(serviceTokenKeys.getAllWorkspaceServiceToken(workspace));
}
});
};
export const useDeleteServiceToken = () => {
const queryClient = useQueryClient();
return useMutation<DeleteServiceTokenRes, {}, string>({
mutationFn: async (serviceTokenId) => {
const { data } = await apiRequest.delete(`/api/v2/service-token/${serviceTokenId}`);
return data;
},
onSuccess: ({ serviceTokenData: { workspace } }) => {
queryClient.invalidateQueries(serviceTokenKeys.getAllWorkspaceServiceToken(workspace));
}
});
};
@@ -0,0 +1,28 @@
export type ServiceToken = {
_id: string;
name: string;
workspace: string;
environment: string;
user: string;
expiresAt: string;
createdAt: string;
updatedAt: string;
__v: number;
};
export type CreateServiceTokenDTO = {
name: string;
workspaceId: string;
environment: string;
expiresIn: number;
encryptedKey: string;
iv: string;
tag: string;
};
export type CreateServiceTokenRes = {
serviceToken: string;
serviceTokenData: ServiceToken;
};
export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken };
@@ -0,0 +1 @@
export { useGetOrgSubscription } from './queries';
@@ -0,0 +1,30 @@
import { useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { GetSubscriptionPlan } from './types';
// import { Workspace } from './types';
const subscriptionKeys = {
getOrgSubsription: (orgID: string) => ['subscription', { orgID }] as const
};
const fetchOrgSubscription = async (orgID: string) => {
const { data } = await apiRequest.get<{ subscriptions: GetSubscriptionPlan }>(
`/api/v1/organization/${orgID}/subscriptions`
);
return data.subscriptions;
};
type UseGetOrgSubscriptionProps = {
orgID: string;
};
export const useGetOrgSubscription = ({ orgID }: UseGetOrgSubscriptionProps) =>
useQuery({
queryKey: subscriptionKeys.getOrgSubsription(orgID),
queryFn: () => fetchOrgSubscription(orgID),
enabled: Boolean(orgID)
});
@@ -0,0 +1,25 @@
export type GetSubscriptionPlan = {
data: { plan: SubscriptionPlan }[];
};
export type SubscriptionPlan = {
id: string;
object: string;
active: boolean;
aggregate_usage: unknown;
amount: 1400;
amount_decimal: 1400;
billing_scheme: string;
created: 1674833546;
currency: string;
interval: string;
interval_count: 1;
livemode: false;
metadata: {};
nickname: null;
product: string;
tiers_mode: unknown;
transform_usage: unknown;
trial_period_days: unknown;
usage_type: string;
};
+3
View File
@@ -0,0 +1,3 @@
export { useGetWsTags } from './queries';
export { useCreateWsTag } from './queries';
export { useDeleteWsTag } from './queries';
+62
View File
@@ -0,0 +1,62 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import {
CreateTagDTO,
CreateTagRes,
DeleteTagDTO,
DeleteWsTagRes,
UserWsTags
} from './types';
const workspaceTags = {
getWsTags: (workspaceID: string) => ['workspace-tags', { workspaceID }] as const
};
const fetchWsTag = async (workspaceID: string) => {
const { data } = await apiRequest.get<{ workspaceTags: UserWsTags }>(
`/api/v2/workspace/${workspaceID}/tags`
);
return data.workspaceTags;
};
export const useGetWsTags = (workspaceID: string) =>
useQuery({
queryKey: workspaceTags.getWsTags(workspaceID),
queryFn: () => fetchWsTag(workspaceID),
enabled: Boolean(workspaceID)
});
export const useCreateWsTag = () => {
const queryClient = useQueryClient();
return useMutation<CreateTagRes, {}, CreateTagDTO>({
mutationFn: async ({ workspaceID, tagName, tagSlug }) => {
const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, {
name: tagName,
slug: tagSlug
})
return data;
},
onSuccess: (tagData) => {
queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
}
});
};
export const useDeleteWsTag = () => {
const queryClient = useQueryClient();
return useMutation<DeleteWsTagRes, {}, DeleteTagDTO>({
mutationFn: async ({ tagID }) => {
const { data } = await apiRequest.delete(`/api/v2/workspace/tags/${tagID}`);
return data
},
onSuccess: (tagData) => {
queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
}
});
};
+39
View File
@@ -0,0 +1,39 @@
export type UserWsTags = WsTag[];
export type WsTag = {
_id: string;
name: string;
slug: string;
workspace: string;
createdAt: string;
updatedAt: string;
__v: number;
}
export type WorkspaceTag = { _id: string; name: string; slug: string };
export type CreateTagDTO = {
workspaceID: string;
tagSlug: string;
tagName: string;
};
export type CreateTagRes = {
name: string;
slug: string;
workspace: string;
createdAt: string;
user: string;
_id: string;
};
export type DeleteTagDTO = { tagID: string; };
export type DeleteWsTagRes = {
name: string;
slug: string;
workspace: string;
createdAt: string;
user: string;
_id: string;
};
+14
View File
@@ -0,0 +1,14 @@
export type { GetAuthTokenAPI } from './auth/types';
export type { UserWsKeyPair } from './keys/types';
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
export type {
CreateEnvironmentDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
UpdateEnvironmentDTO,
Workspace,
WorkspaceEnv,
WorkspaceTag
} from './workspace/types';
@@ -0,0 +1,10 @@
export {
useCreateWsEnvironment,
useDeleteWorkspace,
useDeleteWsEnvironment,
useGetUserWorkspaces,
useGetWorkspaceById,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment} from './queries';
@@ -0,0 +1,121 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import {
CreateEnvironmentDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
ToggleAutoCapitalizationDTO,
UpdateEnvironmentDTO,
Workspace
} from './types';
const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
getAllUserWorkspace: ['workspaces'] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ workspace: Workspace }>(`/api/v1/workspace/${workspaceId}`);
return data.workspace;
}
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
return data.workspaces;
};
export const useGetWorkspaceById = (workspaceId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceById(workspaceId),
queryFn: () => fetchWorkspaceById(workspaceId),
enabled: true
});
};
export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
// mutation
export const useRenameWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, RenameWorkspaceDTO>({
mutationFn: ({ workspaceID, newWorkspaceName }) =>
apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName }),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
export const useToggleAutoCapitalization = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
mutationFn: ({ workspaceID, state }) =>
apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, { autoCapitalization: state }),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
export const useDeleteWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteWorkspaceDTO>({
mutationFn: ({ workspaceID }) => apiRequest.delete(`/api/v1/workspace/${workspaceID}`),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
export const useCreateWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, CreateEnvironmentDTO>({
mutationFn: ({ workspaceID, environmentName, environmentSlug }) =>
apiRequest.post(`/api/v2/workspace/${workspaceID}/environments`, {
environmentName,
environmentSlug
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
export const useUpdateWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateEnvironmentDTO>({
mutationFn: ({ workspaceID, environmentName, environmentSlug, oldEnvironmentSlug }) =>
apiRequest.put(`/api/v2/workspace/${workspaceID}/environments`, {
environmentName,
environmentSlug,
oldEnvironmentSlug
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
export const useDeleteWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteEnvironmentDTO>({
mutationFn: ({ workspaceID, environmentSlug }) =>
apiRequest.delete(`/api/v2/workspace/${workspaceID}/environments`, {
data: { environmentSlug }
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
});
};
+32
View File
@@ -0,0 +1,32 @@
export type Workspace = {
__v: number;
_id: string;
name: string;
organization: string;
autoCapitalization: boolean;
environments: WorkspaceEnv[];
};
export type WorkspaceEnv = { name: string; slug: string };
export type WorkspaceTag = { _id: string; name: string; slug: string };
// mutation dto
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
export type DeleteWorkspaceDTO = { workspaceID: string };
export type CreateEnvironmentDTO = {
workspaceID: string;
environmentSlug: string;
environmentName: string;
};
export type UpdateEnvironmentDTO = {
workspaceID: string;
oldEnvironmentSlug: string;
environmentSlug: string;
environmentName: string;
};
export type DeleteEnvironmentDTO = { workspaceID: string; environmentSlug: string };
+3 -3
View File
@@ -21,7 +21,7 @@ interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> {
popUp: UsePopUpState<T>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void;
handlePopUpClose: (popUpName: keyof UsePopUpState<T>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<T>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<T>, state?: boolean) => void;
}
/**
@@ -50,10 +50,10 @@ export const usePopUp = <T extends Readonly<string[]> | UsePopUpProps[]>(
setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: false } }));
}, []);
const handlePopUpToggle = useCallback((popUpName: keyof UsePopUpState<T>) => {
const handlePopUpToggle = useCallback((popUpName: keyof UsePopUpState<T>, state?: boolean) => {
setPopUp((oldState) => ({
...oldState,
[popUpName]: { isOpen: !oldState[popUpName].isOpen }
[popUpName]: { isOpen: typeof state === 'undefined' ? !oldState[popUpName].isOpen : state }
}));
}, []);
+11 -5
View File
@@ -10,7 +10,9 @@ import Layout from '@app/components/basic/Layout';
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
import { publicPaths } from '@app/const';
import { SubscriptionProvider } from '@app/context';
import { AuthProvider } from '@app/context/AuthContext';
import { WorkspaceProvider } from '@app/context/WorkspaceContext';
import { queryClient } from '@app/reactQuery';
import '@fortawesome/fontawesome-svg-core/styles.css';
@@ -68,11 +70,15 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<NotificationProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</NotificationProvider>
<WorkspaceProvider>
<SubscriptionProvider>
<NotificationProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</NotificationProvider>
</SubscriptionProvider>
</WorkspaceProvider>
</AuthProvider>
</QueryClientProvider>
);
+1 -1
View File
@@ -124,7 +124,7 @@ export default function Activity() {
};
return (
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
<div className="mx-6 lg:mx-0 w-full h-screen">
<NavHeader pageName="Project Activity" isProjectRelated />
{currentSidebarAction && (
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
@@ -26,7 +26,7 @@ const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) =>
})
}).then(async (res) => {
if (res && res.status === 200) {
return res;
return (await res.json()).integrationAuth;
}
console.log('Failed to authorize the integration');
return undefined;
@@ -1,7 +1,13 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
integrationAuthId: string;
integrationAuthId: string;
isActive: boolean;
app: string | null;
appId: string | null;
sourceEnvironment: string;
targetEnvironment: string | null;
owner: string | null;
}
/**
* This route creates a new integration based on the integration authorization with id [integrationAuthId]
@@ -10,7 +16,13 @@ interface Props {
* @returns
*/
const createIntegration = ({
integrationAuthId
integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner
}: Props) =>
SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST',
@@ -18,7 +30,13 @@ const createIntegration = ({
'Content-Type': 'application/json'
},
body: JSON.stringify({
integrationAuthId
integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner
})
}).then(async (res) => {
if (res && res.status === 200) {
@@ -0,0 +1,22 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
/**
* This route lets us get the tags for a certain project
* @param {string} workspaceId
* @returns
*/
const getWorkspaceTags = ({ workspaceId }: { workspaceId: string }) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/tags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (res) => {
if (res?.status === 200) {
return (await res.json()).workspaceTags;
}
console.log('Failed to get the tags available in a certain project');
return undefined;
});
export default getWorkspaceTags;
@@ -4,6 +4,7 @@ interface Workspace {
__v: number;
_id: string;
name: string;
autoCapitalization: boolean;
organization: string;
environments: Array<{ name: string; slug: string }>;
}
+235 -87
View File
@@ -5,23 +5,26 @@ import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import {
faArrowDownAZ,
faArrowDownZA,
faArrowDown,
faArrowLeft,
faArrowUp,
faCheck,
faClockRotateLeft,
faEye,
faEyeSlash,
faFolderOpen,
faMagnifyingGlass,
faPlus
faPlus,
faXmark
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox';
import BottonRightPopup from '@app/components/basic/popups/BottomRightPopup';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import ConfirmEnvOverwriteModal from '@app/components/dashboard/ConfirmEnvOverwriteModal';
import DownloadSecretMenu from '@app/components/dashboard/DownloadSecretsMenu';
import DropZone from '@app/components/dashboard/DropZone';
import KeyPair from '@app/components/dashboard/KeyPair';
@@ -31,6 +34,7 @@ import guidGenerator from '@app/components/utilities/randomId';
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import { IconButton } from '@app/components/v2';
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
@@ -43,6 +47,7 @@ import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
import getWorkspaces from '../api/workspace/getWorkspaces';
import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
type WorkspaceEnv = {
name: string;
@@ -58,6 +63,7 @@ interface SecretDataProps {
id: string;
idOverride: string | undefined;
comment: string;
tags: Tag[];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -81,6 +87,7 @@ interface SnapshotProps {
value: string;
valueOverride: string;
comment: string;
tags: Tag[];
}[];
}
@@ -113,7 +120,7 @@ export default function Dashboard() {
const [blurred, setBlurred] = useState(true);
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
const [isNew, setIsNew] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [searchKeys, setSearchKeys] = useState('');
const [errorDragAndDrop, setErrorDragAndDrop] = useState(false);
const [sortMethod, setSortMethod] = useState('alphabetical');
@@ -125,6 +132,9 @@ export default function Dashboard() {
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
const [numSnapshots, setNumSnapshots] = useState<number>();
const [saveLoading, setSaveLoading] = useState(false);
const [autoCapitalization, setAutoCapitalization] = useState(false);
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -133,11 +143,7 @@ export default function Dashboard() {
const [workspaceEnvs, setWorkspaceEnvs] = useState<WorkspaceEnv[]>([]);
const [selectedSnapshotEnv, setSelectedSnapshotEnv] = useState<WorkspaceEnv>();
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
name: '',
slug: '',
isWriteDenied: false
});
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>();
const [atSecretAreaTop, setAtSecretsAreaTop] = useState(true);
const secretsTop = useRef<HTMLDivElement>(null);
@@ -193,15 +199,18 @@ export default function Dashboard() {
}));
setData(sortedData);
setIsLoading(false);
};
/**
* Reorder rows alphabetically or in the opprosite order
*/
const reorderRows = (dataToReorder: SecretDataProps[] | 1) => {
setSortMethod((prevSort) => (prevSort === 'alphabetical' ? '-alphabetical' : 'alphabetical'));
if (dataToReorder) {
setSortMethod((prevSort) => (prevSort === 'alphabetical' ? '-alphabetical' : 'alphabetical'));
sortValuesHandler(dataToReorder, undefined);
sortValuesHandler(dataToReorder, undefined);
}
};
useEffect(() => {
@@ -216,6 +225,7 @@ export default function Dashboard() {
if (!workspace) {
router.push(`/dashboard/${userWorkspaces?.[0]?._id}`);
}
setAutoCapitalization(workspace?.autoCapitalization ?? true);
const accessibleEnvironments = await getWorkspaceEnvironments({ workspaceId });
setWorkspaceEnvs(accessibleEnvironments || []);
@@ -233,6 +243,8 @@ export default function Dashboard() {
action: 'first_time_secrets_pushed'
});
setHasUserEverPushed(!!userAction);
setProjectTags(await getWorkspaceTags({ workspaceId }));
} catch (error) {
console.log('Error', error);
setData(undefined);
@@ -246,16 +258,17 @@ export default function Dashboard() {
setIsLoading(true);
setBlurred(true);
// ENV
const dataToSort = await getSecretsForProject({
env: selectedEnv.slug,
setIsKeyAvailable,
setData,
workspaceId
});
setInitialData(dataToSort);
reorderRows(dataToSort);
setTimeout(() => setIsLoading(false), 700);
let dataToSort;
if (selectedEnv) {
dataToSort = await getSecretsForProject({
env: selectedEnv.slug,
setIsKeyAvailable,
setData,
workspaceId
});
setInitialData(dataToSort);
reorderRows(dataToSort);
}
} catch (error) {
console.log('Error', error);
setData(undefined);
@@ -274,7 +287,8 @@ export default function Dashboard() {
key: '',
value: '',
valueOverride: undefined,
comment: ''
comment: '',
tags: []
},
...data!
]);
@@ -283,6 +297,23 @@ export default function Dashboard() {
}
};
const addRowToBottom = () => {
setIsNew(false);
setData([
...data!,
{
id: guidGenerator(),
idOverride: guidGenerator(),
pos: data!.length,
key: '',
value: '',
valueOverride: undefined,
comment: '',
tags: []
},
]);
};
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
setButtonReady(true);
toggleSidebar('None');
@@ -316,6 +347,11 @@ export default function Dashboard() {
setButtonReady(true);
};
const modifyTags = (tags: Tag[], pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
setButtonReady(true);
};
// For speed purposes and better perforamance, we are using useCallback
const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos);
@@ -333,6 +369,10 @@ export default function Dashboard() {
modifyComment(value, pos);
}, []);
const listenChangeTags = useCallback((value: Tag[], pos: number) => {
modifyTags(value, pos);
}, []);
/**
* Save the changes of environment variables and push them to the database
*/
@@ -370,7 +410,7 @@ export default function Dashboard() {
});
}
if (selectedEnv.isWriteDenied) {
if (selectedEnv?.isWriteDenied) {
setSaveLoading(false);
return createNotification({
text: 'You are not allowed to edit this environment',
@@ -405,7 +445,9 @@ export default function Dashboard() {
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment)
initDataPoint.comment) ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !==
initDataPoint?.tags
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
@@ -439,7 +481,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride)
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override add', overridesToBeAdded.length);
@@ -454,7 +497,8 @@ export default function Dashboard() {
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment)
.comment !== initDataPoint.comment ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !== initDataPoint?.tags)
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
@@ -466,14 +510,15 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride)
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override update', overridesToBeUpdated.length);
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
}
if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded),
workspaceId,
@@ -481,7 +526,7 @@ export default function Dashboard() {
});
if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
}
if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
if (selectedEnv && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
workspaceId,
@@ -504,11 +549,33 @@ export default function Dashboard() {
return undefined;
};
const addData = (newData: SecretDataProps[]) => {
setData(data!.concat(newData));
const addDataWithMerge = (newData: SecretDataProps[], preserve?: 'old' | 'new') => {
setData((oldData) => {
let filteredOldData = oldData!;
let filteredNewData = newData;
if (preserve === 'new')
filteredOldData = oldData!.filter(
(oldDataPoint) => !newData.find((newDataPoint) => newDataPoint.key === oldDataPoint.key)
);
if (preserve === 'old')
filteredNewData = newData.filter(
(newDataPoint) => !oldData?.find((oldDataPoint) => oldDataPoint.key === newDataPoint.key)
);
return filteredOldData.concat(filteredNewData);
});
setButtonReady(true);
};
const addData = (newData: SecretDataProps[]) => {
if (
newData.some((newDataPoint) => data?.find((dataPoint) => dataPoint.key === newDataPoint.key)) // if newData contains duplicates
) {
setDropZoneData(newData);
return;
}
addDataWithMerge(newData);
};
const changeBlurred = () => {
setBlurred(!blurred);
};
@@ -518,7 +585,7 @@ export default function Dashboard() {
};
return data ? (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
<Head>
<title>{t('common:head-title', { title: t('dashboard:title') })}</title>
<link rel="icon" href="/infisical.ico" />
@@ -526,33 +593,23 @@ export default function Dashboard() {
<meta property="og:title" content={String(t('dashboard:og-title'))} />
<meta name="og:description" content={String(t('dashboard:og-description'))} />
</Head>
<div className="flex flex-row">
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
<div className="w-full max-h-96 pb-2">
<div className="flex flex-row h-full">
<ConfirmEnvOverwriteModal
isOpen={!!dropZoneData}
onClose={() => setDropZoneData(undefined)}
onOverwriteConfirm={(preserve) => {
addDataWithMerge(dropZoneData!, preserve);
setDropZoneData(undefined);
}}
duplicateKeys={
dropZoneData
?.filter((newDataPoint) =>
data?.find((dataPoint) => dataPoint.key === newDataPoint.key)
)
.map((duplicate) => duplicate.key) ?? []
}
/>
<div className="w-full max-h-96 pb-2 dark:[color-scheme:dark]">
<NavHeader pageName={t('dashboard:title')} isProjectRelated />
{checkDocsPopUpVisible && (
<BottonRightPopup
@@ -565,7 +622,7 @@ export default function Dashboard() {
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
/>
)}
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl">
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl">
{snapshotData && (
<div className="flex justify-start max-w-sm mt-1 mr-2">
<Button
@@ -586,7 +643,7 @@ export default function Dashboard() {
</span>
)}
</div>
{!snapshotData && data?.length === 0 && (
{!snapshotData && data?.length === 0 && selectedEnv && (
<ListBox
isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
@@ -606,7 +663,10 @@ export default function Dashboard() {
<div className="flex justify-start max-w-sm mt-1 mr-2">
<Button
text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => togglePITSidebar(true)}
onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true)
}}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
@@ -626,7 +686,7 @@ export default function Dashboard() {
/>
</div>
)}
{snapshotData && (
{snapshotData && selectedEnv && (
<div className="flex justify-start max-w-sm mt-1">
<Button
text={String(t('Rollback to this snapshot'))}
@@ -641,7 +701,8 @@ export default function Dashboard() {
valueOverride: sv.valueOverride,
key: sv.key,
value: sv.value,
comment: ''
comment: '',
tags: sv.tags
}));
setData(rolledBackSecrets);
@@ -653,6 +714,7 @@ export default function Dashboard() {
text: `Rollback has been performed successfully.`,
type: 'success'
});
setButtonReady(false);
}}
color="primary"
size="md"
@@ -663,9 +725,9 @@ export default function Dashboard() {
</div>
</div>
<div className="mx-6 w-full pr-12">
<div className="flex flex-col max-w-5xl pb-1">
<div className="flex flex-col pb-1">
<div className="w-full flex flex-row items-start">
{(snapshotData || data?.length !== 0) && (
{(snapshotData || data?.length !== 0) && selectedEnv && (
<>
{!snapshotData ? (
<ListBox
@@ -696,28 +758,18 @@ export default function Dashboard() {
}
/>
)}
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
<div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
<FontAwesomeIcon
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
className="bg-transparent rounded-l-md py-[0.7rem] pl-4 pr-2 text-bunker-300 text-sm"
icon={faMagnifyingGlass}
/>
<input
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
className="pl-2 text-bunker-300 rounded-r-md bg-transparent w-full h-full outline-none text-sm placeholder:hover:text-bunker-200 placeholder:focus:text-transparent"
value={searchKeys}
onChange={(e) => setSearchKeys(e.target.value)}
placeholder={String(t('dashboard:search-keys'))}
/>
</div>
{!snapshotData && (
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
<Button
onButtonPressed={() => reorderRows(1)}
color="mineshaft"
size="icon-md"
icon={sortMethod === 'alphabetical' ? faArrowDownAZ : faArrowDownZA}
/>
</div>
)}
{!snapshotData && (
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
<DownloadSecretMenu data={data} env={selectedEnv.slug} />
@@ -762,31 +814,85 @@ export default function Dashboard() {
/>
</div>
) : data?.length !== 0 ? (
<div className="flex flex-col w-full mt-1 mb-2">
<div className="flex flex-col w-full mt-1">
<div
onScroll={onSecretsAreaScroll}
className="max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar"
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
>
<div ref={secretsTop} />
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
<div
className='group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]'
>
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
<div className='text-transparent text-xs flex items-center justify-center w-14 h-10 cursor-default'>0</div>
<span className='px-2 text-bunker-300 font-semibold'>Key</span>
{!snapshotData && <IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => reorderRows(1)}
>
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />}
</IconButton>}
</div>
<div className="w-5/12 border-r border-mineshaft-600">
<div
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
>
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
</div>
</div>
<div className="w-2/12 border-r border-mineshaft-600">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
</div>
</div>
<div className="w-2/12">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Tags</div>
</div>
</div>
<div
className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center"
/>
<div className='w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
<div
onKeyDown={() => null}
role="none"
onClick={() => {}}
className="invisible group-hover:visible"
>
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible" icon={faXmark} />
</div>
</div>
</div>
</div>
<div className="bg-mineshaft-800 rounded-b-md border-bunker-600">
{!snapshotData &&
data
?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()) || row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter((row) => !sharedToHide.includes(row.id))
.map((keyPair) => (
<KeyPair
key={keyPair.id}
isCapitalized={autoCapitalization}
key={keyPair.id ? keyPair.id : keyPair.idOverride}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key
)}
toggleSidebar={toggleSidebar}
togglePITSidebar={togglePITSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
deleteRow={deleteCertainRow}
tags={projectTags}
/>
))}
{snapshotData &&
@@ -812,11 +918,14 @@ export default function Dashboard() {
)
.map((keyPair) => (
<KeyPair
isCapitalized={autoCapitalization}
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key
@@ -824,11 +933,23 @@ export default function Dashboard() {
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot
tags={projectTags}
/>
))}
<div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'>
<div className='w-10'/>
<button
type="button"
className='text-bunker-300 relative font-normal h-10 flex items-center w-full cursor-pointer'
onClick={addRowToBottom}
>
<FontAwesomeIcon icon={faPlus} className='mr-3'/>
<span className='text-sm'>Add Secret</span>
</button>
</div>
</div>
{!snapshotData && (
<div className="w-full max-w-5xl px-2 pt-3">
<div className="w-full px-2 pt-3">
<DropZone
setData={addData}
setErrorDragAndDrop={setErrorDragAndDrop}
@@ -843,7 +964,7 @@ export default function Dashboard() {
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 mt-28">
{isKeyAvailable && !snapshotData && (
<DropZone
setData={setData}
@@ -866,11 +987,38 @@ export default function Dashboard() {
)}
</div>
</div>
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
</div>
</div>
) : (
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full" />
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
</div>
);
-41
View File
@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Github() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'github',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Github integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Github.requireAuth = true;
-41
View File
@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Heroku() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'heroku',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Heroku integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Heroku.requireAuth = true;
+78 -98
View File
@@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next';
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
import IntegrationSection from '@app/components/integrations/IntegrationSection';
@@ -20,12 +19,10 @@ import {
} from '../../components/utilities/cryptography/crypto';
import getBot from '../api/bot/getBot';
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
import createIntegration from '../api/integrations/createIntegration';
import deleteIntegration from '../api/integrations/DeleteIntegration';
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
import getAWorkspace from '../api/workspace/getAWorkspace';
import getLatestFileKey from '../api/workspace/getLatestFileKey';
@@ -52,6 +49,7 @@ interface Integration {
}
interface IntegrationOption {
tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
@@ -75,7 +73,6 @@ export default function Integrations() {
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState<any>(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null);
const router = useRouter();
@@ -166,80 +163,83 @@ export default function Integrations() {
}
};
/**
* Handle integration option authorization for a given integration option [integrationOption]
* @param {Object} obj
* @param {Object} obj.integrationOption - an integration option
* @param {String} obj.name
* @param {String} obj.type
* @param {String} obj.docsLink
* @returns
*/
const handleIntegrationOption = async ({
integrationOption,
accessToken
}: {
integrationOption: IntegrationOption,
accessToken?: string;
}) => {
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
if (!bot.isActive) {
await handleBotActivate();
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
let link = '';
switch (integrationOption.slug) {
case 'azure-key-vault':
link = `https://login.microsoftonline.com/${integrationOption.tenantId}/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case 'heroku':
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case 'vercel':
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case 'netlify':
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case 'github':
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/authorize`
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize`
break;
default:
break;
}
if (integrationOption.type === 'oauth') {
// integration is of type OAuth
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
switch (integrationOption.slug) {
case 'heroku':
window.location.assign(
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
);
break;
case 'vercel':
window.location.assign(
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
);
break;
case 'netlify':
window.location.assign(
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
);
break;
case 'github':
window.location.assign(
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
);
break;
default:
break;
}
return;
} if (integrationOption.type === 'pat') {
// integration is of type personal access token
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: integrationOption.slug,
accessToken: accessToken ?? ''
});
setIntegrationAuths([...integrationAuths, integrationAuth])
const integration = await createIntegration({
integrationAuthId: integrationAuth._id
});
setIntegrations([...integrations, integration]);
return;
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
}
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = '';
switch (integrationAuth.integration) {
case 'azure-key-vault':
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'heroku':
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'vercel':
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'netlify':
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'github':
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
}
/**
* Open dialog to activate bot if bot is not active.
@@ -251,34 +251,20 @@ export default function Integrations() {
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
// consider: don't start integration until at [handleIntegrationOption] step
try {
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
if (!integrationAuthX) {
// case: integration has not been authorized before
if (integrationOption.type === 'pat') {
// case: integration requires user to input their personal access token for that integration
setIntegrationAccessTokenDialogOpen(true);
return;
}
// case: integration does not require user to input their personal access token (i.e. it's an OAuth2 integration)
handleIntegrationOption({ integrationOption });
return;
}
if (!bot.isActive) {
await handleBotActivate();
}
// case: integration has been authorized before
// -> create new integration
const integration = await createIntegration({
integrationAuthId: integrationAuthX._id
});
setIntegrations([...integrations, integration]);
if (!integrationAuthX) {
// case: integration has not been authorized
handleUnauthorizedIntegrationOptionPress(integrationOption);
return;
}
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
@@ -360,12 +346,6 @@ export default function Integrations() {
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationAccessTokenDialog
isOpen={isIntegrationAccessTokenDialogOpen}
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
handleIntegrationOption={handleIntegrationOption}
/>
<IntegrationSection
integrations={integrations}
setIntegrations={setIntegrations}
@@ -0,0 +1,124 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function AzureKeyVaultCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [vaultBaseUrl, setVaultBaseUrl] = useState('');
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
const handleButtonClick = async () => {
try {
if (vaultBaseUrl.length === 0) {
setVaultBaseUrlErrorText('Vault URI cannot be blank');
return;
}
if (
!vaultBaseUrl.startsWith('https://')
|| !vaultBaseUrl.endsWith('vault.azure.net')
) {
setVaultBaseUrlErrorText('Vault URI must be like https://<vault_name>.vault.azure.net');
return;
}
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: vaultBaseUrl,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Azure Key Vault Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vault URI"
errorText={vaultBaseUrlErrorText}
isError={vaultBaseUrlErrorText !== '' ?? false}
>
<Input
placeholder='https://example.vault.azure.net'
value={vaultBaseUrl}
onChange={(e) => setVaultBaseUrl(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
AzureKeyVaultCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function AzureKeyVaultOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'azure-key-vault'
});
router.push(
`/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
AzureKeyVaultOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,76 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
} from '../../../components/v2';
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function FlyioCreateIntegrationPage() {
const router = useRouter();
const [accessToken, setAccessToken] = useState('');
const [accessTokenErrorText, setAccessTokenErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setAccessTokenErrorText('');
if (accessToken.length === 0) {
setAccessTokenErrorText('Access token cannot be blank');
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'flyio',
accessToken
});
setIsLoading(false);
router.push(
`/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
}
return (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Render Integration</CardTitle>
<FormControl
label="Fly.io Access Token"
errorText={accessTokenErrorText}
isError={accessTokenErrorText !== '' ?? false}
>
<Input
placeholder=''
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to Fly.io
</Button>
</Card>
</div>
)
}
FlyioCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,122 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function FlyioCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Fly.io Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`flyio-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Fly.io App"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
FlyioCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,123 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function GitHubCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [owner, setOwner] = useState<string | null>(null);
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
setOwner(integrationAuthApps[0]?.owner ?? null);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>GitHub Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="GitHub Repo"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`github-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
GitHubCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function GitHubOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'github'
});
router.push(
`/integrations/github/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
GitHubOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,121 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function HerokuCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Heroku Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Heroku App"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
HerokuCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function HerokuOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'heroku'
});
router.push(
`/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
HerokuOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,143 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
const netlifyEnvironments = [
{ name: 'Local development', slug: 'dev' },
{ name: 'Branch deploys', slug: 'branch-deploy' },
{ name: 'Deploy previews', slug: 'deploy-preview' },
{ name: 'Production', slug: 'production' }
]
export default function NetlifyCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [targetEnvironment, setTargetEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setTargetEnvironment(netlifyEnvironments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Netlify Integration</CardTitle>
<FormControl
label="Project Environment"
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Netlify Site"
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Netlify Context"
>
<Select
value={targetEnvironment}
onValueChange={(val) => setTargetEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{netlifyEnvironments.map((netlifyEnvironment) => (
<SelectItem value={netlifyEnvironment.slug} key={`netlify-environment-${netlifyEnvironment.slug}`}>
{netlifyEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
NetlifyCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function NetlifyOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'netlify'
});
router.push(
`/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
NetlifyOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,76 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Input,
} from '../../../components/v2';
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function RenderCreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState('');
const [apiKeyErrorText, setApiKeyErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText('');
if (apiKey.length === 0) {
setApiKeyErrorText('API Key cannot be blank');
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'render',
accessToken: apiKey
});
setIsLoading(false);
router.push(
`/integrations/render/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
}
return (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Render Integration</CardTitle>
<FormControl
label="Render API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== '' ?? false}
>
<Input
placeholder='rnd_xxx'
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to Render
</Button>
</Card>
</div>
)
}
RenderCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,122 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function RenderCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Render Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Render Service"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
RenderCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,142 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
const vercelEnvironments = [
{ name: 'Development', slug: 'development' },
{ name: 'Preview', slug: 'preview' },
{ name: 'Production', slug: 'production' }
]
export default function VercelCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? '');
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [targetApp, setTargetApp] = useState('');
const [targetEnvironment, setTargetEnvironemnt] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
setTargetApp(integrationAuthApps[0].name);
setTargetEnvironemnt(vercelEnvironments[0].slug);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Vercel Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vercel App"
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vercel App Environment"
>
<Select
value={targetEnvironment}
onValueChange={(val) => setTargetEnvironemnt(val)}
className='w-full border border-mineshaft-500'
>
{vercelEnvironments.map((vercelEnvironment) => (
<SelectItem value={vercelEnvironment.slug} key={`vercel-environment-${vercelEnvironment.slug}`}>
{vercelEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
VercelCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function VercelOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'vercel'
});
router.push(
`/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
VercelOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
-46
View File
@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Netlify() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
// modify comment here
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'netlify',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Netlify integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Netlify.requireAuth = true;
+13 -316
View File
@@ -1,332 +1,29 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faCheck, faCopy, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import AddServiceTokenDialog from '@app/components/basic/dialog/AddServiceTokenDialog';
import InputField from '@app/components/basic/InputField';
import EnvironmentTable from '@app/components/basic/table/EnvironmentsTable';
import ServiceTokenTable from '@app/components/basic/table/ServiceTokenTable';
import NavHeader from '@app/components/navigation/NavHeader';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import deleteEnvironment from '@app/pages/api/environments/deleteEnvironment';
import updateEnvironment from '@app/pages/api/environments/updateEnvironment';
import createEnvironment from '../../api/environments/createEnvironment';
import getServiceTokens from '../../api/serviceToken/getServiceTokens';
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
import getWorkspaces from '../../api/workspace/getWorkspaces';
import renameWorkspace from '../../api/workspace/renameWorkspace';
type EnvData = {
name: string;
slug: string;
};
export default function SettingsBasic() {
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [workspaceName, setWorkspaceName] = useState('');
const [serviceTokens, setServiceTokens] = useState<any[]>([]);
const [environments, setEnvironments] = useState<Array<EnvData>>([]);
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
// const [isAddOpen, setIsAddOpen] = useState(false);
const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = useState(false);
const [projectIdCopied, setProjectIdCopied] = useState(false);
const workspaceId = router.query.id as string;
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { ProjectSettingsPage } from '@app/views/Settings/ProjectSettingsPage';
const ProjectSettings = () => {
const { t } = useTranslation();
/**
* This function copies the project id to the clipboard
*/
function copyToClipboard() {
const copyText = document.getElementById('myInput') as HTMLInputElement;
if (copyText) {
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
navigator.clipboard.writeText(copyText.value);
setProjectIdCopied(true);
setTimeout(() => setProjectIdCopied(false), 2000);
}
}
useEffect(() => {
const load = async () => {
const userWorkspaces = await getWorkspaces();
userWorkspaces.forEach((userWorkspace) => {
if (userWorkspace._id === workspaceId) {
setWorkspaceName(userWorkspace.name);
setEnvironments(userWorkspace.environments);
}
});
const tempServiceTokens = await getServiceTokens({
workspaceId
});
setServiceTokens(tempServiceTokens);
};
load();
}, []);
const modifyWorkspaceName = (newName: string) => {
setButtonReady(true);
setWorkspaceName(newName);
};
const submitChanges = (newWorkspaceName: string) => {
renameWorkspace(workspaceId, newWorkspaceName);
setButtonReady(false);
};
const closeAddServiceTokenModal = () => {
setIsAddServiceTokenDialogOpen(false);
};
/**
* This function deleted a workspace.
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
* It then deletes the workspace and forwards the user to another aviable workspace.
*/
const executeDeletingWorkspace = async () => {
const userWorkspaces = await getWorkspaces();
if (userWorkspaces.length > 1) {
if (
userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
workspaceToBeDeletedName
) {
await deleteWorkspace(workspaceId);
const ws = await getWorkspaces();
router.push(`/dashboard/${ws[0]._id}`);
}
}
};
const onCreateEnvironment = async ({ name, slug }: EnvData) => {
const res = await createEnvironment(workspaceId, {
environmentName: name,
environmentSlug: slug
});
if (res) {
// TODO: on react-query migration do an api call to resync
setEnvironments((env) => [...env, { name, slug }]);
}
};
const onUpdateEnvironment = async (oldSlug: string, { name, slug }: EnvData) => {
const res = await updateEnvironment(workspaceId, {
oldEnvironmentSlug: oldSlug,
environmentName: name,
environmentSlug: slug
});
// TODO: on react-query migration do an api call to resync
if (res) {
setEnvironments((env) => env.map((el) => (el.slug === oldSlug ? { name, slug } : el)));
}
};
const onDeleteEnvironment = async (slugToBeDelete: string) => {
const res = await deleteEnvironment(workspaceId, slugToBeDelete);
// TODO: on react-query migration do an api call to resync
if (res) {
setEnvironments((env) => env.filter(({ slug }) => slug !== slugToBeDelete));
}
};
return (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<>
<Head>
<title>{t('common:head-title', { title: t('settings-project:title') })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<AddServiceTokenDialog
isOpen={isAddServiceTokenDialogOpen}
workspaceId={workspaceId}
environments={environments}
closeModal={closeAddServiceTokenModal}
workspaceName={workspaceName}
serviceTokens={serviceTokens}
setServiceTokens={setServiceTokens}
/>
<div className="flex flex-row mr-6 max-w-5xl">
<div className="w-full max-h-screen pb-2 overflow-y-auto">
<NavHeader pageName={t('settings-project:title')} isProjectRelated />
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
<div className="flex flex-col justify-start items-start text-3xl">
<p className="font-semibold mr-4 text-gray-200">{t('settings-project:title')}</p>
<p className="font-normal mr-4 text-gray-400 text-base">
{t('settings-project:description')}
</p>
</div>
</div>
<div className="flex flex-col ml-6 text-mineshaft-50">
<div className="flex flex-col">
<div className="min-w-md mt-2 flex flex-col items-start">
<div className="bg-white/5 rounded-md px-6 pb-4 flex flex-col items-start w-full mb-6 pt-2">
<p className="text-xl font-semibold mb-4 mt-2">{t('common:display-name')}</p>
<div className="max-h-28 w-full max-w-md mr-auto">
<InputField
label=""
onChangeHandler={modifyWorkspaceName}
type="varName"
value={workspaceName}
placeholder=""
isRequired
/>
</div>
<div className="flex justify-start w-full">
<div className="flex justify-start max-w-sm mt-4 mb-2">
<Button
text={t('common:save-changes') as string}
onButtonPressed={() => submitChanges(workspaceName)}
color="mineshaft"
size="md"
active={buttonReady}
iconDisabled={faCheck}
textDisabled="Saved"
/>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start w-full mb-6 mt-4">
<p className="text-xl font-semibold self-start">{t('common:project-id')}</p>
<p className="text-base text-gray-400 font-normal self-start mt-4">
{t('settings-project:project-id-description')}
</p>
<p className="text-base text-gray-400 font-normal self-start">
{t('settings-project:project-id-description2')}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a
href="https://infisical.com/docs/getting-started/introduction"
target="_blank"
rel="noopener"
className="text-primary hover:opacity-80 duration-200"
>
{t('settings-project:docs')}
</a>
</p>
<p className="mt-4 text-xs text-bunker-300">
{t('settings-project:auto-generated')}
</p>
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400">
<p className="mr-2 font-bold pl-4">{`${t('common:project-id')}:`}</p>
<input
type="text"
value={workspaceId}
id="myInput"
className="bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none"
disabled
/>
<div className="group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200">
<button
type="button"
onClick={copyToClipboard}
className="pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
>
{projectIdCopied ? (
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm">
{t('common:click-to-copy')}
</span>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4">
<EnvironmentTable
data={environments}
onCreateEnv={onCreateEnvironment}
onUpdateEnv={onUpdateEnvironment}
onDeleteEnv={onDeleteEnvironment}
/>
</div>
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-4 mb-4 pt-2">
<div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full">
<p className="text-xl font-semibold mb-3">
{t('section-token:service-tokens')}
</p>
<p className="text-sm text-gray-400">
{t('section-token:service-tokens-description')}
</p>
<p className="text-sm text-gray-400 mb-4">
Please, make sure you are on the
<a
className="text-primary underline underline-offset-2 ml-1"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noreferrer"
>
latest version of CLI
</a>
.
</p>
</div>
<div className="w-48 mt-2">
<Button
text={t('section-token:add-new') as string}
onButtonPressed={() => {
setIsAddServiceTokenDialogOpen(true);
}}
color="mineshaft"
icon={faPlus}
size="md"
/>
</div>
</div>
<ServiceTokenTable
data={serviceTokens}
workspaceName={workspaceName}
setServiceTokens={setServiceTokens as any}
/>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 border-l border-red pl-6 flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
<p className="mt-2 text-md text-gray-400">{t('settings-project:danger-zone-note')}</p>
<div className="max-h-28 w-full max-w-md mr-auto mt-4">
<InputField
label={t('settings-project:project-to-delete') as string}
onChangeHandler={setWorkspaceToBeDeletedName}
type="varName"
value={workspaceToBeDeletedName}
placeholder=""
isRequired
/>
</div>
<button
type="button"
className="max-w-md mt-6 w-full inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-semibold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={executeDeletingWorkspace}
>
{t('settings-project:delete-project')}
</button>
<p className="mt-0.5 ml-1 text-xs text-gray-500">
{t('settings-project:delete-project-note')}
</p>
</div>
</div>
</div>
</div>
</div>
<ProjectSettingsPage />
</>
);
}
};
SettingsBasic.requireAuth = true;
export default ProjectSettings;
export const getServerSideProps = getTranslatedServerSideProps([
ProjectSettings.requireAuth = true;
export const getServerSideProps = getTranslatedStaticProps([
'settings',
'settings-project',
'section-token'
-1
View File
@@ -207,7 +207,6 @@ export default function Users() {
</div>
) : (
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full" />
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
</div>
);
-46
View File
@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Vercel() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
// type check
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'vercel',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Vercel integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Vercel.requireAuth = true;
@@ -0,0 +1,374 @@
import crypto from 'crypto';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import NavHeader from '@app/components/navigation/NavHeader';
// TODO(akhilmhdh):Refactor this into a better utility module package
import {
decryptAssymmetric,
encryptSymmetric
} from '@app/components/utilities/cryptography/crypto';
import { Button, FormControl, Input } from '@app/components/v2';
import { plans } from '@app/const';
import { useSubscription, useWorkspace } from '@app/context';
import { useToggle } from '@app/hooks';
import {
useCreateServiceToken,
useCreateWsEnvironment,
useCreateWsTag,
useDeleteServiceToken,
useDeleteWorkspace,
useDeleteWsEnvironment,
useDeleteWsTag,
useGetUserWsKey,
useGetUserWsServiceTokens,
useGetWsTags,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment
} from '@app/hooks/api';
import { AutoCapitalizationSection } from './components/AutoCapitalizationSection/AutoCapitalizationSection';
import { SecretTagsSection } from './components/SecretTagsSection';
import {
CopyProjectIDSection,
CreateServiceToken,
CreateUpdateEnvFormData,
CreateWsTag,
EnvironmentSection,
ProjectNameChangeSection,
ServiceTokenSection
} from './components';
export const ProjectSettingsPage = () => {
const { t } = useTranslation();
const { currentWorkspace, workspaces } = useWorkspace();
const router = useRouter();
const { data: serviceTokens } = useGetUserWsServiceTokens({
workspaceID: currentWorkspace?._id || ''
});
const workspaceID = currentWorkspace?._id || '';
const { createNotification } = useNotificationContext();
// delete action worksapce
const [deleteProjectInput, setDeleteProjectInput] = useState('');
const [isDeleting, setIsDeleting] = useToggle();
const renameWorkspace = useRenameWorkspace();
const toggleAutoCapitalization = useToggleAutoCapitalization();
const deleteWorkspace = useDeleteWorkspace();
// env crud operation
const createWsEnv = useCreateWsEnvironment();
const updateWsEnv = useUpdateWsEnvironment();
const deleteWsEnv = useDeleteWsEnvironment();
// service token
const { data: latestFileKey } = useGetUserWsKey(workspaceID);
const createServiceToken = useCreateServiceToken();
const deleteServiceToken = useDeleteServiceToken();
// tag
const { data: wsTags } = useGetWsTags(workspaceID);
const createWsTag = useCreateWsTag();
const deleteWsTag = useDeleteWsTag();
// get user subscription
const { subscriptionPlan } = useSubscription();
const host = window.location.origin;
const isEnvServiceAllowed =
subscriptionPlan !== plans.starter || host !== 'https://app.infisical.com';
const onRenameWorkspace = async (name: string) => {
try {
await renameWorkspace.mutateAsync({ workspaceID, newWorkspaceName: name });
createNotification({
text: 'Successfully renamed workspace',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to rename workspace',
type: 'error'
});
}
};
const onAutoCapitalizationToggle = async (state: boolean) => {
try {
await toggleAutoCapitalization.mutateAsync({
workspaceID,
state
});
const text = `Successfully ${state ? 'enabled' : 'disabled'} auto capitalization`;
createNotification({
text,
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to update auto capitalization',
type: 'error'
});
}
};
const onDeleteWorkspace = async () => {
setIsDeleting.on();
try {
await deleteWorkspace.mutateAsync({ workspaceID });
// redirect user to first workspace user is part of
const ws = workspaces.find(({ _id }) => _id !== workspaceID);
router.push(`/dashboard/${ws?._id}`);
createNotification({
text: 'Successfully deleted workspace',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to delete workspace',
type: 'error'
});
} finally {
setIsDeleting.off();
}
};
// workspace environment operation
const onCreateWsEnv = async ({ environmentName, environmentSlug }: CreateUpdateEnvFormData) => {
try {
await createWsEnv.mutateAsync({ workspaceID, environmentName, environmentSlug });
createNotification({
text: 'Successfully created environment',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to create environment',
type: 'error'
});
}
};
const onUpdateWsEnv = async (
oldEnvironmentSlug: string,
{ environmentName, environmentSlug }: CreateUpdateEnvFormData
) => {
try {
await updateWsEnv.mutateAsync({
workspaceID,
environmentName,
environmentSlug,
oldEnvironmentSlug
});
createNotification({
text: 'Successfully updated environment',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to update environment',
type: 'error'
});
}
};
const onDeleteWsEnv = async (environmentSlug: string) => {
try {
await deleteWsEnv.mutateAsync({
workspaceID,
environmentSlug
});
createNotification({
text: 'Successfully deleted environment',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to delete environment',
type: 'error'
});
}
};
const onCreateServiceToken = async ({ environment, expiresIn, name }: CreateServiceToken) => {
// type guard
if (!latestFileKey) return '';
try {
// crypo calculation to generate the key
const key = decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: localStorage.getItem('PRIVATE_KEY') as string
});
const randomBytes = crypto.randomBytes(16).toString('hex');
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: key,
key: randomBytes
});
const res = await createServiceToken.mutateAsync({
encryptedKey: ciphertext,
iv,
tag,
environment,
expiresIn: Number(expiresIn),
name,
workspaceId: workspaceID
});
console.log(res);
createNotification({
text: 'Successfully created a service token',
type: 'success'
});
return res.serviceToken;
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to create a service token',
type: 'error'
});
}
return '';
};
const onCreateWsTag = async ({ name }: CreateWsTag) => {
try {
const res = await createWsTag.mutateAsync({
workspaceID,
tagName: name,
tagSlug: name.replace(" ", "_")
});
createNotification({
text: 'Successfully created a tag',
type: 'success'
});
return res.name;
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to create a tag',
type: 'error'
});
}
return '';
};
const onDeleteTag = async (tagID: string) => {
try {
await deleteWsTag.mutateAsync({ tagID });
createNotification({
text: 'Successfully deleted tag',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to delete the tag',
type: 'error'
});
}
};
const onDeleteServiceToken = async (tokenID: string) => {
try {
await deleteServiceToken.mutateAsync(tokenID);
createNotification({
text: 'Successfully revoked service token',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to delete service token',
type: 'error'
});
}
};
return (
<div className="container mx-auto flex flex-col px-8 text-mineshaft-50 dark dark:[color-scheme:dark]">
{/* TODO(akhilmhdh): Remove this right when layout is refactored */}
<div className="relative right-5">
<NavHeader pageName={t('settings-project:title')} isProjectRelated />
</div>
<div className="my-8 flex max-w-5xl flex-row items-center justify-between text-xl">
<div className="flex flex-col items-start justify-start text-3xl">
<p className="mr-4 font-semibold text-gray-200">{t('settings-project:title')}</p>
<p className="mr-4 text-base font-normal text-gray-400">
{t('settings-project:description')}
</p>
</div>
</div>
<ProjectNameChangeSection
workspaceName={currentWorkspace?.name}
onProjectNameChange={onRenameWorkspace}
/>
<AutoCapitalizationSection
workspaceAutoCapitalization={currentWorkspace?.autoCapitalization}
onAutoCapitalizationChange={onAutoCapitalizationToggle}
/>
<CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} />
<EnvironmentSection
environments={currentWorkspace?.environments || []}
onCreate={onCreateWsEnv}
onDelete={onDeleteWsEnv}
onUpdate={onUpdateWsEnv}
isEnvServiceAllowed={isEnvServiceAllowed}
/>
<ServiceTokenSection
tokens={serviceTokens || []}
environments={currentWorkspace?.environments || []}
onDeleteToken={onDeleteServiceToken}
workspaceName={currentWorkspace?.name || ''}
onCreateToken={onCreateServiceToken}
/>
<SecretTagsSection
tags={wsTags || []}
onDeleteTag={onDeleteTag}
workspaceName={currentWorkspace?.name || ''}
onCreateTag={onCreateWsTag}
/>
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4">
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
<p className="text-md mt-2 text-gray-400">{t('settings-project:danger-zone-note')}</p>
<div className="mr-auto mt-4 max-h-28 w-full max-w-md">
<FormControl
label={
<div className="mb-0.5 text-sm font-normal text-gray-400">
Type <span className="font-bold">{currentWorkspace?.name}</span> to delete the
workspace
</div>
}
>
<Input
onChange={(e) => setDeleteProjectInput(e.target.value)}
value={deleteProjectInput}
placeholder="Type the project name to delete"
/>
</FormControl>
</div>
<Button
colorSchema="danger"
onClick={onDeleteWorkspace}
isDisabled={deleteProjectInput !== currentWorkspace?.name || isDeleting}
isLoading={isDeleting}
>
{t('settings-project:delete-project')}
</Button>
<p className="mt-3 ml-0.5 text-xs text-gray-500">
{t('settings-project:delete-project-note')}
</p>
</div>
</div>
);
};
@@ -0,0 +1,34 @@
import { useTranslation } from 'react-i18next';
import { Checkbox } from '@app/components/v2';
type Props = {
workspaceAutoCapitalization?: boolean;
onAutoCapitalizationChange: (state: boolean) => Promise<void>;
};
export const AutoCapitalizationSection = ({
workspaceAutoCapitalization,
onAutoCapitalizationChange
}: Props) => {
const { t } = useTranslation();
return (
<form>
<div className="mb-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold">
{t('settings-project:auto-capitalization')}
</p>
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={workspaceAutoCapitalization}
onCheckedChange={(state) => {
onAutoCapitalizationChange(state as boolean);
}}
>
{t('settings-project:auto-capitalization-description')}
</Checkbox>
</div>
</form>
);
};
@@ -0,0 +1 @@
export { AutoCapitalizationSection } from './AutoCapitalizationSection';
@@ -0,0 +1,66 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconButton } from '@app/components/v2';
import { useToggle } from '@app/hooks';
type Props = {
workspaceID: string;
};
export const CopyProjectIDSection = ({ workspaceID }: Props): JSX.Element => {
const { t } = useTranslation();
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isProjectIdCopied) {
timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isProjectIdCopied]);
const copyProjectIdToClipboard = () => {
navigator.clipboard.writeText(workspaceID);
setIsProjectIdCopied.on();
};
return (
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-4 pb-2">
<p className="self-start text-xl font-semibold">{t('common:project-id')}</p>
<p className="mt-4 self-start text-base font-normal text-gray-400">
{t('settings-project:project-id-description')}
</p>
<p className="mt-2 self-start text-base font-normal text-gray-400">
{t('settings-project:project-id-description2')}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a
href="https://infisical.com/docs/getting-started/introduction"
target="_blank"
rel="noopener"
className="text-primary duration-200 hover:opacity-80"
>
{t('settings-project:docs')}
</a>
</p>
<p className="mt-4 text-xs text-bunker-300">{t('settings-project:auto-generated')}</p>
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400">
<p className="mr-2 pl-4 font-bold">{`${t('common:project-id')}:`}</p>
<p className="mr-4">{workspaceID}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => copyProjectIdToClipboard()}
>
<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t('common:click-to-copy')}
</span>
</IconButton>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { CopyProjectIDSection } from './CopyProjectIDSection';
@@ -0,0 +1,240 @@
import { Controller, useForm } from 'react-hook-form';
import { faPencil, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import {
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr,
UpgradePlanModal
} from '@app/components/v2';
import { usePopUp } from '@app/hooks/usePopUp';
type Props = {
environments: Array<{ name: string; slug: string }>;
isEnvServiceAllowed: boolean;
onCreate: (data: CreateUpdateEnvFormData) => Promise<void>;
onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise<void>;
onDelete: (envSlug: string) => Promise<void>;
};
const createUpdateEnvSchema = yup.object({
environmentName: yup.string().label('Environment Name').required(),
environmentSlug: yup.string().label('Environment Slug').required()
});
export type CreateUpdateEnvFormData = yup.InferType<typeof createUpdateEnvSchema>;
export const EnvironmentSection = ({
environments,
isEnvServiceAllowed,
onCreate,
onDelete,
onUpdate
}: Props): JSX.Element => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
'createUpdateEnv',
'deleteEnv',
'upgradePlan'
] as const);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<CreateUpdateEnvFormData>({
resolver: yupResolver(createUpdateEnvSchema)
});
const isEnvUpdate = Boolean(popUp?.createUpdateEnv?.data);
const oldEnvSlug = (popUp?.createUpdateEnv?.data as { slug: string })?.slug;
const onEnvModalSubmit = async (data: CreateUpdateEnvFormData) => {
if (isEnvUpdate) {
await onUpdate(oldEnvSlug, data);
} else {
await onCreate(data);
}
handlePopUpClose('createUpdateEnv');
};
const onEnvDeleteSubmit = async (envSlug: string) => {
await onDelete(envSlug);
handlePopUpClose('deleteEnv');
};
return (
<div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-white/5 p-6">
<div className="mb-2 flex w-full flex-row justify-between">
<div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Project Environments</p>
<p className="mb-4 text-base text-gray-400">
Choose which environments will show up in your dashboard like development, staging,
production
</p>
<p className="mr-1 self-start text-sm text-gray-500">
Note: the text in slugs shows how these environmant should be accessed in CLI.
</p>
</div>
<div>
<Button
onClick={() => {
if (isEnvServiceAllowed) {
handlePopUpOpen('createUpdateEnv');
} else {
handlePopUpOpen('upgradePlan');
}
}}
colorSchema="primary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add New Environment
</Button>
</div>
</div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th aria-label="button" />
</Tr>
</THead>
<TBody>
{environments?.length > 0 ? (
environments.map(({ name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex items-center justify-end">
<IconButton
className="mr-3"
onClick={() => {
if (isEnvServiceAllowed) {
handlePopUpOpen('createUpdateEnv', { name, slug });
reset({ environmentName: name, environmentSlug: slug });
} else {
handlePopUpOpen('upgradePlan');
}
}}
colorSchema="secondary"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => {
if (isEnvServiceAllowed) {
handlePopUpOpen('deleteEnv', { name, slug });
} else {
handlePopUpOpen('upgradePlan');
}
}}
colorSchema="danger"
ariaLabel="update"
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
</Td>
</Tr>
))
) : (
<Tr>
<Td colSpan={4} className="pt-7 pb-5 text-center text-bunker-400">
No environments found
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<Modal
isOpen={popUp?.createUpdateEnv?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle('createUpdateEnv', isOpen);
reset();
}}
>
<ModalContent title={isEnvUpdate ? 'Update environment' : 'Create a new environment'}>
<form onSubmit={handleSubmit(onEnvModalSubmit)}>
<Controller
control={control}
defaultValue=""
name="environmentName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="environmentSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Slug"
helperText="Slugs are shorthands used in cli to access environment"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isEnvUpdate ? 'Update' : 'Create'}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.deleteEnv.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
}?`}
onChange={(isOpen) => handlePopUpToggle('deleteEnv', isOpen)}
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
onDeleteApproved={() =>
onEnvDeleteSubmit((popUp?.deleteEnv?.data as { slug: string })?.slug)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan."
/>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More