mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Rework of secret imports on overview page
This commit is contained in:
@@ -813,7 +813,8 @@ export const DASHBOARD = {
|
||||
search: "The text string to filter secret keys and folder names by.",
|
||||
includeSecrets: "Whether to include project secrets in the response.",
|
||||
includeFolders: "Whether to include project folders in the response.",
|
||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
|
||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response.",
|
||||
includeImports: "Whether to include project secret imports in the response."
|
||||
},
|
||||
SECRET_DETAILS_LIST: {
|
||||
projectId: "The ID of the project to list secrets/folders from.",
|
||||
|
||||
@@ -109,6 +109,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(),
|
||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
|
||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
|
||||
includeImports: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeImports),
|
||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
|
||||
}),
|
||||
response: {
|
||||
@@ -124,9 +125,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
imports: SecretImportsSchema.omit({ importEnv: true })
|
||||
.extend({
|
||||
importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }),
|
||||
environment: z.string()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
totalFolderCount: z.number().optional(),
|
||||
totalDynamicSecretCount: z.number().optional(),
|
||||
totalSecretCount: z.number().optional(),
|
||||
totalImportCount: z.number().optional(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
@@ -143,6 +152,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
orderDirection,
|
||||
includeFolders,
|
||||
includeSecrets,
|
||||
includeImports,
|
||||
includeDynamicSecrets
|
||||
} = req.query;
|
||||
|
||||
@@ -159,6 +169,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
let remainingLimit = limit;
|
||||
let adjustedOffset = offset;
|
||||
|
||||
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined;
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
|
||||
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
|
||||
let dynamicSecrets:
|
||||
@@ -168,6 +179,53 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
let totalFolderCount: number | undefined;
|
||||
let totalDynamicSecretCount: number | undefined;
|
||||
let totalSecretCount: number | undefined;
|
||||
let totalImportCount: number | undefined;
|
||||
|
||||
if (includeImports) {
|
||||
totalImportCount = await server.services.secretImport.getProjectImportMultiEnvCount({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
environments,
|
||||
path: secretPath,
|
||||
search
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalImportCount > adjustedOffset) {
|
||||
imports = await server.services.secretImport.getImportsMultiEnv({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
environments,
|
||||
path: secretPath,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_IMPORTS,
|
||||
metadata: {
|
||||
environment: environments.join(","),
|
||||
folderId: imports?.[0]?.folderId,
|
||||
numberOfImports: imports.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
remainingLimit -= imports.length;
|
||||
adjustedOffset = 0;
|
||||
} else {
|
||||
adjustedOffset = Math.max(0, adjustedOffset - totalImportCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeFolders) {
|
||||
// this is the unique count, ie duplicate folders across envs only count as 1
|
||||
@@ -345,8 +403,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
secrets,
|
||||
imports,
|
||||
totalFolderCount,
|
||||
totalDynamicSecretCount,
|
||||
totalImportCount,
|
||||
totalSecretCount,
|
||||
totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
|
||||
};
|
||||
|
||||
@@ -469,6 +469,43 @@ export const secretImportServiceFactory = ({
|
||||
return count;
|
||||
};
|
||||
|
||||
const getProjectImportMultiEnvCount = async ({
|
||||
path: secretPath,
|
||||
environments,
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
search
|
||||
}: Omit<TGetSecretImportsDTO, "environment"> & { environments: string[] }) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
for (const environment of environments) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
|
||||
if (!folders?.length)
|
||||
throw new NotFoundError({
|
||||
message: `Folder with path '${secretPath}' not found on environments with slugs '${environments.join(", ")}'`
|
||||
});
|
||||
const counts = await Promise.all(
|
||||
folders.map((folder) => secretImportDAL.getProjectImportCount({ folderId: folder.id, search }))
|
||||
);
|
||||
|
||||
return counts.reduce((sum, count) => sum + count, 0);
|
||||
};
|
||||
|
||||
const getImports = async ({
|
||||
path: secretPath,
|
||||
environment,
|
||||
@@ -688,6 +725,51 @@ export const secretImportServiceFactory = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const getImportsMultiEnv = async ({
|
||||
path: secretPath,
|
||||
environments,
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
search,
|
||||
limit,
|
||||
offset
|
||||
}: Omit<TGetSecretImportsDTO, "environment"> & { environments: string[] }) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
for (const environment of environments) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
|
||||
if (!folders?.length)
|
||||
throw new NotFoundError({
|
||||
message: `Folder with path '${secretPath}' not found on environments with slugs '${environments.join(", ")}'`
|
||||
});
|
||||
|
||||
const secImportsArrays = await Promise.all(
|
||||
folders.map(async (folder) => {
|
||||
const imports = await secretImportDAL.find({ folderId: folder.id, search, limit, offset });
|
||||
return imports.map((importItem) => ({
|
||||
...importItem,
|
||||
environment: folder.environment.slug
|
||||
}));
|
||||
})
|
||||
);
|
||||
return secImportsArrays.flat();
|
||||
};
|
||||
|
||||
return {
|
||||
createImport,
|
||||
updateImport,
|
||||
@@ -698,6 +780,8 @@ export const secretImportServiceFactory = ({
|
||||
getRawSecretsFromImports,
|
||||
resyncSecretImportReplication,
|
||||
getProjectImportCount,
|
||||
fnSecretsFromImports
|
||||
fnSecretsFromImports,
|
||||
getProjectImportMultiEnvCount,
|
||||
getImportsMultiEnv
|
||||
};
|
||||
};
|
||||
|
||||
@@ -143,6 +143,7 @@ export const useGetProjectSecretsOverview = (
|
||||
search = "",
|
||||
includeSecrets,
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
environments
|
||||
}: TGetDashboardProjectSecretsOverviewDTO,
|
||||
@@ -170,6 +171,7 @@ export const useGetProjectSecretsOverview = (
|
||||
projectId,
|
||||
includeSecrets,
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
environments
|
||||
}),
|
||||
@@ -184,6 +186,7 @@ export const useGetProjectSecretsOverview = (
|
||||
projectId,
|
||||
includeSecrets,
|
||||
includeFolders,
|
||||
includeImports,
|
||||
includeDynamicSecrets,
|
||||
environments
|
||||
}),
|
||||
@@ -197,12 +200,15 @@ export const useGetProjectSecretsOverview = (
|
||||
? unique(select.dynamicSecrets, (i) => i.name)
|
||||
: [];
|
||||
|
||||
const uniqueSecretImports = select.imports ? unique(select.imports, (i) => i.id) : [];
|
||||
|
||||
return {
|
||||
...select,
|
||||
secrets: secrets ? mergePersonalSecrets(secrets) : undefined,
|
||||
totalUniqueSecretsInPage: uniqueSecrets.length,
|
||||
totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length,
|
||||
totalUniqueFoldersInPage: uniqueFolders.length
|
||||
totalUniqueFoldersInPage: uniqueFolders.length,
|
||||
totalUniqueSecretImportsInPage: uniqueSecretImports.length
|
||||
};
|
||||
}, []),
|
||||
placeholderData: (previousData) => previousData
|
||||
|
||||
@@ -9,13 +9,16 @@ export type DashboardProjectSecretsOverviewResponse = {
|
||||
folders?: (TSecretFolder & { environment: string })[];
|
||||
dynamicSecrets?: (TDynamicSecret & { environment: string })[];
|
||||
secrets?: SecretV3Raw[];
|
||||
imports?: TSecretImport[];
|
||||
totalSecretCount?: number;
|
||||
totalFolderCount?: number;
|
||||
totalDynamicSecretCount?: number;
|
||||
totalImportCount?: number;
|
||||
totalCount: number;
|
||||
totalUniqueSecretsInPage: number;
|
||||
totalUniqueDynamicSecretsInPage: number;
|
||||
totalUniqueFoldersInPage: number;
|
||||
totalUniqueSecretImportsInPage: number;
|
||||
};
|
||||
|
||||
export type DashboardProjectSecretsDetailsResponse = {
|
||||
@@ -63,6 +66,7 @@ export type TGetDashboardProjectSecretsOverviewDTO = {
|
||||
includeSecrets?: boolean;
|
||||
includeFolders?: boolean;
|
||||
includeDynamicSecrets?: boolean;
|
||||
includeImports?: boolean;
|
||||
environments: string[];
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export type TSecretImport = {
|
||||
isReplicationSuccess?: boolean;
|
||||
replicationStatus?: string;
|
||||
lastReplicated?: string;
|
||||
environment?: string;
|
||||
};
|
||||
|
||||
export type TGetImportedFoldersByEnvDTO = {
|
||||
@@ -90,31 +91,3 @@ export type TDeleteSecretImportDTO = {
|
||||
environment: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export type TSecretImportMultiEnvData = {
|
||||
currentEnv: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
environmentInfo: WorkspaceEnv;
|
||||
folderId: string;
|
||||
secrets: {
|
||||
id: string;
|
||||
env: string;
|
||||
key: string;
|
||||
value: string;
|
||||
secretValueHidden: boolean;
|
||||
tags?: {
|
||||
id: string;
|
||||
slug: string;
|
||||
color?: string;
|
||||
projectId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
__v: number;
|
||||
}[];
|
||||
comment?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
version: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types";
|
||||
import { TSecretImportMultiEnvData } from "@app/hooks/api/secretImports/types";
|
||||
|
||||
type FolderNameAndDescription = {
|
||||
name: string;
|
||||
@@ -97,37 +96,3 @@ export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secr
|
||||
|
||||
return { secKeys, getSecretByKey, getEnvSecretKeyCount };
|
||||
};
|
||||
|
||||
export const useSecretImportOverview = (secretImports: TSecretImportMultiEnvData[] | undefined) => {
|
||||
const uniqueEnvSecretPaths = useMemo(() => {
|
||||
const uniqueMap = new Map<string, TSecretImportMultiEnvData>();
|
||||
secretImports?.forEach((importData) => {
|
||||
if (importData) {
|
||||
const key = `${importData.environment}-${importData.secretPath}`;
|
||||
if (!uniqueMap.has(key)) {
|
||||
uniqueMap.set(key, importData);
|
||||
}
|
||||
}
|
||||
});
|
||||
return Array.from(uniqueMap.values());
|
||||
}, [secretImports]);
|
||||
|
||||
const isSecretImportPresent = useCallback(
|
||||
(sourceEnv: string, targetEnv: string, secretPath: string) => {
|
||||
return (
|
||||
secretImports?.some(
|
||||
(importData) =>
|
||||
importData?.currentEnv === sourceEnv &&
|
||||
importData?.environment === targetEnv &&
|
||||
importData?.secretPath === secretPath
|
||||
) ?? false
|
||||
);
|
||||
},
|
||||
[secretImports]
|
||||
);
|
||||
|
||||
return {
|
||||
uniqueEnvSecretPaths,
|
||||
isSecretImportPresent
|
||||
};
|
||||
};
|
||||
|
||||
@@ -68,15 +68,9 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
||||
import { TSecretImportMultiEnvData } from "@app/hooks/api/secretImports/types";
|
||||
import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import {
|
||||
useDynamicSecretOverview,
|
||||
useFolderOverview,
|
||||
useSecretImportOverview,
|
||||
useSecretOverview
|
||||
} from "@app/hooks/utils";
|
||||
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
|
||||
|
||||
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
|
||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||
@@ -195,33 +189,12 @@ export const OverviewPage = () => {
|
||||
setVisibleEnvs(userAvailableEnvs);
|
||||
}, [userAvailableEnvs]);
|
||||
|
||||
const {
|
||||
secretImports,
|
||||
isImportedSecretPresentInEnv,
|
||||
getImportedSecretByKey,
|
||||
getEnvImportedSecretKeyCount
|
||||
} = useGetImportedSecretsAllEnvs({
|
||||
projectId: workspaceId,
|
||||
path: secretPath,
|
||||
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
|
||||
});
|
||||
const secretImportsData = useMemo(
|
||||
() =>
|
||||
(secretImports?.map((s) => s.data as TSecretImportMultiEnvData[]) ?? [])
|
||||
?.flatMap((s) => s ?? [])
|
||||
?.filter((secretImport) =>
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, {
|
||||
environment: secretImport.currentEnv,
|
||||
secretPath
|
||||
})
|
||||
)
|
||||
),
|
||||
[secretImports]
|
||||
);
|
||||
const { uniqueEnvSecretPaths, isSecretImportPresent } =
|
||||
useSecretImportOverview(secretImportsData);
|
||||
const { isImportedSecretPresentInEnv, getImportedSecretByKey, getEnvImportedSecretKeyCount } =
|
||||
useGetImportedSecretsAllEnvs({
|
||||
projectId: workspaceId,
|
||||
path: secretPath,
|
||||
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
|
||||
});
|
||||
|
||||
const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
|
||||
{
|
||||
@@ -233,6 +206,7 @@ export const OverviewPage = () => {
|
||||
includeFolders: filter.folder,
|
||||
includeDynamicSecrets: filter.dynamic,
|
||||
includeSecrets: filter.secret,
|
||||
includeImports: filter.import,
|
||||
search: debouncedSearchFilter,
|
||||
limit,
|
||||
offset
|
||||
@@ -244,15 +218,29 @@ export const OverviewPage = () => {
|
||||
secrets,
|
||||
folders,
|
||||
dynamicSecrets,
|
||||
imports,
|
||||
totalFolderCount,
|
||||
totalSecretCount,
|
||||
totalDynamicSecretCount,
|
||||
totalImportCount,
|
||||
totalCount = 0,
|
||||
totalUniqueFoldersInPage,
|
||||
totalUniqueSecretsInPage,
|
||||
totalUniqueSecretImportsInPage,
|
||||
totalUniqueDynamicSecretsInPage
|
||||
} = overview ?? {};
|
||||
|
||||
const importsShaped = imports
|
||||
?.filter((el) => !el.isReserved)
|
||||
?.map(({ importPath, importEnv }) => ({ importPath, importEnv }))
|
||||
.filter(
|
||||
(el, index, self) =>
|
||||
index ===
|
||||
self.findIndex(
|
||||
(item) => item.importPath === el.importPath && item.importEnv.slug === el.importEnv.slug
|
||||
)
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount,
|
||||
offset,
|
||||
@@ -693,7 +681,6 @@ export const OverviewPage = () => {
|
||||
<SecretV2MigrationSection />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<Helmet>
|
||||
@@ -1099,16 +1086,14 @@ export const OverviewPage = () => {
|
||||
/>
|
||||
))}
|
||||
{filter.import &&
|
||||
Object.entries(uniqueEnvSecretPaths).map(([key, secretImportsAllEnvs]) => (
|
||||
importsShaped &&
|
||||
importsShaped?.length > 0 &&
|
||||
importsShaped?.map((item, index) => (
|
||||
<SecretOverviewImportListView
|
||||
secretImport={secretImportsAllEnvs}
|
||||
isImportedSecretPresentInEnv={isSecretImportPresent}
|
||||
secretImport={item}
|
||||
environments={visibleEnvs}
|
||||
key={`overview-${key}`}
|
||||
scrollOffset={debouncedScrollOffset}
|
||||
allSecretImports={(
|
||||
secretImportsData?.flatMap((s) => s ?? []) ?? []
|
||||
).filter(Boolean)}
|
||||
key={`overview-secret-input-${index + 1}`}
|
||||
allSecretImports={imports}
|
||||
/>
|
||||
))}
|
||||
{secKeys.map((key, index) => (
|
||||
@@ -1134,7 +1119,8 @@ export const OverviewPage = () => {
|
||||
(page * perPage > totalCount ? totalCount % perPage : perPage) -
|
||||
(totalUniqueFoldersInPage || 0) -
|
||||
(totalUniqueDynamicSecretsInPage || 0) -
|
||||
(totalUniqueSecretsInPage || 0),
|
||||
(totalUniqueSecretsInPage || 0) -
|
||||
(totalUniqueSecretImportsInPage || 0),
|
||||
0
|
||||
)}
|
||||
/>
|
||||
@@ -1174,6 +1160,7 @@ export const OverviewPage = () => {
|
||||
dynamicSecretCount={totalDynamicSecretCount}
|
||||
secretCount={totalSecretCount}
|
||||
folderCount={totalFolderCount}
|
||||
importCount={totalImportCount}
|
||||
/>
|
||||
}
|
||||
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
|
||||
|
||||
@@ -1,149 +1,76 @@
|
||||
import { faCheck, faFileImport, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faFileImport, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { EmptyState, SecretInput, TableContainer, Td, Tr } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { TSecretImportMultiEnvData } from "@app/hooks/api/secretImports/types";
|
||||
import { Td, Tr } from "@app/components/v2";
|
||||
import { TSecretImport, WorkspaceEnv } from "@app/hooks/api/types";
|
||||
import { EnvFolderIcon } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem";
|
||||
import { computeImportedSecretRows } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView";
|
||||
|
||||
type Props = {
|
||||
secretImport: TSecretImportMultiEnvData;
|
||||
secretImport: { importPath: string; importEnv: WorkspaceEnv };
|
||||
environments: { name: string; slug: string }[];
|
||||
isImportedSecretPresentInEnv: (
|
||||
sourceEnv: string,
|
||||
targetEnv: string,
|
||||
secretPath: string
|
||||
) => boolean;
|
||||
scrollOffset: number;
|
||||
allSecretImports: TSecretImportMultiEnvData[];
|
||||
allSecretImports?: TSecretImport[];
|
||||
};
|
||||
|
||||
export const SecretOverviewImportListView = ({
|
||||
secretImport,
|
||||
environments = [],
|
||||
isImportedSecretPresentInEnv,
|
||||
scrollOffset,
|
||||
allSecretImports = []
|
||||
}: Props) => {
|
||||
const [isFormExpanded, setIsFormExpanded] = useToggle();
|
||||
const environmentImportDetails = secretImport.environmentInfo;
|
||||
const totalCols = environments.length + 1;
|
||||
const isSecretPresentInEnv = (envSlug: string) => {
|
||||
return allSecretImports.some((item) => {
|
||||
if (item.isReplication) {
|
||||
const reservedItem = allSecretImports.find((element) =>
|
||||
element.importPath.includes(`__reserve_replication_${item.id}`)
|
||||
);
|
||||
// If the reserved item exists, check if the envSlug matches
|
||||
if (reservedItem) {
|
||||
return reservedItem.environment === envSlug;
|
||||
}
|
||||
} else {
|
||||
// If the item is not replication, check if the envSlug matches directly
|
||||
return item.environment === envSlug;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const computeImportedSecrets =
|
||||
allSecretImports.length > 0
|
||||
? computeImportedSecretRows(
|
||||
environmentImportDetails.slug,
|
||||
secretImport.secretPath,
|
||||
allSecretImports
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
isHoverable
|
||||
isSelectable
|
||||
onClick={() => setIsFormExpanded.toggle()}
|
||||
className={`group ${isFormExpanded ? "border-t-2 border-mineshaft-500" : ""}`}
|
||||
>
|
||||
<Td className="sticky left-0 z-10 bg-mineshaft-800 bg-clip-padding px-0 py-0 group-hover:bg-mineshaft-700">
|
||||
<div className="group flex cursor-pointer">
|
||||
<div className="flex w-11 items-center py-2 pl-5 text-green-700">
|
||||
<FontAwesomeIcon icon={faFileImport} />
|
||||
</div>
|
||||
<div className="flex flex-grow items-center py-2 pl-4 pr-2">
|
||||
<EnvFolderIcon
|
||||
env={environmentImportDetails.slug || ""}
|
||||
secretPath={secretImport.secretPath || ""}
|
||||
/>
|
||||
</div>
|
||||
<Tr className="group">
|
||||
<Td className="sticky left-0 z-10 bg-mineshaft-800 bg-clip-padding px-0 py-0 group-hover:bg-mineshaft-700">
|
||||
<div className="group flex cursor-pointer">
|
||||
<div className="flex w-11 items-center py-2 pl-5 text-green-700">
|
||||
<FontAwesomeIcon icon={faFileImport} />
|
||||
</div>
|
||||
</Td>
|
||||
{environments.map(({ slug }, i) => {
|
||||
const isPresent = isImportedSecretPresentInEnv(
|
||||
slug,
|
||||
secretImport.environment,
|
||||
secretImport.secretPath
|
||||
);
|
||||
|
||||
return (
|
||||
<Td
|
||||
key={`sec-overview-${slug}-${i + 1}-value`}
|
||||
className={twMerge(
|
||||
"px-0 py-0 group-hover:bg-mineshaft-700",
|
||||
isPresent ? "text-green-600" : "text-red-600"
|
||||
)}
|
||||
>
|
||||
<div className="h-full w-full border-r border-mineshaft-600 px-5 py-[0.85rem]">
|
||||
<div className="flex justify-center">
|
||||
<FontAwesomeIcon
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
icon={
|
||||
isImportedSecretPresentInEnv(
|
||||
slug,
|
||||
secretImport.environment,
|
||||
secretImport.secretPath
|
||||
)
|
||||
? faCheck
|
||||
: faXmark
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
{isFormExpanded && (
|
||||
<Tr>
|
||||
<div className="flex flex-grow items-center py-2 pl-4 pr-2">
|
||||
<EnvFolderIcon
|
||||
env={secretImport.importEnv.slug || ""}
|
||||
secretPath={secretImport.importPath || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
{environments.map(({ slug }, i) => {
|
||||
const isPresent = isSecretPresentInEnv(slug);
|
||||
return (
|
||||
<Td
|
||||
colSpan={totalCols}
|
||||
className={`bg-bunker-600 px-0 py-0 ${
|
||||
isFormExpanded && "border-b-2 border-mineshaft-500"
|
||||
}`}
|
||||
key={`sec-overview-${slug}-${i + 1}-value`}
|
||||
className={twMerge(
|
||||
"px-0 py-0 group-hover:bg-mineshaft-700",
|
||||
isPresent ? "text-green-600" : "text-red-600"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="ml-2 p-2"
|
||||
style={{
|
||||
marginLeft: scrollOffset,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<TableContainer>
|
||||
<table className="secret-table">
|
||||
<thead>
|
||||
<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> */}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{computeImportedSecrets?.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<EmptyState title="No secrets found" icon={faKey} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{computeImportedSecrets.map(({ key, value }, index) => (
|
||||
<tr key={`${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<SecretInput value={value} isReadOnly />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
<div className="h-full w-full border-r border-mineshaft-600 px-5 py-[0.85rem]">
|
||||
<div className="flex justify-center">
|
||||
<FontAwesomeIcon
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
icon={isSecretPresentInEnv(slug) ? faCheck : faXmark}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user