From e598199e97f59898c7cc815535ff1f7ef23eca28 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 2 Dec 2025 17:28:26 -0800 Subject: [PATCH 1/9] style: give project machine identity page facelift --- .../src/ee/services/license/license-fns.ts | 2 +- .../.storybook/decorators/RouterDecorator.tsx | 4 +- frontend/package-lock.json | 91 ++++ frontend/package.json | 2 + .../components/v2/PageHeader/PageHeader.tsx | 4 +- .../src/components/v3/generic/Alert/Alert.tsx | 62 +++ .../src/components/v3/generic/Alert/index.ts | 1 + .../v3/generic/Badge/Badge.stories.tsx | 81 +++- .../src/components/v3/generic/Badge/Badge.tsx | 26 +- .../v3/generic/Button/Button.stories.tsx | 357 +++++++++++++++ .../components/v3/generic/Button/Button.tsx | 139 ++++++ .../src/components/v3/generic/Button/index.ts | 1 + .../v3/generic/ButtonGroup/ButtonGroup.tsx | 83 ++++ .../v3/generic/ButtonGroup/index.ts | 1 + .../src/components/v3/generic/Card/Card.tsx | 88 ++++ .../src/components/v3/generic/Card/index.ts | 1 + .../components/v3/generic/Detail/Detail.tsx | 23 + .../src/components/v3/generic/Detail/index.ts | 1 + .../v3/generic/Dropdown/Dropdown.tsx | 254 +++++++++++ .../components/v3/generic/Dropdown/index.ts | 1 + .../src/components/v3/generic/Empty/Empty.tsx | 100 +++++ .../src/components/v3/generic/Empty/index.ts | 1 + .../v3/generic/IconButton/IconButton.tsx | 112 +++++ .../components/v3/generic/IconButton/index.ts | 1 + .../v3/generic/PageLoader/PageLoader.tsx | 9 + .../components/v3/generic/PageLoader/index.ts | 1 + .../v3/generic/Separator/Separator.tsx | 28 ++ .../components/v3/generic/Separator/index.ts | 1 + .../v3/generic/Table/Table.stories.tsx | 138 ++++++ .../src/components/v3/generic/Table/Table.tsx | 109 +++++ .../src/components/v3/generic/Table/index.ts | 1 + frontend/src/components/v3/generic/index.ts | 11 + .../src/components/v3/platform/ScopeIcons.tsx | 12 +- frontend/src/index.css | 11 +- .../IdentityDetailsByIDPage.tsx | 291 +++++++----- ...rojectAdditionalPrivilegeModifySection.tsx | 92 ++-- ...ntityProjectAdditionalPrivilegeSection.tsx | 422 ++++++++++-------- .../IdentityRoleDetailsSection.tsx | 349 +++++++++------ .../components/ProjectIdentityAuthSection.tsx | 217 ++++++--- .../ProjectIdentityDetailsSection.tsx | 362 +++++++-------- 40 files changed, 2675 insertions(+), 815 deletions(-) create mode 100644 frontend/src/components/v3/generic/Alert/Alert.tsx create mode 100644 frontend/src/components/v3/generic/Alert/index.ts create mode 100644 frontend/src/components/v3/generic/Button/Button.stories.tsx create mode 100644 frontend/src/components/v3/generic/Button/Button.tsx create mode 100644 frontend/src/components/v3/generic/Button/index.ts create mode 100644 frontend/src/components/v3/generic/ButtonGroup/ButtonGroup.tsx create mode 100644 frontend/src/components/v3/generic/ButtonGroup/index.ts create mode 100644 frontend/src/components/v3/generic/Card/Card.tsx create mode 100644 frontend/src/components/v3/generic/Card/index.ts create mode 100644 frontend/src/components/v3/generic/Detail/Detail.tsx create mode 100644 frontend/src/components/v3/generic/Detail/index.ts create mode 100644 frontend/src/components/v3/generic/Dropdown/Dropdown.tsx create mode 100644 frontend/src/components/v3/generic/Dropdown/index.ts create mode 100644 frontend/src/components/v3/generic/Empty/Empty.tsx create mode 100644 frontend/src/components/v3/generic/Empty/index.ts create mode 100644 frontend/src/components/v3/generic/IconButton/IconButton.tsx create mode 100644 frontend/src/components/v3/generic/IconButton/index.ts create mode 100644 frontend/src/components/v3/generic/PageLoader/PageLoader.tsx create mode 100644 frontend/src/components/v3/generic/PageLoader/index.ts create mode 100644 frontend/src/components/v3/generic/Separator/Separator.tsx create mode 100644 frontend/src/components/v3/generic/Separator/index.ts create mode 100644 frontend/src/components/v3/generic/Table/Table.stories.tsx create mode 100644 frontend/src/components/v3/generic/Table/Table.tsx create mode 100644 frontend/src/components/v3/generic/Table/index.ts diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 09ff9e108..d6cb63b1f 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 a559c5cd1..c44ee6b12 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 e8fdda096..bbb72bfa7 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 8009c2118..075807caa 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 5a2ba2e12..05e0c131b 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 000000000..a48a4b7fb --- /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 000000000..b8e17a03c --- /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 4cbf955f9..244bd8108 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 f94bff5f2..9dae877ab 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 000000000..0f975e70d --- /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 000000000..4c065b1d0 --- /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 000000000..e22c29adc --- /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 000000000..753d28a31 --- /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 000000000..d22eaf4c2 --- /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 000000000..5af0d8da6 --- /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 000000000..24d321246 --- /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 000000000..16aa8d0d4 --- /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 000000000..f511dd353 --- /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 000000000..64e8aa0fd --- /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 000000000..f024a9e9a --- /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 000000000..f91ae9fb0 --- /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 000000000..7aa85b1b7 --- /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 000000000..3e7f5ace4 --- /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 000000000..53185101d --- /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 000000000..346eead3a --- /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 000000000..70c6707ce --- /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 000000000..0ebfe6745 --- /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 000000000..4060cb5ec --- /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 000000000..09098795b --- /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 000000000..45ae3210e --- /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 000000000..e40efa476 --- /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 ae21190ba..de3288e4c 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 8f8712319..9f21ce250 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 baa613b36..afd6c1b57 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 9e7484a84..d8fbd73d7 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 956c01d69..fb48245a6 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 e43b80538..62052a6c8 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 507a83a56..a0905abb4 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 f471a3d24..6f1bfe464 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 e4ad97f9d..a5a9e3488 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} - /> -
+ ); }; From 53953e48f14e95db6a3409a20670efb73c844d1f Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 2 Dec 2025 17:34:39 -0800 Subject: [PATCH 2/9] chore: revert license --- backend/src/ee/services/license/license-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index d6cb63b1f..09ff9e108 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: true, + rbac: false, githubOrgSync: false, customRateLimits: false, subOrganization: false, From d5948b32467ac355bb12b61a13e2d0eba45f3c0e Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 2 Dec 2025 18:20:51 -0800 Subject: [PATCH 3/9] fix: correct display name setting of icon button --- frontend/src/components/v3/generic/IconButton/IconButton.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/v3/generic/IconButton/IconButton.tsx b/frontend/src/components/v3/generic/IconButton/IconButton.tsx index 3e7f5ace4..b10006aa4 100644 --- a/frontend/src/components/v3/generic/IconButton/IconButton.tsx +++ b/frontend/src/components/v3/generic/IconButton/IconButton.tsx @@ -5,7 +5,6 @@ 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( @@ -107,6 +106,6 @@ const UnstableIconButton = forwardRef Date: Mon, 8 Dec 2025 15:36:56 -0800 Subject: [PATCH 4/9] improvements: address feedback --- .../v3/generic/Accordion/Accordion.tsx | 79 +++++ .../components/v3/generic/Accordion/index.ts | 1 + .../src/components/v3/generic/Badge/Badge.tsx | 4 +- .../src/components/v3/generic/Card/Card.tsx | 22 +- .../v3/generic/Dropdown/Dropdown.tsx | 3 +- .../src/components/v3/generic/Empty/Empty.tsx | 2 +- .../v3/generic/IconButton/IconButton.tsx | 4 +- .../src/components/v3/generic/Table/Table.tsx | 13 +- frontend/src/components/v3/generic/index.ts | 1 + frontend/src/hooks/api/shared/types.ts | 2 +- frontend/src/index.css | 8 +- .../IdentityDetailsByIDPage.tsx | 13 +- .../IdentityAuthenticationSection.tsx | 97 +++--- .../IdentityAuthFieldDisplay.tsx | 24 ++ .../IdentityAuthLockoutFields.tsx | 0 .../IdentityTokenAuthTokensTable.tsx | 0 ...dentityUniversalAuthClientSecretsTable.tsx | 0 .../ViewIdentityAliCloudAuthContent.tsx | 24 +- .../ViewIdentityAuth/ViewIdentityAuth.tsx | 276 ++++++++++++++++++ .../ViewIdentityAwsAuthContent.tsx | 24 +- .../ViewIdentityAzureAuthContent.tsx | 24 +- .../ViewIdentityContentWrapper.tsx | 0 .../ViewIdentityGcpAuthContent.tsx | 24 +- .../ViewIdentityJwtAuthContent.tsx | 26 +- .../ViewIdentityKubernetesAuthContent.tsx | 24 +- .../ViewIdentityLdapAuthContent.tsx | 24 +- .../ViewIdentityOciAuthContent.tsx | 24 +- .../ViewIdentityOidcAuthContent.tsx | 26 +- .../ViewIdentityTlsCertAuthContent.tsx | 24 +- .../ViewIdentityTokenAuthContent.tsx | 22 +- .../ViewIdentityUniversalAuthContent.tsx | 22 +- .../components/ViewIdentityAuth/index.ts | 1 + .../ViewIdentityAuth/types/index.ts | 7 + .../IdentityAuthFieldDisplay.tsx | 20 -- .../ViewIdentityAuthModal.tsx | 214 -------------- .../components/ViewIdentityAuthModal/index.ts | 1 - .../ViewIdentityAuthModal/types/index.ts | 14 - .../IdentityDetailsByIDPage.tsx | 7 +- ...ntityProjectAdditionalPrivilegeSection.tsx | 6 +- .../IdentityRoleDetailsSection.tsx | 6 +- .../components/ProjectIdentityAuthSection.tsx | 143 +++++---- .../ProjectIdentityDetailsSection.tsx | 25 +- 42 files changed, 599 insertions(+), 682 deletions(-) create mode 100644 frontend/src/components/v3/generic/Accordion/Accordion.tsx create mode 100644 frontend/src/components/v3/generic/Accordion/index.ts create mode 100644 frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityAuthFieldDisplay.tsx rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/IdentityAuthLockoutFields.tsx (100%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/IdentityTokenAuthTokensTable.tsx (100%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/IdentityUniversalAuthClientSecretsTable.tsx (100%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityAliCloudAuthContent.tsx (73%) create mode 100644 frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityAwsAuthContent.tsx (77%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityAzureAuthContent.tsx (76%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityContentWrapper.tsx (100%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityGcpAuthContent.tsx (80%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityJwtAuthContent.tsx (86%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityKubernetesAuthContent.tsx (86%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityLdapAuthContent.tsx (83%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityOciAuthContent.tsx (75%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityOidcAuthContent.tsx (84%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityTlsCertAuthContent.tsx (78%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityTokenAuthContent.tsx (75%) rename frontend/src/pages/organization/IdentityDetailsByIDPage/components/{ViewIdentityAuthModal => ViewIdentityAuth}/ViewIdentityUniversalAuthContent.tsx (86%) create mode 100644 frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/index.ts create mode 100644 frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/types/index.ts delete mode 100644 frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthFieldDisplay.tsx delete mode 100644 frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx delete mode 100644 frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/index.ts delete mode 100644 frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/types/index.ts diff --git a/frontend/src/components/v3/generic/Accordion/Accordion.tsx b/frontend/src/components/v3/generic/Accordion/Accordion.tsx new file mode 100644 index 000000000..dcf4d5559 --- /dev/null +++ b/frontend/src/components/v3/generic/Accordion/Accordion.tsx @@ -0,0 +1,79 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "lucide-react"; + +import { cn } from "../../utils"; + +function UnstableAccordion({ ...props }: React.ComponentProps) { + return ( + + ); +} + +function UnstableAccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function UnstableAccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + "cursor-pointer hover:bg-foreground/5", + "data-[state=open]:bg-foreground/5", + className + )} + {...props} + > + + {children} + + + ); +} + +function UnstableAccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ); +} + +export { + UnstableAccordion, + UnstableAccordionContent, + UnstableAccordionItem, + UnstableAccordionTrigger +}; diff --git a/frontend/src/components/v3/generic/Accordion/index.ts b/frontend/src/components/v3/generic/Accordion/index.ts new file mode 100644 index 000000000..16e0243c2 --- /dev/null +++ b/frontend/src/components/v3/generic/Accordion/index.ts @@ -0,0 +1 @@ +export * from "./Accordion"; diff --git a/frontend/src/components/v3/generic/Badge/Badge.tsx b/frontend/src/components/v3/generic/Badge/Badge.tsx index 9dae877ab..a31f279f2 100644 --- a/frontend/src/components/v3/generic/Badge/Badge.tsx +++ b/frontend/src/components/v3/generic/Badge/Badge.tsx @@ -25,8 +25,8 @@ const badgeVariants = cva( }, variant: { ghost: "text-foreground border-none", - default: "bg-foreground text-background [a&,button&]:hover:bg-primary/35", - outline: "text-foreground border-foreground border", + default: "bg-label text-background border-label [a&,button&]:hover:bg-primary/35", + outline: "text-label border-label 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", diff --git a/frontend/src/components/v3/generic/Card/Card.tsx b/frontend/src/components/v3/generic/Card/Card.tsx index 5af0d8da6..f38930526 100644 --- a/frontend/src/components/v3/generic/Card/Card.tsx +++ b/frontend/src/components/v3/generic/Card/Card.tsx @@ -8,7 +8,10 @@ function UnstableCard({ className, ...props }: React.ComponentProps<"div">) { return (
); @@ -19,7 +22,8 @@ function UnstableCardHeader({ className, ...props }: React.ComponentProps<"div">
) return (
svg]:inline-block [&>svg]:size-[18px]", - className - )} + className={cn("text-lg leading-none font-semibold", className)} {...props} /> ); @@ -42,14 +43,7 @@ function UnstableCardTitle({ className, ...props }: React.ComponentProps<"div">) function CardDescription({ className, ...props }: React.ComponentProps<"div">) { return ( -
svg]:inline-block [&>svg]:size-[12px]", - className - )} - {...props} - /> +
); } diff --git a/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx b/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx index 64e8aa0fd..5a93c3fb6 100644 --- a/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx +++ b/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx @@ -36,7 +36,8 @@ function UnstableDropdownMenuContent({ sideOffset={sideOffset} className={cn( "max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin)", - "z-50 overflow-x-hidden overflow-y-auto rounded-[6px] border border-border/50 bg-popover p-1 text-xs text-foreground shadow-md", + "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", + "z-50 overflow-x-hidden overflow-y-auto rounded-[6px] border border-border bg-popover p-1 text-xs text-foreground shadow-md", className )} {...props} diff --git a/frontend/src/components/v3/generic/Empty/Empty.tsx b/frontend/src/components/v3/generic/Empty/Empty.tsx index f91ae9fb0..3b86bcc88 100644 --- a/frontend/src/components/v3/generic/Empty/Empty.tsx +++ b/frontend/src/components/v3/generic/Empty/Empty.tsx @@ -70,7 +70,7 @@ function UnstableEmptyDescription({ className, ...props }: React.ComponentProps<
a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", + "text-xs/relaxed text-muted [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-project", className )} {...props} diff --git a/frontend/src/components/v3/generic/IconButton/IconButton.tsx b/frontend/src/components/v3/generic/IconButton/IconButton.tsx index b10006aa4..d6fdfb4ce 100644 --- a/frontend/src/components/v3/generic/IconButton/IconButton.tsx +++ b/frontend/src/components/v3/generic/IconButton/IconButton.tsx @@ -35,8 +35,8 @@ const iconButtonVariants = cva( "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]", + xs: "h-7 w-7 [&>svg]:size-3.5 [&>svg]:stroke-[1.75]", + sm: "h-8 w-8 [&>svg]:size-4 [&>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]" }, diff --git a/frontend/src/components/v3/generic/Table/Table.tsx b/frontend/src/components/v3/generic/Table/Table.tsx index 45ae3210e..ffd25b766 100644 --- a/frontend/src/components/v3/generic/Table/Table.tsx +++ b/frontend/src/components/v3/generic/Table/Table.tsx @@ -8,7 +8,7 @@ function UnstableTable({ className, ...props }: React.ComponentProps<"table">) { return (
tr]:last:border-b-0", - className - )} + className={cn("border-t border-border font-medium [&>tr]:last:border-b-0", className)} {...props} /> ); @@ -53,7 +50,7 @@ function UnstableTableRow({ className, ...props }: React.ComponentProps<"tr">) { )
[role=checkbox]]:translate-y-[2px]", + "h-[30px] border-x-0 border-t-0 border-b border-border px-3 text-left align-middle text-xs whitespace-nowrap text-accent [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className )} {...props} @@ -91,7 +88,7 @@ function UnstableTableCaption({ className, ...props }: React.ComponentProps<"cap return (
); diff --git a/frontend/src/components/v3/generic/index.ts b/frontend/src/components/v3/generic/index.ts index de3288e4c..43ddf08b0 100644 --- a/frontend/src/components/v3/generic/index.ts +++ b/frontend/src/components/v3/generic/index.ts @@ -1,3 +1,4 @@ +export * from "./Accordion"; export * from "./Alert"; export * from "./Badge"; export * from "./Button"; diff --git a/frontend/src/hooks/api/shared/types.ts b/frontend/src/hooks/api/shared/types.ts index c57daf3fd..8a07ff4e6 100644 --- a/frontend/src/hooks/api/shared/types.ts +++ b/frontend/src/hooks/api/shared/types.ts @@ -18,7 +18,7 @@ export type TIdentity = { updatedAt: string; hasDeleteProtection: boolean; authMethods: IdentityAuthMethod[]; - activeLockoutAuthMethods: string[]; + activeLockoutAuthMethods: IdentityAuthMethod[]; metadata?: Array; }; diff --git a/frontend/src/index.css b/frontend/src/index.css index afd6c1b57..c1e564d92 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -49,14 +49,14 @@ --color-sub-org: #96ff59; --color-project: #e0ed34; --color-neutral: #adaeb0; - --color-border: #323439; + --color-border: #2b2c30; --color-label: #adaeb0; --color-muted: #707174; - --color-popover: #111419; + --color-popover: #141617; --color-ring: #2d2f33; - --color-container: #16181a; + --color-card: #16181a; --color-accent: #7d7f80; - --color-muted-foreground: ; + --color-container: #1a1c1e; /*legacy color schema */ --color-org-v1: #30b3ff; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 47428f2b8..f1533dacb 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -17,7 +17,6 @@ import { } from "@app/context"; import { useDeleteOrgIdentity, useGetOrgIdentityMembershipById } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; -import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal"; import { OrgAccessControlTabSections } from "@app/types/org"; import { IdentityAuthMethodModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; @@ -125,14 +124,16 @@ const Page = () => { identityId={identityId} handlePopUpOpen={handlePopUpOpen} /> + +
{!isAuthHidden && ( )} +
- )} @@ -171,14 +172,6 @@ const Page = () => { ) } /> - handlePopUpToggle("viewAuthMethod", isOpen)} - authMethod={popUp.viewAuthMethod.data?.authMethod} - lockedOut={popUp.viewAuthMethod.data?.lockedOut || false} - identityId={identityId} - onResetAllLockouts={popUp.viewAuthMethod.data?.refetchIdentity} - /> ); }; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx index dd6234268..aeed2af50 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx @@ -1,16 +1,14 @@ -import { faCog, faLock, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, Tooltip } from "@app/components/v2"; +import { Button } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; -import { - IdentityAuthMethod, - identityAuthToNameMap, - useGetOrgIdentityMembershipById -} from "@app/hooks/api"; +import { IdentityAuthMethod, useGetOrgIdentityMembershipById } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { ViewIdentityAuth } from "../ViewIdentityAuth"; + type Props = { identityId: string; handlePopUpOpen: ( @@ -23,37 +21,42 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P const { data, refetch } = useGetOrgIdentityMembershipById(identityId); return data ? ( -
-
+
+

Authentication

+ {!Object.values(IdentityAuthMethod).every((method) => + data.identity.authMethods.includes(method) + ) && ( + + {(isAllowed) => ( + + )} + + )}
{data.identity.authMethods.length > 0 ? ( -
- {data.identity.authMethods.map((authMethod) => ( - - ))} -
+ ) : (

@@ -61,30 +64,6 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P

)} - {!Object.values(IdentityAuthMethod).every((method) => - data.identity.authMethods.includes(method) - ) && ( - - {(isAllowed) => ( - - )} - - )}
) : (
diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityAuthFieldDisplay.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityAuthFieldDisplay.tsx new file mode 100644 index 000000000..5075d9643 --- /dev/null +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityAuthFieldDisplay.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from "react"; + +import { Detail, DetailLabel, DetailValue } from "@app/components/v3"; + +type Props = { + label: string; + children: ReactNode; + className?: string; +}; + +export const IdentityAuthFieldDisplay = ({ label, children, className }: Props) => { + return ( + + {label} + + {children ? ( +

{children}

+ ) : ( +

Not set

+ )} +
+
+ ); +}; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityAuthLockoutFields.tsx similarity index 100% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthLockoutFields.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityAuthLockoutFields.tsx diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityTokenAuthTokensTable.tsx similarity index 100% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityTokenAuthTokensTable.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityTokenAuthTokensTable.tsx diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityUniversalAuthClientSecretsTable.tsx similarity index 100% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityUniversalAuthClientSecretsTable.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/IdentityUniversalAuthClientSecretsTable.tsx diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAliCloudAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAliCloudAuthContent.tsx similarity index 73% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAliCloudAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAliCloudAuthContent.tsx index 5249d7f65..30837ea27 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAliCloudAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAliCloudAuthContent.tsx @@ -2,7 +2,6 @@ import { faBan } from "@fortawesome/free-solid-svg-icons"; import { EmptyState, Spinner } from "@app/components/v2"; import { useGetIdentityAliCloudAuth } from "@app/hooks/api"; -import { IdentityAliCloudAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { ViewAuthMethodProps } from "./types"; @@ -10,10 +9,8 @@ import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; export const ViewIdentityAliCloudAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, - onDelete, - popUp + onEdit, + onDelete }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityAliCloudAuth(identityId); @@ -34,23 +31,8 @@ export const ViewIdentityAliCloudAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx new file mode 100644 index 000000000..93fdc7b8b --- /dev/null +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx @@ -0,0 +1,276 @@ +import { useParams } from "@tanstack/react-router"; +import { EllipsisIcon, LockIcon } from "lucide-react"; + +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal, Modal, ModalContent, Tooltip } from "@app/components/v2"; +import { + Badge, + UnstableAccordion, + UnstableAccordionContent, + UnstableAccordionItem, + UnstableAccordionTrigger, + UnstableDropdownMenu, + UnstableDropdownMenuContent, + UnstableDropdownMenuItem, + UnstableDropdownMenuTrigger, + UnstableIconButton +} from "@app/components/v3"; +import { useOrganization } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { + IdentityAuthMethod, + identityAuthToNameMap, + useDeleteIdentityAliCloudAuth, + useDeleteIdentityAwsAuth, + useDeleteIdentityAzureAuth, + useDeleteIdentityGcpAuth, + useDeleteIdentityJwtAuth, + useDeleteIdentityKubernetesAuth, + useDeleteIdentityLdapAuth, + useDeleteIdentityOciAuth, + useDeleteIdentityOidcAuth, + useDeleteIdentityTlsCertAuth, + useDeleteIdentityTokenAuth, + useDeleteIdentityUniversalAuth +} from "@app/hooks/api"; +import { IdentityAliCloudAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAliCloudAuthForm"; +import { IdentityAwsAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm"; +import { IdentityAzureAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm"; +import { IdentityGcpAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm"; +import { IdentityJwtAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm"; +import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm"; +import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm"; +import { IdentityOciAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm"; +import { IdentityOidcAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm"; +import { IdentityTlsCertAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm"; +import { IdentityTokenAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm"; +import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm"; + +import { ViewIdentityAliCloudAuthContent } from "./ViewIdentityAliCloudAuthContent"; +import { ViewIdentityAwsAuthContent } from "./ViewIdentityAwsAuthContent"; +import { ViewIdentityAzureAuthContent } from "./ViewIdentityAzureAuthContent"; +import { ViewIdentityGcpAuthContent } from "./ViewIdentityGcpAuthContent"; +import { ViewIdentityJwtAuthContent } from "./ViewIdentityJwtAuthContent"; +import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthContent"; +import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent"; +import { ViewIdentityOciAuthContent } from "./ViewIdentityOciAuthContent"; +import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent"; +import { ViewIdentityTlsCertAuthContent } from "./ViewIdentityTlsCertAuthContent"; +import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent"; +import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent"; + +type Props = { + identityId: string; + authMethods: IdentityAuthMethod[]; + onResetAllLockouts: () => void; + activeLockoutAuthMethods: IdentityAuthMethod[]; +}; + +const AuthMethodComponentMap = { + [IdentityAuthMethod.UNIVERSAL_AUTH]: ViewIdentityUniversalAuthContent, + [IdentityAuthMethod.TOKEN_AUTH]: ViewIdentityTokenAuthContent, + [IdentityAuthMethod.TLS_CERT_AUTH]: ViewIdentityTlsCertAuthContent, + [IdentityAuthMethod.KUBERNETES_AUTH]: ViewIdentityKubernetesAuthContent, + [IdentityAuthMethod.LDAP_AUTH]: ViewIdentityLdapAuthContent, + [IdentityAuthMethod.OCI_AUTH]: ViewIdentityOciAuthContent, + [IdentityAuthMethod.OIDC_AUTH]: ViewIdentityOidcAuthContent, + [IdentityAuthMethod.GCP_AUTH]: ViewIdentityGcpAuthContent, + [IdentityAuthMethod.AWS_AUTH]: ViewIdentityAwsAuthContent, + [IdentityAuthMethod.ALICLOUD_AUTH]: ViewIdentityAliCloudAuthContent, + [IdentityAuthMethod.AZURE_AUTH]: ViewIdentityAzureAuthContent, + [IdentityAuthMethod.JWT_AUTH]: ViewIdentityJwtAuthContent +}; + +const EditAuthMethodMap = { + [IdentityAuthMethod.KUBERNETES_AUTH]: IdentityKubernetesAuthForm, + [IdentityAuthMethod.GCP_AUTH]: IdentityGcpAuthForm, + [IdentityAuthMethod.TLS_CERT_AUTH]: IdentityTlsCertAuthForm, + [IdentityAuthMethod.AWS_AUTH]: IdentityAwsAuthForm, + [IdentityAuthMethod.AZURE_AUTH]: IdentityAzureAuthForm, + [IdentityAuthMethod.ALICLOUD_AUTH]: IdentityAliCloudAuthForm, + [IdentityAuthMethod.UNIVERSAL_AUTH]: IdentityUniversalAuthForm, + [IdentityAuthMethod.TOKEN_AUTH]: IdentityTokenAuthForm, + [IdentityAuthMethod.OCI_AUTH]: IdentityOciAuthForm, + [IdentityAuthMethod.OIDC_AUTH]: IdentityOidcAuthForm, + [IdentityAuthMethod.JWT_AUTH]: IdentityJwtAuthForm, + [IdentityAuthMethod.LDAP_AUTH]: IdentityLdapAuthForm +}; + +export const Content = ({ + identityId, + authMethods, + onResetAllLockouts, + activeLockoutAuthMethods +}: Pick< + Props, + "authMethods" | "identityId" | "onResetAllLockouts" | "activeLockoutAuthMethods" +>) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { projectId } = useParams({ + strict: false + }); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "revokeAuthMethod", + "identityAuthMethod", + "upgradePlan" + ] as const); + + const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth(); + const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth(); + const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth(); + const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth(); + const { mutateAsync: revokeTlsCertAuth } = useDeleteIdentityTlsCertAuth(); + const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); + const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); + const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth(); + const { mutateAsync: revokeOciAuth } = useDeleteIdentityOciAuth(); + const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); + const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth(); + const { mutateAsync: revokeLdapAuth } = useDeleteIdentityLdapAuth(); + + const RemoveAuthMethodMap = { + [IdentityAuthMethod.KUBERNETES_AUTH]: revokeKubernetesAuth, + [IdentityAuthMethod.GCP_AUTH]: revokeGcpAuth, + [IdentityAuthMethod.TLS_CERT_AUTH]: revokeTlsCertAuth, + [IdentityAuthMethod.AWS_AUTH]: revokeAwsAuth, + [IdentityAuthMethod.AZURE_AUTH]: revokeAzureAuth, + [IdentityAuthMethod.ALICLOUD_AUTH]: revokeAliCloudAuth, + [IdentityAuthMethod.UNIVERSAL_AUTH]: revokeUniversalAuth, + [IdentityAuthMethod.TOKEN_AUTH]: revokeTokenAuth, + [IdentityAuthMethod.OCI_AUTH]: revokeOciAuth, + [IdentityAuthMethod.OIDC_AUTH]: revokeOidcAuth, + [IdentityAuthMethod.JWT_AUTH]: revokeJwtAuth, + [IdentityAuthMethod.LDAP_AUTH]: revokeLdapAuth + }; + + const handleDeleteAuthMethod = async (authMethod: IdentityAuthMethod) => { + await RemoveAuthMethodMap[authMethod]({ + identityId, + ...(projectId + ? { projectId } + : { + organizationId: orgId + }) + }); + + createNotification({ + text: "Successfully removed auth method", + type: "success" + }); + handlePopUpToggle("revokeAuthMethod", false); + }; + + const EditForm = popUp.identityAuthMethod?.data + ? EditAuthMethodMap[popUp.identityAuthMethod.data as IdentityAuthMethod] + : null; + + return ( + <> + + {authMethods.map((authMethod) => { + const Component = AuthMethodComponentMap[authMethod]; + + return ( + + + {identityAuthToNameMap[authMethod]} + {activeLockoutAuthMethods?.includes(authMethod) && ( + + + + + + )} + + + + + + + + { + e.stopPropagation(); + handlePopUpOpen("identityAuthMethod", authMethod); + }} + > + Edit Auth Method + + { + e.stopPropagation(); + handlePopUpOpen("revokeAuthMethod", authMethod); + }} + variant="danger" + > + Remove Auth Method + + + + + + handlePopUpOpen("identityAuthMethod", authMethod)} + onDelete={() => handlePopUpOpen("revokeAuthMethod", authMethod)} + onResetAllLockouts={onResetAllLockouts} + lockedOut={activeLockoutAuthMethods?.includes(authMethod)} + /> + + + ); + })} + + handlePopUpToggle("revokeAuthMethod", isOpen)} + deleteKey="confirm" + buttonText="Remove" + onDeleteApproved={() => + handleDeleteAuthMethod(popUp?.revokeAuthMethod?.data as IdentityAuthMethod) + } + /> + handlePopUpToggle("identityAuthMethod", isOpen)} + > + + {EditForm && ( + + )} + + + handlePopUpToggle("upgradePlan", isOpen)} + text={`Your current plan does not include access to ${popUp.upgradePlan.data?.featureName}. To unlock this feature, please upgrade to Infisical ${popUp.upgradePlan.data?.isEnterpriseFeature ? "Enterprise" : "Pro"} plan.`} + /> + + ); +}; + +export const ViewIdentityAuth = ({ + authMethods, + identityId, + onResetAllLockouts, + activeLockoutAuthMethods +}: Props) => { + return ( + onResetAllLockouts()} + /> + ); +}; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAwsAuthContent.tsx similarity index 77% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAwsAuthContent.tsx index dde9f1d2a..c16a8c3a1 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAwsAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAwsAuthContent.tsx @@ -2,7 +2,6 @@ import { faBan } from "@fortawesome/free-solid-svg-icons"; import { EmptyState, Spinner } from "@app/components/v2"; import { useGetIdentityAwsAuth } from "@app/hooks/api"; -import { IdentityAwsAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { ViewAuthMethodProps } from "./types"; @@ -10,10 +9,8 @@ import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; export const ViewIdentityAwsAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, - onDelete, - popUp + onEdit, + onDelete }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityAwsAuth(identityId); @@ -31,23 +28,8 @@ export const ViewIdentityAwsAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAzureAuthContent.tsx similarity index 76% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAzureAuthContent.tsx index 82e4c69e0..5f25f1a50 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAzureAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAzureAuthContent.tsx @@ -2,7 +2,6 @@ import { faBan } from "@fortawesome/free-solid-svg-icons"; import { EmptyState, Spinner } from "@app/components/v2"; import { useGetIdentityAzureAuth } from "@app/hooks/api"; -import { IdentityAzureAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { ViewAuthMethodProps } from "./types"; @@ -10,10 +9,8 @@ import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; export const ViewIdentityAzureAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, - onDelete, - popUp + onEdit, + onDelete }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityAzureAuth(identityId); @@ -31,23 +28,8 @@ export const ViewIdentityAzureAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityContentWrapper.tsx similarity index 100% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityContentWrapper.tsx diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityGcpAuthContent.tsx similarity index 80% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityGcpAuthContent.tsx index 296b4d764..5f24ec54c 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityGcpAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityGcpAuthContent.tsx @@ -2,7 +2,6 @@ import { faBan } from "@fortawesome/free-solid-svg-icons"; import { EmptyState, Spinner } from "@app/components/v2"; import { useGetIdentityGcpAuth } from "@app/hooks/api"; -import { IdentityGcpAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { ViewAuthMethodProps } from "./types"; @@ -10,10 +9,8 @@ import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; export const ViewIdentityGcpAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, - onDelete, - popUp + onEdit, + onDelete }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityGcpAuth(identityId); @@ -31,23 +28,8 @@ export const ViewIdentityGcpAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityJwtAuthContent.tsx similarity index 86% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityJwtAuthContent.tsx index 7fb81d2e1..a372491eb 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityJwtAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityJwtAuthContent.tsx @@ -5,18 +5,15 @@ import { EmptyState, Spinner, Tooltip } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { useGetIdentityJwtAuth } from "@app/hooks/api"; import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums"; -import { IdentityJwtAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm"; -import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper"; +import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityContentWrapper"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { ViewAuthMethodProps } from "./types"; export const ViewIdentityJwtAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, - onDelete, - popUp + onEdit, + onDelete }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityJwtAuth(identityId); @@ -34,23 +31,8 @@ export const ViewIdentityJwtAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityKubernetesAuthContent.tsx similarity index 86% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityKubernetesAuthContent.tsx index 02b0e0c88..e6992ca2e 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityKubernetesAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityKubernetesAuthContent.tsx @@ -6,7 +6,6 @@ import { EyeIcon } from "lucide-react"; import { EmptyState, Spinner, Tooltip } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { gatewaysQueryKeys, useGetIdentityKubernetesAuth } from "@app/hooks/api"; -import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { ViewAuthMethodProps } from "./types"; @@ -14,10 +13,8 @@ import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; export const ViewIdentityKubernetesAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, - onDelete, - popUp + onEdit, + onDelete }: ViewAuthMethodProps) => { const { data: gateways } = useQuery(gatewaysQueryKeys.list()); @@ -44,23 +41,8 @@ export const ViewIdentityKubernetesAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityLdapAuthContent.tsx similarity index 83% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityLdapAuthContent.tsx index b6db96c41..c835fd97c 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityLdapAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityLdapAuthContent.tsx @@ -4,8 +4,7 @@ import { EyeIcon } from "lucide-react"; import { EmptyState, Spinner, Tooltip } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { useClearIdentityLdapAuthLockouts, useGetIdentityLdapAuth } from "@app/hooks/api"; -import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm"; -import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper"; +import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityContentWrapper"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { LockoutFields } from "./IdentityAuthLockoutFields"; @@ -13,10 +12,8 @@ import { ViewAuthMethodProps } from "./types"; export const ViewIdentityLdapAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, onDelete, - popUp, + onEdit, lockedOut, onResetAllLockouts }: ViewAuthMethodProps) => { @@ -37,23 +34,8 @@ export const ViewIdentityLdapAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityOciAuthContent.tsx similarity index 75% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityOciAuthContent.tsx index c577c01e0..7b7e58258 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOciAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityOciAuthContent.tsx @@ -2,7 +2,6 @@ import { faBan } from "@fortawesome/free-solid-svg-icons"; import { EmptyState, Spinner } from "@app/components/v2"; import { useGetIdentityOciAuth } from "@app/hooks/api"; -import { IdentityOciAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOciAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { ViewAuthMethodProps } from "./types"; @@ -10,10 +9,8 @@ import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; export const ViewIdentityOciAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, - onDelete, - popUp + onEdit, + onDelete }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityOciAuth(identityId); @@ -31,23 +28,8 @@ export const ViewIdentityOciAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityOidcAuthContent.tsx similarity index 84% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityOidcAuthContent.tsx index dc7e5b985..27b487a3f 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityOidcAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityOidcAuthContent.tsx @@ -4,18 +4,15 @@ import { EyeIcon } from "lucide-react"; import { EmptyState, Spinner, Tooltip } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { useGetIdentityOidcAuth } from "@app/hooks/api"; -import { IdentityOidcAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm"; -import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper"; +import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityContentWrapper"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { ViewAuthMethodProps } from "./types"; export const ViewIdentityOidcAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, - onDelete, - popUp + onEdit, + onDelete }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityOidcAuth(identityId); @@ -33,23 +30,8 @@ export const ViewIdentityOidcAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityTlsCertAuthContent.tsx similarity index 78% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityTlsCertAuthContent.tsx index 03c4989a6..6ffefbeb6 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTlsCertAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityTlsCertAuthContent.tsx @@ -4,7 +4,6 @@ import { EyeIcon } from "lucide-react"; import { EmptyState, Spinner, Tooltip } from "@app/components/v2"; import { Badge } from "@app/components/v3"; import { useGetIdentityTlsCertAuth } from "@app/hooks/api"; -import { IdentityTlsCertAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { ViewAuthMethodProps } from "./types"; @@ -12,10 +11,8 @@ import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; export const ViewIdentityTlsCertAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, - onDelete, - popUp + onEdit, + onDelete }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityTlsCertAuth(identityId); @@ -36,23 +33,8 @@ export const ViewIdentityTlsCertAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityTokenAuthContent.tsx similarity index 75% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityTokenAuthContent.tsx index d42f9d2ca..b86a4d9ad 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityTokenAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityTokenAuthContent.tsx @@ -2,7 +2,6 @@ import { faBan } from "@fortawesome/free-solid-svg-icons"; import { EmptyState, Spinner } from "@app/components/v2"; import { useGetIdentityTokenAuth, useGetIdentityTokensTokenAuth } from "@app/hooks/api"; -import { IdentityTokenAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { IdentityTokenAuthTokensTable } from "./IdentityTokenAuthTokensTable"; @@ -11,10 +10,8 @@ import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; export const ViewIdentityTokenAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, onDelete, - popUp + onEdit }: ViewAuthMethodProps) => { const { data, isPending } = useGetIdentityTokenAuth(identityId); const { data: tokens = [], isPending: clientSecretsPending } = @@ -34,23 +31,8 @@ export const ViewIdentityTokenAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {data.accessTokenTTL} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityUniversalAuthContent.tsx similarity index 86% rename from frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx rename to frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityUniversalAuthContent.tsx index e9840a28c..86a2e4fb9 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityUniversalAuthContent.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityUniversalAuthContent.tsx @@ -8,7 +8,6 @@ import { useGetIdentityUniversalAuth, useGetIdentityUniversalAuthClientSecrets } from "@app/hooks/api"; -import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { LockoutFields } from "./IdentityAuthLockoutFields"; @@ -18,10 +17,8 @@ import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper"; export const ViewIdentityUniversalAuthContent = ({ identityId, - handlePopUpToggle, - handlePopUpOpen, onDelete, - popUp, + onEdit, lockedOut, onResetAllLockouts }: ViewAuthMethodProps) => { @@ -51,23 +48,8 @@ export const ViewIdentityUniversalAuthContent = ({ ); } - if (popUp.identityAuthMethod.isOpen) { - return ( - - ); - } - return ( - handlePopUpOpen("identityAuthMethod")} - onDelete={onDelete} - identityId={identityId} - > + {Number(data.accessTokenPeriod) > 0 ? ( {data.accessTokenPeriod} diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/index.ts b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/index.ts new file mode 100644 index 000000000..951c84087 --- /dev/null +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/index.ts @@ -0,0 +1 @@ +export * from "./ViewIdentityAuth"; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/types/index.ts b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/types/index.ts new file mode 100644 index 000000000..e4b015410 --- /dev/null +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/types/index.ts @@ -0,0 +1,7 @@ +export type ViewAuthMethodProps = { + identityId: string; + onDelete: () => void; + onEdit: () => void; + lockedOut: boolean; + onResetAllLockouts: () => void; +}; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthFieldDisplay.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthFieldDisplay.tsx deleted file mode 100644 index eb81bbcef..000000000 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/IdentityAuthFieldDisplay.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ReactNode } from "react"; - -type Props = { - label: string; - children: ReactNode; - className?: string; -}; - -export const IdentityAuthFieldDisplay = ({ label, children, className }: Props) => { - return ( -
- {label} - {children ? ( -

{children}

- ) : ( -

Not set

- )} -
- ); -}; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx deleted file mode 100644 index 5c12ddd4c..000000000 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { useParams } from "@tanstack/react-router"; - -import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; -import { createNotification } from "@app/components/notifications"; -import { DeleteActionModal, Modal, ModalContent } from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { usePopUp } from "@app/hooks"; -import { - IdentityAuthMethod, - identityAuthToNameMap, - useDeleteIdentityAliCloudAuth, - useDeleteIdentityAwsAuth, - useDeleteIdentityAzureAuth, - useDeleteIdentityGcpAuth, - useDeleteIdentityJwtAuth, - useDeleteIdentityKubernetesAuth, - useDeleteIdentityLdapAuth, - useDeleteIdentityOciAuth, - useDeleteIdentityOidcAuth, - useDeleteIdentityTlsCertAuth, - useDeleteIdentityTokenAuth, - useDeleteIdentityUniversalAuth -} from "@app/hooks/api"; - -import { ViewAuthMethodProps } from "./types"; -import { ViewIdentityAliCloudAuthContent } from "./ViewIdentityAliCloudAuthContent"; -import { ViewIdentityAwsAuthContent } from "./ViewIdentityAwsAuthContent"; -import { ViewIdentityAzureAuthContent } from "./ViewIdentityAzureAuthContent"; -import { ViewIdentityGcpAuthContent } from "./ViewIdentityGcpAuthContent"; -import { ViewIdentityJwtAuthContent } from "./ViewIdentityJwtAuthContent"; -import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthContent"; -import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent"; -import { ViewIdentityOciAuthContent } from "./ViewIdentityOciAuthContent"; -import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent"; -import { ViewIdentityTlsCertAuthContent } from "./ViewIdentityTlsCertAuthContent"; -import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent"; -import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent"; - -type Props = { - identityId: string; - authMethod?: IdentityAuthMethod; - lockedOut: boolean; - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; - onDeleteAuthMethod: () => void; - onResetAllLockouts: () => void; -}; - -type TRevokeOptions = { - identityId: string; -} & ({ projectId: string } | { organizationId: string }); - -export const Content = ({ - identityId, - authMethod, - lockedOut, - onDeleteAuthMethod, - onResetAllLockouts -}: Pick< - Props, - "authMethod" | "lockedOut" | "identityId" | "onDeleteAuthMethod" | "onResetAllLockouts" ->) => { - const { currentOrg } = useOrganization(); - const orgId = currentOrg?.id || ""; - const { projectId } = useParams({ - strict: false - }); - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ - "revokeAuthMethod", - "upgradePlan", - "identityAuthMethod" - ] as const); - - const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth(); - const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth(); - const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth(); - const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth(); - const { mutateAsync: revokeTlsCertAuth } = useDeleteIdentityTlsCertAuth(); - const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); - const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); - const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth(); - const { mutateAsync: revokeOciAuth } = useDeleteIdentityOciAuth(); - const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); - const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth(); - const { mutateAsync: revokeLdapAuth } = useDeleteIdentityLdapAuth(); - - let Component: (props: ViewAuthMethodProps) => JSX.Element; - let revokeMethod: (revokeOptions: TRevokeOptions) => Promise; - - const handleDelete = () => handlePopUpOpen("revokeAuthMethod"); - - switch (authMethod) { - case IdentityAuthMethod.UNIVERSAL_AUTH: - revokeMethod = revokeUniversalAuth; - Component = ViewIdentityUniversalAuthContent; - break; - case IdentityAuthMethod.TOKEN_AUTH: - revokeMethod = revokeTokenAuth; - Component = ViewIdentityTokenAuthContent; - break; - case IdentityAuthMethod.KUBERNETES_AUTH: - revokeMethod = revokeKubernetesAuth; - Component = ViewIdentityKubernetesAuthContent; - break; - case IdentityAuthMethod.GCP_AUTH: - revokeMethod = revokeGcpAuth; - Component = ViewIdentityGcpAuthContent; - break; - case IdentityAuthMethod.TLS_CERT_AUTH: - revokeMethod = revokeTlsCertAuth; - Component = ViewIdentityTlsCertAuthContent; - break; - case IdentityAuthMethod.AWS_AUTH: - revokeMethod = revokeAwsAuth; - Component = ViewIdentityAwsAuthContent; - break; - case IdentityAuthMethod.AZURE_AUTH: - revokeMethod = revokeAzureAuth; - Component = ViewIdentityAzureAuthContent; - break; - case IdentityAuthMethod.OCI_AUTH: - revokeMethod = revokeOciAuth; - Component = ViewIdentityOciAuthContent; - break; - case IdentityAuthMethod.ALICLOUD_AUTH: - revokeMethod = revokeAliCloudAuth; - Component = ViewIdentityAliCloudAuthContent; - break; - case IdentityAuthMethod.OIDC_AUTH: - revokeMethod = revokeOidcAuth; - Component = ViewIdentityOidcAuthContent; - break; - case IdentityAuthMethod.JWT_AUTH: - revokeMethod = revokeJwtAuth; - Component = ViewIdentityJwtAuthContent; - break; - case IdentityAuthMethod.LDAP_AUTH: - revokeMethod = revokeLdapAuth; - Component = ViewIdentityLdapAuthContent; - break; - default: - throw new Error(`Unhandled Auth Method: ${authMethod}`); - } - - const handleDeleteAuthMethod = async () => { - await revokeMethod({ - identityId, - ...(projectId - ? { projectId } - : { - organizationId: orgId - }) - }); - - createNotification({ - text: "Successfully removed auth method", - type: "success" - }); - handlePopUpToggle("revokeAuthMethod", false); - onDeleteAuthMethod(); - }; - - return ( - <> - - handlePopUpToggle("revokeAuthMethod", isOpen)} - deleteKey="confirm" - buttonText="Remove" - onDeleteApproved={handleDeleteAuthMethod} - /> - handlePopUpToggle("upgradePlan", isOpen)} - text={`Your current plan does not include access to ${popUp.upgradePlan.data?.featureName}. To unlock this feature, please upgrade to Infisical ${popUp.upgradePlan.data?.isEnterpriseFeature ? "Enterprise" : "Pro"} plan.`} - /> - - ); -}; - -export const ViewIdentityAuthModal = ({ - isOpen, - onOpenChange, - authMethod, - identityId, - lockedOut, - onResetAllLockouts -}: Omit) => { - if (!identityId || !authMethod) return null; - - return ( - - - onOpenChange(false)} - onResetAllLockouts={() => onResetAllLockouts()} - /> - - - ); -}; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/index.ts b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/index.ts deleted file mode 100644 index 2da1344a8..000000000 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ViewIdentityAuthModal"; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/types/index.ts b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/types/index.ts deleted file mode 100644 index da31a233c..000000000 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/types/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { UsePopUpState } from "@app/hooks/usePopUp"; - -export type ViewAuthMethodProps = { - identityId: string; - onDelete: () => void; - handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan", "identityAuthMethod"]>) => void; - handlePopUpToggle: ( - popUpName: keyof UsePopUpState<["identityAuthMethod"]>, - state?: boolean - ) => void; - popUp: UsePopUpState<["revokeAuthMethod", "upgradePlan", "identityAuthMethod"]>; - lockedOut: boolean; - onResetAllLockouts: () => void; -}; diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index d8fbd73d7..909f60282 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -179,14 +179,13 @@ const Page = () => { search={{ selectedTab: ProjectAccessControlTabs.Identities }} - className="mb-3 flex w-fit items-center gap-x-1 text-sm text-mineshaft-400 transition duration-100 hover:text-mineshaft-400/80" + className="mb-4 flex w-fit items-center gap-x-1 text-sm text-mineshaft-400 transition duration-100 hover:text-mineshaft-400/80" > Project Machine Identities @@ -256,14 +255,14 @@ const Page = () => { -
+
-
+
{identity ? ( - + Project Additional Privileges Assign one-off policies to this machine identity @@ -96,7 +98,7 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe > {(isAllowed) => ( { handlePopUpOpen("modifyPrivilege"); diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx index a0905abb4..9d88924b3 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityRoleDetailsSection/IdentityRoleDetailsSection.tsx @@ -96,7 +96,9 @@ export const IdentityRoleDetailsSection = ({ return ( <> - + Project Roles Manage roles assigned to this machine identity @@ -112,7 +114,7 @@ export const IdentityRoleDetailsSection = ({ {(isAllowed) => ( { handlePopUpOpen("modifyRole"); }} diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx index 6f1bfe464..6311267a3 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx @@ -1,11 +1,11 @@ import { subject } from "@casl/ability"; -import { EllipsisIcon, LockIcon, PlusIcon } from "lucide-react"; +import { PlusIcon } from "lucide-react"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; +import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { Tooltip } from "@app/components/v2"; +import { DeleteActionModal } from "@app/components/v2"; import { - Badge, UnstableButton, UnstableCard, UnstableCardAction, @@ -17,20 +17,29 @@ import { UnstableEmptyContent, UnstableEmptyDescription, UnstableEmptyHeader, - UnstableEmptyTitle, - UnstableIconButton, - UnstableTable, - UnstableTableBody, - UnstableTableCell, - UnstableTableHead, - UnstableTableHeader, - UnstableTableRow + UnstableEmptyTitle } from "@app/components/v3"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/context"; -import { IdentityAuthMethod, identityAuthToNameMap, TProjectIdentity } from "@app/hooks/api"; +import { + IdentityAuthMethod, + identityAuthToNameMap, + TProjectIdentity, + useDeleteIdentityAliCloudAuth, + useDeleteIdentityAwsAuth, + useDeleteIdentityAzureAuth, + useDeleteIdentityGcpAuth, + useDeleteIdentityJwtAuth, + useDeleteIdentityKubernetesAuth, + useDeleteIdentityLdapAuth, + useDeleteIdentityOciAuth, + useDeleteIdentityOidcAuth, + useDeleteIdentityTlsCertAuth, + useDeleteIdentityTokenAuth, + useDeleteIdentityUniversalAuth +} from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAuthMethodModal } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; -import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal"; +import { ViewIdentityAuth } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth"; type Props = { identity: TProjectIdentity; @@ -39,17 +48,58 @@ type Props = { export const ProjectIdentityAuthenticationSection = ({ identity, refetchIdentity }: Props) => { const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ - "viewAuthMethod", "identityAuthMethod", - "upgradePlan" + "upgradePlan", + "revokeAuthMethod" ]); const hasAuthMethods = Boolean(identity.authMethods.length); + const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth(); + const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth(); + const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth(); + const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth(); + const { mutateAsync: revokeTlsCertAuth } = useDeleteIdentityTlsCertAuth(); + const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); + const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); + const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth(); + const { mutateAsync: revokeOciAuth } = useDeleteIdentityOciAuth(); + const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); + const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth(); + const { mutateAsync: revokeLdapAuth } = useDeleteIdentityLdapAuth(); + + const RemoveAuthMap = { + [IdentityAuthMethod.KUBERNETES_AUTH]: revokeKubernetesAuth, + [IdentityAuthMethod.GCP_AUTH]: revokeGcpAuth, + [IdentityAuthMethod.TLS_CERT_AUTH]: revokeTlsCertAuth, + [IdentityAuthMethod.AWS_AUTH]: revokeAwsAuth, + [IdentityAuthMethod.AZURE_AUTH]: revokeAzureAuth, + [IdentityAuthMethod.ALICLOUD_AUTH]: revokeAliCloudAuth, + [IdentityAuthMethod.UNIVERSAL_AUTH]: revokeUniversalAuth, + [IdentityAuthMethod.TOKEN_AUTH]: revokeTokenAuth, + [IdentityAuthMethod.OCI_AUTH]: revokeOciAuth, + [IdentityAuthMethod.OIDC_AUTH]: revokeOidcAuth, + [IdentityAuthMethod.JWT_AUTH]: revokeJwtAuth, + [IdentityAuthMethod.LDAP_AUTH]: revokeLdapAuth + }; + + const handleDeleteAuthMethod = async (authMethod: IdentityAuthMethod) => { + await RemoveAuthMap[authMethod]({ + identityId: identity.id, + projectId: identity.projectId! + }); + + createNotification({ + text: "Successfully removed auth method", + type: "success" + }); + handlePopUpToggle("revokeAuthMethod", false); + }; + return ( <> - + Authentication Configure authentication methods {hasAuthMethods && @@ -65,7 +115,7 @@ export const ProjectIdentityAuthenticationSection = ({ identity, refetchIdentity > {(isAllowed) => ( {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) && ( - - - - - - )} - - - -
-
-
- ))} -
-
+ ) : ( @@ -175,13 +194,15 @@ export const ProjectIdentityAuthenticationSection = ({ identity, refetchIdentity text={(popUp.upgradePlan?.data as { description: string })?.description} isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature} /> - handlePopUpToggle("viewAuthMethod", isOpen)} - authMethod={popUp.viewAuthMethod.data?.authMethod} - lockedOut={popUp.viewAuthMethod.data?.lockedOut || false} - identityId={identity.id} - onResetAllLockouts={popUp.viewAuthMethod.data?.refetchIdentity} + handlePopUpToggle("revokeAuthMethod", isOpen)} + deleteKey="confirm" + buttonText="Remove" + onDeleteApproved={() => + handleDeleteAuthMethod(popUp.revokeAuthMethod.data as IdentityAuthMethod) + } /> ); diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx index a5a9e3488..f26dd60da 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityDetailsSection.tsx @@ -12,7 +12,6 @@ import { DetailValue, OrgIcon, ProjectIcon, - UnstableButton, UnstableButtonGroup, UnstableCard, UnstableCardAction, @@ -44,8 +43,10 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members return ( <> - - + + Details Machine identity details {!isOrgIdentity && ( @@ -57,17 +58,16 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members })} > {(isAllowed) => ( - { handlePopUpOpen("editIdentity"); }} size="xs" - variant="project" + variant="outline" > - Edit Details - + )} @@ -92,6 +92,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members variant="ghost" size="xs" > + {/* TODO(scott): color this should be a button variant */} {isCopyingId ? : } @@ -118,9 +119,13 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members {identity?.metadata?.length ? ( identity.metadata?.map((el) => ( - - {el.key} - {el.value} + + + {el.key} + + + {el.value} + )) ) : ( From c6da76206fe975366accfc1665b3324a395cd767 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 8 Dec 2025 16:35:20 -0800 Subject: [PATCH 5/9] improvement: add missing permission checks on frontend --- .../ViewIdentityAuth/ViewIdentityAuth.tsx | 78 +++++++++++++++---- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx index 93fdc7b8b..d6e322487 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth/ViewIdentityAuth.tsx @@ -1,8 +1,10 @@ +import { subject } from "@casl/ability"; import { useParams } from "@tanstack/react-router"; import { EllipsisIcon, LockIcon } from "lucide-react"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; +import { VariablePermissionCan } from "@app/components/permissions"; import { DeleteActionModal, Modal, ModalContent, Tooltip } from "@app/components/v2"; import { Badge, @@ -16,7 +18,13 @@ import { UnstableDropdownMenuTrigger, UnstableIconButton } from "@app/components/v3"; -import { useOrganization } from "@app/context"; +import { + OrgPermissionIdentityActions, + OrgPermissionSubjects, + ProjectPermissionIdentityActions, + ProjectPermissionSub, + useOrganization +} from "@app/context"; import { usePopUp } from "@app/hooks"; import { IdentityAuthMethod, @@ -190,23 +198,61 @@ export const Content = ({ - { - e.stopPropagation(); - handlePopUpOpen("identityAuthMethod", authMethod); - }} + - Edit Auth Method - - { - e.stopPropagation(); - handlePopUpOpen("revokeAuthMethod", authMethod); - }} - variant="danger" + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("identityAuthMethod", authMethod); + }} + > + Edit Auth Method + + )} + + - Remove Auth Method - + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("revokeAuthMethod", authMethod); + }} + variant="danger" + > + Remove Auth Method + + )} + From ac1b7d8a08ecab95d990b5bcd93da1c6767fc619 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 8 Dec 2025 16:43:01 -0800 Subject: [PATCH 6/9] improvements: update alert styling and remove border from org identity auth display --- frontend/src/components/v3/generic/Alert/Alert.tsx | 2 +- .../project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/v3/generic/Alert/Alert.tsx b/frontend/src/components/v3/generic/Alert/Alert.tsx index a48a4b7fb..a5de2cb91 100644 --- a/frontend/src/components/v3/generic/Alert/Alert.tsx +++ b/frontend/src/components/v3/generic/Alert/Alert.tsx @@ -6,7 +6,7 @@ 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", + "relative w-full 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: { diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 909f60282..cf0520f7f 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -270,7 +270,7 @@ const Page = () => { /> ) : ( - + Authentication Configure authentication methods From 6f7e2638243dde98fd634dd2731fcef674b7a00f Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 8 Dec 2025 17:05:27 -0800 Subject: [PATCH 7/9] chore: remove dead code from exploration --- .../components/ProjectIdentityAuthSection.tsx | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx index 6311267a3..64066957c 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx @@ -55,47 +55,6 @@ export const ProjectIdentityAuthenticationSection = ({ identity, refetchIdentity const hasAuthMethods = Boolean(identity.authMethods.length); - const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth(); - const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth(); - const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth(); - const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth(); - const { mutateAsync: revokeTlsCertAuth } = useDeleteIdentityTlsCertAuth(); - const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth(); - const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth(); - const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth(); - const { mutateAsync: revokeOciAuth } = useDeleteIdentityOciAuth(); - const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth(); - const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth(); - const { mutateAsync: revokeLdapAuth } = useDeleteIdentityLdapAuth(); - - const RemoveAuthMap = { - [IdentityAuthMethod.KUBERNETES_AUTH]: revokeKubernetesAuth, - [IdentityAuthMethod.GCP_AUTH]: revokeGcpAuth, - [IdentityAuthMethod.TLS_CERT_AUTH]: revokeTlsCertAuth, - [IdentityAuthMethod.AWS_AUTH]: revokeAwsAuth, - [IdentityAuthMethod.AZURE_AUTH]: revokeAzureAuth, - [IdentityAuthMethod.ALICLOUD_AUTH]: revokeAliCloudAuth, - [IdentityAuthMethod.UNIVERSAL_AUTH]: revokeUniversalAuth, - [IdentityAuthMethod.TOKEN_AUTH]: revokeTokenAuth, - [IdentityAuthMethod.OCI_AUTH]: revokeOciAuth, - [IdentityAuthMethod.OIDC_AUTH]: revokeOidcAuth, - [IdentityAuthMethod.JWT_AUTH]: revokeJwtAuth, - [IdentityAuthMethod.LDAP_AUTH]: revokeLdapAuth - }; - - const handleDeleteAuthMethod = async (authMethod: IdentityAuthMethod) => { - await RemoveAuthMap[authMethod]({ - identityId: identity.id, - projectId: identity.projectId! - }); - - createNotification({ - text: "Successfully removed auth method", - type: "success" - }); - handlePopUpToggle("revokeAuthMethod", false); - }; - return ( <> @@ -194,16 +153,6 @@ export const ProjectIdentityAuthenticationSection = ({ identity, refetchIdentity text={(popUp.upgradePlan?.data as { description: string })?.description} isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature} /> - handlePopUpToggle("revokeAuthMethod", isOpen)} - deleteKey="confirm" - buttonText="Remove" - onDeleteApproved={() => - handleDeleteAuthMethod(popUp.revokeAuthMethod.data as IdentityAuthMethod) - } - /> ); }; From dbb4ccc75ec05308b851315f6f71e7c453c4897b Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 8 Dec 2025 17:07:48 -0800 Subject: [PATCH 8/9] chore: remove unused imports --- .../components/ProjectIdentityAuthSection.tsx | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx index 64066957c..f7cfb585c 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/ProjectIdentityAuthSection.tsx @@ -2,9 +2,7 @@ import { subject } from "@casl/ability"; import { PlusIcon } from "lucide-react"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; -import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { DeleteActionModal } from "@app/components/v2"; import { UnstableButton, UnstableCard, @@ -20,23 +18,7 @@ import { UnstableEmptyTitle } from "@app/components/v3"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/context"; -import { - IdentityAuthMethod, - identityAuthToNameMap, - TProjectIdentity, - useDeleteIdentityAliCloudAuth, - useDeleteIdentityAwsAuth, - useDeleteIdentityAzureAuth, - useDeleteIdentityGcpAuth, - useDeleteIdentityJwtAuth, - useDeleteIdentityKubernetesAuth, - useDeleteIdentityLdapAuth, - useDeleteIdentityOciAuth, - useDeleteIdentityOidcAuth, - useDeleteIdentityTlsCertAuth, - useDeleteIdentityTokenAuth, - useDeleteIdentityUniversalAuth -} from "@app/hooks/api"; +import { IdentityAuthMethod, TProjectIdentity } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { IdentityAuthMethodModal } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; import { ViewIdentityAuth } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth"; From 4e4e6f445819b012ec3480bd951f7490d95273f6 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 9 Dec 2025 11:01:38 -0800 Subject: [PATCH 9/9] chore: lint changes --- .../IdentityProjectAdditionalPrivilegeModifySection.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx index e2f6c9197..b51501d48 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/components/IdentityProjectAdditionalPrivilegeSection/IdentityProjectAdditionalPrivilegeModifySection.tsx @@ -208,7 +208,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ rightIcon={} isDisabled={isIdentityEditDisabled} className={twMerge( - "bg-mineshaft-600 hover:bg-mineshaft-500 w-full border-none py-2.5 text-xs capitalize", + "w-full border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500", isTemporary && "text-primary", isExpired && "text-red-600" )} @@ -226,7 +226,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({ className="border border-gray-600 pt-4" >
-
+
Configure Timed Access
{isExpired && Expired} @@ -306,7 +306,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
{isDirty && (