feat(dashboard-v3): resolved create secret issue and allow empty secret values in create secret

This commit is contained in:
Akhil Mohan
2023-09-28 16:24:04 +05:30
parent e902a54af0
commit ef6ee6b2e6
10 changed files with 314 additions and 225 deletions

View File

@@ -1297,7 +1297,7 @@ export const createSecretBatchHelper = async ({
secrets.map(({ secretName, type }) => ({ secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName], secretBlindIndex: secretBlindIndexes[secretName],
type: type, type: type,
...getAuthDataPayloadUserObj(authData) ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
})) }))
) )
.exec(); .exec();

View File

@@ -93,7 +93,8 @@
"uuidv4": "^6.2.13", "uuidv4": "^6.2.13",
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11", "yup": "^0.32.11",
"zod": "^3.22.0" "zod": "^3.22.0",
"zustand": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.0.23", "@storybook/addon-essentials": "^7.0.23",
@@ -23653,6 +23654,33 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
},
"node_modules/zustand": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz",
"integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
} }
}, },
"dependencies": { "dependencies": {
@@ -40792,6 +40820,14 @@
"version": "3.22.0", "version": "3.22.0",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz",
"integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q==" "integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q=="
},
"zustand": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz",
"integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==",
"requires": {
"use-sync-external-store": "1.2.0"
}
} }
} }
} }

View File

@@ -101,7 +101,8 @@
"uuidv4": "^6.2.13", "uuidv4": "^6.2.13",
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11", "yup": "^0.32.11",
"zod": "^3.22.0" "zod": "^3.22.0",
"zustand": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.0.23", "@storybook/addon-essentials": "^7.0.23",

View File

