feat: added user status signs for replication failure etc

This commit is contained in:
=
2024-05-31 23:10:02 +05:30
parent 25a615cbb3
commit b0c472b5e1
6 changed files with 292 additions and 194 deletions
@@ -6,6 +6,9 @@ export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretImport)) { if (await knex.schema.hasTable(TableName.SecretImport)) {
await knex.schema.alterTable(TableName.SecretImport, (t) => { await knex.schema.alterTable(TableName.SecretImport, (t) => {
t.boolean("isReplication").defaultTo(false); t.boolean("isReplication").defaultTo(false);
t.boolean("isReplicationSuccess").nullable();
t.text("replicationStatus").nullable();
t.datetime("lastReplicated").nullable();
}); });
} }
@@ -38,6 +41,9 @@ export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretImport)) { if (await knex.schema.hasTable(TableName.SecretImport)) {
await knex.schema.alterTable(TableName.SecretImport, (t) => { await knex.schema.alterTable(TableName.SecretImport, (t) => {
t.dropColumns("isReplication"); t.dropColumns("isReplication");
t.dropColumns("isReplicationSuccess");
t.dropColumns("replicationStatus");
t.dropColumns("lastReplicated");
}); });
} }
+4 -1
View File
@@ -16,7 +16,10 @@ export const SecretImportsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
folderId: z.string().uuid(), folderId: z.string().uuid(),
isReplication: z.boolean().default(false).nullable().optional() isReplication: z.boolean().default(false).nullable().optional(),
isReplicationSuccess: z.boolean().nullable().optional(),
replicationStatus: z.string().nullable().optional(),
lastReplicated: z.date().nullable().optional()
}); });
export type TSecretImports = z.infer<typeof SecretImportsSchema>; export type TSecretImports = z.infer<typeof SecretImportsSchema>;
@@ -30,7 +30,7 @@ type TSecretReplicationServiceFactoryDep = {
"find" | "findByBlindIndexes" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" "find" | "findByBlindIndexes" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences"
>; >;
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
secretImportDAL: Pick<TSecretImportDALFactory, "find">; secretImportDAL: Pick<TSecretImportDALFactory, "find" | "updateById">;
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds" | "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds" | "findBySecretPath">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">;
@@ -115,131 +115,92 @@ export const secretReplicationServiceFactory = ({
try { try {
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
for (const secretImport of secretImports) { for (const secretImport of secretImports) {
const hasJobCompleted = await keyStore.getItem( try {
keystoreReplicationSuccessKey(job.id as string, secretImport.id), const hasJobCompleted = await keyStore.getItem(
KeyStorePrefixes.SecretReplication keystoreReplicationSuccessKey(job.id as string, secretImport.id),
); KeyStorePrefixes.SecretReplication
if (hasJobCompleted) {
logger.info(
{ jobId: job.id, importId: secretImport.id },
"Skipping this job as this has been successfully replicated."
); );
// eslint-disable-next-line if (hasJobCompleted) {
continue; logger.info(
} { jobId: job.id, importId: secretImport.id },
"Skipping this job as this has been successfully replicated."
const [importedFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [secretImport.folderId]); );
if (!importedFolder) throw new BadRequestError({ message: "Imported folder not found" }); // eslint-disable-next-line
const importFolderId = importedFolder.id; continue;
const localSecrets = await secretDAL.find({
$in: { secretBlindIndex: replicatedSecrets.map(({ secretBlindIndex }) => secretBlindIndex) },
folderId: importFolderId
});
const localSecretsGroupedByBlindIndex = groupBy(localSecrets, (i) => i.secretBlindIndex as string);
const locallyCreatedSecrets = sanitizedSecrets.filter(({ operation, id }) => {
return (
(operation === SecretOperations.Create || operation === SecretOperations.Update) &&
!localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
);
});
const locallyUpdatedSecrets = sanitizedSecrets.filter(
({ operation, id }) =>
(operation === SecretOperations.Create || operation === SecretOperations.Update) &&
localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
);
const locallyDeletedSecrets = sanitizedSecrets.filter(
({ operation, id }) =>
operation === SecretOperations.Delete &&
Boolean(replicatedSecretsGroupBySecretId[id]?.[0]?.secretBlindIndex) &&
localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
);
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
projectId,
importedFolder.environmentSlug,
importedFolder.path
);
// this means it should be a approval request rather than direct replication
if (policy && actor === ActorType.USER) {
const membership = await projectMembershipDAL.findOne({ projectId, userId: actorId });
if (!membership) {
logger.error("Project membership not found in %s for user %s", projectId, actorId);
return;
} }
const localSecretsLatestVersions = localSecrets.map(({ id }) => id); const [importedFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [secretImport.folderId]);
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany( if (!importedFolder) throw new BadRequestError({ message: "Imported folder not found" });
importFolderId, const importFolderId = importedFolder.id;
localSecretsLatestVersions
);
await secretApprovalRequestDAL.transaction(async (tx) => {
const approvalRequestDoc = await secretApprovalRequestDAL.create(
{
folderId: importFolderId,
slug: alphaNumericNanoId(),
policyId: policy.id,
status: "open",
hasMerged: false,
committerId: membership.id,
isReplicated: true
},
tx
);
const commits = locallyCreatedSecrets
.concat(locallyUpdatedSecrets)
.concat(locallyDeletedSecrets)
.map(({ id, operation }) => {
const doc = replicatedSecretsGroupBySecretId[id][0];
const localSecret = localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string]?.[0];
return {
op: operation,
keyEncoding: doc.keyEncoding,
algorithm: doc.algorithm,
requestId: approvalRequestDoc.id,
metadata: doc.metadata,
secretKeyIV: doc.secretKeyIV,
secretKeyTag: doc.secretKeyTag,
secretKeyCiphertext: doc.secretKeyCiphertext,
secretValueIV: doc.secretValueIV,
secretValueTag: doc.secretValueTag,
secretValueCiphertext: doc.secretValueCiphertext,
secretBlindIndex: doc.secretBlindIndex,
secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
isReplicated: true,
skipMultilineEncoding: doc.skipMultilineEncoding,
// except create operation other two needs the secret id and version id
...(operation !== SecretOperations.Create
? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id }
: {})
};
});
const approvalCommits = await secretApprovalRequestSecretDAL.insertMany(commits, tx);
return { ...approvalRequestDoc, commits: approvalCommits }; const localSecrets = await secretDAL.find({
$in: { secretBlindIndex: replicatedSecrets.map(({ secretBlindIndex }) => secretBlindIndex) },
folderId: importFolderId
}); });
} else { const localSecretsGroupedByBlindIndex = groupBy(localSecrets, (i) => i.secretBlindIndex as string);
let nestedImportSecrets: TSyncSecretsDTO["secrets"] = [];
await secretReplicationDAL.transaction(async (tx) => { const locallyCreatedSecrets = sanitizedSecrets.filter(({ operation, id }) => {
if (locallyCreatedSecrets.length) { return (
const newSecrets = await fnSecretBulkInsert({ (operation === SecretOperations.Create || operation === SecretOperations.Update) &&
folderId: importFolderId, !localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
secretVersionDAL, );
secretDAL, });
tx,
secretTagDAL, const locallyUpdatedSecrets = sanitizedSecrets.filter(
secretVersionTagDAL, ({ operation, id }) =>
inputSecrets: locallyCreatedSecrets.map(({ id }) => { (operation === SecretOperations.Create || operation === SecretOperations.Update) &&
localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
);
const locallyDeletedSecrets = sanitizedSecrets.filter(
({ operation, id }) =>
operation === SecretOperations.Delete &&
Boolean(replicatedSecretsGroupBySecretId[id]?.[0]?.secretBlindIndex) &&
localSecretsGroupedByBlindIndex[replicatedSecretsGroupBySecretId[id][0].secretBlindIndex as string]?.[0]
);
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
projectId,
importedFolder.environmentSlug,
importedFolder.path
);
// this means it should be a approval request rather than direct replication
if (policy && actor === ActorType.USER) {
const membership = await projectMembershipDAL.findOne({ projectId, userId: actorId });
if (!membership) {
logger.error("Project membership not found in %s for user %s", projectId, actorId);
return;
}
const localSecretsLatestVersions = localSecrets.map(({ id }) => id);
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
importFolderId,
localSecretsLatestVersions
);
await secretApprovalRequestDAL.transaction(async (tx) => {
const approvalRequestDoc = await secretApprovalRequestDAL.create(
{
folderId: importFolderId,
slug: alphaNumericNanoId(),
policyId: policy.id,
status: "open",
hasMerged: false,
committerId: membership.id,
isReplicated: true
},
tx
);
const commits = locallyCreatedSecrets
.concat(locallyUpdatedSecrets)
.concat(locallyDeletedSecrets)
.map(({ id, operation }) => {
const doc = replicatedSecretsGroupBySecretId[id][0]; const doc = replicatedSecretsGroupBySecretId[id][0];
const localSecret = localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string]?.[0];
return { return {
op: operation,
keyEncoding: doc.keyEncoding, keyEncoding: doc.keyEncoding,
algorithm: doc.algorithm, algorithm: doc.algorithm,
type: doc.type, requestId: approvalRequestDoc.id,
metadata: doc.metadata, metadata: doc.metadata,
secretKeyIV: doc.secretKeyIV, secretKeyIV: doc.secretKeyIV,
secretKeyTag: doc.secretKeyTag, secretKeyTag: doc.secretKeyTag,
@@ -252,31 +213,31 @@ export const secretReplicationServiceFactory = ({
secretCommentTag: doc.secretCommentTag, secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext, secretCommentCiphertext: doc.secretCommentCiphertext,
isReplicated: true, isReplicated: true,
skipMultilineEncoding: doc.skipMultilineEncoding skipMultilineEncoding: doc.skipMultilineEncoding,
// except create operation other two needs the secret id and version id
...(operation !== SecretOperations.Create
? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id }
: {})
}; };
}) });
}); const approvalCommits = await secretApprovalRequestSecretDAL.insertMany(commits, tx);
nestedImportSecrets = nestedImportSecrets.concat(
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id })) return { ...approvalRequestDoc, commits: approvalCommits };
); });
} } else {
if (locallyUpdatedSecrets.length) { let nestedImportSecrets: TSyncSecretsDTO["secrets"] = [];
const newSecrets = await fnSecretBulkUpdate({ await secretReplicationDAL.transaction(async (tx) => {
projectId, if (locallyCreatedSecrets.length) {
folderId: importFolderId, const newSecrets = await fnSecretBulkInsert({
secretVersionDAL, folderId: importFolderId,
secretDAL, secretVersionDAL,
tx, secretDAL,
secretTagDAL, tx,
secretVersionTagDAL, secretTagDAL,
inputSecrets: locallyUpdatedSecrets.map(({ id }) => { secretVersionTagDAL,
const doc = replicatedSecretsGroupBySecretId[id][0]; inputSecrets: locallyCreatedSecrets.map(({ id }) => {
return { const doc = replicatedSecretsGroupBySecretId[id][0];
filter: { return {
folderId: importFolderId,
id: localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string][0].id
},
data: {
keyEncoding: doc.keyEncoding, keyEncoding: doc.keyEncoding,
algorithm: doc.algorithm, algorithm: doc.algorithm,
type: doc.type, type: doc.type,
@@ -293,60 +254,119 @@ export const secretReplicationServiceFactory = ({
secretCommentCiphertext: doc.secretCommentCiphertext, secretCommentCiphertext: doc.secretCommentCiphertext,
isReplicated: true, isReplicated: true,
skipMultilineEncoding: doc.skipMultilineEncoding skipMultilineEncoding: doc.skipMultilineEncoding
} };
}; })
}) });
}); nestedImportSecrets = nestedImportSecrets.concat(
nestedImportSecrets = nestedImportSecrets.concat( newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id }))
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Update, version, id })) );
); }
} if (locallyUpdatedSecrets.length) {
if (locallyDeletedSecrets.length) { const newSecrets = await fnSecretBulkUpdate({
const newSecrets = await secretDAL.delete( projectId,
{ folderId: importFolderId,
$in: { secretVersionDAL,
id: locallyDeletedSecrets.map(({ id }) => id) secretDAL,
tx,
secretTagDAL,
secretVersionTagDAL,
inputSecrets: locallyUpdatedSecrets.map(({ id }) => {
const doc = replicatedSecretsGroupBySecretId[id][0];
return {
filter: {
folderId: importFolderId,
id: localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string][0].id
},
data: {
keyEncoding: doc.keyEncoding,
algorithm: doc.algorithm,
type: doc.type,
metadata: doc.metadata,
secretKeyIV: doc.secretKeyIV,
secretKeyTag: doc.secretKeyTag,
secretKeyCiphertext: doc.secretKeyCiphertext,
secretValueIV: doc.secretValueIV,
secretValueTag: doc.secretValueTag,
secretValueCiphertext: doc.secretValueCiphertext,
secretBlindIndex: doc.secretBlindIndex,
secretCommentIV: doc.secretCommentIV,
secretCommentTag: doc.secretCommentTag,
secretCommentCiphertext: doc.secretCommentCiphertext,
isReplicated: true,
skipMultilineEncoding: doc.skipMultilineEncoding
}
};
})
});
nestedImportSecrets = nestedImportSecrets.concat(
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Update, version, id }))
);
}
if (locallyDeletedSecrets.length) {
const newSecrets = await secretDAL.delete(
{
$in: {
id: locallyDeletedSecrets.map(({ id }) => id)
},
isReplicated: true,
folderId: importFolderId
}, },
isReplicated: true, tx
folderId: importFolderId );
}, nestedImportSecrets = nestedImportSecrets.concat(
tx newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Delete, version, id }))
); );
nestedImportSecrets = nestedImportSecrets.concat( }
newSecrets.map(({ id, version }) => ({ operation: SecretOperations.Delete, version, id })) });
);
}
});
const folderLock = await keyStore const folderLock = await keyStore
.acquireLock([`secret-replication-${importFolderId}`], 5000) .acquireLock([`secret-replication-${importFolderId}`], 5000)
.catch(() => null); .catch(() => null);
if (folderLock) { if (folderLock) {
await snapshotService.performSnapshot(importFolderId); await snapshotService.performSnapshot(importFolderId);
await folderLock.release(); await folderLock.release();
}
await secretQueueService.syncSecrets({
projectId,
secretPath: importedFolder.path,
_deDupeReplicationQueue: deDupeReplicationQueue,
_deDupeQueue: deDupeQueue,
environmentSlug: importedFolder.environmentSlug,
actorId,
actor,
secrets: nestedImportSecrets,
folderId: importedFolder.id,
environmentId: importedFolder.envId
});
} }
await secretQueueService.syncSecrets({ // this is used to avoid multiple times generating secret approval by failed one
projectId, await keyStore.setItemWithExpiry(
secretPath: importedFolder.path, keystoreReplicationSuccessKey(job.id as string, secretImport.id),
_deDupeReplicationQueue: deDupeReplicationQueue, SECRET_IMPORT_SUCCESS_LOCK,
_deDupeQueue: deDupeQueue, 1,
environmentSlug: importedFolder.environmentSlug, KeyStorePrefixes.SecretReplication
actorId, );
actor,
secrets: nestedImportSecrets, await secretImportDAL.updateById(secretImport.id, {
folderId: importedFolder.id, lastReplicated: new Date(),
environmentId: importedFolder.envId replicationStatus: null,
isReplicationSuccess: true
});
} catch (err) {
logger.error(
err,
`Failed to replicate secret with import id=[${secretImport.id}] env=[${secretImport.importEnv.slug}] path=[${secretImport.importPath}]`
);
await secretImportDAL.updateById(secretImport.id, {
lastReplicated: new Date(),
replicationStatus: (err as Error)?.message.slice(0, 500),
isReplicationSuccess: false
}); });
} }
await keyStore.setItemWithExpiry(
keystoreReplicationSuccessKey(job.id as string, secretImport.id),
SECRET_IMPORT_SUCCESS_LOCK,
1,
KeyStorePrefixes.SecretReplication
);
} }
await secretVersionDAL.update({ $in: { id: replicatedSecrets.map(({ id }) => id) } }, { isReplicated: true }); await secretVersionDAL.update({ $in: { id: replicatedSecrets.map(({ id }) => id) } }, { isReplicated: true });
/* eslint-enable no-await-in-loop */ /* eslint-enable no-await-in-loop */
} finally { } finally {
@@ -11,6 +11,9 @@ export type TSecretImport = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
isReplication?: boolean; isReplication?: boolean;
isReplicationSuccess?: boolean;
replicationStatus?: string;
lastReplicated?: string;
}; };
export type TGetImportedFoldersByEnvDTO = { export type TGetImportedFoldersByEnvDTO = {
@@ -2,19 +2,31 @@ import { useEffect } from "react";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { useSortable } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable";
import { import {
faCalendarCheck,
faClose, faClose,
faFileImport, faFileImport,
faFolder, faFolder,
faInfoCircle,
faKey, faKey,
faRotate, faRotate,
faUpDown faUpDown,
faWarning,
faXmark
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, IconButton, SecretInput, TableContainer, Tag } from "@app/components/v2"; import {
EmptyState,
IconButton,
SecretInput,
TableContainer,
Tag,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { useResyncSecretReplication } from "@app/hooks/api"; import { useResyncSecretReplication } from "@app/hooks/api";
@@ -24,6 +36,9 @@ type Props = {
environment: string; environment: string;
secretPath?: string; secretPath?: string;
isReplication?: boolean; isReplication?: boolean;
isReplicationSuccess?: boolean;
replicationStatus?: string;
lastReplicated?: string;
importEnvName: string; importEnvName: string;
importEnvPath: string; importEnvPath: string;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[]; importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
@@ -62,7 +77,10 @@ export const SecretImportItem = ({
importedSecrets = [], importedSecrets = [],
searchTerm = "", searchTerm = "",
secretPath, secretPath,
environment environment,
isReplicationSuccess,
replicationStatus,
lastReplicated
}: Props) => { }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const [isExpanded, setIsExpanded] = useToggle(); const [isExpanded, setIsExpanded] = useToggle();
@@ -137,7 +155,44 @@ export const SecretImportItem = ({
isReplication={isReplication} isReplication={isReplication}
/> />
</div> </div>
<div className="flex items-center space-x-2 px-4 py-2"> <div className="flex items-center space-x-4 px-4 py-2">
{lastReplicated && (
<Tooltip
position="left"
className="max-w-md whitespace-normal break-words"
content={
<div className="flex max-h-[10rem] flex-col overflow-auto ">
<div className="flex self-start">
<FontAwesomeIcon icon={faCalendarCheck} className="pt-0.5 pr-2 text-sm" />
<div className="text-sm">Last Replication</div>
</div>
<div className="pl-5 text-left text-xs">
{lastReplicated
? format(new Date(lastReplicated), "yyyy-MM-dd, hh:mm aaa")
: "-"}
</div>
{!isReplicationSuccess && (
<>
<div className="mt-2 flex self-start">
<FontAwesomeIcon icon={faXmark} className="pt-1 pr-2 text-sm" />
<div className="text-sm">Fail reason</div>
</div>
<div className="pl-5 text-left text-xs">{replicationStatus}</div>
</>
)}
</div>
}
>
<div
className={twMerge(
"opacity-0 group-hover:opacity-100",
!isReplicationSuccess && "text-red-600"
)}
>
<FontAwesomeIcon icon={isReplicationSuccess ? faInfoCircle : faWarning} />
</div>
</Tooltip>
)}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })} a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
@@ -159,7 +159,15 @@ export const SecretImportListView = ({
> >
<SortableContext items={items} strategy={verticalListSortingStrategy}> <SortableContext items={items} strategy={verticalListSortingStrategy}>
{items?.map((item) => { {items?.map((item) => {
const { importPath, importEnv, id, isReplication } = item; const {
importPath,
importEnv,
id,
isReplication,
replicationStatus,
lastReplicated,
isReplicationSuccess
} = item;
return ( return (
<SecretImportItem <SecretImportItem
searchTerm={searchTerm} searchTerm={searchTerm}
@@ -168,6 +176,9 @@ export const SecretImportListView = ({
isReplication={isReplication} isReplication={isReplication}
importEnvPath={importPath} importEnvPath={importPath}
importEnvName={importEnv.name} importEnvName={importEnv.name}
lastReplicated={lastReplicated}
replicationStatus={replicationStatus}
isReplicationSuccess={isReplicationSuccess}
importedSecrets={computeImportedSecretRows( importedSecrets={computeImportedSecretRows(
importEnv.slug, importEnv.slug,
importPath, importPath,