import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; import { subject } from "@casl/ability"; import { faArrowDown, faArrowUp } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; import NavHeader from "@app/components/navigation/NavHeader"; import { createNotification } from "@app/components/notifications"; import { PermissionDeniedBanner } from "@app/components/permissions"; import { ContentLoader, Pagination } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionDynamicSecretActions, ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; import { useDebounce, usePagination, usePopUp } from "@app/hooks"; import { useGetImportedSecretsSingleEnv, useGetSecretApprovalPolicyOfABoard, useGetWorkspaceSnapshotList, useGetWsSnapshotCount, useGetWsTags } from "@app/hooks/api"; import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard"; import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { DynamicSecretListView } from "@app/views/SecretMainPage/components/DynamicSecretListView"; import { FolderListView } from "@app/views/SecretMainPage/components/FolderListView"; import { SecretImportListView } from "@app/views/SecretMainPage/components/SecretImportListView"; import { SecretTableResourceCount } from "@app/views/SecretOverviewPage/components/SecretTableResourceCount/SecretTableResourceCount"; import { SecretV2MigrationSection } from "../SecretOverviewPage/components/SecretV2MigrationSection"; import { ActionBar } from "./components/ActionBar"; import { CreateSecretForm } from "./components/CreateSecretForm"; import { PitDrawer } from "./components/PitDrawer"; import { SecretDropzone } from "./components/SecretDropzone"; import { SecretListView } from "./components/SecretListView"; import { SnapshotView } from "./components/SnapshotView"; import { StoreProvider } from "./SecretMainPage.store"; import { Filter, RowType } from "./SecretMainPage.types"; const LOADER_TEXT = [ "Retrieving your encrypted secrets...", "Fetching folders...", "Getting secret import links..." ]; export const SecretMainPage = () => { const { t } = useTranslation(); const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace(); const router = useRouter(); const { permission } = useProjectPermission(); const [isVisible, setIsVisible] = useState(false); const { offset, limit, orderDirection, setOrderDirection, setPage, perPage, page, setPerPage, orderBy } = usePagination(DashboardSecretsOrderBy.Name); const [snapshotId, setSnapshotId] = useState(null); const isRollbackMode = Boolean(snapshotId); const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["snapshots"] as const); // env slug const environment = router.query.env as string; const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || ""; const secretPath = (router.query.secretPath as string) || "/"; const canReadSecret = permission.can( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName: "*", secretTags: ["*"] }) ); const canReadSecretImports = permission.can( ProjectPermissionActions.Read, subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) ); const canReadDynamicSecret = permission.can( ProjectPermissionDynamicSecretActions.Lease, subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath }) ); const canDoReadRollback = permission.can( ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback ); const defaultFilterState = { tags: {}, searchFilter: (router.query.searchFilter as string) || "", include: { [RowType.Folder]: true, [RowType.Import]: canReadSecretImports, [RowType.DynamicSecret]: canReadDynamicSecret, [RowType.Secret]: canReadSecret } }; const [filter, setFilter] = useState(defaultFilterState); const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter); const [filterHistory, setFilterHistory] = useState>(new Map()); // change filters if permissions change at different paths/env useEffect(() => { setFilter((prev) => ({ ...prev, include: { [RowType.Folder]: true, [RowType.Import]: canReadSecretImports, [RowType.DynamicSecret]: canReadDynamicSecret, [RowType.Secret]: canReadSecret } })); }, [canReadSecret, canReadSecretImports, canReadDynamicSecret]); useEffect(() => { if ( !isWorkspaceLoading && !currentWorkspace?.environments.find((env) => env.slug === environment) && router.isReady ) { router.push(`/project/${workspaceId}/secrets/overview`); createNotification({ text: "No environment found with given slug", type: "error" }); } }, [isWorkspaceLoading, currentWorkspace, environment, router.isReady]); const { data, isLoading: isDetailsLoading, isFetching: isDetailsFetching } = useGetProjectSecretsDetails({ environment, projectId: workspaceId, secretPath, offset, limit, orderBy, search: debouncedSearchFilter, orderDirection, includeImports: canReadSecretImports && filter.include.import, includeFolders: filter.include.folder, includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic, includeSecrets: canReadSecret && filter.include.secret, tags: filter.tags }); const { imports, folders, dynamicSecrets, secrets, totalImportCount = 0, totalFolderCount = 0, totalDynamicSecretCount = 0, totalSecretCount = 0, totalCount = 0 } = data ?? {}; useEffect(() => { // reset page if no longer valid if (totalCount <= offset) setPage(1); }, [totalCount]); // fetch imported secrets to show user the overriden ones const { data: importedSecrets } = useGetImportedSecretsSingleEnv({ projectId: workspaceId, environment, path: secretPath, options: { enabled: canReadSecret } }); // fetch tags const { data: tags } = useGetWsTags(canReadSecret ? workspaceId : ""); const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({ workspaceId, environment, secretPath }); const isProtectedBranch = Boolean(boardPolicy); const { data: snapshotList, isFetchingNextPage: isFetchingNextSnapshotList, fetchNextPage: fetchNextSnapshotList, hasNextPage: hasNextSnapshotListPage } = useGetWorkspaceSnapshotList({ workspaceId, directory: secretPath, environment, isPaused: !popUp.snapshots.isOpen || !canDoReadRollback, limit: 10 }); const { data: snapshotCount, isLoading: isSnapshotCountLoading } = useGetWsSnapshotCount({ workspaceId, environment, directory: secretPath, isPaused: !canDoReadRollback }); const isNotEmpty = Boolean( secrets?.length || folders?.length || imports?.length || dynamicSecrets?.length ); const handleSortToggle = () => setOrderDirection((state) => state === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC ); const handleEnvChange = (slug: string) => { const query: Record = { ...router.query, env: slug }; delete query.secretPath; router.push({ pathname: router.pathname, query }); }; const handleTagToggle = useCallback( (tagSlug: string) => setFilter((state) => { const isTagPresent = Boolean(state.tags?.[tagSlug]); const newTagFilter = { ...state.tags }; if (isTagPresent) delete newTagFilter[tagSlug]; else newTagFilter[tagSlug] = true; return { ...state, tags: newTagFilter }; }), [] ); const handleToggleRowType = useCallback( (rowType: RowType) => setFilter((state) => { return { ...state, include: { ...state.include, [rowType]: !state.include[rowType] } }; }), [] ); const handleSearchChange = useCallback( (searchFilter: string) => setFilter((state) => ({ ...state, searchFilter })), [] ); const handleToggleVisibility = useCallback(() => setIsVisible((state) => !state), []); // snapshot functions const handleSelectSnapshot = useCallback((snapId: string) => { setSnapshotId(snapId); }, []); const handleResetSnapshot = useCallback(() => { setSnapshotId(null); handlePopUpClose("snapshots"); }, []); useEffect(() => { // restore filters for path if set const restore = filterHistory.get(secretPath); setFilter(restore ?? defaultFilterState); setDebouncedSearchFilter(restore?.searchFilter ?? ""); const { searchFilter, ...query } = router.query; // this is a temp work around until we fully transition state to query params, // setting the initial search filter by query and then moving it to internal state if (router.query.searchFilter) { router.push({ pathname: router.pathname, query }); } }, [secretPath]); if (isDetailsLoading) { return ; } const handleResetFilter = () => { // store for breadcrumb nav to restore previously used filters setFilterHistory((prev) => { const curr = new Map(prev); curr.set(secretPath, filter); return curr; }); setFilter(defaultFilterState); setDebouncedSearchFilter(""); }; return (
{!isRollbackMode ? ( <> handlePopUpToggle("snapshots", true)} />
{isNotEmpty && (
{ if (evt.key === "Enter") handleSortToggle(); }} > Key
Value
)} {canReadSecretImports && Boolean(imports?.length) && ( )} {Boolean(folders?.length) && ( )} {canReadDynamicSecret && Boolean(dynamicSecrets?.length) && ( )} {canReadSecret && Boolean(secrets?.length) && ( )} {!canReadSecret && !canReadDynamicSecret && !canReadSecretImports && folders?.length === 0 && }
{!isDetailsLoading && totalCount > 0 && ( } className="rounded-b-md border-t border-solid border-t-mineshaft-600" count={totalCount} page={page} perPage={perPage} onChangePage={(newPage) => setPage(newPage)} onChangePerPage={(newPerPage) => setPerPage(newPerPage)} /> )} handlePopUpToggle("snapshots", isOpen)} hasNextPage={hasNextSnapshotListPage} fetchNextPage={fetchNextSnapshotList} onSelectSnapshot={handleSelectSnapshot} isFetchingNextPage={isFetchingNextSnapshotList} /> ) : ( handlePopUpToggle("snapshots", true)} /> )}
); };