mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'Infisical:main' into cloudflare-pages-integration
This commit is contained in:
BIN
frontend/public/images/integrations/Terraform.png
Normal file
BIN
frontend/public/images/integrations/Terraform.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -17,6 +17,12 @@
|
||||
"image": "Kubernetes",
|
||||
"docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes"
|
||||
},
|
||||
{
|
||||
"name": "Terraform",
|
||||
"slug": "terraform",
|
||||
"image": "Terraform",
|
||||
"docsLink": "https://infisical.com/docs/integrations/frameworks/terraform"
|
||||
},
|
||||
{
|
||||
"name": "React",
|
||||
"slug": "react",
|
||||
|
||||
@@ -4,7 +4,11 @@ import { useRouter } from 'next/router';
|
||||
import { faArrowRight, faCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
// TODO: This needs to be moved from public folder
|
||||
import { contextNetlifyMapping, integrationSlugNameMapping, 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';
|
||||
@@ -27,6 +31,7 @@ interface Integration {
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
interface IntegrationApp {
|
||||
@@ -55,7 +60,6 @@ const IntegrationTile = ({
|
||||
environments = [],
|
||||
handleDeleteIntegration
|
||||
}: Props) => {
|
||||
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||
environments.find(({ slug }) => slug === integration?.environment) || {
|
||||
name: '',
|
||||
@@ -74,16 +78,16 @@ const IntegrationTile = ({
|
||||
});
|
||||
|
||||
setApps(tempApps);
|
||||
|
||||
|
||||
if (integration?.app) {
|
||||
setIntegrationApp(integration.app);
|
||||
} else if (integration?.path && integration?.region) {
|
||||
setIntegrationApp(`${integration.path} (${integration.region})`);
|
||||
} else if (tempApps.length > 0) {
|
||||
setIntegrationApp(tempApps[0].name)
|
||||
} else {
|
||||
setIntegrationApp('');
|
||||
}
|
||||
setIntegrationApp(tempApps[0].name);
|
||||
} else {
|
||||
setIntegrationApp('');
|
||||
}
|
||||
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
@@ -212,12 +216,15 @@ const IntegrationTile = ({
|
||||
return <div />;
|
||||
};
|
||||
|
||||
if (!integrationApp && integration.integration !== "checkly") return <div />;
|
||||
|
||||
const isSelected = integration.integration === 'hashicorp-vault' ? `${integration.app} - path: ${integration.path}` : integrationApp;
|
||||
if (!integrationApp && integration.integration !== 'checkly') return <div />;
|
||||
|
||||
const isSelected =
|
||||
integration.integration === 'hashicorp-vault'
|
||||
? `${integration.app} - path: ${integration.path}`
|
||||
: integrationApp;
|
||||
|
||||
return (
|
||||
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-mineshaft-800 border border-mineshaft-600 p-6">
|
||||
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
|
||||
<div className="flex">
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
|
||||
@@ -235,33 +242,46 @@ const IntegrationTile = ({
|
||||
isFull
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<p className="mb-2 text-xs font-semibold text-gray-400">SECRET PATH</p>
|
||||
<div className="cursor-default 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)} */}
|
||||
{integration.secretPath}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<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>
|
||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300 cursor-default">
|
||||
<p className="mb-2 text-xs font-semibold text-gray-400">INTEGRATION</p>
|
||||
<div className="cursor-default 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)} */}
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
|
||||
{integrationApp ? <div title={integrationApp}>
|
||||
<ListBox
|
||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||
isSelected={isSelected}
|
||||
onChange={(app) => {
|
||||
setIntegrationApp(app);
|
||||
}}
|
||||
/>
|
||||
</div> : <div className='w-52 h-10 rounded-md bg-mineshaft-600 animate-pulse px-4 font-bold py-2'>-</div>}
|
||||
{integrationApp ? (
|
||||
<div title={integrationApp}>
|
||||
<ListBox
|
||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||
isSelected={isSelected}
|
||||
onChange={(app) => {
|
||||
setIntegrationApp(app);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-10 w-52 animate-pulse rounded-md bg-mineshaft-600 px-4 py-2 font-bold">
|
||||
-
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderIntegrationSpecificParams(integration)}
|
||||
</div>
|
||||
<div className="flex items-end cursor-default">
|
||||
<div className="flex cursor-default items-end">
|
||||
{integration.isActive ? (
|
||||
<div className="flex max-w-5xl flex-row items-center rounded-md bg-mineshaft-600 p-[0.44rem] px-4 border border-mineshaft-500">
|
||||
<div className="flex max-w-5xl flex-row items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 p-[0.44rem] px-4">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2.5 text-lg text-primary" />
|
||||
<div className="font-semibold text-gray-300">In Sync</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Integration {
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
const ProjectIntegrationSection = ({
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function InitialLoginStep({
|
||||
{t('login.continue-with-google')}
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full md:px-2 md:py-1 rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={email}
|
||||
@@ -89,7 +89,7 @@ export default function InitialLoginStep({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center w-1/4 lg:w-1/6 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={password}
|
||||
@@ -137,5 +137,11 @@ export default function InitialLoginStep({
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t('login.create-account')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-bunker-400 text-sm flex flex-row">
|
||||
<span className="mr-1">Forgot password?</span>
|
||||
<Link href="/verify-email">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries';
|
||||
export {
|
||||
useCreateFolder,
|
||||
useDeleteFolder,
|
||||
useGetProjectFolders,
|
||||
useGetProjectFoldersBatch,
|
||||
useUpdateFolder
|
||||
} from './queries';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { secretSnapshotKeys } from '../secretSnapshots/queries';
|
||||
import {
|
||||
CreateFolderDTO,
|
||||
DeleteFolderDTO,
|
||||
GetProjectFoldersBatchDTO,
|
||||
GetProjectFoldersDTO,
|
||||
TSecretFolder,
|
||||
UpdateFolderDTO
|
||||
@@ -17,6 +18,26 @@ const queryKeys = {
|
||||
['secret-folders', { workspaceId, environment, parentFolderId }] as const
|
||||
};
|
||||
|
||||
const fetchProjectFolders = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
parentFolderId?: string,
|
||||
parentFolderPath?: string
|
||||
) => {
|
||||
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
|
||||
'/api/v1/folders',
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
parentFolderId,
|
||||
parentFolderPath
|
||||
}
|
||||
}
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetProjectFolders = ({
|
||||
workspaceId,
|
||||
parentFolderId,
|
||||
@@ -27,19 +48,7 @@ export const useGetProjectFolders = ({
|
||||
useQuery({
|
||||
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId),
|
||||
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
|
||||
'/api/v1/folders',
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
parentFolderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
queryFn: async () => fetchProjectFolders(workspaceId, environment, parentFolderId),
|
||||
select: useCallback(
|
||||
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
|
||||
dir,
|
||||
@@ -53,6 +62,25 @@ export const useGetProjectFolders = ({
|
||||
)
|
||||
});
|
||||
|
||||
export const useGetProjectFoldersBatch = ({
|
||||
folders = [],
|
||||
isPaused,
|
||||
parentFolderPath
|
||||
}: GetProjectFoldersBatchDTO) =>
|
||||
useQueries({
|
||||
queries: folders.map(({ workspaceId, environment, parentFolderId }) => ({
|
||||
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderPath),
|
||||
queryFn: async () =>
|
||||
fetchProjectFolders(workspaceId, environment, parentFolderId, parentFolderPath),
|
||||
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
|
||||
select: (data: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
|
||||
environment,
|
||||
folders: data.folders,
|
||||
dir: data.dir
|
||||
})
|
||||
}))
|
||||
});
|
||||
|
||||
export const useCreateFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ export type GetProjectFoldersDTO = {
|
||||
sortDir?: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
export type GetProjectFoldersBatchDTO = {
|
||||
folders: Omit<GetProjectFoldersDTO, 'isPaused' | 'sortDir'>[];
|
||||
isPaused?: boolean;
|
||||
parentFolderPath?: string;
|
||||
};
|
||||
|
||||
export type CreateFolderDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
@@ -29,14 +30,16 @@ export const secretKeys = {
|
||||
const fetchProjectEncryptedSecrets = async (
|
||||
workspaceId: string,
|
||||
env: string | string[],
|
||||
folderId?: string
|
||||
folderId?: string,
|
||||
secretPath?: string
|
||||
) => {
|
||||
if (typeof env === 'string') {
|
||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||
params: {
|
||||
environment: env,
|
||||
workspaceId,
|
||||
folderId: folderId || undefined
|
||||
folderId: folderId || undefined,
|
||||
secretPath
|
||||
}
|
||||
});
|
||||
return data.secrets;
|
||||
@@ -52,7 +55,8 @@ const fetchProjectEncryptedSecrets = async (
|
||||
params: {
|
||||
environment: envPoint,
|
||||
workspaceId,
|
||||
folderId
|
||||
folderId,
|
||||
secretPath
|
||||
}
|
||||
});
|
||||
allEnvData = allEnvData.concat(data.secrets);
|
||||
@@ -77,7 +81,7 @@ export const useGetProjectSecrets = ({
|
||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
||||
select: (data) => {
|
||||
select: useCallback((data: EncryptedSecret[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
@@ -146,21 +150,24 @@ export const useGetProjectSecrets = ({
|
||||
}
|
||||
});
|
||||
return { secrets: sharedSecrets };
|
||||
}
|
||||
}, [decryptFileKey])
|
||||
});
|
||||
|
||||
export const useGetProjectSecretsByKey = ({
|
||||
workspaceId,
|
||||
env,
|
||||
decryptFileKey,
|
||||
isPaused
|
||||
isPaused,
|
||||
folderId,
|
||||
secretPath
|
||||
}: GetProjectSecretsDTO) =>
|
||||
useQuery({
|
||||
// wait for all values to be available
|
||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
|
||||
select: (data) => {
|
||||
// right now secretpath is passed as folderid as only this is used in overview
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
||||
select: useCallback((data: EncryptedSecret[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
@@ -235,7 +242,7 @@ export const useGetProjectSecretsByKey = ({
|
||||
});
|
||||
|
||||
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
||||
}
|
||||
}, [decryptFileKey])
|
||||
});
|
||||
|
||||
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||
@@ -256,7 +263,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
||||
enabled: Boolean(dto.secretId && dto.decryptFileKey),
|
||||
queryKey: secretKeys.getSecretVersion(dto.secretId),
|
||||
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
|
||||
select: (data) => {
|
||||
select: useCallback((data: EncryptedSecretVersion[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
const latestKey = dto.decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
@@ -278,7 +285,7 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
||||
})
|
||||
}))
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
}
|
||||
}, [])
|
||||
});
|
||||
|
||||
export const useBatchSecretsOp = () => {
|
||||
|
||||
@@ -96,6 +96,7 @@ export type GetProjectSecretsDTO = {
|
||||
env: string | string[];
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
folderId?: string;
|
||||
secretPath?: string;
|
||||
isPaused?: boolean;
|
||||
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||
};
|
||||
|
||||
@@ -12,5 +12,5 @@ export type SubscriptionPlan = {
|
||||
tier: number;
|
||||
workspaceLimit: number;
|
||||
workspacesUsed: number;
|
||||
envLimit: number;
|
||||
environmentLimit: number;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
interface Props {
|
||||
integrationAuthId: string;
|
||||
isActive: boolean;
|
||||
secretPath: string;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
sourceEnvironment: string;
|
||||
@@ -20,19 +21,20 @@ interface Props {
|
||||
* @param {String} obj.accessToken - id of integration authorization for which to create the integration
|
||||
* @returns
|
||||
*/
|
||||
const createIntegration = ({
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region
|
||||
const createIntegration = ({
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
secretPath
|
||||
}: Props) =>
|
||||
SecurityClient.fetchCall('/api/v1/integration', {
|
||||
method: 'POST',
|
||||
@@ -40,18 +42,19 @@ const createIntegration = ({
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
secretPath
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
@@ -61,4 +64,4 @@ const createIntegration = ({
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default createIntegration;
|
||||
export default createIntegration;
|
||||
|
||||
@@ -12,13 +12,6 @@ const Dashboard = () => {
|
||||
const queryEnv = router.query.env as string;
|
||||
const isOverviewMode = !queryEnv;
|
||||
|
||||
const onExploreEnv = (slug: string) => {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, env: slug }
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -29,11 +22,7 @@ const Dashboard = () => {
|
||||
<meta name="og:description" content={String(t('dashboard.og-description'))} />
|
||||
</Head>
|
||||
<div className="h-full">
|
||||
{isOverviewMode ? (
|
||||
<DashboardEnvOverview onEnvChange={onExploreEnv} />
|
||||
) : (
|
||||
<DashboardPage envFromTop={queryEnv} />
|
||||
)}
|
||||
{isOverviewMode ? <DashboardEnvOverview /> : <DashboardPage envFromTop={queryEnv} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -44,6 +44,7 @@ interface Integration {
|
||||
integration: string;
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
secretPath:string;
|
||||
integrationAuth: string;
|
||||
}
|
||||
|
||||
@@ -441,7 +442,15 @@ export default function Integrations() {
|
||||
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
<>
|
||||
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">{t('integrations.cloud-integrations')}</h1>
|
||||
<p className="text-base text-gray-400">{t('integrations.click-to-start')}</p>
|
||||
</div>
|
||||
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
|
||||
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem => <div key={elem} className="bg-mineshaft-800 border border-mineshaft-600 animate-pulse h-32 rounded-md"/>)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
|
||||
</div>
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
|
||||
const [path, setPath] = useState('');
|
||||
const [pathErrorText, setPathErrorText] = useState('');
|
||||
@@ -69,9 +70,9 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
const isValidAWSParameterStorePath = (secretPath: string) => {
|
||||
const isValidAWSParameterStorePath = (awsStorePath: string) => {
|
||||
const pattern = /^\/([\w-]+\/)*[\w-]+\/$/;
|
||||
return pattern.test(secretPath) && secretPath.length <= 2048;
|
||||
return pattern.test(awsStorePath) && awsStorePath.length <= 2048;
|
||||
};
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
@@ -101,7 +102,8 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path,
|
||||
region: selectedAWSRegion
|
||||
region: selectedAWSRegion,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -133,6 +135,13 @@ export default function AWSParameterStoreCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="AWS Region">
|
||||
<Select
|
||||
value={selectedAWSRegion}
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [selectedAWSRegion, setSelectedAWSRegion] = useState('');
|
||||
const [targetSecretName, setTargetSecretName] = useState('');
|
||||
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState('');
|
||||
@@ -100,7 +101,8 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: selectedAWSRegion
|
||||
region: selectedAWSRegion,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -132,6 +134,13 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="AWS Region">
|
||||
<Select
|
||||
value={selectedAWSRegion}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function AzureKeyVaultCreateIntegrationPage() {
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [vaultBaseUrl, setVaultBaseUrl] = useState('');
|
||||
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
|
||||
@@ -63,7 +64,8 @@ export default function AzureKeyVaultCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -93,6 +95,13 @@ export default function AzureKeyVaultCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Vault URI"
|
||||
errorText={vaultBaseUrlErrorText}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,8 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
const [targetAppId, setTargetAppId] = useState('');
|
||||
|
||||
@@ -63,7 +73,8 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -80,8 +91,13 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
integrationAuthApps &&
|
||||
targetApp ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gradient-to-tr from-mineshaft-900 to-bunker-900">
|
||||
<Card className="max-w-lg rounded-md p-0 border border-mineshaft-600">
|
||||
<CardTitle className="text-left px-6" subTitle="Choose which environment in Infisical you want to sync with your Checkly account.">Checkly Integration</CardTitle>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
|
||||
<CardTitle
|
||||
className="px-6 text-left"
|
||||
subTitle="Choose which environment in Infisical you want to sync with your Checkly account."
|
||||
>
|
||||
Checkly Integration
|
||||
</CardTitle>
|
||||
<FormControl label="Infisical Project Environment" className="mt-2 px-6">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
@@ -98,6 +114,13 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Checkly Account" className="mt-4 px-6">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,8 @@ export default function CircleCICreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -62,7 +72,8 @@ export default function CircleCICreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -97,6 +108,13 @@ export default function CircleCICreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="CircleCI Project" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,8 @@ export default function FlyioCreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -61,7 +71,8 @@ export default function FlyioCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -96,6 +107,13 @@ export default function FlyioCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Fly.io App" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,7 @@ export default function GitHubCreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [targetAppId, setTargetAppId] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -66,7 +75,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: targetApp.owner,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -100,6 +110,13 @@ export default function GitHubCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="GitHub Repo" className="mt-4">
|
||||
<Select
|
||||
value={targetAppId}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
@@ -44,6 +44,7 @@ export default function GitLabCreateIntegrationPage() {
|
||||
|
||||
const [targetEntity, setTargetEntity] = useState(gitLabEntities[0].value);
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [targetAppId, setTargetAppId] = useState('');
|
||||
const [targetEnvironment, setTargetEnvironment] = useState('');
|
||||
|
||||
@@ -101,7 +102,8 @@ export default function GitLabCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -136,6 +138,13 @@ export default function GitLabCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="GitLab Integration Type">
|
||||
<Select
|
||||
value={targetEntity}
|
||||
@@ -198,13 +207,11 @@ export default function GitLabCreateIntegrationPage() {
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="GitLab Environment Scope (Optional)"
|
||||
>
|
||||
<Input
|
||||
placeholder="*"
|
||||
value={targetEnvironment}
|
||||
onChange={(e) => setTargetEnvironment(e.target.value)}
|
||||
<FormControl label="GitLab Environment Scope (Optional)">
|
||||
<Input
|
||||
placeholder="*"
|
||||
value={targetEnvironment}
|
||||
onChange={(e) => setTargetEnvironment(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
import { Button, Card, CardTitle, FormControl, Input, Select, SelectItem } from '../../../components/v2';
|
||||
|
||||
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';
|
||||
@@ -16,60 +24,58 @@ export default function HashiCorpVaultCreateIntegrationPage() {
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
|
||||
|
||||
const [vaultEnginePath, setVaultEnginePath] = useState('');
|
||||
const [vaultEnginePathErrorText, setVaultEnginePathErrorText ] = useState('');
|
||||
const [vaultEnginePathErrorText, setVaultEnginePathErrorText] = useState('');
|
||||
|
||||
const [vaultSecretPath, setVaultSecretPath] = useState('');
|
||||
const [vaultSecretPathErrorText, setVaultSecretPathErrorText] = useState('');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const isValidVaultPath = (secretPath: string) => {
|
||||
return !(
|
||||
secretPath.length === 0 ||
|
||||
secretPath.startsWith('/') ||
|
||||
secretPath.endsWith('/')
|
||||
);
|
||||
const isValidVaultPath = (vaultPath: string) => {
|
||||
return !(vaultPath.length === 0 || vaultPath.startsWith('/') || vaultPath.endsWith('/'));
|
||||
};
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
if (!integrationAuth?._id) return;
|
||||
if (!integrationAuth?._id) return;
|
||||
|
||||
if (!isValidVaultPath(vaultEnginePath)) {
|
||||
setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv');
|
||||
} else {
|
||||
setVaultEnginePathErrorText('');
|
||||
}
|
||||
|
||||
if (!isValidVaultPath(vaultSecretPath)) {
|
||||
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
|
||||
} else {
|
||||
setVaultSecretPathErrorText('');
|
||||
}
|
||||
|
||||
if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return;
|
||||
if (!isValidVaultPath(vaultEnginePath)) {
|
||||
setVaultEnginePathErrorText('Vault KV Secrets Engine Path must be valid like kv');
|
||||
} else {
|
||||
setVaultEnginePathErrorText('');
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await createIntegration({
|
||||
integrationAuthId: integrationAuth?._id,
|
||||
isActive: true,
|
||||
app: vaultEnginePath,
|
||||
appId: null,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
targetEnvironment: null,
|
||||
targetEnvironmentId: null,
|
||||
targetService: null,
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: vaultSecretPath,
|
||||
region: null
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
if (!isValidVaultPath(vaultSecretPath)) {
|
||||
setVaultSecretPathErrorText('Vault Secret(s) Path must be valid like machine/dev');
|
||||
} else {
|
||||
setVaultSecretPathErrorText('');
|
||||
}
|
||||
|
||||
router.push(`/integrations/${localStorage.getItem('projectData.id')}`);
|
||||
if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await createIntegration({
|
||||
integrationAuthId: integrationAuth?._id,
|
||||
isActive: true,
|
||||
app: vaultEnginePath,
|
||||
appId: null,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
targetEnvironment: null,
|
||||
targetEnvironmentId: null,
|
||||
targetService: null,
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: vaultSecretPath,
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
router.push(`/integrations/${localStorage.getItem('projectData.id')}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
@@ -95,34 +101,41 @@ export default function HashiCorpVaultCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Vault KV Secrets Engine Path"
|
||||
errorText={vaultEnginePathErrorText}
|
||||
isError={vaultEnginePathErrorText !== '' ?? false}
|
||||
>
|
||||
<Input
|
||||
placeholder="kv"
|
||||
value={vaultEnginePath}
|
||||
onChange={(e) => setVaultEnginePath(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="kv"
|
||||
value={vaultEnginePath}
|
||||
onChange={(e) => setVaultEnginePath(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Vault Secret(s) Path"
|
||||
errorText={vaultSecretPathErrorText}
|
||||
isError={vaultSecretPathErrorText !== '' ?? false}
|
||||
>
|
||||
<Input
|
||||
placeholder="machine/dev"
|
||||
value={vaultSecretPath}
|
||||
onChange={(e) => setVaultSecretPath(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="machine/dev"
|
||||
value={vaultSecretPath}
|
||||
onChange={(e) => setVaultSecretPath(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))}
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={!(isValidVaultPath(vaultEnginePath) && isValidVaultPath(vaultSecretPath))}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -23,6 +31,7 @@ export default function HerokuCreateIntegrationPage() {
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -60,7 +69,8 @@ export default function HerokuCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -94,6 +104,13 @@ export default function HerokuCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Heroku App" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -31,7 +39,7 @@ export default function NetlifyCreateIntegrationPage() {
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
const [targetEnvironment, setTargetEnvironment] = useState('');
|
||||
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,7 +79,8 @@ export default function NetlifyCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -106,6 +115,13 @@ export default function NetlifyCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Netlify Site">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
@@ -20,6 +28,7 @@ export default function RailwayCreateIntegrationPage() {
|
||||
const [targetServiceId, setTargetServiceId] = useState('');
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
|
||||
@@ -99,7 +108,8 @@ export default function RailwayCreateIntegrationPage() {
|
||||
targetServiceId: targetService ? targetService.serviceId : null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -134,6 +144,13 @@ export default function RailwayCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Railway Project">
|
||||
<Select
|
||||
value={targetAppId}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -23,7 +31,7 @@ export default function RenderCreateIntegrationPage() {
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,7 +70,8 @@ export default function RenderCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -97,6 +106,13 @@ export default function RenderCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Render Service" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -22,6 +30,7 @@ export default function SupabaseCreateIntegrationPage() {
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -62,7 +71,8 @@ export default function SupabaseCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -97,6 +107,13 @@ export default function SupabaseCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Supabase Project" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -23,7 +31,7 @@ export default function TravisCICreateIntegrationPage() {
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [targetApp, setTargetApp] = useState('');
|
||||
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,7 +70,8 @@ export default function TravisCICreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -97,6 +106,13 @@ export default function TravisCICreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Travis CI Project" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '../../../components/v2';
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
@@ -21,6 +29,7 @@ export default function VercelCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
|
||||
const [secretPath, setSecretPath] = useState('/');
|
||||
const [targetAppId, setTargetAppId] = useState('');
|
||||
const [targetEnvironment, setTargetEnvironment] = useState('');
|
||||
const [targetBranch, setTargetBranch] = useState('');
|
||||
@@ -84,7 +93,8 @@ export default function VercelCreateIntegrationPage() {
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path,
|
||||
region: null
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -119,6 +129,13 @@ export default function VercelCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Vercel App">
|
||||
<Select
|
||||
value={targetAppId}
|
||||
|
||||
@@ -166,9 +166,9 @@ export default function PasswordReset() {
|
||||
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Enter your backup key
|
||||
</p>
|
||||
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
||||
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
|
||||
You can find it in your emrgency kit. You had to download the enrgency kit during signup.
|
||||
<div className="flex flex-row items-center justify-center md:pb-4 mt-4 md:mx-2">
|
||||
<p className="text-sm flex justify-center text-gray-400 w-max max-w-md">
|
||||
You can find it in your emergency kit. You had to download the emergency kit during signup.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
|
||||
@layer utilities {
|
||||
.flex-0 {
|
||||
flex:0;
|
||||
flex: 0;
|
||||
}
|
||||
.flex-2 {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.flex-3 {
|
||||
flex-grow: 3;
|
||||
flex-grow: 3;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.secret-table {
|
||||
@apply bg-mineshaft-800 text-left text-bunker-300 w-full;
|
||||
@apply w-full bg-mineshaft-800 text-left text-bunker-300;
|
||||
}
|
||||
|
||||
/* padding except for comment column */
|
||||
@@ -29,13 +29,45 @@
|
||||
@apply py-1 px-1 pr-2 text-sm;
|
||||
}
|
||||
|
||||
.secret-table th:not(:last-child),.secret-table td:not(:last-child) {
|
||||
.secret-table th:not(:last-child),
|
||||
.secret-table td:not(:last-child) {
|
||||
@apply border-r border-mineshaft-600;
|
||||
}
|
||||
|
||||
.secret-table tr {
|
||||
@apply border-b border-mineshaft-600;
|
||||
}
|
||||
|
||||
.breadcrumb::after,
|
||||
.breadcrumb::before {
|
||||
content: '';
|
||||
height: 60%;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
display: block;
|
||||
position: absolute;
|
||||
@apply bg-mineshaft-800;
|
||||
}
|
||||
|
||||
.breadcrumb:hover::before {
|
||||
@apply bg-mineshaft-600;
|
||||
}
|
||||
|
||||
.breadcrumb:hover::after {
|
||||
@apply bg-mineshaft-600;
|
||||
}
|
||||
|
||||
.breadcrumb::after {
|
||||
left: 5px;
|
||||
bottom: -3px;
|
||||
transform: skew(-30deg);
|
||||
}
|
||||
|
||||
.breadcrumb::before {
|
||||
left: 5px;
|
||||
top: -3px;
|
||||
transform: skew(30deg);
|
||||
}
|
||||
}
|
||||
|
||||
@import '@fontsource/inter/400.css';
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faFolderOpen, faKey, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
import { Button, Input, TableContainer, Tooltip } from '@app/components/v2';
|
||||
import { useWorkspace } from '@app/context';
|
||||
import {
|
||||
useGetProjectFoldersBatch,
|
||||
useGetProjectSecretsByKey,
|
||||
useGetUserWsEnvironments,
|
||||
useGetUserWsKey
|
||||
} from '@app/hooks/api';
|
||||
import { WorkspaceEnv } from '@app/hooks/api/types';
|
||||
|
||||
import { EnvComparisonRow } from './components/EnvComparisonRow';
|
||||
import { FormData, schema } from './DashboardPage.utils';
|
||||
import { FolderComparisonRow } from './components/EnvComparisonRow/FolderComparisonRow';
|
||||
|
||||
export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
||||
export const DashboardEnvOverview = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
|
||||
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id as string;
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const secretPath = router.query?.secretPath as string;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !workspaceId && router.isReady) {
|
||||
@@ -38,14 +34,7 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
||||
}, [isLoading, workspaceId, router.isReady]);
|
||||
|
||||
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
|
||||
workspaceId,
|
||||
onSuccess: (data) => {
|
||||
// get an env with one of the access available
|
||||
const env = data.find(({ isReadDenied }) => !isReadDenied);
|
||||
if (env) {
|
||||
setSelectedEnv(env);
|
||||
}
|
||||
}
|
||||
workspaceId
|
||||
});
|
||||
|
||||
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
|
||||
@@ -54,17 +43,32 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
||||
workspaceId,
|
||||
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
|
||||
decryptFileKey: latestFileKey!,
|
||||
isPaused: false
|
||||
isPaused: false,
|
||||
secretPath
|
||||
});
|
||||
|
||||
const method = useForm<FormData>({
|
||||
// why any: well yup inferred ts expects other keys to defined as undefined
|
||||
defaultValues: secrets as any,
|
||||
values: secrets as any,
|
||||
mode: 'onBlur',
|
||||
resolver: yupResolver(schema)
|
||||
const folders = useGetProjectFoldersBatch({
|
||||
folders:
|
||||
userAvailableEnvs?.map((env) => ({
|
||||
environment: env.slug,
|
||||
workspaceId
|
||||
})) ?? [],
|
||||
parentFolderPath: secretPath
|
||||
});
|
||||
|
||||
const foldersGroupedByEnv = useMemo(() => {
|
||||
const res: Record<string, Record<string, boolean>> = {};
|
||||
folders.forEach(({ data }) => {
|
||||
data?.folders
|
||||
?.filter(({ name }) => name.toLowerCase().includes(searchFilter))
|
||||
?.forEach((folder) => {
|
||||
if (!res?.[folder.name]) res[folder.name] = {};
|
||||
res[folder.name][data.environment] = true;
|
||||
});
|
||||
});
|
||||
return res;
|
||||
}, [folders, userAvailableEnvs, searchFilter]);
|
||||
|
||||
const numSecretsMissingPerEnv = useMemo(() => {
|
||||
// first get all sec in the env then subtract with total to get missing ones
|
||||
const secPerEnvMissing: Record<string, number> = Object.fromEntries(
|
||||
@@ -81,7 +85,43 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
||||
return secPerEnvMissing;
|
||||
}, [secrets, userAvailableEnvs]);
|
||||
|
||||
const isReadOnly = selectedEnv?.isWriteDenied;
|
||||
const onExploreEnv = (slug: string) => {
|
||||
const query: Record<string, string> = { ...router.query, env: slug };
|
||||
delete query.secretPath;
|
||||
// the dir return will have the present directory folder id
|
||||
// use that when clicking on explore to redirect user to there
|
||||
const envFolder = folders.find(({ data }) => slug === data?.environment);
|
||||
const dir = envFolder?.data?.dir?.pop();
|
||||
if (dir) {
|
||||
query.folderId = dir.id;
|
||||
}
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query
|
||||
});
|
||||
};
|
||||
|
||||
const onFolderClick = (path: string) => {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
secretPath: `${router.query?.secretPath || ''}/${path}`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onFolderCrumbClick = (index: number) => {
|
||||
const newSecPath = secretPath.split('/').filter(Boolean).slice(0, index).join('/');
|
||||
const query = { ...router.query, secretPath: `/${newSecPath}` } as Record<string, string>;
|
||||
// root condition
|
||||
if (index === 0) delete query.secretPath;
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query
|
||||
});
|
||||
};
|
||||
|
||||
if (isSecretsLoading || isEnvListLoading) {
|
||||
return (
|
||||
@@ -91,165 +131,195 @@ export const DashboardEnvOverview = ({ onEnvChange }: { onEnvChange: any }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const filteredSecrets = Object.keys(secrets?.secrets || {})?.filter((secret: any) =>
|
||||
secret.toUpperCase().includes(searchFilter.toUpperCase())
|
||||
);
|
||||
// when secrets is not loading and secrets list is empty
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && !Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase()))?.length;
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && !filteredSecrets?.length;
|
||||
const isFoldersEmtpy =
|
||||
!folders.some(({ isLoading: isFolderLoading }) => isFolderLoading) &&
|
||||
!Object.keys(foldersGroupedByEnv).length;
|
||||
const isDashboardEmpty = isFoldersEmtpy && isDashboardSecretEmpty;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<FormProvider {...method}>
|
||||
<form autoComplete="off">
|
||||
{/* breadcrumb row */}
|
||||
<div className="relative right-5">
|
||||
<NavHeader pageName={t('dashboard.title')} isProjectRelated />
|
||||
<div className="relative right-5">
|
||||
<NavHeader pageName={t('dashboard.title')} isProjectRelated />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
||||
<p className="text-md text-bunker-300">
|
||||
Inject your secrets using
|
||||
<a
|
||||
className="mx-1 text-primary/80 hover:text-primary"
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical CLI
|
||||
</a>
|
||||
or
|
||||
<a
|
||||
className="mx-1 text-primary/80 hover:text-primary"
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical SDKs
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-600 py-1 pl-5 pr-2 text-sm"
|
||||
onClick={() => onFolderCrumbClick(0)}
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFolderOpen} className="text-primary" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
||||
<p className="text-md text-bunker-300">
|
||||
Inject your secrets using
|
||||
<a
|
||||
className="mx-1 text-primary/80 hover:text-primary"
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{(secretPath || '')
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.map((path, index, arr) => (
|
||||
<div
|
||||
key={`secret-path-${index + 1}`}
|
||||
className={`breadcrumb relative z-20 ${
|
||||
index + 1 === arr.length ? 'cursor-default' : 'cursor-pointer'
|
||||
} border-solid border-mineshaft-600 py-1 pl-5 pr-2 text-sm text-mineshaft-200`}
|
||||
onClick={() => onFolderCrumbClick(index + 1)}
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
Infisical CLI
|
||||
</a>
|
||||
or
|
||||
<a
|
||||
className="mx-1 text-primary/80 hover:text-primary"
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical SDKs
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute top-[11.1rem] right-6 flex w-full max-w-sm flex-grow space-x-2">
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
placeholder="Search by secret name..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-y-auto">
|
||||
<div className="sticky top-0 mt-8 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
|
||||
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
||||
{path}
|
||||
</div>
|
||||
<div className="sticky top-0 border-none">
|
||||
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="text-sm font-medium ">Secret</div>
|
||||
</div>
|
||||
</div>
|
||||
{numSecretsMissingPerEnv &&
|
||||
userAvailableEnvs?.map((env) => {
|
||||
return (
|
||||
<div
|
||||
key={`header-${env.slug}`}
|
||||
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
|
||||
>
|
||||
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
|
||||
{env.name}
|
||||
{numSecretsMissingPerEnv[env.slug] > 0 && (
|
||||
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100">
|
||||
<Tooltip
|
||||
content={`${
|
||||
numSecretsMissingPerEnv[env.slug]
|
||||
} secrets missing compared to other environments`}
|
||||
>
|
||||
<span className="text-bunker-100">
|
||||
{numSecretsMissingPerEnv[env.slug]}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
<div className="w-80">
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
placeholder="Search by secret/folder name..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto">
|
||||
<div className="sticky top-0 mt-3 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
|
||||
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
||||
</div>
|
||||
<div className="sticky top-0 border-none">
|
||||
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="text-sm font-medium ">Secret</div>
|
||||
</div>
|
||||
</div>
|
||||
{numSecretsMissingPerEnv &&
|
||||
userAvailableEnvs?.map((env) => {
|
||||
return (
|
||||
<div
|
||||
key={`header-${env.slug}`}
|
||||
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
|
||||
>
|
||||
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
|
||||
{env.name}
|
||||
{numSecretsMissingPerEnv[env.slug] > 0 && (
|
||||
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100">
|
||||
<Tooltip
|
||||
content={`${
|
||||
numSecretsMissingPerEnv[env.slug]
|
||||
} secrets missing compared to other environments`}
|
||||
>
|
||||
<span className="text-bunker-100">
|
||||
{numSecretsMissingPerEnv[env.slug]}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
isDashboardSecretEmpty ? '' : ''
|
||||
} no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
|
||||
>
|
||||
{!isDashboardSecretEmpty && (
|
||||
<TableContainer className="border-none">
|
||||
<table className="secret-table relative w-full bg-mineshaft-900">
|
||||
<tbody className="max-h-screen overflow-y-auto">
|
||||
{Object.keys(secrets?.secrets || {})?.filter((secret: any) => secret.toUpperCase().includes(searchFilter.toUpperCase())).map((key, index) => (
|
||||
<EnvComparisonRow
|
||||
key={`row-${key}`}
|
||||
secrets={secrets?.secrets?.[key]}
|
||||
isReadOnly={isReadOnly}
|
||||
index={index}
|
||||
isSecretValueHidden
|
||||
userAvailableEnvs={userAvailableEnvs}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{isDashboardSecretEmpty && (
|
||||
<div className="flex h-40 w-full flex-row rounded-md">
|
||||
<div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faKey} className="text-4xl mb-4" />
|
||||
<span className="mb-1">No secrets found.</span>
|
||||
<span>To add more secrets you can explore any environment.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* In future, we should add an option to add environments here
|
||||
<div className="flex items-start justify-center h-full ml-10">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus}/>}
|
||||
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
isFullWidth
|
||||
className="h-10"
|
||||
>
|
||||
Add Environment
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
|
||||
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-transparent">0</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
isDashboardEmpty ? '' : ''
|
||||
} no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
|
||||
>
|
||||
{!isDashboardEmpty && (
|
||||
<TableContainer className="border-none">
|
||||
<table className="secret-table relative w-full bg-mineshaft-900">
|
||||
<tbody className="max-h-screen overflow-y-auto">
|
||||
{Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => (
|
||||
<FolderComparisonRow
|
||||
key={`${folderName}-${index + 1}`}
|
||||
folderName={folderName}
|
||||
userAvailableEnvs={userAvailableEnvs}
|
||||
folderInEnv={foldersGroupedByEnv[folderName]}
|
||||
onClick={onFolderClick}
|
||||
/>
|
||||
))}
|
||||
{Object.keys(secrets?.secrets || {})
|
||||
?.filter((secret: any) =>
|
||||
secret.toUpperCase().includes(searchFilter.toUpperCase())
|
||||
)
|
||||
.map((key) => (
|
||||
<EnvComparisonRow
|
||||
key={`row-${key}`}
|
||||
secrets={secrets?.secrets?.[key]}
|
||||
isReadOnly
|
||||
isSecretValueHidden
|
||||
userAvailableEnvs={userAvailableEnvs}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{isDashboardEmpty && (
|
||||
<div className="flex h-40 w-full flex-row rounded-md">
|
||||
<div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faKey} className="mb-4 text-4xl" />
|
||||
<span className="mb-1">No secrets/folders found.</span>
|
||||
<span>To add more secrets you can explore any environment.</span>
|
||||
</div>
|
||||
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<span className="text-transparent">0</span>
|
||||
<button type="button" className="mr-2 text-transparent">
|
||||
1
|
||||
</button>
|
||||
</div>
|
||||
{userAvailableEnvs?.map((env) => {
|
||||
return (
|
||||
<div
|
||||
key={`button-${env.slug}`}
|
||||
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
|
||||
>
|
||||
<Button
|
||||
onClick={() => onEnvChange(env.slug)}
|
||||
// router.push(`${router.asPath }?env=${env.slug}`)
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
isFullWidth
|
||||
className="h-10"
|
||||
>
|
||||
Explore {env.name}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
|
||||
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-transparent">0</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<span className="text-transparent">0</span>
|
||||
<button type="button" className="mr-2 text-transparent">
|
||||
1
|
||||
</button>
|
||||
</div>
|
||||
{userAvailableEnvs?.map((env) => {
|
||||
return (
|
||||
<div
|
||||
key={`button-${env.slug}`}
|
||||
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
|
||||
>
|
||||
<Button
|
||||
onClick={() => onExploreEnv(env.slug)}
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
isFullWidth
|
||||
className="h-10"
|
||||
>
|
||||
Explore {env.name}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { useCallback, useState } from 'react';
|
||||
import { faCircle, faEye, faEyeSlash, faMinus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCircle, faEye, faEyeSlash, faKey, faMinus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
type Props = {
|
||||
index: number;
|
||||
secrets: any[] | undefined;
|
||||
// permission and external state's that decided to hide or show
|
||||
isReadOnly?: boolean;
|
||||
@@ -30,7 +29,7 @@ const DashboardInput = ({
|
||||
if (val === undefined)
|
||||
return (
|
||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
||||
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
||||
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
||||
</span>
|
||||
);
|
||||
if (val?.length === 0)
|
||||
@@ -110,7 +109,6 @@ const DashboardInput = ({
|
||||
};
|
||||
|
||||
export const EnvComparisonRow = ({
|
||||
index,
|
||||
secrets,
|
||||
isSecretValueHidden,
|
||||
isReadOnly,
|
||||
@@ -126,7 +124,9 @@ export const EnvComparisonRow = ({
|
||||
return (
|
||||
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
|
||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
||||
<div className="w-10 text-center text-xs text-bunker-400">
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="flex h-8 cursor-default flex-row items-center truncate">
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { faCheck, faFolder, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
type Props = {
|
||||
folderInEnv: Record<string, boolean>;
|
||||
userAvailableEnvs?: Array<{ slug: string; name: string }>;
|
||||
folderName: string;
|
||||
onClick: (folderName: string) => void;
|
||||
};
|
||||
|
||||
export const FolderComparisonRow = ({
|
||||
folderInEnv = {},
|
||||
userAvailableEnvs = [],
|
||||
folderName,
|
||||
onClick
|
||||
}: Props) => (
|
||||
<tr
|
||||
className="group flex min-w-full cursor-pointer flex-row items-center hover:bg-mineshaft-800"
|
||||
onClick={() => onClick(folderName)}
|
||||
>
|
||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-bunker-400">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[200px] xl:min-w-[250px]">
|
||||
<div className="flex h-8 flex-row items-center truncate">{folderName}</div>
|
||||
</td>
|
||||
{userAvailableEnvs?.map(({ slug }) => (
|
||||
<td
|
||||
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
|
||||
folderInEnv[slug]
|
||||
? 'bg-mineshaft-900/30 text-green-500/80'
|
||||
: 'bg-red-800/10 text-red-500/80'
|
||||
}`}
|
||||
key={`${folderName}-${slug}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={folderInEnv[slug] ? faCheck : faXmark} />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
@@ -41,12 +41,11 @@ import {
|
||||
CreateServiceToken,
|
||||
CreateUpdateEnvFormData,
|
||||
CreateWsTag,
|
||||
E2EESection,
|
||||
EnvironmentSection,
|
||||
ProjectIndexSecretsSection,
|
||||
ProjectNameChangeSection,
|
||||
ServiceTokenSection,
|
||||
E2EESection
|
||||
} from './components';
|
||||
ServiceTokenSection} from './components';
|
||||
|
||||
export const ProjectSettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -90,8 +89,8 @@ export const ProjectSettingsPage = () => {
|
||||
|
||||
// get user subscription
|
||||
const { subscription } = useSubscription();
|
||||
const host = window.location.origin;
|
||||
const isEnvServiceAllowed = ((currentWorkspace?.environments || []).length < (subscription?.envLimit || 3) || host !== 'https://app.infisical.com');
|
||||
|
||||
const isEnvServiceAllowed = (subscription?.environmentLimit && currentWorkspace?.environments) ? (currentWorkspace.environments.length < subscription.environmentLimit) : true;
|
||||
|
||||
const onRenameWorkspace = async (name: string) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user