Merge branch 'main' into feat/addReplicateFolderContent

This commit is contained in:
carlosmonastyrski
2025-04-01 08:16:09 -03:00
261 changed files with 6743 additions and 2170 deletions
@@ -29,9 +29,6 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
</div>
<LogsSection
refetchInterval={4000}
remappedHeaders={{
Metadata: "Sync Status"
}}
showFilters={false}
presets={{
eventMetadata: { integrationId: integration.id },
@@ -192,6 +192,15 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
);
}
if (integration.integration === "windmill" && integration.url) {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Instance URL" />
<div className="text-sm text-mineshaft-300">{integration.url}</div>
</div>
);
}
return null;
};
@@ -7,6 +7,7 @@ import {
faAngleDown,
faArrowDown,
faArrowUp,
faFileImport,
faFingerprint,
faFolder,
faFolderBlank,
@@ -97,7 +98,8 @@ export enum EntryType {
enum RowType {
Folder = "folder",
DynamicSecret = "dynamic",
Secret = "secret"
Secret = "secret",
Import = "import"
}
type Filter = {
@@ -107,7 +109,8 @@ type Filter = {
const DEFAULT_FILTER_STATE = {
[RowType.Folder]: true,
[RowType.DynamicSecret]: true,
[RowType.Secret]: true
[RowType.Secret]: true,
[RowType.Import]: true
};
export const OverviewPage = () => {
@@ -199,12 +202,16 @@ export const OverviewPage = () => {
setVisibleEnvs(userAvailableEnvs);
}, [userAvailableEnvs]);
const { isImportedSecretPresentInEnv, getImportedSecretByKey, getEnvImportedSecretKeyCount } =
useGetImportedSecretsAllEnvs({
projectId: workspaceId,
path: secretPath,
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
});
const {
secretImports,
isImportedSecretPresentInEnv,
getImportedSecretByKey,
getEnvImportedSecretKeyCount
} = useGetImportedSecretsAllEnvs({
projectId: workspaceId,
path: secretPath,
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
});
const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
{
@@ -216,6 +223,7 @@ export const OverviewPage = () => {
includeFolders: filter.folder,
includeDynamicSecrets: filter.dynamic,
includeSecrets: filter.secret,
includeImports: filter.import,
search: debouncedSearchFilter,
limit,
offset
@@ -230,12 +238,29 @@ export const OverviewPage = () => {
totalFolderCount,
totalSecretCount,
totalDynamicSecretCount,
totalImportCount,
totalCount = 0,
totalUniqueFoldersInPage,
totalUniqueSecretsInPage,
totalUniqueSecretImportsInPage,
totalUniqueDynamicSecretsInPage
} = overview ?? {};
const secretImportsShaped = secretImports
?.flatMap(({ data }) => data)
.filter(Boolean)
.flatMap((item) => item?.secrets || []);
const handleIsImportedSecretPresentInEnv = (envSlug: string, secretName: string) => {
if (secrets?.some((s) => s.key === secretName && s.env === envSlug)) {
return false;
}
if (secretImportsShaped.some((s) => s.key === secretName && s.sourceEnv === envSlug)) {
return true;
}
return isImportedSecretPresentInEnv(envSlug, secretName);
};
useResetPageHelper({
totalCount,
offset,
@@ -248,7 +273,18 @@ export const OverviewPage = () => {
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
useDynamicSecretOverview(dynamicSecrets);
const { secKeys, getSecretByKey, getEnvSecretKeyCount } = useSecretOverview(secrets);
const { secKeys, getEnvSecretKeyCount } = useSecretOverview(
secrets?.concat(secretImportsShaped) || []
);
const getSecretByKey = useCallback(
(env: string, key: string) => {
const sec = secrets?.find((s) => s.env === env && s.key === key);
return sec;
},
[secrets]
);
const { data: tags } = useGetWsTags(
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? workspaceId : ""
);
@@ -678,7 +714,6 @@ export const OverviewPage = () => {
<SecretV2MigrationSection />
</div>
);
return (
<div className="">
<Helmet>
@@ -767,6 +802,19 @@ export const OverviewPage = () => {
</Button>
</DropdownMenuItem> */}
<DropdownMenuLabel>Filter project resources</DropdownMenuLabel>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
handleToggleRowType(RowType.Import);
}}
icon={filter[RowType.Import] && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faFileImport} className="text-green-700" />
<span>Imports</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
@@ -1099,7 +1147,7 @@ export const OverviewPage = () => {
onToggleSecretSelect={() => toggleSelectedEntry(EntryType.SECRET, key)}
secretPath={secretPath}
getImportedSecretByKey={getImportedSecretByKey}
isImportedSecretPresentInEnv={isImportedSecretPresentInEnv}
isImportedSecretPresentInEnv={handleIsImportedSecretPresentInEnv}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
@@ -1116,7 +1164,8 @@ export const OverviewPage = () => {
(page * perPage > totalCount ? totalCount % perPage : perPage) -
(totalUniqueFoldersInPage || 0) -
(totalUniqueDynamicSecretsInPage || 0) -
(totalUniqueSecretsInPage || 0),
(totalUniqueSecretsInPage || 0) -
(totalUniqueSecretImportsInPage || 0),
0
)}
/>
@@ -1156,6 +1205,7 @@ export const OverviewPage = () => {
dynamicSecretCount={totalDynamicSecretCount}
secretCount={totalSecretCount}
folderCount={totalFolderCount}
importCount={totalImportCount}
/>
}
className="rounded-b-md border-t border-solid border-t-mineshaft-600"
@@ -162,7 +162,7 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
render={({ field, fieldState: { error } }) => (
<Input
autoComplete="off"
isReadOnly={isReadOnly}
isReadOnly={isReadOnly || secrets.filter(Boolean).length === 0}
autoCapitalization={currentWorkspace?.autoCapitalization}
variant="plain"
isDisabled={isOverriden}
@@ -152,7 +152,7 @@ export const AccessApprovalRequest = ({
const isAccepted = request.isApproved;
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
const isRequestedByCurrentUser = request.requestedByUserId === user.id;
const isSelfApproveAllowed = request.policy.allowedSelfApprovals;
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
@@ -189,7 +189,8 @@ export const AccessApprovalRequest = ({
userReviewStatus,
isAccepted,
isSoftEnforcement,
isRequestedByCurrentUser
isRequestedByCurrentUser,
isSelfApproveAllowed
};
};
@@ -342,15 +343,16 @@ export const AccessApprovalRequest = ({
tabIndex={0}
onClick={() => {
if (
(!details.isApprover ||
((!details.isApprover ||
details.isReviewedByUser ||
details.isRejectedByAnyone ||
details.isAccepted) &&
!(
details.isSoftEnforcement &&
details.isRequestedByCurrentUser &&
!details.isAccepted
)
!(
details.isSoftEnforcement &&
details.isRequestedByCurrentUser &&
!details.isAccepted
)) ||
(request.requestedByUserId === user.id && !details.isSelfApproveAllowed)
)
return;
if (membersGroupById?.[request.requestedByUserId].user) {
@@ -12,7 +12,8 @@ import {
Modal,
ModalContent,
Select,
SelectItem
SelectItem,
Switch
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { getMemberLabel } from "@app/helpers/members";
@@ -54,7 +55,8 @@ const formSchema = z
.array()
.default([]),
policyType: z.nativeEnum(PolicyType),
enforcementLevel: z.nativeEnum(EnforcementLevel)
enforcementLevel: z.nativeEnum(EnforcementLevel),
allowedSelfApprovals: z.boolean().default(true)
})
.superRefine((data, ctx) => {
if (!(data.groupApprovers.length || data.userApprovers.length)) {
@@ -101,7 +103,8 @@ export const AccessPolicyForm = ({
editValues?.approvers
?.filter((approver) => approver.type === ApproverType.Group)
.map(({ id, type }) => ({ id, type: type as ApproverType.Group })) || [],
approvals: editValues?.approvals
approvals: editValues?.approvals,
allowedSelfApprovals: editValues?.allowedSelfApprovals
}
: undefined
});
@@ -441,6 +444,27 @@ export const AccessPolicyForm = ({
</FormControl>
)}
/>
<Controller
control={control}
name="allowedSelfApprovals"
defaultValue
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Self Approvals"
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
id="self-approvals"
thumbClassName="bg-mineshaft-800"
isChecked={value}
onCheckedChange={onChange}
>
Allow approvers to review their own requests
</Switch>
</FormControl>
)}
/>
<div className="mt-8 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
Save
@@ -127,7 +127,9 @@ export const SecretApprovalRequestChanges = ({
} = useForm<TReviewFormSchema>({
resolver: zodResolver(reviewFormSchema)
});
const shouldBlockSelfReview =
secretApprovalRequestDetails?.policy?.allowedSelfApprovals === false &&
secretApprovalRequestDetails?.committerUserId === userSession.id;
const isApproving = variables?.status === ApprovalStatus.APPROVED && isUpdatingRequestStatus;
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
@@ -245,117 +247,119 @@ export const SecretApprovalRequestChanges = ({
</div>
</div>
</div>
{!hasMerged && secretApprovalRequestDetails.status === "open" && (
<DropdownMenu
open={popUp.reviewChanges.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
>
<DropdownMenuTrigger asChild>
<Button
variant="outline_bg"
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
>
Review
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" asChild className="mt-3">
<form onSubmit={handleSubmit(handleSubmitReview)}>
<div className="flex w-[400px] flex-col space-y-2 p-5">
<div className="text-lg font-medium">Finish your review</div>
<Controller
control={control}
name="comment"
render={({ field, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<TextArea
{...field}
placeholder="Leave a comment..."
reSize="none"
className="text-md mt-2 h-48 border border-mineshaft-600 bg-bunker-800"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="status"
defaultValue={ApprovalStatus.APPROVED}
render={({ field, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<RadioGroup
value={field.value}
onValueChange={field.onChange}
className="mb-4 space-y-2"
aria-label="Status"
>
<div className="flex items-center gap-2">
<RadioGroupItem
id="approve"
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
value={ApprovalStatus.APPROVED}
aria-labelledby="approve-label"
>
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
</RadioGroupItem>
<span
id="approve-label"
className="cursor-pointer"
onClick={() => field.onChange(ApprovalStatus.APPROVED)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
field.onChange(ApprovalStatus.APPROVED);
}
}}
tabIndex={0}
role="button"
>
Approve
</span>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem
id="reject"
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
value={ApprovalStatus.REJECTED}
aria-labelledby="reject-label"
>
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
</RadioGroupItem>
<span
id="reject-label"
className="cursor-pointer"
onClick={() => field.onChange(ApprovalStatus.REJECTED)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
field.onChange(ApprovalStatus.REJECTED);
}
}}
tabIndex={0}
role="button"
>
Reject
</span>
</div>
</RadioGroup>
</FormControl>
)}
/>
<div className="flex justify-end">
<Button
type="submit"
isLoading={isApproving || isRejecting || isSubmitting}
variant="outline_bg"
>
Submit Review
</Button>
{!hasMerged &&
secretApprovalRequestDetails.status === "open" &&
!shouldBlockSelfReview && (
<DropdownMenu
open={popUp.reviewChanges.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("reviewChanges", isOpen)}
>
<DropdownMenuTrigger asChild>
<Button
variant="outline_bg"
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
>
Review
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" asChild className="mt-3">
<form onSubmit={handleSubmit(handleSubmitReview)}>
<div className="flex w-[400px] flex-col space-y-2 p-5">
<div className="text-lg font-medium">Finish your review</div>
<Controller
control={control}
name="comment"
render={({ field, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<TextArea
{...field}
placeholder="Leave a comment..."
reSize="none"
className="text-md mt-2 h-48 border border-mineshaft-600 bg-bunker-800"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="status"
defaultValue={ApprovalStatus.APPROVED}
render={({ field, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<RadioGroup
value={field.value}
onValueChange={field.onChange}
className="mb-4 space-y-2"
aria-label="Status"
>
<div className="flex items-center gap-2">
<RadioGroupItem
id="approve"
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
value={ApprovalStatus.APPROVED}
aria-labelledby="approve-label"
>
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
</RadioGroupItem>
<span
id="approve-label"
className="cursor-pointer"
onClick={() => field.onChange(ApprovalStatus.APPROVED)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
field.onChange(ApprovalStatus.APPROVED);
}
}}
tabIndex={0}
role="button"
>
Approve
</span>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem
id="reject"
className="h-4 w-4 rounded-full border border-gray-300 text-primary focus:ring-2 focus:ring-mineshaft-500"
value={ApprovalStatus.REJECTED}
aria-labelledby="reject-label"
>
<RadioGroupIndicator className="flex h-full w-full items-center justify-center after:h-2 after:w-2 after:rounded-full after:bg-current" />
</RadioGroupItem>
<span
id="reject-label"
className="cursor-pointer"
onClick={() => field.onChange(ApprovalStatus.REJECTED)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
field.onChange(ApprovalStatus.REJECTED);
}
}}
tabIndex={0}
role="button"
>
Reject
</span>
</div>
</RadioGroup>
</FormControl>
)}
/>
<div className="flex justify-end">
<Button
type="submit"
isLoading={isApproving || isRejecting || isSubmitting}
variant="outline_bg"
>
Submit Review
</Button>
</div>
</div>
</div>
</form>
</DropdownMenuContent>
</DropdownMenu>
)}
</form>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<div className="flex flex-col space-y-4">
{secretApprovalRequestDetails.commits.map(
@@ -422,40 +426,45 @@ export const SecretApprovalRequestChanges = ({
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
<div className="text-sm text-bunker-300">Reviewers</div>
<div className="mt-2 flex flex-col space-y-2 text-sm">
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
const reviewer = reviewedUsers?.[requiredApprover.userId];
return (
<div
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
key={`required-approver-${requiredApprover.userId}`}
>
<div className="flex-grow text-sm">
<Tooltip
content={`${requiredApprover.firstName || ""} ${
requiredApprover.lastName || ""
}`}
>
<span>{requiredApprover?.email} </span>
</Tooltip>
<span className="text-red">*</span>
</div>
<div>
{reviewer?.comment && (
<Tooltip content={reviewer.comment}>
<FontAwesomeIcon
icon={faComment}
size="xs"
className="mr-1 text-mineshaft-300"
/>
{secretApprovalRequestDetails?.policy?.approvers
.filter(
(requiredApprover) =>
!(shouldBlockSelfReview && requiredApprover.userId === userSession.id)
)
.map((requiredApprover) => {
const reviewer = reviewedUsers?.[requiredApprover.userId];
return (
<div
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
key={`required-approver-${requiredApprover.userId}`}
>
<div className="flex-grow text-sm">
<Tooltip
content={`${requiredApprover.firstName || ""} ${
requiredApprover.lastName || ""
}`}
>
<span>{requiredApprover?.email} </span>
</Tooltip>
)}
<Tooltip content={reviewer?.status || ApprovalStatus.PENDING}>
{getReviewedStatusSymbol(reviewer?.status)}
</Tooltip>
<span className="text-red">*</span>
</div>
<div>
{reviewer?.comment && (
<Tooltip content={reviewer.comment}>
<FontAwesomeIcon
icon={faComment}
size="xs"
className="mr-1 text-mineshaft-300"
/>
</Tooltip>
)}
<Tooltip content={reviewer?.status || ApprovalStatus.PENDING}>
{getReviewedStatusSymbol(reviewer?.status)}
</Tooltip>
</div>
</div>
</div>
);
})}
);
})}
{secretApprovalRequestDetails?.reviewers
.filter(
(reviewer) =>
@@ -250,7 +250,11 @@ const Page = () => {
limit: 10
});
const { data: snapshotCount, isPending: isSnapshotCountLoading } = useGetWsSnapshotCount({
const {
data: snapshotCount,
isPending: isSnapshotCountLoading,
isFetching: isSnapshotCountFetching
} = useGetWsSnapshotCount({
workspaceId,
environment,
directory: secretPath,
@@ -414,7 +418,7 @@ const Page = () => {
onSearchChange={handleSearchChange}
onToggleTagFilter={handleTagToggle}
snapshotCount={snapshotCount || 0}
isSnapshotCountLoading={isSnapshotCountLoading}
isSnapshotCountLoading={isSnapshotCountLoading && isSnapshotCountFetching}
onToggleRowType={handleToggleRowType}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
protectedBranchPolicyName={boardPolicy?.name}
@@ -35,9 +35,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
{subscription.auditLogs ? (
<LogsSection
refetchInterval={4000}
remappedHeaders={{
Metadata: "Sync Status"
}}
showFilters={false}
presets={{
eventMetadata: { syncId: secretSync.id },
@@ -3,31 +3,75 @@ import { useNavigate } from "@tanstack/react-router";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
export const WindmillAuthorizePage = () => {
const navigate = useNavigate();
const { mutateAsync } = useSaveIntegrationAccessToken();
const { currentWorkspace } = useWorkspace();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [apiUrl, setApiUrl] = useState<string | null>(null);
const [apiUrlErrorText, setApiUrlErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const isLocalOrPrivateIpAddress = (url: string): boolean => {
try {
const validUrl = new URL(url);
// Check for localhost
if (validUrl.hostname === "localhost" || validUrl.hostname === "127.0.0.1") {
return true;
}
// Check for 10.x.x.x
if (validUrl.hostname.match(/^10\.\d+\.\d+\.\d+/)) {
return true;
}
// Check for host.docker.internal
if (validUrl.hostname === "host.docker.internal") {
return true;
}
// Check for 192.168.x.x
if (validUrl.hostname.match(/^192\.168\.\d+\.\d+/)) {
return true;
}
return false;
} catch (err) {
console.error(err);
return true;
}
};
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
setApiUrlErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank");
return;
}
if (apiUrl) {
if (!apiUrl.startsWith("http://") && !apiUrl.startsWith("https://")) {
setApiUrlErrorText("API URL must start with http:// or https://");
return;
}
if (isInfisicalCloud() && isLocalOrPrivateIpAddress(apiUrl)) {
setApiUrlErrorText("Local IPs not allowed as URL");
return;
}
}
setIsLoading(true);
const integrationAuth = await mutateAsync({
workspaceId: currentWorkspace.id,
integration: "windmill",
accessToken: apiKey
accessToken: apiKey,
url: apiUrl ?? undefined
});
setIsLoading(false);
@@ -57,6 +101,18 @@ export const WindmillAuthorizePage = () => {
>
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<FormControl
label="Windmill Instance URL"
errorText={apiUrlErrorText}
isError={apiUrlErrorText !== ""}
tooltipText="If you are using a custom domain, enter it here. Otherwise, leave it blank."
>
<Input
value={apiUrl ?? ""}
onChange={(e) => setApiUrl(e.target.value.trim() === "" ? null : e.target.value.trim())}
placeholder="https://xxxx.windmill.dev"
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
@@ -67,7 +67,8 @@ export const WindmillConfigurePage = () => {
(integrationAuthApp) => integrationAuthApp.name === targetApp
)?.appId,
sourceEnvironment: selectedSourceEnvironment,
secretPath
secretPath,
url: integrationAuth.url ?? undefined
});
setIsLoading(false);