mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 07:28:09 -05:00
Merge pull request #4979 from Infisical/ENG-4250
improvement: project identity page UI improvements + v3 component progress
This commit is contained in:
@@ -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 <RouterProvider router={router as any} />;
|
||||
};
|
||||
|
||||
91
frontend/package-lock.json
generated
91
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -32,7 +32,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P
|
||||
<div className="mr-4 flex w-full items-center">
|
||||
<h1
|
||||
className={twMerge(
|
||||
"text-3xl font-medium text-white capitalize underline decoration-2 underline-offset-4",
|
||||
"text-2xl font-medium text-white capitalize underline decoration-2 underline-offset-4",
|
||||
scope === "org" && "decoration-org/90",
|
||||
scope === "instance" && "decoration-neutral/90",
|
||||
scope === "namespace" && "decoration-sub-org/90",
|
||||
@@ -51,6 +51,6 @@ export const PageHeader = ({ title, description, children, className, scope }: P
|
||||
</div>
|
||||
<div className="flex items-center gap-2">{children}</div>
|
||||
</div>
|
||||
<div className="mt-2 text-gray-400">{description}</div>
|
||||
<div className="mt-1.5 text-mineshaft-300">{description}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
79
frontend/src/components/v3/generic/Accordion/Accordion.tsx
Normal file
79
frontend/src/components/v3/generic/Accordion/Accordion.tsx
Normal file
@@ -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<typeof AccordionPrimitive.Root>) {
|
||||
return (
|
||||
<AccordionPrimitive.Root
|
||||
data-slot="accordion"
|
||||
className="border border-border bg-container"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableAccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b border-border last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableAccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"flex min-h-12 flex-1 items-center gap-4 border-border bg-container px-4 text-left text-sm font-medium",
|
||||
"transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||
"disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
"cursor-pointer hover:bg-foreground/5",
|
||||
"data-[state=open]:bg-foreground/5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0 text-label transition-transform duration-200" />
|
||||
{children}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableAccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm transition data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("p-6", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
UnstableAccordion,
|
||||
UnstableAccordionContent,
|
||||
UnstableAccordionItem,
|
||||
UnstableAccordionTrigger
|
||||
};
|
||||
1
frontend/src/components/v3/generic/Accordion/index.ts
Normal file
1
frontend/src/components/v3/generic/Accordion/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Accordion";
|
||||
62
frontend/src/components/v3/generic/Alert/Alert.tsx
Normal file
62
frontend/src/components/v3/generic/Alert/Alert.tsx
Normal file
@@ -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 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<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableAlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn("col-start-2 line-clamp-1 min-h-4 tracking-tight text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableAlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"col-start-2 grid justify-items-start gap-1 text-sm text-foreground/75 [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { UnstableAlert, UnstableAlertDescription, UnstableAlertTitle };
|
||||
1
frontend/src/components/v3/generic/Alert/index.ts
Normal file
1
frontend/src/components/v3/generic/Alert/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Alert";
|
||||
@@ -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<typeof meta>;
|
||||
|
||||
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 = {
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const KeyValuePair: Story = {
|
||||
name: "Example: Key-Value Pair",
|
||||
args: {},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use a default and outline badge in conjunction with the `<ButtonGroup />` component to display key-value pairs."
|
||||
}
|
||||
}
|
||||
},
|
||||
decorators: () => (
|
||||
<UnstableButtonGroup>
|
||||
<Badge>Key</Badge>
|
||||
<Badge variant="outline">Value</Badge>
|
||||
</UnstableButtonGroup>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -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-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",
|
||||
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<typeof badgeVariants> &
|
||||
React.ComponentProps<"span"> & {
|
||||
asChild?: boolean;
|
||||
variant: NonNullable<VariantProps<typeof badgeVariants>["variant"]>; // TODO: REMOVE
|
||||
};
|
||||
|
||||
const Badge = forwardRef<HTMLSpanElement, TBadgeProps>(
|
||||
|
||||
357
frontend/src/components/v3/generic/Button/Button.stories.tsx
Normal file
357
frontend/src/components/v3/generic/Button/Button.stories.tsx
Normal file
@@ -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<typeof UnstableButton>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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: (
|
||||
<>
|
||||
<BanIcon />
|
||||
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: (
|
||||
<>
|
||||
<UserIcon />
|
||||
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: (
|
||||
<>
|
||||
<CheckIcon />
|
||||
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: (
|
||||
<>
|
||||
<InfoIcon />
|
||||
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: (
|
||||
<>
|
||||
<TriangleAlertIcon />
|
||||
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: (
|
||||
<>
|
||||
<CircleXIcon />
|
||||
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: (
|
||||
<>
|
||||
<OrgIcon />
|
||||
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: (
|
||||
<>
|
||||
<SubOrgIcon />
|
||||
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: (
|
||||
<>
|
||||
<ProjectIcon />
|
||||
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 <ExternalLinkIcon />
|
||||
</>
|
||||
)
|
||||
},
|
||||
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: (
|
||||
<>
|
||||
<RadarIcon />
|
||||
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: (
|
||||
<>
|
||||
<AsteriskIcon />
|
||||
Secret Value
|
||||
</>
|
||||
)
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Use the `isFullWidth` prop to expand the Buttons width to fill it's parent container."
|
||||
}
|
||||
}
|
||||
},
|
||||
decorators: (Story) => (
|
||||
<div className="w-32">
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
};
|
||||
139
frontend/src/components/v3/generic/Button/Button.tsx
Normal file
139
frontend/src/components/v3/generic/Button/Button.tsx
Normal file
@@ -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<typeof buttonVariants> & {
|
||||
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<HTMLButtonElement | HTMLAnchorElement, UnstableButtonProps>(
|
||||
(
|
||||
{
|
||||
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 && (
|
||||
<Lottie
|
||||
icon={variant === "default" ? "infisical_loading_bw" : "infisical_loading_white"}
|
||||
isAutoPlay
|
||||
className="absolute w-8 rounded-xl"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
switch (props.as) {
|
||||
case "a":
|
||||
return (
|
||||
<a
|
||||
ref={ref as React.Ref<HTMLAnchorElement>}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
{...sharedProps}
|
||||
>
|
||||
{child}
|
||||
</a>
|
||||
);
|
||||
case "link":
|
||||
return (
|
||||
<Link ref={ref as React.Ref<HTMLAnchorElement>} {...props} {...sharedProps}>
|
||||
{child}
|
||||
</Link>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<button
|
||||
ref={ref as React.Ref<HTMLButtonElement>}
|
||||
type="button"
|
||||
disabled={isPending || isDisabled}
|
||||
{...props}
|
||||
{...sharedProps}
|
||||
>
|
||||
{child}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
UnstableButton.displayName = "Button";
|
||||
|
||||
export { buttonVariants, UnstableButton, type UnstableButtonProps };
|
||||
1
frontend/src/components/v3/generic/Button/index.ts
Normal file
1
frontend/src/components/v3/generic/Button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Button";
|
||||
@@ -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<typeof buttonGroupVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableButtonGroupText({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border bg-muted px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableButtonGroupSeparator({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof UnstableSeparator>) {
|
||||
return (
|
||||
<UnstableSeparator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
buttonGroupVariants,
|
||||
UnstableButtonGroup,
|
||||
UnstableButtonGroupSeparator,
|
||||
UnstableButtonGroupText
|
||||
};
|
||||
1
frontend/src/components/v3/generic/ButtonGroup/index.ts
Normal file
1
frontend/src/components/v3/generic/ButtonGroup/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./ButtonGroup";
|
||||
82
frontend/src/components/v3/generic/Card/Card.tsx
Normal file
82
frontend/src/components/v3/generic/Card/Card.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../../utils";
|
||||
|
||||
function UnstableCard({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"flex h-fit flex-col gap-6 rounded-md border border-border bg-card p-5 text-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableCardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-center gap-1",
|
||||
"border-border has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableCardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="card-description" className={cn("text-sm text-accent", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableCardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableCardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return <div data-slot="card-content" className={cn("", className)} {...props} />;
|
||||
}
|
||||
|
||||
function UnstableCardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center border-border [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
UnstableCard,
|
||||
UnstableCardAction,
|
||||
UnstableCardContent,
|
||||
CardDescription as UnstableCardDescription,
|
||||
UnstableCardFooter,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle
|
||||
};
|
||||
1
frontend/src/components/v3/generic/Card/index.ts
Normal file
1
frontend/src/components/v3/generic/Card/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Card";
|
||||
23
frontend/src/components/v3/generic/Detail/Detail.tsx
Normal file
23
frontend/src/components/v3/generic/Detail/Detail.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { cn } from "../../utils";
|
||||
|
||||
function Detail({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return <div data-slot="detail" className={cn("flex flex-col gap-y-1", className)} {...props} />;
|
||||
}
|
||||
|
||||
function DetailLabel({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="detail-label" className={cn("text-xs text-label", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DetailValue({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return <div data-slot="detail-value" className={cn("text-sm", className)} {...props} />;
|
||||
}
|
||||
|
||||
function DetailGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="detail-group" className={cn("flex flex-col gap-y-4", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Detail, DetailGroup, DetailLabel, DetailValue };
|
||||
1
frontend/src/components/v3/generic/Detail/index.ts
Normal file
1
frontend/src/components/v3/generic/Detail/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Detail";
|
||||
255
frontend/src/components/v3/generic/Dropdown/Dropdown.tsx
Normal file
255
frontend/src/components/v3/generic/Dropdown/Dropdown.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
/* 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<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin)",
|
||||
"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}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
isDisabled,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof DropdownMenuPrimitive.Item>, "disabled"> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "danger";
|
||||
isDisabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"text-xs",
|
||||
"data-[variant=danger]:text-danger data-[variant=danger]:focus:bg-danger/10 data-[variant=danger]:*:[svg]:!text-danger",
|
||||
"relative flex cursor-pointer items-center gap-2 rounded-sm px-2 pt-2 pb-1.5 outline-0 select-none focus:bg-foreground/10",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3",
|
||||
className
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-pointer items-center gap-2 rounded-sm pt-2 pr-8 pb-1.5 pl-2 text-xs outline-0 select-none",
|
||||
"focus:bg-foreground/10",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"[&_svg]:pointer-events-none [&_svg]:mb-0.5 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-pointer items-center gap-2 rounded-sm pt-2 pr-8 pb-1.5 pl-2 text-xs outline-0 select-none",
|
||||
"focus:bg-foreground/10",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"[&_svg]:pointer-events-none [&_svg]:mb-0.5 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("px-2 py-1.5 text-xs font-medium text-accent data-[inset]:pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn("ml-auto text-xs tracking-widest text-accent", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-xs outline-0 select-none focus:bg-foreground/10 data-[inset]:pl-8 data-[state=open]:bg-foreground/10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-3.5" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableDropdownMenuSubContent({
|
||||
className,
|
||||
sideOffset = 8,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-[6px] border border-border bg-popover p-1 text-foreground shadow-lg",
|
||||
className
|
||||
)}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type UnstableDropdownMenuChecked = DropdownMenuPrimitive.DropdownMenuCheckboxItemProps["checked"];
|
||||
|
||||
export {
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuCheckboxItem,
|
||||
type UnstableDropdownMenuChecked,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuGroup,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuLabel,
|
||||
UnstableDropdownMenuPortal,
|
||||
UnstableDropdownMenuRadioGroup,
|
||||
UnstableDropdownMenuRadioItem,
|
||||
UnstableDropdownMenuSeparator,
|
||||
UnstableDropdownMenuShortcut,
|
||||
UnstableDropdownMenuSub,
|
||||
UnstableDropdownMenuSubContent,
|
||||
UnstableDropdownMenuSubTrigger,
|
||||
UnstableDropdownMenuTrigger
|
||||
};
|
||||
1
frontend/src/components/v3/generic/Dropdown/index.ts
Normal file
1
frontend/src/components/v3/generic/Dropdown/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Dropdown";
|
||||
100
frontend/src/components/v3/generic/Empty/Empty.tsx
Normal file
100
frontend/src/components/v3/generic/Empty/Empty.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { cn } from "../../utils";
|
||||
|
||||
function UnstableEmpty({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty"
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 border-dashed border-border bg-container p-6 text-center text-balance md:p-12",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableEmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-header"
|
||||
className={cn("flex max-w-sm flex-col items-center gap-2 text-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 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<typeof emptyMediaVariants>) {
|
||||
// return (
|
||||
// <div
|
||||
// data-slot="empty-icon"
|
||||
// data-variant={variant}
|
||||
// className={cn(emptyMediaVariants({ variant, className }))}
|
||||
// {...props}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
function UnstableEmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-title"
|
||||
className={cn("text-sm font-medium tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableEmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-description"
|
||||
className={cn(
|
||||
"text-xs/relaxed text-muted [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-project",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableEmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-content"
|
||||
className={cn(
|
||||
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
UnstableEmpty,
|
||||
UnstableEmptyContent,
|
||||
UnstableEmptyDescription,
|
||||
UnstableEmptyHeader,
|
||||
UnstableEmptyTitle
|
||||
};
|
||||
1
frontend/src/components/v3/generic/Empty/index.ts
Normal file
1
frontend/src/components/v3/generic/Empty/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Empty";
|
||||
111
frontend/src/components/v3/generic/IconButton/IconButton.tsx
Normal file
111
frontend/src/components/v3/generic/IconButton/IconButton.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
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 { 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-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]"
|
||||
},
|
||||
isPending: {
|
||||
true: "text-transparent"
|
||||
},
|
||||
isFullWidth: {
|
||||
true: "w-full",
|
||||
false: "w-fit"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
type UnstableIconButtonProps = React.ComponentProps<"button"> &
|
||||
VariantProps<typeof iconButtonVariants> & {
|
||||
asChild?: boolean;
|
||||
isPending?: boolean;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const UnstableIconButton = forwardRef<HTMLButtonElement, UnstableIconButtonProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant = "default",
|
||||
size = "md",
|
||||
asChild = false,
|
||||
isPending = false,
|
||||
disabled = false,
|
||||
isDisabled = false,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
): JSX.Element => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-slot="button"
|
||||
className={cn(iconButtonVariants({ variant, size, isPending }), className)}
|
||||
disabled={isPending || disabled || isDisabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{isPending && (
|
||||
<Lottie
|
||||
icon={variant === "default" ? "infisical_loading_bw" : "infisical_loading_white"}
|
||||
isAutoPlay
|
||||
className={twMerge(
|
||||
"absolute rounded-xl",
|
||||
size === "xs" && "w-6",
|
||||
size === "sm" && "w-7",
|
||||
size === "md" && "w-8",
|
||||
size === "lg" && "w-9"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
UnstableIconButton.displayName = "IconButton";
|
||||
|
||||
export { iconButtonVariants, UnstableIconButton, type UnstableIconButtonProps };
|
||||
1
frontend/src/components/v3/generic/IconButton/index.ts
Normal file
1
frontend/src/components/v3/generic/IconButton/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./IconButton";
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Lottie } from "@app/components/v2";
|
||||
|
||||
export function UnstablePageLoader() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading" isAutoPlay className="w-24" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
frontend/src/components/v3/generic/PageLoader/index.ts
Normal file
1
frontend/src/components/v3/generic/PageLoader/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./PageLoader";
|
||||
28
frontend/src/components/v3/generic/Separator/Separator.tsx
Normal file
28
frontend/src/components/v3/generic/Separator/Separator.tsx
Normal file
@@ -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<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { UnstableSeparator };
|
||||
1
frontend/src/components/v3/generic/Separator/index.ts
Normal file
1
frontend/src/components/v3/generic/Separator/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Separator";
|
||||
138
frontend/src/components/v3/generic/Table/Table.stories.tsx
Normal file
138
frontend/src/components/v3/generic/Table/Table.stories.tsx
Normal file
@@ -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 (
|
||||
<UnstableTable className="w-[800px]">
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead className="w-1/3">Name</UnstableTableHead>
|
||||
<UnstableTableHead className="w-1/3">Role</UnstableTableHead>
|
||||
<UnstableTableHead className="w-1/3">Managed By</UnstableTableHead>
|
||||
<UnstableTableHead className="text-right" />
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{identities.map((identity) => (
|
||||
<UnstableTableRow key={identity.name}>
|
||||
<UnstableTableCell className="font-medium">{identity.name}</UnstableTableCell>
|
||||
<UnstableTableCell>{identity.role}</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
<Badge variant="project">
|
||||
<ProjectIcon />
|
||||
Project
|
||||
</Badge>
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell className="text-right">
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton variant="ghost" size="xs">
|
||||
<MoreHorizontalIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end" className="w-36">
|
||||
<UnstableDropdownMenuItem>
|
||||
<CopyIcon />
|
||||
Copy ID
|
||||
</UnstableDropdownMenuItem>
|
||||
<UnstableDropdownMenuItem>
|
||||
<EditIcon />
|
||||
Edit Identity
|
||||
</UnstableDropdownMenuItem>
|
||||
<UnstableDropdownMenuItem variant="danger">
|
||||
<TrashIcon />
|
||||
Delete Identity
|
||||
</UnstableDropdownMenuItem>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
</UnstableTableRow>
|
||||
))}
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Generic/Table",
|
||||
component: TableDemo,
|
||||
parameters: {
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {}
|
||||
} satisfies Meta<typeof TableDemo>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const KitchenSInk: Story = {
|
||||
name: "Example: Kitchen Sink",
|
||||
args: {}
|
||||
};
|
||||
106
frontend/src/components/v3/generic/Table/Table.tsx
Normal file
106
frontend/src/components/v3/generic/Table/Table.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
/* 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 (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto border border-border bg-container"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableTableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("text-sm [&_tr]:border-b [&_tr]:hover:bg-transparent", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableTableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody data-slot="table-body" className={cn("[&>tr]:last:border-b-0", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableTableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn("border-t border-border font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableTableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"border-b border-border transition-colors hover:bg-foreground/5 data-[state=selected]:bg-foreground/5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableTableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableTableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"h-[40px] px-3 align-middle whitespace-nowrap text-mineshaft-200 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableTableCaption({ className, ...props }: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
UnstableTable,
|
||||
UnstableTableBody,
|
||||
UnstableTableCaption,
|
||||
UnstableTableCell,
|
||||
UnstableTableFooter,
|
||||
UnstableTableHead,
|
||||
UnstableTableHeader,
|
||||
UnstableTableRow
|
||||
};
|
||||
1
frontend/src/components/v3/generic/Table/index.ts
Normal file
1
frontend/src/components/v3/generic/Table/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Table";
|
||||
@@ -1 +1,13 @@
|
||||
export * from "./Accordion";
|
||||
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";
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export type TIdentity = {
|
||||
updatedAt: string;
|
||||
hasDeleteProtection: boolean;
|
||||
authMethods: IdentityAuthMethod[];
|
||||
activeLockoutAuthMethods: string[];
|
||||
activeLockoutAuthMethods: IdentityAuthMethod[];
|
||||
metadata?: Array<TMetadata & { id: string }>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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: #2b2c30;
|
||||
--color-label: #adaeb0;
|
||||
--color-muted: #707174;
|
||||
--color-popover: #141617;
|
||||
--color-ring: #2d2f33;
|
||||
--color-card: #16181a;
|
||||
--color-accent: #7d7f80;
|
||||
--color-container: #1a1c1e;
|
||||
|
||||
/*legacy color schema */
|
||||
--color-org-v1: #30b3ff;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
{!isAuthHidden && (
|
||||
<IdentityAuthenticationSection
|
||||
identityId={identityId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
)}
|
||||
<IdentityProjectsSection identityId={identityId} />
|
||||
</div>
|
||||
<IdentityProjectsSection identityId={identityId} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -171,14 +172,6 @@ const Page = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ViewIdentityAuthModal
|
||||
isOpen={popUp.viewAuthMethod.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)}
|
||||
authMethod={popUp.viewAuthMethod.data?.authMethod}
|
||||
lockedOut={popUp.viewAuthMethod.data?.lockedOut || false}
|
||||
identityId={identityId}
|
||||
onResetAllLockouts={popUp.viewAuthMethod.data?.refetchIdentity}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 ? (
|
||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Authentication</h3>
|
||||
{!Object.values(IdentityAuthMethod).every((method) =>
|
||||
data.identity.authMethods.includes(method)
|
||||
) && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
allAuthMethods: data.identity.authMethods
|
||||
});
|
||||
}}
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
{data.identity.authMethods.length ? "Add" : "Create"} Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
{data.identity.authMethods.length > 0 ? (
|
||||
<div className="flex flex-col divide-y divide-mineshaft-400/50">
|
||||
{data.identity.authMethods.map((authMethod) => (
|
||||
<button
|
||||
key={authMethod}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("viewAuthMethod", {
|
||||
authMethod,
|
||||
lockedOut: data.identity.activeLockoutAuthMethods.includes(authMethod),
|
||||
refetchIdentity: refetch
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between bg-mineshaft-900 px-4 py-2 text-sm hover:bg-mineshaft-700 data-[state=open]:bg-mineshaft-600"
|
||||
>
|
||||
<span>{identityAuthToNameMap[authMethod]}</span>
|
||||
<div className="flex gap-2">
|
||||
{data.identity.activeLockoutAuthMethods.includes(authMethod) && (
|
||||
<Tooltip content="Auth method has active lockouts">
|
||||
<FontAwesomeIcon icon={faLock} size="xs" className="text-red-400/50" />
|
||||
</Tooltip>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<ViewIdentityAuth
|
||||
activeLockoutAuthMethods={data.identity.activeLockoutAuthMethods}
|
||||
identityId={identityId}
|
||||
authMethods={data.identity.authMethods}
|
||||
onResetAllLockouts={refetch}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full space-y-2 pt-2">
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
@@ -61,30 +64,6 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!Object.values(IdentityAuthMethod).every((method) =>
|
||||
data.identity.authMethods.includes(method)
|
||||
) && (
|
||||
<OrgPermissionCan I={OrgPermissionIdentityActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
allAuthMethods: data.identity.authMethods
|
||||
});
|
||||
}}
|
||||
variant="outline_bg"
|
||||
className="mt-3 w-full"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
{data.identity.authMethods.length ? "Add" : "Create"} Auth Method
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
|
||||
@@ -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 (
|
||||
<Detail className={className}>
|
||||
<DetailLabel>{label}</DetailLabel>
|
||||
<DetailValue>
|
||||
{children ? (
|
||||
<p className="break-words">{children}</p>
|
||||
) : (
|
||||
<p className="text-muted italic">Not set</p>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<IdentityAliCloudAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -0,0 +1,322 @@
|
||||
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,
|
||||
UnstableAccordion,
|
||||
UnstableAccordionContent,
|
||||
UnstableAccordionItem,
|
||||
UnstableAccordionTrigger,
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuTrigger,
|
||||
UnstableIconButton
|
||||
} from "@app/components/v3";
|
||||
import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects,
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSub,
|
||||
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 (
|
||||
<>
|
||||
<UnstableAccordion type={authMethods.length === 1 ? "single" : "multiple"} collapsible>
|
||||
{authMethods.map((authMethod) => {
|
||||
const Component = AuthMethodComponentMap[authMethod];
|
||||
|
||||
return (
|
||||
<UnstableAccordionItem key={authMethod} value={authMethod}>
|
||||
<UnstableAccordionTrigger>
|
||||
<span className="mr-auto">{identityAuthToNameMap[authMethod]}</span>
|
||||
{activeLockoutAuthMethods?.includes(authMethod) && (
|
||||
<Tooltip content="Auth method has active lockouts">
|
||||
<Badge isSquare variant="danger">
|
||||
<LockIcon />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton variant="ghost" size="xs">
|
||||
<EllipsisIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<VariablePermissionCan
|
||||
type={projectId ? "project" : "org"}
|
||||
I={
|
||||
projectId
|
||||
? ProjectPermissionIdentityActions.Edit
|
||||
: OrgPermissionIdentityActions.Edit
|
||||
}
|
||||
a={
|
||||
projectId
|
||||
? subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})
|
||||
: OrgPermissionSubjects.Identity
|
||||
}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("identityAuthMethod", authMethod);
|
||||
}}
|
||||
>
|
||||
Edit Auth Method
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</VariablePermissionCan>
|
||||
<VariablePermissionCan
|
||||
type={projectId ? "project" : "org"}
|
||||
I={
|
||||
projectId
|
||||
? ProjectPermissionIdentityActions.Delete
|
||||
: OrgPermissionIdentityActions.Delete
|
||||
}
|
||||
a={
|
||||
projectId
|
||||
? subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})
|
||||
: OrgPermissionSubjects.Identity
|
||||
}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("revokeAuthMethod", authMethod);
|
||||
}}
|
||||
variant="danger"
|
||||
>
|
||||
Remove Auth Method
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</VariablePermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableAccordionTrigger>
|
||||
<UnstableAccordionContent>
|
||||
<Component
|
||||
identityId={identityId}
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod", authMethod)}
|
||||
onDelete={() => handlePopUpOpen("revokeAuthMethod", authMethod)}
|
||||
onResetAllLockouts={onResetAllLockouts}
|
||||
lockedOut={activeLockoutAuthMethods?.includes(authMethod)}
|
||||
/>
|
||||
</UnstableAccordionContent>
|
||||
</UnstableAccordionItem>
|
||||
);
|
||||
})}
|
||||
</UnstableAccordion>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp?.revokeAuthMethod?.isOpen}
|
||||
title={`Are you sure you want to remove ${popUp?.revokeAuthMethod?.data ? identityAuthToNameMap[popUp?.revokeAuthMethod?.data as IdentityAuthMethod] : "this auth method"} on this identity?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Remove"
|
||||
onDeleteApproved={() =>
|
||||
handleDeleteAuthMethod(popUp?.revokeAuthMethod?.data as IdentityAuthMethod)
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={popUp?.identityAuthMethod?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("identityAuthMethod", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title={`Edit ${popUp?.identityAuthMethod?.data ? identityAuthToNameMap[popUp?.identityAuthMethod?.data as IdentityAuthMethod] : "this auth method"}`}
|
||||
>
|
||||
{EditForm && (
|
||||
<EditForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => 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 (
|
||||
<Content
|
||||
identityId={identityId}
|
||||
authMethods={authMethods}
|
||||
activeLockoutAuthMethods={activeLockoutAuthMethods}
|
||||
onResetAllLockouts={() => onResetAllLockouts()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<IdentityAwsAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityAzureAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityGcpAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityJwtAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityKubernetesAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityLdapAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityOciAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityOidcAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityTlsCertAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityTokenAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
@@ -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 (
|
||||
<IdentityUniversalAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
identityId={identityId}
|
||||
>
|
||||
<ViewIdentityContentWrapper onEdit={onEdit} onDelete={onDelete} identityId={identityId}>
|
||||
{Number(data.accessTokenPeriod) > 0 ? (
|
||||
<IdentityAuthFieldDisplay label="Access Token Period (seconds)">
|
||||
{data.accessTokenPeriod}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./ViewIdentityAuth";
|
||||
@@ -0,0 +1,7 @@
|
||||
export type ViewAuthMethodProps = {
|
||||
identityId: string;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
lockedOut: boolean;
|
||||
onResetAllLockouts: () => void;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const IdentityAuthFieldDisplay = ({ label, children, className }: Props) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<span className="text-sm text-mineshaft-400">{label}</span>
|
||||
{children ? (
|
||||
<p className="text-base leading-4 break-words">{children}</p>
|
||||
) : (
|
||||
<p className="text-base leading-4 text-bunker-400 italic">Not set</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<any>;
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Component
|
||||
identityId={identityId}
|
||||
onDelete={handleDelete}
|
||||
onResetAllLockouts={onResetAllLockouts}
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
lockedOut={lockedOut}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp?.revokeAuthMethod?.isOpen}
|
||||
title={`Are you sure you want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Remove"
|
||||
onDeleteApproved={handleDeleteAuthMethod}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => 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<Props, "onDeleteAuthMethod">) => {
|
||||
if (!identityId || !authMethod) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent className="max-w-2xl" title={identityAuthToNameMap[authMethod]}>
|
||||
<Content
|
||||
identityId={identityId}
|
||||
authMethod={authMethod}
|
||||
lockedOut={lockedOut}
|
||||
onDeleteAuthMethod={() => onOpenChange(false)}
|
||||
onResetAllLockouts={() => onResetAllLockouts()}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./ViewIdentityAuthModal";
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -73,7 +84,10 @@ const Page = () => {
|
||||
enabled: isProjectIdentity
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteIdentity } = useDeleteProjectIdentity();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"removeIdentity",
|
||||
"deleteIdentity",
|
||||
"assumePrivileges"
|
||||
] as const);
|
||||
@@ -102,7 +116,7 @@ const Page = () => {
|
||||
};
|
||||
|
||||
const onRemoveIdentitySubmit = async () => {
|
||||
await deleteMutateAsync({
|
||||
await removeIdentityMutateAsync({
|
||||
identityId,
|
||||
projectId
|
||||
});
|
||||
@@ -110,7 +124,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: {
|
||||
@@ -123,16 +137,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 (
|
||||
<div className="flex w-full items-center justify-center p-24">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
return <UnstablePageLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-8xl flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex max-w-8xl flex-col">
|
||||
{identityMembershipDetails ? (
|
||||
<>
|
||||
<Link
|
||||
@@ -144,127 +177,139 @@ const Page = () => {
|
||||
search={{
|
||||
selectedTab: ProjectAccessControlTabs.Identities
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
className="mb-4 flex w-fit items-center gap-x-1 text-sm text-mineshaft-400 transition duration-100 hover:text-mineshaft-400/80"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
<ChevronLeftIcon size={16} />
|
||||
Project Machine Identities
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
title={identityMembershipDetails?.identity?.name}
|
||||
description={`Machine identity ${isProjectIdentity ? "created" : "added"} on ${identityMembershipDetails?.createdAt && formatRelative(new Date(identityMembershipDetails?.createdAt || ""), new Date())}`}
|
||||
className={!isProjectIdentity ? "mb-4" : undefined}
|
||||
description={`Configure and manage${isProjectIdentity ? " machine identity and " : " "}project access control`}
|
||||
title={identityMembershipDetails.identity.name}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(identityMembershipDetails.id);
|
||||
createNotification({
|
||||
text: "Membership ID copied to clipboard",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Copy Membership ID
|
||||
</Button>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.AssumePrivileges}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails?.identity.id
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Assume privileges of the machine identity"
|
||||
passThrough={false}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handlePopUpOpen("assumePrivileges")}
|
||||
>
|
||||
Assume Privileges
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{!isProjectIdentity && (
|
||||
<DropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableButton variant="outline">
|
||||
Options
|
||||
<EllipsisIcon />
|
||||
</UnstableButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(identityMembershipDetails.id);
|
||||
createNotification({
|
||||
text: "Machine identity ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Copy Machine Identity ID
|
||||
</UnstableDropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.AssumePrivileges}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails?.identity.id
|
||||
})}
|
||||
passThrough={false}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handlePopUpOpen("assumePrivileges")}
|
||||
>
|
||||
Assume Privileges
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
content="Assume the privileges of the machine identity, allowing you to replicate their access behavior."
|
||||
>
|
||||
<div>
|
||||
<InfoIcon className="text-muted" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails?.identity?.id
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Remove from project"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
<UnstableDropdownMenuItem
|
||||
variant="danger"
|
||||
isDisabled={!isAllowed}
|
||||
isLoading={isDeletingIdentity}
|
||||
onClick={() => handlePopUpOpen("deleteIdentity")}
|
||||
onClick={() =>
|
||||
isProjectIdentity
|
||||
? handlePopUpOpen("deleteIdentity")
|
||||
: handlePopUpOpen("removeIdentity")
|
||||
}
|
||||
>
|
||||
Remove Machine Identity
|
||||
</Button>
|
||||
{isProjectIdentity ? "Delete Machine Identity" : "Remove From Project"}
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
</UnstableDropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PageHeader>
|
||||
{!isProjectIdentity && (
|
||||
<Alert hideTitle iconClassName="text-info" className="mb-4 border-info/50 bg-info/10">
|
||||
<AlertDescription>
|
||||
This machine identity is managed by your organization.{" "}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Read}
|
||||
an={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) =>
|
||||
isAllowed ? (
|
||||
<Link
|
||||
to="/organizations/$orgId/identities/$identityId"
|
||||
params={{
|
||||
identityId,
|
||||
orgId: currentOrg.id
|
||||
}}
|
||||
>
|
||||
<span className="cursor-pointer text-info underline underline-offset-2">
|
||||
Click here to manage machine identity.
|
||||
</span>
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
</OrgPermissionCan>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="flex gap-x-4">
|
||||
{identity ? (
|
||||
<div className="flex w-72 flex-col gap-y-4">
|
||||
<ProjectIdentityDetailsSection
|
||||
identity={identity}
|
||||
membership={identityMembershipDetails!}
|
||||
/>
|
||||
<div className="flex flex-col gap-5 lg:flex-row">
|
||||
<ProjectIdentityDetailsSection
|
||||
identity={identity || { ...identityMembershipDetails?.identity, projectId: "" }}
|
||||
isOrgIdentity={!isProjectIdentity}
|
||||
membership={identityMembershipDetails!}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-y-5">
|
||||
{identity ? (
|
||||
<ProjectIdentityAuthenticationSection
|
||||
identity={identity}
|
||||
refetchIdentity={() => refetchIdentity()}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex w-72 flex-col gap-y-4">
|
||||
<ProjectIdentityDetailsSection
|
||||
identity={{ ...identityMembershipDetails?.identity, projectId: "" }}
|
||||
isOrgIdentity
|
||||
membership={identityMembershipDetails!}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
) : (
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Authentication</UnstableCardTitle>
|
||||
<UnstableCardDescription>
|
||||
Configure authentication methods
|
||||
</UnstableCardDescription>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<UnstableAlert variant="org">
|
||||
<OrgIcon />
|
||||
<UnstableAlertTitle>
|
||||
Machine identity managed by organization
|
||||
</UnstableAlertTitle>
|
||||
<UnstableAlertDescription>
|
||||
<p>
|
||||
This machine identity's authentication methods are controlled by your
|
||||
organization. To make changes,{" "}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Read}
|
||||
an={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) =>
|
||||
isAllowed ? (
|
||||
<Link
|
||||
to="/organizations/$orgId/identities/$identityId"
|
||||
className="inline-block cursor-pointer text-foreground underline underline-offset-2"
|
||||
params={{
|
||||
identityId,
|
||||
orgId: currentOrg.id
|
||||
}}
|
||||
>
|
||||
go to organization access control
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
</OrgPermissionCan>
|
||||
.
|
||||
</p>
|
||||
</UnstableAlertDescription>
|
||||
</UnstableAlert>
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
)}
|
||||
<IdentityRoleDetailsSection
|
||||
identityMembershipDetails={identityMembershipDetails}
|
||||
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
||||
@@ -275,9 +320,9 @@ const Page = () => {
|
||||
</div>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteIdentity.isOpen}
|
||||
isOpen={popUp.removeIdentity.isOpen}
|
||||
title={`Are you sure you want to remove ${identityMembershipDetails?.identity?.name} from the project?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
||||
onChange={(isOpen) => handlePopUpToggle("removeIdentity", isOpen)}
|
||||
deleteKey="remove"
|
||||
onDeleteApproved={() => onRemoveIdentitySubmit()}
|
||||
/>
|
||||
@@ -290,6 +335,13 @@ const Page = () => {
|
||||
onConfirmed={handleAssumePrivileges}
|
||||
buttonText="Confirm"
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteIdentity.isOpen}
|
||||
title={`Are you sure you want to delete ${identity?.name}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleDeleteIdentity}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState title="Error: Unable to find the machine identity." className="py-12" />
|
||||
|
||||
@@ -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,59 +181,9 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<form className="flex flex-col gap-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
className="text-lg font-medium text-mineshaft-100"
|
||||
variant="link"
|
||||
onClick={onGoBack}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={onGoBack}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"mr-4 h-10 border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<AddPoliciesButton
|
||||
isDisabled={isDisabled}
|
||||
projectType={currentProject.type}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 border-b border-gray-800 p-4 pt-2 first:rounded-t-md last:rounded-b-md">
|
||||
<div className="text-lg">Overview</div>
|
||||
<p className="mb-4 text-sm text-mineshaft-300">
|
||||
Additional privileges take precedence over roles when permissions conflict
|
||||
</p>
|
||||
<div>
|
||||
<div className="flex items-end space-x-6">
|
||||
<div className="w-full max-w-md">
|
||||
<Controller
|
||||
@@ -348,10 +299,33 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="mb-2 text-lg">Policies</div>
|
||||
<UnstableSeparator />
|
||||
<div>
|
||||
<div className="mb-3 flex w-full items-center justify-between">
|
||||
<div className="text-lg">Policies</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={onGoBack}
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<AddPoliciesButton
|
||||
isDisabled={isDisabled}
|
||||
projectType={currentProject.type}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{(isCreate || !isPending) && <PermissionEmptyState />}
|
||||
<div>
|
||||
<div className="scrollbar-thin max-h-[50vh] overflow-y-auto">
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
|
||||
(permissionSubject) => (
|
||||
<GeneralPermissionPolicies
|
||||
@@ -367,6 +341,20 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<UnstableSeparator />
|
||||
<div className="flex w-full items-center justify-end gap-x-2">
|
||||
<Button colorSchema="secondary" variant="plain" onClick={onGoBack}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -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,232 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
|
||||
handlePopUpClose("deletePrivilege");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<AnimatePresence>
|
||||
{popUp?.modifyPrivilege.isOpen ? (
|
||||
<motion.div
|
||||
key="privilege-modify"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="absolute min-h-40 w-full"
|
||||
>
|
||||
<IdentityProjectAdditionalPrivilegeModifySection
|
||||
onGoBack={() => handlePopUpClose("modifyPrivilege")}
|
||||
identityId={identityId}
|
||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||
isDisabled={permission.cannot(
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})
|
||||
)}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="privilege-list"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 0 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
className="absolute w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">
|
||||
Project Additional Privileges
|
||||
</h3>
|
||||
const hasAdditionalPrivileges = Boolean(identityProjectPrivileges?.length);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader
|
||||
// className="border-b"
|
||||
>
|
||||
<UnstableCardTitle>Project Additional Privileges</UnstableCardTitle>
|
||||
<UnstableCardDescription>
|
||||
Assign one-off policies to this machine identity
|
||||
</UnstableCardDescription>
|
||||
{hasAdditionalPrivileges && (
|
||||
<UnstableCardAction>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Add Privilege"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
<UnstableButton
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyPrivilege");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
<PlusIcon />
|
||||
Add Additional Privileges
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{isPending ? (
|
||||
// scott: todo proper loader
|
||||
<div className="flex h-40 w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Duration</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && (
|
||||
<TableSkeleton columns={3} innerKey="user-project-identity-memberships" />
|
||||
)}
|
||||
{!isPending &&
|
||||
identityProjectPrivileges?.map((privilegeDetails) => {
|
||||
const isTemporary = privilegeDetails?.isTemporary;
|
||||
const isExpired =
|
||||
privilegeDetails.isTemporary &&
|
||||
new Date() > new Date(privilegeDetails.temporaryAccessEndTime || "");
|
||||
) : identityProjectPrivileges?.length ? (
|
||||
<UnstableTable>
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead className="w-1/2">Name</UnstableTableHead>
|
||||
<UnstableTableHead className="w-1/2">Duration</UnstableTableHead>
|
||||
<UnstableTableHead className="w-5" />
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{!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 (
|
||||
<Tr
|
||||
key={`user-project-privilege-${privilegeDetails?.id}`}
|
||||
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
||||
}
|
||||
}}
|
||||
onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)}
|
||||
>
|
||||
<Td>{privilegeDetails.slug}</Td>
|
||||
<Td>
|
||||
<Tooltip asChild={false} content={toolTipText}>
|
||||
<Tag
|
||||
className={twMerge(
|
||||
"capitalize",
|
||||
isTemporary && "text-primary",
|
||||
isExpired && "text-red-600"
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handlePopUpOpen("deletePrivilege", {
|
||||
id: privilegeDetails?.id,
|
||||
slug: privilegeDetails?.slug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton ariaLabel="more-icon" variant="plain">
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
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()
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && !identityProjectPrivileges?.length && (
|
||||
<EmptyState
|
||||
title="This machine identity has no additional privileges"
|
||||
icon={faFolder}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePrivilege.isOpen}
|
||||
deleteKey="remove"
|
||||
title={`Do you want to remove privilege ${
|
||||
(popUp?.deletePrivilege?.data as { slug: string; id: string })?.slug
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
||||
onDeleteApproved={() => handlePrivilegeDelete()}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
toolTipText = `Until ${format(
|
||||
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
||||
"yyyy-MM-dd hh:mm:ss aaa"
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<UnstableTableRow key={`user-project-privilege-${privilegeDetails?.id}`}>
|
||||
<UnstableTableCell className="max-w-0 truncate">
|
||||
{privilegeDetails.slug}
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
{isTemporary ? (
|
||||
<Tooltip content={toolTipText}>
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={isExpired ? "danger" : "warning"}
|
||||
>
|
||||
{isExpired ? <ClockAlertIcon /> : <ClockIcon />}
|
||||
{text}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton size="xs" variant="ghost">
|
||||
<EllipsisIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
||||
}}
|
||||
>
|
||||
Edit Additional Privilege
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
variant="danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deletePrivilege", {
|
||||
id: privilegeDetails?.id,
|
||||
slug: privilegeDetails?.slug
|
||||
});
|
||||
}}
|
||||
>
|
||||
Remove Additional Privilege
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
</UnstableTableRow>
|
||||
);
|
||||
})}
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>
|
||||
This machine identity has no additional privileges
|
||||
</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
Add an additional privilege to grant one-off access policies
|
||||
</UnstableEmptyDescription>
|
||||
</UnstableEmptyHeader>
|
||||
<UnstableEmptyContent>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="project"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyPrivilege");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<PlusIcon />
|
||||
Add Additional Privileges
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableEmptyContent>
|
||||
</UnstableEmpty>
|
||||
)}
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
<Modal
|
||||
isOpen={popUp.modifyPrivilege.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("modifyPrivilege", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
className="max-w-6xl"
|
||||
title="Additional Privileges"
|
||||
subTitle="Additional privileges take precedence over roles when permissions conflict"
|
||||
>
|
||||
<IdentityProjectAdditionalPrivilegeModifySection
|
||||
onGoBack={() => handlePopUpClose("modifyPrivilege")}
|
||||
identityId={identityId}
|
||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||
isDisabled={permission.cannot(
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})
|
||||
)}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePrivilege.isOpen}
|
||||
deleteKey="remove"
|
||||
title={`Do you want to remove privilege ${
|
||||
(popUp?.deletePrivilege?.data as { slug: string; id: string })?.slug
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
||||
onDeleteApproved={() => handlePrivilegeDelete()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,188 @@ export const IdentityRoleDetailsSection = ({
|
||||
handlePopUpClose("deleteRole");
|
||||
};
|
||||
|
||||
const hasRoles = Boolean(identityMembershipDetails?.roles.length);
|
||||
|
||||
return (
|
||||
<div className="mb-4 w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Project Roles</h3>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails.identity.id
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Edit Role(s)"
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader
|
||||
// className="border-b"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyRole");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Role</Th>
|
||||
<Th>Duration</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isMembershipDetailsLoading && (
|
||||
<TableSkeleton columns={3} innerKey="user-project-identities" />
|
||||
)}
|
||||
{!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 (
|
||||
<Tr className="group h-10" key={`user-project-identity-${roleDetails?.id}`}>
|
||||
<Td className="capitalize">
|
||||
{roleDetails.role === "custom"
|
||||
? roleDetails.customRoleName
|
||||
: formatProjectRoleName(roleDetails.role)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip asChild={false} content={toolTipText}>
|
||||
<Tag
|
||||
className={twMerge(
|
||||
"capitalize",
|
||||
isTemporary && "text-primary",
|
||||
isExpired && "text-red-600"
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails.identity.id
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", {
|
||||
id: roleDetails?.id,
|
||||
slug: roleDetails?.customRoleName || roleDetails?.role
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
<UnstableCardTitle>Project Roles</UnstableCardTitle>
|
||||
<UnstableCardDescription>
|
||||
Manage roles assigned to this machine identity
|
||||
</UnstableCardDescription>
|
||||
{hasRoles && (
|
||||
<UnstableCardAction>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails.identity.id
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isMembershipDetailsLoading && !identityMembershipDetails?.roles?.length && (
|
||||
<EmptyState title="This user has no roles" icon={faFolder} />
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyRole");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<PencilIcon />
|
||||
Edit Roles
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
{
|
||||
/* eslint-disable-next-line no-nested-ternary */
|
||||
isMembershipDetailsLoading ? (
|
||||
// scott: todo proper loader
|
||||
<div className="flex h-40 w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
|
||||
</div>
|
||||
) : hasRoles ? (
|
||||
<UnstableTable>
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead className="w-1/2">Role</UnstableTableHead>
|
||||
<UnstableTableHead className="w-1/2">Duration</UnstableTableHead>
|
||||
<UnstableTableHead className="w-5" />
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{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 (
|
||||
<UnstableTableRow
|
||||
className="group h-10"
|
||||
key={`user-project-identity-${roleDetails?.id}`}
|
||||
>
|
||||
<UnstableTableCell className="max-w-0 truncate">
|
||||
{roleDetails.role === "custom"
|
||||
? roleDetails.customRoleName
|
||||
: formatProjectRoleName(roleDetails.role)}
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
{isTemporary ? (
|
||||
<Tooltip content={toolTipText}>
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={isExpired ? "danger" : "warning"}
|
||||
>
|
||||
{isExpired ? <ClockAlertIcon /> : <ClockIcon />}
|
||||
{text}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton size="xs" variant="ghost">
|
||||
<EllipsisIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails.identity.id
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", {
|
||||
id: roleDetails?.id,
|
||||
slug: roleDetails?.customRoleName || roleDetails?.role
|
||||
});
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
variant="danger"
|
||||
>
|
||||
{/* <TrashIcon /> */}
|
||||
Remove Role
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
</UnstableTableRow>
|
||||
);
|
||||
})}
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>
|
||||
This machine identity doesn t have any roles
|
||||
</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
Give this machine identity one or more roles
|
||||
</UnstableEmptyDescription>
|
||||
</UnstableEmptyHeader>
|
||||
<UnstableEmptyContent>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails.identity.id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="project"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyRole");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<PencilIcon />
|
||||
Edit Roles
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableEmptyContent>
|
||||
</UnstableEmpty>
|
||||
)
|
||||
}
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteRole.isOpen}
|
||||
deleteKey="remove"
|
||||
@@ -228,6 +291,6 @@ export const IdentityRoleDetailsSection = ({
|
||||
<IdentityRoleModify identityProjectMembership={identityMembershipDetails} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
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 { 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 {
|
||||
UnstableButton,
|
||||
UnstableCard,
|
||||
UnstableCardAction,
|
||||
UnstableCardContent,
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle,
|
||||
UnstableEmpty,
|
||||
UnstableEmptyContent,
|
||||
UnstableEmptyDescription,
|
||||
UnstableEmptyHeader,
|
||||
UnstableEmptyTitle
|
||||
} from "@app/components/v3";
|
||||
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/context";
|
||||
import { IdentityAuthMethod, identityAuthToNameMap, TProjectIdentity } 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 { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal";
|
||||
import { ViewIdentityAuth } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuth";
|
||||
|
||||
type Props = {
|
||||
identity: TProjectIdentity;
|
||||
@@ -20,81 +30,100 @@ type Props = {
|
||||
|
||||
export const ProjectIdentityAuthenticationSection = ({ identity, refetchIdentity }: Props) => {
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([
|
||||
"viewAuthMethod",
|
||||
"identityAuthMethod",
|
||||
"upgradePlan"
|
||||
"upgradePlan",
|
||||
"revokeAuthMethod"
|
||||
]);
|
||||
|
||||
const hasAuthMethods = Boolean(identity.authMethods.length);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Authentication</h3>
|
||||
</div>
|
||||
{identity.authMethods.length > 0 ? (
|
||||
<div className="flex flex-col divide-y divide-mineshaft-400/50">
|
||||
{identity.authMethods.map((authMethod) => (
|
||||
<button
|
||||
key={authMethod}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("viewAuthMethod", {
|
||||
authMethod,
|
||||
lockedOut: identity.activeLockoutAuthMethods?.includes(authMethod) ?? false,
|
||||
refetchIdentity
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between bg-mineshaft-900 px-4 py-2 text-sm hover:bg-mineshaft-700 data-[state=open]:bg-mineshaft-600"
|
||||
>
|
||||
<span>{identityAuthToNameMap[authMethod]}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{identity.activeLockoutAuthMethods?.includes(authMethod) && (
|
||||
<Tooltip content="Auth method has active lockouts">
|
||||
<Badge isSquare variant="danger">
|
||||
<LockIcon />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
<SettingsIcon className="size-4 text-neutral" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full space-y-2 pt-2">
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
No authentication methods configured. Get started by creating a new auth method.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!Object.values(IdentityAuthMethod).every((method) =>
|
||||
identity.authMethods.includes(method)
|
||||
) && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identity.id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId: identity.id,
|
||||
name: identity.name,
|
||||
allAuthMethods: identity.authMethods
|
||||
});
|
||||
}}
|
||||
variant="outline_bg"
|
||||
className="mt-3 w-full"
|
||||
size="xs"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
{identity.authMethods.length ? "Add" : "Create"} Auth Method
|
||||
</Button>
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Authentication</UnstableCardTitle>
|
||||
<UnstableCardDescription>Configure authentication methods</UnstableCardDescription>
|
||||
{hasAuthMethods &&
|
||||
!Object.values(IdentityAuthMethod).every((method) =>
|
||||
identity.authMethods.includes(method)
|
||||
) && (
|
||||
<UnstableCardAction>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identity.id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="outline"
|
||||
isFullWidth
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId: identity.id,
|
||||
name: identity.name,
|
||||
allAuthMethods: identity.authMethods
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PlusIcon />
|
||||
Add Auth Method
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
{identity.authMethods.length > 0 ? (
|
||||
<ViewIdentityAuth
|
||||
authMethods={identity.authMethods}
|
||||
identityId={identity.id}
|
||||
onResetAllLockouts={refetchIdentity}
|
||||
activeLockoutAuthMethods={identity.activeLockoutAuthMethods}
|
||||
/>
|
||||
) : (
|
||||
<UnstableEmpty className="rounded-sm border bg-mineshaft-800/50">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>
|
||||
This machine identity has no auth methods configured
|
||||
</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
Add an auth method to use this machine identity
|
||||
</UnstableEmptyDescription>
|
||||
</UnstableEmptyHeader>
|
||||
<UnstableEmptyContent>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identity.id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="project"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
identityId: identity.id,
|
||||
name: identity.name,
|
||||
allAuthMethods: identity.authMethods
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PlusIcon />
|
||||
Add Auth Method
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableEmptyContent>
|
||||
</UnstableEmpty>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
<IdentityAuthMethodModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
@@ -106,14 +135,6 @@ export const ProjectIdentityAuthenticationSection = ({ identity, refetchIdentity
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
isEnterpriseFeature={popUp.upgradePlan.data?.isEnterpriseFeature}
|
||||
/>
|
||||
<ViewIdentityAuthModal
|
||||
isOpen={popUp.viewAuthMethod.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)}
|
||||
authMethod={popUp.viewAuthMethod.data?.authMethod}
|
||||
lockedOut={popUp.viewAuthMethod.data?.lockedOut || false}
|
||||
identityId={identity.id}
|
||||
onResetAllLockouts={popUp.viewAuthMethod.data?.refetchIdentity}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,36 +1,29 @@
|
||||
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,
|
||||
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 +34,152 @@ type Props = {
|
||||
};
|
||||
|
||||
export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, membership }: Props) => {
|
||||
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-unused-vars
|
||||
const [_, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
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 (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
|
||||
<DropdownMenu>
|
||||
<>
|
||||
<UnstableCard className="w-full max-w-[22rem]">
|
||||
<UnstableCardHeader
|
||||
// className="border-b"
|
||||
>
|
||||
<UnstableCardTitle>Details</UnstableCardTitle>
|
||||
<UnstableCardDescription>Machine identity details</UnstableCardDescription>
|
||||
{!isOrgIdentity && (
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
rightIcon={
|
||||
<FontAwesomeIcon
|
||||
className="ml-1 transition-transform duration-200 group-data-[state=open]:rotate-180"
|
||||
icon={faChevronDown}
|
||||
/>
|
||||
}
|
||||
colorSchema="secondary"
|
||||
className="group select-none"
|
||||
<UnstableCardAction>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identity.id
|
||||
})}
|
||||
>
|
||||
Options
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
<DropdownMenuContent className="mt-3 min-w-[120px]" align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identity.id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("editIdentity");
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Edit Machine Identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identity.id
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:bg-red-500! hover:text-white!"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () => {
|
||||
handlePopUpOpen("deleteIdentity");
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Machine Identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Machine Identity ID</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm break-all text-mineshaft-300">{identity.id}</p>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content={copyTextId}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(identity.id);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Managed By</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{identity.projectId ? "Project" : "Organization"}
|
||||
</p>
|
||||
</div>
|
||||
{!isOrgIdentity && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Last Login Auth Method</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{membership.lastLoginAuthMethod
|
||||
? identityAuthToNameMap[membership.lastLoginAuthMethod]
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Last Login Time</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{membership.lastLoginTime ? format(membership.lastLoginTime, "PPpp") : "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Delete Protection</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{identity.hasDeleteProtection ? "On" : "Off"}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-mineshaft-300">Metadata</p>
|
||||
{identity?.metadata?.length ? (
|
||||
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||
{identity.metadata?.map((el) => (
|
||||
<div key={el.id} className="flex items-center">
|
||||
<Tag
|
||||
{(isAllowed) => (
|
||||
<UnstableIconButton
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("editIdentity");
|
||||
}}
|
||||
size="xs"
|
||||
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
|
||||
variant="outline"
|
||||
>
|
||||
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
|
||||
<div>{el.key}</div>
|
||||
</Tag>
|
||||
<Tag
|
||||
size="xs"
|
||||
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
|
||||
>
|
||||
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{el.value}
|
||||
</div>
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
<PencilIcon />
|
||||
</UnstableIconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<DetailGroup>
|
||||
<Detail>
|
||||
<DetailLabel>Name</DetailLabel>
|
||||
<DetailValue>{identity.name}</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>ID</DetailLabel>
|
||||
<DetailValue className="flex items-center gap-x-1">
|
||||
{identity.id}
|
||||
<Tooltip content="Copy machine identity ID to clipboard">
|
||||
<UnstableIconButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(identity.id);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
>
|
||||
{/* TODO(scott): color this should be a button variant */}
|
||||
{isCopyingId ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Managed by</DetailLabel>
|
||||
<DetailValue>
|
||||
{isOrgIdentity ? (
|
||||
<Badge variant="org">
|
||||
<OrgIcon />
|
||||
Organization
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="project">
|
||||
<ProjectIcon />
|
||||
Project
|
||||
</Badge>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Metadata</DetailLabel>
|
||||
<DetailValue className="flex flex-wrap gap-2">
|
||||
{identity?.metadata?.length ? (
|
||||
identity.metadata?.map((el) => (
|
||||
<UnstableButtonGroup className="min-w-0" key={el.id}>
|
||||
<Badge isTruncatable>
|
||||
<span>{el.key}</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" isTruncatable>
|
||||
<span>{el.value}</span>
|
||||
</Badge>
|
||||
</UnstableButtonGroup>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted italic">No metadata</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>{isOrgIdentity ? "Joined project" : "Created"}</DetailLabel>
|
||||
<DetailValue>{format(membership.createdAt, "PPpp")}</DetailValue>
|
||||
</Detail>
|
||||
{!isOrgIdentity && (
|
||||
<>
|
||||
<Detail>
|
||||
<DetailLabel>Last Login Method</DetailLabel>
|
||||
<DetailValue>
|
||||
{membership.lastLoginAuthMethod ? (
|
||||
identityAuthToNameMap[membership.lastLoginAuthMethod]
|
||||
) : (
|
||||
<span className="text-muted italic">N/A</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Last Logged In</DetailLabel>
|
||||
<DetailValue>
|
||||
{membership.lastLoginTime ? (
|
||||
format(membership.lastLoginTime, "PPpp")
|
||||
) : (
|
||||
<span className="text-muted italic">N/A</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Delete protection</DetailLabel>
|
||||
<DetailValue>
|
||||
{identity.hasDeleteProtection ? (
|
||||
<Badge variant="success">
|
||||
<CheckIcon />
|
||||
Enabled
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="neutral">
|
||||
<BanIcon />
|
||||
Disabled
|
||||
</Badge>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
</>
|
||||
)}
|
||||
</DetailGroup>
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
<Modal
|
||||
isOpen={popUp.editIdentity.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle("editIdentity", open)}
|
||||
@@ -236,14 +191,6 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteIdentity.isOpen}
|
||||
title={`Are you sure you want to delete ${identity.name}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={handleDeleteIdentity}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user