Merge branch 'main' into ENG-2705

This commit is contained in:
x032205
2025-06-12 15:18:00 -04:00
169 changed files with 12560 additions and 719 deletions
@@ -44,6 +44,7 @@ import {
} from "@app/hooks/api";
import { useGetProjectSecretsDetails } from "@app/hooks/api/dashboard";
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
import { useGetFolderCommitsCount } from "@app/hooks/api/folderCommits";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
@@ -277,6 +278,17 @@ const Page = () => {
limit: 10
});
const {
data: { count: folderCommitsCount, folderId } = { count: 0, folderId: "" },
isPending: isFolderCommitsCountLoading,
isFetching: isFolderCommitsCountFetching
} = useGetFolderCommitsCount({
directory: secretPath,
workspaceId,
environment,
isPaused: !canDoReadRollback
});
const {
data: snapshotCount,
isPending: isSnapshotCountLoading,
@@ -288,6 +300,39 @@ const Page = () => {
isPaused: !canDoReadRollback
});
const isPITEnabled = !currentWorkspace?.showSnapshotsLegacy;
const changesCount = useMemo(() => {
return isPITEnabled ? folderCommitsCount : snapshotCount;
}, [folderCommitsCount, snapshotCount]);
const isChangesCountPending = useMemo(() => {
return isPITEnabled ? isFolderCommitsCountLoading || isSnapshotCountLoading : false;
}, [isFolderCommitsCountLoading, isSnapshotCountLoading]);
const isChangesCountFetching = useMemo(() => {
return isPITEnabled ? isFolderCommitsCountFetching || isSnapshotCountFetching : false;
}, [isFolderCommitsCountFetching, isSnapshotCountFetching]);
const handleOnClickRollbackMode = () => {
if (isPITEnabled) {
navigate({
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId` as const,
params: {
projectId: workspaceId,
folderId,
environment
},
search: (query) => ({
...query,
secretPath
})
});
} else {
handlePopUpToggle("snapshots", true);
}
};
const noAccessSecretCount = Math.max(
(page * perPage > totalCount ? totalCount % perPage : perPage) -
(imports?.length || 0) -
@@ -448,13 +493,14 @@ const Page = () => {
onVisibilityToggle={handleToggleVisibility}
onSearchChange={handleSearchChange}
onToggleTagFilter={handleTagToggle}
snapshotCount={snapshotCount || 0}
isSnapshotCountLoading={isSnapshotCountLoading && isSnapshotCountFetching}
snapshotCount={changesCount || 0}
isSnapshotCountLoading={isChangesCountPending && isChangesCountFetching}
onToggleRowType={handleToggleRowType}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
onClickRollbackMode={handleOnClickRollbackMode}
protectedBranchPolicyName={boardPolicy?.name}
importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
isPITEnabled={isPITEnabled}
/>
<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">
@@ -618,7 +664,7 @@ const Page = () => {
secretPath={secretPath}
secrets={secrets}
folders={folders}
snapshotCount={snapshotCount}
snapshotCount={changesCount}
onGoBack={handleResetSnapshot}
onClickListSnapshot={() => handlePopUpToggle("snapshots", true)}
/>
@@ -56,7 +56,10 @@ import {
useSubscription,
useWorkspace
} from "@app/context";
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
import {
ProjectPermissionCommitsActions,
ProjectPermissionSecretRotationActions
} from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks";
import {
useCreateFolder,
@@ -123,6 +126,7 @@ type Props = {
isImported: boolean;
}[];
}[];
isPITEnabled: boolean;
};
export const ActionBar = ({
@@ -142,6 +146,7 @@ export const ActionBar = ({
onToggleRowType,
protectedBranchPolicyName,
importedBy,
isPITEnabled = false,
usedBySecretSyncs
}: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
@@ -777,8 +782,8 @@ export const ActionBar = ({
</div>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SecretRollback}
I={isPITEnabled ? ProjectPermissionCommitsActions.Read : ProjectPermissionActions.Read}
a={isPITEnabled ? ProjectPermissionSub.Commits : ProjectPermissionSub.SecretRollback}
>
{(isAllowed) => (
<Button
@@ -796,7 +801,7 @@ export const ActionBar = ({
className="h-10"
isDisabled={!isAllowed}
>
{`${snapshotCount} ${snapshotCount === 1 ? "Snapshot" : "Snapshots"}`}
{`${snapshotCount} ${isPITEnabled ? "Commit" : "Snapshot"}${snapshotCount === 1 ? "" : "s"}`}
</Button>
)}
</ProjectPermissionCan>
@@ -61,24 +61,38 @@ const formSchema = z
.object({
provider: z.discriminatedUnion("credentialType", [
z.object({
url: z.string().url().trim().min(1),
url: z.string().trim().optional(),
clusterToken: z.string().trim().optional(),
ca: z.string().optional(),
sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesDynamicSecretCredentialType.Static),
serviceAccountName: z.string().trim().min(1),
namespace: z.string().trim().min(1),
namespace: z
.string()
.trim()
.min(1)
.refine(
(val) => !val.includes(","),
"Namespace must be a single value, not a comma-separated list"
),
gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)),
authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api)
}),
z.object({
url: z.string().url().trim().min(1),
url: z.string().trim().optional(),
clusterToken: z.string().trim().optional(),
ca: z.string().optional(),
sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesDynamicSecretCredentialType.Dynamic),
namespace: z.string().trim().min(1),
namespace: z
.string()
.trim()
.min(1)
.refine((val) => {
const namespaces = val.split(",").map((ns) => ns.trim());
return namespaces.length > 0 && namespaces.every((ns) => ns.length > 0);
}, "Must be a valid comma-separated list of namespace values"),
gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)),
roleType: z.nativeEnum(RoleType),
@@ -116,12 +130,21 @@ const formSchema = z
message: "When auth method is set to Gateway, a gateway must be selected"
});
}
if (data.provider.authMethod === AuthMethod.Api && !data.provider.clusterToken) {
ctx.addIssue({
path: ["provider.clusterToken"],
code: z.ZodIssueCode.custom,
message: "When auth method is set to Token, a cluster token must be provided"
});
if (data.provider.authMethod === AuthMethod.Api) {
if (!data.provider.clusterToken) {
ctx.addIssue({
path: ["provider.clusterToken"],
code: z.ZodIssueCode.custom,
message: "When auth method is set to Token, a cluster token must be provided"
});
}
if (!data.provider.url) {
ctx.addIssue({
path: ["provider.url"],
code: z.ZodIssueCode.custom,
message: "When auth method is set to Token, a cluster URL must be provided"
});
}
}
});
@@ -188,7 +211,13 @@ export const KubernetesInputForm = ({
try {
const isDefaultUsernameTemplate = usernameTemplate === "{{randomUsername}}";
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.Kubernetes, inputs: provider },
provider: {
type: DynamicSecretProviders.Kubernetes,
inputs: {
...provider,
url: provider.url || undefined
}
},
maxTTL: rest.maxTTL,
name: rest.name,
path: secretPath,
@@ -333,69 +362,6 @@ export const KubernetesInputForm = ({
)}
</OrgPermissionCan>
</div>
<Controller
control={control}
name="provider.url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Cluster URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mb-2 flex items-center">
<span className="mr-3 flex items-center text-sm text-mineshaft-400">
Enable SSL
<Tooltip
className="ml-1 max-w-md"
content={
<span>
If enabled, you can optionally provide a custom CA certificate. Leave
blank to use the system/public CA.
</span>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</span>
<Controller
name="provider.sslEnabled"
control={control}
render={({ field: { value, onChange } }) => (
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-enabled"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
aria-label="Enable SSL"
/>
)}
/>
</div>
<Controller
control={control}
name="provider.ca"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA"
isError={Boolean(error?.message)}
errorText={error?.message}
className={sslEnabled ? "" : "opacity-50"}
>
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
isDisabled={!sslEnabled}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.authMethod"
@@ -420,6 +386,71 @@ export const KubernetesInputForm = ({
</FormControl>
)}
/>
{authMethod === AuthMethod.Api && (
<>
<Controller
control={control}
name="provider.url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Cluster URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mb-2 flex items-center">
<span className="mr-3 flex items-center text-sm text-mineshaft-400">
Enable SSL
<Tooltip
className="ml-1 max-w-md"
content={
<span>
If enabled, you can optionally provide a custom CA certificate. Leave
blank to use the system/public CA.
</span>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</span>
<Controller
name="provider.sslEnabled"
control={control}
render={({ field: { value, onChange } }) => (
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-enabled"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
aria-label="Enable SSL"
/>
)}
/>
</div>
<Controller
control={control}
name="provider.ca"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA"
isError={Boolean(error?.message)}
errorText={error?.message}
className={sslEnabled ? "" : "opacity-50"}
>
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
isDisabled={!sslEnabled}
/>
</FormControl>
)}
/>
</>
)}
{authMethod === AuthMethod.Api && (
<Controller
control={control}
@@ -507,7 +538,11 @@ export const KubernetesInputForm = ({
name="provider.namespace"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Namespace"
label={
credentialType === KubernetesDynamicSecretCredentialType.Static
? "Namespace"
: "Allowed Namespace(s)"
}
isError={Boolean(error?.message)}
errorText={error?.message}
>
@@ -371,12 +371,149 @@ const renderOutputForm = (
return null;
};
const kubernetesFormSchema = z.object({
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional(),
namespace: z.string().optional()
});
type TKubernetesForm = z.infer<typeof kubernetesFormSchema>;
export const CreateKubernetesDynamicSecretLease = ({
onClose,
projectSlug,
dynamicSecretName,
provider,
secretPath,
environment
}: Props) => {
const {
control,
formState: { isSubmitting },
handleSubmit
} = useForm<TKubernetesForm>({
resolver: zodResolver(kubernetesFormSchema),
defaultValues: {
ttl: "1h"
}
});
const createDynamicSecretLease = useCreateDynamicSecretLease();
const handleDynamicSecretLeaseCreate = async ({ ttl, namespace }: TKubernetesForm) => {
if (createDynamicSecretLease.isPending) return;
try {
await createDynamicSecretLease.mutateAsync({
environmentSlug: environment,
projectSlug,
path: secretPath,
ttl,
dynamicSecretName,
config: {
namespace: namespace || undefined
},
provider
});
createNotification({
type: "success",
text: "Successfully leased dynamic secret"
});
} catch (error) {
console.log(error);
createNotification({
type: "error",
text: "Failed to lease dynamic secret"
});
}
};
const handleLeaseRegeneration = async (data: { ttl?: string }) => {
handleDynamicSecretLeaseCreate(data);
};
const isOutputMode = Boolean(createDynamicSecretLease?.data);
return (
<div>
<AnimatePresence>
{!isOutputMode && (
<motion.div
key="lease-input"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<form onSubmit={handleSubmit(handleDynamicSecretLeaseCreate)}>
<Controller
control={control}
name="namespace"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Namespace"
isError={Boolean(error?.message)}
errorText={error?.message}
helperText="The Kubernetes namespace to lease the dynamic secret to. If not specified, the first namespace defined in the configuration will be used."
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="ttl"
defaultValue="1h"
render={({ field, fieldState: { error } }) => (
<FormControl
label={<TtlFormLabel label="Default TTL" />}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
</motion.div>
)}
{isOutputMode && (
<motion.div
key="lease-output"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
{renderOutputForm(
provider,
createDynamicSecretLease.data?.data,
handleLeaseRegeneration
)}
</motion.div>
)}
</AnimatePresence>
</div>
);
};
const formSchema = z.object({
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
});
type TForm = z.infer<typeof formSchema>;
type Props = {
@@ -422,7 +559,8 @@ export const CreateDynamicSecretLease = ({
projectSlug,
path: secretPath,
ttl,
dynamicSecretName
dynamicSecretName,
provider
});
createNotification({
@@ -451,6 +589,19 @@ export const CreateDynamicSecretLease = ({
}
}, [provider]);
if (provider === DynamicSecretProviders.Kubernetes) {
return (
<CreateKubernetesDynamicSecretLease
onClose={onClose}
projectSlug={projectSlug}
dynamicSecretName={dynamicSecretName}
provider={provider}
secretPath={secretPath}
environment={environment}
/>
);
}
const isOutputMode = Boolean(createDynamicSecretLease?.data);
if (isPreloading) {
@@ -59,24 +59,38 @@ const formSchema = z
.object({
inputs: z.discriminatedUnion("credentialType", [
z.object({
url: z.string().url().trim().min(1),
url: z.string().trim().optional(),
clusterToken: z.string().trim().optional(),
ca: z.string().optional(),
sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesDynamicSecretCredentialType.Static),
serviceAccountName: z.string().trim().min(1),
namespace: z.string().trim().min(1),
namespace: z
.string()
.trim()
.min(1)
.refine(
(val) => !val.includes(","),
"Namespace must be a single value, not a comma-separated list"
),
gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)),
authMethod: z.nativeEnum(AuthMethod).default(AuthMethod.Api)
}),
z.object({
url: z.string().url().trim().min(1),
url: z.string().trim().optional(),
clusterToken: z.string().trim().optional(),
ca: z.string().optional(),
sslEnabled: z.boolean().default(false),
credentialType: z.literal(KubernetesDynamicSecretCredentialType.Dynamic),
namespace: z.string().trim().min(1),
namespace: z
.string()
.trim()
.min(1)
.refine((val) => {
const namespaces = val.split(",").map((ns) => ns.trim());
return namespaces.length > 0 && namespaces.every((ns) => ns.length > 0);
}, "Must be a valid comma-separated list of namespace values"),
gatewayId: z.string().optional(),
audiences: z.array(z.string().trim().min(1)),
roleType: z.nativeEnum(RoleType),
@@ -113,12 +127,21 @@ const formSchema = z
message: "When auth method is set to Gateway, a gateway must be selected"
});
}
if (data.inputs.authMethod === AuthMethod.Api && !data.inputs.clusterToken) {
ctx.addIssue({
path: ["inputs.clusterToken"],
code: z.ZodIssueCode.custom,
message: "When auth method is set to Token, a cluster token must be provided"
});
if (data.inputs.authMethod === AuthMethod.Api) {
if (!data.inputs.clusterToken) {
ctx.addIssue({
path: ["inputs.clusterToken"],
code: z.ZodIssueCode.custom,
message: "When auth method is set to Token, a cluster token must be provided"
});
}
if (!data.inputs.url) {
ctx.addIssue({
path: ["inputs.url"],
code: z.ZodIssueCode.custom,
message: "When auth method is set to Token, a cluster URL must be provided"
});
}
}
});
@@ -178,7 +201,10 @@ export const EditDynamicSecretKubernetesForm = ({
projectSlug,
environmentSlug: environment,
data: {
inputs: formData.inputs,
inputs: {
...formData.inputs,
url: formData.inputs.url || undefined
},
newName: formData.newName === dynamicSecret.name ? undefined : formData.newName,
defaultTTL: formData.defaultTTL,
maxTTL: formData.maxTTL,
@@ -328,70 +354,6 @@ export const EditDynamicSecretKubernetesForm = ({
)}
</OrgPermissionCan>
</div>
<Controller
control={control}
name="inputs.url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Cluster URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mb-2 flex items-center">
<span className="mr-3 flex items-center text-sm text-mineshaft-400">
Enable SSL
<Tooltip
className="ml-1 max-w-md"
content={
<span>
If enabled, you can optionally provide a custom CA certificate. Leave
blank to use the system/public CA.
</span>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</span>
<Controller
name="inputs.sslEnabled"
control={control}
render={({ field: { value, onChange } }) => (
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-enabled"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
aria-label="Enable SSL"
/>
)}
/>
</div>
<Controller
control={control}
name="inputs.ca"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA"
isError={Boolean(error?.message)}
errorText={error?.message}
className={sslEnabled ? "" : "opacity-50"}
>
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
isDisabled={!sslEnabled}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.authMethod"
@@ -416,6 +378,73 @@ export const EditDynamicSecretKubernetesForm = ({
</FormControl>
)}
/>
{authMethod === AuthMethod.Api && (
<>
<Controller
control={control}
name="inputs.url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Cluster URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mb-2 flex items-center">
<span className="mr-3 flex items-center text-sm text-mineshaft-400">
Enable SSL
<Tooltip
className="ml-1 max-w-md"
content={
<span>
If enabled, you can optionally provide a custom CA certificate.
Leave blank to use the system/public CA.
</span>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</span>
<Controller
name="inputs.sslEnabled"
control={control}
render={({ field: { value, onChange } }) => (
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-enabled"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
aria-label="Enable SSL"
/>
)}
/>
</div>
<Controller
control={control}
name="inputs.ca"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA"
isError={Boolean(error?.message)}
errorText={error?.message}
className={sslEnabled ? "" : "opacity-50"}
>
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
isDisabled={!sslEnabled}
/>
</FormControl>
)}
/>
</>
)}
{authMethod === AuthMethod.Api && (
<Controller
control={control}
@@ -502,7 +531,11 @@ export const EditDynamicSecretKubernetesForm = ({
name="inputs.namespace"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Namespace"
label={
credentialType === KubernetesDynamicSecretCredentialType.Static
? "Namespace"
: "Allowed Namespace(s)"
}
isError={Boolean(error?.message)}
errorText={error?.message}
>
@@ -3,6 +3,7 @@ import { InfiniteData } from "@tanstack/react-query";
import { formatDistance } from "date-fns";
import { Button, Drawer, DrawerContent } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { TSecretSnapshot } from "@app/hooks/api/secretSnapshots/types";
type Props = {
@@ -39,6 +40,20 @@ export const PitDrawer = ({
subTitle="Note: This will recover secrets for all environments in this project"
>
<div className="flex flex-col space-y-2">
<NoticeBannerV2 title="Snapshots are being deprecated" className="mb-2">
<p className="my-1 text-sm text-mineshaft-300">
Snapshots are being deprecated in favor of{" "}
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/pit-recovery"
rel="noopener noreferrer"
className="underline decoration-primary underline-offset-2 hover:text-mineshaft-200"
>
Commits
</a>
. This feature will be officially removed in November 2025.
</p>
</NoticeBannerV2>
{secretSnaphots?.pages?.map((group, i) => (
<Fragment key={`snapshot-item-${i + 1}`}>
{group.map(({ id, createdAt }, index) => (
@@ -9,6 +9,7 @@ import { usePopUp } from "@app/hooks";
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { commitKeys } from "@app/hooks/api/folderCommits/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";
@@ -264,6 +265,12 @@ export const SecretListView = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: secretApprovalRequestKeys.count({ workspaceId })
});
@@ -315,6 +322,12 @@ export const SecretListView = ({
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: secretApprovalRequestKeys.count({ workspaceId })
});
@@ -12,6 +12,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, ContentLoader, Input, Tag, Tooltip } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useGetSnapshotSecrets, usePerformSecretRollback } from "@app/hooks/api";
import { SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types";
@@ -60,6 +61,7 @@ export const SnapshotView = ({
const rollingFolder = snapshotData?.folders || [];
const rollingSecrets = snapshotData?.secrets || [];
const isAllowedRollback = false;
const folderDiffView = useMemo(() => {
const folderGroupById = folders.reduce<Record<string, TSecretFolder>>(
@@ -157,6 +159,20 @@ export const SnapshotView = ({
<h6 className="text-2xl">Snapshot</h6>
<Tag colorSchema="green">{new Date(snapshotData?.createdAt || "").toLocaleString()}</Tag>
</div>
<NoticeBannerV2 title="Snapshots are being deprecated" className="my-2">
<p className="my-1 text-sm text-mineshaft-300">
Snapshots are being deprecated in favor of{" "}
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/pit-recovery"
rel="noopener noreferrer"
className="underline decoration-primary underline-offset-2 hover:text-mineshaft-200"
>
Commits
</a>
. This feature will be officially removed in November 2025.
</p>
</NoticeBannerV2>
<div className="mt-4 flex items-center space-x-2">
<div className="w-2/5">
<Input
@@ -168,17 +184,19 @@ export const SnapshotView = ({
/>
</div>
<div className="flex-grow" />
<div>
<Button
variant="outline_bg"
onClick={onClickListSnapshot}
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
className="h-10"
isDisabled={isRollingBack}
>
{snapshotCount} Commits
</Button>
</div>
{isAllowedRollback && (
<div>
<Button
variant="outline_bg"
onClick={onClickListSnapshot}
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
className="h-10"
isDisabled={isRollingBack}
>
{snapshotCount} Commits
</Button>
</div>
)}
<div>
<Button
onClick={onGoBack}
@@ -198,7 +216,7 @@ export const SnapshotView = ({
{(isAllowed) => (
<Button
onClick={handleClickRollback}
isDisabled={isRollingBack || !isAllowed}
isDisabled={isRollingBack || !isAllowed || !isAllowedRollback}
isLoading={isRollingBack}
leftIcon={<FontAwesomeIcon icon={faUndo} />}
className="h-10"