mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
Merge remote-tracking branch 'refs/remotes/origin/main' into Aashish-Upadhyay-101/TravisCI-integration
This commit is contained in:
22
.github/pull_request_template.md
vendored
Normal file
22
.github/pull_request_template.md
vendored
Normal 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). 📝
|
||||
64
backend/package-lock.json
generated
64
backend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
16
backend/src/config/request.ts
Normal file
16
backend/src/config/request.ts
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -118,7 +118,6 @@ router.delete( // TODO - rewire dashboard to this route
|
||||
workspaceController.deleteWorkspaceMembership
|
||||
);
|
||||
|
||||
|
||||
router.patch(
|
||||
'/:workspaceId/auto-capitalization',
|
||||
requireAuth({
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Bot, IBot } from '../models';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { handleEventHelper } from '../helpers/event';
|
||||
|
||||
interface Event {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
26
docs/cli/project-config.mdx
Normal file
26
docs/cli/project-config.mdx
Normal 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.
|
||||
BIN
docs/images/email-socketlabs-credentials.png
Normal file
BIN
docs/images/email-socketlabs-credentials.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
BIN
docs/images/email-socketlabs-dashboard.png
Normal file
BIN
docs/images/email-socketlabs-dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 468 KiB |
BIN
docs/images/email-socketlabs-domains.png
Normal file
BIN
docs/images/email-socketlabs-domains.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 332 KiB |
@@ -30,3 +30,20 @@ Select which Infisical environment secrets you want to sync to which Vercel app
|
||||
|
||||

|
||||

|
||||
|
||||
<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>
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"cli/commands/reset"
|
||||
]
|
||||
},
|
||||
"cli/project-config",
|
||||
"cli/faq"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
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>
|
||||
|
||||

|
||||
|
||||
<Info>
|
||||
Remember that you will need to restart Infisical for this to work properly.
|
||||
</Info>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -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)' }
|
||||
}
|
||||
};
|
||||
|
||||
20
frontend/src/components/v2/EmptyState/EmptyState.stories.tsx
Normal file
20
frontend/src/components/v2/EmptyState/EmptyState.stories.tsx
Normal 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} />
|
||||
};
|
||||
21
frontend/src/components/v2/EmptyState/EmptyState.tsx
Normal file
21
frontend/src/components/v2/EmptyState/EmptyState.tsx
Normal 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>
|
||||
);
|
||||
1
frontend/src/components/v2/EmptyState/index.tsx
Normal file
1
frontend/src/components/v2/EmptyState/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { EmptyState } from './EmptyState';
|
||||
17
frontend/src/components/v2/Skeleton/Skeleton.stories.tsx
Normal file
17
frontend/src/components/v2/Skeleton/Skeleton.stories.tsx
Normal 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} />
|
||||
};
|
||||
12
frontend/src/components/v2/Skeleton/Skeleton.tsx
Normal file
12
frontend/src/components/v2/Skeleton/Skeleton.tsx
Normal 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)} />
|
||||
);
|
||||
1
frontend/src/components/v2/Skeleton/index.tsx
Normal file
1
frontend/src/components/v2/Skeleton/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { Skeleton } from './Skeleton';
|
||||
@@ -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>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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'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
|
||||
|
||||
@@ -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 || ''}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
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
@@ -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
372
i18n/README.id.md
Normal file
File diff suppressed because one or more lines are too long
@@ -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
368
i18n/README.pt-br.md
Normal file
File diff suppressed because one or more lines are too long
@@ -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 |
Reference in New Issue
Block a user