Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-02-14 17:40:42 +07:00
39 changed files with 1681 additions and 1253 deletions
@@ -38,7 +38,6 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
const router = useRouter();
const [myRole, setMyRole] = useState('member');
const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]);
console.log(123, userData)
const workspaceId = router.query.id as string;
// Delete the row in the table (e.g. a user)
@@ -198,15 +197,15 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
</div>
</td>
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl">
{userProjectMemberships[row.userId]
? userProjectMemberships[row.userId]?.map((project: any) => (
<div key={project.id} className='mx-1 min-w-max px-1.5 bg-mineshaft-500 rounded-sm text-sm text-bunker-200 flex items-center'>
<span className='mb-0.5 cursor-default'>{project.name}</span>
</div>
))
: <span className='ml-1 text-bunker-100 rounded-sm px-1 py-0.5 text-sm bg-red/80'>This user isn&apos;t part of any projects yet.</span>}
</td>
<div className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl break-all">
{userProjectMemberships[row.userId]
? userProjectMemberships[row.userId]?.map((project: any) => (
<div key={project._id} className='mx-1 min-w-max px-1.5 bg-mineshaft-500 rounded-sm text-sm text-bunker-200 flex items-center'>
<span className='mb-0.5 cursor-default'>{project.name}</span>
</div>
))
: <span className='ml-1 text-bunker-100 rounded-sm px-1 py-0.5 text-sm bg-red/80'>This user isn&apos;t part of any projects yet.</span>}
</div>
</td>
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
{myUser !== row.email &&
@@ -44,9 +44,9 @@ const CloudIntegration = ({
tabIndex={0}
className={`relative ${
cloudIntegrationOption.isAvailable
? 'hover:bg-white/10 duration-200 cursor-pointer'
? 'cursor-pointer duration-200 hover:bg-white/10'
: 'opacity-50'
} flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`}
} flex h-32 flex-row items-center rounded-md bg-white/5 p-4`}
onClick={() => {
if (!cloudIntegrationOption.isAvailable) return;
setSelectedIntegrationOption(cloudIntegrationOption);
@@ -61,22 +61,22 @@ const CloudIntegration = ({
alt="integration logo"
/>
{cloudIntegrationOption.name.split(' ').length > 2 ? (
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
<div className="text-base">
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
</div>
</div>
) : (
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegrationOption.name}
</div>
)}
{cloudIntegrationOption.isAvailable &&
integrationAuths
.map((authorization) => authorization.integration)
.map((authorization) => authorization?.integration)
.includes(cloudIntegrationOption.slug) && (
<div className="absolute group z-40 top-0 right-0 flex flex-row">
<div className="group absolute top-0 right-0 z-40 flex flex-row">
<div
onKeyDown={() => null}
role="button"
@@ -86,8 +86,7 @@ const CloudIntegration = ({
const deletedIntegrationAuth = await deleteIntegrationAuth({
integrationAuthId: integrationAuths
.filter(
(authorization) =>
authorization.integration === cloudIntegrationOption.slug
(authorization) => authorization.integration === cloudIntegrationOption.slug
)
.map((authorization) => authorization._id)[0]
});
@@ -96,20 +95,20 @@ const CloudIntegration = ({
integrationAuth: deletedIntegrationAuth
});
}}
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
className="flex w-max cursor-pointer flex-row items-center rounded-b-md bg-red py-0.5 px-2 text-xs opacity-0 duration-200 group-hover:opacity-100"
>
<FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px" />
<FontAwesomeIcon icon={faX} className="mr-2 py-px text-xs" />
Revoke
</div>
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
<FontAwesomeIcon icon={faCheck} className="text-xs mr-2" />
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-90 duration-200 group-hover:opacity-100">
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
Authorized
</div>
</div>
)}
{!cloudIntegrationOption.isAvailable && (
<div className="absolute group z-50 top-0 right-0 flex flex-row">
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
<div className="group absolute top-0 right-0 z-50 flex flex-row">
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-yellow py-0.5 px-2 text-xs text-black opacity-90">
Coming Soon
</div>
</div>
@@ -45,8 +45,8 @@ type Props = {
handleDeleteIntegration: (args: { integration: Integration }) => void;
};
const IntegrationTile = ({
integration,
const IntegrationTile = ({
integration,
integrations,
bot,
setBot,
@@ -57,7 +57,7 @@ const IntegrationTile = ({
// 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) || {
environments.find(({ slug }) => slug === integration?.environment) || {
name: '',
slug: ''
}
@@ -69,11 +69,10 @@ const IntegrationTile = ({
useEffect(() => {
const loadIntegration = async () => {
const tempApps: [IntegrationApp] = await getIntegrationApps({
integrationAuthId: integration.integrationAuth
integrationAuthId: integration?.integrationAuth
});
setApps(tempApps);
if (integration?.app) {
@@ -90,15 +89,16 @@ const IntegrationTile = ({
case 'vercel':
setIntegrationTargetEnvironment(
integration?.targetEnvironment
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
: 'Development'
? integration.targetEnvironment.charAt(0).toUpperCase() +
integration.targetEnvironment.substring(1)
: 'Development'
);
break;
case 'netlify':
setIntegrationTargetEnvironment(
integration?.targetEnvironment
? contextNetlifyMapping[integration.targetEnvironment]
: 'Local development'
integration?.targetEnvironment
? contextNetlifyMapping[integration.targetEnvironment]
: 'Local development'
);
break;
default:
@@ -108,7 +108,7 @@ const IntegrationTile = ({
loadIntegration();
}, []);
const handleStartIntegration = async () => {
const reformatTargetEnvironment = (targetEnvironment: string) => {
switch (integration.integration) {
@@ -119,13 +119,13 @@ const IntegrationTile = ({
default:
return null;
}
}
};
try {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const appId = siteApp?.appId ?? null;
const owner = siteApp?.owner ?? null;
// return updated integration
const updatedIntegration = await updateIntegration({
integrationId: integration._id,
@@ -136,15 +136,15 @@ const IntegrationTile = ({
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
owner
});
setIntegrations(
integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i)
integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i))
);
} catch (err) {
console.error(err);
}
}
};
// eslint-disable-next-line @typescript-eslint/no-shadow
const renderIntegrationSpecificParams = (integration: Integration) => {
try {
@@ -152,7 +152,7 @@ const IntegrationTile = ({
case 'vercel':
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
<div className="mb-2 w-60 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
<ListBox
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
isSelected={integrationTargetEnvironment}
@@ -164,7 +164,7 @@ const IntegrationTile = ({
case 'netlify':
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div>
<div className="mb-2 text-xs font-semibold text-gray-400">CONTEXT</div>
<ListBox
data={
!integration.isActive
@@ -189,10 +189,10 @@ const IntegrationTile = ({
if (!integrationApp) return <div />;
return (
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-white/5 p-6">
<div className="flex">
<div>
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
<ListBox
data={!integration.isActive ? environments.map(({ name }) => name) : null}
isSelected={integrationEnvironment.name}
@@ -208,7 +208,7 @@ const IntegrationTile = ({
/>
</div>
<div className="pt-2">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" />
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
</div>
<div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
@@ -218,7 +218,7 @@ const IntegrationTile = ({
</div>
</div>
<div className="mr-2">
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div>
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={integrationApp}
@@ -231,9 +231,9 @@ const IntegrationTile = ({
</div>
<div className="flex items-end">
{integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" />
<div className="text-gray-300 font-semibold">In Sync</div>
<div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
<FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
<div className="font-semibold text-gray-300">In Sync</div>
</div>
) : (
<Button
@@ -243,11 +243,13 @@ const IntegrationTile = ({
size="md"
/>
)}
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
<div className="ml-2 opacity-50 duration-200 hover:opacity-100">
<Button
onButtonPressed={() => handleDeleteIntegration({
integration
})}
onButtonPressed={() =>
handleDeleteIntegration({
integration
})
}
color="red"
size="icon-md"
icon={faX}
@@ -26,7 +26,7 @@ interface Integration {
}
const ProjectIntegrationSection = ({
integrations,
integrations,
setIntegrations,
bot,
setBot,
@@ -35,22 +35,20 @@ const ProjectIntegrationSection = ({
}: Props) => {
return integrations.length > 0 ? (
<div className="mb-12">
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
<h1 className="font-semibold text-3xl">Current Integrations</h1>
<p className="text-base text-gray-400">
Manage integrations with third-party services.
</p>
<div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Current Integrations</h1>
<p className="text-base text-gray-400">Manage integrations with third-party services.</p>
</div>
{integrations.map((integration: Integration) => {
return (
<IntegrationTile
key={`integration-${integration._id.toString()}`}
integration={integration}
key={`integration-${integration?._id.toString()}`}
integration={integration}
integrations={integrations}
bot={bot}
setBot={setBot}
setIntegrations={setIntegrations}
environments={environments}
environments={environments}
handleDeleteIntegration={handleDeleteIntegration}
/>
);
@@ -59,6 +57,6 @@ const ProjectIntegrationSection = ({
) : (
<div />
);
}
export default ProjectIntegrationSection;
};
export default ProjectIntegrationSection;
@@ -38,6 +38,13 @@ export const Secondary: Story = {
}
};
export const Star: Story = {
args: {
children: 'Hello Infisical',
variant: 'star'
}
};
export const Danger: Story = {
args: {
children: 'Hello Infisical',
+13 -1
View File
@@ -31,7 +31,9 @@ const buttonVariants = cva(
variant: {
solid: '',
outline: ['bg-transparent', 'border-2', 'border-solid'],
plain: ''
plain: '',
// a constant color not in use on hover or click goes colorSchema color
star: 'text-bunker-200 bg-mineshaft-500'
},
isDisabled: {
true: 'bg-mineshaft opacity-40 cursor-not-allowed',
@@ -53,6 +55,16 @@ const buttonVariants = cva(
}
},
compoundVariants: [
{
colorSchema: 'primary',
variant: 'star',
className: 'hover:bg-primary hover:text-black'
},
{
colorSchema: 'danger',
variant: 'star',
className: 'hover:bg-red hover:text-white'
},
{
colorSchema: 'primary',
variant: 'outline',
+1 -1
View File
@@ -46,7 +46,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal>
<SelectPrimitive.Content
className={twMerge(
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 border border-mineshaft-500 drop-shadow-xl font-inter text-bunker-100 shadow-md z-[100]',
'relative top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
dropdownContainerClassName
)}
position={position}
+2
View File
@@ -44,3 +44,5 @@ const plansProd: Mapping = {
};
export const plans = plansProd || plansDev;
export const leaveConfirmDefaultMessage = 'Do you want to save your results before leaving this page?';
@@ -27,7 +27,7 @@ export const OrgProvider = ({ children }: Props): JSX.Element => {
const value = useMemo<TOrgContext>(
() => ({
orgs: userOrgs,
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID),
currentOrg: (userOrgs || []).find(({ _id }) => _id === currentWsOrgID) || (userOrgs || [])[0],
isLoading
}),
[currentWsOrgID, userOrgs, isLoading]
+11 -17
View File
@@ -9,7 +9,6 @@ import getActionData from '@app/ee/api/secrets/GetActionData';
import patienceDiff from '@app/ee/utilities/findTextDifferences';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import DashboardInputField from '../../components/dashboard/DashboardInputField';
import {
decryptAssymmetric,
decryptSymmetric
@@ -130,7 +129,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<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-[calc(100vh-56px)] w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
@@ -142,7 +141,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
/>
</div>
) : (
<div className="h-min overflow-y-auto">
<div className="h-min">
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">
{t(`activity:event.${actionMetaData?.name}`)}
@@ -157,7 +156,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
</div>
</div>
<div className="flex flex-col px-4">
<div className="flex flex-col px-4 overflow-y-auto h-[calc(100vh-120px)] overflow-y-autp">
{(actionMetaData?.name === 'readSecrets' ||
actionMetaData?.name === 'addSecrets' ||
actionMetaData?.name === 'deleteSecrets') &&
@@ -166,14 +165,9 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<div className="text-xs text-bunker-200 mt-4 pl-1 ph-no-capture">
{item.newSecretVersion.key}
</div>
<DashboardInputField
onChangeHandler={() => {}}
type="value"
position={1}
value={item.newSecretVersion.value}
isDuplicate={false}
blurred={false}
/>
<div className='w-full font-mono text-sm break-all bg-mineshaft-600 px-2 py-0.5 rounded-md border border-mineshaft-500 text-bunker-200'>
{item.newSecretVersion.value ? <span> {item.newSecretVersion.value} </span> : <span className='text-bunker-400'> EMPTY </span>}
</div>
</div>
))}
{actionMetaData?.name === 'updateSecrets' &&
@@ -182,8 +176,8 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
<div className="text-xs text-bunker-200 mt-4 pl-1">
{item.newSecretVersion.key}
</div>
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
<div className="bg-red/30 px-2 ph-no-capture">
<div className="break-all text-bunker-200 font-mono rounded-md overflow-hidden border border-mineshaft-500">
<div className="bg-red/40 px-2 ph-no-capture">
-{' '}
{patienceDiff(
item.oldSecretVersion.value.split(''),
@@ -194,14 +188,14 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
character.bIndex !== -1 && (
<span
key={`actionData.${id + 1}.line.${lineId + 1}`}
className={`${character.aIndex === -1 && 'bg-red-700/80'}`}
className={`${character.aIndex === -1 && 'text-bunker-100 bg-red-700/80'}`}
>
{character.line}
</span>
)
)}
</div>
<div className="bg-green-500/30 px-2 ph-no-capture">
<div className="break-all bg-green-500/40 px-2 ph-no-capture">
+{' '}
{patienceDiff(
item.oldSecretVersion.value.split(''),
@@ -212,7 +206,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
character.aIndex !== -1 && (
<span
key={`actionData.${id + 1}.linev2.${lineId + 1}`}
className={`${character.bIndex === -1 && 'bg-green-700/80'}`}
className={`${character.bIndex === -1 && 'text-bunker-100 bg-green-700/80'}`}
>
{character.line}
</span>
+1
View File
@@ -1,2 +1,3 @@
export { useLeaveConfirm } from './useLeaveConfirm';
export { usePopUp } from './usePopUp';
export { useToggle } from './useToggle';
+55
View File
@@ -0,0 +1,55 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { leaveConfirmDefaultMessage } from '@app/const';
type LeaveConfirmProps = {
initialValue: boolean,
message?: string
}
interface LeaveConfirmReturn {
hasUnsavedChanges: boolean,
setHasUnsavedChanges: Dispatch<SetStateAction<boolean>>,
}
export function useLeaveConfirm({
initialValue,
message = leaveConfirmDefaultMessage,
}: LeaveConfirmProps): LeaveConfirmReturn {
const router = useRouter()
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(initialValue);
const onRouteChangeStart = useCallback(() => {
if (hasUnsavedChanges) {
if (window.confirm(message)) {
return true
}
throw new Error("Abort route change by user's confirmation.")
}
return false;
}, [hasUnsavedChanges])
const handleWindowClose = useCallback((e: any) => {
if (!hasUnsavedChanges) {
return;
}
e.preventDefault();
e.returnValue = message;
}, []);
useEffect(() => {
router.events.on("routeChangeStart", onRouteChangeStart);
window.addEventListener('beforeunload', handleWindowClose);
return () => {
router.events.off("routeChangeStart", onRouteChangeStart);
window.removeEventListener('beforeunload', handleWindowClose);
}
}, [onRouteChangeStart, handleWindowClose]);
return {
hasUnsavedChanges,
setHasUnsavedChanges,
};
}
+65 -49
View File
@@ -64,7 +64,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
// eslint-disable-next-line prefer-const
let { workspaces, currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
workspaces = workspaces.filter(ws => ws.organization === currentOrg?._id)
workspaces = workspaces.filter((ws) => ws.organization === currentOrg?._id);
const { user } = useUser();
const createWs = useCreateWorkspace();
@@ -98,7 +98,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') {
const userOrgs = await getOrganizations();
localStorage.setItem('orgData.id', userOrgs[0]._id);
localStorage.setItem('orgData.id', userOrgs[0]?._id);
}
const orgUserProjects = await getOrganizationUserProjects({
@@ -124,7 +124,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) &&
!['callback', 'create', 'authorize'].includes(intendedWorkspaceId) && userWorkspaces[0]?._id !== undefined &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
@@ -219,50 +219,58 @@ export const AppLayout = ({ children }: LayoutProps) => {
<aside className="w-full border-r border-mineshaft-500 bg-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between">
<div>
{currentWorkspace
? <div className="w-full p-4 mt-3 mb-4">
<p className="text-xs font-semibold ml-1.5 mb-1 uppercase text-gray-400">Project</p>
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full py-2.5 bg-mineshaft-600 font-medium"
onValueChange={(value) => {
router.push(`/dashboard/${value}`);
}}
position="popper"
dropdownContainerClassName="left-0 text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
>
{workspaces.map(({ _id, name }) => (
<SelectItem key={`ws-layout-list-${_id}`} value={_id} className={`${currentWorkspace?._id === _id && "bg-mineshaft-600"}`}>
{name}
</SelectItem>
))}
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
<div className="w-full">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
</Select>
</div>
: <div className="w-full p-4 mt-3 mb-4">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>}
<div className={`${currentWorkspace ? "block" : "hidden"}`}>
{currentWorkspace ? (
<div className="w-full p-4 mt-3 mb-4">
<p className="text-xs font-semibold ml-1.5 mb-1 uppercase text-gray-400">
Project
</p>
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full py-2.5 bg-mineshaft-600 font-medium"
onValueChange={(value) => {
router.push(`/dashboard/${value}`);
}}
position="popper"
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50"
>
{workspaces.map(({ _id, name }) => (
<SelectItem
key={`ws-layout-list-${_id}`}
value={_id}
className={`${currentWorkspace?._id === _id && 'bg-mineshaft-600'}`}
>
{name}
</SelectItem>
))}
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
<div className="w-full">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
</Select>
</div>
) : (
<div className="w-full p-4 mt-3 mb-4">
<Button
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
color="mineshaft"
size="sm"
onClick={() => handlePopUpOpen('addNewWs')}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Project
</Button>
</div>
)}
<div className={`${currentWorkspace ? 'block' : 'hidden'}`}>
<Menu>
<Link href={`/dashboard/${currentWorkspace?._id}`} passHref>
<a>
@@ -393,7 +401,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
</FormControl>
)}
/>
<div className='pl-1 mt-4'>
<div className="pl-1 mt-4">
<Controller
control={control}
name="addMembers"
@@ -415,11 +423,19 @@ export const AppLayout = ({ children }: LayoutProps) => {
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className=""
className="mr-4"
type="submit"
>
Create Project
</Button>
<Button
key="layout-cancel-create-project"
onClick={() => handlePopUpClose('addNewWs')}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>
+41 -76
View File
@@ -35,9 +35,11 @@ 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 { leaveConfirmDefaultMessage } from '@app/const';
import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount';
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
import { useLeaveConfirm } from '@app/hooks';
import addSecrets from '../api/files/AddSecrets';
import deleteSecrets from '../api/files/DeleteSecrets';
@@ -116,7 +118,6 @@ function findDuplicates(arr: any[]) {
export default function Dashboard() {
const [data, setData] = useState<SecretDataProps[] | null>();
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [blurred, setBlurred] = useState(true);
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
@@ -137,6 +138,7 @@ export default function Dashboard() {
const [dropZoneData, setDropZoneData] = useState<SecretDataProps[]>();
const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({initialValue: false});
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -153,36 +155,6 @@ export default function Dashboard() {
setAtSecretsAreaTop(false);
}
};
// #TODO: fix save message for changing reroutes
// const beforeRouteHandler = (url) => {
// const warningText =
// "Do you want to save your results bfore leaving this page?";
// if (!buttonReady) return;
// if (router.asPath !== url && !confirm(warningText)) {
// // router.events.emit('routeChangeError');
// // setData(data)
// savePush();
// throw `Route change to "${url}" was aborted (this error can be safely ignored).`;
// } else {
// setButtonReady(false);
// }
// };
// prompt the user if they try and leave with unsaved changes
useEffect(() => {
const warningText = 'Do you want to save your results before leaving this page?';
const handleWindowClose = (e: any) => {
if (!buttonReady) return;
e.preventDefault();
e.returnValue = warningText;
};
window.addEventListener('beforeunload', handleWindowClose);
// router.events.on('routeChangeStart', beforeRouteHandler);
return () => {
window.removeEventListener('beforeunload', handleWindowClose);
// router.events.off('routeChangeStart', beforeRouteHandler);
};
}, [buttonReady]);
// TODO(akhilmhdh): change to FP
const sortValuesHandler = (
@@ -318,7 +290,7 @@ export default function Dashboard() {
};
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => {
setButtonReady(true);
setHasUnsavedChanges(true);
toggleSidebar('None');
createNotification({
text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`,
@@ -332,27 +304,27 @@ export default function Dashboard() {
const modifyValue = (value: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
setButtonReady(true);
setHasUnsavedChanges(true);
};
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
setButtonReady(true);
setHasUnsavedChanges(true);
};
const modifyKey = (key: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
setButtonReady(true);
setHasUnsavedChanges(true);
};
const modifyComment = (comment: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
setButtonReady(true);
setHasUnsavedChanges(true);
};
const modifyTags = (tags: Tag[], pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
setButtonReady(true);
setHasUnsavedChanges(true);
};
// For speed purposes and better perforamance, we are using useCallback
@@ -422,7 +394,7 @@ export default function Dashboard() {
}
// Once "Save changes" is clicked, disable that button
setButtonReady(false);
setHasUnsavedChanges(false);
const secretsToBeDeleted = initialData!
.filter(
@@ -566,7 +538,7 @@ export default function Dashboard() {
);
return filteredOldData.concat(filteredNewData);
});
setButtonReady(true);
setHasUnsavedChanges(true);
};
const addData = (newData: SecretDataProps[]) => {
@@ -587,6 +559,26 @@ export default function Dashboard() {
deleteRow({ ids, secretName });
};
const handleOnEnvironmentChange = (envName: string) => {
if(hasUnsavedChanges) {
if (!window.confirm(leaveConfirmDefaultMessage)) return;
}
const selectedWorkspaceEnv = workspaceEnvs.find(({ name }: { name: string }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
};
if (selectedWorkspaceEnv) {
if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv);
else setSelectedEnv(selectedWorkspaceEnv);
}
setHasUnsavedChanges(false);
};
return data ? (
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white dark">
<Head>
@@ -655,16 +647,7 @@ export default function Dashboard() {
<ListBox
isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
onChange={handleOnEnvironmentChange}
/>
)}
</div>
@@ -681,14 +664,14 @@ export default function Dashboard() {
icon={faClockRotateLeft}
/>}
</div>
{(data?.length !== 0 || buttonReady) && !snapshotData && (
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
<div className="flex justify-start max-w-sm mt-1">
<Button
text={String(t('common:save-changes'))}
onButtonPressed={savePush}
color="primary"
size="md"
active={buttonReady}
active={hasUnsavedChanges}
iconDisabled={faCheck}
textDisabled={String(t('common:saved'))}
loading={saveLoading}
@@ -723,11 +706,11 @@ export default function Dashboard() {
text: `Rollback has been performed successfully.`,
type: 'success'
});
setButtonReady(false);
setHasUnsavedChanges(false);
}}
color="primary"
size="md"
active={buttonReady}
active={hasUnsavedChanges}
/>
</div>
)}
@@ -742,31 +725,13 @@ export default function Dashboard() {
<ListBox
isSelected={selectedEnv.name}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
onChange={handleOnEnvironmentChange}
/>
) : (
<ListBox
isSelected={selectedSnapshotEnv?.name || ''}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedSnapshotEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
isReadDenied: false
}
)
}
onChange={handleOnEnvironmentChange}
/>
)}
<div className="h-10 w-full bg-mineshaft-700 hover:bg-white/10 ml-2 rounded-md flex flex-row items-center">
@@ -970,7 +935,7 @@ export default function Dashboard() {
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
setButtonReady={setHasUnsavedChanges}
keysExist
numCurrentRows={data.length}
/>
@@ -986,7 +951,7 @@ export default function Dashboard() {
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
setButtonReady={setHasUnsavedChanges}
numCurrentRows={data.length}
keysExist={false}
/>
@@ -1013,7 +978,7 @@ export default function Dashboard() {
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
buttonReady={hasUnsavedChanges}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}
+6
View File
@@ -198,6 +198,9 @@ export default function Integrations() {
case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize`
break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/authorize`
break;
default:
break;
}
@@ -241,6 +244,9 @@ export default function Integrations() {
case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'circleci':
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
@@ -0,0 +1,77 @@
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 CircleCICreateIntegrationPage() {
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: 'circleci',
accessToken: apiKey,
accessId: null,
});
setIsLoading(false);
router.push(
`/integrations/circleci/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'>CircleCI Integration</CardTitle>
<FormControl
label="CircleCI API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== '' ?? false}
>
<Input
placeholder=''
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to CircleCI
</Button>
</Card>
</div>
)
}
CircleCICreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -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,
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 CircleCICreateIntegrationPage() {
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,
path: null,
region: 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'>CircleCI 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="CircleCI 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 />
}
CircleCICreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);