mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Finished the env overview feature
This commit is contained in:
@@ -6,7 +6,7 @@ import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config';
|
||||
|
||||
export const initPostHog = () => {
|
||||
// @ts-ignore
|
||||
console.log("Init Infisical")
|
||||
console.log("Hi there 👋")
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,52 +1,89 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { useOrganization, useWorkspace } from '@app/context';
|
||||
|
||||
import { Select, SelectItem, Tooltip } from '../v2';
|
||||
|
||||
// TODO: make links clickable and clean up
|
||||
|
||||
/**
|
||||
* This is the component at the top of almost every page.
|
||||
* It shows how to navigate to a certain page.
|
||||
* It future these links should also be clickable and hoverable
|
||||
* @param obj
|
||||
* @param obj.pageName - Name of the page
|
||||
* @param obj.isProjectRelated - whether or not this page is related to project (determine if it's 2 or 3 navigation steps)
|
||||
* @param obj.isOrganizationRelated - whether or not this page is related to organization (determine if it's 2 or 3 navigation steps)
|
||||
* @param {object} obj
|
||||
* @param {string} obj.pageName - Name of the page
|
||||
* @param {boolean} obj.isProjectRelated - whether or not this page is related to project (determine if it's 2 or 3 navigation steps)
|
||||
* @param {boolean} obj.isOrganizationRelated - whether or not this page is related to organization (determine if it's 2 or 3 navigation steps)
|
||||
* @param {string} obj.currentEnv - current environment inside a project
|
||||
* @param {string} obj.userAvailableEnvs - environments that are available to a user in this project (used for the dropdown)
|
||||
* @param {string} obj.onEnvChange - the action that happens when an env is changed
|
||||
* @returns
|
||||
*/
|
||||
export default function NavHeader({
|
||||
pageName,
|
||||
isProjectRelated,
|
||||
isOrganizationRelated
|
||||
isOrganizationRelated,
|
||||
currentEnv,
|
||||
userAvailableEnvs,
|
||||
onEnvChange
|
||||
}: {
|
||||
pageName: string;
|
||||
isProjectRelated?: boolean;
|
||||
isOrganizationRelated?: boolean;
|
||||
currentEnv?: string;
|
||||
userAvailableEnvs?: any[];
|
||||
onEnvChange?: (slug: string) => void;
|
||||
}): JSX.Element {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className="ml-6 flex flex-row items-center pt-8">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-primary">{currentOrg?.name}</div>
|
||||
<div className="text-sm font-semibold text-bunker-300">{currentOrg?.name}</div>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm font-semibold text-primary">{currentWorkspace?.name}</div>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
|
||||
<div className="text-sm font-semibold text-bunker-300">{currentWorkspace?.name}</div>
|
||||
</>
|
||||
)}
|
||||
{isOrganizationRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm font-semibold text-primary">Organization Settings</div>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
|
||||
<div className="text-sm font-semibold text-bunker-300">Organization Settings</div>
|
||||
</>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm text-gray-400">{pageName}</div>
|
||||
{pageName === 'Secrets'
|
||||
? <a className="text-sm font-semibold text-primary/80 hover:text-primary" href={`${router.asPath.split("?")[0]}`}>{pageName}</a>
|
||||
: <div className="text-sm text-gray-400">{pageName}</div>}
|
||||
{currentEnv &&
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||
<div className='pl-3 rounded-md hover:bg-bunker-100/10'>
|
||||
<Tooltip content="Select environment">
|
||||
<Select
|
||||
value={userAvailableEnvs?.filter(uae => uae.name === currentEnv)[0]?.slug}
|
||||
onValueChange={(value) => {
|
||||
if (value && onEnvChange) onEnvChange(value);
|
||||
}}
|
||||
className="text-sm pl-0 font-medium text-primary/80 hover:text-primary bg-transparent"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
||||
>
|
||||
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { forwardRef, ReactNode } from 'react';
|
||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faCheck, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCaretDown, faCheck, faChevronUp } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -49,7 +49,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
</SelectPrimitive.Value>
|
||||
{!isDisabled && (
|
||||
<SelectPrimitive.Icon className="ml-3">
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||
<FontAwesomeIcon icon={faCaretDown} size="sm" />
|
||||
</SelectPrimitive.Icon>
|
||||
)}
|
||||
</SelectPrimitive.Trigger>
|
||||
@@ -76,7 +76,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
)}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectPrimitive.ScrollDownButton>
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||
<FontAwesomeIcon icon={faCaretDown} size="xs" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
|
||||
@@ -34,7 +34,7 @@ export type TableProps = {
|
||||
export const Table = ({ children, className }: TableProps): JSX.Element => (
|
||||
<table
|
||||
className={twMerge(
|
||||
'w-full rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
|
||||
'w-full rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -36,18 +36,7 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s
|
||||
|
||||
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
|
||||
@@ -59,19 +48,6 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s
|
||||
});
|
||||
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
|
||||
@@ -93,7 +69,6 @@ export const useGetProjectSecrets = ({
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
|
||||
select: (data) => {
|
||||
console.log(878787878, data)
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
@@ -108,7 +83,7 @@ export const useGetProjectSecrets = ({
|
||||
// this used for add-only mode in dashboard
|
||||
// type won't be there thus only one key is shown
|
||||
const duplicateSecretKey: Record<string, boolean> = {};
|
||||
data.forEach((encSecret) => {
|
||||
data.forEach((encSecret: EncryptedSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import queryString from 'query-string';
|
||||
|
||||
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 { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const env = queryString.parse(router.asPath.split('?')[1])?.env;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -16,8 +22,9 @@ const Dashboard = () => {
|
||||
<meta property="og:title" content={String(t('dashboard:og-title'))} />
|
||||
<meta name="og:description" content={String(t('dashboard:og-description'))} />
|
||||
</Head>
|
||||
{/* <DashboardPage /> */}
|
||||
<DashboardEnvOverview />
|
||||
{env
|
||||
? <DashboardPage envFromTop={String(env)}/>
|
||||
: <DashboardEnvOverview />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
TableContainer
|
||||
TableContainer,
|
||||
Tooltip
|
||||
} from '@app/components/v2';
|
||||
import { useWorkspace } from '@app/context';
|
||||
import { usePopUp, useToggle } from '@app/hooks';
|
||||
import { usePopUp } from '@app/hooks';
|
||||
import {
|
||||
useCreateWsTag,
|
||||
useGetProjectSecrets,
|
||||
@@ -23,26 +24,13 @@ import {
|
||||
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();
|
||||
@@ -57,11 +45,7 @@ export const DashboardEnvOverview = () => {
|
||||
'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;
|
||||
@@ -77,26 +61,25 @@ export const DashboardEnvOverview = () => {
|
||||
workspaceId,
|
||||
onSuccess: (data) => {
|
||||
// get an env with one of the access available
|
||||
const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
|
||||
const env = data.find(({ isReadDenied }) => !isReadDenied);
|
||||
if (env) {
|
||||
setSelectedEnv(env);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const userAvailableEnvs = wsEnv?.filter(
|
||||
({ isReadDenied }) => !isReadDenied
|
||||
);
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
env: wsEnv?.map(env => env.slug) ?? [],
|
||||
env: userAvailableEnvs?.map(env => env.slug) ?? [],
|
||||
decryptFileKey: latestFileKey!,
|
||||
isPaused: Boolean(snapshotId)
|
||||
isPaused: false
|
||||
});
|
||||
|
||||
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>({
|
||||
@@ -116,27 +99,8 @@ export const DashboardEnvOverview = () => {
|
||||
// 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 {
|
||||
@@ -169,11 +133,8 @@ export const DashboardEnvOverview = () => {
|
||||
|
||||
// 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
|
||||
);
|
||||
const numSecretsMissingPerEnv = userAvailableEnvs?.map(envir => ({[envir.slug]: [... new Set(secrets?.secrets.map((secret: any) => secret.key))].length - [... new Set(secrets?.secrets.filter(s => s.env === envir.slug).map((secret: any) => secret.key))].length})).reduce((acc, cur) => ({ ...acc, ...cur }), {})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
@@ -183,71 +144,116 @@ export const DashboardEnvOverview = () => {
|
||||
<div className="relative right-5">
|
||||
<NavHeader pageName={t('dashboard:title')} isProjectRelated />
|
||||
</div>
|
||||
<div className="mt-8 ml-1">
|
||||
<div className="mt-6 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"
|
||||
<p className="text-md text-bunker-300">Inject your secrets using
|
||||
<a
|
||||
className="text-primary/80 hover:text-primary mx-1"
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Add Environment
|
||||
</Button>
|
||||
</div> */}
|
||||
Infisical CLI
|
||||
</a>
|
||||
or
|
||||
<a
|
||||
className="text-primary/80 hover:text-primary mx-1"
|
||||
href="https://infisical.com/docs/sdks/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Infisical SDKs
|
||||
</a> </p>
|
||||
</div>
|
||||
<div className="overflow-y-auto">
|
||||
<div className="sticky top-0 absolute flex flex-row h-10 bg-mineshaft-800 border border-mineshaft-600 rounded-md mt-8 min-w-[60.3rem]">
|
||||
<div className="sticky top-0 w-10 px-4 flex items-center justify-center border-none">
|
||||
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||
</div>
|
||||
<div className="sticky top-0 border-none">
|
||||
<div className="min-w-[200px] lg:min-w-[220px] xl:min-w-[250px] relative flex items-center justify-start h-full w-full">
|
||||
<div className="text-sm font-medium ">Secret</div>
|
||||
</div>
|
||||
</div>
|
||||
{numSecretsMissingPerEnv && userAvailableEnvs?.map(env => {
|
||||
return <div key={`header-${env.slug}`} className="flex flex-row w-full bg-mineshaft-800 rounded-md items-center border-none min-w-[11rem]">
|
||||
<div className="text-sm font-medium w-full text-center text-bunker-200/[.99] flex flex-row justify-center">
|
||||
{env.name}
|
||||
{numSecretsMissingPerEnv[env.slug] > 0 && <div className="bg-red rounded-sm h-[1.1rem] w-[1.1rem] mt-0.5 text-bunker-100 ml-2.5 text-xs border border-red-400 flex items-center justify-center cursor-default">
|
||||
<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>
|
||||
<div className={`${isDashboardSecretEmpty ? "" : ""} flex flex-row items-start justify-center mt-3 h-full max-h-[calc(100vh-370px)] min-w-[60.3rem] flex-grow w-full overflow-x-hidden no-scrollbar no-scrollbar::-webkit-scrollbar`}>
|
||||
{!isDashboardSecretEmpty && (
|
||||
<TableContainer className='border-none'>
|
||||
<table className="relative secret-table w-full relative bg-bunker-800">
|
||||
<tbody className="overflow-y-auto max-h-screen">
|
||||
{[... new Set(secrets?.secrets.map((secret: any) => secret.key))].map((key, index) => (
|
||||
<EnvComparisonRow
|
||||
key={`row-${key}`}
|
||||
secrets={secrets?.secrets.filter(secret => secret.key === key)}
|
||||
isReadOnly={isReadOnly}
|
||||
index={index}
|
||||
isSecretValueHidden
|
||||
userAvailableEnvs={userAvailableEnvs}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{isDashboardSecretEmpty &&
|
||||
<div className='flex flex-row h-40 rounded-md mt-1 w-full'>
|
||||
<div className="sticky top-0 w-10 px-4 flex items-center justify-center border-none">
|
||||
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||
</div>
|
||||
<div className="sticky top-0 border-none">
|
||||
<div className="min-w-[200px] lg:min-w-[220px] xl:min-w-[250px] relative flex items-center justify-start h-full w-full">
|
||||
<div className="text-sm font-medium text-transparent">Secret</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full bg-mineshaft-800 text-bunker-300 rounded-md items-center justify-center border-none mx-2 min-w-[11rem]">
|
||||
<span className="mb-1">No secrets are available in this project yet.</span>
|
||||
<span>You can go into any environment to add secrets there.</span>
|
||||
</div>
|
||||
</div>}
|
||||
{/* In future, we should add an option to add environments here
|
||||
<div className="ml-10 h-full flex items-start justify-center">
|
||||
<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 min-w-full flex flex-row items-center mt-4">
|
||||
<div 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></div>
|
||||
<div className="flex flex-row justify-between items-center min-w-[200px] 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="flex flex-row w-full justify-center h-10 items-center border-none mb-1 mx-2 min-w-[10rem]">
|
||||
<Button
|
||||
onClick={() => router.push(`${router.asPath }?env=${env.slug}`)}
|
||||
variant="outline_bg"
|
||||
colorSchema="primary"
|
||||
isFullWidth
|
||||
className="h-10"
|
||||
>
|
||||
Explore {env.name}
|
||||
</Button>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Modal
|
||||
|
||||
@@ -28,8 +28,6 @@ import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Select,
|
||||
SelectItem,
|
||||
TableContainer,
|
||||
Tag,
|
||||
Tooltip
|
||||
@@ -86,7 +84,7 @@ const USER_ACTION_PUSH = 'first_time_secrets_pushed';
|
||||
* 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 DashboardPage = () => {
|
||||
export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
@@ -126,8 +124,8 @@ export const DashboardPage = () => {
|
||||
onSuccess: (data) => {
|
||||
// get an env with one of the access available
|
||||
const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
|
||||
if (env) {
|
||||
setSelectedEnv(env);
|
||||
if (env && data?.map(wsenv => wsenv.slug).includes(envFromTop)) {
|
||||
setSelectedEnv(data?.filter(dp => dp.slug === envFromTop)[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -357,6 +355,7 @@ export const DashboardPage = () => {
|
||||
}
|
||||
const env = wsEnv?.find((el) => el.slug === slug);
|
||||
if (env) setSelectedEnv(env);
|
||||
router.push(`${router.asPath.split("?")[0]}?env=${slug}`)
|
||||
};
|
||||
|
||||
// record all deleted ids
|
||||
@@ -417,7 +416,13 @@ export const DashboardPage = () => {
|
||||
<form autoComplete="off">
|
||||
{/* breadcrumb row */}
|
||||
<div className="relative right-5">
|
||||
<NavHeader pageName={t('dashboard:title')} isProjectRelated />
|
||||
<NavHeader
|
||||
pageName={t('dashboard:title')}
|
||||
currentEnv={userAvailableEnvs?.filter(envir => envir.slug === envFromTop)[0].name || ''}
|
||||
isProjectRelated
|
||||
userAvailableEnvs={userAvailableEnvs}
|
||||
onEnvChange={onEnvChange}
|
||||
/>
|
||||
</div>
|
||||
{/* Secrets, commit and save button section */}
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
@@ -468,23 +473,6 @@ export const DashboardPage = () => {
|
||||
</div>
|
||||
{/* Environment, search and other action row */}
|
||||
<div className="mt-4 flex items-center space-x-2">
|
||||
<div>
|
||||
<Tooltip content="Select environment">
|
||||
<Select
|
||||
value={selectedEnv?.slug}
|
||||
onValueChange={onEnvChange}
|
||||
position="popper"
|
||||
className="min-w-[180px] bg-mineshaft-600 h-10 font-medium"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
||||
>
|
||||
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Input
|
||||
className="bg-mineshaft-600 h-[2.3rem] placeholder-mineshaft-50"
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export { EnvComparisonHeader } from './EnvComparison';
|
||||
@@ -1,27 +1,25 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { SyntheticEvent, useRef } from 'react';
|
||||
import { SyntheticEvent, useRef, useState } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCircle, faEye, faEyeSlash } 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';
|
||||
import { FormData } 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 DashboardInput = ({ isOverridden, isSecretValueHidden, isReadOnly, secret, index }: { isOverridden: boolean, isSecretValueHidden: boolean, isReadOnly?: boolean, secret: any, index: number } ): JSX.Element => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
if (ref.current === null) return;
|
||||
@@ -29,20 +27,19 @@ const DashboardInput = ({ isOverridden, isSecretValueHidden, isAddOnly, isReadOn
|
||||
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">
|
||||
return <td key={`row-${secret?.key || ''}--`} className={`flex flex-row w-full min-w-[11rem] justify-center h-10 items-center ${!(secret?.value || secret?.value === '') ? "bg-red-400/10" : "bg-mineshaft-900/30"}`}>
|
||||
<div className="group relative whitespace-pre flex flex-col justify-center w-full">
|
||||
<input
|
||||
// {...register(`secrets.${index}.valueOverride`)}
|
||||
value={(isOverridden ? secret.valueOverride : secret?.value || '-')}
|
||||
defaultValue={(isOverridden ? secret.valueOverride : secret?.value || '')}
|
||||
onScroll={syncScroll}
|
||||
readOnly={isReadOnly || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)}
|
||||
readOnly={isReadOnly}
|
||||
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`}
|
||||
} z-10 peer font-mono ph-no-capture bg-transparent caret-transparent text-transparent text-sm px-2 py-2 w-full outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
@@ -54,11 +51,11 @@ const DashboardInput = ({ isOverridden, isSecretValueHidden, isAddOnly, isReadOn
|
||||
} ${!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) => {
|
||||
{(secret?.value || secret?.value === '') && (isOverridden ? secret.valueOverride : secret?.value)?.split('').length === 0 && <span className='text-bunker-400/80 font-sans w-full'>EMPTY</span>}
|
||||
{(secret?.value || secret?.value === '') && (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}>
|
||||
<span className="ph-no-capture text-yellow" key={word}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">
|
||||
{word.slice(2, word.length - 1)}
|
||||
@@ -81,18 +78,19 @@ const DashboardInput = ({ isOverridden, isSecretValueHidden, isAddOnly, isReadOn
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{!(secret?.value || secret?.value === '') && <span className='text-red-500/80 font-sans text-xs italic'>missing</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(() => (
|
||||
{(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>}
|
||||
{(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').length === 0 && <span className='text-bunker-400/80 text-sm'>EMPTY</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -105,48 +103,28 @@ export const EnvComparisonRow = ({
|
||||
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)
|
||||
const [areValuesHiddenThisRow, setAreValuesHiddenThisRow] = useState(true);
|
||||
|
||||
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>
|
||||
<tr className="group min-w-full flex flex-row items-center hover:bg-bunker-700">
|
||||
<td className="w-10 h-10 px-4 flex items-center justify-center border-none"><div className='text-center w-10 text-xs text-bunker-400'>{index + 1}</div></td>
|
||||
<td className="flex flex-row justify-between items-center h-full min-w-[200px] lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="flex flex-row items-center h-8">{secret?.key || ''}</div>
|
||||
<button type="button" className='mr-2 text-bunker-400 hover:text-bunker-300 invisible group-hover:visible' onClick={() => setAreValuesHiddenThisRow(!areValuesHiddenThisRow)}>
|
||||
<FontAwesomeIcon icon={areValuesHiddenThisRow ? faEye : faEyeSlash} />
|
||||
</button>
|
||||
</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} />
|
||||
</>
|
||||
return <DashboardInput key={`row-${secret?.key || ''}-${env.slug}`} isOverridden={false} isSecretValueHidden={areValuesHiddenThisRow && isSecretValueHidden} isReadOnly={isReadOnly} secret={secrets?.filter(sec => sec.env === env.slug)[0]} index={index} />
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -156,7 +156,7 @@ export const SecretInputRow = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="group min-w-full flex flex-row items-center">
|
||||
<tr className="group min-w-full flex flex-row items-center" key={index}>
|
||||
<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>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -279,7 +279,7 @@ export const SecretInputRow = ({
|
||||
{(isOverridden ? secret.valueOverride : secret.value)?.split(REGEX).map((word) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={index}>
|
||||
<span className="ph-no-capture text-yellow" key={guidGenerator()}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">
|
||||
{word.slice(2, word.length - 1)}
|
||||
|
||||
@@ -64,7 +64,6 @@ export const SecretTagsSection = ({
|
||||
});
|
||||
|
||||
const onFormSubmit = async (data: CreateWsTag) => {
|
||||
console.log(19191, data);
|
||||
await onCreateTag(data);
|
||||
handlePopUpClose('CreateSecretTag');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user