mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 22:28:15 +00:00
misc: migrated to react-state
This commit is contained in:
@@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { SecretOverviewPage } from "@app/views/SecretOverviewPage";
|
import { SecretOverviewPage } from "@app/views/SecretOverviewPage";
|
||||||
import { StoreProvider } from "@app/views/SecretOverviewPage/SecretOverviewPage.store";
|
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -17,9 +16,7 @@ const Dashboard = () => {
|
|||||||
<meta name="og:description" content={String(t("dashboard.og-description"))} />
|
<meta name="og:description" content={String(t("dashboard.og-description"))} />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<StoreProvider>
|
|
||||||
<SecretOverviewPage />
|
<SecretOverviewPage />
|
||||||
</StoreProvider>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { createStore, StateCreator, StoreApi, useStore } from "zustand";
|
|
||||||
|
|
||||||
export enum EntryType {
|
|
||||||
FOLDER = "folder",
|
|
||||||
SECRET = "secret"
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectedEntriesState = {
|
|
||||||
selectedEntries: {
|
|
||||||
[EntryType.FOLDER]: Record<string, boolean>;
|
|
||||||
[EntryType.SECRET]: Record<string, boolean>;
|
|
||||||
};
|
|
||||||
action: {
|
|
||||||
toggle: (type: EntryType, key: string) => void;
|
|
||||||
reset: () => void;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const createSelectedSecretStore: StateCreator<SelectedEntriesState> = (set) => ({
|
|
||||||
selectedEntries: {
|
|
||||||
[EntryType.FOLDER]: {},
|
|
||||||
[EntryType.SECRET]: {}
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
toggle: (type: EntryType, key: string) =>
|
|
||||||
set((state) => {
|
|
||||||
const isChecked = Boolean(state.selectedEntries[type]?.[key]);
|
|
||||||
const newChecks = { ...state.selectedEntries };
|
|
||||||
// remove selection if its present else add it
|
|
||||||
if (isChecked) delete newChecks[type][key];
|
|
||||||
else newChecks[type][key] = true;
|
|
||||||
return { selectedEntries: newChecks };
|
|
||||||
}),
|
|
||||||
reset: () =>
|
|
||||||
set({
|
|
||||||
selectedEntries: {
|
|
||||||
[EntryType.FOLDER]: {},
|
|
||||||
[EntryType.SECRET]: {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const StoreContext = createContext<StoreApi<SelectedEntriesState> | null>(null);
|
|
||||||
export const StoreProvider = ({ children }: { children: ReactNode }) => {
|
|
||||||
const storeRef = useRef<StoreApi<SelectedEntriesState>>();
|
|
||||||
const router = useRouter();
|
|
||||||
if (!storeRef.current) {
|
|
||||||
storeRef.current = createStore<SelectedEntriesState>((...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: SelectedEntriesState) => T): T => {
|
|
||||||
const ctx = useContext(StoreContext);
|
|
||||||
if (!ctx) throw new Error("Missing context provider");
|
|
||||||
return useStore(ctx, selector);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSelectedEntries = () => useStoreContext((state) => state.selectedEntries);
|
|
||||||
export const useSelectedEntryActions = () => useStoreContext((state) => state.action);
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -71,7 +71,11 @@ import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynam
|
|||||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||||
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
||||||
import { SelectionPanel } from "./components/SelectionPanel/SelectionPanel";
|
import { SelectionPanel } from "./components/SelectionPanel/SelectionPanel";
|
||||||
import { EntryType, useSelectedEntries, useSelectedEntryActions } from "./SecretOverviewPage.store";
|
|
||||||
|
export enum EntryType {
|
||||||
|
FOLDER = "folder",
|
||||||
|
SECRET = "secret"
|
||||||
|
}
|
||||||
|
|
||||||
export const SecretOverviewPage = () => {
|
export const SecretOverviewPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -84,15 +88,6 @@ export const SecretOverviewPage = () => {
|
|||||||
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
|
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
|
||||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleParentTableWidthResize = () => {
|
|
||||||
setExpandableTableWidth(parentTableRef.current?.clientWidth || 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("resize", handleParentTableWidthResize);
|
|
||||||
return () => window.removeEventListener("resize", handleParentTableWidthResize);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parentTableRef.current) {
|
if (parentTableRef.current) {
|
||||||
setExpandableTableWidth(parentTableRef.current.clientWidth);
|
setExpandableTableWidth(parentTableRef.current.clientWidth);
|
||||||
@@ -107,8 +102,55 @@ export const SecretOverviewPage = () => {
|
|||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
const secretPath = (router.query?.secretPath as string) || "/";
|
const secretPath = (router.query?.secretPath as string) || "/";
|
||||||
|
|
||||||
const selectedEntries = useSelectedEntries();
|
const [selectedEntries, setSelectedEntries] = useState<{
|
||||||
const { toggle: toggleSelectedEntry } = useSelectedEntryActions();
|
[EntryType.FOLDER]: Record<string, boolean>;
|
||||||
|
[EntryType.SECRET]: Record<string, boolean>;
|
||||||
|
}>({
|
||||||
|
[EntryType.FOLDER]: {},
|
||||||
|
[EntryType.SECRET]: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleSelectedEntry = useCallback(
|
||||||
|
(type: EntryType, key: string) => {
|
||||||
|
const isChecked = Boolean(selectedEntries[type]?.[key]);
|
||||||
|
const newChecks = { ...selectedEntries };
|
||||||
|
|
||||||
|
// remove selection if its present else add it
|
||||||
|
if (isChecked) {
|
||||||
|
delete newChecks[type][key];
|
||||||
|
} else {
|
||||||
|
newChecks[type][key] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedEntries(newChecks);
|
||||||
|
},
|
||||||
|
[selectedEntries]
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetSelectedEntries = useCallback(() => {
|
||||||
|
setSelectedEntries({
|
||||||
|
[EntryType.FOLDER]: {},
|
||||||
|
[EntryType.SECRET]: {}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleParentTableWidthResize = () => {
|
||||||
|
setExpandableTableWidth(parentTableRef.current?.clientWidth || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRouteChangeStart = () => {
|
||||||
|
resetSelectedEntries();
|
||||||
|
};
|
||||||
|
|
||||||
|
router.events.on("routeChangeStart", onRouteChangeStart);
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleParentTableWidthResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleParentTableWidthResize);
|
||||||
|
router.events.off("routeChangeStart", onRouteChangeStart);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isWorkspaceLoading && !workspaceId && router.isReady) {
|
if (!isWorkspaceLoading && !workspaceId && router.isReady) {
|
||||||
@@ -553,6 +595,8 @@ export const SecretOverviewPage = () => {
|
|||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
getSecretByKey={getSecretByKey}
|
getSecretByKey={getSecretByKey}
|
||||||
getFolderByNameAndEnv={getFolderByNameAndEnv}
|
getFolderByNameAndEnv={getFolderByNameAndEnv}
|
||||||
|
selectedEntries={selectedEntries}
|
||||||
|
resetSelectedEntries={resetSelectedEntries}
|
||||||
/>
|
/>
|
||||||
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
||||||
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">
|
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||||
|
|||||||
@@ -15,23 +15,35 @@ import { usePopUp } from "@app/hooks";
|
|||||||
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
|
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
|
||||||
import { DecryptedSecret, TDeleteSecretBatchDTO, TSecretFolder } from "@app/hooks/api/types";
|
import { DecryptedSecret, TDeleteSecretBatchDTO, TSecretFolder } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { useSelectedEntries, useSelectedEntryActions } from "../../SecretOverviewPage.store";
|
export enum EntryType {
|
||||||
|
FOLDER = "folder",
|
||||||
|
SECRET = "secret"
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
|
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
|
||||||
getFolderByNameAndEnv: (name: string, env: string) => TSecretFolder | undefined;
|
getFolderByNameAndEnv: (name: string, env: string) => TSecretFolder | undefined;
|
||||||
|
resetSelectedEntries: () => void;
|
||||||
|
selectedEntries: {
|
||||||
|
[EntryType.FOLDER]: Record<string, boolean>;
|
||||||
|
[EntryType.SECRET]: Record<string, boolean>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPath }: Props) => {
|
export const SelectionPanel = ({
|
||||||
|
getFolderByNameAndEnv,
|
||||||
|
getSecretByKey,
|
||||||
|
secretPath,
|
||||||
|
resetSelectedEntries,
|
||||||
|
selectedEntries
|
||||||
|
}: Props) => {
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||||
"bulkDeleteEntries"
|
"bulkDeleteEntries"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const selectedEntries = useSelectedEntries();
|
|
||||||
const { reset: resetSelectedEntries } = useSelectedEntryActions();
|
|
||||||
const selectedCount =
|
const selectedCount =
|
||||||
Object.keys(selectedEntries.folder).length + Object.keys(selectedEntries.secret).length;
|
Object.keys(selectedEntries.folder).length + Object.keys(selectedEntries.secret).length;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user