mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 21:27:31 +00:00
First commit of env overview
This commit is contained in:
@@ -34,6 +34,7 @@ const buttonVariants = cva(
|
|||||||
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
||||||
plain: '',
|
plain: '',
|
||||||
selected: '',
|
selected: '',
|
||||||
|
outline_bg: '',
|
||||||
// a constant color not in use on hover or click goes colorSchema color
|
// a constant color not in use on hover or click goes colorSchema color
|
||||||
star: 'text-bunker-200 bg-mineshaft-500'
|
star: 'text-bunker-200 bg-mineshaft-500'
|
||||||
},
|
},
|
||||||
@@ -67,6 +68,11 @@ const buttonVariants = cva(
|
|||||||
variant: 'selected',
|
variant: 'selected',
|
||||||
className: 'bg-primary/10 border border-primary/50 text-bunker-200'
|
className: 'bg-primary/10 border border-primary/50 text-bunker-200'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
colorSchema: 'primary',
|
||||||
|
variant: 'outline_bg',
|
||||||
|
className: 'bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
colorSchema: 'secondary',
|
colorSchema: 'secondary',
|
||||||
variant: 'star',
|
variant: 'star',
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
|||||||
}, [secretId]);
|
}, [secretId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full min-w-40 h-[12.4rem] px-4 mt-4 text-sm text-bunker-300 overflow-x-none">
|
<div className="w-full min-w-40 h-[12.4rem] px-4 mt-4 text-sm text-bunker-300 overflow-x-none dark">
|
||||||
<p className="">{t('dashboard:sidebar.version-history')}</p>
|
<p className="">{t('dashboard:sidebar.version-history')}</p>
|
||||||
<div className="pl-1 py-0.5 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full">
|
<div className="pl-1 py-0.5 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -102,7 +102,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
|||||||
<div className="w-0 h-full border-l border-bunker-300 mt-1" />
|
<div className="w-0 h-full border-l border-bunker-300 mt-1" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]">
|
<div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]">
|
||||||
<div className="pr-2 pt-1 text-bunker-300/90">
|
<div className="pr-2 text-bunker-300/90">
|
||||||
{new Date(version.createdAt).toLocaleDateString('en-US', {
|
{new Date(version.createdAt).toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ import {
|
|||||||
|
|
||||||
export const secretKeys = {
|
export const secretKeys = {
|
||||||
// this is also used in secretSnapshot part
|
// this is also used in secretSnapshot part
|
||||||
getProjectSecret: (workspaceId: string, env: string) => [{ workspaceId, env }, 'secrets'],
|
getProjectSecret: (workspaceId: string, env: string | string[]) => [{ workspaceId, env }, 'secrets'],
|
||||||
getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions']
|
getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions']
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string) => {
|
const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | string[]) => {
|
||||||
|
if (typeof env === 'string') {
|
||||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||||
params: {
|
params: {
|
||||||
environment: env,
|
environment: env,
|
||||||
@@ -31,6 +32,53 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data.secrets;
|
return data.secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof env === 'object') {
|
||||||
|
let allEnvData: any = [];
|
||||||
|
// env.map(async (envPoint: string) => {
|
||||||
|
// const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||||
|
// params: {
|
||||||
|
// environment: envPoint,
|
||||||
|
// workspaceId
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// console.log(111, envPoint, data.secrets)
|
||||||
|
// allEnvData = allEnvData.concat(data.secrets);
|
||||||
|
// // await allEnvData.push(...data.secrets)
|
||||||
|
// console.log(222, allEnvData)
|
||||||
|
// })
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const envPoint of env) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||||
|
params: {
|
||||||
|
environment: envPoint,
|
||||||
|
workspaceId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
allEnvData = allEnvData.concat(data.secrets);
|
||||||
|
}
|
||||||
|
// const { data: data1 } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||||
|
// params: {
|
||||||
|
// environment: env[0],
|
||||||
|
// workspaceId
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// const { data: data2 } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||||
|
// params: {
|
||||||
|
// environment: env[1],
|
||||||
|
// workspaceId
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// allEnvData = data1.secrets.concat(data2.secrets);
|
||||||
|
|
||||||
|
return allEnvData;
|
||||||
|
// eslint-disable-next-line no-else-return
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetProjectSecrets = ({
|
export const useGetProjectSecrets = ({
|
||||||
@@ -45,6 +93,7 @@ export const useGetProjectSecrets = ({
|
|||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env),
|
queryKey: secretKeys.getProjectSecret(workspaceId, env),
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
|
console.log(878787878, data)
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
const latestKey = decryptFileKey;
|
const latestKey = decryptFileKey;
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
@@ -93,12 +142,12 @@ export const useGetProjectSecrets = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (encSecret.type === 'personal') {
|
if (encSecret.type === 'personal') {
|
||||||
personalSecrets[decryptedSecret.key] = { id: encSecret._id, value: secretValue };
|
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { id: encSecret._id, value: secretValue };
|
||||||
} else {
|
} else {
|
||||||
if (!duplicateSecretKey?.[decryptedSecret.key]) {
|
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||||
sharedSecrets.push(decryptedSecret);
|
sharedSecrets.push(decryptedSecret);
|
||||||
}
|
}
|
||||||
duplicateSecretKey[decryptedSecret.key] = true;
|
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
sharedSecrets.forEach((val) => {
|
sharedSecrets.forEach((val) => {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export type BatchSecretDTO = {
|
|||||||
|
|
||||||
export type GetProjectSecretsDTO = {
|
export type GetProjectSecretsDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
env: string;
|
env: string | string[];
|
||||||
decryptFileKey: UserWsKeyPair;
|
decryptFileKey: UserWsKeyPair;
|
||||||
isPaused?: boolean;
|
isPaused?: boolean;
|
||||||
onSuccess?: (data: DecryptedSecret[]) => void;
|
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||||
|
|||||||
@@ -271,11 +271,12 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
{name}
|
{name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
{/* <hr className="mt-1 mb-1 h-px border-0 bg-gray-700" /> */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Button
|
<Button
|
||||||
className="w-full py-2 text-bunker-200 bg-mineshaft-500 hover:bg-primary/90 hover:text-black"
|
className="w-full py-2 text-bunker-200 bg-mineshaft-700"
|
||||||
color="mineshaft"
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handlePopUpOpen('addNewWs')}
|
onClick={() => handlePopUpOpen('addNewWs')}
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import Head from 'next/head';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||||
import { DashboardPage } from '@app/views/DashboardPage';
|
// import { DashboardPage } from '@app/views/DashboardPage';
|
||||||
|
import { DashboardEnvOverview } from '@app/views/DashboardPage/DashboardEnvOverview';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -15,7 +16,8 @@ const Dashboard = () => {
|
|||||||
<meta property="og:title" content={String(t('dashboard:og-title'))} />
|
<meta property="og:title" content={String(t('dashboard:og-title'))} />
|
||||||
<meta name="og:description" content={String(t('dashboard:og-description'))} />
|
<meta name="og:description" content={String(t('dashboard:og-description'))} />
|
||||||
</Head>
|
</Head>
|
||||||
<DashboardPage />
|
{/* <DashboardPage /> */}
|
||||||
|
<DashboardEnvOverview />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { FormProvider, useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
|
||||||
|
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||||
|
import NavHeader from '@app/components/navigation/NavHeader';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
TableContainer
|
||||||
|
} from '@app/components/v2';
|
||||||
|
import { useWorkspace } from '@app/context';
|
||||||
|
import { usePopUp, useToggle } from '@app/hooks';
|
||||||
|
import {
|
||||||
|
useCreateWsTag,
|
||||||
|
useGetProjectSecrets,
|
||||||
|
useGetUserWsEnvironments,
|
||||||
|
useGetUserWsKey,
|
||||||
|
} from '@app/hooks/api';
|
||||||
|
import { WorkspaceEnv } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
import { CreateTagModal } from './components/CreateTagModal';
|
||||||
|
import { EnvComparisonHeader } from './components/EnvComparisonHeader';
|
||||||
|
import { EnvComparisonRow } from './components/EnvComparisonRow';
|
||||||
|
import {
|
||||||
|
DEFAULT_SECRET_VALUE,
|
||||||
|
FormData,
|
||||||
|
schema
|
||||||
|
} from './DashboardPage.utils';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Some imp aspects to consider. Here there are multiple stats changing
|
||||||
|
* Thus ideally we need to use a context. But instead we rely on react hook form
|
||||||
|
* React hook form provides context and high performance proxy based rendering
|
||||||
|
* It also handles error handling and transferring states between inputs
|
||||||
|
*
|
||||||
|
* Another thing is the purpose of overrideAction
|
||||||
|
* Before we would remove the value for personal secret when user toggle and user couldn't get it back
|
||||||
|
* They have to reload the browser or go back all over again
|
||||||
|
* Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving
|
||||||
|
* They will get it back
|
||||||
|
*/
|
||||||
|
export const DashboardEnvOverview = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const { popUp
|
||||||
|
// , handlePopUpOpen
|
||||||
|
, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
'secretDetails',
|
||||||
|
'addTag',
|
||||||
|
'secretSnapshots',
|
||||||
|
'uploadedSecOpts',
|
||||||
|
'compareSecrets'
|
||||||
|
] as const);
|
||||||
|
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
|
||||||
|
const [snapshotId, setSnaphotId] = useState<string | null>(null);
|
||||||
|
console.log(setIsSecretValueHidden, setSnaphotId)
|
||||||
|
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
|
||||||
|
// const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
||||||
|
|
||||||
|
const { currentWorkspace, isLoading } = useWorkspace();
|
||||||
|
const workspaceId = currentWorkspace?._id as string;
|
||||||
|
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !workspaceId && router.isReady) {
|
||||||
|
router.push('/noprojects');
|
||||||
|
}
|
||||||
|
}, [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, isWriteDenied }) => !isWriteDenied || !isReadDenied);
|
||||||
|
if (env) {
|
||||||
|
setSelectedEnv(env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||||
|
workspaceId,
|
||||||
|
env: wsEnv?.map(env => env.slug) ?? [],
|
||||||
|
decryptFileKey: latestFileKey!,
|
||||||
|
isPaused: Boolean(snapshotId)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(333333, secrets, [... new Set(secrets?.secrets.map((secret: any) => secret.key))])
|
||||||
|
|
||||||
|
// mutation calls
|
||||||
|
// const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
|
||||||
|
// const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
|
||||||
|
// const { mutateAsync: registerUserAction } = useRegisterUserAction();
|
||||||
|
const { mutateAsync: createWsTag } = useCreateWsTag();
|
||||||
|
|
||||||
|
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 {
|
||||||
|
control,
|
||||||
|
// handleSubmit,
|
||||||
|
// getValues,
|
||||||
|
// setValue,
|
||||||
|
// formState: { isSubmitting, dirtyFields },
|
||||||
|
// reset
|
||||||
|
} = method;
|
||||||
|
const formSecrets = useWatch({ control, name: 'secrets' });
|
||||||
|
console.log(formSecrets)
|
||||||
|
const { fields, prepend,
|
||||||
|
// append, remove, update
|
||||||
|
} = useFieldArray({ control, name: 'secrets' });
|
||||||
|
console.log(987, fields, secrets?.secrets.map((secret: any) => secret.key))
|
||||||
|
|
||||||
|
const isRollbackMode = Boolean(snapshotId);
|
||||||
|
const isReadOnly = selectedEnv?.isWriteDenied;
|
||||||
|
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
|
||||||
|
// const canDoRollback = !isReadOnly && !isAddOnly;
|
||||||
|
|
||||||
|
|
||||||
|
// const onSortSecrets = () => {
|
||||||
|
// const dir = sortDir === 'asc' ? 'desc' : 'asc';
|
||||||
|
// const sec = getValues('secrets') || [];
|
||||||
|
// const sortedSec = sec.sort((a, b) =>
|
||||||
|
// dir === 'asc' ? a?.key?.localeCompare(b?.key || '') : b?.key?.localeCompare(a?.key || '')
|
||||||
|
// );
|
||||||
|
// setValue('secrets', sortedSec);
|
||||||
|
// setSortDir(dir);
|
||||||
|
// };
|
||||||
|
|
||||||
|
const onCreateWsTag = async (tagName: string) => {
|
||||||
|
try {
|
||||||
|
await createWsTag({
|
||||||
|
workspaceID: workspaceId,
|
||||||
|
tagName,
|
||||||
|
tagSlug: tagName.replace(' ', '_')
|
||||||
|
});
|
||||||
|
handlePopUpClose('addTag');
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully created a tag',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to create a tag',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSecretsLoading || isEnvListLoading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex h-full w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
|
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// when secrets is not loading and secrets list is empty
|
||||||
|
const isDashboardSecretEmpty = !isSecretsLoading && !formSecrets?.length;
|
||||||
|
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty);
|
||||||
|
|
||||||
|
const userAvailableEnvs = wsEnv?.filter(
|
||||||
|
({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
<div className="mt-8 ml-1">
|
||||||
|
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
||||||
|
<p className="text-md text-bunker-300">Put your secrets to work with the <span className="text-primary">Infisical CLI</span></p>
|
||||||
|
</div>
|
||||||
|
<div className={`${isSecretEmpty ? "" : ""} flex flex-row items-start justify-center mt-10 h-[calc(100vh-270px)] overflow-y-scroll overflow-x-hidden no-scrollbar no-scrollbar::-webkit-scrollbar`}>
|
||||||
|
{!isSecretEmpty && (
|
||||||
|
<TableContainer className='border-0'>
|
||||||
|
<table className="secret-table relative bg-bunker-800">
|
||||||
|
<EnvComparisonHeader userAvailableEnvs={userAvailableEnvs} />
|
||||||
|
<tbody className="overflow-y-auto max-h-screen">
|
||||||
|
{[... new Set(secrets?.secrets.map((secret: any) => secret.key))].map((key, index) => (
|
||||||
|
<EnvComparisonRow
|
||||||
|
key={key}
|
||||||
|
secrets={secrets?.secrets.filter(secret => secret.key === key)}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
isAddOnly={isAddOnly}
|
||||||
|
index={index}
|
||||||
|
isSecretValueHidden={isSecretValueHidden}
|
||||||
|
userAvailableEnvs={userAvailableEnvs}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className="group min-w-full flex flex-row items-center border-none mt-4">
|
||||||
|
<td className="w-10 h-10 px-4 flex items-center justify-center border-none"><div className='text-center w-10 text-xs text-transparent'>0</div></td>
|
||||||
|
<td className="border-none">
|
||||||
|
<div className="min-w-[220px] lg:min-w-[240px] xl:min-w-[280px] relative flex items-center justify-end w-full text-transparent">1</div>
|
||||||
|
</td>
|
||||||
|
{userAvailableEnvs?.map(env => {
|
||||||
|
return <>
|
||||||
|
<td className="w-10 px-4 flex items-center justify-center h-10 border-none">
|
||||||
|
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||||
|
</td>
|
||||||
|
<td className="flex flex-row w-full justify-center h-10 items-center border-none">
|
||||||
|
<Button
|
||||||
|
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
|
||||||
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="primary"
|
||||||
|
isFullWidth
|
||||||
|
className="h-10"
|
||||||
|
>
|
||||||
|
Explore {env.name}
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
{/* <div className="ml-10 h-full flex items-start justify-center">
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus}/>}
|
||||||
|
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
|
||||||
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="primary"
|
||||||
|
isFullWidth
|
||||||
|
className="h-10"
|
||||||
|
>
|
||||||
|
Add Environment
|
||||||
|
</Button>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addTag?.isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
handlePopUpToggle('addTag', open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Create tag"
|
||||||
|
subTitle="Specify your tag name, and the slug will be created automatically."
|
||||||
|
>
|
||||||
|
<CreateTagModal onCreateTag={onCreateWsTag} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export const EnvComparisonHeader = ({ userAvailableEnvs }: { userAvailableEnvs?: any[] }): JSX.Element => (
|
||||||
|
<thead>
|
||||||
|
<tr className="absolute flex flex-row sticky top-0 h-12">
|
||||||
|
<td className="w-10 px-4 flex items-center justify-center border-none">
|
||||||
|
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||||
|
</td>
|
||||||
|
<td className="border-none">
|
||||||
|
<div className="min-w-[220px] lg:min-w-[240px] xl:min-w-[280px] relative flex items-center justify-end w-full">
|
||||||
|
<div className="text-sm font-medium text-transparent ">Secret</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{userAvailableEnvs?.map(env => {
|
||||||
|
return <>
|
||||||
|
<td className="w-10 px-4 flex items-center justify-center border-none">
|
||||||
|
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||||
|
</td>
|
||||||
|
<th className="flex flex-row w-full bg-mineshaft-800 border border-mineshaft-600 rounded-t-md items-center"><div className="text-md font-medium w-full text-center">{env.name}</div></th>
|
||||||
|
</>
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { EnvComparisonHeader } from './EnvComparison';
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
|
import { SyntheticEvent, useRef } from 'react';
|
||||||
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import guidGenerator from '@app/components/utilities/randomId';
|
||||||
|
import { Input } from '@app/components/v2';
|
||||||
|
|
||||||
|
import { FormData, SecretActionType } from '../../DashboardPage.utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
index: number;
|
||||||
|
secrets: any[] | undefined;
|
||||||
|
// permission and external state's that decided to hide or show
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
isAddOnly?: boolean;
|
||||||
|
isSecretValueHidden: boolean;
|
||||||
|
userAvailableEnvs?: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const REGEX = /([$]{.*?})/g;
|
||||||
|
|
||||||
|
const DashboardInput = ({ isOverridden, isSecretValueHidden, isAddOnly, isReadOnly, secret, shouldBeBlockedInAddOnly, index }: { isOverridden: boolean, isSecretValueHidden: boolean, isAddOnly?: boolean, isReadOnly?: boolean, secret: any, shouldBeBlockedInAddOnly?: boolean, index: number } ): JSX.Element => {
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||||
|
if (ref.current === null) return;
|
||||||
|
|
||||||
|
ref.current.scrollTop = e.currentTarget.scrollTop;
|
||||||
|
ref.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||||
|
};
|
||||||
|
console.log(33333333, secret)
|
||||||
|
|
||||||
|
return <td className="flex flex-row w-full justify-center h-10 items-center bg-mineshaft-900">
|
||||||
|
<div className="group relative whitespace-pre flex flex-col justify-center w-full">
|
||||||
|
<input
|
||||||
|
// {...register(`secrets.${index}.valueOverride`)}
|
||||||
|
value={(isOverridden ? secret.valueOverride : secret?.value || '-')}
|
||||||
|
onScroll={syncScroll}
|
||||||
|
readOnly={isReadOnly || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)}
|
||||||
|
className={`${
|
||||||
|
isSecretValueHidden
|
||||||
|
? 'text-transparent focus:text-transparent active:text-transparent'
|
||||||
|
: ''
|
||||||
|
} z-10 peer font-mono ph-no-capture bg-transparent caret-transparent text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`${
|
||||||
|
isSecretValueHidden && !isOverridden && secret?.value
|
||||||
|
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400 duration-200'
|
||||||
|
: ''
|
||||||
|
} ${!secret?.value && "text-bunker-400 justify-center"}
|
||||||
|
absolute flex flex-row whitespace-pre font-mono z-0 ${isSecretValueHidden && secret?.value ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||||
|
>
|
||||||
|
{(isOverridden ? secret.valueOverride : secret?.value || '-')?.split('').length === 0 && <span className='text-bunker-400/80 font-sans'>EMPTY</span>}
|
||||||
|
{(isOverridden ? secret.valueOverride : secret?.value || '-')?.split(REGEX).map((word: string) => {
|
||||||
|
if (word.match(REGEX) !== null) {
|
||||||
|
return (
|
||||||
|
<span className="ph-no-capture text-yellow" key={index}>
|
||||||
|
{word.slice(0, 2)}
|
||||||
|
<span className="ph-no-capture text-yellow-200/80">
|
||||||
|
{word.slice(2, word.length - 1)}
|
||||||
|
</span>
|
||||||
|
{word.slice(word.length - 1, word.length) === '}' ? (
|
||||||
|
<span className="ph-no-capture text-yellow">
|
||||||
|
{word.slice(word.length - 1, word.length)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="ph-no-capture text-yellow-400">
|
||||||
|
{word.slice(word.length - 1, word.length)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span key={`${word}_${index + 1}`} className="ph-no-capture">
|
||||||
|
{word}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{(isSecretValueHidden && secret?.value) && (
|
||||||
|
<div className='absolute flex flex-row justify-between items-center z-0 peer pr-2 peer-active:hidden peer-focus:hidden group-hover:bg-white/[0.00] duration-100 h-10 w-full text-bunker-400 text-clip'>
|
||||||
|
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||||
|
{(isOverridden ? secret.valueOverride : secret?.value || '-')?.split('').map(() => (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
key={guidGenerator()}
|
||||||
|
className="text-xxs mr-0.5"
|
||||||
|
icon={faCircle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{(isOverridden ? secret.valueOverride : secret?.value || '-')?.split('').length === 0 && <span className='text-bunker-400/80 text-sm'>EMPTY</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EnvComparisonRow = ({
|
||||||
|
index,
|
||||||
|
secrets,
|
||||||
|
isSecretValueHidden,
|
||||||
|
isReadOnly,
|
||||||
|
isAddOnly,
|
||||||
|
userAvailableEnvs
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const {
|
||||||
|
// register, setValue,
|
||||||
|
control } = useFormContext<FormData>();
|
||||||
|
console.log(1282828822, userAvailableEnvs)
|
||||||
|
|
||||||
|
console.log('index', index)
|
||||||
|
// to get details on a secret
|
||||||
|
const secret = useWatch({ name: `secrets.${index}`, control });
|
||||||
|
|
||||||
|
// when secret is override by personal values
|
||||||
|
const isOverridden =
|
||||||
|
secret.overrideAction === SecretActionType.Created ||
|
||||||
|
secret.overrideAction === SecretActionType.Modified;
|
||||||
|
|
||||||
|
const isCreatedSecret = !secret?._id;
|
||||||
|
const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly;
|
||||||
|
console.log(893892749827097, secrets)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className="group min-w-full flex flex-row items-center">
|
||||||
|
<td className="w-10 h-10 px-4 flex items-center justify-center"><div className='text-center w-10 text-xs text-bunker-400'>{index + 1}</div></td>
|
||||||
|
<td className="border-none">
|
||||||
|
<div className="min-w-[220px] lg:min-w-[240px] xl:min-w-[280px] relative flex items-center justify-end w-full">
|
||||||
|
<Input
|
||||||
|
autoComplete="off"
|
||||||
|
variant="plain"
|
||||||
|
isDisabled={isReadOnly || shouldBeBlockedInAddOnly}
|
||||||
|
className="w-full focus:text-bunker-100 focus:ring-transparent"
|
||||||
|
value={secret.key}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{userAvailableEnvs?.map(env => {
|
||||||
|
return <>
|
||||||
|
<td className="w-10 px-4 flex items-center justify-center h-10">
|
||||||
|
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||||
|
</td>
|
||||||
|
<DashboardInput isOverridden={isOverridden} isSecretValueHidden={isSecretValueHidden} isAddOnly={isAddOnly} isReadOnly={isReadOnly} secret={secrets?.filter(sec => sec.env === env.slug)[0]} shouldBeBlockedInAddOnly={shouldBeBlockedInAddOnly} index={index} />
|
||||||
|
</>
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { EnvComparisonRow } from './EnvComparisonRow';
|
||||||
Reference in New Issue
Block a user