Merge remote-tracking branch 'refs/remotes/origin/main' into Aashish-Upadhyay-101/TravisCI-integration

This commit is contained in:
Aashish-Upadhyay-101
2023-02-24 13:36:35 +05:45
55 changed files with 2130 additions and 531 deletions

22
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,22 @@
# Description 📣
*Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.*
## Type ✨
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation
# Tests 🛠️
*Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible*
```sh
# Here's some code block to paste some code snippets
```
---
- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/code-of-conduct). 📝

File diff suppressed because one or more lines are too long

View File

@@ -19,6 +19,7 @@
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1311.0",
"axios": "^1.1.3",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
@@ -3473,6 +3474,17 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -5317,6 +5329,15 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios-retry": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.4.0.tgz",
"integrity": "sha512-VdgaP+gHH4iQYCCNUWF2pcqeciVOdGrBBAYUfTY+wPcO5Ltvp/37MLFNCmJKo7Gj3SHvCSdL8ouI1qLYJN3liA==",
"dependencies": {
"@babel/runtime": "^7.15.4",
"is-retry-allowed": "^2.2.0"
}
},
"node_modules/babel-jest": {
"version": "29.3.1",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.3.1.tgz",
@@ -7604,6 +7625,17 @@
"node": ">=0.10.0"
}
},
"node_modules/is-retry-allowed": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz",
"integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -12179,6 +12211,11 @@
"node": ">=8.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@@ -16584,6 +16621,14 @@
"@babel/helper-plugin-utils": "^7.19.0"
}
},
"@babel/runtime": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"requires": {
"regenerator-runtime": "^0.13.11"
}
},
"@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -18075,6 +18120,15 @@
"proxy-from-env": "^1.1.0"
}
},
"axios-retry": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.4.0.tgz",
"integrity": "sha512-VdgaP+gHH4iQYCCNUWF2pcqeciVOdGrBBAYUfTY+wPcO5Ltvp/37MLFNCmJKo7Gj3SHvCSdL8ouI1qLYJN3liA==",
"requires": {
"@babel/runtime": "^7.15.4",
"is-retry-allowed": "^2.2.0"
}
},
"babel-jest": {
"version": "29.3.1",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.3.1.tgz",
@@ -19771,6 +19825,11 @@
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"is-retry-allowed": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz",
"integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg=="
},
"is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -23088,6 +23147,11 @@
"picomatch": "^2.2.1"
}
},
"regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",

View File

@@ -10,6 +10,7 @@
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1311.0",
"axios": "^1.1.3",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",

View File

@@ -0,0 +1,16 @@
import axios from 'axios';
import axiosRetry from 'axios-retry';
const axiosInstance = axios.create();
// add retry functionality to the axios instance
axiosRetry(axiosInstance, {
retries: 3,
retryDelay: (retryCount) => retryCount * 1000, // delay between retries (in milliseconds)
retryCondition: (error) => {
// only retry if the error is a network error or a 5xx server error
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
},
});
export default axiosInstance;

View File

