mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 23:26:20 +00:00
Merge pull request #2423 from scott-ray-wilson/secrets-pagination
Feature: Secrets Overview Page Pagination/Optimizations
This commit is contained in:
@@ -40,6 +40,8 @@ export const Pagination = ({
|
|||||||
const upperLimit = Math.ceil(count / perPage);
|
const upperLimit = Math.ceil(count / perPage);
|
||||||
const nextPageNumber = Math.min(upperLimit, page + 1);
|
const nextPageNumber = Math.min(upperLimit, page + 1);
|
||||||
const canGoNext = page + 1 <= upperLimit;
|
const canGoNext = page + 1 <= upperLimit;
|
||||||
|
const canGoFirst = page > 1;
|
||||||
|
const canGoLast = page < upperLimit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -73,6 +75,16 @@ export const Pagination = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="pagination-first"
|
||||||
|
className="relative"
|
||||||
|
onClick={() => onChangePage(1)}
|
||||||
|
isDisabled={!canGoFirst}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="absolute left-2.5 top-1 text-xs" icon={faChevronLeft} />
|
||||||
|
<FontAwesomeIcon className="text-xs" icon={faChevronLeft} />
|
||||||
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="pagination-prev"
|
ariaLabel="pagination-prev"
|
||||||
@@ -89,6 +101,16 @@ export const Pagination = ({
|
|||||||
>
|
>
|
||||||
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
|
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="pagination-last"
|
||||||
|
className="relative"
|
||||||
|
onClick={() => onChangePage(upperLimit)}
|
||||||
|
isDisabled={!canGoLast}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="absolute left-2.5 top-1 text-xs" icon={faChevronRight} />
|
||||||
|
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -163,27 +163,29 @@ export const useGetImportedSecretsAllEnvs = ({
|
|||||||
queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []),
|
queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []),
|
||||||
enabled: Boolean(projectId) && Boolean(env),
|
enabled: Boolean(projectId) && Boolean(env),
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
select: (data: TImportedSecrets[]) => {
|
select: useCallback(
|
||||||
return data.map((el) => ({
|
(data: Awaited<ReturnType<typeof fetchImportedSecrets>>) =>
|
||||||
environment: el.environment,
|
data.map((el) => ({
|
||||||
secretPath: el.secretPath,
|
environment: el.environment,
|
||||||
environmentInfo: el.environmentInfo,
|
secretPath: el.secretPath,
|
||||||
folderId: el.folderId,
|
environmentInfo: el.environmentInfo,
|
||||||
secrets: el.secrets.map((encSecret) => {
|
folderId: el.folderId,
|
||||||
return {
|
secrets: el.secrets.map((encSecret) => {
|
||||||
id: encSecret.id,
|
return {
|
||||||
env: encSecret.environment,
|
id: encSecret.id,
|
||||||
key: encSecret.secretKey,
|
env: encSecret.environment,
|
||||||
value: encSecret.secretValue,
|
key: encSecret.secretKey,
|
||||||
tags: encSecret.tags,
|
value: encSecret.secretValue,
|
||||||
comment: encSecret.secretComment,
|
tags: encSecret.tags,
|
||||||
createdAt: encSecret.createdAt,
|
comment: encSecret.secretComment,
|
||||||
updatedAt: encSecret.updatedAt,
|
createdAt: encSecret.createdAt,
|
||||||
version: encSecret.version
|
updatedAt: encSecret.updatedAt,
|
||||||
};
|
version: encSecret.version
|
||||||
})
|
};
|
||||||
}));
|
})
|
||||||
}
|
})),
|
||||||
|
[]
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export const useGetProjectSecrets = ({
|
|||||||
// wait for all values to be available
|
// wait for all values to be available
|
||||||
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
|
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
|
||||||
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
|
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
|
||||||
queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
const serverResponse = error.response?.data as { message: string };
|
const serverResponse = error.response?.data as { message: string };
|
||||||
@@ -119,7 +119,10 @@ export const useGetProjectSecrets = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
select: ({ secrets }) => mergePersonalSecrets(secrets)
|
select: useCallback(
|
||||||
|
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets),
|
||||||
|
[]
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGetProjectSecretsAllEnv = ({
|
export const useGetProjectSecretsAllEnv = ({
|
||||||
@@ -131,7 +134,11 @@ export const useGetProjectSecretsAllEnv = ({
|
|||||||
|
|
||||||
const secrets = useQueries({
|
const secrets = useQueries({
|
||||||
queries: envs.map((environment) => ({
|
queries: envs.map((environment) => ({
|
||||||
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
|
queryKey: secretKeys.getProjectSecret({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
}),
|
||||||
enabled: Boolean(workspaceId && environment),
|
enabled: Boolean(workspaceId && environment),
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
if (axios.isAxiosError(error) && !isErrorHandled) {
|
if (axios.isAxiosError(error) && !isErrorHandled) {
|
||||||
@@ -147,12 +154,17 @@ export const useGetProjectSecretsAllEnv = ({
|
|||||||
setIsErrorHandled.on();
|
setIsErrorHandled.on();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||||
select: (el: SecretV3RawResponse) =>
|
staleTime: 60 * 1000,
|
||||||
mergePersonalSecrets(el.secrets).reduce<Record<string, SecretV3RawSanitized>>(
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
(prev, curr) => ({ ...prev, [curr.key]: curr }),
|
select: useCallback(
|
||||||
{}
|
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) =>
|
||||||
)
|
mergePersonalSecrets(data.secrets).reduce<Record<string, SecretV3RawSanitized>>(
|
||||||
|
(prev, curr) => ({ ...prev, [curr.key]: curr }),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, 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";
|
||||||
@@ -8,14 +8,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||||
import { ContentLoader } from "@app/components/v2";
|
import { ContentLoader, Pagination } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { useDebounce, usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetDynamicSecrets,
|
useGetDynamicSecrets,
|
||||||
useGetImportedSecretsSingleEnv,
|
useGetImportedSecretsSingleEnv,
|
||||||
@@ -39,7 +39,7 @@ 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 { StoreProvider } from "./SecretMainPage.store";
|
||||||
import { Filter, GroupBy, SortDir } from "./SecretMainPage.types";
|
import { Filter, SortDir } from "./SecretMainPage.types";
|
||||||
|
|
||||||
const LOADER_TEXT = [
|
const LOADER_TEXT = [
|
||||||
"Retrieving your encrypted secrets...",
|
"Retrieving your encrypted secrets...",
|
||||||
@@ -47,6 +47,7 @@ const LOADER_TEXT = [
|
|||||||
"Getting secret import links..."
|
"Getting secret import links..."
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const INIT_PER_PAGE = 10;
|
||||||
export const SecretMainPage = () => {
|
export const SecretMainPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
|
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
|
||||||
@@ -59,6 +60,10 @@ export const SecretMainPage = () => {
|
|||||||
tags: {},
|
tags: {},
|
||||||
searchFilter: (router.query.searchFilter as string) || ""
|
searchFilter: (router.query.searchFilter as string) || ""
|
||||||
});
|
});
|
||||||
|
const debouncedSearchFilter = useDebounce(filter.searchFilter);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||||
|
const paginationOffset = (page - 1) * perPage;
|
||||||
|
|
||||||
const [snapshotId, setSnapshotId] = useState<string | null>(null);
|
const [snapshotId, setSnapshotId] = useState<string | null>(null);
|
||||||
const isRollbackMode = Boolean(snapshotId);
|
const isRollbackMode = Boolean(snapshotId);
|
||||||
@@ -185,11 +190,6 @@ export const SecretMainPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGroupByChange = useCallback(
|
|
||||||
(groupBy?: GroupBy) => setFilter((state) => ({ ...state, groupBy })),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTagToggle = useCallback(
|
const handleTagToggle = useCallback(
|
||||||
(tagId: string) =>
|
(tagId: string) =>
|
||||||
setFilter((state) => {
|
setFilter((state) => {
|
||||||
@@ -223,6 +223,107 @@ export const SecretMainPage = () => {
|
|||||||
const loadingOnAccess =
|
const loadingOnAccess =
|
||||||
canReadSecret &&
|
canReadSecret &&
|
||||||
(isSecretsLoading || isSecretImportsLoading || isFoldersLoading || isDynamicSecretLoading);
|
(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 - filteredFolders.length);
|
||||||
|
const paginatiedDynamicSecrets =
|
||||||
|
remainingRows > 0
|
||||||
|
? filteredDynamicSecrets.slice(
|
||||||
|
dynamicSecretStartIndex,
|
||||||
|
dynamicSecretStartIndex + remainingRows
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
remainingRows -= paginatiedDynamicSecrets.length;
|
||||||
|
const secretStartIndex = Math.max(
|
||||||
|
0,
|
||||||
|
paginationOffset - 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]);
|
||||||
|
|
||||||
// loading screen when you don't have permission but as folder's is viewable need to wait for that
|
// loading screen when you don't have permission but as folder's is viewable need to wait for that
|
||||||
const loadingOnDenied = !canReadSecret && isFoldersLoading;
|
const loadingOnDenied = !canReadSecret && isFoldersLoading;
|
||||||
if (loadingOnAccess || loadingOnDenied) {
|
if (loadingOnAccess || loadingOnDenied) {
|
||||||
@@ -258,7 +359,6 @@ export const SecretMainPage = () => {
|
|||||||
filter={filter}
|
filter={filter}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
onVisiblilityToggle={handleToggleVisibility}
|
onVisiblilityToggle={handleToggleVisibility}
|
||||||
onGroupByChange={handleGroupByChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
onToggleTagFilter={handleTagToggle}
|
onToggleTagFilter={handleTagToggle}
|
||||||
snapshotCount={snapshotCount || 0}
|
snapshotCount={snapshotCount || 0}
|
||||||
@@ -291,7 +391,7 @@ export const SecretMainPage = () => {
|
|||||||
{canReadSecret && (
|
{canReadSecret && (
|
||||||
<SecretImportListView
|
<SecretImportListView
|
||||||
searchTerm={filter.searchFilter}
|
searchTerm={filter.searchFilter}
|
||||||
secretImports={secretImports}
|
secretImports={rows.imports}
|
||||||
isFetching={isSecretImportsLoading || isSecretImportsFetching}
|
isFetching={isSecretImportsLoading || isSecretImportsFetching}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
@@ -301,7 +401,7 @@ export const SecretMainPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FolderListView
|
<FolderListView
|
||||||
folders={folders}
|
folders={rows.folders}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
@@ -314,15 +414,13 @@ export const SecretMainPage = () => {
|
|||||||
environment={environment}
|
environment={environment}
|
||||||
projectSlug={projectSlug}
|
projectSlug={projectSlug}
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
dynamicSecrets={dynamicSecrets || []}
|
dynamicSecrets={rows.dynamicSecrets || []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadSecret && (
|
{canReadSecret && (
|
||||||
<SecretListView
|
<SecretListView
|
||||||
secrets={secrets}
|
secrets={rows.secrets}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
filter={filter}
|
|
||||||
sortDir={sortDir}
|
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
@@ -331,6 +429,16 @@ export const SecretMainPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
|
{!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>
|
||||||
</div>
|
</div>
|
||||||
<CreateSecretForm
|
<CreateSecretForm
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export type Filter = {
|
export type Filter = {
|
||||||
tags: Record<string, boolean>;
|
tags: Record<string, boolean>;
|
||||||
searchFilter: string;
|
searchFilter: string;
|
||||||
groupBy?: GroupBy | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum SortDir {
|
export enum SortDir {
|
||||||
@@ -9,6 +8,8 @@ export enum SortDir {
|
|||||||
DESC = "desc"
|
DESC = "desc"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GroupBy {
|
export enum RowType {
|
||||||
PREFIX = "prefix"
|
Folder = "folder",
|
||||||
|
DynamicSecret = "dynamic",
|
||||||
|
Secret = "Secret"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ import {
|
|||||||
useSelectedSecretActions,
|
useSelectedSecretActions,
|
||||||
useSelectedSecrets
|
useSelectedSecrets
|
||||||
} from "../../SecretMainPage.store";
|
} from "../../SecretMainPage.store";
|
||||||
import { Filter, GroupBy } from "../../SecretMainPage.types";
|
import { Filter } from "../../SecretMainPage.types";
|
||||||
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
||||||
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
||||||
import { FolderForm } from "./FolderForm";
|
import { FolderForm } from "./FolderForm";
|
||||||
@@ -81,7 +81,6 @@ type Props = {
|
|||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
snapshotCount: number;
|
snapshotCount: number;
|
||||||
isSnapshotCountLoading?: boolean;
|
isSnapshotCountLoading?: boolean;
|
||||||
onGroupByChange: (opt?: GroupBy) => void;
|
|
||||||
onSearchChange: (term: string) => void;
|
onSearchChange: (term: string) => void;
|
||||||
onToggleTagFilter: (tagId: string) => void;
|
onToggleTagFilter: (tagId: string) => void;
|
||||||
onVisiblilityToggle: () => void;
|
onVisiblilityToggle: () => void;
|
||||||
@@ -101,7 +100,6 @@ export const ActionBar = ({
|
|||||||
isSnapshotCountLoading,
|
isSnapshotCountLoading,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onToggleTagFilter,
|
onToggleTagFilter,
|
||||||
onGroupByChange,
|
|
||||||
onVisiblilityToggle,
|
onVisiblilityToggle,
|
||||||
onClickRollbackMode
|
onClickRollbackMode
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -307,16 +305,6 @@ export const ActionBar = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="p-0">
|
<DropdownMenuContent align="end" className="p-0">
|
||||||
<DropdownMenuGroup>Group By</DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem
|
|
||||||
iconPos="right"
|
|
||||||
icon={
|
|
||||||
filter?.groupBy === GroupBy.PREFIX && <FontAwesomeIcon icon={faCheckCircle} />
|
|
||||||
}
|
|
||||||
onClick={() => onGroupByChange(!filter.groupBy ? GroupBy.PREFIX : undefined)}
|
|
||||||
>
|
|
||||||
Prefix
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuGroup>Filter By</DropdownMenuGroup>
|
<DropdownMenuGroup>Filter By</DropdownMenuGroup>
|
||||||
<DropdownSubMenu>
|
<DropdownSubMenu>
|
||||||
<DropdownSubMenuTrigger
|
<DropdownSubMenuTrigger
|
||||||
|
|||||||
+1
-1
@@ -123,7 +123,7 @@ export const SecretImportListView = ({
|
|||||||
if (!isFetching) {
|
if (!isFetching) {
|
||||||
setItems(secretImports);
|
setItems(secretImports);
|
||||||
}
|
}
|
||||||
}, [isFetching]);
|
}, [isFetching, secretImports]);
|
||||||
|
|
||||||
const { mutateAsync: deleteSecretImport } = useDeleteSecretImport();
|
const { mutateAsync: deleteSecretImport } = useDeleteSecretImport();
|
||||||
const { mutate: updateSecretImport } = useUpdateSecretImport();
|
const { mutate: updateSecretImport } = useUpdateSecretImport();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { CreateTagModal } from "@app/components/tags/CreateTagModal";
|
import { CreateTagModal } from "@app/components/tags/CreateTagModal";
|
||||||
@@ -16,7 +15,7 @@ import { WsTag } from "@app/hooks/api/types";
|
|||||||
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
|
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
|
||||||
|
|
||||||
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||||
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
|
import { Filter } from "../../SecretMainPage.types";
|
||||||
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
|
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
|
||||||
import { SecretItem } from "./SecretItem";
|
import { SecretItem } from "./SecretItem";
|
||||||
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
||||||
@@ -26,53 +25,11 @@ type Props = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
filter: Filter;
|
|
||||||
sortDir?: SortDir;
|
|
||||||
tags?: WsTag[];
|
tags?: WsTag[];
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isProtectedBranch?: boolean;
|
isProtectedBranch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reorderSecretGroupByUnderscore = (secrets: SecretV3RawSanitized[], sortDir: SortDir) => {
|
|
||||||
const groupedSecrets: Record<string, SecretV3RawSanitized[]> = {};
|
|
||||||
secrets.forEach((secret) => {
|
|
||||||
const lastSeperatorIndex = secret.key.lastIndexOf("_");
|
|
||||||
const namespace =
|
|
||||||
lastSeperatorIndex !== -1 ? secret.key.substring(0, lastSeperatorIndex) : "misc";
|
|
||||||
if (!groupedSecrets?.[namespace]) groupedSecrets[namespace] = [];
|
|
||||||
groupedSecrets[namespace].push(secret);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.keys(groupedSecrets)
|
|
||||||
.sort((a, b) =>
|
|
||||||
sortDir === SortDir.ASC
|
|
||||||
? a.toLowerCase().localeCompare(b.toLowerCase())
|
|
||||||
: b.toLowerCase().localeCompare(a.toLowerCase())
|
|
||||||
)
|
|
||||||
.map((namespace) => ({ namespace, secrets: groupedSecrets[namespace] }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const reorderSecret = (
|
|
||||||
secrets: SecretV3RawSanitized[],
|
|
||||||
sortDir: SortDir,
|
|
||||||
filter?: GroupBy | null
|
|
||||||
) => {
|
|
||||||
if (filter === GroupBy.PREFIX) {
|
|
||||||
return reorderSecretGroupByUnderscore(secrets, sortDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
namespace: "",
|
|
||||||
secrets: secrets?.sort((a, b) =>
|
|
||||||
sortDir === SortDir.ASC
|
|
||||||
? a.key.toLowerCase().localeCompare(b.key.toLowerCase())
|
|
||||||
: b.key.toLowerCase().localeCompare(a.key.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filterSecrets = (secrets: SecretV3RawSanitized[], filter: Filter) =>
|
export const filterSecrets = (secrets: SecretV3RawSanitized[], filter: Filter) =>
|
||||||
secrets.filter(({ key, value, tags }) => {
|
secrets.filter(({ key, value, tags }) => {
|
||||||
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
|
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
|
||||||
@@ -88,8 +45,6 @@ export const SecretListView = ({
|
|||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretPath = "/",
|
secretPath = "/",
|
||||||
filter,
|
|
||||||
sortDir = SortDir.ASC,
|
|
||||||
tags: wsTags = [],
|
tags: wsTags = [],
|
||||||
isVisible,
|
isVisible,
|
||||||
isProtectedBranch = false
|
isProtectedBranch = false
|
||||||
@@ -331,52 +286,30 @@ export const SecretListView = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{reorderSecret(secrets, sortDir, filter.groupBy).map(
|
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
|
||||||
({ namespace, secrets: groupedSecrets }) => {
|
<FontAwesomeIcon icon={icon} symbol={symbol} key={`font-awesome-svg-spritie-${symbol}`} />
|
||||||
const filteredSecrets = filterSecrets(groupedSecrets, filter);
|
))}
|
||||||
return (
|
{secrets.map((secret) => (
|
||||||
<div className="flex flex-col" key={`${namespace}-${groupedSecrets.length}`}>
|
<SecretItem
|
||||||
<div
|
environment={environment}
|
||||||
className={twMerge(
|
secretPath={secretPath}
|
||||||
"text-md h-0 bg-bunker-600 capitalize transition-all",
|
tags={wsTags}
|
||||||
Boolean(namespace) && Boolean(filteredSecrets.length) && "h-11 py-3 pl-4 "
|
isSelected={selectedSecrets?.[secret.id]}
|
||||||
)}
|
onToggleSecretSelect={toggleSelectedSecret}
|
||||||
key={namespace}
|
isVisible={isVisible}
|
||||||
>
|
secret={secret}
|
||||||
{namespace}
|
key={secret.id}
|
||||||
</div>
|
onSaveSecret={handleSaveSecret}
|
||||||
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
|
onDeleteSecret={onDeleteSecret}
|
||||||
<FontAwesomeIcon
|
onDetailViewSecret={onDetailViewSecret}
|
||||||
icon={icon}
|
onCreateTag={onCreateTag}
|
||||||
symbol={symbol}
|
handleSecretShare={() =>
|
||||||
key={`font-awesome-svg-spritie-${symbol}`}
|
handlePopUpOpen("createSharedSecret", {
|
||||||
/>
|
value: secret.valueOverride ?? secret.value
|
||||||
))}
|
})
|
||||||
{filteredSecrets.map((secret) => (
|
}
|
||||||
<SecretItem
|
/>
|
||||||
environment={environment}
|
))}
|
||||||
secretPath={secretPath}
|
|
||||||
tags={wsTags}
|
|
||||||
isSelected={selectedSecrets?.[secret.id]}
|
|
||||||
onToggleSecretSelect={toggleSelectedSecret}
|
|
||||||
isVisible={isVisible}
|
|
||||||
secret={secret}
|
|
||||||
key={secret.id}
|
|
||||||
onSaveSecret={handleSaveSecret}
|
|
||||||
onDeleteSecret={onDeleteSecret}
|
|
||||||
onDetailViewSecret={onDetailViewSecret}
|
|
||||||
onCreateTag={onCreateTag}
|
|
||||||
handleSecretShare={() =>
|
|
||||||
handlePopUpOpen("createSharedSecret", {
|
|
||||||
value: secret.valueOverride ?? secret.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteSecret.isOpen}
|
isOpen={popUp.deleteSecret.isOpen}
|
||||||
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
|
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, 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";
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -49,7 +50,7 @@ import {
|
|||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { useDebounce, usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
useCreateSecretV3,
|
useCreateSecretV3,
|
||||||
@@ -78,6 +79,14 @@ export enum EntryType {
|
|||||||
SECRET = "secret"
|
SECRET = "secret"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RowType {
|
||||||
|
Folder = "folder",
|
||||||
|
DynamicSecret = "dynamic",
|
||||||
|
Secret = "Secret"
|
||||||
|
}
|
||||||
|
|
||||||
|
const INIT_PER_PAGE = 10;
|
||||||
|
|
||||||
export const SecretOverviewPage = () => {
|
export const SecretOverviewPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -101,6 +110,7 @@ export const SecretOverviewPage = () => {
|
|||||||
const workspaceId = currentWorkspace?.id as string;
|
const workspaceId = currentWorkspace?.id as string;
|
||||||
const projectSlug = currentWorkspace?.slug as string;
|
const projectSlug = currentWorkspace?.slug as string;
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
|
const debouncedSearchFilter = useDebounce(searchFilter);
|
||||||
const secretPath = (router.query?.secretPath as string) || "/";
|
const secretPath = (router.query?.secretPath as string) || "/";
|
||||||
|
|
||||||
const [selectedEntries, setSelectedEntries] = useState<{
|
const [selectedEntries, setSelectedEntries] = useState<{
|
||||||
@@ -111,6 +121,9 @@ export const SecretOverviewPage = () => {
|
|||||||
[EntryType.SECRET]: {}
|
[EntryType.SECRET]: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||||
|
|
||||||
const toggleSelectedEntry = useCallback(
|
const toggleSelectedEntry = useCallback(
|
||||||
(type: EntryType, key: string) => {
|
(type: EntryType, key: string) => {
|
||||||
const isChecked = Boolean(selectedEntries[type]?.[key]);
|
const isChecked = Boolean(selectedEntries[type]?.[key]);
|
||||||
@@ -439,7 +452,40 @@ export const SecretOverviewPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isWorkspaceLoading) {
|
const rows = useMemo(() => {
|
||||||
|
const filteredSecretNames =
|
||||||
|
secKeys
|
||||||
|
?.filter((name) => name.toUpperCase().includes(debouncedSearchFilter.toUpperCase()))
|
||||||
|
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a))) ?? [];
|
||||||
|
const filteredFolderNames =
|
||||||
|
folderNames
|
||||||
|
?.filter((name) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||||
|
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a))) ?? [];
|
||||||
|
const filteredDynamicSecrets =
|
||||||
|
dynamicSecretNames
|
||||||
|
?.filter((name) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||||
|
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a))) ?? [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...filteredFolderNames.map((name) => ({ name, type: RowType.Folder })),
|
||||||
|
...filteredDynamicSecrets.map((name) => ({ name, type: RowType.DynamicSecret })),
|
||||||
|
...filteredSecretNames.map((name) => ({ name, type: RowType.Secret }))
|
||||||
|
];
|
||||||
|
}, [sortDir, debouncedSearchFilter, secKeys, folderNames, dynamicSecretNames]);
|
||||||
|
|
||||||
|
const paginationOffset = (page - 1) * perPage;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// reset page if no longer valid
|
||||||
|
if (rows.length < paginationOffset) setPage(1);
|
||||||
|
}, [rows.length]);
|
||||||
|
|
||||||
|
const isTableLoading =
|
||||||
|
folders?.some(({ isLoading }) => isLoading) ||
|
||||||
|
secrets?.some(({ isLoading }) => isLoading) ||
|
||||||
|
dynamicSecrets?.some(({ isLoading }) => isLoading);
|
||||||
|
|
||||||
|
if (isWorkspaceLoading || isTableLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<img
|
<img
|
||||||
@@ -454,32 +500,16 @@ export const SecretOverviewPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTableLoading = !(
|
|
||||||
folders?.some(({ isLoading }) => !isLoading) && secrets?.some(({ isLoading }) => !isLoading)
|
|
||||||
);
|
|
||||||
|
|
||||||
const canViewOverviewPage = Boolean(userAvailableEnvs.length);
|
const canViewOverviewPage = Boolean(userAvailableEnvs.length);
|
||||||
// This is needed to also show imports from other paths – right now those are missing.
|
// This is needed to also show imports from other paths – right now those are missing.
|
||||||
// const combinedKeys = [...secKeys, ...secretImports.map((impSecrets) => impSecrets?.data?.map((impSec) => impSec.secrets?.map((impSecKey) => impSecKey.key))).flat().flat()];
|
// const combinedKeys = [...secKeys, ...secretImports.map((impSecrets) => impSecrets?.data?.map((impSec) => impSec.secrets?.map((impSecKey) => impSecKey.key))).flat().flat()];
|
||||||
const filteredSecretNames = secKeys
|
|
||||||
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
|
|
||||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
|
||||||
const filteredFolderNames = folderNames
|
|
||||||
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
|
|
||||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
|
||||||
const filteredDynamicSecrets = dynamicSecretNames
|
|
||||||
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
|
|
||||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
|
||||||
|
|
||||||
const isTableEmpty =
|
const isTableEmpty =
|
||||||
!(
|
!(
|
||||||
folders?.every(({ isLoading }) => isLoading) &&
|
folders?.every(({ isLoading }) => isLoading) &&
|
||||||
secrets?.every(({ isLoading }) => isLoading) &&
|
secrets?.every(({ isLoading }) => isLoading) &&
|
||||||
dynamicSecrets?.every(({ isLoading }) => isLoading)
|
dynamicSecrets?.every(({ isLoading }) => isLoading)
|
||||||
) &&
|
) && rows.length === 0;
|
||||||
filteredSecretNames?.length === 0 &&
|
|
||||||
filteredFolderNames?.length === 0 &&
|
|
||||||
filteredDynamicSecrets?.length === 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -656,7 +686,7 @@ export const SecretOverviewPage = () => {
|
|||||||
resetSelectedEntries={resetSelectedEntries}
|
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>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr className="sticky top-0 z-20 border-0">
|
<Tr className="sticky top-0 z-20 border-0">
|
||||||
@@ -753,7 +783,7 @@ export const SecretOverviewPage = () => {
|
|||||||
<Td colSpan={visibleEnvs.length + 1}>
|
<Td colSpan={visibleEnvs.length + 1}>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title={
|
title={
|
||||||
searchFilter
|
debouncedSearchFilter
|
||||||
? "No secret found for your search, add one now"
|
? "No secret found for your search, add one now"
|
||||||
: "Let's add some secrets"
|
: "Let's add some secrets"
|
||||||
}
|
}
|
||||||
@@ -774,48 +804,59 @@ export const SecretOverviewPage = () => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
{!isTableLoading &&
|
{!isTableLoading &&
|
||||||
filteredFolderNames.map((folderName, index) => (
|
rows.slice(paginationOffset, paginationOffset + perPage).map((row, index) => {
|
||||||
<SecretOverviewFolderRow
|
switch (row.type) {
|
||||||
folderName={folderName}
|
case RowType.Secret:
|
||||||
isFolderPresentInEnv={isFolderPresentInEnv}
|
if (visibleEnvs?.length === 0) return null;
|
||||||
isSelected={selectedEntries.folder[folderName]}
|
return (
|
||||||
onToggleFolderSelect={() => toggleSelectedEntry(EntryType.FOLDER, folderName)}
|
<SecretOverviewTableRow
|
||||||
environments={visibleEnvs}
|
isSelected={selectedEntries.secret[row.name]}
|
||||||
key={`overview-${folderName}-${index + 1}`}
|
onToggleSecretSelect={() =>
|
||||||
onClick={handleFolderClick}
|
toggleSelectedEntry(EntryType.SECRET, row.name)
|
||||||
onToggleFolderEdit={(name: string) =>
|
}
|
||||||
handlePopUpOpen("updateFolder", { name })
|
secretPath={secretPath}
|
||||||
}
|
getImportedSecretByKey={getImportedSecretByKey}
|
||||||
/>
|
isImportedSecretPresentInEnv={isImportedSecretPresentInEnv}
|
||||||
))}
|
onSecretCreate={handleSecretCreate}
|
||||||
{!isTableLoading &&
|
onSecretDelete={handleSecretDelete}
|
||||||
filteredDynamicSecrets.map((dynamicSecretName, index) => (
|
onSecretUpdate={handleSecretUpdate}
|
||||||
<SecretOverviewDynamicSecretRow
|
key={`overview-${row.name}-${index + 1}`}
|
||||||
dynamicSecretName={dynamicSecretName}
|
environments={visibleEnvs}
|
||||||
isDynamicSecretInEnv={isDynamicSecretPresentInEnv}
|
secretKey={row.name}
|
||||||
environments={visibleEnvs}
|
getSecretByKey={getSecretByKey}
|
||||||
key={`overview-${dynamicSecretName}-${index + 1}`}
|
expandableColWidth={expandableTableWidth}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
{!isTableLoading &&
|
case RowType.DynamicSecret:
|
||||||
visibleEnvs?.length > 0 &&
|
return (
|
||||||
filteredSecretNames.map((key, index) => (
|
<SecretOverviewDynamicSecretRow
|
||||||
<SecretOverviewTableRow
|
dynamicSecretName={row.name}
|
||||||
isSelected={selectedEntries.secret[key]}
|
isDynamicSecretInEnv={isDynamicSecretPresentInEnv}
|
||||||
onToggleSecretSelect={() => toggleSelectedEntry(EntryType.SECRET, key)}
|
environments={visibleEnvs}
|
||||||
secretPath={secretPath}
|
key={`overview-${row.name}-${index + 1}`}
|
||||||
getImportedSecretByKey={getImportedSecretByKey}
|
/>
|
||||||
isImportedSecretPresentInEnv={isImportedSecretPresentInEnv}
|
);
|
||||||
onSecretCreate={handleSecretCreate}
|
case RowType.Folder:
|
||||||
onSecretDelete={handleSecretDelete}
|
return (
|
||||||
onSecretUpdate={handleSecretUpdate}
|
<SecretOverviewFolderRow
|
||||||
key={`overview-${key}-${index + 1}`}
|
folderName={row.name}
|
||||||
environments={visibleEnvs}
|
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||||
secretKey={key}
|
isSelected={selectedEntries.folder[row.name]}
|
||||||
getSecretByKey={getSecretByKey}
|
onToggleFolderSelect={() =>
|
||||||
expandableColWidth={expandableTableWidth}
|
toggleSelectedEntry(EntryType.FOLDER, row.name)
|
||||||
/>
|
}
|
||||||
))}
|
environments={visibleEnvs}
|
||||||
|
key={`overview-${row.name}-${index + 1}`}
|
||||||
|
onClick={handleFolderClick}
|
||||||
|
onToggleFolderEdit={(name: string) =>
|
||||||
|
handlePopUpOpen("updateFolder", { name })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
</TBody>
|
</TBody>
|
||||||
<TFoot>
|
<TFoot>
|
||||||
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
|
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
|
||||||
@@ -842,6 +883,16 @@ export const SecretOverviewPage = () => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</TFoot>
|
</TFoot>
|
||||||
</Table>
|
</Table>
|
||||||
|
{!isTableLoading && rows.length > INIT_PER_PAGE && (
|
||||||
|
<Pagination
|
||||||
|
className="border-t border-solid border-t-mineshaft-600"
|
||||||
|
count={rows.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user