mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(dashboard-v3): resolved create secret issue and allow empty secret values in create secret
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
38
frontend/package-lock.json
generated
38
frontend/package-lock.json
generated
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
64
frontend/src/views/SecretMainPage/SecretMainPage.store.tsx
Normal file
64
frontend/src/views/SecretMainPage/SecretMainPage.store.tsx
Normal 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);
|
||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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]">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user