diff --git a/.env.example b/.env.example index 79fca5686..df316753e 100644 --- a/.env.example +++ b/.env.example @@ -31,12 +31,12 @@ MONGO_PASSWORD=example SITE_URL=http://localhost:8080 # Mail/SMTP -SMTP_HOST= # required -SMTP_USERNAME= # required -SMTP_PASSWORD= # required +SMTP_HOST= +SMTP_USERNAME= +SMTP_PASSWORD= SMTP_PORT=587 SMTP_SECURE=false -SMTP_FROM_ADDRESS= # required +SMTP_FROM_ADDRESS= SMTP_FROM_NAME=Infisical # Integration @@ -66,4 +66,4 @@ STRIPE_WEBHOOK_SECRET= STRIPE_PRODUCT_STARTER= STRIPE_PRODUCT_TEAM= STRIPE_PRODUCT_PRO= -NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= \ No newline at end of file +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= diff --git a/README.md b/README.md index 1e3b977b1..32a3ce732 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ - + diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 6b3d24dfb..328749472 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -75,8 +75,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => { const secret = crypto.randomBytes(16).toString('hex'); const secretHash = await bcrypt.hash(secret, getSaltRounds()); - const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + let expiresAt; + if (!!expiresIn) { + expiresAt = new Date() + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + } let user, serviceAccount; diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts index 29208a795..b6f3e4341 100644 --- a/frontend/src/components/analytics/posthog.ts +++ b/frontend/src/components/analytics/posthog.ts @@ -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 diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index e1ee09080..c8af1ce46 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -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 ( {currentOrg?.name?.charAt(0)} - {currentOrg?.name} + {currentOrg?.name} {isProjectRelated && ( <> - - {currentWorkspace?.name} + + {currentWorkspace?.name} > )} {isOrganizationRelated && ( <> - - Organization Settings + + Organization Settings > )} - {pageName} + {pageName === 'Secrets' + ? {pageName} + : {pageName}} + {currentEnv && + <> + + + + 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 }) => ( + + {name} + + ))} + + + + >} ); } diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx index c4a89023c..17ae7b867 100644 --- a/frontend/src/components/v2/Button/Button.tsx +++ b/frontend/src/components/v2/Button/Button.tsx @@ -34,6 +34,7 @@ const buttonVariants = cva( outline: ['bg-transparent', 'border-2', 'border-solid'], plain: '', selected: '', + outline_bg: '', // a constant color not in use on hover or click goes colorSchema color star: 'text-bunker-200 bg-mineshaft-500' }, @@ -67,6 +68,11 @@ const buttonVariants = cva( variant: 'selected', 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', variant: 'star', diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index ffeaf3c3f..46cd3fb03 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -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( {!isDisabled && ( - + )} @@ -76,7 +76,7 @@ export const Select = forwardRef( )} - + diff --git a/frontend/src/ee/components/SecretVersionList.tsx b/frontend/src/ee/components/SecretVersionList.tsx index 3f16a228d..dbf7c5bf2 100644 --- a/frontend/src/ee/components/SecretVersionList.tsx +++ b/frontend/src/ee/components/SecretVersionList.tsx @@ -76,7 +76,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => { }, [secretId]); return ( - + {t('dashboard:sidebar.version-history')} {isLoading ? ( @@ -102,7 +102,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => { - + {new Date(version.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 9b9c49df5..031c661d1 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -19,18 +19,42 @@ import { export const secretKeys = { // 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'] }; -const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string) => { - const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { - params: { - environment: env, - workspaceId +const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | string[]) => { + if (typeof env === 'string') { + const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { + params: { + environment: env, + workspaceId + } + }); + return data.secrets; + } + + if (typeof env === 'object') { + let allEnvData: any = []; + + // 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); } - }); - return data.secrets; + + return allEnvData; + // eslint-disable-next-line no-else-return + } else { + return null; + } + }; export const useGetProjectSecrets = ({ @@ -59,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 = {}; - data.forEach((encSecret) => { + data.forEach((encSecret: EncryptedSecret) => { const secretKey = decryptSymmetric({ ciphertext: encSecret.secretKeyCiphertext, iv: encSecret.secretKeyIV, @@ -93,12 +117,12 @@ export const useGetProjectSecrets = ({ }; if (encSecret.type === 'personal') { - personalSecrets[decryptedSecret.key] = { id: encSecret._id, value: secretValue }; + personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { id: encSecret._id, value: secretValue }; } else { - if (!duplicateSecretKey?.[decryptedSecret.key]) { + if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) { sharedSecrets.push(decryptedSecret); } - duplicateSecretKey[decryptedSecret.key] = true; + duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true; } }); sharedSecrets.forEach((val) => { diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 7888e64b3..567fecc9a 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -90,7 +90,7 @@ export type BatchSecretDTO = { export type GetProjectSecretsDTO = { workspaceId: string; - env: string; + env: string | string[]; decryptFileKey: UserWsKeyPair; isPaused?: boolean; onSuccess?: (data: DecryptedSecret[]) => void; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 93ed5b860..85828c3c5 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -20,6 +20,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { yupResolver } from '@hookform/resolvers/yup'; +import queryString from 'query-string'; import * as yup from 'yup'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; @@ -158,7 +159,10 @@ export const AppLayout = ({ children }: LayoutProps) => { .map((workspace: { _id: string }) => workspace._id) .includes(intendedWorkspaceId) ) { - router.push(`/dashboard/${userWorkspaces[0]._id}`); + const { env } = queryString.parse(router.asPath.split('?')[1]); + if (!env) { + router.push(`/dashboard/${userWorkspaces[0]._id}`); + } } else { setWorkspaceMapping( Object.fromEntries( @@ -271,11 +275,12 @@ export const AppLayout = ({ children }: LayoutProps) => { {name} ))} - + {/* */} handlePopUpOpen('addNewWs')} leftIcon={} diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx index 3b51d729b..8aa480c81 100644 --- a/frontend/src/pages/dashboard/[id].tsx +++ b/frontend/src/pages/dashboard/[id].tsx @@ -19,9 +19,9 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tag } from 'public/data/frequentInterfaces'; +import queryString from 'query-string'; import Button from '@app/components/basic/buttons/Button'; -import ListBox from '@app/components/basic/Listbox'; import BottonRightPopup from '@app/components/basic/popups/BottomRightPopup'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; import ConfirmEnvOverwriteModal from '@app/components/dashboard/ConfirmEnvOverwriteModal'; @@ -41,6 +41,8 @@ import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercet import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback'; import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar'; import { useLeaveConfirm } from '@app/hooks'; +// import { DashboardPage } from '@app/views/DashboardPage'; +import { DashboardEnvOverview } from '@app/views/DashboardPage/DashboardEnvOverview'; // import addSecrets from '../api/files/AddSecrets'; // import deleteSecrets from '../api/files/DeleteSecrets'; @@ -147,7 +149,6 @@ function findDuplicates(arr: any[]) { export default function Dashboard() { const [data, setData] = useState(); const [initialData, setInitialData] = useState([]); - const router = useRouter(); const [blurred, setBlurred] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true); const [isNew, setIsNew] = useState(false); @@ -169,7 +170,10 @@ export default function Dashboard() { const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); const { t } = useTranslation(); + const { createNotification } = useNotificationContext(); + const router = useRouter(); + const envInURL = queryString.parse(router.asPath.split('?')[1])?.env; const workspaceId = router.query.id as string; const [workspaceEnvs, setWorkspaceEnvs] = useState([]); @@ -783,13 +787,13 @@ export default function Dashboard() { deleteRow({ ids, secretName }); }; - const handleOnEnvironmentChange = (envName: string) => { + const handleOnEnvironmentChange = (envSlug: string) => { if (hasUnsavedChanges) { if (!window.confirm(leaveConfirmDefaultMessage)) return; } const selectedWorkspaceEnv = workspaceEnvs.find( - ({ name }: { name: string }) => envName === name + ({ slug }: { slug: string }) => envSlug === slug ) || { name: 'unknown', slug: 'unknown', @@ -797,6 +801,8 @@ export default function Dashboard() { isReadDenied: false }; + console.log(124, envSlug, selectedWorkspaceEnv) + if (selectedWorkspaceEnv) { if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv); else setSelectedEnv(selectedWorkspaceEnv); @@ -809,7 +815,10 @@ export default function Dashboard() { }) }; - return data ? ( + return + {!envInURL + ? + : (data ? ( {t('common:head-title', { title: t('dashboard:title') })} @@ -835,7 +844,13 @@ export default function Dashboard() { } /> - + envir.slug === envInURL)[0].name || ''} + isProjectRelated + userAvailableEnvs={workspaceEnvs} + onEnvChange={handleOnEnvironmentChange} + /> {checkDocsPopUpVisible && ( setSnapshotData(undefined)} color="mineshaft" size="md" @@ -863,7 +878,7 @@ export default function Dashboard() { {snapshotData ? 'Secret Snapshot' : t('dashboard:title')} {snapshotData && ( - + {new Date(snapshotData.createdAt).toLocaleString()} )} @@ -873,13 +888,6 @@ export default function Dashboard() { )} - {!snapshotData && data?.length === 0 && selectedEnv && ( - name)} - onChange={handleOnEnvironmentChange} - /> - )} @@ -960,20 +968,7 @@ export default function Dashboard() { {(snapshotData || data?.length !== 0) && selectedEnv && ( <> - {!snapshotData ? ( - name)} - onChange={handleOnEnvironmentChange} - /> - ) : ( - name)} - onChange={handleOnEnvironmentChange} - /> - )} - + @@ -1223,10 +1218,10 @@ export default function Dashboard() { ) : ( - + - ); + ))} } Dashboard.requireAuth = true; diff --git a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx new file mode 100644 index 000000000..a7545fb1c --- /dev/null +++ b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx @@ -0,0 +1,275 @@ +import { useEffect, useState } from 'react'; +import { FormProvider, 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, + Tooltip +} from '@app/components/v2'; +import { useWorkspace } from '@app/context'; +import { usePopUp } 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 { EnvComparisonRow } from './components/EnvComparisonRow'; +import { + FormData, + schema +} from './DashboardPage.utils'; + + +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 [selectedEnv, setSelectedEnv] = useState(null); + + 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 }) => !isReadDenied); + if (env) { + setSelectedEnv(env); + } + } + }); + + const userAvailableEnvs = wsEnv?.filter( + ({ isReadDenied }) => !isReadDenied + ); + + const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ + workspaceId, + env: userAvailableEnvs?.map(env => env.slug) ?? [], + decryptFileKey: latestFileKey!, + isPaused: false + }); + + // mutation calls + const { mutateAsync: createWsTag } = useCreateWsTag(); + + const method = useForm({ + // 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' }); + + const isReadOnly = selectedEnv?.isWriteDenied; + + 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 ( + + + + ); + } + + // when secrets is not loading and secrets list is empty + const isDashboardSecretEmpty = !isSecretsLoading && !formSecrets?.length; + + 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 ( + + + + {/* breadcrumb row */} + + + + + Secrets Overview + Inject your secrets using + + Infisical CLI + + or + + Infisical SDKs + + + + + + {0} + + + + Secret + + + {numSecretsMissingPerEnv && userAvailableEnvs?.map(env => { + return + + {env.name} + {numSecretsMissingPerEnv[env.slug] > 0 && + {numSecretsMissingPerEnv[env.slug]} + } + + + })} + + + {!isDashboardSecretEmpty && ( + + + + {[... new Set(secrets?.secrets.map((secret: any) => secret.key))].map((key, index) => ( + secret.key === key)} + isReadOnly={isReadOnly} + index={index} + isSecretValueHidden + userAvailableEnvs={userAvailableEnvs} + /> + ))} + + + + )} + {isDashboardSecretEmpty && + + + {0} + + + + Secret + + + + No secrets are available in this project yet. + You can go into any environment to add secrets there. + + } + {/* In future, we should add an option to add environments here + + } + onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })} + variant="outline_bg" + colorSchema="primary" + isFullWidth + className="h-10" + > + Add Environment + + */} + + + 0 + + 0 + 1 + + {userAvailableEnvs?.map(env => { + return + router.push(`${router.asPath }?env=${env.slug}`)} + variant="outline_bg" + colorSchema="primary" + isFullWidth + className="h-10" + > + Explore {env.name} + + + })} + + + + { + handlePopUpToggle('addTag', open); + }} + > + + + + + + + ); +}; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index 146b42165..75a42c0df 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -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 = () => { {/* breadcrumb row */} - + envir.slug === envFromTop)[0].name || ''} + isProjectRelated + userAvailableEnvs={userAvailableEnvs} + onEnvChange={onEnvChange} + /> {/* Secrets, commit and save button section */} @@ -468,23 +473,6 @@ export const DashboardPage = () => { {/* Environment, search and other action row */} - - - - {userAvailableEnvs?.map(({ name, slug }) => ( - - {name} - - ))} - - - { + const ref = useRef(null); + const syncScroll = (e: SyntheticEvent) => { + if (ref.current === null) return; + + ref.current.scrollTop = e.currentTarget.scrollTop; + ref.current.scrollLeft = e.currentTarget.scrollLeft; + }; + + return + + + + {(secret?.value || secret?.value === '') && (isOverridden ? secret.valueOverride : secret?.value)?.split('').length === 0 && EMPTY} + {(secret?.value || secret?.value === '') && (isOverridden ? secret.valueOverride : secret?.value)?.split(REGEX).map((word: string) => { + if (word.match(REGEX) !== null) { + return ( + + {word.slice(0, 2)} + + {word.slice(2, word.length - 1)} + + {word.slice(word.length - 1, word.length) === '}' ? ( + + {word.slice(word.length - 1, word.length)} + + ) : ( + + {word.slice(word.length - 1, word.length)} + + )} + + ); + } + return ( + + {word} + + ); + })} + {!(secret?.value || secret?.value === '') && missing} + + {(isSecretValueHidden && secret?.value) && ( + + + {(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').map(() => ( + + ))} + {(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').length === 0 && EMPTY} + + + )} + + +} + +export const EnvComparisonRow = ({ + index, + secrets, + isSecretValueHidden, + isReadOnly, + userAvailableEnvs +}: Props): JSX.Element => { + const { + // register, setValue, + control } = useFormContext(); + + // to get details on a secret + const secret = useWatch({ name: `secrets.${index}`, control }); + + const [areValuesHiddenThisRow, setAreValuesHiddenThisRow] = useState(true); + + return ( + + {index + 1} + + {secret?.key || ''} + setAreValuesHiddenThisRow(!areValuesHiddenThisRow)}> + + + + {userAvailableEnvs?.map(env => { + return sec.env === env.slug)[0]} index={index} /> + })} + + ); +}; diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx new file mode 100644 index 000000000..e0c9f8847 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx @@ -0,0 +1 @@ +export { EnvComparisonRow } from './EnvComparisonRow'; diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 850e8866c..f0b2cd092 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -156,7 +156,7 @@ export const SecretInputRow = ({ } return ( - + {index + 1} { if (word.match(REGEX) !== null) { return ( - + {word.slice(0, 2)} {word.slice(2, word.length - 1)} diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx index 98019e0ad..cbfc1aeed 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx @@ -64,7 +64,6 @@ export const SecretTagsSection = ({ }); const onFormSubmit = async (data: CreateWsTag) => { - console.log(19191, data); await onCreateTag(data); handlePopUpClose('CreateSecretTag'); }; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx index b857a3224..10c95ee55 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx @@ -37,13 +37,14 @@ const apiTokenExpiry = [ { label: '7 Days', value: 604800 }, { label: '1 Month', value: 2592000 }, { label: '6 months', value: 15552000 }, - { label: '12 months', value: 31104000 } + { label: '12 months', value: 31104000 }, + { label: 'Never', value: null }, ]; const createServiceTokenSchema = yup.object({ name: yup.string().required().label('Service Token Name'), environment: yup.string().required().label('Environment'), - expiresIn: yup.string().required().label('Service Token Expiration'), + expiresIn: yup.string().optional().label('Service Token Expiration'), permissions: yup.object().shape({ read: yup.boolean().required(), write: yup.boolean().required() @@ -213,7 +214,7 @@ export const ServiceTokenSection = ({ className="w-full" > {apiTokenExpiry.map(({ label, value }) => ( - + {label} ))} @@ -332,7 +333,7 @@ export const ServiceTokenSection = ({ {row.name} {row.environment} - {new Date(row.expiresAt).toUTCString()} + {row.expiresAt && new Date(row.expiresAt).toUTCString()}
{t('dashboard:sidebar.version-history')}
{snapshotData ? 'Secret Snapshot' : t('dashboard:title')}
Secrets Overview
Inject your secrets using + + Infisical CLI + + or + + Infisical SDKs +