chore(frontend): changed source code to src folder from root

This commit is contained in:
akhilmhdh
2023-01-16 00:34:22 +05:30
parent c563548a1c
commit 63e9d83ba4
193 changed files with 280 additions and 33 deletions
@@ -0,0 +1,123 @@
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { useTranslation } from 'next-i18next';
import Plan from '~/components/billing/Plan';
import NavHeader from '~/components/navigation/NavHeader';
import { STRIPE_PRODUCT_PRO, STRIPE_PRODUCT_STARTER } from '~/utilities/config';
import { getTranslatedServerSideProps } from '~/utilities/withTranslateProps';
import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription';
import getOrganizationUsers from '../../api/organization/GetOrgUsers';
export default function SettingsBilling() {
const [currentPlan, setCurrentPlan] = useState('');
const [numUsers, setNumUsers] = useState(0);
const { t } = useTranslation();
const plans = [
{
key: 1,
name: t('billing:starter.name')!,
price: t('billing:free')!,
priceExplanation: t('billing:starter.price-explanation')!,
text: t('billing:starter.text')!,
subtext: t('billing:starter.subtext')!,
buttonTextMain: t('billing:downgrade')!,
buttonTextSecondary: t('billing:learn-more')!,
current: currentPlan == STRIPE_PRODUCT_STARTER,
},
{
key: 2,
name: t('billing:professional.name')!,
price: '$9',
priceExplanation: t('billing:professional.price-explanation')!,
subtext: t('billing:professional.subtext')!,
text: t('billing:professional.text')!,
buttonTextMain: t('billing:upgrade')!,
buttonTextSecondary: t('billing:learn-more')!,
current: currentPlan == STRIPE_PRODUCT_PRO,
},
{
key: 3,
name: t('billing:enterprise.name')!,
price: t('billing:custom-pricing')!,
text: t('billing:enterprise.text')!,
buttonTextMain: t('billing:schedule-demo')!,
buttonTextSecondary: t('billing:learn-more')!,
current: false,
},
];
useEffect(() => {
(async () => {
const orgId = localStorage.getItem('orgData.id') as string;
const subscriptions = await getOrganizationSubscriptions({
orgId,
});
setCurrentPlan(subscriptions.data[0].plan.id);
const orgUsers = await getOrganizationUsers({
orgId,
});
setNumUsers(orgUsers.length);
})();
}, []);
return (
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
<Head>
<title>{t('common:head-title', { title: t('billing:title') })}</title>
<link rel='icon' href='/infisical.ico' />
</Head>
<div className='flex flex-row'>
<div className='w-full max-h-screen pb-2 overflow-y-auto'>
<NavHeader pageName={t('billing:title')} />
<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('billing:title')}
</p>
<p className='font-normal mr-4 text-gray-400 text-base'>
{t('billing:description')}
</p>
</div>
</div>
<div className='flex flex-col ml-6 text-mineshaft-50'>
<p className='text-xl font-semibold'>{t('billing:subscription')}</p>
<div className='flex flex-row mt-4 overflow-x-auto'>
{plans.map((plan) => (
<Plan key={plan.name} plan={plan} />
))}
</div>
<p className='text-xl font-bold mt-12'>
{t('billing:current-usage')}
</p>
<div className='flex flex-row'>
<div className='mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col'>
<p className='text-6xl font-bold'>{numUsers}</p>
<p className='text-gray-300'>
{numUsers > 1
? 'Organization members'
: 'Organization member'}
</p>
</div>
{/* <div className="mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col">
<p className="text-6xl font-bold">1 </p>
<p className="text-gray-300">Organization projects</p>
</div> */}
</div>
</div>
</div>
</div>
</div>
);
}
SettingsBilling.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps([
'settings',
'billing',
]);
+398
View File
@@ -0,0 +1,398 @@
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import {
faMagnifyingGlass,
faPlus,
faX,
} from '@fortawesome/free-solid-svg-icons';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '~/components/basic/buttons/Button';
import AddIncidentContactDialog from '~/components/basic/dialog/AddIncidentContactDialog';
import AddUserDialog from '~/components/basic/dialog/AddUserDialog';
import InputField from '~/components/basic/InputField';
import UserTable from '~/components/basic/table/UserTable';
import NavHeader from '~/components/navigation/NavHeader';
import guidGenerator from '~/utilities/randomId';
import { getTranslatedServerSideProps } from '~/utilities/withTranslateProps';
import addUserToOrg from '../../api/organization/addUserToOrg';
import deleteIncidentContact from '../../api/organization/deleteIncidentContact';
import getIncidentContacts from '../../api/organization/getIncidentContacts';
import getOrganization from '../../api/organization/GetOrg';
import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription';
import getOrganizationUsers from '../../api/organization/GetOrgUsers';
import renameOrg from '../../api/organization/renameOrg';
import getUser from '../../api/user/getUser';
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
import getWorkspaces from '../../api/workspace/getWorkspaces';
export default function SettingsOrg() {
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [orgName, setOrgName] = useState('');
const [emailUser, setEmailUser] = useState('');
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
const [searchUsers, setSearchUsers] = useState('');
const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] =
useState(false);
const [isAddUserOpen, setIsAddUserOpen] = useState(
router.asPath.split('?')[1] == 'invite'
);
const [incidentContacts, setIncidentContacts] = useState<string[]>([]);
const [searchIncidentContact, setSearchIncidentContact] = useState('');
const [userList, setUserList] = useState<any[]>([]);
const [personalEmail, setPersonalEmail] = useState('');
const [email, setEmail] = useState('');
const [currentPlan, setCurrentPlan] = useState('');
const workspaceId = router.query.id as string;
const { t } = useTranslation();
useEffect(() => {
(async () => {
const orgId = localStorage.getItem('orgData.id') as string;
const org = await getOrganization({
orgId,
});
setOrgName(org.name);
const incidentContactsData = await getIncidentContacts(
localStorage.getItem('orgData.id') as string
);
setIncidentContacts(
incidentContactsData?.map((contact) => contact.email)
);
const user = await getUser();
setPersonalEmail(user.email);
const orgUsers = await getOrganizationUsers({
orgId,
});
setUserList(
orgUsers.map((user) => ({
key: guidGenerator(),
firstName: user.user?.firstName,
lastName: user.user?.lastName,
email: user.user?.email == null ? user.inviteEmail : user.user?.email,
role: user?.role,
status: user?.status,
userId: user.user?._id,
membershipId: user._id,
publicKey: user.user?.publicKey,
}))
);
const subscriptions = await getOrganizationSubscriptions({
orgId,
});
setCurrentPlan(subscriptions.data[0].plan.product);
})();
}, []);
const modifyOrgName = (newName: string) => {
setButtonReady(true);
setOrgName(newName);
};
const submitChanges = (newOrgName: string) => {
renameOrg(localStorage.getItem('orgData.id') as string, newOrgName);
setButtonReady(false);
};
function closeAddUserModal() {
setIsAddUserOpen(false);
}
function closeAddIncidentContactModal() {
setIsAddIncidentContactOpen(false);
}
function openAddUserModal() {
setIsAddUserOpen(true);
}
function openAddIncidentContactModal() {
setIsAddIncidentContactOpen(true);
}
async function submitAddUserModal(email: string) {
await addUserToOrg(email, localStorage.getItem('orgData.id') as string);
setEmail('');
setIsAddUserOpen(false);
router.reload();
}
const deleteIncidentContactFully = (incidentContact: string) => {
setIncidentContacts(
incidentContacts.filter((contact) => contact != incidentContact)
);
deleteIncidentContact(
localStorage.getItem('orgData.id') as string,
incidentContact
);
};
/**
* 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 userWorkspaces = await getWorkspaces();
router.push('/dashboard/' + userWorkspaces[0]._id);
}
}
};
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-org:title') })}
</title>
<link rel='icon' href='/infisical.ico' />
</Head>
<div className='flex flex-row'>
<div className='w-full max-h-screen pb-2 overflow-y-auto'>
<NavHeader pageName={t('settings-org:title')} />
<AddIncidentContactDialog
isOpen={isAddIncidentContactOpen}
closeModal={closeAddIncidentContactModal}
workspaceId={workspaceId}
incidentContacts={incidentContacts}
setIncidentContacts={setIncidentContacts}
/>
<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-org:title')}
</p>
<p className='font-normal mr-4 text-gray-400 text-base'>
{t('settings-org:description')}
</p>
</div>
</div>
<div className='flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-5xl'>
<div className='flex flex-col'>
<div className='min-w-md mt-2 flex flex-col items-end pb-4'>
<div className='bg-white/5 rounded-md px-6 py-4 flex flex-col items-start w-full mb-6'>
<div className='max-h-28 w-full max-w-md mr-auto'>
<p className='font-semibold mr-4 text-gray-200 text-xl mb-2'>
{t('common:display-name')}
</p>
<InputField
label=''
// label="Organization Name"
onChangeHandler={modifyOrgName}
type='varName'
value={orgName}
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(orgName)}
color='mineshaft'
size='md'
active={buttonReady}
iconDisabled={faCheck}
textDisabled={t('common:saved') as string}
/>
</div>
</div>
</div>
</div>
</div>
<div className='bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start w-full mb-6'>
<p className='font-semibold mr-4 text-white text-xl'>
{t('section-members:org-members')}
</p>
<p className='mr-4 text-gray-400 mt-2 mb-2'>
{t('section-members:org-members-description')}
</p>
<AddUserDialog
isOpen={isAddUserOpen}
closeModal={closeAddUserModal}
submitModal={submitAddUserModal}
email={emailUser}
workspaceId={workspaceId}
setEmail={setEmailUser}
currentPlan={currentPlan}
orgName={orgName}
/>
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
<div className='pb-1 w-full flex flex-row items-start max-w-6xl'>
<div className='h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex-row '>
<FontAwesomeIcon
className='bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400'
icon={faMagnifyingGlass}
/>
<input
className='pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none'
value={searchUsers}
onChange={(e) => setSearchUsers(e.target.value)}
placeholder={t('section-members:search-members') as string}
/>
</div>
<div className='mt-2 ml-2 min-w-max flex flex-row items-start justify-start'>
<Button
text={t('section-members:add-member') as string}
onButtonPressed={openAddUserModal}
color='mineshaft'
size='md'
icon={faPlus}
/>
</div>
</div>
{userList && (
<div className='overflow-y-auto max-w-6xl w-full'>
<UserTable
userData={userList}
changeData={setUserList}
myUser={personalEmail}
filter={searchUsers.toLowerCase()}
resendInvite={submitAddUserModal}
isOrg={true}
// onClick={openDeleteModal}
// deleteUser={deleteMembership}
// setUserIdToBeDeleted={setUserIdToBeDeleted}
className='w-full'
/>
</div>
)}
</div>
<div className='bg-white/5 rounded-md px-6 pt-6 pb-6 flex flex-col items-start w-full mb-6 mt-4'>
<div className='flex flex-row max-w-5xl justify-between items-center w-full'>
<div className='flex flex-col justify-between w-full max-w-3xl'>
<p className='text-xl font-semibold mb-3 min-w-max'>
{t('section-incident:incident-contacts')}
</p>
<p className='text-xs text-gray-500 mb-2 min-w-max'>
{t('section-incident:incident-contacts-description')}
</p>
</div>
<div className='mt-4 mb-2 min-w-max flex flex-row items-end justify-center'>
<Button
text={t('section-incident:add-contact') as string}
onButtonPressed={openAddIncidentContactModal}
color='mineshaft'
size='md'
icon={faPlus}
/>
</div>
</div>
<div className='h-12 w-full max-w-5xl bg-white/5 mt-2 flex items-center rounded-t-md flwex-row'>
<FontAwesomeIcon
className='bg-white/5 rounded-tl-md py-4 pl-4 pr-2 text-gray-400'
icon={faMagnifyingGlass}
/>
<input
className='pl-2 text-gray-400 rounded-tr-md bg-white/5 w-full h-full outline-none'
value={searchIncidentContact}
onChange={(e) => setSearchIncidentContact(e.target.value)}
placeholder={t('common:search') as string}
/>
</div>
{incidentContacts?.filter((email) =>
email.includes(searchIncidentContact)
).length > 0 ? (
incidentContacts
.filter((email) => email.includes(searchIncidentContact))
.map((contact) => (
<div
key={guidGenerator()}
className='flex flex-row items-center justify-between max-w-5xl px-4 py-3 hover:bg-white/5 border-t border-gray-600 w-full'
>
<p className='text-gray-300'>{contact}</p>
<div className='opacity-50 hover:opacity-100 duration-200'>
<Button
onButtonPressed={() =>
deleteIncidentContactFully(contact)
}
color='red'
size='icon-sm'
icon={faX}
/>
</div>
</div>
))
) : (
<div className='w-full flex flex-row justify-center mt-6 max-w-4xl ml-6'>
<p className='text-gray-400'>
{t('section-incident:no-incident-contacts')}
</p>
</div>
)}
</div>
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
<p className="text-xl font-bold text-red">
Danger Zone
</p>
<p className="mt-4 text-md text-gray-400">
As soon as you delete an organization, you will
not be able to undo it. This will immediately
remove all organization members and cancel your
subscription. If you still want to do that,
please enter the name of the organization below.
</p>
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
<InputField
label="Organization to be Deleted"
onChangeHandler={
setWorkspaceToBeDeletedName
}
type="varName"
value={workspaceToBeDeletedName}
placeholder=""
isRequired
/>
</div>
<button
type="button"
className="mt-6 w-full max-w-xl 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-bold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={executeDeletingWorkspace}
>
Delete Project
</button>
<p className="mt-0.5 ml-1 text-xs text-gray-500">
Note: You can only delete a project in case you
have more than one.
</p>
</div> */}
</div>
</div>
</div>
</div>
);
}
SettingsOrg.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps([
'settings',
'settings-org',
'section-incident',
'section-members',
]);
@@ -0,0 +1,370 @@
import { useEffect, useState } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { faCheck, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "~/components/basic/buttons/Button";
import InputField from "~/components/basic/InputField";
import ListBox from "~/components/basic/Listbox";
import ApiKeyTable from "~/components/basic/table/ApiKeyTable";
import NavHeader from "~/components/navigation/NavHeader";
import changePassword from "~/components/utilities/cryptography/changePassword";
import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey";
import passwordCheck from "~/utilities/checks/PasswordCheck";
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps";
import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog";
import getAPIKeys from "../../api/apiKey/getAPIKeys";
import getUser from "../../api/user/getUser";
export default function PersonalSettings() {
const [personalEmail, setPersonalEmail] = useState('');
const [personalName, setPersonalName] = useState('');
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [currentPasswordError, setCurrentPasswordError] = useState(false);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [backupPassword, setBackupPassword] = useState('');
const [passwordChanged, setPasswordChanged] = useState(false);
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
const [backupKeyError, setBackupKeyError] = useState(false);
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false)
const [apiKeys, setApiKeys] = useState<any[]>([]);
const { t } = useTranslation();
const router = useRouter();
const lang = router.locale ?? 'en';
const setLanguage = async (to: string) => {
router.push(router.asPath, router.asPath, { locale: to });
localStorage.setItem('lang', to);
};
useEffect(() => {
const load = async () => {
try {
const user = await getUser();
setApiKeys(
await getAPIKeys()
);
setPersonalEmail(user.email);
setPersonalName(user.firstName + " " + user.lastName);
} catch (err) {
console.error(err);
}
}
load();
}, []);
const closeAddApiKeyModal = () => {
setIsAddApiKeyDialogOpen(false);
};
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-personal:title') })}
</title>
<link rel='icon' href='/infisical.ico' />
</Head>
<AddApiKeyDialog
isOpen={isAddApiKeyDialogOpen}
closeModal={closeAddApiKeyModal}
apiKeys={apiKeys}
setApiKeys={setApiKeys}
/>
<div className="flex flex-row">
<div className="w-full max-h-screen pb-2 overflow-y-auto">
<NavHeader
pageName={t('settings-personal:title')}
isProjectRelated={false}
/>
<div className='flex flex-row justify-between items-center ml-6 mt-8 mb-6 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-personal:title')}
</p>
<p className='font-normal mr-4 text-gray-400 text-base'>
{t('settings-personal:description')}
</p>
</div>
</div>
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-5xl">
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 flex flex-col items-start w-full mb-6 mt-4">
<p className="text-xl font-semibold self-start">
{t("settings-personal:change-language")}
</p>
<div className='max-h-28 w-ful mt-4'>
<ListBox
selected={lang}
onChange={setLanguage}
data={['en', 'ko', 'fr']}
text={`${t('common:language')}: `}
/>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-2 mb-8 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("settings-personal:api-keys.title")}
</p>
<p className="text-sm text-gray-400">
{t("settings-personal:api-keys.description")}
</p>
</div>
<div className="w-48 mt-2">
<Button
text={String(t("settings-personal:api-keys.add-new"))}
onButtonPressed={() => {
setIsAddApiKeyDialogOpen(true);
}}
color="mineshaft"
icon={faPlus}
size="md"
/>
</div>
</div>
<ApiKeyTable
data={apiKeys}
setApiKeys={setApiKeys as any}
/>
</div>
<div className='bg-white/5 rounded-md px-6 pt-5 pb-6 flex flex-col items-start w-full mb-6'>
<div className='flex flex-row max-w-5xl justify-between items-center w-full'>
<div className='flex flex-col justify-between w-full max-w-3xl'>
<p className='text-xl font-semibold mb-3 min-w-max'>
{t('section-password:change')}
</p>
</div>
</div>
<div className='max-w-xl w-full'>
<InputField
label={t('section-password:current') as string}
onChangeHandler={(password) => {
setCurrentPassword(password);
}}
type='password'
value={currentPassword}
isRequired
error={currentPasswordError}
errorText={t('section-password:current-wrong') as string}
autoComplete='current-password'
id='current-password'
/>
<div className='py-2'></div>
<InputField
label={t('section-password:new') as string}
onChangeHandler={(password) => {
setNewPassword(password);
passwordCheck({
password,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false,
});
}}
type='password'
value={newPassword}
isRequired
error={
passwordErrorLength &&
passwordErrorLowerCase &&
passwordErrorNumber
}
autoComplete='new-password'
id='new-password'
/>
</div>
{passwordErrorLength ||
passwordErrorLowerCase ||
passwordErrorNumber ? (
<div className='w-full mt-3 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md max-w-xl mb-2'>
<div className={`text-gray-400 text-sm mb-1`}>
{t('section-password:validate-base')}
</div>
<div className='flex flex-row justify-start items-center ml-1'>
{passwordErrorLength ? (
<FontAwesomeIcon
icon={faX}
className='text-md text-red mr-2.5'
/>
) : (
<FontAwesomeIcon
icon={faCheck}
className='text-md text-primary mr-2'
/>
)}
<div
className={`${
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section-password:validate-length')}
</div>
</div>
<div className='flex flex-row justify-start items-center ml-1'>
{passwordErrorLowerCase ? (
<FontAwesomeIcon
icon={faX}
className='text-md text-red mr-2.5'
/>
) : (
<FontAwesomeIcon
icon={faCheck}
className='text-md text-primary mr-2'
/>
)}
<div
className={`${
passwordErrorLowerCase
? 'text-gray-400'
: 'text-gray-600'
} text-sm`}
>
{t('section-password:validate-case')}
</div>
</div>
<div className='flex flex-row justify-start items-center ml-1'>
{passwordErrorNumber ? (
<FontAwesomeIcon
icon={faX}
className='text-md text-red mr-2.5'
/>
) : (
<FontAwesomeIcon
icon={faCheck}
className='text-md text-primary mr-2'
/>
)}
<div
className={`${
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section-password:validate-number')}
</div>
</div>
</div>
) : (
<div className='py-2'></div>
)}
<div className='flex flex-row items-center mt-3 w-52 pr-3'>
<Button
text={t('section-password:change') as string}
onButtonPressed={() => {
if (
!passwordErrorLength &&
!passwordErrorLowerCase &&
!passwordErrorNumber
) {
changePassword(
personalEmail,
currentPassword,
newPassword,
setCurrentPasswordError,
setPasswordChanged,
setCurrentPassword,
setNewPassword
);
}
}}
color='mineshaft'
size='md'
active={
newPassword != '' &&
currentPassword != '' &&
!(
passwordErrorLength ||
passwordErrorLowerCase ||
passwordErrorNumber
)
}
textDisabled={t('section-password:change') as string}
/>
<FontAwesomeIcon
icon={faCheck}
className={`ml-4 text-primary text-3xl ${
passwordChanged ? 'opacity-100' : 'opacity-0'
} duration-300`}
/>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-5 pb-6 mt-2 flex flex-col items-start flex flex-col items-start w-full mb-6">
<div className="flex flex-row max-w-5xl justify-between items-center w-full">
<div className="flex flex-col justify-between w-full max-w-3xl">
<p className="text-xl font-semibold mb-3 min-w-max">
{t("settings-personal:emergency.name")}
</p>
<p className='text-sm text-mineshaft-300 min-w-max'>
{t('settings-personal:emergency.text1')}
</p>
<p className='text-sm text-mineshaft-300 mb-5 min-w-max'>
{t('settings-personal:emergency.text2')}
</p>
</div>
</div>
<div className='w-full max-w-xl mb-4'>
<InputField
label={t('section-password:current') as string}
onChangeHandler={setBackupPassword}
type='password'
value={backupPassword}
isRequired
error={backupKeyError}
errorText={t('section-password:current-wrong') as string}
autoComplete='current-password'
id='current-password'
/>
</div>
<div className='flex flex-row items-center mt-3 w-60'>
<Button
text={t('settings-personal:emergency.download') as string}
onButtonPressed={() => {
issueBackupKey({
email: personalEmail,
password: backupPassword,
personalName,
setBackupKeyError,
setBackupKeyIssued,
});
}}
color='mineshaft'
size='md'
active={backupPassword != ''}
textDisabled={
t('settings-personal:emergency.download') as string
}
/>
<FontAwesomeIcon
icon={faCheck}
className={`ml-4 text-primary text-3xl ${
backupKeyIssued ? 'opacity-100' : 'opacity-0'
} duration-300`}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
PersonalSettings.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps([
"settings",
"settings-personal",
"section-password",
"section-api-key"
]);
@@ -0,0 +1,357 @@
import { useEffect, useState } from 'react';
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 '~/components/basic/buttons/Button';
import AddServiceTokenDialog from '~/components/basic/dialog/AddServiceTokenDialog';
import InputField from '~/components/basic/InputField';
import EnvironmentTable from '~/components/basic/table/EnvironmentsTable';
import ServiceTokenTable from '~/components/basic/table/ServiceTokenTable';
import NavHeader from '~/components/navigation/NavHeader';
import deleteEnvironment from '~/pages/api/environments/deleteEnvironment';
import updateEnvironment from '~/pages/api/environments/updateEnvironment';
import { getTranslatedServerSideProps } from '~/utilities/withTranslateProps';
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;
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 userWorkspaces = await getWorkspaces();
router.push('/dashboard/' + userWorkspaces[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={true}
/>
<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 pt-6 pb-4 flex flex-col items-start 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
></input>
<div className='group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200'>
<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>
);
}
SettingsBasic.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps([
'settings',
'settings-project',
'section-token',
]);