@@ -28,9 +28,9 @@ export const ContentLoader = ({ text, frequency = 2000 }: Props) => {
<img src="/images/loading/loading.gif" height={210} width={240} alt="loading animation" /> <img src="/images/loading/loading.gif" height={210} width={240} alt="loading animation" />
</div> </div>
{text && isTextArray && ( {text && isTextArray && (
<AnimatePresence> <AnimatePresence exitBeforeEnter>
<motion.div <motion.div
className="text-primary text-sm absolute bottom-1/4" className="text-primary"
key={`content-loader-${pos}`} key={`content-loader-${pos}`}
initial={{ opacity: 0, translateY: 20 }} initial={{ opacity: 0, translateY: 20 }}
animate={{ opacity: 1, translateY: 0 }} animate={{ opacity: 1, translateY: 0 }}

View File

@@ -0,0 +1,64 @@
import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
import { useRouter } from "next/router";
import { createStore, StateCreator, StoreApi, useStore } from "zustand";
// akhilmhdh: Don't remove this file if ur thinking why use zustand just for selected selects state
// This is first step and the whole secret crud will be moved to this global page scope state
// this will allow more stuff like undo grouping stuffs etc
type SelectedSecretState = {
selectedSecret: Record<string, boolean>;
action: {
toggle: (id: string) => void;
reset: () => void;
};
};
const createSelectedSecretStore: StateCreator<SelectedSecretState> = (set) => ({
selectedSecret: {},
action: {
toggle: (id) =>
set((state) => {
const isChecked = Boolean(state.selectedSecret?.[id]);
const newChecks = { ...state.selectedSecret };
// remove selection if its present else add it
if (isChecked) delete newChecks[id];
else newChecks[id] = true;
return { selectedSecret: newChecks };
}),
reset: () => set({ selectedSecret: {} })
}
});
const StoreContext = createContext<StoreApi<SelectedSecretState> | null>(null);
export const StoreProvider = ({ children }: { children: ReactNode }) => {
const storeRef = useRef<StoreApi<SelectedSecretState>>();
const router = useRouter();
if (!storeRef.current) {
storeRef.current = createStore<SelectedSecretState>((...a) => ({
...createSelectedSecretStore(...a)
}));
}
useEffect(() => {
const onRouteChangeStart = () => {
const state = storeRef.current?.getState();
state?.action.reset();
};
router.events.on("routeChangeStart", onRouteChangeStart);
return () => {
router.events.off("routeChangeStart", onRouteChangeStart);
};
}, []);
return <StoreContext.Provider value={storeRef.current}>{children}</StoreContext.Provider>;
};
const useStoreContext = <T extends unknown>(selector: (state: SelectedSecretState) => T): T => {
const ctx = useContext(StoreContext);
if (!ctx) throw new Error("Missing ");
return useStore(ctx, selector);
};
// selected secret context
export const useSelectedSecrets = () => useStoreContext((state) => state.selectedSecret);
export const useSelectedSecretActions = () => useStoreContext((state) => state.action);

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
@@ -34,10 +34,11 @@ import { SecretDropzone } from "./components/SecretDropzone";
import { SecretImportListView } from "./components/SecretImportListView"; import { SecretImportListView } from "./components/SecretImportListView";
import { SecretListView } from "./components/SecretListView"; import { SecretListView } from "./components/SecretListView";
import { SnapshotView } from "./components/SnapshotView"; import { SnapshotView } from "./components/SnapshotView";
import { StoreProvider } from "./SecretMainPage.store";
import { Filter, GroupBy, SortDir } from "./SecretMainPage.types"; import { Filter, GroupBy, SortDir } from "./SecretMainPage.types";
const LOADER_TEXT = [ const LOADER_TEXT = [
"Retriving and decrypting your E2EE encrypted secrets", "Retriving your encrypted secrets",
"Fetching folders", "Fetching folders",
"Getting secret import links" "Getting secret import links"
]; ];
@@ -54,7 +55,6 @@ export const SecretMainPage = () => {
tags: {}, tags: {},
searchFilter: "" searchFilter: ""
}); });
const [selectedSecret, setSelectedSecret] = useState<Record<string, boolean>>({});
const [snapshotId, setSnapshotId] = useState<string | null>(null); const [snapshotId, setSnapshotId] = useState<string | null>(null);
const isRollbackMode = Boolean(snapshotId); const isRollbackMode = Boolean(snapshotId);
@@ -73,17 +73,6 @@ export const SecretMainPage = () => {
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback
); );
useEffect(() => {
const onRouteChangeStart = () => {
setSelectedSecret({});
};
router.events.on("routeChangeStart", onRouteChangeStart);
return () => {
router.events.off("routeChangeStart", onRouteChangeStart);
};
}, []);
const { data: decryptFileKey } = useGetUserWsKey(workspaceId); const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
// fetch secrets // fetch secrets
@@ -187,20 +176,6 @@ export const SecretMainPage = () => {
const handleToggleVisibility = useCallback(() => setIsVisible((state) => !state), []); const handleToggleVisibility = useCallback(() => setIsVisible((state) => !state), []);
const handleToggleSecretSelect = useCallback(
(id: string) =>
setSelectedSecret((state) => {
const isChecked = Boolean(state?.[id]);
const newChecks = { ...state };
if (isChecked) delete newChecks[id];
else newChecks[id] = true;
return newChecks;
}),
[]
);
const handleResetSecretSelect = useCallback(() => setSelectedSecret({}), []);
// snapshot functions // snapshot functions
const handleSelectSnapshot = useCallback((snapId: string) => { const handleSelectSnapshot = useCallback((snapId: string) => {
setSnapshotId(snapId); setSnapshotId(snapId);
@@ -221,138 +196,137 @@ export const SecretMainPage = () => {
} }
return ( return (
<div className="container flex flex-col mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]"> <StoreProvider>
<div className="relative right-6 -top-2 mb-2 ml-6"> <div className="container flex flex-col mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<NavHeader <div className="relative right-6 -top-2 mb-2 ml-6">
pageName={t("dashboard.title")} <NavHeader
currentEnv={environment} pageName={t("dashboard.title")}
userAvailableEnvs={currentWorkspace?.environments} currentEnv={environment}
isFolderMode userAvailableEnvs={currentWorkspace?.environments}
secretPath={secretPath} isFolderMode
isProjectRelated secretPath={secretPath}
onEnvChange={handleEnvChange} isProjectRelated
onEnvChange={handleEnvChange}
/>
</div>
{!isRollbackMode ? (
<>
<ActionBar
secrets={secrets}
importedSecrets={importedSecrets}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isVisible={isVisible}
decryptFileKey={decryptFileKey!}
filter={filter}
tags={tags}
onVisiblilityToggle={handleToggleVisibility}
onGroupByChange={handleGroupByChange}
onSearchChange={handleSearchChange}
onToggleTagFilter={handleTagToggle}
snapshotCount={snapshotCount || 0}
autoCapitalization={currentWorkspace?.autoCapitalization}
isSnapshotCountLoading={isSnapshotCountLoading}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
/>
<div
className={twMerge(
"mt-3 overflow-auto thin-scrollbar bg-mineshaft-800 text-left text-bunker-300 rounded-md text-sm border border-mineshaft-600"
)}
>
<div className="flex flex-col ">
{isNotEmtpy && (
<div className="flex font-medium border-b border-mineshaft-600">
<div style={{ width: "2.8rem" }} className="px-4 py-3 flex-shrink-0" />
<div
className="w-80 flex-shrink-0 border-r flex items-center border-mineshaft-600 px-4 py-2"
role="button"
tabIndex={0}
onClick={handleSortToggle}
onKeyDown={(evt) => {
if (evt.key === "Enter") handleSortToggle();
}}
>
Key
<FontAwesomeIcon
icon={sortDir === SortDir.ASC ? faArrowDown : faArrowUp}
className="ml-2"
/>
</div>
<div className="flex-grow px-4 py-2">Value</div>
</div>
)}
{canReadSecret && (
<SecretImportListView
searchTerm={filter.searchFilter}
secretImports={secretImports}
isFetching={isSecretImportsLoading || isSecretImportsFetching}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
secrets={secrets}
importedSecrets={importedSecrets}
/>
)}
<FolderListView
folders={folders}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
sortDir={sortDir}
/>
{canReadSecret && (
<SecretListView
secrets={secrets}
tags={tags}
filter={filter}
sortDir={sortDir}
isVisible={isVisible}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
decryptFileKey={decryptFileKey!}
/>
)}
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
</div>
</div>
<SecretDropzone
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
decryptFileKey={decryptFileKey!}
secretPath={secretPath}
isSmaller={isNotEmtpy}
environments={currentWorkspace?.environments}
/>
</>
) : (
<SnapshotView
snapshotId={snapshotId || ""}
decryptFileKey={decryptFileKey!}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
secrets={secrets}
folders={folders}
snapshotCount={snapshotCount}
onGoBack={handleResetSnapshot}
onClickListSnapshot={() => handlePopUpToggle("snapshots", true)}
/>
)}
<PitDrawer
secretSnaphots={snapshotList}
snapshotId={snapshotId}
isDrawerOpen={popUp.snapshots.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("snapshots", isOpen)}
hasNextPage={hasNextSnapshotListPage}
fetchNextPage={fetchNextSnapshotList}
onSelectSnapshot={handleSelectSnapshot}
isFetchingNextPage={isFetchingNextSnapshotList}
/> />
</div> </div>
{!isRollbackMode ? ( </StoreProvider>
<>
<ActionBar
secrets={secrets}
importedSecrets={importedSecrets}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
isVisible={isVisible}
decryptFileKey={decryptFileKey!}
filter={filter}
tags={tags}
selectedSecrets={selectedSecret}
onVisiblilityToggle={handleToggleVisibility}
onGroupByChange={handleGroupByChange}
onSearchChange={handleSearchChange}
onToggleTagFilter={handleTagToggle}
onResetSelectedSecret={handleResetSecretSelect}
snapshotCount={snapshotCount || 0}
isSnapshotCountLoading={isSnapshotCountLoading}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
/>
<div
className={twMerge(
"mt-3 overflow-auto thin-scrollbar bg-mineshaft-800 text-left text-bunker-300 rounded-md text-sm border border-mineshaft-600"
)}
>
<div className="flex flex-col ">
{isNotEmtpy && (
<div className="flex font-medium border-b border-mineshaft-600">
<div className="w-[2.8rem] px-4 py-3 flex-shrink-0" />
<div
className="w-80 flex-shrink-0 border-r flex items-center border-mineshaft-600 w-11 px-4 py-2"
role="button"
tabIndex={0}
onClick={handleSortToggle}
onKeyDown={(evt) => {
if (evt.key === "Enter") handleSortToggle();
}}
>
Key
<FontAwesomeIcon
icon={sortDir === SortDir.ASC ? faArrowDown : faArrowUp}
className="ml-2"
/>
</div>
<div className="flex-grow px-4 py-2">Value</div>
</div>
)}
{canReadSecret && (
<SecretImportListView
searchTerm={filter.searchFilter}
secretImports={secretImports}
isFetching={isSecretImportsLoading || isSecretImportsFetching}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
secrets={secrets}
importedSecrets={importedSecrets}
/>
)}
<FolderListView
folders={folders}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
sortDir={sortDir}
/>
{canReadSecret && (
<SecretListView
secrets={secrets}
tags={tags}
filter={filter}
sortDir={sortDir}
isVisible={isVisible}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
decryptFileKey={decryptFileKey!}
selectedSecrets={selectedSecret}
onToggleSecretSelect={handleToggleSecretSelect}
/>
)}
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
</div>
</div>
<SecretDropzone
secrets={secrets}
environment={environment}
workspaceId={workspaceId}
decryptFileKey={decryptFileKey!}
secretPath={secretPath}
isSmaller={isNotEmtpy}
environments={currentWorkspace?.environments}
/>
</>
) : (
<SnapshotView
snapshotId={snapshotId || ""}
decryptFileKey={decryptFileKey!}
environment={environment}
workspaceId={workspaceId}
secretPath={secretPath}
secrets={secrets}
folders={folders}
snapshotCount={snapshotCount}
onGoBack={handleResetSnapshot}
onClickListSnapshot={() => handlePopUpToggle("snapshots", true)}
/>
)}
<PitDrawer
secretSnaphots={snapshotList}
snapshotId={snapshotId}
isDrawerOpen={popUp.snapshots.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("snapshots", isOpen)}
hasNextPage={hasNextSnapshotListPage}
fetchNextPage={fetchNextSnapshotList}
onSelectSnapshot={handleSelectSnapshot}
isFetchingNextPage={isFetchingNextSnapshotList}
/>
</div>
); );
}; };

View File

@@ -45,6 +45,7 @@ import { usePopUp } from "@app/hooks";
import { useCreateFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { useCreateFolder, useDeleteSecretBatch } from "@app/hooks/api";
import { DecryptedSecret, TImportedSecrets, UserWsKeyPair, WsTag } from "@app/hooks/api/types"; import { DecryptedSecret, TImportedSecrets, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
import { Filter, GroupBy } from "../../SecretMainPage.types"; import { Filter, GroupBy } from "../../SecretMainPage.types";
import { CreateSecretForm } from "./CreateSecretForm"; import { CreateSecretForm } from "./CreateSecretForm";
import { CreateSecretImportForm } from "./CreateSecretImportForm"; import { CreateSecretImportForm } from "./CreateSecretImportForm";
@@ -61,14 +62,13 @@ type Props = {
filter: Filter; filter: Filter;
tags?: WsTag[]; tags?: WsTag[];
isVisible?: boolean; isVisible?: boolean;
selectedSecrets: Record<string, boolean>;
snapshotCount: number; snapshotCount: number;
isSnapshotCountLoading?: boolean; isSnapshotCountLoading?: boolean;
autoCapitalization?: boolean;
onGroupByChange: (opt?: GroupBy) => void; onGroupByChange: (opt?: GroupBy) => void;
onSearchChange: (term: string) => void; onSearchChange: (term: string) => void;
onToggleTagFilter: (tagId: string) => void; onToggleTagFilter: (tagId: string) => void;
onVisiblilityToggle: () => void; onVisiblilityToggle: () => void;
onResetSelectedSecret: () => void;
onClickRollbackMode: () => void; onClickRollbackMode: () => void;
}; };
@@ -82,14 +82,13 @@ export const ActionBar = ({
filter, filter,
tags = [], tags = [],
isVisible, isVisible,
selectedSecrets,
snapshotCount, snapshotCount,
isSnapshotCountLoading, isSnapshotCountLoading,
autoCapitalization,
onSearchChange, onSearchChange,
onToggleTagFilter, onToggleTagFilter,
onGroupByChange, onGroupByChange,
onVisiblilityToggle, onVisiblilityToggle,
onResetSelectedSecret,
onClickRollbackMode onClickRollbackMode
}: Props) => { }: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
@@ -106,6 +105,8 @@ export const ActionBar = ({
const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: createFolder } = useCreateFolder();
const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch();
const selectedSecrets = useSelectedSecrets();
const { reset: resetSelectedSecret } = useSelectedSecretActions();
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
const handleFolderCreate = async (folderName: string) => { const handleFolderCreate = async (folderName: string) => {
@@ -170,7 +171,7 @@ export const ActionBar = ({
environment, environment,
secrets: bulkDeletedSecrets.map(({ key }) => ({ secretName: key, type: "shared" })) secrets: bulkDeletedSecrets.map(({ key }) => ({ secretName: key, type: "shared" }))
}); });
onResetSelectedSecret(); resetSelectedSecret();
handlePopUpClose("bulkDeleteSecrets"); handlePopUpClose("bulkDeleteSecrets");
createNotification({ createNotification({
type: "success", type: "success",
@@ -376,7 +377,7 @@ export const ActionBar = ({
> >
<div className="text-bunker-300 flex items-center bg-mineshaft-800 mt-3.5 py-2 px-4 rounded-md border border-mineshaft-600"> <div className="text-bunker-300 flex items-center bg-mineshaft-800 mt-3.5 py-2 px-4 rounded-md border border-mineshaft-600">
<Tooltip content="Clear"> <Tooltip content="Clear">
<IconButton variant="plain" ariaLabel="clear-selection" onClick={onResetSelectedSecret}> <IconButton variant="plain" ariaLabel="clear-selection" onClick={resetSelectedSecret}>
<FontAwesomeIcon icon={faMinusSquare} size="lg" /> <FontAwesomeIcon icon={faMinusSquare} size="lg" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -410,6 +411,7 @@ export const ActionBar = ({
secrets={secrets} secrets={secrets}
environment={environment} environment={environment}
workspaceId={workspaceId} workspaceId={workspaceId}
autoCapitalize={autoCapitalization}
secretPath={secretPath} secretPath={secretPath}
decryptFileKey={decryptFileKey!} decryptFileKey={decryptFileKey!}
isOpen={popUp.addSecret.isOpen} isOpen={popUp.addSecret.isOpen}

View File

@@ -9,7 +9,7 @@ import { DecryptedSecret, UserWsKeyPair } from "@app/hooks/api/types";
const typeSchema = z.object({ const typeSchema = z.object({
key: z.string(), key: z.string(),
value: z.string() value: z.string().optional()
}); });
type TFormSchema = z.infer<typeof typeSchema>; type TFormSchema = z.infer<typeof typeSchema>;
@@ -24,6 +24,7 @@ type Props = {
isOpen?: boolean; isOpen?: boolean;
onClose: () => void; onClose: () => void;
onTogglePopUp: (isOpen: boolean) => void; onTogglePopUp: (isOpen: boolean) => void;
autoCapitalize?: boolean;
}; };
export const CreateSecretForm = ({ export const CreateSecretForm = ({
@@ -33,7 +34,8 @@ export const CreateSecretForm = ({
secretPath = "/", secretPath = "/",
isOpen, isOpen,
onClose, onClose,
onTogglePopUp onTogglePopUp,
autoCapitalize = true
}: Props) => { }: Props) => {
const { const {
register, register,
@@ -54,7 +56,7 @@ export const CreateSecretForm = ({
workspaceId, workspaceId,
secretPath, secretPath,
secretName: key, secretName: key,
secretValue: value, secretValue: value || "",
secretComment: "", secretComment: "",
type: "shared", type: "shared",
latestFileKey: decryptFileKey latestFileKey: decryptFileKey
@@ -82,7 +84,11 @@ export const CreateSecretForm = ({
> >
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}> <FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
<Input {...register("key")} placeholder="Type your secret name" /> <Input
{...register("key")}
placeholder="Type your secret name"
autoCapitalization={autoCapitalize}
/>
</FormControl> </FormControl>
<Controller <Controller
control={control} control={control}

View File

@@ -168,24 +168,31 @@ export const SecretDetailSidebar = ({
<FormControl label="Key"> <FormControl label="Key">
<Input isDisabled {...register("key")} /> <Input isDisabled {...register("key")} />
</FormControl> </FormControl>
<Controller <ProjectPermissionCan
name="value" I={ProjectPermissionActions.Edit}
key="secret-value" a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
control={control} >
render={({ field }) => ( {(isAllowed) => (
<FormControl label="Value"> <Controller
<SecretInput name="value"
isReadOnly={isReadOnly} key="secret-value"
key="secret-value" control={control}
isDisabled={isOverridden} render={({ field }) => (
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5" <FormControl label="Value">
{...field} <SecretInput
autoFocus={false} isReadOnly={isReadOnly}
/> key="secret-value"
</FormControl> isDisabled={isOverridden || isAllowed}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
</FormControl>
)}
/>
)} )}
/> </ProjectPermissionCan>
<div className="mb-2"> <div className="mb-2 border-b border-mineshaft-600 pb-4">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
@@ -217,34 +224,6 @@ export const SecretDetailSidebar = ({
)} )}
/> />
)} )}
<div className="my-2 mb-6 border-t border-mineshaft-600 pt-4">
<Controller
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={onChange}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Disable multi line encoding
<Tooltip content="Infisical encodes multiline secrets by escaping newlines and wrappign in quotes. To disable, enable this option">
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</Switch>
)}
</ProjectPermissionCan>
)}
/>
</div>
<FormControl label="Tags" className=""> <FormControl label="Tags" className="">
<div className="overflow-hidden grid gap-2 grid-flow-col auto-cols-min pt-2"> <div className="overflow-hidden grid gap-2 grid-flow-col auto-cols-min pt-2">
{fields.map(({ tagColor, id: formId, name, _id }) => ( {fields.map(({ tagColor, id: formId, name, _id }) => (
@@ -341,6 +320,34 @@ export const SecretDetailSidebar = ({
rows={5} rows={5}
/> />
</FormControl> </FormControl>
<div className="my-2 mb-6 border-b border-mineshaft-600 pb-4">
<Controller
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={onChange}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Disable multi line encoding
<Tooltip content="Infisical encodes multiline secrets by escaping newlines and wrappign in quotes. To disable, enable this option">
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</Switch>
)}
</ProjectPermissionCan>
)}
/>
</div>
<div className="dark mb-4 text-sm text-bunker-300 flex-grow"> <div className="dark mb-4 text-sm text-bunker-300 flex-grow">
<div className="mb-2">Version History</div> <div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]"> <div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">

View File

@@ -13,6 +13,7 @@ import { UserWsKeyPair, WsTag } from "@app/hooks/api/types";
import { secretSnapshotKeys } from "~/hooks/api/secretSnapshots/queries"; import { secretSnapshotKeys } from "~/hooks/api/secretSnapshots/queries";
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types"; import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
import { SecretDetailSidebar } from "./SecretDetaiSidebar"; import { SecretDetailSidebar } from "./SecretDetaiSidebar";
import { SecretItem } from "./SecretItem"; import { SecretItem } from "./SecretItem";
@@ -27,8 +28,6 @@ type Props = {
sortDir?: SortDir; sortDir?: SortDir;
tags?: WsTag[]; tags?: WsTag[];
isVisible?: boolean; isVisible?: boolean;
selectedSecrets: Record<string, boolean>;
onToggleSecretSelect: (id: string) => void;
}; };
const reorderSecretGroupByUnderscore = (secrets: DecryptedSecret[], sortDir: SortDir) => { const reorderSecretGroupByUnderscore = (secrets: DecryptedSecret[], sortDir: SortDir) => {
@@ -86,9 +85,7 @@ export const SecretListView = ({
filter, filter,
sortDir = SortDir.ASC, sortDir = SortDir.ASC,
tags: wsTags = [], tags: wsTags = [],
isVisible, isVisible
selectedSecrets,
onToggleSecretSelect
}: Props) => { }: Props) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -114,6 +111,8 @@ export const SecretListView = ({
onSuccess: undefined onSuccess: undefined
} }
}); });
const selectedSecrets = useSelectedSecrets();
const { toggle: toggleSelectedSecret } = useSelectedSecretActions();
const handleSecretOperation = async ( const handleSecretOperation = async (
operation: "create" | "update" | "delete", operation: "create" | "update" | "delete",
@@ -303,7 +302,7 @@ export const SecretListView = ({
secretPath={secretPath} secretPath={secretPath}
tags={wsTags} tags={wsTags}
isSelected={selectedSecrets?.[secret._id]} isSelected={selectedSecrets?.[secret._id]}
onToggleSecretSelect={onToggleSecretSelect} onToggleSecretSelect={toggleSelectedSecret}
isVisible={isVisible} isVisible={isVisible}
secret={secret} secret={secret}
key={secret._id} key={secret._id}