diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 09ff9e1081..d6cb63b1fe 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -68,7 +68,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ secretVersioning: true, pitRecovery: false, ipAllowlisting: false, - rbac: false, + rbac: true, githubOrgSync: false, customRateLimits: false, subOrganization: false, diff --git a/frontend/.storybook/decorators/RouterDecorator.tsx b/frontend/.storybook/decorators/RouterDecorator.tsx index a559c5cd1e..c44ee6b125 100644 --- a/frontend/.storybook/decorators/RouterDecorator.tsx +++ b/frontend/.storybook/decorators/RouterDecorator.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import type { Decorator } from "@storybook/react-vite"; import { createRootRoute, createRouter, RouterProvider } from "@tanstack/react-router"; -export const RouterDecorator: Decorator = (Story) => { +export const RouterDecorator: Decorator = (Story, params) => { const router = useMemo(() => { const routeTree = createRootRoute({ component: Story @@ -11,7 +11,7 @@ export const RouterDecorator: Decorator = (Story) => { return createRouter({ routeTree }); - }, [Story]); + }, [Story, params]); return ; }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e8fdda0965..bbb72bfa7c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -42,6 +42,7 @@ "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.1.3", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", @@ -136,6 +137,7 @@ "prettier": "3.4.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.14", + "tw-animate-css": "^1.4.0", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", "vite": "^6.2.0", @@ -3353,6 +3355,85 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -14777,6 +14858,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8009c2118a..075807caa8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -51,6 +51,7 @@ "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.1.3", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", @@ -145,6 +146,7 @@ "prettier": "3.4.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.14", + "tw-animate-css": "^1.4.0", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", "vite": "^6.2.0", diff --git a/frontend/src/components/v2/PageHeader/PageHeader.tsx b/frontend/src/components/v2/PageHeader/PageHeader.tsx index 5a2ba2e123..05e0c131bf 100644 --- a/frontend/src/components/v2/PageHeader/PageHeader.tsx +++ b/frontend/src/components/v2/PageHeader/PageHeader.tsx @@ -32,7 +32,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P

{children}

-
{description}
+
{description}
); diff --git a/frontend/src/components/v3/generic/Alert/Alert.tsx b/frontend/src/components/v3/generic/Alert/Alert.tsx new file mode 100644 index 0000000000..a48a4b7fb9 --- /dev/null +++ b/frontend/src/components/v3/generic/Alert/Alert.tsx @@ -0,0 +1,62 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; +import { cva, type VariantProps } from "cva"; + +import { cn } from "../../utils"; + +const alertVariants = cva( + "relative w-full rounded-sm border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-container text-card-foreground", + info: "bg-info/10 text-info border-info/20", + org: "bg-org/10 text-org border-org/20" + } + }, + defaultVariants: { + variant: "default" + } + } +); + +function UnstableAlert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function UnstableAlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableAlertDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { UnstableAlert, UnstableAlertDescription, UnstableAlertTitle }; diff --git a/frontend/src/components/v3/generic/Alert/index.ts b/frontend/src/components/v3/generic/Alert/index.ts new file mode 100644 index 0000000000..b8e17a03c9 --- /dev/null +++ b/frontend/src/components/v3/generic/Alert/index.ts @@ -0,0 +1 @@ +export * from "./Alert"; diff --git a/frontend/src/components/v3/generic/Badge/Badge.stories.tsx b/frontend/src/components/v3/generic/Badge/Badge.stories.tsx index 4cbf955f93..244bd81087 100644 --- a/frontend/src/components/v3/generic/Badge/Badge.stories.tsx +++ b/frontend/src/components/v3/generic/Badge/Badge.stories.tsx @@ -16,6 +16,7 @@ import { } from "lucide-react"; import { OrgIcon, ProjectIcon, SubOrgIcon } from "../../platform"; +import { UnstableButtonGroup } from "../ButtonGroup"; import { Badge } from "./Badge"; /** @@ -33,13 +34,34 @@ const meta = { argTypes: { variant: { control: "select", - options: ["neutral", "success", "info", "warning", "danger", "project", "org", "sub-org"] + options: [ + "default", + "outline", + "neutral", + "success", + "info", + "warning", + "danger", + "project", + "org", + "sub-org" + ] }, isTruncatable: { table: { disable: true } }, + isFullWidth: { + table: { + disable: true + } + }, + isSquare: { + table: { + disable: true + } + }, asChild: { table: { disable: true @@ -57,6 +79,38 @@ const meta = { export default meta; type Story = StoryObj; +export const Default: Story = { + name: "Variant: Default", + args: { + variant: "default", + children: <>Default + }, + parameters: { + docs: { + description: { + story: + "Use this variant when other badge variants are not applicable or as the key when displaying key-value pairs with ButtonGroup." + } + } + } +}; + +export const Outline: Story = { + name: "Variant: Outline", + args: { + variant: "outline", + children: <>Outline + }, + parameters: { + docs: { + description: { + story: + "Use this variant when other badge variants are not applicable or as the value when displaying key-value pairs with ButtonGroup." + } + } + } +}; + export const Neutral: Story = { name: "Variant: Neutral", args: { @@ -71,8 +125,7 @@ export const Neutral: Story = { parameters: { docs: { description: { - story: - "Use this variant when indicating neutral or disabled states or when linking to external documents." + story: "Use this variant when indicating neutral or disabled states." } } } @@ -133,7 +186,8 @@ export const Info: Story = { parameters: { docs: { description: { - story: "Use this variant when indicating informational states." + story: + "Use this variant when indicating informational states or when linking to external documentation." } } } @@ -374,3 +428,22 @@ export const IsFullWidth: Story = {
) }; + +export const KeyValuePair: Story = { + name: "Example: Key-Value Pair", + args: {}, + parameters: { + docs: { + description: { + story: + "Use a default and outline badge in conjunction with the `` component to display key-value pairs." + } + } + }, + decorators: () => ( + + Key + Value + + ) +}; diff --git a/frontend/src/components/v3/generic/Badge/Badge.tsx b/frontend/src/components/v3/generic/Badge/Badge.tsx index f94bff5f2e..9dae877abe 100644 --- a/frontend/src/components/v3/generic/Badge/Badge.tsx +++ b/frontend/src/components/v3/generic/Badge/Badge.tsx @@ -6,7 +6,7 @@ import { cn } from "@app/components/v3/utils"; const badgeVariants = cva( [ - "select-none items-center align-middle rounded-sm h-4.5 px-1.5 text-xs", + "select-none border items-center align-middle rounded-sm h-4.5 px-1.5 text-xs", "gap-x-1 [a&,button&]:cursor-pointer inline-flex font-normal", "[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&_svg:not([class*='size-'])]:size-3", "transition duration-200 ease-in-out" @@ -24,19 +24,22 @@ const badgeVariants = cva( true: "w-4.5 justify-center px-0.5" }, variant: { - ghost: "text-mineshaft-200 gap-x-2", - neutral: "bg-neutral/25 text-neutral [a&,button&]:hover:bg-neutral/35", - success: "bg-success/25 text-success [a&,button&]:hover:bg-success/35", - info: "bg-info/25 text-info [a&,button&]:hover:bg-info/35", - warning: "bg-warning/25 text-warning [a&,button&]:hover:bg-warning/35", - danger: "bg-danger/25 text-danger [a&,button&]:hover:bg-danger/35", - project: "bg-project/25 text-project [a&,button&]:hover:bg-project/35", - org: "bg-org/25 text-org [a&,button&]:hover:bg-org/35", - "sub-org": "bg-sub-org/25 text-sub-org [a&,button&]:hover:bg-sub-org/35" + ghost: "text-foreground border-none", + default: "bg-foreground text-background [a&,button&]:hover:bg-primary/35", + outline: "text-foreground border-foreground border", + neutral: "bg-neutral/15 border-neutral/10 text-neutral [a&,button&]:hover:bg-neutral/35", + success: "bg-success/15 border-success/10 text-success [a&,button&]:hover:bg-success/35", + info: "bg-info/15 border-info/10 border text-info [a&,button&]:hover:bg-info/35", + warning: "bg-warning/15 border-warning/10 text-warning [a&,button&]:hover:bg-warning/35", + danger: "bg-danger/15 border-danger/10 text-danger border [a&,button&]:hover:bg-danger/35", + project: + "bg-project/15 text-project border-project/10 border [a&,button&]:hover:bg-project/35", + org: "bg-org/15 border border-org/10 text-org [a&,button&]:hover:bg-org/35", + "sub-org": "bg-sub-org/15 border-sub-org/10 text-sub-org [a&,button&]:hover:bg-sub-org/35" } }, defaultVariants: { - variant: "neutral" + variant: "default" } } ); @@ -44,7 +47,6 @@ const badgeVariants = cva( type TBadgeProps = VariantProps & React.ComponentProps<"span"> & { asChild?: boolean; - variant: NonNullable["variant"]>; // TODO: REMOVE }; const Badge = forwardRef( diff --git a/frontend/src/components/v3/generic/Button/Button.stories.tsx b/frontend/src/components/v3/generic/Button/Button.stories.tsx new file mode 100644 index 0000000000..0f975e70de --- /dev/null +++ b/frontend/src/components/v3/generic/Button/Button.stories.tsx @@ -0,0 +1,357 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { + AsteriskIcon, + BanIcon, + CheckIcon, + CircleXIcon, + ExternalLinkIcon, + InfoIcon, + RadarIcon, + TriangleAlertIcon, + UserIcon +} from "lucide-react"; + +import { OrgIcon, ProjectIcon, SubOrgIcon } from "../../platform"; +import { UnstableButton } from "./Button"; + +/** + * Buttons act as an indicator that can optionally be made interactable. + * You can place text and icons inside a Button. + * Buttons are often used for the indication of a status, state or scope. + */ +const meta = { + title: "Generic/Button", + component: UnstableButton, + parameters: { + layout: "centered" + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: [ + "default", + "outline", + "neutral", + "success", + "info", + "warning", + "danger", + "project", + "org", + "sub-org" + ] + }, + size: { + control: "select", + options: ["xs", "sm", "md", "lg"] + }, + isPending: { + control: "boolean" + }, + isFullWidth: { + control: "boolean" + }, + isDisabled: { + control: "boolean" + }, + as: { + table: { + disable: true + } + }, + children: { + table: { + disable: true + } + } + }, + args: { children: "Button", isPending: false, isDisabled: false, isFullWidth: false, size: "md" } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + name: "Variant: Default", + args: { + variant: "default", + children: <>Default + }, + parameters: { + docs: { + description: { + story: + "Use this variant when other Button variants are not applicable or as the key when displaying key-value pairs with ButtonGroup." + } + } + } +}; + +export const Outline: Story = { + name: "Variant: Outline", + args: { + variant: "outline", + children: <>Outline + }, + parameters: { + docs: { + description: { + story: + "Use this variant when other Button variants are not applicable or as the value when displaying key-value pairs with ButtonGroup." + } + } + } +}; + +export const Neutral: Story = { + name: "Variant: Neutral", + args: { + variant: "neutral", + children: ( + <> + + Disabled + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating neutral or disabled states." + } + } + } +}; + +export const Ghost: Story = { + name: "Variant: Ghost", + args: { + variant: "ghost", + children: ( + <> + + User + + ) + }, + parameters: { + docs: { + description: { + story: + "Use this variant when indicating a configuration or property value. Avoid using this variant as an interactive element as it is not intuitive to interact with." + } + } + } +}; + +export const Success: Story = { + name: "Variant: Success", + args: { + variant: "success", + children: ( + <> + + Success + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating successful or healthy states." + } + } + } +}; + +export const Info: Story = { + name: "Variant: Info", + args: { + variant: "info", + children: ( + <> + + Info + + ) + }, + parameters: { + docs: { + description: { + story: + "Use this variant when indicating informational states or when linking to external documentation." + } + } + } +}; + +export const Warning: Story = { + name: "Variant: Warning", + args: { + variant: "warning", + children: ( + <> + + Warning + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating activity or attention warranting states." + } + } + } +}; + +export const Danger: Story = { + name: "Variant: Danger", + args: { + variant: "danger", + children: ( + <> + + Danger + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating destructive or error states." + } + } + } +}; + +export const Organization: Story = { + name: "Variant: Organization", + args: { + variant: "org", + children: ( + <> + + Organization + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating organization scope or links." + } + } + } +}; + +export const SubOrganization: Story = { + name: "Variant: Sub-Organization", + args: { + variant: "sub-org", + children: ( + <> + + Sub-Organization + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating sub-organization scope or links." + } + } + } +}; + +export const Project: Story = { + name: "Variant: Project", + args: { + variant: "project", + children: ( + <> + + Project + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating project scope or links." + } + } + } +}; + +export const AsExternalLink: Story = { + name: "Example: As External Link", + args: { + variant: "info", + as: "a", + href: "https://www.infisical.com", + children: ( + <> + Link + + ) + }, + parameters: { + docs: { + description: { + story: 'Use the `as="a"` prop to use a Button as an external `a` tag component.' + } + } + } +}; + +export const AsRouterLink: Story = { + name: "Example: As Router Link", + args: { + variant: "project", + as: "link", + children: ( + <> + + Secret Scanning + + ) + }, + parameters: { + docs: { + description: { + story: 'Use the `as="link"` prop to use a Button as an internal `Link` component.' + } + } + } +}; + +export const IsFullWidth: Story = { + name: "Example: isFullWidth", + args: { + variant: "neutral", + isFullWidth: true, + + children: ( + <> + + Secret Value + + ) + }, + parameters: { + docs: { + description: { + story: + "Use the `isFullWidth` prop to expand the Buttons width to fill it's parent container." + } + } + }, + decorators: (Story) => ( +
+ +
+ ) +}; diff --git a/frontend/src/components/v3/generic/Button/Button.tsx b/frontend/src/components/v3/generic/Button/Button.tsx new file mode 100644 index 0000000000..4c065b1d0d --- /dev/null +++ b/frontend/src/components/v3/generic/Button/Button.tsx @@ -0,0 +1,139 @@ +import * as React from "react"; +import { forwardRef } from "react"; +import { Link, LinkProps } from "@tanstack/react-router"; +import { cva, type VariantProps } from "cva"; + +import { Lottie } from "@app/components/v2"; +import { cn } from "@app/components/v3/utils"; + +const buttonVariants = cva( + cn( + "inline-flex items-center active:scale-[0.95] justify-center border cursor-pointer whitespace-nowrap", + " text-sm transition-all disabled:pointer-events-none disabled:opacity-75 shrink-0", + "[&>svg]:pointer-events-none [&>svg]:shrink-0", + "focus-visible:ring-ring outline-0 focus-visible:ring-2 select-none" + ), + { + variants: { + variant: { + default: + "border-foreground bg-foreground text-background hover:bg-foreground/90 hover:border-foreground/90", + neutral: + "border-neutral/10 bg-neutral/40 text-foreground hover:bg-neutral/50 hover:border-neutral/20", + outline: "text-foreground hover:bg-foreground/10 border-border hover:border-foreground/20", + ghost: "text-foreground hover:bg-foreground/10 border-transparent", + project: + "border-project/25 bg-project/15 text-foreground hover:bg-project/30 hover:border-project/30", + org: "border-org/25 bg-org/15 text-foreground hover:bg-org/30 hover:border-org/30", + "sub-org": + "border-sub-org/25 bg-sub-org/15 text-foreground hover:bg-sub-org/30 hover:border-sub-org/30", + success: + "border-success/25 bg-success/15 text-foreground hover:bg-success/30 hover:border-success/30", + info: "border-info/25 bg-info/15 text-foreground hover:bg-info/30 hover:border-info/30", + warning: + "border-warning/25 bg-warning/15 text-foreground hover:bg-warning/30 hover:border-warning/30", + danger: + "border-danger/25 bg-danger/15 text-foreground hover:bg-danger/30 hover:border-danger/30" + }, + size: { + xs: "h-7 px-2 rounded-[3px] text-xs [&>svg]:size-3 gap-1.5", + sm: "h-8 px-2.5 rounded-[4px] text-sm [&>svg]:size-3 gap-1.5", + md: "h-9 px-3 rounded-[5px] text-sm [&>svg]:size-3.5 gap-1.5", + lg: "h-10 px-3 rounded-[6px] text-sm [&>svg]:size-3.5 gap-1.5" + }, + isPending: { + true: "text-transparent" + }, + isFullWidth: { + true: "w-full", + false: "w-fit" + } + }, + defaultVariants: { + variant: "default", + size: "md" + } + } +); + +type UnstableButtonProps = (VariantProps & { + isPending?: boolean; + isFullWidth?: boolean; + isDisabled?: boolean; +}) & + ( + | ({ as?: "button" | undefined } & React.ComponentProps<"button">) + | ({ as: "link"; className?: string } & LinkProps) + | ({ as: "a" } & React.ComponentProps<"a">) + ); + +const UnstableButton = forwardRef( + ( + { + className, + variant = "default", + size = "md", + isPending = false, + isFullWidth = false, + isDisabled = false, + children, + ...props + }, + ref + ): JSX.Element => { + const sharedProps = { + "data-slot": "button", + className: cn(buttonVariants({ variant, size, isPending, isFullWidth }), className) + }; + + const child = ( + <> + {children} + {isPending && ( + + )} + + ); + + switch (props.as) { + case "a": + return ( + } + target="_blank" + rel="noopener noreferrer" + {...props} + {...sharedProps} + > + {child} + + ); + case "link": + return ( + } {...props} {...sharedProps}> + {child} + + ); + default: + return ( + + ); + } + } +); + +UnstableButton.displayName = "Button"; + +export { buttonVariants, UnstableButton, type UnstableButtonProps }; diff --git a/frontend/src/components/v3/generic/Button/index.ts b/frontend/src/components/v3/generic/Button/index.ts new file mode 100644 index 0000000000..e22c29adcf --- /dev/null +++ b/frontend/src/components/v3/generic/Button/index.ts @@ -0,0 +1 @@ +export * from "./Button"; diff --git a/frontend/src/components/v3/generic/ButtonGroup/ButtonGroup.tsx b/frontend/src/components/v3/generic/ButtonGroup/ButtonGroup.tsx new file mode 100644 index 0000000000..753d28a319 --- /dev/null +++ b/frontend/src/components/v3/generic/ButtonGroup/ButtonGroup.tsx @@ -0,0 +1,83 @@ +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "cva"; + +import { cn } from "../../utils"; +import { UnstableSeparator } from "../Separator"; + +const buttonGroupVariants = cva( + "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", + { + variants: { + orientation: { + horizontal: + "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", + vertical: + "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none" + } + }, + defaultVariants: { + orientation: "horizontal" + } + } +); + +function UnstableButtonGroup({ + className, + orientation, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function UnstableButtonGroupText({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean; +}) { + const Comp = asChild ? Slot : "div"; + + return ( + + ); +} + +function UnstableButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + buttonGroupVariants, + UnstableButtonGroup, + UnstableButtonGroupSeparator, + UnstableButtonGroupText +}; diff --git a/frontend/src/components/v3/generic/ButtonGroup/index.ts b/frontend/src/components/v3/generic/ButtonGroup/index.ts new file mode 100644 index 0000000000..d22eaf4c2e --- /dev/null +++ b/frontend/src/components/v3/generic/ButtonGroup/index.ts @@ -0,0 +1 @@ +export * from "./ButtonGroup"; diff --git a/frontend/src/components/v3/generic/Card/Card.tsx b/frontend/src/components/v3/generic/Card/Card.tsx new file mode 100644 index 0000000000..5af0d8da67 --- /dev/null +++ b/frontend/src/components/v3/generic/Card/Card.tsx @@ -0,0 +1,88 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; + +import { cn } from "../../utils"; + +function UnstableCard({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableCardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableCardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]:inline-block [&>svg]:size-[18px]", + className + )} + {...props} + /> + ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]:inline-block [&>svg]:size-[12px]", + className + )} + {...props} + /> + ); +} + +function UnstableCardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableCardContent({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function UnstableCardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + UnstableCard, + UnstableCardAction, + UnstableCardContent, + CardDescription as UnstableCardDescription, + UnstableCardFooter, + UnstableCardHeader, + UnstableCardTitle +}; diff --git a/frontend/src/components/v3/generic/Card/index.ts b/frontend/src/components/v3/generic/Card/index.ts new file mode 100644 index 0000000000..24d3212465 --- /dev/null +++ b/frontend/src/components/v3/generic/Card/index.ts @@ -0,0 +1 @@ +export * from "./Card"; diff --git a/frontend/src/components/v3/generic/Detail/Detail.tsx b/frontend/src/components/v3/generic/Detail/Detail.tsx new file mode 100644 index 0000000000..16aa8d0d47 --- /dev/null +++ b/frontend/src/components/v3/generic/Detail/Detail.tsx @@ -0,0 +1,23 @@ +import { cn } from "../../utils"; + +function Detail({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function DetailLabel({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DetailValue({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function DetailGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Detail, DetailGroup, DetailLabel, DetailValue }; diff --git a/frontend/src/components/v3/generic/Detail/index.ts b/frontend/src/components/v3/generic/Detail/index.ts new file mode 100644 index 0000000000..f511dd353f --- /dev/null +++ b/frontend/src/components/v3/generic/Detail/index.ts @@ -0,0 +1 @@ +export * from "./Detail"; diff --git a/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx b/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx new file mode 100644 index 0000000000..64e8aa0fdb --- /dev/null +++ b/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx @@ -0,0 +1,254 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; + +import { cn } from "@app/components/v3/utils"; + +function UnstableDropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function UnstableDropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuItem({ + className, + inset, + variant = "default", + isDisabled, + ...props +}: Omit, "disabled"> & { + inset?: boolean; + variant?: "default" | "danger"; + isDisabled?: boolean; +}) { + return ( + + ); +} + +function UnstableDropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function UnstableDropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function UnstableDropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function UnstableDropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function UnstableDropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { + return ( + + ); +} + +function UnstableDropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function UnstableDropdownMenuSubContent({ + className, + sideOffset = 8, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +type UnstableDropdownMenuChecked = DropdownMenuPrimitive.DropdownMenuCheckboxItemProps["checked"]; + +export { + UnstableDropdownMenu, + UnstableDropdownMenuCheckboxItem, + type UnstableDropdownMenuChecked, + UnstableDropdownMenuContent, + UnstableDropdownMenuGroup, + UnstableDropdownMenuItem, + UnstableDropdownMenuLabel, + UnstableDropdownMenuPortal, + UnstableDropdownMenuRadioGroup, + UnstableDropdownMenuRadioItem, + UnstableDropdownMenuSeparator, + UnstableDropdownMenuShortcut, + UnstableDropdownMenuSub, + UnstableDropdownMenuSubContent, + UnstableDropdownMenuSubTrigger, + UnstableDropdownMenuTrigger +}; diff --git a/frontend/src/components/v3/generic/Dropdown/index.ts b/frontend/src/components/v3/generic/Dropdown/index.ts new file mode 100644 index 0000000000..f024a9e9a1 --- /dev/null +++ b/frontend/src/components/v3/generic/Dropdown/index.ts @@ -0,0 +1 @@ +export * from "./Dropdown"; diff --git a/frontend/src/components/v3/generic/Empty/Empty.tsx b/frontend/src/components/v3/generic/Empty/Empty.tsx new file mode 100644 index 0000000000..f91ae9fb07 --- /dev/null +++ b/frontend/src/components/v3/generic/Empty/Empty.tsx @@ -0,0 +1,100 @@ +import { cn } from "../../utils"; + +function UnstableEmpty({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableEmptyHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +// scott: TODO + +// const emptyMediaVariants = cva( +// "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0", +// { +// variants: { +// variant: { +// default: "bg-transparent", +// icon: "bg-bunker-900 rounded text-foreground flex size-10 shrink-0 items-center justify-center [&_svg:not([class*='size-'])]:size-6" +// } +// }, +// defaultVariants: { +// variant: "default" +// } +// } +// ); + +// function EmptyMedia({ +// className, +// variant = "default", +// ...props +// }: React.ComponentProps<"div"> & VariantProps) { +// return ( +//
+// ); +// } + +function UnstableEmptyTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableEmptyDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +
a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", + className + )} + {...props} + /> + ); +} + +function UnstableEmptyContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + UnstableEmpty, + UnstableEmptyContent, + UnstableEmptyDescription, + UnstableEmptyHeader, + UnstableEmptyTitle +}; diff --git a/frontend/src/components/v3/generic/Empty/index.ts b/frontend/src/components/v3/generic/Empty/index.ts new file mode 100644 index 0000000000..7aa85b1b7d --- /dev/null +++ b/frontend/src/components/v3/generic/Empty/index.ts @@ -0,0 +1 @@ +export * from "./Empty"; diff --git a/frontend/src/components/v3/generic/IconButton/IconButton.tsx b/frontend/src/components/v3/generic/IconButton/IconButton.tsx new file mode 100644 index 0000000000..3e7f5ace4f --- /dev/null +++ b/frontend/src/components/v3/generic/IconButton/IconButton.tsx @@ -0,0 +1,112 @@ +import * as React from "react"; +import { forwardRef } from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "cva"; +import { twMerge } from "tailwind-merge"; + +import { Lottie } from "@app/components/v2"; +import { UnstableButton } from "@app/components/v3/generic"; +import { cn } from "@app/components/v3/utils"; + +const iconButtonVariants = cva( + cn( + "inline-flex items-center active:scale-[0.99] justify-center border cursor-pointer whitespace-nowrap rounded-[4px] text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-75 [&_svg]:pointer-events-none shrink-0 [&>svg]:shrink-0", + "focus-visible:ring-ring outline-0 focus-visible:ring-2" + ), + { + variants: { + variant: { + default: + "border-foreground bg-foreground text-background hover:bg-foreground/90 hover:border-foreground/90", + accent: + "border-accent/10 bg-accent/40 text-foreground hover:bg-accent/50 hover:border-accent/20", + outline: "text-foreground hover:bg-foreground/20 border-border hover:border-foreground/50", + ghost: "text-foreground hover:bg-foreground/40 border-transparent", + project: + "border-project/75 bg-project/40 text-foreground hover:bg-project/50 hover:border-kms", + org: "border-org/75 bg-org/40 text-foreground hover:bg-org/50 hover:border-org", + "sub-org": + "border-sub-org/75 bg-sub-org/40 text-foreground hover:bg-sub-org/50 hover:border-namespace", + success: + "border-success/75 bg-success/40 text-foreground hover:bg-success/50 hover:border-success", + info: "border-info/75 bg-info/40 text-foreground hover:bg-info/50 hover:border-info", + warning: + "border-warning/75 bg-warning/40 text-foreground hover:bg-warning/50 hover:border-warning", + danger: + "border-danger/75 bg-danger/40 text-foreground hover:bg-danger/50 hover:border-danger" + }, + size: { + xs: "h-6 w-6 [&>svg]:size-4 rounded-[5px] [&>svg]:stroke-[1.75]", + sm: "h-8 w-8 [&>svg]:size-5 [&>svg]:stroke-[1.5]", + md: "h-9 w-9 [&>svg]:size-6 [&>svg]:stroke-[1.5]", + lg: "h-10 w-10 [&>svg]:size-7 [&>svg]:stroke-[1.5]" + }, + isPending: { + true: "text-transparent" + }, + isFullWidth: { + true: "w-full", + false: "w-fit" + } + }, + defaultVariants: { + variant: "default", + size: "md" + } + } +); + +type UnstableIconButtonProps = React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + isPending?: boolean; + isDisabled?: boolean; + }; + +const UnstableIconButton = forwardRef( + ( + { + className, + variant = "default", + size = "md", + asChild = false, + isPending = false, + disabled = false, + isDisabled = false, + children, + ...props + }, + ref + ): JSX.Element => { + const Comp = asChild ? Slot : "button"; + + return ( + + {children} + {isPending && ( + + )} + + ); + } +); + +UnstableButton.displayName = "IconButton"; + +export { iconButtonVariants, UnstableIconButton, type UnstableIconButtonProps }; diff --git a/frontend/src/components/v3/generic/IconButton/index.ts b/frontend/src/components/v3/generic/IconButton/index.ts new file mode 100644 index 0000000000..53185101de --- /dev/null +++ b/frontend/src/components/v3/generic/IconButton/index.ts @@ -0,0 +1 @@ +export * from "./IconButton"; diff --git a/frontend/src/components/v3/generic/PageLoader/PageLoader.tsx b/frontend/src/components/v3/generic/PageLoader/PageLoader.tsx new file mode 100644 index 0000000000..346eead3af --- /dev/null +++ b/frontend/src/components/v3/generic/PageLoader/PageLoader.tsx @@ -0,0 +1,9 @@ +import { Lottie } from "@app/components/v2"; + +export function UnstablePageLoader() { + return ( +
+ +
+ ); +} diff --git a/frontend/src/components/v3/generic/PageLoader/index.ts b/frontend/src/components/v3/generic/PageLoader/index.ts new file mode 100644 index 0000000000..70c6707ce9 --- /dev/null +++ b/frontend/src/components/v3/generic/PageLoader/index.ts @@ -0,0 +1 @@ +export * from "./PageLoader"; diff --git a/frontend/src/components/v3/generic/Separator/Separator.tsx b/frontend/src/components/v3/generic/Separator/Separator.tsx new file mode 100644 index 0000000000..0ebfe67459 --- /dev/null +++ b/frontend/src/components/v3/generic/Separator/Separator.tsx @@ -0,0 +1,28 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "../../utils"; + +function UnstableSeparator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { UnstableSeparator }; diff --git a/frontend/src/components/v3/generic/Separator/index.ts b/frontend/src/components/v3/generic/Separator/index.ts new file mode 100644 index 0000000000..4060cb5ecd --- /dev/null +++ b/frontend/src/components/v3/generic/Separator/index.ts @@ -0,0 +1 @@ +export * from "./Separator"; diff --git a/frontend/src/components/v3/generic/Table/Table.stories.tsx b/frontend/src/components/v3/generic/Table/Table.stories.tsx new file mode 100644 index 0000000000..09098795bf --- /dev/null +++ b/frontend/src/components/v3/generic/Table/Table.stories.tsx @@ -0,0 +1,138 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { CopyIcon, EditIcon, MoreHorizontalIcon, TrashIcon } from "lucide-react"; + +import { + Badge, + UnstableDropdownMenu, + UnstableDropdownMenuContent, + UnstableDropdownMenuItem, + UnstableDropdownMenuTrigger, + UnstableIconButton +} from "@app/components/v3/generic"; +import { ProjectIcon } from "@app/components/v3/platform"; + +import { + UnstableTable, + UnstableTableBody, + UnstableTableCell, + UnstableTableHead, + UnstableTableHeader, + UnstableTableRow +} from "./Table"; + +const identities: { + name: string; + role: string; + managedBy?: { scope: "org" | "namespace"; name: string }; +}[] = [ + { + name: "machine-one", + role: "Admin", + managedBy: { + scope: "org", + name: "infisical" + } + }, + { + name: "machine-two", + role: "Viewer", + managedBy: { + scope: "namespace", + name: "engineering" + } + }, + { + name: "machine-three", + role: "Developer" + }, + { + name: "machine-four", + role: "Admin", + managedBy: { + scope: "namespace", + name: "dev-ops" + } + }, + { + name: "machine-five", + role: "Viewer", + managedBy: { + scope: "org", + name: "infisical" + } + }, + { + name: "machine-six", + role: "Developer" + } +]; + +function TableDemo() { + return ( + + + + Name + Role + Managed By + + + + + {identities.map((identity) => ( + + {identity.name} + {identity.role} + + + + Project + + + + + + + + + + + + + Copy ID + + + + Edit Identity + + + + Delete Identity + + + + + + ))} + + + ); +} + +const meta = { + title: "Generic/Table", + component: TableDemo, + parameters: { + layout: "centered" + }, + tags: ["autodocs"], + argTypes: {} +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const KitchenSInk: Story = { + name: "Example: Kitchen Sink", + args: {} +}; diff --git a/frontend/src/components/v3/generic/Table/Table.tsx b/frontend/src/components/v3/generic/Table/Table.tsx new file mode 100644 index 0000000000..45ae3210e7 --- /dev/null +++ b/frontend/src/components/v3/generic/Table/Table.tsx @@ -0,0 +1,109 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; + +import { cn } from "@app/components/v3/utils"; + +function UnstableTable({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ); +} + +function UnstableTableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ); +} + +function UnstableTableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + tr]:last:border-b-0", className)} {...props} /> + ); +} + +function UnstableTableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ); +} + +function UnstableTableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ); +} + +function UnstableTableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ); +} + +function UnstableTableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ); +} + +function UnstableTableCaption({ className, ...props }: React.ComponentProps<"caption">) { + return ( +
+ ); +} + +export { + UnstableTable, + UnstableTableBody, + UnstableTableCaption, + UnstableTableCell, + UnstableTableFooter, + UnstableTableHead, + UnstableTableHeader, + UnstableTableRow +}; diff --git a/frontend/src/components/v3/generic/Table/index.ts b/frontend/src/components/v3/generic/Table/index.ts new file mode 100644 index 0000000000..e40efa4761 --- /dev/null +++ b/frontend/src/components/v3/generic/Table/index.ts @@ -0,0 +1 @@ +export * from "./Table"; diff --git a/frontend/src/components/v3/generic/index.ts b/frontend/src/components/v3/generic/index.ts index ae21190ba6..de3288e4c6 100644 --- a/frontend/src/components/v3/generic/index.ts +++ b/frontend/src/components/v3/generic/index.ts @@ -1 +1,12 @@ +export * from "./Alert"; export * from "./Badge"; +export * from "./Button"; +export * from "./ButtonGroup"; +export * from "./Card"; +export * from "./Detail"; +export * from "./Dropdown"; +export * from "./Empty"; +export * from "./IconButton"; +export * from "./PageLoader"; +export * from "./Separator"; +export * from "./Table"; diff --git a/frontend/src/components/v3/platform/ScopeIcons.tsx b/frontend/src/components/v3/platform/ScopeIcons.tsx index 8f87123197..9f21ce2502 100644 --- a/frontend/src/components/v3/platform/ScopeIcons.tsx +++ b/frontend/src/components/v3/platform/ScopeIcons.tsx @@ -1,8 +1,8 @@ import { BoxesIcon, BoxIcon, Building2Icon, ServerIcon } from "lucide-react"; -const InstanceIcon = ServerIcon; -const OrgIcon = Building2Icon; -const SubOrgIcon = BoxesIcon; -const ProjectIcon = BoxIcon; - -export { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon }; +export { + ServerIcon as InstanceIcon, + Building2Icon as OrgIcon, + BoxIcon as ProjectIcon, + BoxesIcon as SubOrgIcon +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index baa613b364..afd6c1b57b 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import "tw-animate-css"; @source not "../public"; @@ -39,7 +40,7 @@ /* Colors v2 */ --color-background: #19191c; - --color-foreground: white; + --color-foreground: #ebebeb; --color-success: #2ecc71; --color-info: #63b0bd; --color-warning: #f1c40f; @@ -48,6 +49,14 @@ --color-sub-org: #96ff59; --color-project: #e0ed34; --color-neutral: #adaeb0; + --color-border: #323439; + --color-label: #adaeb0; + --color-muted: #707174; + --color-popover: #111419; + --color-ring: #2d2f33; + --color-container: #16181a; + --color-accent: #7d7f80; + --color-muted-foreground: ; /*legacy color schema */ --color-org-v1: #30b3ff; diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 9e7484a84a..d8fbd73d74 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -1,24 +1,36 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { subject } from "@casl/ability"; -import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { DropdownMenu } from "@radix-ui/react-dropdown-menu"; import { useQuery } from "@tanstack/react-query"; import { Link, useNavigate, useParams } from "@tanstack/react-router"; -import { formatRelative } from "date-fns"; +import { ChevronLeftIcon, EllipsisIcon, InfoIcon } from "lucide-react"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan, ProjectPermissionCan } from "@app/components/permissions"; import { - Alert, - AlertDescription, - Button, ConfirmActionModal, DeleteActionModal, EmptyState, PageHeader, - Spinner + Tooltip } from "@app/components/v2"; +import { + OrgIcon, + UnstableAlert, + UnstableAlertDescription, + UnstableAlertTitle, + UnstableButton, + UnstableCard, + UnstableCardContent, + UnstableCardDescription, + UnstableCardHeader, + UnstableCardTitle, + UnstableDropdownMenuContent, + UnstableDropdownMenuItem, + UnstableDropdownMenuTrigger, + UnstablePageLoader +} from "@app/components/v3"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, @@ -36,7 +48,7 @@ import { useGetProjectIdentityMembershipV2 } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; -import { projectIdentityQuery } from "@app/hooks/api/projectIdentity"; +import { projectIdentityQuery, useDeleteProjectIdentity } from "@app/hooks/api/projectIdentity"; import { ProjectIdentityAuthenticationSection } from "@app/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection"; import { ProjectIdentityDetailsSection } from "@app/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection"; import { ProjectAccessControlTabs } from "@app/types/project"; @@ -56,8 +68,7 @@ const Page = () => { const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } = useGetProjectIdentityMembershipV2(projectId, identityId); - const { mutateAsync: deleteMutateAsync, isPending: isDeletingIdentity } = - useDeleteProjectIdentityMembership(); + const { mutateAsync: removeIdentityMutateAsync } = useDeleteProjectIdentityMembership(); const isProjectIdentity = Boolean(identityMembershipDetails?.identity.projectId); const isNonScopedIdentity = @@ -75,7 +86,10 @@ const Page = () => { enabled: isProjectIdentity }); + const { mutateAsync: deleteIdentity } = useDeleteProjectIdentity(); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "removeIdentity", "deleteIdentity", "assumePrivileges" ] as const); @@ -104,7 +118,7 @@ const Page = () => { }; const onRemoveIdentitySubmit = async () => { - await deleteMutateAsync({ + await removeIdentityMutateAsync({ identityId, projectId }); @@ -112,7 +126,7 @@ const Page = () => { text: "Successfully removed machine identity from project", type: "success" }); - handlePopUpClose("deleteIdentity"); + handlePopUpClose("removeIdentity"); navigate({ to: `${getProjectBaseURL(currentProject.type)}/access-management` as const, params: { @@ -125,16 +139,35 @@ const Page = () => { }); }; + const handleDeleteIdentity = async () => { + if (!identity) return; + + try { + await deleteIdentity({ + identityId: identity.id, + projectId: identity.projectId! + }); + + navigate({ + to: `${getProjectBaseURL(currentProject.type)}/access-management`, + search: { + selectedTab: "identities" + } + }); + } catch { + createNotification({ + type: "error", + text: "Failed to delete project machine identity" + }); + } + }; + if (isMembershipDetailsLoading || (isProjectIdentity && isProjectIdentityPending)) { - return ( -
- -
- ); + return ; } return ( -
+
{identityMembershipDetails ? ( <> { search={{ selectedTab: ProjectAccessControlTabs.Identities }} - className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" + className="mb-3 flex w-fit items-center gap-x-1 text-sm text-mineshaft-400 transition duration-100 hover:text-mineshaft-400/80" > - + Project Machine Identities -
- - - {(isAllowed) => ( - - )} - - {!isProjectIdentity && ( + + + + Options + + + + + { + navigator.clipboard.writeText(identityMembershipDetails.id); + createNotification({ + text: "Machine identity ID copied to clipboard", + type: "info" + }); + }} + > + Copy Machine Identity ID + + + {(isAllowed) => ( + handlePopUpOpen("assumePrivileges")} + > + Assume Privileges + +
+ +
+
+
+ )} +
{(isAllowed) => ( - + {isProjectIdentity ? "Delete Machine Identity" : "Remove From Project"} + )} - )} -
+ +
- {!isProjectIdentity && ( - - - This machine identity is managed by your organization.{" "} - - {(isAllowed) => - isAllowed ? ( - - - Click here to manage machine identity. - - - ) : null - } - - - - )} -
- {identity ? ( -
- +
+ + +
+ {identity ? ( refetchIdentity()} /> -
- ) : ( -
-
- -
-
- )} -
+ ) : ( + + + Authentication + + Configure authentication methods + + + + + + + Machine identity managed by organization + + +

+ This machine identity's authentication methods are controlled by your + organization. To make changes,{" "} + + {(isAllowed) => + isAllowed ? ( + + go to organization access control + + ) : null + } + + . +

+
+
+
+
+ )} {
handlePopUpToggle("deleteIdentity", isOpen)} + onChange={(isOpen) => handlePopUpToggle("removeIdentity", isOpen)} deleteKey="remove" onDeleteApproved={() => onRemoveIdentitySubmit()} /> @@ -292,6 +338,13 @@ const Page = () => { onConfirmed={handleAssumePrivileges} buttonText="Confirm" /> + handlePopUpToggle("deleteIdentity", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleDeleteIdentity} + /> ) : ( diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index 956c01d694..fb48245a67 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -1,6 +1,6 @@ import { Controller, FormProvider, useForm } from "react-hook-form"; import { subject } from "@casl/ability"; -import { faCaretDown, faChevronLeft, faClock, faSave } from "@fortawesome/free-solid-svg-icons"; +import { faCaretDown, faClock, faSave } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { format, formatDistance } from "date-fns"; @@ -21,6 +21,7 @@ import { Tag, Tooltip } from "@app/components/v2"; +import { UnstableSeparator } from "@app/components/v3"; import { ProjectPermissionIdentityActions, ProjectPermissionSub, @@ -180,55 +181,9 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ } return ( -
+ -
- -
- {isDirty && ( - - )} -
- - -
-
-
-
-
Overview
-

- Additional privileges take precedence over roles when permissions conflict -

+
-
-
Policies
+ +
+
+
Policies
+
+ {isDirty && ( + + )} +
+ +
+
+
{(isCreate || !isPending) && } -
+
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map( (permissionSubject) => (
+ +
+ + +
); diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx index e43b805382..62052a6c84 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeSection.tsx @@ -1,27 +1,36 @@ import { subject } from "@casl/ability"; -import { faEllipsisV, faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format, formatDistance } from "date-fns"; -import { AnimatePresence, motion } from "framer-motion"; -import { twMerge } from "tailwind-merge"; +import { ClockAlertIcon, ClockIcon, EllipsisIcon, PlusIcon } from "lucide-react"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; +import { DeleteActionModal, Lottie, Modal, ModalContent, Tooltip } from "@app/components/v2"; import { - DeleteActionModal, - EmptyState, - IconButton, - Table, - TableContainer, - TableSkeleton, - Tag, - TBody, - Td, - Th, - THead, - Tooltip, - Tr -} from "@app/components/v2"; + Badge, + UnstableButton, + UnstableCard, + UnstableCardAction, + UnstableCardContent, + UnstableCardDescription, + UnstableCardHeader, + UnstableCardTitle, + UnstableDropdownMenu, + UnstableDropdownMenuContent, + UnstableDropdownMenuItem, + UnstableDropdownMenuTrigger, + UnstableEmpty, + UnstableEmptyContent, + UnstableEmptyDescription, + UnstableEmptyHeader, + UnstableEmptyTitle, + UnstableIconButton, + UnstableTable, + UnstableTableBody, + UnstableTableCell, + UnstableTableHead, + UnstableTableHeader, + UnstableTableRow +} from "@app/components/v3"; import { ProjectPermissionActions, ProjectPermissionIdentityActions, @@ -67,193 +76,230 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe handlePopUpClose("deletePrivilege"); }; - return ( -
- - {popUp?.modifyPrivilege.isOpen ? ( - - handlePopUpClose("modifyPrivilege")} - identityId={identityId} - privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id} - isDisabled={permission.cannot( - ProjectPermissionIdentityActions.Edit, - subject(ProjectPermissionSub.Identity, { - identityId - }) - )} - /> - - ) : ( - -
-

- Project Additional Privileges -

+ const hasAdditionalPrivileges = Boolean(identityProjectPrivileges?.length); + return ( + <> + + + Project Additional Privileges + + Assign one-off policies to this machine identity + + {hasAdditionalPrivileges && ( + {(isAllowed) => ( - { handlePopUpOpen("modifyPrivilege"); }} isDisabled={!isAllowed} > - - + + Add Additional Privileges + )} + + )} + + + {/* eslint-disable-next-line no-nested-ternary */} + {isPending ? ( + // scott: todo proper loader +
+
-
- - - - - - - - - - {isPending && ( - - )} - {!isPending && - identityProjectPrivileges?.map((privilegeDetails) => { - const isTemporary = privilegeDetails?.isTemporary; - const isExpired = - privilegeDetails.isTemporary && - new Date() > new Date(privilegeDetails.temporaryAccessEndTime || ""); + ) : identityProjectPrivileges?.length ? ( + + + + Name + Duration + + + + + {!isPending && + identityProjectPrivileges?.map((privilegeDetails) => { + const isTemporary = privilegeDetails?.isTemporary; + const isExpired = + privilegeDetails.isTemporary && + new Date() > new Date(privilegeDetails.temporaryAccessEndTime || ""); - let text = "Permanent"; - let toolTipText = "Non-Expiring Access"; - if (privilegeDetails.isTemporary) { - if (isExpired) { - text = "Access Expired"; - toolTipText = "Timed Access Expired"; - } else { - text = formatDistance( - new Date(privilegeDetails.temporaryAccessEndTime || ""), - new Date() - ); - toolTipText = `Until ${format( - new Date(privilegeDetails.temporaryAccessEndTime || ""), - "yyyy-MM-dd hh:mm:ss aaa" - )}`; - } - } - - return ( - { - if (evt.key === "Enter") { - handlePopUpOpen("modifyPrivilege", privilegeDetails); - } - }} - onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)} - > - - - - + let text = "Permanent"; + let toolTipText = "Non-Expiring Access"; + if (privilegeDetails.isTemporary) { + if (isExpired) { + text = "Access Expired"; + toolTipText = "Timed Access Expired"; + } else { + text = formatDistance( + new Date(privilegeDetails.temporaryAccessEndTime || ""), + new Date() ); - })} - -
NameDuration -
{privilegeDetails.slug} - - - {text} - - - -
- - {(isAllowed) => ( - { - e.stopPropagation(); - e.preventDefault(); - handlePopUpOpen("deletePrivilege", { - id: privilegeDetails?.id, - slug: privilegeDetails?.slug - }); - }} - > - - - )} - - - - -
-
- {!isPending && !identityProjectPrivileges?.length && ( - - )} -
-
- handlePopUpToggle("deletePrivilege", isOpen)} - onDeleteApproved={() => handlePrivilegeDelete()} - /> - - )} - -
+ toolTipText = `Until ${format( + new Date(privilegeDetails.temporaryAccessEndTime || ""), + "yyyy-MM-dd hh:mm:ss aaa" + )}`; + } + } + + return ( + + + {privilegeDetails.slug} + + + {isTemporary ? ( + + + {isExpired ? : } + {text} + + + ) : ( + text + )} + + + + + + + + + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("modifyPrivilege", privilegeDetails); + }} + > + Edit Additional Privilege + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deletePrivilege", { + id: privilegeDetails?.id, + slug: privilegeDetails?.slug + }); + }} + > + Remove Additional Privilege + + )} + + + + + + ); + })} + + + ) : ( + + + + This machine identity has no additional privileges + + + Add an additional privilege to grant one-off access policies + + + + + {(isAllowed) => ( + { + handlePopUpOpen("modifyPrivilege"); + }} + isDisabled={!isAllowed} + > + + Add Additional Privileges + + )} + + + + )} + + + handlePopUpToggle("modifyPrivilege", isOpen)} + > + + handlePopUpClose("modifyPrivilege")} + identityId={identityId} + privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id} + isDisabled={permission.cannot( + ProjectPermissionIdentityActions.Edit, + subject(ProjectPermissionSub.Identity, { + identityId + }) + )} + /> + + + handlePopUpToggle("deletePrivilege", isOpen)} + onDeleteApproved={() => handlePrivilegeDelete()} + /> + ); }; diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx index 507a83a562..a0905abb48 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx @@ -1,28 +1,36 @@ import { subject } from "@casl/ability"; -import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format, formatDistance } from "date-fns"; -import { twMerge } from "tailwind-merge"; +import { ClockAlertIcon, ClockIcon, EllipsisIcon, PencilIcon } from "lucide-react"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; +import { DeleteActionModal, Lottie, Modal, ModalContent, Tooltip } from "@app/components/v2"; import { - DeleteActionModal, - EmptyState, - IconButton, - Modal, - ModalContent, - Table, - TableContainer, - TableSkeleton, - Tag, - TBody, - Td, - Th, - THead, - Tooltip, - Tr -} from "@app/components/v2"; + Badge, + UnstableButton, + UnstableCard, + UnstableCardAction, + UnstableCardContent, + UnstableCardDescription, + UnstableCardHeader, + UnstableCardTitle, + UnstableDropdownMenu, + UnstableDropdownMenuContent, + UnstableDropdownMenuItem, + UnstableDropdownMenuTrigger, + UnstableEmpty, + UnstableEmptyContent, + UnstableEmptyDescription, + UnstableEmptyHeader, + UnstableEmptyTitle, + UnstableIconButton, + UnstableTable, + UnstableTableBody, + UnstableTableCell, + UnstableTableHead, + UnstableTableHeader, + UnstableTableRow +} from "@app/components/v3"; import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context"; import { formatProjectRoleName } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; @@ -83,133 +91,186 @@ export const IdentityRoleDetailsSection = ({ handlePopUpClose("deleteRole"); }; + const hasRoles = Boolean(identityMembershipDetails?.roles.length); + return ( -
-
-

Project Roles

- - {(isAllowed) => ( - { - handlePopUpOpen("modifyRole"); - }} - isDisabled={!isAllowed} - > - - - )} - -
-
- - - - - - - - - - {isMembershipDetailsLoading && ( - - )} - {!isMembershipDetailsLoading && - identityMembershipDetails?.roles?.map((roleDetails) => { - const isTemporary = roleDetails?.isTemporary; - const isExpired = - roleDetails.isTemporary && - new Date() > new Date(roleDetails.temporaryAccessEndTime || ""); - - let text = "Permanent"; - let toolTipText = "Non-Expiring Access"; - if (roleDetails.isTemporary) { - if (isExpired) { - text = "Access Expired"; - toolTipText = "Timed Access Expired"; - } else { - text = formatDistance( - new Date(roleDetails.temporaryAccessEndTime || ""), - new Date() - ); - toolTipText = `Until ${format( - new Date(roleDetails.temporaryAccessEndTime || ""), - "yyyy-MM-dd hh:mm:ss aaa" - )}`; - } - } - - return ( - - - - - - ); + <> + + + Project Roles + + Manage roles assigned to this machine identity + + {hasRoles && ( + + -
RoleDuration -
- {roleDetails.role === "custom" - ? roleDetails.customRoleName - : formatProjectRoleName(roleDetails.role)} - - - - {text} - - - -
- - {(isAllowed) => ( - { - e.stopPropagation(); - handlePopUpOpen("deleteRole", { - id: roleDetails?.id, - slug: roleDetails?.customRoleName || roleDetails?.role - }); - }} - > - - - )} - -
-
- {!isMembershipDetailsLoading && !identityMembershipDetails?.roles?.length && ( - + > + {(isAllowed) => ( + { + handlePopUpOpen("modifyRole"); + }} + isDisabled={!isAllowed} + > + + Edit Roles + + )} + + )} -
-
+ + + { + /* eslint-disable-next-line no-nested-ternary */ + isMembershipDetailsLoading ? ( + // scott: todo proper loader +
+ +
+ ) : hasRoles ? ( + + + + Role + Duration + + + + + {identityMembershipDetails?.roles?.map((roleDetails) => { + const isTemporary = roleDetails?.isTemporary; + const isExpired = + roleDetails.isTemporary && + new Date() > new Date(roleDetails.temporaryAccessEndTime || ""); + + let text = "Permanent"; + let toolTipText = "Non-Expiring Access"; + if (roleDetails.isTemporary) { + if (isExpired) { + text = "Access Expired"; + toolTipText = "Timed Access Expired"; + } else { + text = formatDistance( + new Date(roleDetails.temporaryAccessEndTime || ""), + new Date() + ); + toolTipText = `Until ${format( + new Date(roleDetails.temporaryAccessEndTime || ""), + "yyyy-MM-dd hh:mm:ss aaa" + )}`; + } + } + + return ( + + + {roleDetails.role === "custom" + ? roleDetails.customRoleName + : formatProjectRoleName(roleDetails.role)} + + + {isTemporary ? ( + + + {isExpired ? : } + {text} + + + ) : ( + text + )} + + + + + + + + + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteRole", { + id: roleDetails?.id, + slug: roleDetails?.customRoleName || roleDetails?.role + }); + }} + isDisabled={!isAllowed} + variant="danger" + > + {/* */} + Remove Role + + )} + + + + + + ); + })} + + + ) : ( + + + + This machine identity doesn t have any roles + + + Give this machine identity one or more roles + + + + + {(isAllowed) => ( + { + handlePopUpOpen("modifyRole"); + }} + isDisabled={!isAllowed} + > + + Edit Roles + + )} + + + + ) + } +
+ -
+ ); }; diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx index f471a3d24e..6f1bfe4649 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx @@ -1,12 +1,31 @@ import { subject } from "@casl/ability"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { LockIcon, SettingsIcon } from "lucide-react"; +import { EllipsisIcon, LockIcon, PlusIcon } from "lucide-react"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Button, Tooltip } from "@app/components/v2"; -import { Badge } from "@app/components/v3"; +import { Tooltip } from "@app/components/v2"; +import { + Badge, + UnstableButton, + UnstableCard, + UnstableCardAction, + UnstableCardContent, + UnstableCardDescription, + UnstableCardHeader, + UnstableCardTitle, + UnstableEmpty, + UnstableEmptyContent, + UnstableEmptyDescription, + UnstableEmptyHeader, + UnstableEmptyTitle, + UnstableIconButton, + UnstableTable, + UnstableTableBody, + UnstableTableCell, + UnstableTableHead, + UnstableTableHeader, + UnstableTableRow +} from "@app/components/v3"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/context"; import { IdentityAuthMethod, identityAuthToNameMap, TProjectIdentity } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -25,76 +44,126 @@ export const ProjectIdentityAuthenticationSection = ({ identity, refetchIdentity "upgradePlan" ]); + const hasAuthMethods = Boolean(identity.authMethods.length); + return ( -
-
-

Authentication

-
- {identity.authMethods.length > 0 ? ( -
- {identity.authMethods.map((authMethod) => ( - - ))} -
- ) : ( -
-

- No authentication methods configured. Get started by creating a new auth method. -

-
- )} - {!Object.values(IdentityAuthMethod).every((method) => - identity.authMethods.includes(method) - ) && ( - - {(isAllowed) => ( - + <> + + + Authentication + Configure authentication methods + {hasAuthMethods && + !Object.values(IdentityAuthMethod).every((method) => + identity.authMethods.includes(method) + ) && ( + + + {(isAllowed) => ( + { + handlePopUpOpen("identityAuthMethod", { + identityId: identity.id, + name: identity.name, + allAuthMethods: identity.authMethods + }); + }} + > + + Add Auth Method + + )} + + + )} + + + {identity.authMethods.length > 0 ? ( + + + Method + + + + {identity.authMethods.map((authMethod) => ( + + handlePopUpOpen("viewAuthMethod", { + authMethod, + lockedOut: identity.activeLockoutAuthMethods?.includes(authMethod) ?? false, + refetchIdentity + }) + } + > + {identityAuthToNameMap[authMethod]} + +
+ {identity.activeLockoutAuthMethods?.includes(authMethod) && ( + + + + + + )} + + + +
+
+
+ ))} +
+
+ ) : ( + + + + This machine identity has no auth methods configured + + + Add an auth method to use this machine identity + + + + + {(isAllowed) => ( + { + handlePopUpOpen("identityAuthMethod", { + identityId: identity.id, + name: identity.name, + allAuthMethods: identity.authMethods + }); + }} + > + + Add Auth Method + + )} + + + )} -
- )} + + -
+ ); }; diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx index e4ad97f9d4..a5a9e34889 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx @@ -1,36 +1,30 @@ import { subject } from "@casl/ability"; -import { - faCheck, - faChevronDown, - faCopy, - faEdit, - faKey, - faTrash -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useNavigate } from "@tanstack/react-router"; import { format } from "date-fns"; -import { twMerge } from "tailwind-merge"; +import { BanIcon, CheckIcon, ClipboardListIcon, PencilIcon } from "lucide-react"; -import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; +import { Modal, ModalContent, Tooltip } from "@app/components/v2"; import { - Button, - DeleteActionModal, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - IconButton, - Modal, - ModalContent, - Tag, - Tooltip -} from "@app/components/v2"; -import { ProjectPermissionIdentityActions, ProjectPermissionSub, useProject } from "@app/context"; -import { getProjectBaseURL } from "@app/helpers/project"; + Badge, + Detail, + DetailGroup, + DetailLabel, + DetailValue, + OrgIcon, + ProjectIcon, + UnstableButton, + UnstableButtonGroup, + UnstableCard, + UnstableCardAction, + UnstableCardContent, + UnstableCardDescription, + UnstableCardHeader, + UnstableCardTitle, + UnstableIconButton +} from "@app/components/v3"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/context"; import { usePopUp, useTimedReset } from "@app/hooks"; -import { identityAuthToNameMap, TProjectIdentity, useDeleteProjectIdentity } from "@app/hooks/api"; +import { identityAuthToNameMap, TProjectIdentity } from "@app/hooks/api"; import { IdentityProjectMembershipV1 } from "@app/hooks/api/identities/types"; import { ProjectIdentityModal } from "@app/pages/project/AccessControlPage/components/IdentityTab/components/ProjectIdentityModal"; @@ -41,190 +35,146 @@ type Props = { }; export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, membership }: Props) => { - const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-unused-vars + const [_, isCopyingId, setCopyTextId] = useTimedReset({ initialState: "Copy ID to clipboard" }); - const { currentProject } = useProject(); - const { mutateAsync: deleteIdentity } = useDeleteProjectIdentity(); - const navigate = useNavigate(); - const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ - "editIdentity", - "deleteIdentity" - ] as const); - - const handleDeleteIdentity = async () => { - try { - await deleteIdentity({ - identityId: identity.id, - projectId: identity.projectId! - }); - - navigate({ - to: `${getProjectBaseURL(currentProject.type)}/access-management`, - search: { - selectedTab: "identities" - } - }); - } catch { - createNotification({ - type: "error", - text: "Failed to delete project machine identity" - }); - } - }; + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["editIdentity"] as const); return ( -
-
-

Details

- + <> + + + Details + Machine identity details {!isOrgIdentity && ( - - - - )} - - - {(isAllowed) => ( - } - onClick={async () => { - handlePopUpOpen("editIdentity"); - }} - disabled={!isAllowed} - > - Edit Machine Identity - - )} - - - {(isAllowed) => ( - { - handlePopUpOpen("deleteIdentity"); - }} - icon={} - disabled={!isAllowed} - > - Delete Machine Identity - - )} - - - -
-
-
-

Machine Identity ID

-
-

{identity.id}

-
- - { - navigator.clipboard.writeText(identity.id); - setCopyTextId("Copied"); - }} - > - - - -
-
-
-
-

Managed By

-

- {identity.projectId ? "Project" : "Organization"} -

-
- {!isOrgIdentity && ( - <> -
-

Last Login Auth Method

-

- {membership.lastLoginAuthMethod - ? identityAuthToNameMap[membership.lastLoginAuthMethod] - : "-"} -

-
-
-

Last Login Time

-

- {membership.lastLoginTime ? format(membership.lastLoginTime, "PPpp") : "-"} -

-
-
-

Delete Protection

-

- {identity.hasDeleteProtection ? "On" : "Off"} -

-
- - )} -
-

Metadata

- {identity?.metadata?.length ? ( -
- {identity.metadata?.map((el) => ( -
- ( + { + handlePopUpOpen("editIdentity"); + }} size="xs" - className="mr-0 flex items-center rounded-r-none border border-mineshaft-500" + variant="project" > - -
{el.key}
-
- -
- {el.value} -
-
-
- ))} -
- ) : ( -

-

+ + Edit Details + + )} + + )} -
-
+ + + + + Name + {identity.name} + + + ID + + {identity.id} + + { + navigator.clipboard.writeText(identity.id); + setCopyTextId("Copied"); + }} + variant="ghost" + size="xs" + > + {isCopyingId ? : } + + + + + + Managed by + + {isOrgIdentity ? ( + + + Organization + + ) : ( + + + Project + + )} + + + + Metadata + + {identity?.metadata?.length ? ( + identity.metadata?.map((el) => ( + + {el.key} + {el.value} + + )) + ) : ( + No metadata + )} + + + + {isOrgIdentity ? "Joined project" : "Created"} + {format(membership.createdAt, "PPpp")} + + {!isOrgIdentity && ( + <> + + Last Login Method + + {membership.lastLoginAuthMethod ? ( + identityAuthToNameMap[membership.lastLoginAuthMethod] + ) : ( + N/A + )} + + + + Last Logged In + + {membership.lastLoginTime ? ( + format(membership.lastLoginTime, "PPpp") + ) : ( + N/A + )} + + + + Delete protection + + {identity.hasDeleteProtection ? ( + + + Enabled + + ) : ( + + + Disabled + + )} + + + + )} + + + handlePopUpToggle("editIdentity", open)} @@ -236,14 +186,6 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members /> - - handlePopUpToggle("deleteIdentity", isOpen)} - deleteKey="confirm" - onDeleteApproved={handleDeleteIdentity} - /> -
+ ); };