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
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). 📝
+7 -4
View File
File diff suppressed because one or more lines are too long
+64
View File
@@ -19,6 +19,7 @@
"await-to-js": "^3.0.0", "await-to-js": "^3.0.0",
"aws-sdk": "^2.1311.0", "aws-sdk": "^2.1311.0",
"axios": "^1.1.3", "axios": "^1.1.3",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2", "bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0", "builder-pattern": "^2.2.0",
@@ -3473,6 +3474,17 @@
"@babel/core": "^7.0.0-0" "@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": { "node_modules/@babel/template": {
"version": "7.18.10", "version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -5317,6 +5329,15 @@
"proxy-from-env": "^1.1.0" "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": { "node_modules/babel-jest": {
"version": "29.3.1", "version": "29.3.1",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.3.1.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.3.1.tgz",
@@ -7604,6 +7625,17 @@
"node": ">=0.10.0" "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": { "node_modules/is-stream": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -12179,6 +12211,11 @@
"node": ">=8.10.0" "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": { "node_modules/regexpp": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@@ -16584,6 +16621,14 @@
"@babel/helper-plugin-utils": "^7.19.0" "@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": { "@babel/template": {
"version": "7.18.10", "version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
@@ -18075,6 +18120,15 @@
"proxy-from-env": "^1.1.0" "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": { "babel-jest": {
"version": "29.3.1", "version": "29.3.1",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.3.1.tgz", "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", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" "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": { "is-stream": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -23088,6 +23147,11 @@
"picomatch": "^2.2.1" "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": { "regexpp": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+1
View File
@@ -10,6 +10,7 @@
"await-to-js": "^3.0.0", "await-to-js": "^3.0.0",
"aws-sdk": "^2.1311.0", "aws-sdk": "^2.1311.0",
"axios": "^1.1.3", "axios": "^1.1.3",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2", "bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0", "builder-pattern": "^2.2.0",
+16
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;
+69 -26
View File
@@ -32,6 +32,7 @@ import {
INTEGRATION_CIRCLECI_API_URL, INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
} from "../variables"; } from "../variables";
import axiosWithRetry from '../config/request';
/** /**
* Sync/push [secrets] to [app] in integration named [integration] * Sync/push [secrets] to [app] in integration named [integration]
@@ -568,10 +569,46 @@ const syncSecretsVercel = async ({
: {}), : {}),
}; };
const res = ( // const res = (
await Promise.all( // await Promise.all(
( // (
await axios.get( // 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`, `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{ {
params, params,
@@ -583,9 +620,15 @@ const syncSecretsVercel = async ({
)) ))
.data .data
.envs .envs
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)) .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}`, 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, params,
headers: { headers: {
@@ -593,11 +636,13 @@ const syncSecretsVercel = async ({
'Accept-Encoding': 'application/json' 'Accept-Encoding': 'application/json'
} }
} }
)).data) )).data;
)).reduce((obj: any, secret: any) => ({
...obj, res[vercelSecret.key] = decryptedSecret;
[secret.key]: secret } else {
}), {}); res[vercelSecret.key] = vercelSecret;
}
}
const updateSecrets: VercelSecret[] = []; const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = []; const deleteSecrets: VercelSecret[] = [];
@@ -625,8 +670,10 @@ const syncSecretsVercel = async ({
id: res[key].id, id: res[key].id,
key: key, key: key,
value: secrets[key], value: secrets[key],
type: "encrypted", type: res[key].type,
target: [integration.targetEnvironment], target: res[key].target.includes(integration.targetEnvironment)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment]
}); });
} }
} else { } else {
@@ -635,7 +682,7 @@ const syncSecretsVercel = async ({
id: res[key].id, id: res[key].id,
key: key, key: key,
value: res[key].value, value: res[key].value,
type: "encrypted", type: "encrypted", // value doesn't matter
target: [integration.targetEnvironment], target: [integration.targetEnvironment],
}); });
} }
@@ -643,7 +690,7 @@ const syncSecretsVercel = async ({
// Sync/push new secrets // Sync/push new secrets
if (newSecrets.length > 0) { if (newSecrets.length > 0) {
await axios.post( await axiosWithRetry.post(
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
newSecrets, newSecrets,
{ {
@@ -656,11 +703,10 @@ const syncSecretsVercel = async ({
); );
} }
// Sync/push updated secrets for await (const secret of updateSecrets) {
if (updateSecrets.length > 0) { if (secret.type !== 'sensitive') {
updateSecrets.forEach(async (secret: VercelSecret) => {
const { id, ...updatedSecret } = secret; const { id, ...updatedSecret } = secret;
await axios.patch( await axiosWithRetry.patch(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
updatedSecret, updatedSecret,
{ {
@@ -671,13 +717,11 @@ const syncSecretsVercel = async ({
}, },
} }
); );
}); }
} }
// Delete secrets for await (const secret of deleteSecrets) {
if (deleteSecrets.length > 0) { await axiosWithRetry.delete(
deleteSecrets.forEach(async (secret: VercelSecret) => {
await axios.delete(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{ {
params, params,
@@ -687,7 +731,6 @@ const syncSecretsVercel = async ({
}, },
} }
); );
});
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
-1
View File
@@ -118,7 +118,6 @@ router.delete( // TODO - rewire dashboard to this route
workspaceController.deleteWorkspaceMembership workspaceController.deleteWorkspaceMembership
); );
router.patch( router.patch(
'/:workspaceId/auto-capitalization', '/:workspaceId/auto-capitalization',
requireAuth({ requireAuth({
-2
View File
@@ -1,5 +1,3 @@
import { Bot, IBot } from '../models';
import * as Sentry from '@sentry/node';
import { handleEventHelper } from '../helpers/event'; import { handleEventHelper } from '../helpers/event';
interface Event { interface Event {
+11 -1
View File
@@ -1,6 +1,10 @@
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE } from '../config'; 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 SMTPConnection from 'nodemailer/lib/smtp-connection';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
@@ -27,6 +31,12 @@ if (SMTP_SECURE) {
ciphers: 'TLSv1.2' ciphers: 'TLSv1.2'
} }
break; break;
case SMTP_HOST_SOCKETLABS:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
default: default:
if (SMTP_HOST.includes('amazonaws.com')) { if (SMTP_HOST.includes('amazonaws.com')) {
mailOpts.tls = { mailOpts.tls = {
+6 -1
View File
@@ -44,7 +44,11 @@ import {
ACTION_DELETE_SECRETS, ACTION_DELETE_SECRETS,
ACTION_READ_SECRETS ACTION_READ_SECRETS
} from './action'; } 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 { PLAN_STARTER, PLAN_PRO } from './stripe';
import { import {
MFA_METHOD_EMAIL MFA_METHOD_EMAIL
@@ -105,6 +109,7 @@ export {
INTEGRATION_OPTIONS, INTEGRATION_OPTIONS,
SMTP_HOST_SENDGRID, SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN, SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
PLAN_STARTER, PLAN_STARTER,
PLAN_PRO, PLAN_PRO,
MFA_METHOD_EMAIL, MFA_METHOD_EMAIL,
+3 -1
View File
@@ -1,7 +1,9 @@
const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net'; const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
const SMTP_HOST_MAILGUN = 'smtp.mailgun.org'; const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
export { export {
SMTP_HOST_SENDGRID, SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS
} }
+7 -4
View File
@@ -36,9 +36,12 @@ var exportCmd = &cobra.Command{
// util.RequireLocalWorkspaceFile() // util.RequireLocalWorkspaceFile()
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
envName, err := cmd.Flags().GetString("env") environmentName, _ := cmd.Flags().GetString("env")
if err != nil { if !cmd.Flags().Changed("env") {
util.HandleError(err) environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
} }
shouldExpandSecrets, err := cmd.Flags().GetBool("expand") shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
@@ -66,7 +69,7 @@ var exportCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") 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 { if err != nil {
util.HandleError(err, "Unable to fetch secrets") util.HandleError(err, "Unable to fetch secrets")
} }
+18 -6
View File
@@ -87,7 +87,7 @@ var loginCmd = &cobra.Command{
break break
} else if mfaErrorResponse != nil { } else if mfaErrorResponse != nil {
if mfaErrorResponse.Context.Code == "mfa_invalid" { 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) fmt.Println(msg)
if i == 5 { if i == 5 {
util.PrintErrorMessageAndExit("No tries left, please try again in a bit") 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" { 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 break
} }
i++ i++
@@ -120,6 +120,7 @@ var loginCmd = &cobra.Command{
var decryptedPrivateKey []byte var decryptedPrivateKey []byte
if loginTwoResponse.EncryptionVersion == 1 { if loginTwoResponse.EncryptionVersion == 1 {
log.Debug("Login version 1")
encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey) encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey)
tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag) tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag)
if err != nil { if err != nil {
@@ -134,11 +135,15 @@ var loginCmd = &cobra.Command{
paddedPassword := fmt.Sprintf("%032s", password) paddedPassword := fmt.Sprintf("%032s", password)
key := []byte(paddedPassword) key := []byte(paddedPassword)
decryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV) computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(key, encryptedPrivateKey, tag, IV)
if err != nil || len(decryptedPrivateKey) == 0 { if err != nil || len(computedDecryptedPrivateKey) == 0 {
util.HandleError(err) util.HandleError(err)
} }
decryptedPrivateKey = computedDecryptedPrivateKey
} else if loginTwoResponse.EncryptionVersion == 2 { } else if loginTwoResponse.EncryptionVersion == 2 {
log.Debug("Login version 2")
protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey) protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey)
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
@@ -191,14 +196,21 @@ var loginCmd = &cobra.Command{
util.HandleError(err) util.HandleError(err)
} }
decryptedPrivateKey, err = crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv) computedDecryptedPrivateKey, err := crypto.DecryptSymmetric(decryptedProtectedKeyInHex, encryptedPrivateKey, nonProtectedTag, nonProtectedIv)
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
decryptedPrivateKey = computedDecryptedPrivateKey
} else { } else {
util.PrintErrorMessageAndExit("Insufficient details to decrypt private key") 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{ userCredentialsToBeStored := &models.UserCredentials{
Email: email, Email: email,
PrivateKey: string(decryptedPrivateKey), PrivateKey: string(decryptedPrivateKey),
@@ -340,7 +352,7 @@ func generateFromPassword(password string, salt []byte, p *params) (hash []byte,
func askForMFACode() string { func askForMFACode() string {
mfaCodePromptUI := promptui.Prompt{ mfaCodePromptUI := promptui.Prompt{
Label: "MFA verification code", Label: "Enter the 2FA verification code sent to your email",
} }
mfaVerifyCode, err := mfaCodePromptUI.Run() mfaVerifyCode, err := mfaCodePromptUI.Run()
+7 -4
View File
@@ -54,9 +54,12 @@ var runCmd = &cobra.Command{
return nil return nil
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
envName, err := cmd.Flags().GetString("env") environmentName, _ := cmd.Flags().GetString("env")
if err != nil { if !cmd.Flags().Changed("env") {
util.HandleError(err, "Unable to parse flag") environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := cmd.Flags().GetString("token")
@@ -79,7 +82,7 @@ var runCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") 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 { 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") util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid")
+30 -18
View File
@@ -19,7 +19,6 @@ import (
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
"github.com/Infisical/infisical-merge/packages/visualize" "github.com/Infisical/infisical-merge/packages/visualize"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -31,9 +30,12 @@ var secretsCmd = &cobra.Command{
PreRun: toggleDebug, PreRun: toggleDebug,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env") environmentName, _ := cmd.Flags().GetString("env")
if err != nil { if !cmd.Flags().Changed("env") {
util.HandleError(err) environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := cmd.Flags().GetString("token")
@@ -94,9 +96,12 @@ var secretsSetCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
util.RequireLocalWorkspaceFile() util.RequireLocalWorkspaceFile()
environmentName, err := cmd.Flags().GetString("env") environmentName, _ := cmd.Flags().GetString("env")
if err != nil { if !cmd.Flags().Changed("env") {
util.HandleError(err, "Unable to parse flag") environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
} }
workspaceFile, err := util.GetWorkSpaceFromFile() workspaceFile, err := util.GetWorkSpaceFromFile()
@@ -270,11 +275,12 @@ var secretsDeleteCmd = &cobra.Command{
PreRun: toggleDebug, PreRun: toggleDebug,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env") environmentName, _ := cmd.Flags().GetString("env")
if err != nil { if !cmd.Flags().Changed("env") {
log.Errorln("Unable to parse the environment name flag") environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
log.Debugln(err) if environmentFromWorkspace != "" {
return environmentName = environmentFromWorkspace
}
} }
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
@@ -330,9 +336,12 @@ var secretsDeleteCmd = &cobra.Command{
} }
func getSecretsByNames(cmd *cobra.Command, args []string) { func getSecretsByNames(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env") environmentName, _ := cmd.Flags().GetString("env")
if err != nil { if !cmd.Flags().Changed("env") {
util.HandleError(err, "Unable to parse flag") environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := cmd.Flags().GetString("token")
@@ -373,9 +382,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
} }
func generateExampleEnv(cmd *cobra.Command, args []string) { func generateExampleEnv(cmd *cobra.Command, args []string) {
environmentName, err := cmd.Flags().GetString("env") environmentName, _ := cmd.Flags().GetString("env")
if err != nil { if !cmd.Flags().Changed("env") {
util.HandleError(err, "Unable to parse flag") environmentFromWorkspace := util.GetEnvelopmentBasedOnGitBranch()
if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace
}
} }
infisicalToken, err := cmd.Flags().GetString("token") infisicalToken, err := cmd.Flags().GetString("token")
+2
View File
@@ -41,6 +41,7 @@ type Workspace struct {
type WorkspaceConfigFile struct { type WorkspaceConfigFile struct {
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
DefaultEnvironment string `json:"defaultEnvironment"` DefaultEnvironment string `json:"defaultEnvironment"`
GitBranchToEnvironmentMapping map[string]string `json:"gitBranchToEnvironmentMapping"`
} }
type SymmetricEncryptionResult struct { type SymmetricEncryptionResult struct {
@@ -51,6 +52,7 @@ type SymmetricEncryptionResult struct {
type GetAllSecretsParameters struct { type GetAllSecretsParameters struct {
Environment string Environment string
EnvironmentPassedViaFlag bool
InfisicalToken string InfisicalToken string
TagSlugs string TagSlugs string
} }
+15
View File
@@ -1,10 +1,14 @@
package util package util
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"os" "os"
"os/exec"
"path"
"strings"
) )
type DecodedSymmetricEncryptionDetails = struct { type DecodedSymmetricEncryptionDetails = struct {
@@ -110,3 +114,14 @@ func GetHashFromStringList(list []string) string {
sum := sha256.Sum256(hash.Sum(nil)) sum := sha256.Sum256(hash.Sum(nil))
return fmt.Sprintf("%x", sum) 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
}
+24 -4
View File
@@ -131,10 +131,6 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models
return nil, err return nil, err
} }
if workspaceFile.DefaultEnvironment != "" {
params.Environment = workspaceFile.DefaultEnvironment
}
// Verify environment // Verify environment
err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials) err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials)
if err != nil { if err != nil {
@@ -485,3 +481,27 @@ func DeleteBackupSecrets() error {
return os.RemoveAll(fullPathToSecretsBackupFolder) 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 ""
}
}
+4
View File
@@ -19,3 +19,7 @@ If you are still experiencing trouble, please seek support.
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. 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>
<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>
+1 -1
View File
@@ -20,7 +20,7 @@ The Infisical CLI provides a way to inject environment variables from the platfo
### Updates ### Updates
```bash ```bash
brew upgrade infisical brew update && brew upgrade infisical
``` ```
</Tab> </Tab>
+26
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

+17
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-create.png)
![integrations vercel](../../images/integrations-vercel.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>
+1
View File
@@ -119,6 +119,7 @@
"cli/commands/reset" "cli/commands/reset"
] ]
}, },
"cli/project-config",
"cli/faq" "cli/faq"
] ]
}, },
+44 -4
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). 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. 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) 2. Create a SendGrid API Key under Settings > [API Keys](https://app.sendgrid.com/settings/api_keys)
@@ -50,8 +51,9 @@ SMTP_FROM_NAME=Infisical
<Info> <Info>
Remember that you will need to restart Infisical for this to work properly. Remember that you will need to restart Infisical for this to work properly.
</Info> </Info>
</Accordion>
## Mailgun <Accordion title="Mailgun">
1. Create an account and configure [Mailgun](https://www.mailgun.com) to send emails. 1. Create an account and configure [Mailgun](https://www.mailgun.com) to send emails.
2. Obtain your Mailgun credentials in Sending > Overview > SMTP 2. Obtain your Mailgun credentials in Sending > Overview > SMTP
@@ -70,7 +72,9 @@ [email protected] # your email address being used to send out em
SMTP_FROM_NAME=Infisical 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. 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 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: 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_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
SMTP_USERNAME=xxx # your SMTP username SMTP_USERNAME=xxx # your SMTP username
SMTP_PASSWORD=xxx # your SMTP password SMTP_PASSWORD=xxx # your SMTP password
@@ -95,3 +98,40 @@ SMTP_FROM_NAME=Infisical
<Info> <Info>
Remember that you will need to restart Infisical for this to work properly. Remember that you will need to restart Infisical for this to work properly.
</Info> </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
[email protected] # 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 `[email protected]`.
</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>
+1 -1
View File
@@ -10,6 +10,6 @@ export const parameters = {
} }
}, },
darkMode: { darkMode: {
dark: { ...themes.dark, appContentBg: '#0e1014', appBg: '#0e1014' } dark: { ...themes.dark, appContentBg: 'rgb(14,16,20)', appBg: 'rgb(14,16,20)' }
} }
}; };
@@ -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} />
};
@@ -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>
);
@@ -0,0 +1 @@
export { EmptyState } from './EmptyState';
@@ -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} />
};
@@ -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)} />
);
@@ -0,0 +1 @@
export { Skeleton } from './Skeleton';
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'; 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> = { const meta: Meta<typeof Table> = {
title: 'Components/Table', title: 'Components/Table',
@@ -39,3 +39,22 @@ export const Basic: Story = {
</TableContainer> </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>
)
};
+31 -4
View File
@@ -1,6 +1,8 @@
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react'; import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { Skeleton } from '../Skeleton';
export type TableContainerProps = { export type TableContainerProps = {
children: ReactNode; children: ReactNode;
isRounded?: boolean; isRounded?: boolean;
@@ -14,7 +16,7 @@ export const TableContainer = ({
}: TableContainerProps): JSX.Element => ( }: TableContainerProps): JSX.Element => (
<div <div
className={twMerge( 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', isRounded && 'rounded-md',
className className
)} )}
@@ -32,7 +34,7 @@ export type TableProps = {
export const Table = ({ children, className }: TableProps): JSX.Element => ( export const Table = ({ children, className }: TableProps): JSX.Element => (
<table <table
className={twMerge( 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 className
)} )}
> >
@@ -59,7 +61,10 @@ export type TrProps = {
} & HTMLAttributes<HTMLTableRowElement>; } & HTMLAttributes<HTMLTableRowElement>;
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => ( 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} {children}
</tr> </tr>
); );
@@ -71,7 +76,7 @@ export type ThProps = {
}; };
export const Th = ({ children, className }: ThProps): JSX.Element => ( 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 // table body
@@ -95,3 +100,25 @@ export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
{children} {children}
</td> </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>
))}
</>
);
+1 -1
View File
@@ -7,4 +7,4 @@ export type {
ThProps, ThProps,
TrProps TrProps
} from './Table'; } from './Table';
export { Table, TableContainer, TBody, Td, Th, THead, Tr } from './Table'; export { Table, TableContainer, TableSkeleton,TBody, Td, Th, THead, Tr } from './Table';
+2
View File
@@ -3,12 +3,14 @@ export * from './Card';
export * from './Checkbox'; export * from './Checkbox';
export * from './DeleteActionModal'; export * from './DeleteActionModal';
export * from './Dropdown'; export * from './Dropdown';
export * from './EmptyState';
export * from './FormControl'; export * from './FormControl';
export * from './IconButton'; export * from './IconButton';
export * from './Input'; export * from './Input';
export * from './Menu'; export * from './Menu';
export * from './Modal'; export * from './Modal';
export * from './Select'; export * from './Select';
export * from './Skeleton';
export * from './Spinner'; export * from './Spinner';
export * from './Switch'; export * from './Switch';
export * from './Table'; export * from './Table';
@@ -36,10 +36,12 @@ export const OrgSettingsPage = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const orgId = currentOrg?._id || ''; const orgId = currentOrg?._id || '';
const { data: orgUsers } = useGetOrgUsers(orgId); const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId);
const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId); const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
useGetUserWorkspaceMemberships(orgId);
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || ''); const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
const { data: incidentContact } = useGetOrgIncidentContact(orgId); const { data: incidentContact, isLoading: IsIncidentContactLoading } =
useGetOrgIncidentContact(orgId);
const renameOrg = useRenameOrg(); const renameOrg = useRenameOrg();
const removeUserOrgMembership = useDeleteOrgMembership(); const removeUserOrgMembership = useDeleteOrgMembership();
@@ -84,7 +86,7 @@ export const OrgSettingsPage = () => {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
createNotification({ createNotification({
text: 'Failed to remove user from org', text: 'Failed to remove user from the organization',
type: 'error' type: 'error'
}); });
} }
@@ -95,7 +97,7 @@ export const OrgSettingsPage = () => {
try { try {
await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email }); await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
createNotification({ createNotification({
text: 'Successfully invited user to org', text: 'Successfully invited user to the organization.',
type: 'success' type: 'success'
}); });
} catch (error) { } catch (error) {
@@ -197,9 +199,9 @@ export const OrgSettingsPage = () => {
/** /**
* This function deleted a workspace. * 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 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 executeDeletingWorkspace = async () => {
// const userWorkspaces = await getWorkspaces(); // 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"> <div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
<OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} /> <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"> <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')} {t('section-members:org-members')}
</p> </p>
<p className="mr-4 mt-2 mb-2 text-gray-400">
{t('section-members:org-members-description')}
</p>
<OrgMembersTable <OrgMembersTable
isLoading={isOrgUserLoading || IsWsMembershipLoading}
isMoreUserNotAllowed={isMoreUsersNotAllowed} isMoreUserNotAllowed={isMoreUsersNotAllowed}
orgName={currentOrg?.name || ''} orgName={currentOrg?.name || ''}
members={orgUsers} members={orgUsers}
@@ -261,6 +261,7 @@ export const OrgSettingsPage = () => {
</div> </div>
<div className="w-full"> <div className="w-full">
<OrgIncidentContactsTable <OrgIncidentContactsTable
isLoading={IsIncidentContactLoading}
contacts={incidentContact} contacts={incidentContact}
onRemoveContact={onRemoveIncidentContact} onRemoveContact={onRemoveIncidentContact}
onAddContact={onAddIncidentContact} onAddContact={onAddIncidentContact}
@@ -1,6 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -8,6 +13,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -15,16 +21,17 @@ import {
ModalContent, ModalContent,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
THead, THead,
Tr Tr} from '@app/components/v2';
} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { IncidentContact } from '@app/hooks/api/types'; import { IncidentContact } from '@app/hooks/api/types';
type Props = { type Props = {
isLoading?: boolean;
contacts?: IncidentContact[]; contacts?: IncidentContact[];
onRemoveContact: (email: string) => Promise<void>; onRemoveContact: (email: string) => Promise<void>;
onAddContact: (email: string) => Promise<void>; onAddContact: (email: string) => Promise<void>;
@@ -39,7 +46,8 @@ type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
export const OrgIncidentContactsTable = ({ export const OrgIncidentContactsTable = ({
contacts = [], contacts = [],
onAddContact, onAddContact,
onRemoveContact onRemoveContact,
isLoading
}: Props) => { }: Props) => {
const [searchContact, setSearchContact] = useState(''); const [searchContact, setSearchContact] = useState('');
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -66,6 +74,10 @@ export const OrgIncidentContactsTable = ({
handlePopUpClose('removeContact'); handlePopUpClose('removeContact');
}; };
const filteredContacts = contacts.filter(({ email }) =>
email.toLocaleLowerCase().includes(searchContact)
);
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex"> <div className="mb-4 flex">
@@ -96,9 +108,8 @@ export const OrgIncidentContactsTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{contacts {isLoading && <TableSkeleton columns={2} key="incident-contact" />}
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact)) {filteredContacts?.map(({ email }) => (
?.map(({ email }) => (
<Tr key={email}> <Tr key={email}>
<Td className="w-full">{email}</Td> <Td className="w-full">{email}</Td>
<Td className="mr-4"> <Td className="mr-4">
@@ -114,10 +125,8 @@ export const OrgIncidentContactsTable = ({
))} ))}
</TBody> </TBody>
</Table> </Table>
{contacts {filteredContacts?.length === 0 && !isLoading && (
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact)) <EmptyState title="No incident contacts found" icon={faContactBook} />
?.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>
)} )}
</TableContainer> </TableContainer>
</div> </div>
@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -8,6 +8,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -17,14 +18,14 @@ import {
SelectItem, SelectItem,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
Tag, Tag,
TBody, TBody,
Td, Td,
Th, Th,
THead, THead,
Tr, Tr,
UpgradePlanModal UpgradePlanModal} from '@app/components/v2';
} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { OrgUser, Workspace } from '@app/hooks/api/types'; import { OrgUser, Workspace } from '@app/hooks/api/types';
@@ -32,6 +33,7 @@ type Props = {
members?: OrgUser[]; members?: OrgUser[];
workspaceMemberships?: Record<string, Workspace[]>; workspaceMemberships?: Record<string, Workspace[]>;
orgName: string; orgName: string;
isLoading?: boolean;
isMoreUserNotAllowed: boolean; isMoreUserNotAllowed: boolean;
onRemoveMember: (userId: string) => Promise<void>; onRemoveMember: (userId: string) => Promise<void>;
onInviteMember: (email: string) => Promise<void>; onInviteMember: (email: string) => Promise<void>;
@@ -56,7 +58,8 @@ export const OrgMembersTable = ({
onInviteMember, onInviteMember,
onGrantAccess, onGrantAccess,
onRoleChange, onRoleChange,
userId userId,
isLoading
}: Props) => { }: Props) => {
const [searchMemberFilter, setSearchMemberFilter] = useState(''); const [searchMemberFilter, setSearchMemberFilter] = useState('');
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -72,8 +75,8 @@ export const OrgMembersTable = ({
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) }); } = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
const onAddMember = ({ email }: TAddMemberForm) => { const onAddMember = async ({ email }: TAddMemberForm) => {
onInviteMember(email); await onInviteMember(email);
handlePopUpClose('addMember'); handlePopUpClose('addMember');
reset(); reset();
}; };
@@ -140,7 +143,9 @@ export const OrgMembersTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => { {isLoading && <TableSkeleton columns={5} key="org-members" />}
{!isLoading &&
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
const name = user ? `${user.firstName} ${user.lastName}` : '-'; const name = user ? `${user.firstName} ${user.lastName}` : '-';
const email = user?.email || inviteEmail; const email = user?.email || inviteEmail;
const userWs = workspaceMemberships?.[user?._id]; const userWs = workspaceMemberships?.[user?._id];
@@ -192,21 +197,25 @@ export const OrgMembersTable = ({
)} )}
</Td> </Td>
<Td> <Td>
{userId !== user?._id && <IconButton {userId !== user?._id && (
<IconButton
ariaLabel="delete" ariaLabel="delete"
colorSchema="danger" colorSchema="danger"
isDisabled={userId === user?._id} isDisabled={userId === user?._id}
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })} onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
> >
<FontAwesomeIcon icon={faTrash} /> <FontAwesomeIcon icon={faTrash} />
</IconButton>} </IconButton>
)}
</Td> </Td>
</Tr> </Tr>
); );
})} })}
</TBody> </TBody>
</Table> </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> </TableContainer>
</div> </div>
<Modal <Modal
@@ -45,11 +45,9 @@ import {
export const ProjectSettingsPage = () => { export const ProjectSettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentWorkspace, workspaces } = useWorkspace(); const { currentWorkspace, workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
const router = useRouter(); const router = useRouter();
const { data: serviceTokens } = useGetUserWsServiceTokens({
workspaceID: currentWorkspace?._id || ''
});
const workspaceID = currentWorkspace?._id || ''; const workspaceID = currentWorkspace?._id || '';
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
// delete action worksapce // delete action worksapce
@@ -66,12 +64,15 @@ export const ProjectSettingsPage = () => {
const deleteWsEnv = useDeleteWsEnvironment(); const deleteWsEnv = useDeleteWsEnvironment();
// service token // service token
const { data: serviceTokens, isLoading: isServiceTokenLoading } = useGetUserWsServiceTokens({
workspaceID: currentWorkspace?._id || ''
});
const { data: latestFileKey } = useGetUserWsKey(workspaceID); const { data: latestFileKey } = useGetUserWsKey(workspaceID);
const createServiceToken = useCreateServiceToken(); const createServiceToken = useCreateServiceToken();
const deleteServiceToken = useDeleteServiceToken(); const deleteServiceToken = useDeleteServiceToken();
// tag // tag
const { data: wsTags } = useGetWsTags(workspaceID); const { data: wsTags, isLoading: isTagLoading } = useGetWsTags(workspaceID);
const createWsTag = useCreateWsTag(); const createWsTag = useCreateWsTag();
const deleteWsTag = useDeleteWsTag(); const deleteWsTag = useDeleteWsTag();
@@ -300,7 +301,7 @@ export const ProjectSettingsPage = () => {
}; };
return ( 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 */} {/* TODO(akhilmhdh): Remove this right when layout is refactored */}
<div className="relative right-5"> <div className="relative right-5">
<NavHeader pageName={t('settings-project:title')} isProjectRelated /> <NavHeader pageName={t('settings-project:title')} isProjectRelated />
@@ -319,6 +320,7 @@ export const ProjectSettingsPage = () => {
/> />
<CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} /> <CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} />
<EnvironmentSection <EnvironmentSection
isLoading={isWorkspaceLoading}
environments={currentWorkspace?.environments || []} environments={currentWorkspace?.environments || []}
onCreate={onCreateWsEnv} onCreate={onCreateWsEnv}
onDelete={onDeleteWsEnv} onDelete={onDeleteWsEnv}
@@ -326,6 +328,7 @@ export const ProjectSettingsPage = () => {
isEnvServiceAllowed={isEnvServiceAllowed} isEnvServiceAllowed={isEnvServiceAllowed}
/> />
<ServiceTokenSection <ServiceTokenSection
isLoading={isServiceTokenLoading}
tokens={serviceTokens || []} tokens={serviceTokens || []}
environments={currentWorkspace?.environments || []} environments={currentWorkspace?.environments || []}
onDeleteToken={onDeleteServiceToken} onDeleteToken={onDeleteServiceToken}
@@ -333,6 +336,7 @@ export const ProjectSettingsPage = () => {
onCreateToken={onCreateServiceToken} onCreateToken={onCreateServiceToken}
/> />
<SecretTagsSection <SecretTagsSection
isLoading={isTagLoading}
tags={wsTags || []} tags={wsTags || []}
onDeleteTag={onDeleteTag} onDeleteTag={onDeleteTag}
workspaceName={currentWorkspace?.name || ''} workspaceName={currentWorkspace?.name || ''}
@@ -13,11 +13,8 @@ export const AutoCapitalizationSection = ({
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( 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"> <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"> <p className="mb-4 mt-2 text-xl font-semibold">{t('settings-project:auto-capitalization')}</p>
{t('settings-project:auto-capitalization')}
</p>
<Checkbox <Checkbox
className="data-[state=checked]:bg-primary" className="data-[state=checked]:bg-primary"
id="autoCapitalization" id="autoCapitalization"
@@ -29,6 +26,5 @@ export const AutoCapitalizationSection = ({
{t('settings-project:auto-capitalization-description')} {t('settings-project:auto-capitalization-description')}
</Checkbox> </Checkbox>
</div> </div>
</form>
); );
}; };
@@ -7,6 +7,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -14,6 +15,7 @@ import {
ModalContent, ModalContent,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
@@ -25,6 +27,7 @@ import { usePopUp } from '@app/hooks/usePopUp';
type Props = { type Props = {
environments: Array<{ name: string; slug: string }>; environments: Array<{ name: string; slug: string }>;
isLoading?: boolean;
isEnvServiceAllowed: boolean; isEnvServiceAllowed: boolean;
onCreate: (data: CreateUpdateEnvFormData) => Promise<void>; onCreate: (data: CreateUpdateEnvFormData) => Promise<void>;
onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise<void>; onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise<void>;
@@ -43,6 +46,7 @@ export const EnvironmentSection = ({
isEnvServiceAllowed, isEnvServiceAllowed,
onCreate, onCreate,
onDelete, onDelete,
isLoading,
onUpdate onUpdate
}: Props): JSX.Element => { }: Props): JSX.Element => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -116,7 +120,8 @@ export const EnvironmentSection = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{environments?.length > 0 ? ( {isLoading && <TableSkeleton columns={3} key="project-envs" />}
{!isLoading &&
environments.map(({ name, slug }) => ( environments.map(({ name, slug }) => (
<Tr key={name}> <Tr key={name}>
<Td>{name}</Td> <Td>{name}</Td>
@@ -152,11 +157,11 @@ export const EnvironmentSection = ({
</IconButton> </IconButton>
</Td> </Td>
</Tr> </Tr>
)) ))}
) : ( {!isLoading && environments?.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="pt-7 pb-5 text-center text-bunker-400"> <Td colSpan={3}>
No environments found <EmptyState title="No environments found" />
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -1,5 +1,5 @@
import { Controller, useForm } from 'react-hook-form'; 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -7,6 +7,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -16,23 +17,24 @@ import {
ModalTrigger, ModalTrigger,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
THead, THead,
Tr, Tr} from '@app/components/v2';
} from '@app/components/v2';
import { usePopUp } from '@app/hooks'; import { usePopUp } from '@app/hooks';
import { WorkspaceTag } from '@app/hooks/api/types'; import { WorkspaceTag } from '@app/hooks/api/types';
const createTagSchema = yup.object({ 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>; export type CreateWsTag = yup.InferType<typeof createTagSchema>;
type Props = { type Props = {
tags: WorkspaceTag[]; tags: WorkspaceTag[];
isLoading?: boolean;
workspaceName: string; workspaceName: string;
onDeleteTag: (tagID: string) => Promise<void>; onDeleteTag: (tagID: string) => Promise<void>;
onCreateTag: (data: CreateWsTag) => Promise<string>; onCreateTag: (data: CreateWsTag) => Promise<string>;
@@ -42,6 +44,7 @@ type DeleteModalData = { name: string; id: string };
export const SecretTagsSection = ({ export const SecretTagsSection = ({
tags = [], tags = [],
isLoading,
onDeleteTag, onDeleteTag,
workspaceName, workspaceName,
onCreateTag onCreateTag
@@ -76,7 +79,10 @@ export const SecretTagsSection = ({
<div className="flex w-full flex-row justify-between"> <div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Secret Tags</p> <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>
<div> <div>
<Modal <Modal
@@ -93,7 +99,7 @@ export const SecretTagsSection = ({
</ModalTrigger> </ModalTrigger>
<ModalContent <ModalContent
title={`Add a tag for ${workspaceName}`} title={`Add a tag for ${workspaceName}`}
subTitle='Specify your tag name, and the slug will be created automatically.' subTitle="Specify your tag name, and the slug will be created automatically."
> >
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
@@ -102,7 +108,7 @@ export const SecretTagsSection = ({
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label='Tag Name' label="Tag Name"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
@@ -130,7 +136,7 @@ export const SecretTagsSection = ({
</Modal> </Modal>
</div> </div>
</div> </div>
<TableContainer className='mt-4'> <TableContainer className="mt-4">
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
@@ -140,7 +146,8 @@ export const SecretTagsSection = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{tags?.length > 0 ? ( {isLoading && <TableSkeleton columns={3} key="secret-tags" />}
{!isLoading &&
tags.map(({ _id, name, slug }) => ( tags.map(({ _id, name, slug }) => (
<Tr key={name}> <Tr key={name}>
<Td>{name}</Td> <Td>{name}</Td>
@@ -160,11 +167,11 @@ export const SecretTagsSection = ({
</IconButton> </IconButton>
</Td> </Td>
</Tr> </Tr>
)) ))}
) : ( {!isLoading && tags?.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="py-6 text-center text-bunker-400"> <Td colSpan={3}>
No tags found for this project <EmptyState title="No secret tags found" icon={faTags} />
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -9,6 +9,7 @@ import * as yup from 'yup';
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
@@ -20,6 +21,7 @@ import {
SelectItem, SelectItem,
Table, Table,
TableContainer, TableContainer,
TableSkeleton,
TBody, TBody,
Td, Td,
Th, Th,
@@ -47,6 +49,7 @@ export type CreateServiceToken = yup.InferType<typeof createServiceTokenSchema>;
type Props = { type Props = {
tokens: ServiceToken[]; tokens: ServiceToken[];
isLoading?: boolean;
workspaceName: string; workspaceName: string;
environments: WorkspaceEnv[]; environments: WorkspaceEnv[];
onDeleteToken: (serviceTokenID: string) => Promise<void>; onDeleteToken: (serviceTokenID: string) => Promise<void>;
@@ -57,6 +60,7 @@ type DeleteModalData = { name: string; id: string };
export const ServiceTokenSection = ({ export const ServiceTokenSection = ({
tokens = [], tokens = [],
isLoading,
onDeleteToken, onDeleteToken,
workspaceName, workspaceName,
environments = [], environments = [],
@@ -252,7 +256,7 @@ export const ServiceTokenSection = ({
isOpen={popUp.deleteAPITokenConfirmation.isOpen} isOpen={popUp.deleteAPITokenConfirmation.isOpen}
title={`Delete ${ title={`Delete ${
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || ' ' (popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || ' '
} api key?`} } service token?`}
onChange={(isOpen) => handlePopUpToggle('deleteAPITokenConfirmation', isOpen)} onChange={(isOpen) => handlePopUpToggle('deleteAPITokenConfirmation', isOpen)}
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name} deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose('deleteAPITokenConfirmation')} onClose={() => handlePopUpClose('deleteAPITokenConfirmation')}
@@ -269,7 +273,8 @@ export const ServiceTokenSection = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{tokens?.length > 0 ? ( {isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
{!isLoading &&
tokens.map((row) => ( tokens.map((row) => (
<Tr key={row._id}> <Tr key={row._id}>
<Td>{row.name}</Td> <Td>{row.name}</Td>
@@ -290,11 +295,11 @@ export const ServiceTokenSection = ({
</IconButton> </IconButton>
</Td> </Td>
</Tr> </Tr>
)) ))}
) : ( {!isLoading && tokens?.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="py-6 text-center text-bunker-400"> <Td colSpan={4} className="py-6 text-center text-bunker-400">
No service tokens found <EmptyState title="No service tokens found" icon={faKey} />
</Td> </Td>
</Tr> </Tr>
)} )}
+4 -2
View File
@@ -1340,7 +1340,6 @@ module.exports = {
900: '#176437', 900: '#176437',
DEFAULT: '#2ecc71' DEFAULT: '#2ecc71'
} }
}
}, },
keyframes: { keyframes: {
type: { type: {
@@ -1356,7 +1355,7 @@ module.exports = {
'85%, 90%': { transform: 'translateX(9ch)' }, '85%, 90%': { transform: 'translateX(9ch)' },
'95%, 100%': { transform: 'translateX(11ch)' } '95%, 100%': { transform: 'translateX(11ch)' }
}, },
// REQUIRED BY DEISGN COMPONENT // REQUIRED BY DESIGN COMPONENT
// MODAL // MODAL
fadeIn: { fadeIn: {
'0%': { opacity: 0 }, '0%': { opacity: 0 },
@@ -1485,6 +1484,8 @@ module.exports = {
slideUpAndFade: 'slideUpAndFade 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)', slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
// END // 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', bounce: 'bounce 1000ms ease-in-out infinite',
spin: 'spin 4000ms ease-in-out infinite', spin: 'spin 4000ms ease-in-out infinite',
cursor: 'cursor .6s linear infinite alternate', cursor: 'cursor .6s linear infinite alternate',
@@ -1496,6 +1497,7 @@ module.exports = {
popdown: 'popdown 300ms ease-in-out', popdown: 'popdown 300ms ease-in-out',
popright: 'popright 100ms ease-in-out', popright: 'popright 100ms ease-in-out',
popleft: 'popleft 100ms ease-in-out' popleft: 'popleft 100ms ease-in-out'
}
}, },
fontSize: { fontSize: {
xxxs: '.23rem', xxxs: '.23rem',
+368
View File
File diff suppressed because one or more lines are too long
+43 -41
View File
File diff suppressed because one or more lines are too long
+14 -7
View File
@@ -16,7 +16,7 @@
<h4 align="center"> <h4 align="center">
<a href="https://github.com/medusajs/medusa/blob/master/LICENSE"> <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>
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md"> <a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" /> <img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
@@ -105,6 +105,7 @@ 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! :) 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> <table>
<tr> <tr>
<th>Plataformas </th> <th>Plataformas </th>
@@ -151,7 +152,7 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
</tr> </tr>
<tr> <tr>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 AWS PS (https://github.com/Infisical/infisical/issues/286) 🔜 Supabase
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cicd/githubactions"> <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) 🔜 GCP SM (https://github.com/Infisical/infisical/issues/285)
</td> </td>
<td align="left" valign="middle"> <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>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91) 🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
@@ -189,8 +192,8 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
🔜 TravisCI 🔜 TravisCI
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cloud/netlify"> <a href="https://infisical.com/docs/integrations/cloud/aws-secret-manager">
✔️ Netlify ✔️ AWS Secrets Manager
</a> </a>
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
@@ -202,7 +205,9 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
🔜 Bitbucket 🔜 Bitbucket
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 Supabase <a href="https://infisical.com/docs/integrations/cloud/aws-parameter-store">
✔️ AWS Parameter Store
</a>
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cloud/render"> <a href="https://infisical.com/docs/integrations/cloud/render">
@@ -218,7 +223,9 @@ Actualmente estamos sentando bases y construyendo [integraciones](https://infisi
🔜 Serverless 🔜 Serverless
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 AWS Lambda <a href="https://infisical.com/docs/integrations/cloud/netlify">
✔️ Netlify
</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
+372
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -49,10 +49,10 @@
- **[시크릿 버전 기록](https://infisical.com/docs/getting-started/dashboard/versioning)** 을 통해 어떤 시크릿이든 버전 기록을 볼 수 있어요 - **[시크릿 버전 기록](https://infisical.com/docs/getting-started/dashboard/versioning)** 을 통해 어떤 시크릿이든 버전 기록을 볼 수 있어요
- **[활동 로그](https://infisical.com/docs/getting-started/dashboard/audit-logs)** 를 통해 프로젝트의 모든 활동을 볼 수 있어요 - **[활동 로그](https://infisical.com/docs/getting-started/dashboard/audit-logs)** 를 통해 프로젝트의 모든 활동을 볼 수 있어요
- **[지정 시점으로 시크릿 복구](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** 를 사용해 특정한 시점으로 시크릿을 복구할 수 있어요 - **[지정 시점으로 시크릿 복구](https://infisical.com/docs/getting-started/dashboard/pit-recovery)** 를 사용해 특정한 시점으로 시크릿을 복구할 수 있어요
- **2단계 인증**
- **프로젝트 별 인증** (read/write 컨트롤도 곧 찾아옵니다)
- 🔜 Digital Ocean 및 Heroku로 **원클릭 배포** - 🔜 Digital Ocean 및 Heroku로 **원클릭 배포**
- 🔜 **프로젝트 별 인증** (read/write 컨트롤도 곧 찾아옵니다)
- 🔜 **자동 시크릿 로테이션** - 🔜 **자동 시크릿 로테이션**
- 🔜 **2단계 인증**
- 🔜 **Slack & MS Teams** 연동 - 🔜 **Slack & MS Teams** 연동
그 외에 더 많은 기능들이 있어요. 그 외에 더 많은 기능들이 있어요.
File diff suppressed because one or more lines are too long
+14 -8
View File
@@ -16,7 +16,7 @@
<h4 align="center"> <h4 align="center">
<a href="https://github.com/medusajs/medusa/blob/master/LICENSE"> <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>
<a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md"> <a href="https://github.com/infisical/infisical/blob/main/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" /> <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. - **[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. - **[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. - **[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. - 🔜 **Tek tıkla Deploy** edin, Digital Ocean ve Heroku'ya.
- 🔜 **Kimlik Doğrulama/Yetkilendirme** projeleriniz için okuma/yazma kontrolleri (pek yakında) - 🔜 **Kimlik Doğrulama/Yetkilendirme** projeleriniz için okuma/yazma kontrolleri (pek yakında)
- 🔜 **Otomatik Sır Rotasyonu** - 🔜 **Otomatik Sır Rotasyonu**
- 🔜 **2 Faktör Kimlik Doğrulama**
- 🔜 **Erişim Günlükleri** - 🔜 **Erişim Günlükleri**
- 🔜 **Slack & MS Teams** entegrasyonları - 🔜 **Slack & MS Teams** entegrasyonları
@@ -150,7 +150,7 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
</tr> </tr>
<tr> <tr>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 AWS PS (https://github.com/Infisical/infisical/issues/286) 🔜 Supabase
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cicd/githubactions"> <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) 🔜 GCP SM (https://github.com/Infisical/infisical/issues/285)
</td> </td>
<td align="left" valign="middle"> <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>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 CircleCI (https://github.com/Infisical/infisical/issues/91) 🔜 CircleCI (https://github.com/Infisical/infisical/issues/91)
@@ -188,8 +190,8 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
🔜 TravisCI 🔜 TravisCI
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cloud/netlify"> <a href="https://infisical.com/docs/integrations/cloud/aws-secret-manager">
✔️ Netlify ✔️ AWS Secrets Manager
</a> </a>
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
@@ -201,7 +203,9 @@ Nereden başlayacağınızdan emin değil misiniz? O zaman;
🔜 Bitbucket 🔜 Bitbucket
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 Supabase <a href="https://infisical.com/docs/integrations/cloud/aws-parameter-store">
✔️ AWS Parameter Store
</a>
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
<a href="https://infisical.com/docs/integrations/cloud/render"> <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 🔜 Serverless
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 AWS Lambda <a href="https://infisical.com/docs/integrations/cloud/netlify">
✔️ Netlify
</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB