mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 22:27:48 +00:00
Fix merge conflicts
This commit is contained in:
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+222
@@ -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"
|
||||||
Reference in New Issue
Block a user