@@ -32,6 +32,7 @@ import {
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
} from "../variables";
import axiosWithRetry from '../config/request';
/**
* Sync/push [secrets] to [app] in integration named [integration]
@@ -555,7 +556,7 @@ const syncSecretsVercel = async ({
value: string;
target: string[];
}
try {
// Get all (decrypted) secrets back from Vercel in
// decrypted format
@@ -568,40 +569,84 @@ const syncSecretsVercel = async ({
: {}),
};
const res = (
await Promise.all(
(
await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
// const res = (
// await Promise.all(
// (
// await axios.get(
// `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
// {
// params,
// headers: {
// Authorization: `Bearer ${accessToken}`,
// 'Accept-Encoding': 'application/json'
// }
// }
// ))
// .data
// .envs
// .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
// .map(async (secret: VercelSecret) => {
// if (secret.type === 'encrypted') {
// // case: secret is encrypted -> need to decrypt
// const decryptedSecret = (await axios.get(
// `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
// {
// params,
// headers: {
// Authorization: `Bearer ${accessToken}`,
// 'Accept-Encoding': 'application/json'
// }
// }
// )).data;
// return decryptedSecret;
// }
// return secret;
// }))).reduce((obj: any, secret: any) => ({
// ...obj,
// [secret.key]: secret
// }), {});
const vercelSecrets: VercelSecret[] = (await axiosWithRetry.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
))
.data
.envs
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment));
const res: { [key: string]: VercelSecret } = {};
for await (const vercelSecret of vercelSecrets) {
if (vercelSecret.type === 'encrypted') {
// case: secret is encrypted -> need to decrypt
const decryptedSecret = (await axiosWithRetry.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
))
.data
.envs
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
.map(async (secret: VercelSecret) => (await axios.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
)).data)
)).reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret
}), {});
}
)).data;
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
res[vercelSecret.key] = decryptedSecret;
} else {
res[vercelSecret.key] = vercelSecret;
}
}
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
// Identify secrets to create
Object.keys(secrets).map((key) => {
@@ -625,8 +670,10 @@ const syncSecretsVercel = async ({
id: res[key].id,
key: key,
value: secrets[key],
type: "encrypted",
target: [integration.targetEnvironment],
type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment]
});
}
} else {
@@ -635,7 +682,7 @@ const syncSecretsVercel = async ({
id: res[key].id,
key: key,
value: res[key].value,
type: "encrypted",
type: "encrypted", // value doesn't matter
target: [integration.targetEnvironment],
});
}
@@ -643,7 +690,7 @@ const syncSecretsVercel = async ({
// Sync/push new secrets
if (newSecrets.length > 0) {
await axios.post(
await axiosWithRetry.post(
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
newSecrets,
{
@@ -655,12 +702,11 @@ const syncSecretsVercel = async ({
}
);
}
// Sync/push updated secrets
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: VercelSecret) => {
for await (const secret of updateSecrets) {
if (secret.type !== 'sensitive') {
const { id, ...updatedSecret } = secret;
await axios.patch(
await axiosWithRetry.patch(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
updatedSecret,
{
@@ -671,23 +717,20 @@ const syncSecretsVercel = async ({
},
}
);
});
}
}
// Delete secrets
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (secret: VercelSecret) => {
await axios.delete(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
});
for await (const secret of deleteSecrets) {
await axiosWithRetry.delete(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
}
);
}
} catch (err) {
Sentry.setUser(null);

View File

@@ -118,7 +118,6 @@ router.delete( // TODO - rewire dashboard to this route
workspaceController.deleteWorkspaceMembership
);
router.patch(
'/:workspaceId/auto-capitalization',
requireAuth({

View File

@@ -1,5 +1,3 @@
import { Bot, IBot } from '../models';
import * as Sentry from '@sentry/node';
import { handleEventHelper } from '../helpers/event';
interface Event {

View File

@@ -1,6 +1,10 @@
import nodemailer from 'nodemailer';
import { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE } from '../config';
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from '../variables';
import {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS
} from '../variables';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import * as Sentry from '@sentry/node';
@@ -27,6 +31,12 @@ if (SMTP_SECURE) {
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_SOCKETLABS:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
default:
if (SMTP_HOST.includes('amazonaws.com')) {
mailOpts.tls = {

View File

@@ -44,7 +44,11 @@ import {
ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS
} from './action';
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp';
import {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS
} from './smtp';
import { PLAN_STARTER, PLAN_PRO } from './stripe';
import {
MFA_METHOD_EMAIL
@@ -105,6 +109,7 @@ export {
INTEGRATION_OPTIONS,
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
PLAN_STARTER,
PLAN_PRO,
MFA_METHOD_EMAIL,

View File

@@ -1,7 +1,9 @@
const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
export {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS
}

View File

@@ -36,9 +36,12 @@ var exportCmd = &cobra.Command{
// util.RequireLocalWorkspaceFile()
},
Run: func(cmd *cobra.Command, args []string) {
envName, err := cmd.Flags().GetString("env")
if err != nil {
util.HandleError(err)
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
@@ -66,7 +69,7 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil {
util.HandleError(err, "Unable to fetch secrets")
}

View File

@@ -87,7 +87,7 @@ var loginCmd = &cobra.Command{
break
} else if mfaErrorResponse != nil {
if mfaErrorResponse.Context.Code == "mfa_invalid" {
msg := fmt.Sprintf("Incorrect, MFA code. You have %v attempts left", 5-i)
msg := fmt.Sprintf("Incorrect, verification code. You have %v attempts left", 5-i)
fmt.Println(msg)
if i == 5 {
util.PrintErrorMessageAndExit("No tries left, please try again in a bit")
@@ -96,7 +96,7 @@ var loginCmd = &cobra.Command{
}
if mfaErrorResponse.Context.Code == "mfa_expired" {
util.PrintErrorMessageAndExit("Your MFA code has expired, please try logging in again")
util.PrintErrorMessageAndExit("Your 2FA verification code has expired, please try logging in again")
break
}
i++
@@ -120,6 +120,7 @@ var loginCmd = &cobra.Command{
var decryptedPrivateKey []byte
if loginTwoResponse.EncryptionVersion == 1 {
log.Debug("Login version 1")
encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey)
tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag)
if err != nil {
@@ -134,11 +135,15 @@ var loginCmd = &cobra.Command{
paddedPassword := fmt.Sprintf("%032s", password)
key := []byte(paddedPassword)
decryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV)
if err != nil || len(decryptedPrivateKey) == 0 {
computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV)
if err != nil || len(computedDecryptedPrivateKey) == 0 {
util.HandleError(err)
}
decryptedPrivateKey = computedDecryptedPrivateKey
} else if loginTwoResponse.EncryptionVersion == 2 {
log.Debug("Login version 2")
protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey)
if err != nil {
util.HandleError(err)
@@ -191,14 +196,21 @@ var loginCmd = &cobra.Command{
util.HandleError(err)
}
decryptedPrivateKey, err = crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv)
computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv)
if err != nil {
util.HandleError(err)
}
decryptedPrivateKey = computedDecryptedPrivateKey
} else {
util.PrintErrorMessageAndExit("Insufficient details to decrypt private key")
}
if string(decryptedPrivateKey) == "" || email == "" || loginTwoResponse.Token == "" {
log.Debugf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token)
util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info")
}
userCredentialsToBeStored := &models.UserCredentials{
Email: email,
PrivateKey: string(decryptedPrivateKey),
@@ -340,7 +352,7 @@ func generateFromPassword(password string, salt []byte, p *params) (hash []byte,
func askForMFACode() string {
mfaCodePromptUI := promptui.Prompt{
Label: "MFA verification code",
Label: "Enter the 2FA verification code sent to your email",
}
mfaVerifyCode, err := mfaCodePromptUI.Run()

View File

@@ -54,9 +54,12 @@ var runCmd = &cobra.Command{
return nil
},
Run: func(cmd *cobra.Command, args []string) {
envName, err := cmd.Flags().GetString("env")
if err != nil {
util.HandleError(err, "Unable to parse flag")
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
infisicalToken, err := cmd.Flags().GetString("token")
@@ -79,7 +82,7 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: envName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs})
if err != nil {
util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")

View File

@@ -19,7 +19,6 @@ import (
"github.com/Infisical/infisical-merge/packages/util"
"github.com/Infisical/infisical-merge/packages/visualize"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@@ -31,9 +30,12 @@ var secretsCmd = &cobra.Command{
PreRun: toggleDebug,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
util.HandleError(err)
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
infisicalToken, err := cmd.Flags().GetString("token")
@@ -94,9 +96,12 @@ var secretsSetCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
util.RequireLocalWorkspaceFile()
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
util.HandleError(err, "Unable to parse flag")
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
workspaceFile, err := util.GetWorkSpaceFromFile()
@@ -270,11 +275,12 @@ var secretsDeleteCmd = &cobra.Command{
PreRun: toggleDebug,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
log.Errorln("Unable to parse the environment name flag")
log.Debugln(err)
return
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
@@ -330,9 +336,12 @@ var secretsDeleteCmd = &cobra.Command{
}
func getSecretsByNames(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
util.HandleError(err, "Unable to parse flag")
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
infisicalToken, err := cmd.Flags().GetString("token")
@@ -373,9 +382,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
}
func generateExampleEnv(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env")
if err != nil {
util.HandleError(err, "Unable to parse flag")
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
}
infisicalToken, err := cmd.Flags().GetString("token")

View File

@@ -39,8 +39,9 @@ type Workspace struct {
}
type WorkspaceConfigFile struct {
WorkspaceId string `json:"workspaceId"`
DefaultEnvironment string `json:"defaultEnvironment"`
WorkspaceId string `json:"workspaceId"`
DefaultEnvironment string `json:"defaultEnvironment"`
GitBranchToEnvironmentMapping map[string]string `json:"gitBranchToEnvironmentMapping"`
}
type SymmetricEncryptionResult struct {
@@ -50,7 +51,8 @@ type SymmetricEncryptionResult struct {
}
type GetAllSecretsParameters struct {
Environment string
InfisicalToken string
TagSlugs string
Environment string
EnvironmentPassedViaFlag bool
InfisicalToken string
TagSlugs string
}

View File

@@ -1,10 +1,14 @@
package util
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
"os/exec"
"path"
"strings"
)
type DecodedSymmetricEncryptionDetails = struct {
@@ -110,3 +114,14 @@ func GetHashFromStringList(list []string) string {
sum := sha256.Sum256(hash.Sum(nil))
return fmt.Sprintf("%x", sum)
}
func getCurrentBranch() (string, error) {
cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", err
}
return path.Base(strings.TrimSpace(out.String())), nil
}

View File

@@ -131,10 +131,6 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, err
}
if workspaceFile.DefaultEnvironment != "" {
params.Environment = workspaceFile.DefaultEnvironment
}
// Verify environment
err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
if err != nil {
@@ -485,3 +481,27 @@ func DeleteBackupSecrets() error {
return os.RemoveAll(fullPathToSecretsBackupFolder)
}
func GetEnvelopmentBasedOnGitBranch() string {
branch, err := getCurrentBranch()
if err != nil {
log.Debugf("getEnvelopmentBasedOnGitBranch: [err=%s]", err)
}
workspaceFile, err := GetWorkSpaceFromFile()
if err != nil {
log.Debugf("getEnvelopmentBasedOnGitBranch: [err=%s]", err)
return ""
}
envBasedOnGitBranch, ok := workspaceFile.GitBranchToEnvironmentMapping[branch]
log.Debugf("GetEnvelopmentBasedOnGitBranch: [envBasedOnGitBranch=%s] [ok=%s]", envBasedOnGitBranch, ok)
if err == nil && ok {
return envBasedOnGitBranch
} else {
log.Debugf("getEnvelopmentBasedOnGitBranch: [err=%s]", err)
return ""
}
}

View File

@@ -18,4 +18,8 @@ If you are still experiencing trouble, please seek support.
<Accordion title="Can I fetch secrets with Infisical if I am offline?">
Yes. If you have previously retrieved secrets for a specific project and environment (such as dev, staging, or prod), the `run`/`secret` command will utilize the saved secrets, even when offline, on subsequent fetch attempts.
</Accordion>
<Accordion title="Can I upload the .infisical.json file that was generated?">
Yes. This is simply a configuration file and contains no sensitive data.
</Accordion>

View File

@@ -20,7 +20,7 @@ The Infisical CLI provides a way to inject environment variables from the platfo
### Updates
```bash
brew upgrade infisical
brew update && brew upgrade infisical
```
</Tab>

View File

@@ -0,0 +1,26 @@
---
title: "Project config file"
description: "Project config file & customization options"
---
To link your local project on your machine with an Infisical project, we suggest using the infisical init CLI command. This will generate a `.infisical.json` file in the root directory of your project.
The `.infisical.json` file specifies various parameters, such as the Infisical project to retrieve secrets from, along with other configuration options. Furthermore, you can define additional properties in the file to further tailor your local development experience.
## Set Infisical environment based on GitHub branch
When fetching your secrets from Infisical, you can switch between environments by using the `--env` flag. However, in certain cases, you may prefer the environment to be automatically mapped based on the current GitHub branch you are working on.
To achieve this, simply add the `gitBranchToEnvironmentMapping` property to your configuration file, as shown below.
```json .infisical.json
{
"workspaceId": "63ee5410a45f7a1ed39ba118",
"gitBranchToEnvironmentMapping": {
"branchName": "dev",
"anotherBranchName": "staging"
}
}
```
### How it works
After configuring this property, every time you use the CLI with the specified configuration file, it will automatically verify if there is a corresponding environment mapping for the current Github branch you are on.
If it exists, the CLI will use that environment to retrieve secrets. You can override this behavior by explicitly using the `--env` flag while interacting with the CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

View File

@@ -30,3 +30,20 @@ Select which Infisical environment secrets you want to sync to which Vercel app
![integrations vercel](../../images/integrations-vercel-create.png)
![integrations vercel](../../images/integrations-vercel.png)
<Info>
Infisical syncs every envar to Vercel with type `encrypted` unless an existing
envar with the same name in Vercel exists with a different type. Note that
Infisical will not be able to update Vercel envars with type `sensitive` since
they can only be decrypted and modified by Vercel's deployment systems.
</Info>
<Warning>
The following environment variable names are reserved by Vercel and cannot be
synced: `AWS_SECRET_KEY`, `AWS_EXECUTION_ENV`, `AWS_LAMBDA_LOG_GROUP_NAME`,
`AWS_LAMBDA_LOG_STREAM_NAME`, `AWS_LAMBDA_FUNCTION_NAME`,
`AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, `AWS_LAMBDA_FUNCTION_VERSION`,
`NOW_REGION`, `TZ`, `LAMBDA_TASK_ROOT`, `LAMBDA_RUNTIME_DIR`,
`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`,
`AWS_REGION`, and `AWS_DEFAULT_REGION`.
</Warning>

View File

@@ -119,6 +119,7 @@
"cli/commands/reset"
]
},
"cli/project-config",
"cli/faq"
]
},

View File

@@ -25,7 +25,8 @@ By default, you need to configure the following SMTP [environment variables](htt
Below you will find details on how to configure common email providers (not in any particular order).
## Twilio SendGrid
<AccordionGroup>
<Accordion title="Twilio SendGrid">
1. Create an account and configure [SendGrid](https://sendgrid.com) to send emails.
2. Create a SendGrid API Key under Settings > [API Keys](https://app.sendgrid.com/settings/api_keys)
@@ -47,11 +48,12 @@ SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out em
SMTP_FROM_NAME=Infisical
```
<Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>
<Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>
</Accordion>
## Mailgun
<Accordion title="Mailgun">
1. Create an account and configure [Mailgun](https://www.mailgun.com) to send emails.
2. Obtain your Mailgun credentials in Sending > Overview > SMTP
@@ -70,7 +72,9 @@ SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out em
SMTP_FROM_NAME=Infisical
```
## AWS SES
</Accordion>
<Accordion title="AWS SES">
1. Create an account and [configure AWS SES](https://aws.amazon.com/premiumsupport/knowledge-center/ses-set-up-connect-smtp/) to send emails in the Amazon SES console.
2. Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials
@@ -82,7 +86,6 @@ SMTP_FROM_NAME=Infisical
3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables:
```
SMTP_HOST=smtp.mailgun.org # obtained from credentials page
SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
SMTP_USERNAME=xxx # your SMTP username
SMTP_PASSWORD=xxx # your SMTP password
@@ -95,3 +98,40 @@ SMTP_FROM_NAME=Infisical
<Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>
</Accordion>
<Accordion title="SocketLabs">
1. Create an account and configure [SocketLabs](https://www.socketlabs.com/) to send emails.
2. From the dashboard, navigate to SMTP Credentials > SMTP & APIs > SMTP Credentials to obtain your SocketLabs SMTP credentials.
![opening SocketLabs dashboard](../../images/email-socketlabs-dashboard.png)
![obtaining SocketLabs credentials](../../images/email-socketlabs-credentials.png)
3. With your SocketLabs SMTP credentials, you can now set up your SMTP environment variables:
```
SMTP_HOST=smtp.socketlabs.com
SMTP_USERNAME=username # obtained from your credentials
SMTP_PASSWORD=password # obtained from your credentials
SMTP_PORT=587
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```
<Note>
The `SMTP_FROM_ADDRESS` environment variable should be an email for an
authenticated domain under Configuration > Domain Management in SocketLabs.
For example, if you're using SocketLabs in sandbox mode, then you may use an
email like `team@sandbox.socketlabs.dev`.
</Note>
![SocketLabs domain management](../../images/email-socketlabs-domains.png)
<Info>
Remember that you will need to restart Infisical for this to work properly.
</Info>
</Accordion>
</AccordionGroup>

View File

@@ -10,6 +10,6 @@ export const parameters = {
}
},
darkMode: {
dark: { ...themes.dark, appContentBg: '#0e1014', appBg: '#0e1014' }
dark: { ...themes.dark, appContentBg: 'rgb(14,16,20)', appBg: 'rgb(14,16,20)' }
}
};

View File

@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from '@storybook/react';
import { EmptyState } from './EmptyState';
const meta: Meta<typeof EmptyState> = {
title: 'Components/EmptyState',
component: EmptyState,
tags: ['v2'],
argTypes: {},
args: {
title: 'No members found'
}
};
export default meta;
type Story = StoryObj<typeof EmptyState>;
export const Basic: Story = {
render: (args) => <EmptyState {...args} />
};

View File

@@ -0,0 +1,21 @@
import { ReactNode } from 'react';
import { faCubesStacked, IconDefinition } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { twMerge } from 'tailwind-merge';
type Props = {
title: ReactNode;
className?: string;
children?: ReactNode;
icon?: IconDefinition;
};
export const EmptyState = ({ title, className, children, icon = faCubesStacked }: Props) => (
<div className={twMerge('flex w-full flex-col items-center px-2 pt-6 text-bunker-300', className)}>
<FontAwesomeIcon icon={icon} size="2x" className='mr-4' />
<div className='flex flex-row items-center py-4'>
<div className="text-bunker-300 text-sm">{title}</div>
<div>{children}</div>
</div>
</div>
);

View File

@@ -0,0 +1 @@
export { EmptyState } from './EmptyState';

View File

@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Skeleton } from './Skeleton';
const meta: Meta<typeof Skeleton> = {
title: 'Components/Skeleton',
component: Skeleton,
tags: ['v2'],
argTypes: {}
};
export default meta;
type Story = StoryObj<typeof Skeleton>;
export const Basic: Story = {
render: (args) => <Skeleton {...args} />
};

View File

@@ -0,0 +1,12 @@
import { twMerge } from 'tailwind-merge';
export type Props = {
className?: string;
};
// To show something is coming up
// Can be used with cards
// Tables etc
export const Skeleton = ({ className }: Props) => (
<div className={twMerge('h-6 w-full animate-pulse rounded-md bg-mineshaft-800', className)} />
);

View File

@@ -0,0 +1 @@
export { Skeleton } from './Skeleton';

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table';
import { Table, TableContainer, TableSkeleton, TBody, Td, Th, THead, Tr } from './Table';
const meta: Meta<typeof Table> = {
title: 'Components/Table',
@@ -39,3 +39,22 @@ export const Basic: Story = {
</TableContainer>
)
};
export const Loading: Story = {
render: (args) => (
<TableContainer>
<Table {...args}>
<THead>
<Tr>
<Th>Head#1</Th>
<Th>Head#2</Th>
<Th>Head#3</Th>
</Tr>
</THead>
<TBody>
<TableSkeleton columns={3} key="story-book-table" />
</TBody>
</Table>
</TableContainer>
)
};

View File

@@ -1,6 +1,8 @@
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react';
import { twMerge } from 'tailwind-merge';
import { Skeleton } from '../Skeleton';
export type TableContainerProps = {
children: ReactNode;
isRounded?: boolean;
@@ -14,7 +16,7 @@ export const TableContainer = ({
}: TableContainerProps): JSX.Element => (
<div
className={twMerge(
'relative w-full overflow-x-auto border border-solid border-mineshaft-700 font-inter shadow-md',
'relative w-full overflow-x-auto border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter shadow-md',
isRounded && 'rounded-md',
className
)}
@@ -32,7 +34,7 @@ export type TableProps = {
export const Table = ({ children, className }: TableProps): JSX.Element => (
<table
className={twMerge(
'w-full rounded rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
'w-full rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
className
)}
>
@@ -59,7 +61,10 @@ export type TrProps = {
} & HTMLAttributes<HTMLTableRowElement>;
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
<tr className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)} {...props}>
<tr
className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)}
{...props}
>
{children}
</tr>
);
@@ -71,7 +76,7 @@ export type ThProps = {
};
export const Th = ({ children, className }: ThProps): JSX.Element => (
<th className={twMerge('px-5 pt-4 pb-3.5 font-medium font-semibold bg-bunker-500', className)}>{children}</th>
<th className={twMerge('bg-bunker-500 px-5 pt-4 pb-3.5 font-semibold', className)}>{children}</th>
);
// table body
@@ -95,3 +100,25 @@ export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
{children}
</td>
);
export type TBodyLoader = {
rows?: number;
columns: number;
className?: string;
// unique key for mapping
key: string;
};
export const TableSkeleton = ({ rows = 3, columns, key, className }: TBodyLoader): JSX.Element => (
<>
{Array.apply(0, Array(rows)).map((_x, i) => (
<Tr key={`${key}-skeleton-rows-${i + 1}`}>
{Array.apply(0, Array(columns)).map((_y, j) => (
<Td key={`${key}-skeleton-rows-${i + 1}-column-${j + 1}`}>
<Skeleton className={className} />
</Td>
))}
</Tr>
))}
</>
);

View File

@@ -7,4 +7,4 @@ export type {
ThProps,
TrProps
} from './Table';
export { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table';
export { Table, TableContainer, TableSkeleton,TBody, Td, Th, THead, Tr } from './Table';

View File

@@ -3,12 +3,14 @@ export * from './Card';
export * from './Checkbox';
export * from './DeleteActionModal';
export * from './Dropdown';
export * from './EmptyState';
export * from './FormControl';
export * from './IconButton';
export * from './Input';
export * from './Menu';
export * from './Modal';
export * from './Select';
export * from './Skeleton';
export * from './Spinner';
export * from './Switch';
export * from './Table';

View File

@@ -36,10 +36,12 @@ export const OrgSettingsPage = () => {
const { createNotification } = useNotificationContext();
const orgId = currentOrg?._id || '';
const { data: orgUsers } = useGetOrgUsers(orgId);
const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId);
const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId);
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
useGetUserWorkspaceMemberships(orgId);
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
const { data: incidentContact } = useGetOrgIncidentContact(orgId);
const { data: incidentContact, isLoading: IsIncidentContactLoading } =
useGetOrgIncidentContact(orgId);
const renameOrg = useRenameOrg();
const removeUserOrgMembership = useDeleteOrgMembership();
@@ -84,7 +86,7 @@ export const OrgSettingsPage = () => {
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to remove user from org',
text: 'Failed to remove user from the organization',
type: 'error'
});
}
@@ -95,7 +97,7 @@ export const OrgSettingsPage = () => {
try {
await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
createNotification({
text: 'Successfully invited user to org',
text: 'Successfully invited user to the organization.',
type: 'success'
});
} catch (error) {
@@ -197,9 +199,9 @@ export const OrgSettingsPage = () => {
/**
* This function deleted a workspace.
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
* It first checks if there is more than one workspace available. Otherwise, it doesn't delete
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
* It then deletes the workspace and forwards the user to another aviable workspace.
* It then deletes the workspace and forwards the user to another available workspace.
*/
// const executeDeletingWorkspace = async () => {
// const userWorkspaces = await getWorkspaces();
@@ -230,13 +232,11 @@ export const OrgSettingsPage = () => {
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
<OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} />
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
<p className="mr-4 text-xl font-semibold text-white">
<p className="mr-4 mb-4 text-xl font-semibold text-white">
{t('section-members:org-members')}
</p>
<p className="mr-4 mt-2 mb-2 text-gray-400">
{t('section-members:org-members-description')}
</p>
<OrgMembersTable
isLoading={isOrgUserLoading || IsWsMembershipLoading}
isMoreUserNotAllowed={isMoreUsersNotAllowed}
orgName={currentOrg?.name || ''}
members={orgUsers}
@@ -261,6 +261,7 @@ export const OrgSettingsPage = () => {
</div>
<div className="w-full">
<OrgIncidentContactsTable
isLoading={IsIncidentContactLoading}
contacts={incidentContact}
onRemoveContact={onRemoveIncidentContact}
onAddContact={onAddIncidentContact}

View File

@@ -1,6 +1,11 @@
import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
import {
faContactBook,
faMagnifyingGlass,
faPlus,
faTrash
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
@@ -8,6 +13,7 @@ import * as yup from 'yup';
import {
Button,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
@@ -15,16 +21,17 @@ import {
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from '@app/components/v2';
Tr} from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { IncidentContact } from '@app/hooks/api/types';
type Props = {
isLoading?: boolean;
contacts?: IncidentContact[];
onRemoveContact: (email: string) => Promise<void>;
onAddContact: (email: string) => Promise<void>;
@@ -39,7 +46,8 @@ type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
export const OrgIncidentContactsTable = ({
contacts = [],
onAddContact,
onRemoveContact
onRemoveContact,
isLoading
}: Props) => {
const [searchContact, setSearchContact] = useState('');
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -66,6 +74,10 @@ export const OrgIncidentContactsTable = ({
handlePopUpClose('removeContact');
};
const filteredContacts = contacts.filter(({ email }) =>
email.toLocaleLowerCase().includes(searchContact)
);
return (
<div className="w-full">
<div className="mb-4 flex">
@@ -96,28 +108,25 @@ export const OrgIncidentContactsTable = ({
</Tr>
</THead>
<TBody>
{contacts
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
?.map(({ email }) => (
<Tr key={email}>
<Td className="w-full">{email}</Td>
<Td className="mr-4">
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen('removeContact', { email })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
{filteredContacts?.map(({ email }) => (
<Tr key={email}>
<Td className="w-full">{email}</Td>
<Td className="mr-4">
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen('removeContact', { email })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
))}
</TBody>
</Table>
{contacts
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
?.length === 0 && (
<div className='py-4 bg-bunker-800 text-sm text-center text-bunker-400 w-full mx-auto flex justify-center'>No incident contacts found</div>
{filteredContacts?.length === 0 && !isLoading && (
<EmptyState title="No incident contacts found" icon={faContactBook} />
)}
</TableContainer>
</div>

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
import { faMagnifyingGlass, faPlus, faTrash, faUsers } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
@@ -8,6 +8,7 @@ import * as yup from 'yup';
import {
Button,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
@@ -17,14 +18,14 @@ import {
SelectItem,
Table,
TableContainer,
TableSkeleton,
Tag,
TBody,
Td,
Th,
THead,
Tr,
UpgradePlanModal
} from '@app/components/v2';
UpgradePlanModal} from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { OrgUser, Workspace } from '@app/hooks/api/types';
@@ -32,6 +33,7 @@ type Props = {
members?: OrgUser[];
workspaceMemberships?: Record<string, Workspace[]>;
orgName: string;
isLoading?: boolean;
isMoreUserNotAllowed: boolean;
onRemoveMember: (userId: string) => Promise<void>;
onInviteMember: (email: string) => Promise<void>;
@@ -56,7 +58,8 @@ export const OrgMembersTable = ({
onInviteMember,
onGrantAccess,
onRoleChange,
userId
userId,
isLoading
}: Props) => {
const [searchMemberFilter, setSearchMemberFilter] = useState('');
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -72,8 +75,8 @@ export const OrgMembersTable = ({
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
const onAddMember = ({ email }: TAddMemberForm) => {
onInviteMember(email);
const onAddMember = async ({ email }: TAddMemberForm) => {
await onInviteMember(email);
handlePopUpClose('addMember');
reset();
};
@@ -140,73 +143,79 @@ export const OrgMembersTable = ({
</Tr>
</THead>
<TBody>
{filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
const name = user ? `${user.firstName} ${user.lastName}` : '-';
const email = user?.email || inviteEmail;
const userWs = workspaceMemberships?.[user?._id];
{isLoading && <TableSkeleton columns={5} key="org-members" />}
{!isLoading &&
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
const name = user ? `${user.firstName} ${user.lastName}` : '-';
const email = user?.email || inviteEmail;
const userWs = workspaceMemberships?.[user?._id];
return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
{status === 'accepted' && (
<Select
defaultValue={role}
isDisabled={userId === user?._id}
className="w-full bg-mineshaft-600"
onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole)
}
>
{(isIamOwner || role === 'owner') && (
<SelectItem value="owner">owner</SelectItem>
)}
<SelectItem value="admin">admin</SelectItem>
<SelectItem value="member">member</SelectItem>
</Select>
)}
{(status === 'invited' || status === 'verified') && (
<Button colorSchema="secondary" onClick={() => onInviteMember(email)}>
Resent Invite
</Button>
)}
{status === 'completed' && (
<Button
colorSchema="secondary"
onClick={() => onGrantAccess(user?._id, user?.publicKey)}
>
Grant Access
</Button>
)}
</Td>
<Td>
{userWs ? (
userWs?.map(({ name: wsName, _id }) => (
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
{wsName}
</Tag>
))
) : (
<Tag colorSchema="red">This user isn&apos;t part of any projects yet</Tag>
)}
</Td>
<Td>
{userId !== user?._id && <IconButton
ariaLabel="delete"
colorSchema="danger"
isDisabled={userId === user?._id}
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>}
</Td>
</Tr>
);
})}
return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
{status === 'accepted' && (
<Select
defaultValue={role}
isDisabled={userId === user?._id}
className="w-full bg-mineshaft-600"
onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole)
}
>
{(isIamOwner || role === 'owner') && (
<SelectItem value="owner">owner</SelectItem>
)}
<SelectItem value="admin">admin</SelectItem>
<SelectItem value="member">member</SelectItem>
</Select>
)}
{(status === 'invited' || status === 'verified') && (
<Button colorSchema="secondary" onClick={() => onInviteMember(email)}>
Resent Invite
</Button>
)}
{status === 'completed' && (
<Button
colorSchema="secondary"
onClick={() => onGrantAccess(user?._id, user?.publicKey)}
>
Grant Access
</Button>
)}
</Td>
<Td>
{userWs ? (
userWs?.map(({ name: wsName, _id }) => (
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
{wsName}
</Tag>
))
) : (
<Tag colorSchema="red">This user isn&apos;t part of any projects yet</Tag>
)}
</Td>
<Td>
{userId !== user?._id && (
<IconButton
ariaLabel="delete"
colorSchema="danger"
isDisabled={userId === user?._id}
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</Td>
</Tr>
);
})}
</TBody>
</Table>
{filterdUser.length === 0 && <tr className='bg-bunker-800 text-sm py-4 text-center text-bunker-400 w-full mx-auto flex justify-center'><td className='col-span-5'>No project members found</td></tr>}
{!isLoading && filterdUser?.length === 0 && (
<EmptyState title="No project members found" icon={faUsers} />
)}
</TableContainer>
</div>
<Modal

View File

@@ -45,11 +45,9 @@ import {
export const ProjectSettingsPage = () => {
const { t } = useTranslation();
const { currentWorkspace, workspaces } = useWorkspace();
const { currentWorkspace, workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
const router = useRouter();
const { data: serviceTokens } = useGetUserWsServiceTokens({
workspaceID: currentWorkspace?._id || ''
});
const workspaceID = currentWorkspace?._id || '';
const { createNotification } = useNotificationContext();
// delete action worksapce
@@ -66,12 +64,15 @@ export const ProjectSettingsPage = () => {
const deleteWsEnv = useDeleteWsEnvironment();
// service token
const { data: serviceTokens, isLoading: isServiceTokenLoading } = useGetUserWsServiceTokens({
workspaceID: currentWorkspace?._id || ''
});
const { data: latestFileKey } = useGetUserWsKey(workspaceID);
const createServiceToken = useCreateServiceToken();
const deleteServiceToken = useDeleteServiceToken();
// tag
const { data: wsTags } = useGetWsTags(workspaceID);
const { data: wsTags, isLoading: isTagLoading } = useGetWsTags(workspaceID);
const createWsTag = useCreateWsTag();
const deleteWsTag = useDeleteWsTag();
@@ -300,7 +301,7 @@ export const ProjectSettingsPage = () => {
};
return (
<div className="container mx-auto flex flex-col px-8 text-mineshaft-50 dark dark:[color-scheme:dark]">
<div className="dark container mx-auto flex flex-col px-8 text-mineshaft-50 dark:[color-scheme:dark]">
{/* TODO(akhilmhdh): Remove this right when layout is refactored */}
<div className="relative right-5">
<NavHeader pageName={t('settings-project:title')} isProjectRelated />
@@ -319,6 +320,7 @@ export const ProjectSettingsPage = () => {
/>
<CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} />
<EnvironmentSection
isLoading={isWorkspaceLoading}
environments={currentWorkspace?.environments || []}
onCreate={onCreateWsEnv}
onDelete={onDeleteWsEnv}
@@ -326,6 +328,7 @@ export const ProjectSettingsPage = () => {
isEnvServiceAllowed={isEnvServiceAllowed}
/>
<ServiceTokenSection
isLoading={isServiceTokenLoading}
tokens={serviceTokens || []}
environments={currentWorkspace?.environments || []}
onDeleteToken={onDeleteServiceToken}
@@ -333,6 +336,7 @@ export const ProjectSettingsPage = () => {
onCreateToken={onCreateServiceToken}
/>
<SecretTagsSection
isLoading={isTagLoading}
tags={wsTags || []}
onDeleteTag={onDeleteTag}
workspaceName={currentWorkspace?.name || ''}

View File

@@ -13,22 +13,18 @@ export const AutoCapitalizationSection = ({
}: Props) => {
const { t } = useTranslation();
return (
<form>
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold">
{t('settings-project:auto-capitalization')}
</p>
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={workspaceAutoCapitalization}
onCheckedChange={(state) => {
onAutoCapitalizationChange(state as boolean);
}}
>
{t('settings-project:auto-capitalization-description')}
</Checkbox>
</div>
</form>
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold">{t('settings-project:auto-capitalization')}</p>
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={workspaceAutoCapitalization}
onCheckedChange={(state) => {
onAutoCapitalizationChange(state as boolean);
}}
>
{t('settings-project:auto-capitalization-description')}
</Checkbox>
</div>
);
};

View File

@@ -7,6 +7,7 @@ import * as yup from 'yup';
import {
Button,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
@@ -14,6 +15,7 @@ import {
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
@@ -25,6 +27,7 @@ import { usePopUp } from '@app/hooks/usePopUp';
type Props = {
environments: Array<{ name: string; slug: string }>;
isLoading?: boolean;
isEnvServiceAllowed: boolean;
onCreate: (data: CreateUpdateEnvFormData) => Promise<void>;
onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise<void>;
@@ -43,6 +46,7 @@ export const EnvironmentSection = ({
isEnvServiceAllowed,
onCreate,
onDelete,
isLoading,
onUpdate
}: Props): JSX.Element => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -116,7 +120,8 @@ export const EnvironmentSection = ({
</Tr>
</THead>
<TBody>
{environments?.length > 0 ? (
{isLoading && <TableSkeleton columns={3} key="project-envs" />}
{!isLoading &&
environments.map(({ name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
@@ -152,11 +157,11 @@ export const EnvironmentSection = ({
</IconButton>
</Td>
</Tr>
))
) : (
))}
{!isLoading && environments?.length === 0 && (
<Tr>
<Td colSpan={4} className="pt-7 pb-5 text-center text-bunker-400">
No environments found
<Td colSpan={3}>
<EmptyState title="No environments found" />
</Td>
</Tr>
)}

View File

@@ -1,5 +1,5 @@
import { Controller, useForm } from 'react-hook-form';
import { faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { faPlus, faTags, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
@@ -7,6 +7,7 @@ import * as yup from 'yup';
import {
Button,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
@@ -16,23 +17,24 @@ import {
ModalTrigger,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr,
} from '@app/components/v2';
Tr} from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { WorkspaceTag } from '@app/hooks/api/types';
const createTagSchema = yup.object({
name: yup.string().required().label('Tag Name'),
name: yup.string().required().label('Tag Name')
});
export type CreateWsTag = yup.InferType<typeof createTagSchema>;
type Props = {
tags: WorkspaceTag[];
isLoading?: boolean;
workspaceName: string;
onDeleteTag: (tagID: string) => Promise<void>;
onCreateTag: (data: CreateWsTag) => Promise<string>;
@@ -42,6 +44,7 @@ type DeleteModalData = { name: string; id: string };
export const SecretTagsSection = ({
tags = [],
isLoading,
onDeleteTag,
workspaceName,
onCreateTag
@@ -76,7 +79,10 @@ export const SecretTagsSection = ({
<div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
<p className="text-sm text-gray-400">Every secret can be assigned to one or more tags. Here you can add and remove tags for the current project.</p>
<p className="text-sm text-gray-400">
Every secret can be assigned to one or more tags. Here you can add and remove tags for
the current project.
</p>
</div>
<div>
<Modal
@@ -92,8 +98,8 @@ export const SecretTagsSection = ({
</Button>
</ModalTrigger>
<ModalContent
title={`Add a tag for ${ workspaceName}`}
subTitle='Specify your tag name, and the slug will be created automatically.'
title={`Add a tag for ${workspaceName}`}
subTitle="Specify your tag name, and the slug will be created automatically."
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
@@ -102,7 +108,7 @@ export const SecretTagsSection = ({
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label='Tag Name'
label="Tag Name"
isError={Boolean(error)}
errorText={error?.message}
>
@@ -130,7 +136,7 @@ export const SecretTagsSection = ({
</Modal>
</div>
</div>
<TableContainer className='mt-4'>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
@@ -140,7 +146,8 @@ export const SecretTagsSection = ({
</Tr>
</THead>
<TBody>
{tags?.length > 0 ? (
{isLoading && <TableSkeleton columns={3} key="secret-tags" />}
{!isLoading &&
tags.map(({ _id, name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
@@ -149,7 +156,7 @@ export const SecretTagsSection = ({
<IconButton
onClick={() =>
handlePopUpOpen('deleteTagConfirmation', {
name,
name,
id: _id
})
}
@@ -160,11 +167,11 @@ export const SecretTagsSection = ({
</IconButton>
</Td>
</Tr>
))
) : (
))}
{!isLoading && tags?.length === 0 && (
<Tr>
<Td colSpan={4} className="py-6 text-center text-bunker-400">
No tags found for this project
<Td colSpan={3}>
<EmptyState title="No secret tags found" icon={faTags} />
</Td>
</Tr>
)}

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { faCheck, faCopy, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { faCheck, faCopy, faKey, faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
@@ -9,6 +9,7 @@ import * as yup from 'yup';
import {
Button,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
@@ -20,6 +21,7 @@ import {
SelectItem,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
@@ -47,6 +49,7 @@ export type CreateServiceToken = yup.InferType<typeof createServiceTokenSchema>;
type Props = {
tokens: ServiceToken[];
isLoading?: boolean;
workspaceName: string;
environments: WorkspaceEnv[];
onDeleteToken: (serviceTokenID: string) => Promise<void>;
@@ -57,6 +60,7 @@ type DeleteModalData = { name: string; id: string };
export const ServiceTokenSection = ({
tokens = [],
isLoading,
onDeleteToken,
workspaceName,
environments = [],
@@ -252,7 +256,7 @@ export const ServiceTokenSection = ({
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
title={`Delete ${
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || ' '
} api key?`}
} service token?`}
onChange={(isOpen) => handlePopUpToggle('deleteAPITokenConfirmation', isOpen)}
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose('deleteAPITokenConfirmation')}
@@ -269,7 +273,8 @@ export const ServiceTokenSection = ({
</Tr>
</THead>
<TBody>
{tokens?.length > 0 ? (
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
{!isLoading &&
tokens.map((row) => (
<Tr key={row._id}>
<Td>{row.name}</Td>
@@ -290,11 +295,11 @@ export const ServiceTokenSection = ({
</IconButton>
</Td>
</Tr>
))
) : (
))}
{!isLoading && tokens?.length === 0 && (
<Tr>
<Td colSpan={4} className="py-6 text-center text-bunker-400">
No service tokens found
<EmptyState title="No service tokens found" icon={faKey} />
</Td>
</Tr>
)}

View File

@@ -1340,163 +1340,165 @@ module.exports = {
900: '#176437',
DEFAULT: '#2ecc71'
}
},
keyframes: {
type: {
'0%': { transform: 'translateX(0ch)' },
'5%, 10%': { transform: 'translateX(1ch)' },
'15%, 20%': { transform: 'translateX(2ch)' },
'25%, 30%': { transform: 'translateX(3ch)' },
'35%, 40%': { transform: 'translateX(4ch)' },
'45%, 50%': { transform: 'translateX(5ch)' },
'55%, 60%': { transform: 'translateX(6ch)' },
'65%, 70%': { transform: 'translateX(7ch)' },
'75%, 80%': { transform: 'translateX(8ch)' },
'85%, 90%': { transform: 'translateX(9ch)' },
'95%, 100%': { transform: 'translateX(11ch)' }
},
// REQUIRED BY DESIGN COMPONENT
// MODAL
fadeIn: {
'0%': { opacity: 0 },
'100%': { opacity: 1 }
},
popIn: {
from: {
opacity: 0,
transform: 'translate(-50%, -48%) scale(0.96)'
},
to: {
opacity: 1,
transform: 'translate(-50%, -50%) scale(1)'
}
},
// Dropdown
slideUpAndFade: {
from: {
opacity: 0,
transform: ' translateY(2px)'
},
to: {
opacity: 1,
transform: ' translateY(0)'
}
},
slideRightAndFade: {
from: {
opacity: 0,
transform: ' translateX(-2px)'
},
to: {
opacity: 1,
transform: ' translateX(0)'
}
},
slideDownAndFade: {
from: {
opacity: 0,
transform: ' translateY(-2px)'
},
to: {
opacity: 1,
transform: ' translateY(0)'
}
},
slideLeftAndFade: {
from: {
opacity: 0,
transform: ' translateX(2px)'
},
to: {
opacity: 1,
transform: ' translateX(0)'
}
},
// END
spin: {
'0%': { transform: 'rotate(0deg)' },
'40%': { transform: 'rotate(360deg)' },
'100%': { transform: 'rotate(360deg)' }
},
bounce: {
'0%': { transform: 'translateY(-90%)' },
'100%': { transform: 'translateY(-100%)' }
},
wiggle: {
'0%, 100%': { transform: 'rotate(-3deg)' },
'50%': { transform: 'rotate(3deg)' }
},
ping: {
'75%, 100%': {
transform: 'scale(2)',
opacity: 0
}
},
popup: {
'0%': {
transform: 'scale(0.2)',
opacity: 0
// transform: "translateY(120%)",
},
'100%': {
transform: 'scale(1)',
opacity: 1
// transform: "translateY(100%)",
}
},
popright: {
'0%': {
transform: 'translateX(-100%)'
},
'100%': {
transform: 'translateX(0%)'
}
},
popleft: {
'0%': {
transform: 'translateX(100%)'
},
'100%': {
transform: 'translateX(0%)'
}
},
popdown: {
'0%': {
transform: 'scale(0.2)',
opacity: 0
// transform: "translateY(80%)",
},
'100%': {
transform: 'scale(1)',
opacity: 1
// transform: "translateY(100%)",
}
}
},
animation: {
// Design Lib
// MODAL
fadeIn: 'fadeIn 100ms cubic-bezier(0.16, 1, 0.3, 1)',
popIn: 'popIn 150ms cubic-bezier(0.16, 1, 0.3, 1);',
// Dropdown
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
// END
// TODO:(akhilmhdh) remove all these unused and keep the config file as small as possible
// Make the whole color pallelte into simpler
bounce: 'bounce 1000ms ease-in-out infinite',
spin: 'spin 4000ms ease-in-out infinite',
cursor: 'cursor .6s linear infinite alternate',
type: 'type 2.7s ease-out .8s infinite alternate both',
'type-reverse': 'type 1.8s ease-out 0s infinite alternate-reverse both',
wiggle: 'wiggle 200ms ease-in-out',
ping: 'ping 1000ms ease-in-out infinite',
popup: 'popup 300ms ease-in-out',
popdown: 'popdown 300ms ease-in-out',
popright: 'popright 100ms ease-in-out',
popleft: 'popleft 100ms ease-in-out'
}
},
keyframes: {
type: {
'0%': { transform: 'translateX(0ch)' },
'5%, 10%': { transform: 'translateX(1ch)' },
'15%, 20%': { transform: 'translateX(2ch)' },
'25%, 30%': { transform: 'translateX(3ch)' },
'35%, 40%': { transform: 'translateX(4ch)' },
'45%, 50%': { transform: 'translateX(5ch)' },
'55%, 60%': { transform: 'translateX(6ch)' },
'65%, 70%': { transform: 'translateX(7ch)' },
'75%, 80%': { transform: 'translateX(8ch)' },
'85%, 90%': { transform: 'translateX(9ch)' },
'95%, 100%': { transform: 'translateX(11ch)' }
},
// REQUIRED BY DEISGN COMPONENT
// MODAL
fadeIn: {
'0%': { opacity: 0 },
'100%': { opacity: 1 }
},
popIn: {
from: {
opacity: 0,
transform: 'translate(-50%, -48%) scale(0.96)'
},
to: {
opacity: 1,
transform: 'translate(-50%, -50%) scale(1)'
}
},
// Dropdown
slideUpAndFade: {
from: {
opacity: 0,
transform: ' translateY(2px)'
},
to: {
opacity: 1,
transform: ' translateY(0)'
}
},
slideRightAndFade: {
from: {
opacity: 0,
transform: ' translateX(-2px)'
},
to: {
opacity: 1,
transform: ' translateX(0)'
}
},
slideDownAndFade: {
from: {
opacity: 0,
transform: ' translateY(-2px)'
},
to: {
opacity: 1,
transform: ' translateY(0)'
}
},
slideLeftAndFade: {
from: {
opacity: 0,
transform: ' translateX(2px)'
},
to: {
opacity: 1,
transform: ' translateX(0)'
}
},
// END
spin: {
'0%': { transform: 'rotate(0deg)' },
'40%': { transform: 'rotate(360deg)' },
'100%': { transform: 'rotate(360deg)' }
},
bounce: {
'0%': { transform: 'translateY(-90%)' },
'100%': { transform: 'translateY(-100%)' }
},
wiggle: {
'0%, 100%': { transform: 'rotate(-3deg)' },
'50%': { transform: 'rotate(3deg)' }
},
ping: {
'75%, 100%': {
transform: 'scale(2)',
opacity: 0
}
},
popup: {
'0%': {
transform: 'scale(0.2)',
opacity: 0
// transform: "translateY(120%)",
},
'100%': {
transform: 'scale(1)',
opacity: 1
// transform: "translateY(100%)",
}
},
popright: {
'0%': {
transform: 'translateX(-100%)'
},
'100%': {
transform: 'translateX(0%)'
}
},
popleft: {
'0%': {
transform: 'translateX(100%)'
},
'100%': {
transform: 'translateX(0%)'
}
},
popdown: {
'0%': {
transform: 'scale(0.2)',
opacity: 0
// transform: "translateY(80%)",
},
'100%': {
transform: 'scale(1)',
opacity: 1
// transform: "translateY(100%)",
}
}
},
animation: {
// Design Lib
// MODAL
fadeIn: 'fadeIn 100ms cubic-bezier(0.16, 1, 0.3, 1)',
popIn: 'popIn 150ms cubic-bezier(0.16, 1, 0.3, 1);',
// Dropdown
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
// END
bounce: 'bounce 1000ms ease-in-out infinite',
spin: 'spin 4000ms ease-in-out infinite',
cursor: 'cursor .6s linear infinite alternate',
type: 'type 2.7s ease-out .8s infinite alternate both',
'type-reverse': 'type 1.8s ease-out 0s infinite alternate-reverse both',
wiggle: 'wiggle 200ms ease-in-out',
ping: 'ping 1000ms ease-in-out infinite',
popup: 'popup 300ms ease-in-out',
popdown: 'popdown 300ms ease-in-out',
popright: 'popright 100ms ease-in-out',
popleft: 'popleft 100ms ease-in-out'
},
fontSize: {
xxxs: '.23rem',
xxs: '.5rem',

368
i18n/README.de.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@
<h4 align="center">
<a href="https://github.com/medusajs/medusa/blob/master/LICENSE">
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Medusa is released under the MIT license." />
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical is released under the MIT license." />
</a>
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
@@ -105,12 +105,13 @@ Estamos actualmente en Alpha público.
Actualmente estamos sentando bases y construyendo [integraciones](https://infisical.com/docs/integrations/overview) para que los secretos se puedan sincronizar en todas partes. ¡Cualquier ayuda es bienvenida! :)
<table>
<tr>
<th>Plataformas </th>
<th>Marcos</th>
</tr>
<tr>
<tr>
<td>
<table>
@@ -151,7 +152,7 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
</tr>
<tr>
<td align="left" valign="middle">
🔜 AWS PS (https://github.com/Infisical/infisical/issues/286)
🔜 Supabase
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cicd/githubactions">
@@ -167,7 +168,9 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
🔜 GCP SM (https://github.com/Infisical/infisical/issues/285)
</td>
<td align="left" valign="middle">
🔜 GitLab CI/CD (https://github.com/Infisical/infisical/issues/134)
<a href="https://infisical.com/docs/integrations/cicd/gitlab">
✔️ GitLab CI/CD
</a>
</td>
<td align="left" valign="middle">
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
@@ -189,8 +192,8 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
🔜 TravisCI
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cloud/netlify">
✔️ Netlify
<a href="https://infisical.com/docs/integrations/cloud/aws-secret-manager">
✔️ AWS Secrets Manager
</a>
</td>
<td align="left" valign="middle">
@@ -202,7 +205,9 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
🔜 Bitbucket
</td>
<td align="left" valign="middle">
🔜 Supabase
<a href="https://infisical.com/docs/integrations/cloud/aws-parameter-store">
✔️ AWS Parameter Store
</a>
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cloud/render">
@@ -218,7 +223,9 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
🔜 Serverless
</td>
<td align="left" valign="middle">
🔜 AWS Lambda
<a href="https://infisical.com/docs/integrations/cloud/netlify">
✔️ Netlify
</a>
</td>
</tr>
</tbody>
@@ -327,7 +334,7 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
</table>
</td>
</tr>
</tr>
</table>
## 🏘 Código abierto vs pagado

372
i18n/README.id.md Normal file

File diff suppressed because one or more lines are too long

View File

@@ -49,10 +49,10 @@
- **[시크릿 버전 기록](https://infisical.com/docs/getting-started/dashboard/versioning)** 을 통해 어떤 시크릿이든 버전 기록을 볼 수 있어요
- **[활동 로그](https://infisical.com/docs/getting-started/dashboard/audit-logs)** 를 통해 프로젝트의 모든 활동을 볼 수 있어요
- **[지정 시점으로 시크릿 복구](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** 를 사용해 특정한 시점으로 시크릿을 복구할 수 있어요
- **2단계 인증**
- **프로젝트 별 인증** (read/write 컨트롤도 곧 찾아옵니다)
- 🔜 Digital Ocean 및 Heroku로 **원클릭 배포**
- 🔜 **프로젝트 별 인증** (read/write 컨트롤도 곧 찾아옵니다)
- 🔜 **자동 시크릿 로테이션**
- 🔜 **2단계 인증**
- 🔜 **Slack & MS Teams** 연동
그 외에 더 많은 기능들이 있어요.

368
i18n/README.pt-br.md Normal file

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@
<h4 align="center">
<a href="https://github.com/medusajs/medusa/blob/master/LICENSE">
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Medusa is released under the MIT license." />
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical is released under the MIT license." />
</a>
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
@@ -47,10 +47,10 @@ Basit olması için tasarlandı, sadece birkaç dakika içerisinde harekete geç
- **[Sırlar İçin Versiyon Kontrol](https://infisical.com/docs/getting-started/dashboard/versioning)** - herhangi bir sır için değişiklik geçmişini görüntüleyin.
- **[Aktivite Günlükleri](https://infisical.com/docs/getting-started/dashboard/audit-logs)** - projedeki tüm değişikleri kayıt altına almak için.
- **[Anında Geri Yükleme](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** - sırlarınızın herhangi bir snapshotına geri yükleme yapmanız için.
- **2 Faktör Kimlik Doğrulama**
- 🔜 **Tek tıkla Deploy** edin, Digital Ocean ve Heroku'ya.
- 🔜 **Kimlik Doğrulama/Yetkilendirme** projeleriniz için okuma/yazma kontrolleri (pek yakında)
- 🔜 **Otomatik Sır Rotasyonu**
- 🔜 **2 Faktör Kimlik Doğrulama**
- 🔜 **Erişim Günlükleri**
- 🔜 **Slack & MS Teams** entegrasyonları
@@ -109,7 +109,7 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
<th>Platforms </th>
<th>Frameworks</th>
</tr>
<tr>
<tr>
<td>
<table>
@@ -150,7 +150,7 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
</tr>
<tr>
<td align="left" valign="middle">
🔜 AWS PS (https://github.com/Infisical/infisical/issues/286)
🔜 Supabase
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cicd/githubactions">
@@ -166,7 +166,9 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
🔜 GCP SM (https://github.com/Infisical/infisical/issues/285)
</td>
<td align="left" valign="middle">
🔜 GitLab CI/CD (https://github.com/Infisical/infisical/issues/134)
<a href="https://infisical.com/docs/integrations/cicd/gitlab">
✔️ GitLab CI/CD
</a>
</td>
<td align="left" valign="middle">
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
@@ -188,8 +190,8 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
🔜 TravisCI
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cloud/netlify">
✔️ Netlify
<a href="https://infisical.com/docs/integrations/cloud/aws-secret-manager">
✔️ AWS Secrets Manager
</a>
</td>
<td align="left" valign="middle">
@@ -201,7 +203,9 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
🔜 Bitbucket
</td>
<td align="left" valign="middle">
🔜 Supabase
<a href="https://infisical.com/docs/integrations/cloud/aws-parameter-store">
✔️ AWS Parameter Store
</a>
</td>
<td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cloud/render">
@@ -217,7 +221,9 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
🔜 Serverless
</td>
<td align="left" valign="middle">
🔜 AWS Lambda
<a href="https://infisical.com/docs/integrations/cloud/netlify">
✔️ Netlify
</a>
</td>
</tr>
</tbody>
@@ -326,7 +332,7 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
</table>
</td>
</tr>
</tr>
</table>
## 🏘 Açık kaynak mı yoksa ücretlimi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB