diff --git a/backend/spec.json b/backend/spec.json index 013b5fd4d..1afbc8dce 100644 --- a/backend/spec.json +++ b/backend/spec.json @@ -3203,6 +3203,9 @@ "name": { "example": "any" }, + "tagColor": { + "example": "any" + }, "slug": { "example": "any" } diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index c87b3756a..473512390 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -234,6 +234,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) } @@ -966,6 +969,7 @@ export const getSecrets = async (req: Request, res: Response) => { ); const postHogClient = await TelemetryService.getPostHogClient(); + if (postHogClient) { postHogClient.capture({ event: "secrets pulled", diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 0d945c3e5..fa21fc971 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -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), diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index d63f05cf7..84174ac87 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -117,7 +117,7 @@ const secretVersionSchema = new Schema( ref: "Tag", type: [Schema.Types.ObjectId], default: [], - }, + } }, { timestamps: true, diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index d4544d32b..2947d2a8e 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -393,9 +393,10 @@ 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({ secret: secret._id, version: secret.version, @@ -415,7 +416,7 @@ export const createSecretHelper = async ({ algorithm: ALGORITHM_AES_256_GCM, keyEncoding: ENCODING_SCHEME_UTF8 }); - + // (EE) add version for new secret await EESecretService.addSecretVersions({ secretVersions: [secretVersion] @@ -566,15 +567,17 @@ export const getSecretsHelper = async ({ ); const postHogClient = await TelemetryService.getPostHogClient(); + + const numberOfSignupSecrets = (secrets.filter((secret) => secret?.metadata?.source === "signup")).length; - if (postHogClient) { + if (postHogClient && (secrets.length - numberOfSignupSecrets > 0)) { postHogClient.capture({ event: "secrets pulled", distinctId: await TelemetryService.getDistinctId({ authData }), properties: { - numberOfSecrets: secrets.length, + numberOfSecrets: secrets.length - numberOfSignupSecrets, environment, workspaceId, folderId, diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index 34a4d7501..4ef3de456 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -31,6 +31,9 @@ export interface ISecret { keyEncoding: "utf8" | "base64"; tags?: string[]; folder?: string; + metadata?: { + [key: string]: string; + } } const secretSchema = new Schema( @@ -131,6 +134,9 @@ const secretSchema = new Schema( type: String, default: "root", }, + metadata: { + type: Schema.Types.Mixed + } }, { timestamps: true, diff --git a/backend/src/models/tag.ts b/backend/src/models/tag.ts index 53bf085d3..ec649e254 100644 --- a/backend/src/models/tag.ts +++ b/backend/src/models/tag.ts @@ -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( required: true, trim: true, }, + tagColor: { + type: String, + required: false, + trim: true, + }, slug: { type: String, required: true, diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index 7ccfd17cd..aca1b6d8c 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -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 diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index 848b830de..45297bdc0 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -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 ## Related blogs -- [Set up Infisical in a development cluster](https://iamunnip.hashnode.dev/infisical-open-source-secretops-kubernetes-setup) \ No newline at end of file +- [Set up Infisical in a development cluster](https://iamunnip.hashnode.dev/infisical-open-source-secretops-kubernetes-setup) diff --git a/docs/spec.yaml b/docs/spec.yaml index 799c7f6f4..5cf129be3 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -1949,6 +1949,8 @@ paths: properties: name: example: any + tagColor: + example: any slug: example: any /api/v2/workspace/tags/{tagId}: diff --git a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx new file mode 100644 index 000000000..a8b965269 --- /dev/null +++ b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx @@ -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; + 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 ( + +
+ Add tags to {secKey || "this secret"} +
+
+
+ {wsTags?.map((wsTag: WsTag) => ( +
handleSelectTag(wsTag)} + onMouseEnter={() => handleTagOnMouseEnter(wsTag)} + onMouseLeave={() => handleTagOnMouseLeave()} + tabIndex={0} role="button" + onKeyDown={() => { }}> + { + + (checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && + } +
+
+ + {wsTag.slug} + +
+
+ ))} +
handleOnCreateTagOpen()} + tabIndex={0} role="button" + onKeyDown={() => { }}> + + Add new tag +
+
+ + ) +} + +export default AddTagPopoverContent \ No newline at end of file diff --git a/frontend/src/components/utilities/isValidHexColor.ts b/frontend/src/components/utilities/isValidHexColor.ts new file mode 100644 index 000000000..86c14b142 --- /dev/null +++ b/frontend/src/components/utilities/isValidHexColor.ts @@ -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); +} \ No newline at end of file diff --git a/frontend/src/components/v2/Checkbox/Checkbox.tsx b/frontend/src/components/v2/Checkbox/Checkbox.tsx index 3c546f067..64ec0a54a 100644 --- a/frontend/src/components/v2/Checkbox/Checkbox.tsx +++ b/frontend/src/components/v2/Checkbox/Checkbox.tsx @@ -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} > - + diff --git a/frontend/src/components/v2/Tag/Tag.tsx b/frontend/src/components/v2/Tag/Tag.tsx index 13672e7e9..10c1d6246 100644 --- a/frontend/src/components/v2/Tag/Tag.tsx +++ b/frontend/src/components/v2/Tag/Tag.tsx @@ -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; 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) => (
{children} - {onClose && ( - - )}
); diff --git a/frontend/src/const.ts b/frontend/src/const.ts index f2309df31..267d80ccd 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -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 + }, +] \ No newline at end of file diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index e1c3e10bf..bd9fee048 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -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; }; diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx index 74900da0e..7c230e02a 100644 --- a/frontend/src/hooks/api/tags/queries.tsx +++ b/frontend/src/hooks/api/tags/queries.tsx @@ -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({ - 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(); diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts index 56b7171bf..de09f5ac4 100644 --- a/frontend/src/hooks/api/tags/types.ts +++ b/frontend/src/hooks/api/tags/types.ts @@ -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; -}; \ No newline at end of file +}; + +export type SecretTags = { + id: string; + _id: string; + slug: string; + tagColor: string; +} + +export type TagColor = { + id: number; + hex: string + rgba: string + name: string + selected: boolean +} \ No newline at end of file diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index ae8d17427..6d5070fe0 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -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"; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index b83cb839d..faaaeb49c 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -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"); diff --git a/frontend/src/views/DashboardPage/DashboardPage.utils.ts b/frontend/src/views/DashboardPage/DashboardPage.utils.ts index a8e63f89f..bd0131e8e 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.utils.ts +++ b/frontend/src/views/DashboardPage/DashboardPage.utils.ts @@ -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)), diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx index 22973c1a6..b399ee886 100644 --- a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx +++ b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx @@ -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; + onCreateTag: (tagName: string, tagColor: string) => Promise; }; const createTagSchema = yup.object({ @@ -23,11 +33,62 @@ export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => { resolver: yupResolver(createTagSchema) }); + const [tagsColors] = useState(secretTagsColors) + const [selectedTagColor, setSelectedTagColor] = useState(tagsColors[0]) + const [showHexInput, setShowHexInput] = useState(false) + const [tagColor, setTagColor] = useState("") + + 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 (
{ )} /> + +
+
Tag Color
+
+
+
+
+ +
+
+ { + tagsColors.map(($tagColor: TagColor) => { + return ( +
+ +
handleColorChange($tagColor)} + tabIndex={0} role="button" + onKeyDown={() => { }} + > + { + $tagColor.selected && + } +
+
+
+ ) + }) + } +
+ +
+
+ { + isValidHexColor(tagColor) && ( +
+ +
+ ) + } + + { + !isValidHexColor(tagColor) && ( +
+ ) + } +
+
+ ) => setTagColor(e.target.value)} + /> +
+
+ +
+
+
setShowHexInput((prev) => !prev)} style={{ border: "1px solid rgba(220, 216, 254, 0.376)" }} + tabIndex={0} role="button" + onKeyDown={() => { }}> + { + !showHexInput && # + } +
+
+
+
+
+
- ))} - -
- + onSelectTag(wsTag)} + handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} + handleTagOnMouseLeave={() => handleTagOnMouseLeave()} + checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} + handleOnCreateTagOpen={() => onCreateTagOpen()} + />
)} @@ -460,20 +439,16 @@ export const SecretInputRow = memo( - - -