mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-27 09:40:45 +00:00
Compare commits
28 Commits
infisical-
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
293a62b632 | |||
a1f08b064e | |||
50977cf788 | |||
8ee6710e9b | |||
9fa28f5b5e | |||
ae375916e8 | |||
21f1648998 | |||
88695a2f8c | |||
77114e02cf | |||
3ac1795a5b | |||
8d6f59b253 | |||
7fd77b14ff | |||
8d3d7d98e3 | |||
6cac879ed0 | |||
ac66834daa | |||
0616f24923 | |||
4e1abc6eba | |||
8f57377130 | |||
2d7c7f075e | |||
c342b22d49 | |||
b8120f7512 | |||
ca18883bd3 | |||
8b381b2b80 | |||
954806d950 | |||
d6d3302659 | |||
9a1b453c86 | |||
66ea3ba172 | |||
cb42db3de4 |
@ -3203,6 +3203,9 @@
|
||||
"name": {
|
||||
"example": "any"
|
||||
},
|
||||
"tagColor": {
|
||||
"example": "any"
|
||||
},
|
||||
"slug": {
|
||||
"example": "any"
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
ACTION_UPDATE_SECRETS,
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
K8_USER_AGENT_NAME,
|
||||
SECRET_PERSONAL
|
||||
} from "../../variables";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
@ -59,7 +60,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
|
||||
let secretPath = req.body.secretPath as string;
|
||||
let folderId = req.body.folderId as string;
|
||||
|
||||
|
||||
const createSecrets: BatchSecret[] = [];
|
||||
const updateSecrets: BatchSecret[] = [];
|
||||
const deleteSecrets: { _id: Types.ObjectId, secretName: string; }[] = [];
|
||||
@ -154,7 +155,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
const auditLogs = await Promise.all(
|
||||
createdSecrets.map((secret, index) => {
|
||||
return EEAuditLogService.createAuditLog(
|
||||
@ -178,7 +179,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
);
|
||||
|
||||
await AuditLog.insertMany(auditLogs);
|
||||
|
||||
|
||||
const addAction = (await EELogService.createAction({
|
||||
name: ACTION_ADD_SECRETS,
|
||||
userId: req.user?._id,
|
||||
@ -234,6 +235,9 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
$unset: {
|
||||
'metadata.source': true as true
|
||||
},
|
||||
...u,
|
||||
_id: new Types.ObjectId(u._id)
|
||||
}
|
||||
@ -277,7 +281,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
$in: updateSecrets.map((u) => new Types.ObjectId(u._id))
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const auditLogs = await Promise.all(
|
||||
updateSecrets.map((secret) => {
|
||||
return EEAuditLogService.createAuditLog(
|
||||
@ -329,26 +333,26 @@ export const batchSecrets = async (req: Request, res: Response) => {
|
||||
// handle delete secrets
|
||||
if (deleteSecrets.length > 0) {
|
||||
const deleteSecretIds: Types.ObjectId[] = deleteSecrets.map((s) => s._id);
|
||||
|
||||
|
||||
const deletedSecretsObj = (await Secret.find({
|
||||
_id: {
|
||||
$in: deleteSecretIds
|
||||
}
|
||||
}))
|
||||
.reduce(
|
||||
(obj: any, secret: ISecret) => ({
|
||||
...obj,
|
||||
[secret._id.toString()]: secret
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
.reduce(
|
||||
(obj: any, secret: ISecret) => ({
|
||||
...obj,
|
||||
[secret._id.toString()]: secret
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
await Secret.deleteMany({
|
||||
_id: {
|
||||
$in: deleteSecretIds
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: deleteSecretIds
|
||||
});
|
||||
@ -949,7 +953,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
channel,
|
||||
ipAddress: req.realIP
|
||||
}));
|
||||
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
@ -966,21 +970,36 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
);
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
|
||||
// reduce the number of events captured
|
||||
let shouldRecordK8Event = false
|
||||
if (req.authData.userAgent == K8_USER_AGENT_NAME) {
|
||||
const randomNumber = Math.random();
|
||||
if (randomNumber > 0.9) {
|
||||
shouldRecordK8Event = true
|
||||
}
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets pulled",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData: req.authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
channel,
|
||||
folderId,
|
||||
userAgent: req.headers?.["user-agent"]
|
||||
}
|
||||
});
|
||||
const shouldCapture = req.authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event;
|
||||
const approximateForNoneCapturedEvents = secrets.length * 10
|
||||
|
||||
if (shouldCapture) {
|
||||
postHogClient.capture({
|
||||
event: "secrets pulled",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData: req.authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: shouldRecordK8Event ? approximateForNoneCapturedEvents : secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: req.authData.userAgentType,
|
||||
userAgent: req.authData.userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
@ -1087,10 +1106,10 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
||||
tags,
|
||||
...(secretCommentCiphertext !== undefined && secretCommentIV && secretCommentTag
|
||||
? {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
}
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,11 @@ import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
|
||||
export const createWorkspaceTag = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { name, slug } = req.body;
|
||||
const { name, slug, tagColor } = req.body;
|
||||
|
||||
const tagToCreate = {
|
||||
name,
|
||||
tagColor,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
slug,
|
||||
user: new Types.ObjectId(req.user._id),
|
||||
|
@ -117,7 +117,7 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
ref: "Tag",
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
ALGORITHM_AES_256_GCM,
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
K8_USER_AGENT_NAME,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED
|
||||
} from "../variables";
|
||||
@ -393,7 +394,8 @@ export const createSecretHelper = async ({
|
||||
secretCommentTag,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
metadata
|
||||
}).save();
|
||||
|
||||
const secretVersion = new SecretVersion({
|
||||
@ -567,21 +569,33 @@ export const getSecretsHelper = async ({
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
|
||||
// reduce the number of events captured
|
||||
let shouldRecordK8Event = false
|
||||
if (authData.userAgent == K8_USER_AGENT_NAME) {
|
||||
const randomNumber = Math.random();
|
||||
if (randomNumber > 0.9) {
|
||||
shouldRecordK8Event = true
|
||||
}
|
||||
}
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets pulled",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.userAgentType,
|
||||
userAgent: authData.userAgent
|
||||
}
|
||||
});
|
||||
const shouldCapture = authData.userAgent !== K8_USER_AGENT_NAME || shouldRecordK8Event;
|
||||
const approximateForNoneCapturedEvents = secrets.length * 10
|
||||
|
||||
if (shouldCapture) {
|
||||
postHogClient.capture({
|
||||
event: "secrets pulled",
|
||||
distinctId: await TelemetryService.getDistinctId({ authData }),
|
||||
properties: {
|
||||
numberOfSecrets: shouldRecordK8Event ? approximateForNoneCapturedEvents : secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.userAgentType,
|
||||
userAgent: authData.userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return secrets;
|
||||
|
@ -31,6 +31,9 @@ export interface ISecret {
|
||||
keyEncoding: "utf8" | "base64";
|
||||
tags?: string[];
|
||||
folder?: string;
|
||||
metadata?: {
|
||||
[key: string]: string;
|
||||
}
|
||||
}
|
||||
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
@ -131,6 +134,9 @@ const secretSchema = new Schema<ISecret>(
|
||||
type: String,
|
||||
default: "root",
|
||||
},
|
||||
metadata: {
|
||||
type: Schema.Types.Mixed
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
@ -3,6 +3,7 @@ import { Schema, Types, model } from "mongoose";
|
||||
export interface ITag {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
tagColor: string;
|
||||
slug: string;
|
||||
user: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
@ -15,6 +16,11 @@ const tagSchema = new Schema<ITag>(
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
tagColor: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -11,7 +11,7 @@ router.post("/token", validateRequest, authController.getNewToken);
|
||||
router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1)
|
||||
"/login1",
|
||||
authLimiter,
|
||||
body("email").exists().trim().notEmpty(),
|
||||
body("email").exists().trim().notEmpty().toLowerCase(),
|
||||
body("clientPublicKey").exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login1
|
||||
@ -20,7 +20,7 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1)
|
||||
router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login2)
|
||||
"/login2",
|
||||
authLimiter,
|
||||
body("email").exists().trim().notEmpty(),
|
||||
body("email").exists().trim().notEmpty().toLowerCase(),
|
||||
body("clientProof").exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login2
|
||||
|
@ -8,7 +8,7 @@ import { authLimiter } from "../../helpers/rateLimiter";
|
||||
router.post( // TODO: deprecate (moved to api/v3/auth/login1)
|
||||
"/login1",
|
||||
authLimiter,
|
||||
body("email").isString().trim().notEmpty(),
|
||||
body("email").isString().trim().notEmpty().toLowerCase(),
|
||||
body("clientPublicKey").isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login1
|
||||
@ -17,7 +17,7 @@ router.post( // TODO: deprecate (moved to api/v3/auth/login1)
|
||||
router.post( // TODO: deprecate (moved to api/v3/auth/login1)
|
||||
"/login2",
|
||||
authLimiter,
|
||||
body("email").isString().trim().notEmpty(),
|
||||
body("email").isString().trim().notEmpty().toLowerCase(),
|
||||
body("clientProof").isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
authController.login2
|
||||
|
@ -48,6 +48,7 @@ router.post(
|
||||
}),
|
||||
param("workspaceId").exists().trim(),
|
||||
body("name").exists().trim(),
|
||||
body("tagColor").exists().trim(),
|
||||
body("slug").exists().trim(),
|
||||
validateRequest,
|
||||
tagController.createWorkspaceTag
|
||||
|
@ -9,7 +9,7 @@ const router = express.Router();
|
||||
router.post(
|
||||
"/login1",
|
||||
authLimiter,
|
||||
body("email").isString().trim(),
|
||||
body("email").isString().trim().toLowerCase(),
|
||||
body("providerAuthToken").isString().trim().optional({nullable: true}),
|
||||
body("clientPublicKey").isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
@ -19,7 +19,7 @@ router.post(
|
||||
router.post(
|
||||
"/login2",
|
||||
authLimiter,
|
||||
body("email").isString().trim(),
|
||||
body("email").isString().trim().toLowerCase(),
|
||||
body("providerAuthToken").isString().trim().optional({nullable: true}),
|
||||
body("clientProof").isString().trim().notEmpty(),
|
||||
validateRequest,
|
||||
|
@ -54,6 +54,14 @@ export const getAllImportedSecrets = async (
|
||||
type: "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "tags", // note this is the name of the collection in the database, not the Mongoose model name
|
||||
localField: "tags",
|
||||
foreignField: "_id",
|
||||
as: "tags"
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
|
@ -2,4 +2,6 @@ export enum AuthMode {
|
||||
JWT = "jwt",
|
||||
SERVICE_TOKEN = "serviceToken",
|
||||
API_KEY = "apiKey"
|
||||
}
|
||||
}
|
||||
|
||||
export const K8_USER_AGENT_NAME = "k8-operator"
|
@ -111,14 +111,14 @@ frontend:
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/frontend
|
||||
tag: "v0.1.3"
|
||||
tag: "v0.26.0" # <--- frontend version
|
||||
pullPolicy: Always
|
||||
|
||||
backend:
|
||||
replicaCount: 2
|
||||
image:
|
||||
repository: infisical/backend
|
||||
tag: "v0.1.3"
|
||||
tag: "v0.26.0" # <--- backend version
|
||||
pullPolicy: Always
|
||||
|
||||
backendEnvironmentVariables:
|
||||
@ -126,7 +126,7 @@ backendEnvironmentVariables:
|
||||
|
||||
ingress:
|
||||
nginx:
|
||||
enabled: false #<-- if you would like to install nginx along with Infisical
|
||||
enabled: true #<-- if you would like to install nginx along with Infisical
|
||||
|
||||
```
|
||||
|
||||
@ -217,4 +217,4 @@ Once installation is complete, you will have to create the first account. No def
|
||||
</Info>
|
||||
|
||||
## Related blogs
|
||||
- [Set up Infisical in a development cluster](https://iamunnip.hashnode.dev/infisical-open-source-secretops-kubernetes-setup)
|
||||
- [Set up Infisical in a development cluster](https://iamunnip.hashnode.dev/infisical-open-source-secretops-kubernetes-setup)
|
||||
|
@ -1949,6 +1949,8 @@ paths:
|
||||
properties:
|
||||
name:
|
||||
example: any
|
||||
tagColor:
|
||||
example: any
|
||||
slug:
|
||||
example: any
|
||||
/api/v2/workspace/tags/{tagId}:
|
||||
|
@ -0,0 +1,78 @@
|
||||
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Checkbox, PopoverContent } from "@app/components/v2";
|
||||
|
||||
import { WsTag } from "../../hooks/api/tags/types";
|
||||
|
||||
interface Props {
|
||||
wsTags: WsTag[] | undefined;
|
||||
secKey: string;
|
||||
selectedTagIds: Record<string, boolean>;
|
||||
handleSelectTag: (wsTag: WsTag) => void;
|
||||
handleTagOnMouseEnter: (wsTag: WsTag) => void;
|
||||
handleTagOnMouseLeave: () => void;
|
||||
checkIfTagIsVisible: (wsTag: WsTag) => boolean;
|
||||
handleOnCreateTagOpen: () => void
|
||||
}
|
||||
|
||||
const AddTagPopoverContent = ({
|
||||
wsTags,
|
||||
secKey,
|
||||
selectedTagIds,
|
||||
handleSelectTag,
|
||||
handleTagOnMouseEnter,
|
||||
handleTagOnMouseLeave,
|
||||
checkIfTagIsVisible,
|
||||
handleOnCreateTagOpen
|
||||
}: Props) => {
|
||||
return (
|
||||
<PopoverContent
|
||||
side="left"
|
||||
className="relative max-h-96 w-auto min-w-[200px] p-2 overflow-y-auto overflow-x-hidden border border-mineshaft-600 bg-mineshaft-800 text-bunker-200"
|
||||
hideCloseBtn
|
||||
>
|
||||
<div className=" text-center text-sm font-medium text-bunker-200">
|
||||
Add tags to {secKey || "this secret"}
|
||||
</div>
|
||||
<div className="absolute left-0 w-full border-mineshaft-600 border-t mt-2" />
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
{wsTags?.map((wsTag: WsTag) => (
|
||||
<div key={`tag-${wsTag._id}`} className="mt-4 h-[32px] relative flex items-center justify-start hover:border-mineshaft-600 hover:border hover:bg-mineshaft-700 p-2 rounded-md hover:text-bunker-200 bg-none"
|
||||
onClick={() => handleSelectTag(wsTag)}
|
||||
onMouseEnter={() => handleTagOnMouseEnter(wsTag)}
|
||||
onMouseLeave={() => handleTagOnMouseLeave()}
|
||||
tabIndex={0} role="button"
|
||||
onKeyDown={() => { }}>
|
||||
{
|
||||
|
||||
(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && <Checkbox
|
||||
id="autoCapitalization"
|
||||
isChecked={selectedTagIds?.[wsTag.slug]}
|
||||
className="absolute top-[50%] translate-y-[-50%] left-[10px] "
|
||||
checkIndicatorBg={`${!selectedTagIds?.[wsTag.slug] ? "text-transparent" : "text-mineshaft-800"}`}
|
||||
/>
|
||||
}
|
||||
<div className="ml-7 flex items-center gap-3">
|
||||
<div className="w-[10px] h-[10px] rounded-full" style={{ background: wsTag?.tagColor ? wsTag.tagColor : "#bec2c8" }}> </div>
|
||||
<span >
|
||||
{wsTag.slug}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className="h-[32px] relative flex items-center cursor-pointer justify-start border-mineshaft-600 border bg-mineshaft-700 p-2 rounded-md hover:text-bunker-200 bg-none"
|
||||
onClick={() => handleOnCreateTagOpen()}
|
||||
tabIndex={0} role="button"
|
||||
onKeyDown={() => { }}>
|
||||
<FontAwesomeIcon icon={faPlus} className="ml-1 mr-2" />
|
||||
<span> Add new tag</span>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddTagPopoverContent
|
5
frontend/src/components/utilities/isValidHexColor.ts
Normal file
5
frontend/src/components/utilities/isValidHexColor.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const isValidHexColor = (hexColor: string) => {
|
||||
const hexColorPattern = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
|
||||
|
||||
return hexColorPattern.test(hexColor);
|
||||
}
|
@ -8,11 +8,12 @@ export type CheckboxProps = Omit<
|
||||
CheckboxPrimitive.CheckboxProps,
|
||||
"checked" | "disabled" | "required"
|
||||
> & {
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
id: string;
|
||||
isDisabled?: boolean;
|
||||
isChecked?: boolean;
|
||||
isRequired?: boolean;
|
||||
checkIndicatorBg?: string | undefined;
|
||||
};
|
||||
|
||||
export const Checkbox = ({
|
||||
@ -22,6 +23,7 @@ export const Checkbox = ({
|
||||
isChecked,
|
||||
isDisabled,
|
||||
isRequired,
|
||||
checkIndicatorBg,
|
||||
...props
|
||||
}: CheckboxProps): JSX.Element => {
|
||||
return (
|
||||
@ -39,7 +41,7 @@ export const Checkbox = ({
|
||||
{...props}
|
||||
id={id}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className="text-bunker-800">
|
||||
<CheckboxPrimitive.Indicator className={`${checkIndicatorBg || "text-bunker-800"}`}>
|
||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
|
@ -1,19 +1,14 @@
|
||||
import { ReactNode } from "react";
|
||||
import { faClose } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { cva, VariantProps } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onClose?: () => void;
|
||||
color?: string;
|
||||
isDisabled?: boolean;
|
||||
} & VariantProps<typeof tagVariants>;
|
||||
|
||||
const tagVariants = cva(
|
||||
"inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200",
|
||||
"inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 rounded-[30px] text-gray-400 ",
|
||||
{
|
||||
variants: {
|
||||
colorSchema: {
|
||||
@ -32,25 +27,10 @@ export const Tag = ({
|
||||
children,
|
||||
className,
|
||||
colorSchema = "gray",
|
||||
color,
|
||||
isDisabled,
|
||||
size = "sm",
|
||||
onClose
|
||||
}: Props) => (
|
||||
size = "sm" }: Props) => (
|
||||
<div
|
||||
className={twMerge(tagVariants({ colorSchema, className, size }))}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{children}
|
||||
{onClose && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isDisabled}
|
||||
className="ml-2 flex items-center justify-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -51,3 +51,69 @@ const plansProd: Mapping = {
|
||||
export const plans = plansProd || plansDev;
|
||||
|
||||
export const leaveConfirmDefaultMessage = "Your changes will be lost if you leave the page. Are you sure you want to continue?";
|
||||
|
||||
export const secretTagsColors = [
|
||||
{
|
||||
id: 1,
|
||||
hex: "#bec2c8",
|
||||
rgba: "rgb(128,128,128, 0.8)",
|
||||
name: "Grey",
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hex: "#95a2b3",
|
||||
rgba: "rgb(0,0,255, 0.8)",
|
||||
name: "blue",
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
hex: "#5e6ad2",
|
||||
rgba: "rgb(128,0,128, 0.8)",
|
||||
name: "Purple",
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
hex: "#26b5ce",
|
||||
rgba: "rgb(0,128,128, 0.8)",
|
||||
name: "Teal",
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
hex: "#4cb782",
|
||||
rgba: "rgb(0,128,0, 0.8)",
|
||||
name: "Green",
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
hex: "#f2c94c",
|
||||
rgba: "rgb(255,255,0, 0.8)",
|
||||
name: "Yellow",
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
hex: "#f2994a",
|
||||
rgba: "rgb(128,128,0, 0.8)",
|
||||
name: "Orange",
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
hex: "#f7c8c1",
|
||||
rgba: "rgb(128,0,0, 0.8)",
|
||||
name: "Pink",
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
hex: "#eb5757",
|
||||
rgba: "rgb(255,0,0, 0.8)",
|
||||
name: "Red",
|
||||
selected: false
|
||||
},
|
||||
]
|
@ -38,7 +38,7 @@ const fetchProjectEncryptedSecrets = async (
|
||||
folderId?: string,
|
||||
secretPath?: string
|
||||
) => {
|
||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v2/secrets", {
|
||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v3/secrets", {
|
||||
params: {
|
||||
environment: env,
|
||||
workspaceId,
|
||||
@ -46,6 +46,7 @@ const fetchProjectEncryptedSecrets = async (
|
||||
secretPath
|
||||
}
|
||||
});
|
||||
|
||||
return data.secrets;
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
CreateTagRes,
|
||||
DeleteTagDTO,
|
||||
DeleteWsTagRes,
|
||||
UserWsTags
|
||||
UserWsTags,
|
||||
} from "./types";
|
||||
|
||||
const workspaceTags = {
|
||||
@ -30,13 +30,15 @@ export const useGetWsTags = (workspaceID: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const useCreateWsTag = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<CreateTagRes, {}, CreateTagDTO>({
|
||||
mutationFn: async ({ workspaceID, tagName, tagSlug }) => {
|
||||
mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }) => {
|
||||
const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, {
|
||||
name: tagName,
|
||||
tagColor: tagColor || "",
|
||||
slug: tagSlug
|
||||
})
|
||||
return data;
|
||||
@ -47,6 +49,7 @@ export const useCreateWsTag = () => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useDeleteWsTag = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
@ -4,6 +4,7 @@ export type WsTag = {
|
||||
_id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
tagColor?: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@ -16,6 +17,7 @@ export type CreateTagDTO = {
|
||||
workspaceID: string;
|
||||
tagSlug: string;
|
||||
tagName: string;
|
||||
tagColor: string;
|
||||
};
|
||||
|
||||
export type CreateTagRes = {
|
||||
@ -23,6 +25,7 @@ export type CreateTagRes = {
|
||||
slug: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
tagColor?: string;
|
||||
user: string;
|
||||
_id: string;
|
||||
};
|
||||
@ -36,4 +39,19 @@ export type DeleteWsTagRes = {
|
||||
createdAt: string;
|
||||
user: string;
|
||||
_id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type SecretTags = {
|
||||
id: string;
|
||||
_id: string;
|
||||
slug: string;
|
||||
tagColor: string;
|
||||
}
|
||||
|
||||
export type TagColor = {
|
||||
id: number;
|
||||
hex: string
|
||||
rgba: string
|
||||
name: string
|
||||
selected: boolean
|
||||
}
|
@ -107,6 +107,31 @@
|
||||
@apply bg-primary-400;
|
||||
}
|
||||
}
|
||||
.tags-conic-bg {
|
||||
background: conic-gradient(rgb(235, 87, 87), rgb(242, 201, 76), rgb(76, 183, 130), rgb(78, 167, 252), rgb(250, 96, 122));
|
||||
}
|
||||
|
||||
.show-tags {
|
||||
transform: translateY(10px);
|
||||
transition: all 0.2s;
|
||||
opacity: 1;
|
||||
}
|
||||
.hide-tags {
|
||||
transform: translateY(-20px);
|
||||
transition: all 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.show-hex-input {
|
||||
transform: translateY(-33px);
|
||||
transition: all 0.2s;
|
||||
opacity: 1;
|
||||
}
|
||||
.hide-hex-input {
|
||||
transform: translateY(20px);
|
||||
transition: all 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@import "@fontsource/inter/400.css";
|
||||
@import "@fontsource/inter/500.css";
|
||||
|
@ -297,6 +297,7 @@ export const DashboardPage = () => {
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
@ -513,11 +514,12 @@ export const DashboardPage = () => {
|
||||
}, []);
|
||||
|
||||
const onCreateWsTag = useCallback(
|
||||
async (tagName: string) => {
|
||||
async (tagName: string, tagColor: string) => {
|
||||
try {
|
||||
await createWsTag({
|
||||
workspaceID: workspaceId,
|
||||
tagName,
|
||||
tagColor,
|
||||
tagSlug: tagName.replace(" ", "_")
|
||||
});
|
||||
handlePopUpClose("addTag");
|
||||
|
@ -58,7 +58,8 @@ const secretSchema = yup.object({
|
||||
yup.object({
|
||||
_id: yup.string().required(),
|
||||
name: yup.string().required(),
|
||||
slug: yup.string().required()
|
||||
slug: yup.string().required(),
|
||||
tagColor: yup.string().nullable(),
|
||||
})
|
||||
),
|
||||
overrideAction: yup.string().notRequired().oneOf(Object.values(SecretActionType)),
|
||||
|
@ -1,11 +1,21 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
faCheck
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose } from "@app/components/v2";
|
||||
import { Button, FormControl, Input, ModalClose, Tooltip } from "@app/components/v2";
|
||||
|
||||
import { isValidHexColor } from "../../../../components/utilities/isValidHexColor";
|
||||
import { secretTagsColors } from "../../../../const"
|
||||
import { TagColor } from "../../../../hooks/api/tags/types";
|
||||
|
||||
|
||||
type Props = {
|
||||
onCreateTag: (tagName: string) => Promise<void>;
|
||||
onCreateTag: (tagName: string, tagColor: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const createTagSchema = yup.object({
|
||||
@ -23,11 +33,62 @@ export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => {
|
||||
resolver: yupResolver(createTagSchema)
|
||||
});
|
||||
|
||||
const [tagsColors] = useState<TagColor[]>(secretTagsColors)
|
||||
const [selectedTagColor, setSelectedTagColor] = useState<TagColor>(tagsColors[0])
|
||||
const [showHexInput, setShowHexInput] = useState<boolean>(false)
|
||||
const [tagColor, setTagColor] = useState<string>("")
|
||||
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
await onCreateTag(name);
|
||||
await onCreateTag(name, tagColor);
|
||||
reset();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const clonedTagColors = [...tagsColors]
|
||||
const selectedTagBgColor = clonedTagColors.find($tagColor => $tagColor.selected);
|
||||
|
||||
if (selectedTagBgColor) {
|
||||
setSelectedTagColor(selectedTagBgColor);
|
||||
setTagColor(selectedTagBgColor.hex);
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const tagsList = document.querySelector(".secret-tags-wrapper")
|
||||
const tagsHexWrapper = document.querySelector(".tags-hex-wrapper")
|
||||
|
||||
if (showHexInput) {
|
||||
tagsList?.classList.add("hide-tags")
|
||||
tagsList?.classList.remove("show-tags")
|
||||
tagsHexWrapper?.classList.add("show-hex-input")
|
||||
tagsHexWrapper?.classList.remove("hide-hex-input")
|
||||
} else {
|
||||
tagsList?.classList.remove("hide-tags")
|
||||
tagsList?.classList.add("show-tags")
|
||||
tagsHexWrapper?.classList.remove("show-hex-input")
|
||||
tagsHexWrapper?.classList.add("hide-hex-input")
|
||||
}
|
||||
}, [showHexInput])
|
||||
|
||||
|
||||
const handleColorChange = (clickedTagColor: TagColor) => {
|
||||
const updatedTagColors = [...tagsColors];
|
||||
const clickedTagColorIndex = updatedTagColors.findIndex(($tagColor) => $tagColor.id === clickedTagColor.id);
|
||||
const updatedClickedTagColor = updatedTagColors[clickedTagColorIndex];
|
||||
|
||||
updatedTagColors.forEach((tgColor) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
tgColor.selected = false;
|
||||
});
|
||||
|
||||
if (selectedTagColor.id !== clickedTagColor.id) {
|
||||
updatedClickedTagColor.selected = !updatedClickedTagColor.selected;
|
||||
setSelectedTagColor(updatedClickedTagColor);
|
||||
setTagColor(updatedClickedTagColor.hex);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
@ -40,6 +101,81 @@ export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="mb-0.5 ml-1 block text-sm font-normal text-mineshaft-400">Tag Color</div>
|
||||
<div className="flex gap-2 h-[50px]">
|
||||
<div className="w-[12%] h-[2.813rem] inline-flex font-inter items-center justify-center border relative rounded-md border-mineshaft-500 bg-mineshaft-900 hover:bg-mineshaft-800">
|
||||
<div className="w-[26px] h-[26px] rounded-full" style={{ background: `${tagColor}` }} />
|
||||
</div>
|
||||
|
||||
<div className="w-[88%] h-[2.813rem] flex-wrap inline-flex gap-3 items-center border rounded-md border-mineshaft-500 bg-mineshaft-900 hover:bg-mineshaft-800 relative">
|
||||
<div className="flex-wrap inline-flex gap-3 items-center secret-tags-wrapper pl-3">
|
||||
{
|
||||
tagsColors.map(($tagColor: TagColor) => {
|
||||
return (
|
||||
<div key={`tag-color-${$tagColor.id}`}>
|
||||
<Tooltip content={`${$tagColor.name}`}>
|
||||
<div className=" flex items-center justify-center w-[26px] h-[26px] hover:ring-offset-2 hover:ring-2 bg-[#bec2c8] border-2 p-2 hover:shadow-lg border-transparent hover:border-black rounded-full"
|
||||
key={`tag-${$tagColor.id}`}
|
||||
style={{ backgroundColor: `${$tagColor.hex}` }}
|
||||
|
||||
onClick={() => handleColorChange($tagColor)}
|
||||
tabIndex={0} role="button"
|
||||
onKeyDown={() => { }}
|
||||
>
|
||||
{
|
||||
$tagColor.selected && <FontAwesomeIcon icon={faCheck} style={{ color: "#00000070" }} />
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 px-2 tags-hex-wrapper" >
|
||||
<div className="w-1/6 flex items-center relative rounded-md hover:bg-mineshaft-800">
|
||||
{
|
||||
isValidHexColor(tagColor) && (
|
||||
<div className="w-[26px] h-[26px] rounded-full flex items-center justify-center" style={{ background: `${tagColor}` }}>
|
||||
<FontAwesomeIcon icon={faCheck} style={{ color: "#00000070" }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!isValidHexColor(tagColor) && (
|
||||
<div className="border-dashed border bg-blue rounded-full w-[26px] h-[26px] border-mineshaft-500" />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="w-10/12">
|
||||
<Input
|
||||
variant="plain"
|
||||
className="w-full focus:text-bunker-100 focus:ring-transparent bg-transparent"
|
||||
autoCapitalization={false}
|
||||
value={tagColor}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTagColor(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[26px] h-[26px] flex items-center justify-center absolute top-[10px] right-[-4px] translate-x-[-50%]">
|
||||
<div className="border-mineshaft-500 border h-[2.1rem] mr-4 absolute right-5" />
|
||||
<div className={`flex items-center justify-center w-[26px] h-[26px] bg-transparent cursor-pointer hover:ring-offset-1 hover:ring-2 border-mineshaft-500 border bg-mineshaft-900 rounded-[3px] p-2 ${showHexInput ? "tags-conic-bg rounded-full" : ""}`} onClick={() => setShowHexInput((prev) => !prev)} style={{ border: "1px solid rgba(220, 216, 254, 0.376)" }}
|
||||
tabIndex={0} role="button"
|
||||
onKeyDown={() => { }}>
|
||||
{
|
||||
!showHexInput && <span>#</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||
Create
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { memo, useEffect,useRef, useState } from "react";
|
||||
import {
|
||||
Control,
|
||||
Controller,
|
||||
@ -15,7 +15,6 @@ import {
|
||||
faCopy,
|
||||
faEllipsis,
|
||||
faInfoCircle,
|
||||
faPlus,
|
||||
faTags,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@ -24,38 +23,22 @@ import { cx } from "cva";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
IconButton,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
SecretInput,
|
||||
Tag,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
Tooltip} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
|
||||
import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent";
|
||||
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
||||
|
||||
const tagColors = [
|
||||
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
|
||||
{ bg: "bg-[#cb1c8d]/40", text: "text-[#f2c6e3]/70" },
|
||||
{ bg: "bg-[#badc58]/40", text: "text-[#eef6d5]/70" },
|
||||
{ bg: "bg-[#ff5400]/40", text: "text-[#ffddcc]/70" },
|
||||
{ bg: "bg-[#3AB0FF]/40", text: "text-[#f0fffd]/70" },
|
||||
{ bg: "bg-[#6F1AB6]/40", text: "text-[#FFE5F1]/70" },
|
||||
{ bg: "bg-[#C40B13]/40", text: "text-[#FFDEDE]/70" },
|
||||
{ bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" }
|
||||
];
|
||||
|
||||
type Props = {
|
||||
index: number;
|
||||
// backend generated unique id
|
||||
@ -95,12 +78,12 @@ export const SecretInputRow = memo(
|
||||
onSecretDelete,
|
||||
searchTerm,
|
||||
control,
|
||||
register,
|
||||
// register,
|
||||
setValue,
|
||||
isKeyError,
|
||||
keyError,
|
||||
secUniqId,
|
||||
autoCapitalization
|
||||
autoCapitalization,
|
||||
}: Props): JSX.Element => {
|
||||
const isKeySubDisabled = useRef<boolean>(false);
|
||||
// comment management in a row
|
||||
@ -110,10 +93,8 @@ export const SecretInputRow = memo(
|
||||
append
|
||||
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
||||
|
||||
const tagColorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]]))
|
||||
|
||||
// display the tags in alphabetical order
|
||||
secretTags.sort((a, b) => a.name.localeCompare(b.name))
|
||||
secretTags.sort((a, b) => a?.name?.localeCompare(b?.name))
|
||||
|
||||
// to get details on a secret
|
||||
const overrideAction = useWatch({
|
||||
@ -145,11 +126,24 @@ export const SecretInputRow = memo(
|
||||
// when secret is override by personal values
|
||||
const isOverridden =
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
|
||||
|
||||
const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue);
|
||||
const [hoveredTag, setHoveredTag] = useState<WsTag | null>(null);
|
||||
|
||||
const handleTagOnMouseEnter = (wsTag: WsTag) => {
|
||||
setHoveredTag(wsTag);
|
||||
}
|
||||
|
||||
const handleTagOnMouseLeave = () => {
|
||||
setHoveredTag(null);
|
||||
}
|
||||
|
||||
const checkIfTagIsVisible = (wsTag: WsTag) => wsTag._id === hoveredTag?._id;
|
||||
|
||||
const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true });
|
||||
const tags =
|
||||
useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || [];
|
||||
const tags = useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || [];
|
||||
|
||||
const selectedTagIds = tags.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.slug]: true }),
|
||||
{}
|
||||
@ -157,6 +151,7 @@ export const SecretInputRow = memo(
|
||||
|
||||
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isInviteLinkCopied) {
|
||||
@ -165,6 +160,7 @@ export const SecretInputRow = memo(
|
||||
return () => clearTimeout(timer);
|
||||
}, [isInviteLinkCopied]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setEditorRef(isOverridden ? secValueOverride : secValue);
|
||||
}, [isOverridden]);
|
||||
@ -195,13 +191,13 @@ export const SecretInputRow = memo(
|
||||
const onSelectTag = (selectedTag: WsTag) => {
|
||||
const shouldAppend = !selectedTagIds[selectedTag.slug];
|
||||
if (shouldAppend) {
|
||||
append(selectedTag);
|
||||
const {_id: id, name, slug, tagColor} = selectedTag
|
||||
append({_id: id, name, slug, tagColor});
|
||||
} else {
|
||||
const pos = tags.findIndex(({ slug }) => selectedTag.slug === slug);
|
||||
const pos = tags.findIndex(({ slug }: { slug: string }) => selectedTag.slug === slug);
|
||||
remove(pos);
|
||||
}
|
||||
};
|
||||
|
||||
const isCreatedSecret = !secId;
|
||||
const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly;
|
||||
|
||||
@ -228,6 +224,7 @@ export const SecretInputRow = memo(
|
||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
||||
</td>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
@ -326,21 +323,38 @@ export const SecretInputRow = memo(
|
||||
</td>
|
||||
<td className="min-w-sm flex">
|
||||
<div className="flex h-8 items-center pl-2">
|
||||
{secretTags.map(({ id, _id, slug }, i) => {
|
||||
// This map lookup shouldn't ever fail, but if it does we default to the first color
|
||||
const tagColor = tagColorByTagId.get(_id) || tagColors[0]
|
||||
{secretTags.map(({ id, slug, tagColor}) => {
|
||||
return (
|
||||
<Tag
|
||||
className={cx(
|
||||
tagColor.bg,
|
||||
tagColor.text
|
||||
)}
|
||||
isDisabled={isReadOnly || isAddOnly || isRollbackMode}
|
||||
onClose={() => remove(i)}
|
||||
key={id}
|
||||
>
|
||||
{slug}
|
||||
</Tag>)
|
||||
<>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<Tag
|
||||
// isDisabled={isReadOnly || isAddOnly || isRollbackMode}
|
||||
// onClose={() => remove(i)}
|
||||
key={id}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="rounded-full border-mineshaft-500 bg-transparent flex items-center gap-1.5 justify-around">
|
||||
<div className="w-[10px] h-[10px] rounded-full" style={{ background: tagColor || "#bec2c8" }} />
|
||||
{slug}
|
||||
</div>
|
||||
</Tag>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<AddTagPopoverContent
|
||||
wsTags={wsTags}
|
||||
secKey={secKey || "this secret"}
|
||||
selectedTagIds={selectedTagIds}
|
||||
handleSelectTag={(wsTag: WsTag) => onSelectTag(wsTag)}
|
||||
handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)}
|
||||
handleTagOnMouseLeave={() => handleTagOnMouseLeave()}
|
||||
checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)}
|
||||
handleOnCreateTagOpen={() => onCreateTagOpen()}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
<div className="w-0 overflow-hidden group-hover:w-6">
|
||||
<Tooltip content="Copy value">
|
||||
@ -372,51 +386,16 @@ export const SecretInputRow = memo(
|
||||
</Tooltip>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="left"
|
||||
className="max-h-96 w-auto min-w-[200px] overflow-y-auto overflow-x-hidden border border-mineshaft-600 bg-mineshaft-800 p-2 text-bunker-200"
|
||||
hideCloseBtn
|
||||
>
|
||||
<div className="mb-2 px-2 text-center text-sm font-medium text-bunker-200">
|
||||
Add tags to {secKey || "this secret"}
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
{wsTags?.map((wsTag) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"justify-start bg-mineshaft-600 text-bunker-100 hover:bg-mineshaft-500",
|
||||
selectedTagIds?.[wsTag.slug] && "text-primary"
|
||||
)}
|
||||
onClick={() => onSelectTag(wsTag)}
|
||||
leftIcon={
|
||||
<Checkbox
|
||||
className="mr-0 data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isChecked={selectedTagIds?.[wsTag.slug]}
|
||||
onCheckedChange={() => {}}
|
||||
>
|
||||
{}
|
||||
</Checkbox>
|
||||
}
|
||||
key={wsTag._id}
|
||||
>
|
||||
{wsTag.slug}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="star"
|
||||
color="primary"
|
||||
size="sm"
|
||||
className="mt-4 h-7 justify-start bg-mineshaft-600 px-1"
|
||||
onClick={onCreateTagOpen}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add new tag
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
<AddTagPopoverContent
|
||||
wsTags={wsTags}
|
||||
secKey={secKey || "this secret"}
|
||||
selectedTagIds={selectedTagIds}
|
||||
handleSelectTag={(wsTag: WsTag) => onSelectTag(wsTag)}
|
||||
handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)}
|
||||
handleTagOnMouseLeave={() => handleTagOnMouseLeave()}
|
||||
checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)}
|
||||
handleOnCreateTagOpen={() => onCreateTagOpen()}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
@ -460,20 +439,16 @@ export const SecretInputRow = memo(
|
||||
<FontAwesomeIcon icon={faComment} />
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
|
||||
sticky="always"
|
||||
>
|
||||
<FormControl label="Comment" className="mb-0">
|
||||
<TextArea
|
||||
isDisabled={isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly}
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
{...register(`secrets.${index}.comment`)}
|
||||
rows={8}
|
||||
cols={30}
|
||||
/>
|
||||
</FormControl>
|
||||
</PopoverContent>
|
||||
<AddTagPopoverContent
|
||||
wsTags={wsTags}
|
||||
secKey={secKey || "this secret"}
|
||||
selectedTagIds={selectedTagIds}
|
||||
handleSelectTag={(wsTag: WsTag) => onSelectTag(wsTag)}
|
||||
handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)}
|
||||
handleTagOnMouseLeave={() => handleTagOnMouseLeave()}
|
||||
checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)}
|
||||
handleOnCreateTagOpen={() => onCreateTagOpen()}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
@ -51,7 +51,7 @@ export const InitialStep = ({
|
||||
|
||||
// attemptCliLogin
|
||||
const isCliLoginSuccessful = await attemptCliLogin({
|
||||
email,
|
||||
email: email.toLowerCase(),
|
||||
password,
|
||||
})
|
||||
|
||||
@ -78,7 +78,7 @@ export const InitialStep = ({
|
||||
}
|
||||
} else {
|
||||
const isLoginSuccessful = await attemptLogin({
|
||||
email,
|
||||
email: email.toLowerCase(),
|
||||
password,
|
||||
});
|
||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||
|
@ -60,7 +60,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const addMemberFormSchema = yup.object({
|
||||
email: yup.string().email().required().label("Email").trim()
|
||||
email: yup.string().email().required().label("Email").trim().lowercase()
|
||||
});
|
||||
|
||||
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
||||
|
@ -1,13 +1,25 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LogsFilter } from "./LogsFilter";
|
||||
import { LogsTable } from "./LogsTable";
|
||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
||||
|
||||
export const LogsSection = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const router = useRouter();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
@ -20,6 +32,12 @@ export const LogsSection = () => {
|
||||
perPage: 10
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.auditLogs) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
const eventType = watch("eventType") as EventType | undefined;
|
||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||
@ -54,6 +72,19 @@ export const LogsSection = () => {
|
||||
perPage={perPage}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
|
||||
if (!isOpen) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpToggle("upgradePlan", isOpen)
|
||||
}}
|
||||
text="You can use audit logs if you switch to a paid Infisical plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faFolderBlank, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowDown, faArrowUp, faFolderBlank, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
@ -10,6 +10,7 @@ import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Table,
|
||||
TableContainer,
|
||||
@ -46,6 +47,7 @@ export const SecretOverviewPage = () => {
|
||||
// coz when overflow the table goes to the right
|
||||
const parentTableRef = useRef<HTMLTableElement>(null);
|
||||
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
useEffect(() => {
|
||||
const handleParentTableWidthResize = () => {
|
||||
@ -219,7 +221,7 @@ export const SecretOverviewPage = () => {
|
||||
|
||||
const filteredSecretNames = secKeys?.filter((name) =>
|
||||
name.toUpperCase().includes(searchFilter.toUpperCase())
|
||||
);
|
||||
).sort((a, b) => sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a));
|
||||
const filteredFolderNames = folderNames?.filter((name) =>
|
||||
name.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
);
|
||||
@ -278,6 +280,9 @@ export const SecretOverviewPage = () => {
|
||||
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0">
|
||||
<div className="flex items-center border-b border-r border-mineshaft-600 px-5 pt-4 pb-3.5">
|
||||
Name
|
||||
<IconButton variant="plain" className="ml-2" ariaLabel="sort" onClick={() => setSortDir(prev => prev === "asc" ? "desc" : "asc")}>
|
||||
<FontAwesomeIcon icon={sortDir === "asc" ? faArrowDown : faArrowUp} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
{userAvailableEnvs?.map(({ name, slug }, index) => {
|
||||
|
@ -53,7 +53,8 @@ export const AddSecretTagModal = ({
|
||||
await createWsTag.mutateAsync({
|
||||
workspaceID: currentWorkspace?._id,
|
||||
tagName: name,
|
||||
tagSlug: name.replace(" ", "_")
|
||||
tagSlug: name.replace(/\s+/g, " ").replace(" ", "_"),
|
||||
tagColor: ""
|
||||
});
|
||||
|
||||
handlePopUpClose("CreateSecretTag");
|
||||
@ -62,6 +63,7 @@ export const AddSecretTagModal = ({
|
||||
text: "Successfully created a tag",
|
||||
type: "success"
|
||||
});
|
||||
reset()
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
|
@ -7,7 +7,7 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.3.1
|
||||
version: 0.3.2
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
|
@ -126,4 +126,5 @@ Create the mongodb connection string.
|
||||
{{- if .Values.mongodbConnection.externalMongoDBConnectionString -}}
|
||||
{{- $connectionString = .Values.mongodbConnection.externalMongoDBConnectionString -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- printf "%s" $connectionString -}}
|
||||
{{- end -}}
|
Reference in New Issue
Block a user