Fix merge conflicts

This commit is contained in:
Tuan Dang
2024-03-06 15:48:32 -08:00
19 changed files with 749 additions and 292 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# Required # Required
DB_CONNECTION_URI=postgres://infisical:infisical@db:5432/infisical DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# JWT # JWT
# Required secrets to sign JWT tokens # Required secrets to sign JWT tokens
+27 -13
View File
@@ -12,7 +12,7 @@ import uuid
REPO_OWNER = "infisical" REPO_OWNER = "infisical"
REPO_NAME = "infisical" REPO_NAME = "infisical"
TOKEN = os.environ["GITHUB_TOKEN"] TOKEN = os.environ["GITHUB_TOKEN"]
# SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"] SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"] OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
SLACK_MSG_COLOR = "#36a64f" SLACK_MSG_COLOR = "#36a64f"
@@ -30,6 +30,23 @@ def set_multiline_output(name, value):
print(value, file=fh) print(value, file=fh)
print(delimiter, file=fh) print(delimiter, file=fh)
def post_changelog_to_slack(changelog, tag):
slack_payload = {
"text": "Hey team, it's changelog time! :wave:",
"attachments": [
{
"color": SLACK_MSG_COLOR,
"title": f"🗓️Infisical Changelog - {tag}",
"text": changelog,
}
],
}
response = requests.post(SLACK_WEBHOOK_URL, json=slack_payload)
if response.status_code != 200:
raise Exception("Failed to post changelog to Slack.")
def find_previous_release_tag(release_tag:str): def find_previous_release_tag(release_tag:str):
previous_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0", f"{release_tag}^"]).decode("utf-8").strip() previous_tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0", f"{release_tag}^"]).decode("utf-8").strip()
while not(previous_tag.startswith("infisical/")): while not(previous_tag.startswith("infisical/")):
@@ -123,20 +140,17 @@ The changelog should:
6. Linear Links: note that the Linear link is optional, include it only if provided. 6. Linear Links: note that the Linear link is optional, include it only if provided.
7. Do not wrap your answer in a codeblock. Just output the text, nothing else 7. Do not wrap your answer in a codeblock. Just output the text, nothing else
Here's a good example to follow, please try to match the formatting as closely as possible, only changing the content of the changelog and have some liberty with the introduction. Notice the importance of the formatting of a changelog item: Here's a good example to follow, please try to match the formatting as closely as possible, only changing the content of the changelog and have some liberty with the introduction. Notice the importance of the formatting of a changelog item:
``` - <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>))
- <https://github.com/facebook/react/pull/27304/|#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/|WEB-1234>))
```
And here's an example of the full changelog: And here's an example of the full changelog:
```
*Features* *Features*
• <https://github.com/facebook/react/pull/27304/|#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/|WEB-1234>) • <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>)
*Fixes & Improvements* *Fixes & Improvements*
• <https://github.com/facebook/react/pull/27304/|#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/|WEB-1234>) • <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>)
*Technical Updates* *Technical Updates*
• <https://github.com/facebook/react/pull/27304/|#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/|WEB-1234>) • <https://github.com/facebook/react/pull/27304/%7C#27304>: We optimize our ci to strip comments and minify production builds. (<https://linear.app/example/issue/WEB-1234/%7CWEB-1234>)
Stay tuned for more exciting updates coming soon! Stay tuned for more exciting updates coming soon!
```
And here are the commits: And here are the commits:
{} {}
""".format( """.format(
@@ -166,11 +180,11 @@ if __name__ == "__main__":
pr_details = extract_commit_details_from_prs(prs) pr_details = extract_commit_details_from_prs(prs)
# Generate changelog # Generate changelog
changelog = f"## Infisical - {latest_tag}\n\n{generate_changelog_with_openai(pr_details)}" changelog = generate_changelog_with_openai(pr_details)
post_changelog_to_slack(changelog,latest_tag)
# Print or post changelog to Slack # Print or post changelog to Slack
set_multiline_output("changelog", changelog) # set_multiline_output("changelog", changelog)
except Exception as e: except Exception as e:
print(str(e)) print(str(e))
@@ -2,14 +2,21 @@ name: Generate Changelog
permissions: permissions:
contents: write contents: write
on: [workflow_dispatch] on:
workflow_dispatch:
push:
tags:
- "infisical/v*.*.*-postgres"
jobs: jobs:
generate_changelog: generate_changelog:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
@@ -24,13 +31,4 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Set git identity SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@infisical.noreply.github.com'
- name: Save the changelog to file
run: |
echo "${{ steps.gen-changelog.outputs.changelog }}" >> CHANGELOG.md
git add CHANGELOG.md
git commit -m "chore: changelog update" --no-verify
git push origin main
+11
View File
@@ -197,3 +197,14 @@ dockers:
- "infisical/cli:{{ .Major }}.{{ .Minor }}" - "infisical/cli:{{ .Major }}.{{ .Minor }}"
- "infisical/cli:{{ .Major }}" - "infisical/cli:{{ .Major }}"
- "infisical/cli:latest" - "infisical/cli:latest"
- dockerfile: docker/alpine
goos: linux
goarch: arm64
ids:
- all-other-builds
image_templates:
- "infisical/cli:{{ .Version }}"
- "infisical/cli:{{ .Major }}.{{ .Minor }}"
- "infisical/cli:{{ .Major }}"
- "infisical/cli:latest"
+6 -1
View File
@@ -9,7 +9,12 @@ export async function seed(knex: Knex): Promise<void> {
await knex(TableName.Users).del(); await knex(TableName.Users).del();
await knex(TableName.UserEncryptionKey).del(); await knex(TableName.UserEncryptionKey).del();
await knex(TableName.SuperAdmin).del(); await knex(TableName.SuperAdmin).del();
await knex(TableName.SuperAdmin).insert([{ initialized: true, allowSignUp: true }]);
await knex(TableName.SuperAdmin).insert([
// eslint-disable-next-line
// @ts-ignore
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
]);
// Inserts seed entries // Inserts seed entries
const [user] = await knex(TableName.Users) const [user] = await knex(TableName.Users)
.insert([ .insert([
+2
View File
@@ -278,6 +278,8 @@ export const registerRoutes = async (
incidentContactDAL, incidentContactDAL,
tokenService, tokenService,
projectDAL, projectDAL,
projectMembershipDAL,
projectKeyDAL,
smtpService, smtpService,
userDAL, userDAL,
orgBotDAL orgBotDAL
@@ -441,16 +441,19 @@ const syncSecretsAWSParameterStore = async ({
}) => { }) => {
if (!accessId) return; if (!accessId) return;
AWS.config.update({ const config = new AWS.Config({
region: integration.region as string, region: integration.region as string,
accessKeyId: accessId, credentials: {
secretAccessKey: accessToken accessKeyId: accessId,
secretAccessKey: accessToken
}
}); });
const ssm = new AWS.SSM({ const ssm = new AWS.SSM({
apiVersion: "2014-11-06", apiVersion: "2014-11-06",
region: integration.region as string region: integration.region as string
}); });
ssm.config.update(config);
const params = { const params = {
Path: integration.path as string, Path: integration.path as string,
@@ -514,12 +517,6 @@ const syncSecretsAWSParameterStore = async ({
} }
}) })
); );
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
}; };
/** /**
@@ -541,12 +538,6 @@ const syncSecretsAWSSecretManager = async ({
try { try {
if (!accessId) return; if (!accessId) return;
AWS.config.update({
region: integration.region as string,
accessKeyId: accessId,
secretAccessKey: accessToken
});
secretsManager = new SecretsManagerClient({ secretsManager = new SecretsManagerClient({
region: integration.region as string, region: integration.region as string,
credentials: { credentials: {
@@ -575,12 +566,6 @@ const syncSecretsAWSSecretManager = async ({
}) })
); );
} }
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
} catch (err) { } catch (err) {
if (err instanceof ResourceNotFoundException && secretsManager) { if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send( await secretsManager.send(
@@ -590,11 +575,6 @@ const syncSecretsAWSSecretManager = async ({
}) })
); );
} }
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined
});
} }
}; };
+49 -3
View File
@@ -22,6 +22,8 @@ import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
@@ -44,6 +46,8 @@ type TOrgServiceFactoryDep = {
orgRoleDAL: TOrgRoleDALFactory; orgRoleDAL: TOrgRoleDALFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
incidentContactDAL: TIncidentContactsDALFactory; incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">; samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
smtpService: TSmtpService; smtpService: TSmtpService;
@@ -65,6 +69,8 @@ export const orgServiceFactory = ({
permissionService, permissionService,
smtpService, smtpService,
projectDAL, projectDAL,
projectMembershipDAL,
projectKeyDAL,
tokenService, tokenService,
orgBotDAL, orgBotDAL,
licenseService, licenseService,
@@ -513,10 +519,50 @@ export const orgServiceFactory = ({
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId); const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
const membership = await orgDAL.deleteMembershipById(membershipId, orgId); const deletedMembership = await orgDAL.transaction(async (tx) => {
const orgMembership = await orgDAL.deleteMembershipById(membershipId, orgId, tx);
await licenseService.updateSubscriptionOrgMemberCount(orgId); if (!orgMembership.userId) {
return membership; await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
}
// Get all the project memberships of the user in the organization
const projectMemberships = await projectMembershipDAL.findProjectMembershipsByUserId(orgId, orgMembership.userId);
// Delete all the project memberships of the user in the organization
await projectMembershipDAL.delete(
{
$in: {
id: projectMemberships.map((membership) => membership.id)
}
},
tx
);
// Get all the project keys of the user in the organization
const projectKeys = await projectKeyDAL.find({
$in: {
projectId: projectMemberships.map((membership) => membership.projectId)
},
receiverId: orgMembership.userId
});
// Delete all the project keys of the user in the organization
await projectKeyDAL.delete(
{
$in: {
id: projectKeys.map((key) => key.id)
}
},
tx
);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return orgMembership;
});
return deletedMembership;
}; };
/* /*
@@ -83,5 +83,25 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
} }
}; };
return { ...projectMemberOrm, findAllProjectMembers, findProjectGhostUser, findMembershipsByUsername }; const findProjectMembershipsByUserId = async (orgId: string, userId: string) => {
try {
const memberships = await db(TableName.ProjectMembership)
.where({ userId })
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.where({ [`${TableName.Project}.orgId` as "orgId"]: orgId })
.select(selectAllTableCols(TableName.ProjectMembership));
return memberships;
} catch (error) {
throw new DatabaseError({ error, name: "Find project memberships by user id" });
}
};
return {
...projectMemberOrm,
findAllProjectMembers,
findProjectGhostUser,
findMembershipsByUsername,
findProjectMembershipsByUserId
};
}; };
+24 -5
View File
@@ -102,8 +102,11 @@ export const projectQueueFactory = ({
const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId); const oldProjectKey = await projectKeyDAL.findLatestProjectKey(data.startedByUserId, data.projectId);
if (!project || !oldProjectKey) { if (!project) {
throw new Error("Project or project key not found"); throw new Error("Project not found");
}
if (!oldProjectKey) {
throw new Error("Old project key not found");
} }
if (project.upgradeStatus !== ProjectUpgradeStatus.Failed && project.upgradeStatus !== null) { if (project.upgradeStatus !== ProjectUpgradeStatus.Failed && project.upgradeStatus !== null) {
@@ -267,8 +270,19 @@ export const projectQueueFactory = ({
const user = await userDAL.findUserEncKeyByUserId(key.receiverId); const user = await userDAL.findUserEncKeyByUserId(key.receiverId);
const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId }); const [orgMembership] = await orgDAL.findMembership({ userId: key.receiverId, orgId: project.orgId });
if (!user || !orgMembership) { if (!user) {
throw new Error(`User with ID ${key.receiverId} was not found during upgrade, or user is not in org.`); throw new Error(`User with ID ${key.receiverId} was not found during upgrade.`);
}
if (!orgMembership) {
// This can happen. Since we don't remove project memberships and project keys when a user is removed from an org, this is a valid case.
logger.info("User is not in organization", {
userId: key.receiverId,
orgId: project.orgId,
projectId: project.id
});
// eslint-disable-next-line no-continue
continue;
} }
const [newMember] = assignWorkspaceKeysToMembers({ const [newMember] = assignWorkspaceKeysToMembers({
@@ -532,7 +546,12 @@ export const projectQueueFactory = ({
logger.error("Failed to upgrade project, because no project was found", data); logger.error("Failed to upgrade project, because no project was found", data);
} else { } else {
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed); await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
logger.error(err, "Failed to upgrade project"); logger.error("Failed to upgrade project", err, {
extra: {
project,
jobData: data
}
});
} }
throw err; throw err;
@@ -375,6 +375,10 @@ export const secretServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
if (inputSecret.newSecretName === "") {
throw new BadRequestError({ message: "New secret name cannot be empty" });
}
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id; const folderId = folder.id;
@@ -30,7 +30,7 @@ export const Checkbox = ({
<div className="flex items-center font-inter text-bunker-300"> <div className="flex items-center font-inter text-bunker-300">
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
className={twMerge( className={twMerge(
"flex items-center justify-center w-4 h-4 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600", "flex items-center flex-shrink-0 justify-center w-4 h-4 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600",
isDisabled && "bg-bunker-400 hover:bg-bunker-400", isDisabled && "bg-bunker-400 hover:bg-bunker-400",
isChecked && "bg-primary hover:bg-primary", isChecked && "bg-primary hover:bg-primary",
Boolean(children) && "mr-3", Boolean(children) && "mr-3",
@@ -46,7 +46,7 @@ export const Checkbox = ({
<FontAwesomeIcon icon={faCheck} size="sm" /> <FontAwesomeIcon icon={faCheck} size="sm" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
<label className="text-sm whitespace-nowrap" htmlFor={id}> <label className="text-sm whitespace-nowrap truncate" htmlFor={id}>
{children} {children}
{isRequired && <span className="pl-1 text-red">*</span>} {isRequired && <span className="pl-1 text-red">*</span>}
</label> </label>
@@ -81,7 +81,7 @@ export const CreateSecretImportForm = ({
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
const axiosError = err as AxiosError const axiosError = err as AxiosError;
if (axiosError?.response?.status === 401) { if (axiosError?.response?.status === 401) {
createNotification({ createNotification({
text: "You do not have access to the selected environment/path", text: "You do not have access to the selected environment/path",
@@ -1,24 +1,4 @@
/* eslint-disable simple-import-sort/imports */ /* eslint-disable simple-import-sort/imports */
import { memo, useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faCheck,
faClock,
faClose,
faCodeBranch,
faComment,
faCopy,
faEllipsis,
faKey,
faTag,
faTags
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
@@ -48,9 +28,29 @@ import {
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { DecryptedSecret } from "@app/hooks/api/secrets/types"; import { DecryptedSecret } from "@app/hooks/api/secrets/types";
import { WsTag } from "@app/hooks/api/types"; import { WsTag } from "@app/hooks/api/types";
import { subject } from "@casl/ability";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faCheck,
faClock,
faClose,
faCodeBranch,
faComment,
faCopy,
faEllipsis,
faKey,
faTag,
faTags
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { memo, useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
import { CreateReminderForm } from "./CreateReminderForm"; import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
type Props = { type Props = {
secret: DecryptedSecret; secret: DecryptedSecret;
@@ -104,7 +104,8 @@ export const SecretItem = memo(
setValue, setValue,
reset, reset,
getValues, getValues,
formState: { isDirty, isSubmitting } trigger,
formState: { isDirty, isSubmitting, errors }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
defaultValues: secret, defaultValues: secret,
values: secret, values: secret,
@@ -235,15 +236,18 @@ export const SecretItem = memo(
<Controller <Controller
name="key" name="key"
control={control} control={control}
render={({ field }) => ( render={({ field, fieldState: { error } }) => (
<Input <Input
autoComplete="off" autoComplete="off"
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
autoCapitalization={currentWorkspace?.autoCapitalization} autoCapitalization={currentWorkspace?.autoCapitalization}
variant="plain" variant="plain"
isDisabled={isOverriden} isDisabled={isOverriden}
placeholder={error?.message}
isError={Boolean(error)}
onKeyUp={() => trigger("key")}
{...field} {...field}
className="w-full px-0 focus:text-bunker-100 focus:ring-transparent" className="w-full px-0 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
/> />
)} )}
/> />
@@ -497,7 +501,7 @@ export const SecretItem = memo(
animate={{ x: 0, opacity: 1 }} animate={{ x: 0, opacity: 1 }}
exit={{ x: -10, opacity: 0 }} exit={{ x: -10, opacity: 0 }}
> >
<Tooltip content="Save"> <Tooltip content={errors.key ? errors.key?.message : "Save"}>
<IconButton <IconButton
ariaLabel="more" ariaLabel="more"
variant="plain" variant="plain"
@@ -507,12 +511,16 @@ export const SecretItem = memo(
"p-0 text-primary opacity-0 group-hover:opacity-100", "p-0 text-primary opacity-0 group-hover:opacity-100",
isDirty && "opacity-100" isDirty && "opacity-100"
)} )}
isDisabled={isSubmitting} isDisabled={isSubmitting || Boolean(errors.key)}
> >
{isSubmitting ? ( {isSubmitting ? (
<Spinner className="m-0 h-4 w-4 p-0" /> <Spinner className="m-0 h-4 w-4 p-0" />
) : ( ) : (
<FontAwesomeIcon icon={faCheck} size="lg" className="text-primary" /> <FontAwesomeIcon
icon={faCheck}
size="lg"
className={twMerge("text-primary", errors.key && "text-mineshaft-300")}
/>
)} )}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -206,7 +206,7 @@ export const SecretListView = ({
reminderRepeatDays, reminderRepeatDays,
reminderNote reminderNote
} = modSecret; } = modSecret;
const hasKeyChanged = oldKey !== key; const hasKeyChanged = oldKey !== key && key;
const tagIds = tags?.map(({ id }) => id); const tagIds = tags?.map(({ id }) => id);
const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id); const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id);
@@ -8,7 +8,7 @@ export enum SecretActionType {
} }
export const formSchema = z.object({ export const formSchema = z.object({
key: z.string().trim(), key: z.string().trim().min(1, { message: "Secret key is required" }),
value: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())), value: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
idOverride: z.string().trim().optional(), idOverride: z.string().trim().optional(),
valueOverride: z valueOverride: z
@@ -2,22 +2,31 @@ import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { subject } from "@casl/ability";
import { import {
faAngleDown,
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faFolderBlank, faFolderBlank,
faMagnifyingGlass faFolderPlus,
faMagnifyingGlass,
faPlus
} 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 { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader"; import NavHeader from "@app/components/navigation/NavHeader";
import { PermissionDeniedBanner } from "@app/components/permissions"; import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
EmptyState, EmptyState,
IconButton, IconButton,
Input, Input,
Modal,
ModalContent,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -30,7 +39,13 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert"; import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
import { useOrganization, useWorkspace } from "@app/context"; import {
ProjectPermissionActions,
ProjectPermissionSub,
useOrganization,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { import {
useCreateFolder, useCreateFolder,
useCreateSecretV3, useCreateSecretV3,
@@ -42,6 +57,8 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs"; import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection"; import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow"; import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
@@ -110,6 +127,40 @@ export const SecretOverviewPage = () => {
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3(); const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: createFolder } = useCreateFolder();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"addSecretsInAllEnvs",
"addFolder",
"misc"
] as const);
const handleFolderCreate = async (folderName: string) => {
const promises = userAvailableEnvs.map((env) => {
const environment = env.slug;
return createFolder({
name: folderName,
path: secretPath,
environment,
projectId: workspaceId
});
});
const results = await Promise.allSettled(promises);
const isFoldersAdded = results.some((result) => result.status === "fulfilled");
if (isFoldersAdded) {
handlePopUpClose("addFolder");
createNotification({
type: "success",
text: "Successfully created folder"
});
} else {
createNotification({
type: "error",
text: "Failed to create folder"
});
}
};
const handleSecretCreate = async (env: string, key: string, value: string) => { const handleSecretCreate = async (env: string, key: string, value: string) => {
try { try {
// create folder if not existing // create folder if not existing
@@ -269,210 +320,286 @@ export const SecretOverviewPage = () => {
filteredFolderNames?.length === 0; filteredFolderNames?.length === 0;
return ( return (
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]"> <>
<ProjectIndexSecretsSection decryptFileKey={latestFileKey!} /> <div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<div className="relative right-5 ml-4"> <ProjectIndexSecretsSection decryptFileKey={latestFileKey!} />
<NavHeader pageName={t("dashboard.title")} isProjectRelated /> <div className="relative right-5 ml-4">
</div> <NavHeader pageName={t("dashboard.title")} isProjectRelated />
<div className="space-y-8">
<div className="mt-6">
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
<p className="text-md text-bunker-300">
Inject your secrets using
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical CLI
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/api"
target="_blank"
rel="noopener noreferrer"
>
Infisical API
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
, and
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
>
more
</a>
.
</p>
</div> </div>
<div className="space-y-8">
<div className="mt-6">
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
<p className="text-md text-bunker-300">
Inject your secrets using
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical CLI
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/api"
target="_blank"
rel="noopener noreferrer"
>
Infisical API
</a>
,
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/sdks/overview"
target="_blank"
rel="noopener noreferrer"
>
Infisical SDKs
</a>
, and
<a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank"
rel="noopener noreferrer"
>
more
</a>
.
</p>
</div>
{currentWorkspace?.version === ProjectVersion.V1 && ( {currentWorkspace?.version === ProjectVersion.V1 && (
<UpgradeProjectAlert project={currentWorkspace} /> <UpgradeProjectAlert project={currentWorkspace} />
)} )}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} /> <FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
<div className="w-80"> <div className="flex flex-row items-center justify-center space-x-2">
<Input <div className="w-80">
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80" <Input
placeholder="Search by secret/folder name..." className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
value={searchFilter} placeholder="Search by secret/folder name..."
onChange={(e) => setSearchFilter(e.target.value)} value={searchFilter}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} onChange={(e) => setSearchFilter(e.target.value)}
/> leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/>
</div>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
>
{(isAllowed) => (
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
className="h-10 rounded-r-none"
isDisabled={!isAllowed}
>
Add Secret
</Button>
)}
</ProjectPermissionCan>
<DropdownMenu
open={popUp.misc.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add-folder-or-import"
variant="outline_bg"
className="rounded-l-none bg-mineshaft-600 p-3"
>
<FontAwesomeIcon icon={faAngleDown} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
onClick={() => {
handlePopUpOpen("addFolder");
handlePopUpClose("misc");
}}
isDisabled={!isAllowed}
variant="outline_bg"
className="h-10"
isFullWidth
>
Add Folder
</Button>
)}
</ProjectPermissionCan>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div> </div>
</div> </div>
</div> <div className="thin-scrollbar mt-4" ref={parentTableRef}>
<div className="thin-scrollbar mt-4" ref={parentTableRef}> <TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto"> <Table>
<Table> <THead>
<THead> <Tr className="sticky top-0 z-20 border-0">
<Tr className="sticky top-0 z-20 border-0"> <Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0"> <div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3">
<div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-3.5 pb-3"> Name
Name <IconButton
<IconButton variant="plain"
variant="plain" className="ml-2"
className="ml-2" ariaLabel="sort"
ariaLabel="sort" onClick={() => setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))}
onClick={() => setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))} >
> <FontAwesomeIcon icon={sortDir === "asc" ? faArrowDown : faArrowUp} />
<FontAwesomeIcon icon={sortDir === "asc" ? faArrowDown : faArrowUp} /> </IconButton>
</IconButton> </div>
</div> </Th>
</Th> {userAvailableEnvs?.map(({ name, slug }, index) => {
{userAvailableEnvs?.map(({ name, slug }, index) => { const envSecKeyCount = getEnvSecretKeyCount(slug);
const envSecKeyCount = getEnvSecretKeyCount(slug); const missingKeyCount = secKeys.length - envSecKeyCount;
const missingKeyCount = secKeys.length - envSecKeyCount; return (
return ( <Th
<Th className="min-table-row min-w-[11rem] border-b-0 p-0 text-center"
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center" key={`secret-overview-${name}-${index + 1}`}
key={`secret-overview-${name}-${index + 1}`} >
> <div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pt-3.5 pb-[0.83rem]">
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pt-3.5 pb-[0.83rem]"> <button
<button type="button"
type="button" className="text-sm font-medium duration-100 hover:text-mineshaft-100"
className="text-sm font-medium duration-100 hover:text-mineshaft-100" onClick={() => handleExploreEnvClick(slug)}
>
{name}
</button>
{missingKeyCount > 0 && (
<Tooltip
className="max-w-none lowercase"
content={`${missingKeyCount} secrets missing\n compared to other environments`}
>
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
<span className="text-bunker-100">{missingKeyCount}</span>
</div>
</Tooltip>
)}
</div>
</Th>
);
})}
</Tr>
</THead>
<TBody>
{canViewOverviewPage && isTableLoading && (
<TableSkeleton
columns={userAvailableEnvs.length + 1}
innerKey="secret-overview-loading"
rows={5}
className="bg-mineshaft-700"
/>
)}
{isTableEmpty && !isTableLoading && (
<Tr>
<Td colSpan={userAvailableEnvs.length + 1}>
<EmptyState title="Let's add some secrets" icon={faFolderBlank} iconSize="3x">
<Link
href={{
pathname: "/project/[id]/secrets/[env]",
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
}}
>
<Button
className="mt-4"
variant="outline_bg"
colorSchema="primary"
size="md"
>
Go to {userAvailableEnvs?.[0]?.name}
</Button>
</Link>
</EmptyState>
</Td>
</Tr>
)}
{!isTableLoading &&
filteredFolderNames.map((folderName, index) => (
<SecretOverviewFolderRow
folderName={folderName}
isFolderPresentInEnv={isFolderPresentInEnv}
environments={userAvailableEnvs}
key={`overview-${folderName}-${index + 1}`}
onClick={handleFolderClick}
/>
))}
{!isTableLoading &&
(userAvailableEnvs?.length > 0 ? (
filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
secretPath={secretPath}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
key={`overview-${key}-${index + 1}`}
environments={userAvailableEnvs}
secretKey={key}
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
/>
))
) : (
<PermissionDeniedBanner />
))}
</TBody>
<TFoot>
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
<Td className="sticky left-0 z-10 border-0 bg-mineshaft-800 p-0">
<div
className="w-full border-t border-r border-mineshaft-600"
style={{ height: "45px" }}
/>
</Td>
{userAvailableEnvs.map(({ name, slug }) => (
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0">
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2">
<Button
size="xs"
variant="outline_bg"
isFullWidth
onClick={() => handleExploreEnvClick(slug)} onClick={() => handleExploreEnvClick(slug)}
> >
{name} Explore
</button>
{missingKeyCount > 0 && (
<Tooltip
className="max-w-none lowercase"
content={`${missingKeyCount} secrets missing\n compared to other environments`}
>
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
<span className="text-bunker-100">{missingKeyCount}</span>
</div>
</Tooltip>
)}
</div>
</Th>
);
})}
</Tr>
</THead>
<TBody>
{canViewOverviewPage && isTableLoading && (
<TableSkeleton
columns={userAvailableEnvs.length + 1}
innerKey="secret-overview-loading"
rows={5}
className="bg-mineshaft-700"
/>
)}
{isTableEmpty && !isTableLoading && (
<Tr>
<Td colSpan={userAvailableEnvs.length + 1}>
<EmptyState title="Let's add some secrets" icon={faFolderBlank} iconSize="3x">
<Link
href={{
pathname: "/project/[id]/secrets/[env]",
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
}}
>
<Button
className="mt-4"
variant="outline_bg"
colorSchema="primary"
size="md"
>
Go to {userAvailableEnvs?.[0]?.name}
</Button> </Button>
</Link> </div>
</EmptyState> </Td>
</Td> ))}
</Tr> </Tr>
)} </TFoot>
{!isTableLoading && </Table>
filteredFolderNames.map((folderName, index) => ( </TableContainer>
<SecretOverviewFolderRow </div>
folderName={folderName}
isFolderPresentInEnv={isFolderPresentInEnv}
environments={userAvailableEnvs}
key={`overview-${folderName}-${index + 1}`}
onClick={handleFolderClick}
/>
))}
{!isTableLoading &&
(userAvailableEnvs?.length > 0 ? (
filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
secretPath={secretPath}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
key={`overview-${key}-${index + 1}`}
environments={userAvailableEnvs}
secretKey={key}
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
/>
))
) : (
<PermissionDeniedBanner />
))}
</TBody>
<TFoot>
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
<Td className="sticky left-0 z-10 border-0 bg-mineshaft-800 p-0">
<div
className="w-full border-t border-r border-mineshaft-600"
style={{ height: "45px" }}
/>
</Td>
{userAvailableEnvs.map(({ name, slug }) => (
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0">
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2">
<Button
size="xs"
variant="outline_bg"
isFullWidth
onClick={() => handleExploreEnvClick(slug)}
>
Explore
</Button>
</div>
</Td>
))}
</Tr>
</TFoot>
</Table>
</TableContainer>
</div> </div>
</div> <CreateSecretForm
secretPath={secretPath}
isOpen={popUp.addSecretsInAllEnvs.isOpen}
getSecretByKey={getSecretByKey}
onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretsInAllEnvs", isOpen)}
onClose={() => handlePopUpClose("addSecretsInAllEnvs")}
decryptFileKey={latestFileKey!}
/>
<Modal
isOpen={popUp.addFolder.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addFolder", isOpen)}
>
<ModalContent title="Create Folder">
<FolderForm onCreateFolder={handleFolderCreate} />
</ModalContent>
</Modal>
</>
); );
}; };
@@ -0,0 +1,222 @@
import { Controller, useForm } from "react-hook-form";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
Checkbox,
FormControl,
FormLabel,
Input,
Modal,
ModalContent,
SecretInput,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { DecryptedSecret, UserWsKeyPair } from "@app/hooks/api/types";
const typeSchema = z
.object({
key: z.string().min(1, "Key is required"),
value: z.string().optional(),
environments: z.record(z.boolean().optional())
})
.refine((data) => data.key !== undefined, {
message: "Please enter secret name"
});
type TFormSchema = z.infer<typeof typeSchema>;
type Props = {
secretPath?: string;
decryptFileKey: UserWsKeyPair;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
// modal props
isOpen?: boolean;
onClose: () => void;
onTogglePopUp: (isOpen: boolean) => void;
};
export const CreateSecretForm = ({
secretPath = "/",
decryptFileKey,
isOpen,
getSecretByKey,
onClose,
onTogglePopUp
}: Props) => {
const {
register,
handleSubmit,
control,
reset,
watch,
formState: { isSubmitting, errors }
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
const newSecretKey = watch("key");
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const environments = currentWorkspace?.environments || [];
const { createNotification } = useNotificationContext();
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: createFolder } = useCreateFolder();
const handleFormSubmit = async ({ key, value, environments: selectedEnv }: TFormSchema) => {
const environmentsSelected = environments.filter(({ slug }) => selectedEnv[slug]);
const isEnvironmentsSelected = environmentsSelected.length;
if (!isEnvironmentsSelected) {
createNotification({ type: "error", text: "Select at least one environment" });
return;
}
const promises = environmentsSelected.map(async (env) => {
const environment = env.slug;
// create folder if not existing
if (secretPath !== "/") {
// /hello/world -> [hello","world"]
const pathSegment = secretPath.split("/").filter(Boolean);
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
const folderName = pathSegment.at(-1);
if (folderName && parentPath) {
await createFolder({
projectId: workspaceId,
path: parentPath,
environment,
name: folderName
});
}
}
const isEdit = getSecretByKey(environment, key) !== undefined;
if (isEdit) {
return updateSecretV3({
environment,
workspaceId,
secretPath,
secretName: key,
secretValue: value || "",
type: "shared",
latestFileKey: decryptFileKey
});
}
return createSecretV3({
environment,
workspaceId,
secretPath,
secretName: key,
secretValue: value || "",
secretComment: "",
type: "shared",
latestFileKey: decryptFileKey
});
});
const results = await Promise.allSettled(promises);
const isSecretsAdded = results.some((result) => result.status === "fulfilled");
if (isSecretsAdded) {
createNotification({
type: "success",
text: "Secrets created successfully"
});
onClose();
reset();
} else {
createNotification({
type: "error",
text: "Failed to create secrets"
});
}
};
return (
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
<ModalContent
className="max-h-[80vh] overflow-y-auto"
title="Bulk Create & Update"
subTitle="Create & update a secret across many environments"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
<Input
{...register("key")}
placeholder="Type your secret name"
autoCapitalization={currentWorkspace?.autoCapitalization}
/>
</FormControl>
<Controller
control={control}
name="value"
render={({ field }) => (
<FormControl
label="Value"
isError={Boolean(errors?.value)}
errorText={errors?.value?.message}
>
<SecretInput
{...field}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/>
</FormControl>
)}
/>
<FormLabel label="Environments" className="mb-2" />
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto ">
{environments.map((env) => {
return (
<Controller
name={`environments.${env.slug}`}
key={`secret-input-${env.slug}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`secret-input-${env.slug}`}
>
{env.name}
{getSecretByKey(env.slug, newSecretKey) && (
<Tooltip content="Secret exists. Will be overwritten">
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
</Tooltip>
)}
</Checkbox>
)}
/>
);
})}
</div>
<div className="mt-7 flex items-center">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className="mr-4"
type="submit"
>
Create Secret
</Button>
<Button
key="layout-cancel-create-project"
onClick={onClose}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export { CreateSecretForm } from "./CreateSecretForm"