mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 19:28:35 +00:00
feature: server-side pagination/filtering/sorting for secrets overview and main pages
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
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";
|
||||
@@ -17,29 +18,28 @@ import {
|
||||
} from "@app/context";
|
||||
import { useDebounce, usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useGetDynamicSecrets,
|
||||
useGetImportedSecretsSingleEnv,
|
||||
useGetProjectFolders,
|
||||
useGetProjectSecrets,
|
||||
useGetSecretApprovalPolicyOfABoard,
|
||||
useGetSecretImports,
|
||||
useGetWorkspaceSnapshotList,
|
||||
useGetWsSnapshotCount,
|
||||
useGetWsTags
|
||||
} from "@app/hooks/api";
|
||||
import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
|
||||
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 { DynamicSecretListView } from "./components/DynamicSecretListView";
|
||||
import { FolderListView } from "./components/FolderListView";
|
||||
import { PitDrawer } from "./components/PitDrawer";
|
||||
import { SecretDropzone } from "./components/SecretDropzone";
|
||||
import { SecretImportListView } from "./components/SecretImportListView";
|
||||
import { SecretListView } from "./components/SecretListView";
|
||||
import { SnapshotView } from "./components/SnapshotView";
|
||||
import { StoreProvider } from "./SecretMainPage.store";
|
||||
import { Filter, SortDir } from "./SecretMainPage.types";
|
||||
import { Filter, RowType } from "./SecretMainPage.types";
|
||||
|
||||
const LOADER_TEXT = [
|
||||
"Retrieving your encrypted secrets...",
|
||||
@@ -55,12 +55,8 @@ export const SecretMainPage = () => {
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [sortDir, setSortDir] = useState<SortDir>(SortDir.ASC);
|
||||
const [filter, setFilter] = useState<Filter>({
|
||||
tags: {},
|
||||
searchFilter: (router.query.searchFilter as string) || ""
|
||||
});
|
||||
const debouncedSearchFilter = useDebounce(filter.searchFilter);
|
||||
const [orderDirection, setOrderDirection] = useState<OrderByDirection>(OrderByDirection.ASC);
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||
const paginationOffset = (page - 1) * perPage;
|
||||
@@ -83,6 +79,18 @@ export const SecretMainPage = () => {
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
const [filter, setFilter] = useState<Filter>({
|
||||
tags: {},
|
||||
searchFilter: (router.query.searchFilter as string) || "",
|
||||
include: {
|
||||
[RowType.Folder]: true,
|
||||
[RowType.Import]: canReadSecret,
|
||||
[RowType.DynamicSecret]: canReadSecret,
|
||||
[RowType.Secret]: canReadSecret
|
||||
}
|
||||
});
|
||||
const debouncedSearchFilter = useDebounce(filter.searchFilter);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isWorkspaceLoading &&
|
||||
@@ -91,43 +99,43 @@ export const SecretMainPage = () => {
|
||||
) {
|
||||
router.push(`/project/${workspaceId}/secrets/overview`);
|
||||
createNotification({
|
||||
text: "No envronment found with given slug",
|
||||
text: "No environment found with given slug",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}, [isWorkspaceLoading, currentWorkspace, environment, router.isReady]);
|
||||
|
||||
// fetch secrets
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
options: {
|
||||
enabled: canReadSecret
|
||||
}
|
||||
});
|
||||
|
||||
// fetch folders
|
||||
const { data: folders, isLoading: isFoldersLoading } = useGetProjectFolders({
|
||||
projectId: workspaceId,
|
||||
environment,
|
||||
path: secretPath
|
||||
});
|
||||
|
||||
// fetch secret imports
|
||||
const {
|
||||
data: secretImports,
|
||||
isLoading: isSecretImportsLoading,
|
||||
isFetching: isSecretImportsFetching
|
||||
} = useGetSecretImports({
|
||||
projectId: workspaceId,
|
||||
data,
|
||||
isLoading: isDetailsLoading,
|
||||
isFetching: isDetailsFetching
|
||||
} = useGetProjectSecretsDetails({
|
||||
environment,
|
||||
path: secretPath,
|
||||
options: {
|
||||
enabled: canReadSecret
|
||||
}
|
||||
projectId: workspaceId,
|
||||
secretPath,
|
||||
offset: paginationOffset,
|
||||
limit: perPage,
|
||||
search: debouncedSearchFilter,
|
||||
orderDirection,
|
||||
includeImports: canReadSecret && filter.include.import,
|
||||
includeFolders: filter.include.folder,
|
||||
includeDynamicSecrets: canReadSecret && 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 ?? {};
|
||||
|
||||
// fetch imported secrets to show user the overriden ones
|
||||
const { data: importedSecrets } = useGetImportedSecretsSingleEnv({
|
||||
projectId: workspaceId,
|
||||
@@ -138,13 +146,7 @@ export const SecretMainPage = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const { data: dynamicSecrets, isLoading: isDynamicSecretLoading } = useGetDynamicSecrets({
|
||||
projectSlug,
|
||||
environmentSlug: environment,
|
||||
path: secretPath
|
||||
});
|
||||
|
||||
// fech tags
|
||||
// fetch tags
|
||||
const { data: tags } = useGetWsTags(canReadSecret ? workspaceId : "");
|
||||
|
||||
const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({
|
||||
@@ -174,12 +176,14 @@ export const SecretMainPage = () => {
|
||||
isPaused: !canDoReadRollback
|
||||
});
|
||||
|
||||
const isNotEmtpy = Boolean(
|
||||
secrets?.length || folders?.length || secretImports?.length || dynamicSecrets?.length
|
||||
const isNotEmpty = Boolean(
|
||||
secrets?.length || folders?.length || imports?.length || dynamicSecrets?.length
|
||||
);
|
||||
|
||||
const handleSortToggle = () =>
|
||||
setSortDir((state) => (state === SortDir.ASC ? SortDir.DESC : SortDir.ASC));
|
||||
setOrderDirection((state) =>
|
||||
state === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||
);
|
||||
|
||||
const handleEnvChange = (slug: string) => {
|
||||
const query: Record<string, string> = { ...router.query, env: slug };
|
||||
@@ -191,17 +195,31 @@ export const SecretMainPage = () => {
|
||||
};
|
||||
|
||||
const handleTagToggle = useCallback(
|
||||
(tagId: string) =>
|
||||
(tagSlug: string) =>
|
||||
setFilter((state) => {
|
||||
const isTagPresent = Boolean(state.tags?.[tagId]);
|
||||
const isTagPresent = Boolean(state.tags?.[tagSlug]);
|
||||
const newTagFilter = { ...state.tags };
|
||||
if (isTagPresent) delete newTagFilter[tagId];
|
||||
else newTagFilter[tagId] = true;
|
||||
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 })),
|
||||
[]
|
||||
@@ -219,126 +237,18 @@ export const SecretMainPage = () => {
|
||||
handlePopUpClose("snapshots");
|
||||
}, []);
|
||||
|
||||
// loading screen when u have permission
|
||||
const loadingOnAccess =
|
||||
canReadSecret &&
|
||||
(isSecretsLoading || isSecretImportsLoading || isFoldersLoading || isDynamicSecretLoading);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const filteredSecrets =
|
||||
secrets
|
||||
?.filter(({ key, tags: secretTags, value }) => {
|
||||
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
|
||||
return (
|
||||
(!isTagFilterActive || secretTags?.some(({ id }) => filter.tags?.[id])) &&
|
||||
(key.toUpperCase().includes(debouncedSearchFilter.toUpperCase()) ||
|
||||
value?.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||
);
|
||||
})
|
||||
.sort((a, b) =>
|
||||
sortDir === SortDir.ASC ? a.key.localeCompare(b.key) : b.key.localeCompare(a.key)
|
||||
) ?? [];
|
||||
const filteredFolders =
|
||||
folders
|
||||
?.filter(({ name }) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||
.sort((a, b) =>
|
||||
sortDir === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
||||
) ?? [];
|
||||
const filteredDynamicSecrets =
|
||||
dynamicSecrets
|
||||
?.filter(({ name }) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||
.sort((a, b) =>
|
||||
sortDir === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
||||
) ?? [];
|
||||
const filteredSecretImports =
|
||||
secretImports
|
||||
?.filter(({ importPath }) =>
|
||||
importPath.toLowerCase().includes(debouncedSearchFilter.toLowerCase())
|
||||
)
|
||||
.sort((a, b) =>
|
||||
sortDir === "asc"
|
||||
? a.importPath.localeCompare(b.importPath)
|
||||
: b.importPath.localeCompare(a.importPath)
|
||||
) ?? [];
|
||||
|
||||
const totalRows =
|
||||
filteredSecretImports.length +
|
||||
filteredFolders.length +
|
||||
filteredDynamicSecrets.length +
|
||||
filteredSecrets.length;
|
||||
|
||||
const paginatedImports = filteredSecretImports.slice(
|
||||
paginationOffset,
|
||||
paginationOffset + perPage
|
||||
);
|
||||
|
||||
let remainingRows = perPage - paginatedImports.length;
|
||||
const foldersStartIndex = Math.max(0, paginationOffset - filteredSecretImports.length);
|
||||
const paginatedFolders =
|
||||
remainingRows > 0
|
||||
? filteredFolders.slice(foldersStartIndex, foldersStartIndex + remainingRows)
|
||||
: [];
|
||||
|
||||
remainingRows -= paginatedFolders.length;
|
||||
const dynamicSecretStartIndex = Math.max(
|
||||
0,
|
||||
paginationOffset - filteredSecretImports.length - filteredFolders.length
|
||||
);
|
||||
const paginatiedDynamicSecrets =
|
||||
remainingRows > 0
|
||||
? filteredDynamicSecrets.slice(
|
||||
dynamicSecretStartIndex,
|
||||
dynamicSecretStartIndex + remainingRows
|
||||
)
|
||||
: [];
|
||||
|
||||
remainingRows -= paginatiedDynamicSecrets.length;
|
||||
const secretStartIndex = Math.max(
|
||||
0,
|
||||
paginationOffset -
|
||||
filteredSecretImports.length -
|
||||
filteredFolders.length -
|
||||
filteredDynamicSecrets.length
|
||||
);
|
||||
|
||||
const paginatiedSecrets =
|
||||
remainingRows > 0
|
||||
? filteredSecrets.slice(secretStartIndex, secretStartIndex + remainingRows)
|
||||
: [];
|
||||
|
||||
return {
|
||||
imports: paginatedImports,
|
||||
folders: paginatedFolders,
|
||||
secrets: paginatiedSecrets,
|
||||
dynamicSecrets: paginatiedDynamicSecrets,
|
||||
totalRows
|
||||
};
|
||||
}, [
|
||||
sortDir,
|
||||
debouncedSearchFilter,
|
||||
folders,
|
||||
secrets,
|
||||
dynamicSecrets,
|
||||
paginationOffset,
|
||||
perPage,
|
||||
filter.tags,
|
||||
importedSecrets
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (rows.totalRows < paginationOffset) setPage(1);
|
||||
}, [rows.totalRows]);
|
||||
if (totalCount < paginationOffset) setPage(1);
|
||||
}, [totalCount]);
|
||||
|
||||
// loading screen when you don't have permission but as folder's is viewable need to wait for that
|
||||
const loadingOnDenied = !canReadSecret && isFoldersLoading;
|
||||
if (loadingOnAccess || loadingOnDenied) {
|
||||
if (isDetailsLoading) {
|
||||
return <ContentLoader text={LOADER_TEXT} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StoreProvider>
|
||||
<div className="container mx-auto flex h-full flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<SecretV2MigrationSection />
|
||||
<div className="relative right-6 -top-2 mb-2 ml-6">
|
||||
<NavHeader
|
||||
@@ -372,17 +282,22 @@ export const SecretMainPage = () => {
|
||||
isVisible={isVisible}
|
||||
filter={filter}
|
||||
tags={tags}
|
||||
onVisiblilityToggle={handleToggleVisibility}
|
||||
onVisibilityToggle={handleToggleVisibility}
|
||||
onSearchChange={handleSearchChange}
|
||||
onToggleTagFilter={handleTagToggle}
|
||||
snapshotCount={snapshotCount || 0}
|
||||
isSnapshotCountLoading={isSnapshotCountLoading}
|
||||
onToggleRowType={handleToggleRowType}
|
||||
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
|
||||
/>
|
||||
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300">
|
||||
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
|
||||
<div className="flex flex-col" id="dashboard">
|
||||
{isNotEmtpy && (
|
||||
<div className="flex border-b border-mineshaft-600 font-medium">
|
||||
{isNotEmpty && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"sticky top-0 flex border-b border-mineshaft-600 bg-mineshaft-800 font-medium"
|
||||
)}
|
||||
>
|
||||
<div style={{ width: "2.8rem" }} className="flex-shrink-0 px-4 py-3" />
|
||||
<div
|
||||
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 px-4 py-2"
|
||||
@@ -395,45 +310,43 @@ export const SecretMainPage = () => {
|
||||
>
|
||||
Key
|
||||
<FontAwesomeIcon
|
||||
icon={sortDir === SortDir.ASC ? faArrowDown : faArrowUp}
|
||||
icon={orderDirection === OrderByDirection.ASC ? faArrowDown : faArrowUp}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow px-4 py-2">Value</div>
|
||||
</div>
|
||||
)}
|
||||
{canReadSecret && (
|
||||
{canReadSecret && imports?.length && (
|
||||
<SecretImportListView
|
||||
searchTerm={filter.searchFilter}
|
||||
secretImports={rows.imports}
|
||||
isFetching={isSecretImportsLoading || isSecretImportsFetching}
|
||||
searchTerm={debouncedSearchFilter}
|
||||
secretImports={imports}
|
||||
isFetching={isDetailsFetching}
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
secrets={secrets}
|
||||
importedSecrets={importedSecrets}
|
||||
/>
|
||||
)}
|
||||
<FolderListView
|
||||
folders={rows.folders}
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
sortDir={sortDir}
|
||||
searchTerm={filter.searchFilter}
|
||||
/>
|
||||
{canReadSecret && (
|
||||
{folders?.length && (
|
||||
<FolderListView
|
||||
folders={folders}
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
/>
|
||||
)}
|
||||
{canReadSecret && dynamicSecrets?.length && (
|
||||
<DynamicSecretListView
|
||||
sortDir={sortDir}
|
||||
environment={environment}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecrets={rows.dynamicSecrets || []}
|
||||
dynamicSecrets={dynamicSecrets}
|
||||
/>
|
||||
)}
|
||||
{canReadSecret && (
|
||||
{canReadSecret && secrets?.length && (
|
||||
<SecretListView
|
||||
secrets={rows.secrets}
|
||||
secrets={secrets}
|
||||
tags={tags}
|
||||
isVisible={isVisible}
|
||||
environment={environment}
|
||||
@@ -443,18 +356,26 @@ export const SecretMainPage = () => {
|
||||
/>
|
||||
)}
|
||||
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
|
||||
{!loadingOnAccess && rows.totalRows > INIT_PER_PAGE && (
|
||||
<Pagination
|
||||
className="border-t border-solid border-t-mineshaft-600"
|
||||
count={rows.totalRows}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!isDetailsLoading && totalCount > INIT_PER_PAGE && (
|
||||
<Pagination
|
||||
startAdornment={
|
||||
<SecretTableResourceCount
|
||||
dynamicSecretCount={totalDynamicSecretCount}
|
||||
importCount={totalImportCount}
|
||||
secretCount={totalSecretCount}
|
||||
folderCount={totalFolderCount}
|
||||
/>
|
||||
}
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
<CreateSecretForm
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
@@ -467,7 +388,7 @@ export const SecretMainPage = () => {
|
||||
environment={environment}
|
||||
workspaceId={workspaceId}
|
||||
secretPath={secretPath}
|
||||
isSmaller={isNotEmtpy}
|
||||
isSmaller={isNotEmpty}
|
||||
environments={currentWorkspace?.environments}
|
||||
isProtectedBranch={isProtectedBranch}
|
||||
/>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
export type Filter = {
|
||||
tags: Record<string, boolean>;
|
||||
searchFilter: string;
|
||||
include: {
|
||||
[key in RowType]: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export enum SortDir {
|
||||
ASC = "asc",
|
||||
DESC = "desc"
|
||||
}
|
||||
|
||||
export enum RowType {
|
||||
Folder = "folder",
|
||||
Import = "import",
|
||||
DynamicSecret = "dynamic",
|
||||
Secret = "Secret"
|
||||
Secret = "secret"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
faFileImport,
|
||||
faFilter,
|
||||
faFingerprint,
|
||||
faFolder,
|
||||
faFolderPlus,
|
||||
faKey,
|
||||
faMagnifyingGlass,
|
||||
faMinusSquare,
|
||||
faPlus,
|
||||
@@ -62,7 +64,7 @@ import {
|
||||
useSelectedSecretActions,
|
||||
useSelectedSecrets
|
||||
} from "../../SecretMainPage.store";
|
||||
import { Filter } from "../../SecretMainPage.types";
|
||||
import { Filter, RowType } from "../../SecretMainPage.types";
|
||||
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
||||
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
||||
import { FolderForm } from "./FolderForm";
|
||||
@@ -83,7 +85,8 @@ type Props = {
|
||||
isSnapshotCountLoading?: boolean;
|
||||
onSearchChange: (term: string) => void;
|
||||
onToggleTagFilter: (tagId: string) => void;
|
||||
onVisiblilityToggle: () => void;
|
||||
onVisibilityToggle: () => void;
|
||||
onToggleRowType: (rowType: RowType) => void;
|
||||
onClickRollbackMode: () => void;
|
||||
};
|
||||
|
||||
@@ -100,8 +103,9 @@ export const ActionBar = ({
|
||||
isSnapshotCountLoading,
|
||||
onSearchChange,
|
||||
onToggleTagFilter,
|
||||
onVisiblilityToggle,
|
||||
onClickRollbackMode
|
||||
onVisibilityToggle,
|
||||
onClickRollbackMode,
|
||||
onToggleRowType
|
||||
}: Props) => {
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"addFolder",
|
||||
@@ -298,7 +302,9 @@ export const ActionBar = ({
|
||||
ariaLabel="Download"
|
||||
className={twMerge(
|
||||
"transition-all",
|
||||
Object.keys(filter.tags).length && "border-primary/50 text-primary"
|
||||
(Object.keys(filter.tags).length ||
|
||||
Object.values(filter.include).filter((include) => !include).length) &&
|
||||
"border-primary/50 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFilter} />
|
||||
@@ -306,6 +312,60 @@ export const ActionBar = ({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="p-0">
|
||||
<DropdownMenuGroup>Filter By</DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onToggleRowType(RowType.Import);
|
||||
}}
|
||||
icon={filter?.include[RowType.Import] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faFileImport} className=" text-green-700" />
|
||||
<span>Imports</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onToggleRowType(RowType.Folder);
|
||||
}}
|
||||
icon={filter?.include[RowType.Folder] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-yellow-700" />
|
||||
<span>Folders</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onToggleRowType(RowType.DynamicSecret);
|
||||
}}
|
||||
icon={
|
||||
filter?.include[RowType.DynamicSecret] && <FontAwesomeIcon icon={faCheckCircle} />
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faFingerprint} className=" text-yellow-700" />
|
||||
<span>Dynamic Secrets</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onToggleRowType(RowType.Secret);
|
||||
}}
|
||||
icon={filter?.include[RowType.Secret] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faKey} className=" text-bunker-300" />
|
||||
<span>Secrets</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownSubMenu>
|
||||
<DropdownSubMenuTrigger
|
||||
iconPos="right"
|
||||
@@ -319,10 +379,10 @@ export const ActionBar = ({
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onToggleTagFilter(id);
|
||||
onToggleTagFilter(slug);
|
||||
}}
|
||||
key={id}
|
||||
icon={filter?.tags[id] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
icon={filter?.tags[slug] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
@@ -346,7 +406,7 @@ export const ActionBar = ({
|
||||
</IconButton>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton variant="outline_bg" ariaLabel="Reveal" onClick={onVisiblilityToggle}>
|
||||
<IconButton variant="outline_bg" ariaLabel="Reveal" onClick={onVisibilityToggle}>
|
||||
<FontAwesomeIcon icon={isVisible ? faEyeSlash : faEye} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
+142
-156
@@ -27,7 +27,6 @@ import {
|
||||
TDynamicSecret
|
||||
} from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
import { SortDir } from "../../SecretMainPage.types";
|
||||
import { CreateDynamicSecretLease } from "./CreateDynamicSecretLease";
|
||||
import { DynamicSecretLease } from "./DynamicSecretLease";
|
||||
import { EditDynamicSecretForm } from "./EditDynamicSecretForm";
|
||||
@@ -38,19 +37,17 @@ const formatProviderName = (type: DynamicSecretProviders) => {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
dynamicSecrets: TDynamicSecret[];
|
||||
dynamicSecrets?: TDynamicSecret[];
|
||||
environment: string;
|
||||
projectSlug: string;
|
||||
secretPath?: string;
|
||||
sortDir: SortDir;
|
||||
};
|
||||
|
||||
export const DynamicSecretListView = ({
|
||||
dynamicSecrets = [],
|
||||
environment,
|
||||
projectSlug,
|
||||
secretPath = "/",
|
||||
sortDir = SortDir.ASC
|
||||
secretPath = "/"
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"dynamicSecretLeases",
|
||||
@@ -59,7 +56,6 @@ export const DynamicSecretListView = ({
|
||||
"deleteDynamicSecret"
|
||||
] as const);
|
||||
|
||||
|
||||
const deleteDynamicSecret = useDeleteDynamicSecret();
|
||||
|
||||
const handleDynamicSecretDelete = async () => {
|
||||
@@ -90,158 +86,148 @@ export const DynamicSecretListView = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{dynamicSecrets
|
||||
.sort((a, b) =>
|
||||
sortDir === SortDir.ASC
|
||||
? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
: b.name.toLowerCase().localeCompare(a.name.toLowerCase())
|
||||
)
|
||||
.map((secret) => {
|
||||
const isRevocking = secret.status === DynamicSecretStatus.Deleting;
|
||||
return (
|
||||
<Modal
|
||||
key={secret.id}
|
||||
isOpen={
|
||||
popUp.dynamicSecretLeases.isOpen && popUp.dynamicSecretLeases.data === secret.id
|
||||
}
|
||||
onOpenChange={(state) => handlePopUpToggle("dynamicSecretLeases", state)}
|
||||
{dynamicSecrets.map((secret) => {
|
||||
const isRevoking = secret.status === DynamicSecretStatus.Deleting;
|
||||
return (
|
||||
<Modal
|
||||
key={secret.id}
|
||||
isOpen={
|
||||
popUp.dynamicSecretLeases.isOpen && popUp.dynamicSecretLeases.data === secret.id
|
||||
}
|
||||
onOpenChange={(state) => handlePopUpToggle("dynamicSecretLeases", state)}
|
||||
>
|
||||
<div
|
||||
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter" && !isRevoking)
|
||||
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!isRevoking) {
|
||||
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter" && !isRevocking)
|
||||
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!isRevocking) {
|
||||
handlePopUpOpen("dynamicSecretLeases", secret.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex w-11 items-center px-5 py-3 text-yellow-700">
|
||||
<FontAwesomeIcon icon={faFingerprint} />
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-grow items-center px-4 py-3"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{secret.name}
|
||||
<Tag className="ml-4 py-0 px-2 text-xs normal-case">
|
||||
{formatProviderName(secret.type)}
|
||||
</Tag>
|
||||
{Boolean(secret.status) && (
|
||||
<Tooltip content={secret?.statusDetails || secret.status || ""}>
|
||||
<FontAwesomeIcon
|
||||
className={
|
||||
secret.status === DynamicSecretStatus.Deleting
|
||||
? "text-yellow-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
icon={faWarning}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 px-4 py-2">
|
||||
<Button
|
||||
size="xs"
|
||||
className="m-0 py-0.5 px-2 opacity-0 group-hover:opacity-100"
|
||||
isDisabled={isRevocking}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("createDynamicSecretLease", secret);
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
{secret.status === DynamicSecretStatus.FailedDeletion && (
|
||||
<Tooltip content="This action will remove the secret from internal storage, but it will remain in external systems. Use this option only after you've confirmed that your external leases are handled.">
|
||||
<Button
|
||||
size="xs"
|
||||
className="m-0 py-0.5 px-2"
|
||||
colorSchema="danger"
|
||||
isDisabled={isRevocking}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("deleteDynamicSecret", {
|
||||
...secret,
|
||||
isForced: true
|
||||
});
|
||||
}}
|
||||
>
|
||||
Force Delete
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="edit-dynamic-secret"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("updateDynamicSecret", secret);
|
||||
}}
|
||||
isDisabled={!isAllowed || isRevocking}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-dynamic-secret"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("deleteDynamicSecret", secret);
|
||||
}}
|
||||
isDisabled={!isAllowed || isRevocking}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="flex w-11 items-center px-5 py-3 text-yellow-700">
|
||||
<FontAwesomeIcon icon={faFingerprint} />
|
||||
</div>
|
||||
<ModalContent
|
||||
title="Dynamic secret leases"
|
||||
subTitle="Revoke or renew your secret leases"
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<DynamicSecretLease
|
||||
onClickNewLease={() => handlePopUpOpen("createDynamicSecretLease", secret)}
|
||||
onClose={() => handlePopUpClose("dynamicSecretLeases")}
|
||||
projectSlug={projectSlug}
|
||||
key={secret.id}
|
||||
dynamicSecretName={secret.name}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-grow items-center px-4 py-3" role="button" tabIndex={0}>
|
||||
{secret.name}
|
||||
<Tag className="ml-4 py-0 px-2 text-xs normal-case">
|
||||
{formatProviderName(secret.type)}
|
||||
</Tag>
|
||||
{Boolean(secret.status) && (
|
||||
<Tooltip content={secret?.statusDetails || secret.status || ""}>
|
||||
<FontAwesomeIcon
|
||||
className={
|
||||
secret.status === DynamicSecretStatus.Deleting
|
||||
? "text-yellow-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
icon={faWarning}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 px-4 py-2">
|
||||
<Button
|
||||
size="xs"
|
||||
className="m-0 py-0.5 px-2 opacity-0 group-hover:opacity-100"
|
||||
isDisabled={isRevoking}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("createDynamicSecretLease", secret);
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
{secret.status === DynamicSecretStatus.FailedDeletion && (
|
||||
<Tooltip content="This action will remove the secret from internal storage, but it will remain in external systems. Use this option only after you've confirmed that your external leases are handled.">
|
||||
<Button
|
||||
size="xs"
|
||||
className="m-0 py-0.5 px-2"
|
||||
colorSchema="danger"
|
||||
isDisabled={isRevoking}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("deleteDynamicSecret", {
|
||||
...secret,
|
||||
isForced: true
|
||||
});
|
||||
}}
|
||||
>
|
||||
Force Delete
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="edit-dynamic-secret"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("updateDynamicSecret", secret);
|
||||
}}
|
||||
isDisabled={!isAllowed || isRevoking}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-dynamic-secret"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("deleteDynamicSecret", secret);
|
||||
}}
|
||||
isDisabled={!isAllowed || isRevoking}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<ModalContent
|
||||
title="Dynamic secret leases"
|
||||
subTitle="Revoke or renew your secret leases"
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<DynamicSecretLease
|
||||
onClickNewLease={() => handlePopUpOpen("createDynamicSecretLease", secret)}
|
||||
onClose={() => handlePopUpClose("dynamicSecretLeases")}
|
||||
projectSlug={projectSlug}
|
||||
key={secret.id}
|
||||
dynamicSecretName={secret.name}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
})}
|
||||
<Modal
|
||||
isOpen={popUp.createDynamicSecretLease.isOpen}
|
||||
onOpenChange={(state) => handlePopUpToggle("createDynamicSecretLease", state)}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||
|
||||
import { SortDir } from "../../SecretMainPage.types";
|
||||
import { FolderForm } from "../ActionBar/FolderForm";
|
||||
|
||||
type Props = {
|
||||
@@ -19,17 +18,13 @@ type Props = {
|
||||
environment: string;
|
||||
workspaceId: string;
|
||||
secretPath?: string;
|
||||
sortDir: SortDir;
|
||||
searchTerm?: string;
|
||||
};
|
||||
|
||||
export const FolderListView = ({
|
||||
folders = [],
|
||||
environment,
|
||||
workspaceId,
|
||||
searchTerm,
|
||||
secretPath = "/",
|
||||
sortDir = SortDir.ASC
|
||||
secretPath = "/"
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"updateFolder",
|
||||
@@ -104,84 +99,77 @@ export const FolderListView = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{folders
|
||||
.filter(({ name }) => name.toUpperCase().includes(String(searchTerm?.toUpperCase())))
|
||||
.sort((a, b) =>
|
||||
sortDir === SortDir.ASC
|
||||
? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
: b.name.toLowerCase().localeCompare(a.name.toLowerCase())
|
||||
)
|
||||
.map(({ name, id }) => (
|
||||
<div
|
||||
key={id}
|
||||
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="flex w-11 items-center px-5 py-3 text-yellow-700">
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-grow items-center px-4 py-3"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") handleFolderClick(name);
|
||||
}}
|
||||
onClick={() => handleFolderClick(name)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(
|
||||
shouldCheckFolderPermission
|
||||
? ProjectPermissionSub.SecretFolders
|
||||
: ProjectPermissionSub.Secrets,
|
||||
{ environment, secretPath }
|
||||
)}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="edit-folder"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("updateFolder", { id, name })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(
|
||||
shouldCheckFolderPermission
|
||||
? ProjectPermissionSub.SecretFolders
|
||||
: ProjectPermissionSub.Secrets,
|
||||
{ environment, secretPath }
|
||||
)}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-folder"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{folders.map(({ name, id }) => (
|
||||
<div
|
||||
key={id}
|
||||
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="flex w-11 items-center px-5 py-3 text-yellow-700">
|
||||
<FontAwesomeIcon icon={faFolder} />
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className="flex flex-grow items-center px-4 py-3"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") handleFolderClick(name);
|
||||
}}
|
||||
onClick={() => handleFolderClick(name)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(
|
||||
shouldCheckFolderPermission
|
||||
? ProjectPermissionSub.SecretFolders
|
||||
: ProjectPermissionSub.Secrets,
|
||||
{ environment, secretPath }
|
||||
)}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="edit-folder"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("updateFolder", { id, name })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(
|
||||
shouldCheckFolderPermission
|
||||
? ProjectPermissionSub.SecretFolders
|
||||
: ProjectPermissionSub.Secrets,
|
||||
{ environment, secretPath }
|
||||
)}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-folder"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Modal
|
||||
isOpen={popUp.updateFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("updateFolder", isOpen)}
|
||||
|
||||
@@ -14,9 +14,10 @@ import { Button, Modal, ModalContent } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api";
|
||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
|
||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
||||
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
|
||||
@@ -190,6 +191,9 @@ export const SecretDropzone = ({
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
|
||||
);
|
||||
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
|
||||
handlePopUpClose("overlapKeyWarning");
|
||||
createNotification({
|
||||
|
||||
+4
-4
@@ -291,7 +291,7 @@ export const SecretImportItem = ({
|
||||
<tr>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Key</td>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Value</td>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Override</td>
|
||||
{/* <td style={{ padding: "0.25rem 1rem" }}>Override</td> */}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -304,7 +304,7 @@ export const SecretImportItem = ({
|
||||
)}
|
||||
{importedSecrets
|
||||
.filter((secret) => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
|
||||
.map(({ key, value, overriden }, index) => (
|
||||
.map(({ key, value }, index) => (
|
||||
<tr key={`${id}-${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
@@ -312,9 +312,9 @@ export const SecretImportItem = ({
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput value={value} isReadOnly />
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{/* <td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
</td>
|
||||
</td> */}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
+8
-8
@@ -90,18 +90,18 @@ type Props = {
|
||||
secretPath?: string;
|
||||
secretImports?: TSecretImport[];
|
||||
isFetching?: boolean;
|
||||
secrets?: SecretV3RawSanitized[];
|
||||
// secrets?: SecretV3RawSanitized[];
|
||||
importedSecrets?: TImportedSecrets;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export const SecretImportListView = ({
|
||||
secretImports = [],
|
||||
secretImports,
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
importedSecrets,
|
||||
secrets = [],
|
||||
// secrets = [],
|
||||
isFetching,
|
||||
searchTerm
|
||||
}: Props) => {
|
||||
@@ -117,11 +117,11 @@ export const SecretImportListView = ({
|
||||
useSensor(KeyboardSensor, {})
|
||||
);
|
||||
|
||||
const [items, setItems] = useState(secretImports);
|
||||
const [items, setItems] = useState(secretImports ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetching) {
|
||||
setItems(secretImports);
|
||||
setItems(secretImports ?? []);
|
||||
}
|
||||
}, [isFetching, secretImports]);
|
||||
|
||||
@@ -170,7 +170,7 @@ export const SecretImportListView = ({
|
||||
};
|
||||
|
||||
const handleOpenReplicationSecrets = (replicationImportId: string) => {
|
||||
const reservedImport = secretImports.find(
|
||||
const reservedImport = secretImports?.find(
|
||||
({ isReserved, importPath, importEnv }) =>
|
||||
importEnv.slug === environment &&
|
||||
isReserved &&
|
||||
@@ -208,8 +208,8 @@ export const SecretImportListView = ({
|
||||
importedSecrets={computeImportedSecretRows(
|
||||
item.importEnv.slug,
|
||||
item.importPath,
|
||||
importedSecrets,
|
||||
secrets
|
||||
importedSecrets
|
||||
// secrets scott - now that secrets are paginated we are not showing if they are overridden (yet?)
|
||||
)}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CreateTagModal } from "@app/components/tags/CreateTagModal";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
@@ -213,6 +214,12 @@ export const SecretListView = ({
|
||||
});
|
||||
if (cb) cb();
|
||||
}
|
||||
queryClient.invalidateQueries(
|
||||
dashboardKeys.getDashboardSecrets({
|
||||
projectId: workspaceId,
|
||||
secretPath
|
||||
})
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||
);
|
||||
@@ -246,6 +253,9 @@ export const SecretListView = ({
|
||||
try {
|
||||
await handleSecretOperation("delete", SecretType.Shared, key, { secretId });
|
||||
// wrap this in another function and then reuse
|
||||
queryClient.invalidateQueries(
|
||||
dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user