mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge remote-tracking branch 'origin/main' into PAM-12-add-k8s-for-pam
This commit is contained in:
@@ -67,6 +67,7 @@ const Content = ({ secretRotation }: ContentProps) => {
|
||||
case SecretRotation.MySqlCredentials:
|
||||
case SecretRotation.MsSqlCredentials:
|
||||
case SecretRotation.OracleDBCredentials:
|
||||
case SecretRotation.MongoDBCredentials:
|
||||
Component = (
|
||||
<ViewSqlCredentialsRotationGeneratedCredentials
|
||||
generatedCredentialsResponse={generatedCredentialsResponse}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
|
||||
import { ViewRotationGeneratedCredentialsDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/ViewRotationGeneratedCredentialsDisplay";
|
||||
import { TMongoDBCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mongodb-credentials-rotation";
|
||||
import { TMsSqlCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mssql-credentials-rotation";
|
||||
import { TMySqlCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/mysql-credentials-rotation";
|
||||
import { TOracleDBCredentialsRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/oracledb-credentials-rotation";
|
||||
@@ -10,7 +11,8 @@ type Props = {
|
||||
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
|
||||
| TMySqlCredentialsRotationGeneratedCredentialsResponse
|
||||
| TOracleDBCredentialsRotationGeneratedCredentialsResponse
|
||||
| TPostgresCredentialsRotationGeneratedCredentialsResponse;
|
||||
| TPostgresCredentialsRotationGeneratedCredentialsResponse
|
||||
| TMongoDBCredentialsRotationGeneratedCredentialsResponse;
|
||||
};
|
||||
|
||||
export const ViewSqlCredentialsRotationGeneratedCredentials = ({
|
||||
|
||||
@@ -21,7 +21,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields,
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields,
|
||||
[SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields
|
||||
[SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields,
|
||||
[SecretRotation.MongoDBCredentials]: SqlCredentialsRotationParametersFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ParametersFields = () => {
|
||||
|
||||
@@ -24,7 +24,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields,
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields,
|
||||
[SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields
|
||||
[SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields,
|
||||
[SecretRotation.MongoDBCredentials]: SqlCredentialsRotationReviewFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2ReviewFields = () => {
|
||||
|
||||
@@ -21,7 +21,8 @@ const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
|
||||
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
|
||||
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields,
|
||||
[SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields
|
||||
[SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields,
|
||||
[SecretRotation.MongoDBCredentials]: SqlCredentialsRotationSecretsMappingFields
|
||||
};
|
||||
|
||||
export const SecretRotationV2SecretsMappingFields = () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotation
|
||||
import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema";
|
||||
import { AzureClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema";
|
||||
import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema";
|
||||
import { MongoDBCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mongodb-credentials-rotation-schema";
|
||||
import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema";
|
||||
import { MySqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mysql-credentials-rotation-schema";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema";
|
||||
@@ -27,7 +28,8 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema,
|
||||
OktaClientSecretRotationSchema,
|
||||
RedisCredentialsRotationSchema
|
||||
RedisCredentialsRotationSchema,
|
||||
MongoDBCredentialsRotationSchema
|
||||
]),
|
||||
z.object({ id: z.string().optional() })
|
||||
)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema";
|
||||
import { SqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
|
||||
export const MongoDBCredentialsRotationSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretRotation.MongoDBCredentials)
|
||||
})
|
||||
.merge(SqlCredentialsRotationSchema)
|
||||
.merge(BaseSecretRotationSchema);
|
||||
@@ -65,6 +65,12 @@ const inputParentContainerVariants = cva("inline-flex font-inter items-center bo
|
||||
}
|
||||
});
|
||||
|
||||
const data1pIgnore = (autoComplete?: string) => {
|
||||
if (!autoComplete) return true;
|
||||
|
||||
return !autoComplete.match(/(email|password|username)/i);
|
||||
};
|
||||
|
||||
export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> &
|
||||
VariantProps<typeof inputVariants> &
|
||||
Props;
|
||||
@@ -86,6 +92,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
isReadOnly,
|
||||
autoCapitalization,
|
||||
warning,
|
||||
autoComplete,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -116,6 +123,8 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
readOnly={isReadOnly}
|
||||
disabled={isDisabled}
|
||||
onInput={handleInput}
|
||||
autoComplete={autoComplete}
|
||||
data-1p-ignore={data1pIgnore(autoComplete)}
|
||||
className={twMerge(
|
||||
leftIcon ? "pl-10" : "pl-2.5",
|
||||
rightIcon || warning ? "pr-10" : "pr-2.5",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -24,8 +24,6 @@ apiRequest.interceptors.request.use((config) => {
|
||||
const token = getAuthToken();
|
||||
const providerAuthToken = SecurityClient.getProviderAuthToken();
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
if (config.headers) {
|
||||
if (signupTempToken) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
@@ -40,17 +38,6 @@ apiRequest.interceptors.request.use((config) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${providerAuthToken}`;
|
||||
}
|
||||
|
||||
const rootOrgHeader = config.headers.get("x-root-org");
|
||||
|
||||
if (rootOrgHeader) {
|
||||
config.headers.delete("x-root-org");
|
||||
} else {
|
||||
const subOrganization = params.get("subOrganization");
|
||||
if (subOrganization) {
|
||||
config.headers.set("x-infisical-org", subOrganization);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -61,7 +61,8 @@ export const leaveConfirmDefaultMessage =
|
||||
export enum SessionStorageKeys {
|
||||
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN",
|
||||
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL",
|
||||
AUTH_CONSENT = "AUTH_CONSENT"
|
||||
AUTH_CONSENT = "AUTH_CONSENT",
|
||||
MFA_TEMP_TOKEN = "MFA_TEMP_TOKEN"
|
||||
}
|
||||
|
||||
export const secretTagsColors = [
|
||||
|
||||
@@ -359,6 +359,10 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/organizations/$orgId/projects/pam/$projectId/sessions",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/"
|
||||
),
|
||||
ApprovalRequestDetailPage: setRoute(
|
||||
"/organizations/$orgId/projects/pam/$projectId/approval-requests/$approvalRequestId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/approval-requests/$approvalRequestId"
|
||||
),
|
||||
PamSessionByIDPage: setRoute(
|
||||
"/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useRouteContext, useSearch } from "@tanstack/react-router";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
|
||||
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
|
||||
|
||||
@@ -10,28 +10,23 @@ export const useOrganization = () => {
|
||||
select: (el) => el.organizationId
|
||||
});
|
||||
|
||||
const subOrganization = useSearch({
|
||||
strict: false,
|
||||
select: (el) => el?.subOrganization
|
||||
});
|
||||
|
||||
const { data: currentOrg } = useSuspenseQuery({
|
||||
queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"),
|
||||
queryKey: organizationKeys.getOrgById(organizationId),
|
||||
queryFn: () => fetchOrganizationById(organizationId),
|
||||
staleTime: Infinity
|
||||
});
|
||||
const isSubOrganization = currentOrg.id !== currentOrg.rootOrgId && Boolean(currentOrg.rootOrgId);
|
||||
|
||||
const org = useMemo(
|
||||
() => ({
|
||||
currentOrg: {
|
||||
...currentOrg,
|
||||
id: currentOrg?.subOrganization?.id || currentOrg?.id,
|
||||
parentOrgId: currentOrg.id
|
||||
parentOrgId: isSubOrganization ? currentOrg?.parentOrgId : null
|
||||
},
|
||||
isSubOrganization: Boolean(currentOrg.subOrganization),
|
||||
isRootOrganization: !currentOrg.subOrganization
|
||||
isSubOrganization,
|
||||
isRootOrganization: !isSubOrganization
|
||||
}),
|
||||
[currentOrg, subOrganization]
|
||||
[currentOrg]
|
||||
);
|
||||
|
||||
return org;
|
||||
|
||||
@@ -228,6 +228,16 @@ export enum ProjectPermissionPamSessionActions {
|
||||
// Terminate = "terminate"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionApprovalRequestActions {
|
||||
Read = "read",
|
||||
Create = "create"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionApprovalRequestGrantActions {
|
||||
Read = "read",
|
||||
Revoke = "revoke"
|
||||
}
|
||||
|
||||
export type IdentityManagementSubjectFields = {
|
||||
identityId: string;
|
||||
};
|
||||
@@ -339,7 +349,9 @@ export enum ProjectPermissionSub {
|
||||
PamFolders = "pam-folders",
|
||||
PamResources = "pam-resources",
|
||||
PamAccounts = "pam-accounts",
|
||||
PamSessions = "pam-sessions"
|
||||
PamSessions = "pam-sessions",
|
||||
ApprovalRequests = "approval-requests",
|
||||
ApprovalRequestGrants = "approval-request-grants"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@@ -577,6 +589,8 @@ export type ProjectPermissionSet =
|
||||
| (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions];
|
||||
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]
|
||||
| [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests]
|
||||
| [ProjectPermissionApprovalRequestGrantActions, ProjectPermissionSub.ApprovalRequestGrants];
|
||||
|
||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
HCVaultConnectionMethod,
|
||||
HumanitecConnectionMethod,
|
||||
LdapConnectionMethod,
|
||||
MongoDBConnectionMethod,
|
||||
MsSqlConnectionMethod,
|
||||
MySqlConnectionMethod,
|
||||
OktaConnectionMethod,
|
||||
@@ -129,6 +130,7 @@ export const APP_CONNECTION_MAP: Record<
|
||||
[AppConnection.Northflank]: { name: "Northflank", image: "Northflank.png" },
|
||||
[AppConnection.Okta]: { name: "Okta", image: "Okta.png" },
|
||||
[AppConnection.Redis]: { name: "Redis", image: "Redis.png" },
|
||||
[AppConnection.MongoDB]: { name: "MongoDB", image: "MongoDB.png" },
|
||||
[AppConnection.LaravelForge]: {
|
||||
name: "Laravel Forge",
|
||||
image: "Laravel Forge.png",
|
||||
@@ -181,6 +183,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
case OracleDBConnectionMethod.UsernameAndPassword:
|
||||
case AzureADCSConnectionMethod.UsernamePassword:
|
||||
case RedisConnectionMethod.UsernameAndPassword:
|
||||
case MongoDBConnectionMethod.UsernameAndPassword:
|
||||
return { name: "Username & Password", icon: faLock };
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
case TeamCityConnectionMethod.AccessToken:
|
||||
|
||||
@@ -54,6 +54,11 @@ export const SECRET_ROTATION_MAP: Record<
|
||||
name: "Redis Credentials",
|
||||
image: "Redis.png",
|
||||
size: 50
|
||||
},
|
||||
[SecretRotation.MongoDBCredentials]: {
|
||||
name: "MongoDB Credentials",
|
||||
image: "MongoDB.png",
|
||||
size: 50
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,7 +72,8 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
|
||||
[SecretRotation.OktaClientSecret]: AppConnection.Okta,
|
||||
[SecretRotation.RedisCredentials]: AppConnection.Redis
|
||||
[SecretRotation.RedisCredentials]: AppConnection.Redis,
|
||||
[SecretRotation.MongoDBCredentials]: AppConnection.MongoDB
|
||||
};
|
||||
|
||||
// if a rotation can potentially have downtime due to rotating a single credential set this to false
|
||||
@@ -81,7 +87,8 @@ export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
|
||||
[SecretRotation.LdapPassword]: false,
|
||||
[SecretRotation.AwsIamUserSecret]: true,
|
||||
[SecretRotation.OktaClientSecret]: true,
|
||||
[SecretRotation.RedisCredentials]: true
|
||||
[SecretRotation.RedisCredentials]: true,
|
||||
[SecretRotation.MongoDBCredentials]: true
|
||||
};
|
||||
|
||||
export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ export enum AppConnection {
|
||||
Northflank = "northflank",
|
||||
Okta = "okta",
|
||||
Redis = "redis",
|
||||
MongoDB = "mongodb",
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef"
|
||||
}
|
||||
|
||||
@@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Redis;
|
||||
};
|
||||
|
||||
export type TMongoDBConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.MongoDB;
|
||||
};
|
||||
|
||||
export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.DNSMadeEasy;
|
||||
};
|
||||
@@ -229,6 +233,8 @@ export type TAppConnectionOption =
|
||||
| TOktaConnectionOption
|
||||
| TAzureAdCsConnectionOption
|
||||
| TLaravelForgeConnectionOption
|
||||
| TRedisConnectionOption
|
||||
| TMongoDBConnectionOption
|
||||
| TChefConnectionOption
|
||||
| TDNSMadeEasyConnectionOption;
|
||||
|
||||
@@ -274,6 +280,7 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.Okta]: TOktaConnectionOption;
|
||||
[AppConnection.AzureADCS]: TAzureAdCsConnectionOption;
|
||||
[AppConnection.Redis]: TRedisConnectionOption;
|
||||
[AppConnection.MongoDB]: TMongoDBConnectionOption;
|
||||
[AppConnection.LaravelForge]: TLaravelForgeConnectionOption;
|
||||
[AppConnection.Chef]: TChefConnectionOption;
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ import { THerokuConnection } from "./heroku-connection";
|
||||
import { THumanitecConnection } from "./humanitec-connection";
|
||||
import { TLaravelForgeConnection } from "./laravel-forge-connection";
|
||||
import { TLdapConnection } from "./ldap-connection";
|
||||
import { TMongoDBConnection } from "./mongodb-connection";
|
||||
import { TMsSqlConnection } from "./mssql-connection";
|
||||
import { TMySqlConnection } from "./mysql-connection";
|
||||
import { TNetlifyConnection } from "./netlify-connection";
|
||||
@@ -69,6 +70,7 @@ export * from "./heroku-connection";
|
||||
export * from "./humanitec-connection";
|
||||
export * from "./laravel-forge-connection";
|
||||
export * from "./ldap-connection";
|
||||
export * from "./mongodb-connection";
|
||||
export * from "./mssql-connection";
|
||||
export * from "./mysql-connection";
|
||||
export * from "./netlify-connection";
|
||||
@@ -129,6 +131,7 @@ export type TAppConnection =
|
||||
| TNorthflankConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
| TMongoDBConnection
|
||||
| TChefConnection
|
||||
| TDNSMadeEasyConnection;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum MongoDBConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
||||
|
||||
export type TMongoDBConnectionCredentials = {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
tlsEnabled: boolean;
|
||||
tlsRejectUnauthorized: boolean;
|
||||
tlsCertificate?: string;
|
||||
};
|
||||
|
||||
export type TMongoDBConnection = TRootAppConnection & { app: AppConnection.MongoDB } & {
|
||||
method: MongoDBConnectionMethod.UsernameAndPassword;
|
||||
credentials: TMongoDBConnectionCredentials;
|
||||
};
|
||||
10
frontend/src/hooks/api/approvalGrants/index.tsx
Normal file
10
frontend/src/hooks/api/approvalGrants/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export { useRevokeApprovalGrant } from "./mutations";
|
||||
export { approvalGrantQuery } from "./queries";
|
||||
export {
|
||||
ApprovalGrantStatus,
|
||||
type PamAccessGrantAttributes,
|
||||
type TApprovalGrant,
|
||||
type TGetApprovalGrantByIdDTO,
|
||||
type TListApprovalGrantsDTO,
|
||||
type TRevokeApprovalGrantDTO
|
||||
} from "./types";
|
||||
23
frontend/src/hooks/api/approvalGrants/mutations.tsx
Normal file
23
frontend/src/hooks/api/approvalGrants/mutations.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { approvalGrantQuery } from "./queries";
|
||||
import { TApprovalGrant, TRevokeApprovalGrantDTO } from "./types";
|
||||
|
||||
export const useRevokeApprovalGrant = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ policyType, grantId, revocationReason }: TRevokeApprovalGrantDTO) => {
|
||||
const { data } = await apiRequest.post<{ grant: TApprovalGrant }>(
|
||||
`/api/v1/approval-policies/${policyType}/grants/${grantId}/revoke`,
|
||||
{ revocationReason }
|
||||
);
|
||||
return data.grant;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: approvalGrantQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
37
frontend/src/hooks/api/approvalGrants/queries.tsx
Normal file
37
frontend/src/hooks/api/approvalGrants/queries.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TApprovalGrant, TGetApprovalGrantByIdDTO, TListApprovalGrantsDTO } from "./types";
|
||||
|
||||
export const approvalGrantQuery = {
|
||||
allKey: () => ["approval-grants"] as const,
|
||||
getByIdKey: (params: TGetApprovalGrantByIdDTO) =>
|
||||
[...approvalGrantQuery.allKey(), "by-id", params] as const,
|
||||
listKey: (params: TListApprovalGrantsDTO) =>
|
||||
[...approvalGrantQuery.allKey(), "list", params] as const,
|
||||
getById: (params: TGetApprovalGrantByIdDTO) =>
|
||||
queryOptions({
|
||||
queryKey: approvalGrantQuery.getByIdKey(params),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ grant: TApprovalGrant }>(
|
||||
`/api/v1/approval-policies/${params.policyType}/grants/${params.grantId}`
|
||||
);
|
||||
return data.grant;
|
||||
}
|
||||
}),
|
||||
list: (params: TListApprovalGrantsDTO) =>
|
||||
queryOptions({
|
||||
queryKey: approvalGrantQuery.listKey(params),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
grants: TApprovalGrant[];
|
||||
}>(`/api/v1/approval-policies/${params.policyType}/grants`, {
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
});
|
||||
return data.grants;
|
||||
}
|
||||
})
|
||||
};
|
||||
46
frontend/src/hooks/api/approvalGrants/types.ts
Normal file
46
frontend/src/hooks/api/approvalGrants/types.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ApprovalPolicyType } from "../approvalPolicies";
|
||||
|
||||
export enum ApprovalGrantStatus {
|
||||
Active = "active",
|
||||
Expired = "expired",
|
||||
Revoked = "revoked"
|
||||
}
|
||||
|
||||
// PAM Access Grant Attributes
|
||||
export type PamAccessGrantAttributes = {
|
||||
accountPath: string;
|
||||
accessDuration: string;
|
||||
};
|
||||
|
||||
// Base Grant Type
|
||||
export type TApprovalGrant = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
requestId: string | null;
|
||||
granteeUserId: string | null;
|
||||
revokedByUserId: string | null;
|
||||
revocationReason: string | null;
|
||||
status: ApprovalGrantStatus;
|
||||
type: ApprovalPolicyType;
|
||||
attributes: PamAccessGrantAttributes;
|
||||
createdAt: string;
|
||||
expiresAt: string | null;
|
||||
revokedAt: string | null;
|
||||
};
|
||||
|
||||
// DTOs
|
||||
export type TListApprovalGrantsDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TGetApprovalGrantByIdDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
grantId: string;
|
||||
};
|
||||
|
||||
export type TRevokeApprovalGrantDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
grantId: string;
|
||||
revocationReason?: string;
|
||||
};
|
||||
19
frontend/src/hooks/api/approvalPolicies/index.tsx
Normal file
19
frontend/src/hooks/api/approvalPolicies/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
export {
|
||||
useCreateApprovalPolicy,
|
||||
useDeleteApprovalPolicy,
|
||||
useUpdateApprovalPolicy
|
||||
} from "./mutations";
|
||||
export { approvalPolicyQuery } from "./queries";
|
||||
export {
|
||||
type ApprovalPolicyStep,
|
||||
ApprovalPolicyType,
|
||||
ApproverType,
|
||||
type PamAccessPolicyConditions,
|
||||
type PamAccessPolicyConstraints,
|
||||
type TApprovalPolicy,
|
||||
type TCreateApprovalPolicyDTO,
|
||||
type TDeleteApprovalPolicyDTO,
|
||||
type TGetApprovalPolicyByIdDTO,
|
||||
type TListApprovalPoliciesDTO,
|
||||
type TUpdateApprovalPolicyDTO
|
||||
} from "./types";
|
||||
58
frontend/src/hooks/api/approvalPolicies/mutations.tsx
Normal file
58
frontend/src/hooks/api/approvalPolicies/mutations.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { approvalPolicyQuery } from "./queries";
|
||||
import {
|
||||
TApprovalPolicy,
|
||||
TCreateApprovalPolicyDTO,
|
||||
TDeleteApprovalPolicyDTO,
|
||||
TUpdateApprovalPolicyDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ policyType, ...dto }: TCreateApprovalPolicyDTO) => {
|
||||
const { data } = await apiRequest.post<{ policy: TApprovalPolicy }>(
|
||||
`/api/v1/approval-policies/${policyType}`,
|
||||
dto
|
||||
);
|
||||
return data.policy;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: approvalPolicyQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ policyType, policyId, ...updates }: TUpdateApprovalPolicyDTO) => {
|
||||
const { data } = await apiRequest.patch<{ policy: TApprovalPolicy }>(
|
||||
`/api/v1/approval-policies/${policyType}/${policyId}`,
|
||||
updates
|
||||
);
|
||||
return data.policy;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: approvalPolicyQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ policyType, policyId }: TDeleteApprovalPolicyDTO) => {
|
||||
const { data } = await apiRequest.delete<{ policyId: string }>(
|
||||
`/api/v1/approval-policies/${policyType}/${policyId}`
|
||||
);
|
||||
return data.policyId;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: approvalPolicyQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
37
frontend/src/hooks/api/approvalPolicies/queries.tsx
Normal file
37
frontend/src/hooks/api/approvalPolicies/queries.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TApprovalPolicy, TGetApprovalPolicyByIdDTO, TListApprovalPoliciesDTO } from "./types";
|
||||
|
||||
export const approvalPolicyQuery = {
|
||||
allKey: () => ["approval-policies"] as const,
|
||||
getByIdKey: (params: TGetApprovalPolicyByIdDTO) =>
|
||||
[...approvalPolicyQuery.allKey(), "by-id", params] as const,
|
||||
listKey: (params: TListApprovalPoliciesDTO) =>
|
||||
[...approvalPolicyQuery.allKey(), "list", params] as const,
|
||||
getById: (params: TGetApprovalPolicyByIdDTO) =>
|
||||
queryOptions({
|
||||
queryKey: approvalPolicyQuery.getByIdKey(params),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ policy: TApprovalPolicy }>(
|
||||
`/api/v1/approval-policies/${params.policyType}/${params.policyId}`
|
||||
);
|
||||
return data.policy;
|
||||
}
|
||||
}),
|
||||
list: (params: TListApprovalPoliciesDTO) =>
|
||||
queryOptions({
|
||||
queryKey: approvalPolicyQuery.listKey(params),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
policies: TApprovalPolicy[];
|
||||
}>(`/api/v1/approval-policies/${params.policyType}`, {
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
});
|
||||
return data.policies;
|
||||
}
|
||||
})
|
||||
};
|
||||
83
frontend/src/hooks/api/approvalPolicies/types.ts
Normal file
83
frontend/src/hooks/api/approvalPolicies/types.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
export enum ApprovalPolicyType {
|
||||
PamAccess = "pam-access"
|
||||
}
|
||||
|
||||
export enum ApproverType {
|
||||
Group = "group",
|
||||
User = "user"
|
||||
}
|
||||
|
||||
export type ApprovalPolicyStep = {
|
||||
name?: string | null;
|
||||
requiredApprovals: number;
|
||||
notifyApprovers?: boolean;
|
||||
approvers: {
|
||||
type: ApproverType;
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type PamAccessPolicyConditions = {
|
||||
accountPaths: string[];
|
||||
}[];
|
||||
|
||||
export type PamAccessPolicyConstraints = {
|
||||
accessDuration: {
|
||||
min: string;
|
||||
max: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TApprovalPolicy = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
maxRequestTtl?: string | null;
|
||||
type: ApprovalPolicyType;
|
||||
conditions: {
|
||||
version: number;
|
||||
conditions: PamAccessPolicyConditions;
|
||||
};
|
||||
constraints: {
|
||||
version: number;
|
||||
constraints: PamAccessPolicyConstraints;
|
||||
};
|
||||
steps: ApprovalPolicyStep[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateApprovalPolicyDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
projectId: string;
|
||||
name: string;
|
||||
maxRequestTtl?: string | null;
|
||||
conditions: PamAccessPolicyConditions;
|
||||
constraints: PamAccessPolicyConstraints;
|
||||
steps: ApprovalPolicyStep[];
|
||||
};
|
||||
|
||||
export type TUpdateApprovalPolicyDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
policyId: string;
|
||||
name?: string;
|
||||
maxRequestTtl?: string | null;
|
||||
conditions?: PamAccessPolicyConditions;
|
||||
constraints?: PamAccessPolicyConstraints;
|
||||
steps?: ApprovalPolicyStep[];
|
||||
};
|
||||
|
||||
export type TGetApprovalPolicyByIdDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
policyId: string;
|
||||
};
|
||||
|
||||
export type TListApprovalPoliciesDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TDeleteApprovalPolicyDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
policyId: string;
|
||||
};
|
||||
20
frontend/src/hooks/api/approvalRequests/index.tsx
Normal file
20
frontend/src/hooks/api/approvalRequests/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
export {
|
||||
useApproveApprovalRequest,
|
||||
useCancelApprovalRequest,
|
||||
useCreateApprovalRequest,
|
||||
useRejectApprovalRequest
|
||||
} from "./mutations";
|
||||
export { approvalRequestQuery } from "./queries";
|
||||
export {
|
||||
type ApprovalRequestApproval,
|
||||
ApprovalRequestStatus,
|
||||
type ApprovalRequestStep,
|
||||
ApprovalRequestStepStatus,
|
||||
type PamAccessRequestData,
|
||||
type TApprovalRequest,
|
||||
type TApproveApprovalRequestDTO,
|
||||
type TCreateApprovalRequestDTO,
|
||||
type TGetApprovalRequestByIdDTO,
|
||||
type TListApprovalRequestsDTO,
|
||||
type TRejectApprovalRequestDTO
|
||||
} from "./types";
|
||||
75
frontend/src/hooks/api/approvalRequests/mutations.tsx
Normal file
75
frontend/src/hooks/api/approvalRequests/mutations.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { approvalRequestQuery } from "./queries";
|
||||
import {
|
||||
TApprovalRequest,
|
||||
TApproveApprovalRequestDTO,
|
||||
TCancelApprovalRequestDTO,
|
||||
TCreateApprovalRequestDTO,
|
||||
TRejectApprovalRequestDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateApprovalRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ policyType, ...dto }: TCreateApprovalRequestDTO) => {
|
||||
const { data } = await apiRequest.post<{ request: TApprovalRequest }>(
|
||||
`/api/v1/approval-policies/${policyType}/requests`,
|
||||
dto
|
||||
);
|
||||
return data.request;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useApproveApprovalRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ policyType, requestId, comment }: TApproveApprovalRequestDTO) => {
|
||||
const { data } = await apiRequest.post<{ request: TApprovalRequest }>(
|
||||
`/api/v1/approval-policies/${policyType}/requests/${requestId}/approve`,
|
||||
{ comment }
|
||||
);
|
||||
return data.request;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRejectApprovalRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ policyType, requestId, comment }: TRejectApprovalRequestDTO) => {
|
||||
const { data } = await apiRequest.post<{ request: TApprovalRequest }>(
|
||||
`/api/v1/approval-policies/${policyType}/requests/${requestId}/reject`,
|
||||
{ comment }
|
||||
);
|
||||
return data.request;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCancelApprovalRequest = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ policyType, requestId }: TCancelApprovalRequestDTO) => {
|
||||
const { data } = await apiRequest.post<{ request: TApprovalRequest }>(
|
||||
`/api/v1/approval-policies/${policyType}/requests/${requestId}/cancel`
|
||||
);
|
||||
return data.request;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() });
|
||||
}
|
||||
});
|
||||
};
|
||||
37
frontend/src/hooks/api/approvalRequests/queries.tsx
Normal file
37
frontend/src/hooks/api/approvalRequests/queries.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TApprovalRequest, TGetApprovalRequestByIdDTO, TListApprovalRequestsDTO } from "./types";
|
||||
|
||||
export const approvalRequestQuery = {
|
||||
allKey: () => ["approval-requests"] as const,
|
||||
getByIdKey: (params: TGetApprovalRequestByIdDTO) =>
|
||||
[...approvalRequestQuery.allKey(), "by-id", params] as const,
|
||||
listKey: (params: TListApprovalRequestsDTO) =>
|
||||
[...approvalRequestQuery.allKey(), "list", params] as const,
|
||||
getById: (params: TGetApprovalRequestByIdDTO) =>
|
||||
queryOptions({
|
||||
queryKey: approvalRequestQuery.getByIdKey(params),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ request: TApprovalRequest }>(
|
||||
`/api/v1/approval-policies/${params.policyType}/requests/${params.requestId}`
|
||||
);
|
||||
return data.request;
|
||||
}
|
||||
}),
|
||||
list: (params: TListApprovalRequestsDTO) =>
|
||||
queryOptions({
|
||||
queryKey: approvalRequestQuery.listKey(params),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
requests: TApprovalRequest[];
|
||||
}>(`/api/v1/approval-policies/${params.policyType}/requests`, {
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
});
|
||||
return data.requests;
|
||||
}
|
||||
})
|
||||
};
|
||||
110
frontend/src/hooks/api/approvalRequests/types.ts
Normal file
110
frontend/src/hooks/api/approvalRequests/types.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { ApprovalPolicyType, ApproverType } from "../approvalPolicies";
|
||||
|
||||
export enum ApprovalRequestStatus {
|
||||
Pending = "pending",
|
||||
Approved = "approved",
|
||||
Rejected = "rejected",
|
||||
Expired = "expired",
|
||||
Cancelled = "cancelled"
|
||||
}
|
||||
|
||||
export enum ApprovalRequestStepStatus {
|
||||
Pending = "pending",
|
||||
InProgress = "in-progress",
|
||||
Approved = "approved",
|
||||
Rejected = "rejected"
|
||||
}
|
||||
|
||||
export enum ApprovalRequestApprovalDecision {
|
||||
Approved = "approved",
|
||||
Rejected = "rejected"
|
||||
}
|
||||
|
||||
export type ApprovalRequestApproval = {
|
||||
id: string;
|
||||
stepId: string;
|
||||
approverUserId: string;
|
||||
decision: ApprovalRequestApprovalDecision.Approved;
|
||||
comment?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ApprovalRequestStep = {
|
||||
id: string;
|
||||
requestId: string;
|
||||
name?: string | null;
|
||||
requiredApprovals: number;
|
||||
notifyApprovers?: boolean | null;
|
||||
stepNumber: number;
|
||||
status: ApprovalRequestStepStatus;
|
||||
startedAt?: string | null;
|
||||
completedAt?: string | null;
|
||||
approvers: {
|
||||
type: ApproverType;
|
||||
id: string;
|
||||
}[];
|
||||
approvals: ApprovalRequestApproval[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type PamAccessRequestData = {
|
||||
accountPath: string;
|
||||
accessDuration: string;
|
||||
};
|
||||
|
||||
export type TApprovalRequest = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
policyId: string;
|
||||
type: ApprovalPolicyType;
|
||||
status: ApprovalRequestStatus;
|
||||
requesterId: string;
|
||||
requesterName: string;
|
||||
requesterEmail: string;
|
||||
justification?: string | null;
|
||||
expiresAt?: string | null;
|
||||
requestData: {
|
||||
version: number;
|
||||
requestData: PamAccessRequestData;
|
||||
};
|
||||
steps: ApprovalRequestStep[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateApprovalRequestDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
projectId: string;
|
||||
justification?: string | null;
|
||||
requestDuration?: string | null;
|
||||
requestData: PamAccessRequestData;
|
||||
};
|
||||
|
||||
export type TGetApprovalRequestByIdDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
export type TListApprovalRequestsDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TApproveApprovalRequestDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
requestId: string;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
export type TRejectApprovalRequestDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
requestId: string;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
export type TCancelApprovalRequestDTO = {
|
||||
policyType: ApprovalPolicyType;
|
||||
requestId: string;
|
||||
};
|
||||
@@ -291,7 +291,22 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.SIGN_CERTIFICATE_FROM_PROFILE]: "Sign Certificate From Profile",
|
||||
[EventType.ORDER_CERTIFICATE_FROM_PROFILE]: "Order Certificate From Profile",
|
||||
[EventType.GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE]:
|
||||
"Get Certificate Profile Latest Active Bundle"
|
||||
"Get Certificate Profile Latest Active Bundle",
|
||||
|
||||
[EventType.APPROVAL_POLICY_CREATE]: "Create Approval Policy",
|
||||
[EventType.APPROVAL_POLICY_UPDATE]: "Update Approval Policy",
|
||||
[EventType.APPROVAL_POLICY_DELETE]: "Delete Approval Policy",
|
||||
[EventType.APPROVAL_POLICY_LIST]: "List Approval Policies",
|
||||
[EventType.APPROVAL_POLICY_GET]: "Get Approval Policy",
|
||||
[EventType.APPROVAL_REQUEST_GET]: "Get Approval Request",
|
||||
[EventType.APPROVAL_REQUEST_LIST]: "List Approval Requests",
|
||||
[EventType.APPROVAL_REQUEST_CREATE]: "Create Approval Request",
|
||||
[EventType.APPROVAL_REQUEST_APPROVE]: "Approve Approval Request",
|
||||
[EventType.APPROVAL_REQUEST_REJECT]: "Reject Approval Request",
|
||||
[EventType.APPROVAL_REQUEST_CANCEL]: "Cancel Approval Request",
|
||||
[EventType.APPROVAL_REQUEST_GRANT_LIST]: "List Approval Request Grants",
|
||||
[EventType.APPROVAL_REQUEST_GRANT_GET]: "Get Approval Request Grant",
|
||||
[EventType.APPROVAL_REQUEST_GRANT_REVOKE]: "Revoke Approval Request Grant"
|
||||
};
|
||||
|
||||
export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = {
|
||||
@@ -309,7 +324,21 @@ const sharedProjectEvents = [
|
||||
EventType.REMOVE_PROJECT_MEMBER,
|
||||
EventType.CREATE_PROJECT_ROLE,
|
||||
EventType.UPDATE_PROJECT_ROLE,
|
||||
EventType.DELETE_PROJECT_ROLE
|
||||
EventType.DELETE_PROJECT_ROLE,
|
||||
EventType.APPROVAL_POLICY_CREATE,
|
||||
EventType.APPROVAL_POLICY_UPDATE,
|
||||
EventType.APPROVAL_POLICY_DELETE,
|
||||
EventType.APPROVAL_POLICY_LIST,
|
||||
EventType.APPROVAL_POLICY_GET,
|
||||
EventType.APPROVAL_REQUEST_GET,
|
||||
EventType.APPROVAL_REQUEST_LIST,
|
||||
EventType.APPROVAL_REQUEST_CREATE,
|
||||
EventType.APPROVAL_REQUEST_APPROVE,
|
||||
EventType.APPROVAL_REQUEST_REJECT,
|
||||
EventType.APPROVAL_REQUEST_CANCEL,
|
||||
EventType.APPROVAL_REQUEST_GRANT_LIST,
|
||||
EventType.APPROVAL_REQUEST_GRANT_GET,
|
||||
EventType.APPROVAL_REQUEST_GRANT_REVOKE
|
||||
];
|
||||
|
||||
export const projectToEventsMap: Partial<Record<ProjectType, EventType[]>> = {
|
||||
|
||||
@@ -282,5 +282,20 @@ export enum EventType {
|
||||
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
|
||||
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
|
||||
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
|
||||
GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle"
|
||||
GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle",
|
||||
|
||||
APPROVAL_POLICY_CREATE = "approval-policy-create",
|
||||
APPROVAL_POLICY_UPDATE = "approval-policy-update",
|
||||
APPROVAL_POLICY_DELETE = "approval-policy-delete",
|
||||
APPROVAL_POLICY_LIST = "approval-policy-list",
|
||||
APPROVAL_POLICY_GET = "approval-policy-get",
|
||||
APPROVAL_REQUEST_GET = "approval-request-get",
|
||||
APPROVAL_REQUEST_LIST = "approval-request-list",
|
||||
APPROVAL_REQUEST_CREATE = "approval-request-create",
|
||||
APPROVAL_REQUEST_APPROVE = "approval-request-approve",
|
||||
APPROVAL_REQUEST_REJECT = "approval-request-reject",
|
||||
APPROVAL_REQUEST_CANCEL = "approval-request-cancel",
|
||||
APPROVAL_REQUEST_GRANT_LIST = "approval-request-grant-list",
|
||||
APPROVAL_REQUEST_GRANT_GET = "approval-request-grant-get",
|
||||
APPROVAL_REQUEST_GRANT_REVOKE = "approval-request-grant-revoke"
|
||||
}
|
||||
|
||||
@@ -58,10 +58,12 @@ export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const selectOrganization = async (data: {
|
||||
export type SelectOrganizationParams = {
|
||||
organizationId: string;
|
||||
userAgent?: UserAgentType;
|
||||
}) => {
|
||||
};
|
||||
|
||||
export const selectOrganization = async (data: SelectOrganizationParams) => {
|
||||
const { data: res } = await apiRequest.post<{
|
||||
token: string;
|
||||
isMfaEnabled: boolean;
|
||||
@@ -73,7 +75,7 @@ export const selectOrganization = async (data: {
|
||||
export const useSelectOrganization = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => {
|
||||
mutationFn: async (details: SelectOrganizationParams) => {
|
||||
const data = await selectOrganization(details);
|
||||
|
||||
// If a custom user agent is set, then this session is meant for another consuming application, not the web application.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type GetAuthTokenAPI = {
|
||||
token: string;
|
||||
organizationId?: string;
|
||||
subOrganizationId?: string;
|
||||
};
|
||||
|
||||
export enum UserEncryptionVersion {
|
||||
|
||||
@@ -5,7 +5,6 @@ export enum IdentityProjectAdditionalPrivilegeTemporaryMode {
|
||||
}
|
||||
|
||||
export type TIdentityProjectPrivilege = {
|
||||
projectMembershipId: string;
|
||||
slug: string;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export * from "./accessApproval";
|
||||
export * from "./admin";
|
||||
export * from "./apiKeys";
|
||||
export * from "./approvalGrants";
|
||||
export * from "./approvalPolicies";
|
||||
export * from "./approvalRequests";
|
||||
export * from "./assumePrivileges";
|
||||
export * from "./auditLogs";
|
||||
export * from "./auditLogStreams";
|
||||
|
||||
@@ -42,7 +42,7 @@ export const organizationKeys = {
|
||||
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
|
||||
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
|
||||
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
|
||||
getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }],
|
||||
getOrgById: (orgId: string) => ["organization", { orgId }],
|
||||
getAvailableIdentities: () => ["available-identities"],
|
||||
getAvailableUsers: () => ["available-users"]
|
||||
};
|
||||
@@ -67,7 +67,7 @@ export const fetchOrganizationById = async (id: string) => {
|
||||
const {
|
||||
data: { organization }
|
||||
} = await apiRequest.get<{
|
||||
organization: Organization & { subOrganization?: { id: string; name: string } };
|
||||
organization: Organization;
|
||||
}>(`/api/v1/organization/${id}`);
|
||||
return organization;
|
||||
};
|
||||
|
||||
@@ -30,6 +30,8 @@ export type Organization = {
|
||||
maxSharedSecretLifetime: number;
|
||||
maxSharedSecretViewLimit: number | null;
|
||||
blockDuplicateSecretSyncDestinations: boolean;
|
||||
parentOrgId: string | null;
|
||||
rootOrgId: string | null;
|
||||
};
|
||||
|
||||
export type UpdateOrgDTO = {
|
||||
|
||||
@@ -16,7 +16,8 @@ export enum PamResourceType {
|
||||
CockroachDB = "cockroachdb",
|
||||
Elasticsearch = "elasticsearch",
|
||||
Snowflake = "snowflake",
|
||||
DynamoDB = "dynamodb"
|
||||
DynamoDB = "dynamodb",
|
||||
AwsIam = "aws-iam"
|
||||
}
|
||||
|
||||
export enum PamResourceOrderBy {
|
||||
|
||||
@@ -20,5 +20,6 @@ export const PAM_RESOURCE_TYPE_MAP: Record<
|
||||
[PamResourceType.CockroachDB]: { name: "CockroachDB", image: "CockroachDB.png" },
|
||||
[PamResourceType.Elasticsearch]: { name: "Elasticsearch", image: "Elastic.png" },
|
||||
[PamResourceType.Snowflake]: { name: "Snowflake", image: "Snowflake.png" },
|
||||
[PamResourceType.DynamoDB]: { name: "DynamoDB", image: "DynamoDB.png", size: 55 }
|
||||
[PamResourceType.DynamoDB]: { name: "DynamoDB", image: "DynamoDB.png", size: 55 },
|
||||
[PamResourceType.AwsIam]: { name: "AWS IAM", image: "Amazon Web Services.png" }
|
||||
};
|
||||
|
||||
@@ -120,6 +120,45 @@ export const useDeletePamAccount = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export type TAccessPamAccountDTO = {
|
||||
accountId: string;
|
||||
accountPath: string;
|
||||
projectId: string;
|
||||
duration: string;
|
||||
};
|
||||
|
||||
export type TAccessPamAccountResponse = {
|
||||
sessionId: string;
|
||||
resourceType: string;
|
||||
consoleUrl?: string;
|
||||
metadata?: Record<string, string | undefined>;
|
||||
relayClientCertificate?: string;
|
||||
relayClientPrivateKey?: string;
|
||||
relayServerCertificateChain?: string;
|
||||
gatewayClientCertificate?: string;
|
||||
gatewayClientPrivateKey?: string;
|
||||
gatewayServerCertificateChain?: string;
|
||||
relayHost?: string;
|
||||
};
|
||||
|
||||
export const useAccessPamAccount = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ accountId, accountPath, projectId, duration }: TAccessPamAccountDTO) => {
|
||||
const { data } = await apiRequest.post<TAccessPamAccountResponse>(
|
||||
"/api/v1/pam/accounts/access",
|
||||
{
|
||||
accountId,
|
||||
accountPath,
|
||||
projectId,
|
||||
duration
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Folders
|
||||
export const useCreatePamFolder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
25
frontend/src/hooks/api/pam/types/aws-iam-resource.ts
Normal file
25
frontend/src/hooks/api/pam/types/aws-iam-resource.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { PamResourceType } from "../enums";
|
||||
import { TBasePamAccount } from "./base-account";
|
||||
import { TBasePamResource } from "./base-resource";
|
||||
|
||||
export type TAwsIamConnectionDetails = {
|
||||
roleArn: string;
|
||||
};
|
||||
|
||||
export type TAwsIamCredentials = {
|
||||
targetRoleArn: string;
|
||||
defaultSessionDuration: number;
|
||||
};
|
||||
|
||||
export type TAwsIamResource = Omit<TBasePamResource, "gatewayId"> & {
|
||||
resourceType: PamResourceType.AwsIam;
|
||||
gatewayId?: string | null;
|
||||
connectionDetails: TAwsIamConnectionDetails;
|
||||
};
|
||||
|
||||
export type TAwsIamAccount = Omit<
|
||||
TBasePamAccount,
|
||||
"rotationEnabled" | "rotationIntervalSeconds" | "lastRotatedAt"
|
||||
> & {
|
||||
credentials: TAwsIamCredentials;
|
||||
};
|
||||
@@ -6,19 +6,31 @@ import {
|
||||
PamResourceType,
|
||||
PamSessionStatus
|
||||
} from "../enums";
|
||||
import { TAwsIamAccount, TAwsIamResource } from "./aws-iam-resource";
|
||||
import { TKubernetesAccount, TKubernetesResource } from "./kubernetes-resource";
|
||||
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
|
||||
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
||||
import { TSSHAccount, TSSHResource } from "./ssh-resource";
|
||||
|
||||
export * from "./aws-iam-resource";
|
||||
export * from "./kubernetes-resource";
|
||||
export * from "./mysql-resource";
|
||||
export * from "./postgres-resource";
|
||||
export * from "./ssh-resource";
|
||||
|
||||
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TKubernetesResource;
|
||||
export type TPamResource =
|
||||
| TPostgresResource
|
||||
| TMySQLResource
|
||||
| TSSHResource
|
||||
| TAwsIamResource
|
||||
| TKubernetesResource;
|
||||
|
||||
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TKubernetesAccount;
|
||||
export type TPamAccount =
|
||||
| TPostgresAccount
|
||||
| TMySQLAccount
|
||||
| TSSHAccount
|
||||
| TAwsIamAccount
|
||||
| TKubernetesAccount;
|
||||
|
||||
export type TPamFolder = {
|
||||
id: string;
|
||||
|
||||
@@ -3,14 +3,16 @@ import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types";
|
||||
import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "./types";
|
||||
|
||||
export const projectKeys = {
|
||||
getProjectById: (projectId: string) => ["projects", { projectId }] as const,
|
||||
allProjectQueries: () => ["projects"] as const,
|
||||
getProjectById: (projectId: string) =>
|
||||
[...projectKeys.allProjectQueries(), { projectId }] as const,
|
||||
getProjectSecrets: (projectId: string) => [{ projectId }, "project-secrets"] as const,
|
||||
getProjectIndexStatus: (projectId: string) => [{ projectId }, "project-index-status"] as const,
|
||||
getProjectUpgradeStatus: (projectId: string) => [{ projectId }, "project-upgrade-status"],
|
||||
getProjectMemberships: (orgId: string) => [{ orgId }, "project-memberships"],
|
||||
getProjectAuthorization: (projectId: string) => [{ projectId }, "project-authorizations"],
|
||||
getProjectIntegrations: (projectId: string) => [{ projectId }, "project-integrations"],
|
||||
getAllUserProjects: () => ["projects"] as const,
|
||||
getAllUserProjects: () => [...projectKeys.allProjectQueries()] as const,
|
||||
getProjectAuditLogs: (projectId: string) => [{ projectId }, "project-audit-logs"] as const,
|
||||
getProjectUsers: (
|
||||
projectId: string,
|
||||
@@ -28,7 +30,8 @@ export const projectKeys = {
|
||||
// allows invalidation using above key without knowing params
|
||||
getProjectIdentityMembershipsWithParams: ({ projectId, ...params }: TListProjectIdentitiesDTO) =>
|
||||
[...projectKeys.getProjectIdentityMemberships(projectId), params] as const,
|
||||
searchProject: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const,
|
||||
searchProject: (dto: TSearchProjectsDTO) =>
|
||||
[...projectKeys.allProjectQueries(), "search-projects", dto] as const,
|
||||
getProjectGroupMemberships: (projectId: string) => [{ projectId }, "project-groups"] as const,
|
||||
getProjectGroupMembershipDetails: (projectId: string, groupId: string) =>
|
||||
[{ projectId, groupId }, "project-group-membership-details"] as const,
|
||||
|
||||
@@ -8,7 +8,8 @@ export enum SecretRotation {
|
||||
LdapPassword = "ldap-password",
|
||||
AwsIamUserSecret = "aws-iam-user-secret",
|
||||
OktaClientSecret = "okta-client-secret",
|
||||
RedisCredentials = "redis-credentials"
|
||||
RedisCredentials = "redis-credentials",
|
||||
MongoDBCredentials = "mongodb-credentials"
|
||||
}
|
||||
|
||||
export enum SecretRotationStatus {
|
||||
|
||||
@@ -31,6 +31,11 @@ import { TSqlCredentialsRotationOption } from "@app/hooks/api/secretRotationsV2/
|
||||
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
import {
|
||||
TMongoDBCredentialsRotation,
|
||||
TMongoDBCredentialsRotationGeneratedCredentialsResponse,
|
||||
TMongoDBCredentialsRotationOption
|
||||
} from "./mongodb-credentials-rotation";
|
||||
import {
|
||||
TMySqlCredentialsRotation,
|
||||
TMySqlCredentialsRotationGeneratedCredentialsResponse
|
||||
@@ -61,6 +66,7 @@ export type TSecretRotationV2 = (
|
||||
| TAwsIamUserSecretRotation
|
||||
| TOktaClientSecretRotation
|
||||
| TRedisCredentialsRotation
|
||||
| TMongoDBCredentialsRotation
|
||||
) & {
|
||||
secrets: (SecretV3RawSanitized | null)[];
|
||||
};
|
||||
@@ -72,7 +78,8 @@ export type TSecretRotationV2Option =
|
||||
| TLdapPasswordRotationOption
|
||||
| TAwsIamUserSecretRotationOption
|
||||
| TOktaClientSecretRotationOption
|
||||
| TRedisCredentialsRotationOption;
|
||||
| TRedisCredentialsRotationOption
|
||||
| TMongoDBCredentialsRotationOption;
|
||||
|
||||
export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] };
|
||||
|
||||
@@ -88,7 +95,8 @@ export type TViewSecretRotationGeneratedCredentialsResponse =
|
||||
| TLdapPasswordRotationGeneratedCredentialsResponse
|
||||
| TAwsIamUserSecretRotationGeneratedCredentialsResponse
|
||||
| TOktaClientSecretRotationGeneratedCredentialsResponse
|
||||
| TRedisCredentialsRotationGeneratedCredentialsResponse;
|
||||
| TRedisCredentialsRotationGeneratedCredentialsResponse
|
||||
| TMongoDBCredentialsRotationGeneratedCredentialsResponse;
|
||||
|
||||
export type TCreateSecretRotationV2DTO = DiscriminativePick<
|
||||
TSecretRotationV2,
|
||||
@@ -142,6 +150,7 @@ export type TSecretRotationOptionMap = {
|
||||
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption;
|
||||
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationOption;
|
||||
[SecretRotation.RedisCredentials]: TRedisCredentialsRotationOption;
|
||||
[SecretRotation.MongoDBCredentials]: TMongoDBCredentialsRotationOption;
|
||||
};
|
||||
|
||||
export type TSecretRotationGeneratedCredentialsResponseMap = {
|
||||
@@ -155,4 +164,5 @@ export type TSecretRotationGeneratedCredentialsResponseMap = {
|
||||
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.OktaClientSecret]: TOktaClientSecretRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.RedisCredentials]: TRedisCredentialsRotationGeneratedCredentialsResponse;
|
||||
[SecretRotation.MongoDBCredentials]: TMongoDBCredentialsRotationGeneratedCredentialsResponse;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
|
||||
import {
|
||||
TSecretRotationV2Base,
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase,
|
||||
TSqlCredentialsRotationGeneratedCredentials,
|
||||
TSqlCredentialsRotationProperties
|
||||
} from "@app/hooks/api/secretRotationsV2/types/shared";
|
||||
|
||||
export type TMongoDBCredentialsRotation = TSecretRotationV2Base & {
|
||||
type: SecretRotation.MongoDBCredentials;
|
||||
} & TSqlCredentialsRotationProperties;
|
||||
|
||||
export type TMongoDBCredentialsRotationGeneratedCredentialsResponse =
|
||||
TSecretRotationV2GeneratedCredentialsResponseBase<
|
||||
SecretRotation.MongoDBCredentials,
|
||||
TSqlCredentialsRotationGeneratedCredentials
|
||||
>;
|
||||
|
||||
export type TMongoDBCredentialsRotationOption = {
|
||||
name: string;
|
||||
type: SecretRotation.MongoDBCredentials;
|
||||
connection: AppConnection.MongoDB;
|
||||
template: {
|
||||
createUserStatement: string;
|
||||
secretsMapping: TMongoDBCredentialsRotation["secretsMapping"];
|
||||
};
|
||||
};
|
||||
@@ -18,7 +18,7 @@ export type TIdentity = {
|
||||
updatedAt: string;
|
||||
hasDeleteProtection: boolean;
|
||||
authMethods: IdentityAuthMethod[];
|
||||
activeLockoutAuthMethods: string[];
|
||||
activeLockoutAuthMethods: IdentityAuthMethod[];
|
||||
metadata?: Array<TMetadata & { id: string }>;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,10 +11,7 @@ export const useCreateSubOrganization = () => {
|
||||
mutationFn: async (dto: TCreateSubOrganizationDTO) => {
|
||||
const { data } = await apiRequest.post<{ organization: TSubOrganization }>(
|
||||
"/api/v1/sub-organizations",
|
||||
dto,
|
||||
{
|
||||
headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header
|
||||
}
|
||||
dto
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -78,6 +78,7 @@ export type TUserMembership = {
|
||||
scope: string;
|
||||
scopeOrgId: string;
|
||||
actorUserId: string;
|
||||
actorGroupId: string;
|
||||
};
|
||||
|
||||
export type TProjectMembership = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -80,7 +80,11 @@ const getPlan = (subscription: SubscriptionPlan) => {
|
||||
return "Free";
|
||||
};
|
||||
|
||||
const getFormattedSupportEmailLink = (variables: { org_id: string; domain: string }) => {
|
||||
const getFormattedSupportEmailLink = (variables: {
|
||||
org_id: string;
|
||||
domain: string;
|
||||
root_org_id?: string;
|
||||
}) => {
|
||||
const email = "support@infisical.com";
|
||||
|
||||
const body = `Hello Infisical Support Team,
|
||||
@@ -94,6 +98,7 @@ Issue Details:
|
||||
|
||||
Account Info:
|
||||
- Organization ID: ${variables.org_id}
|
||||
${variables.root_org_id ? `- Root Organization ID: ${variables.root_org_id}` : ""}
|
||||
- Domain: ${variables.domain}
|
||||
|
||||
Thank you,
|
||||
@@ -169,6 +174,10 @@ export const Navbar = () => {
|
||||
|
||||
const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days);
|
||||
|
||||
const rootOrg = isSubOrganization
|
||||
? orgs?.find((org) => org.id === currentOrg.rootOrgId) || currentOrg
|
||||
: currentOrg;
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalIntrusive) {
|
||||
setShowCardDeclinedModal(true);
|
||||
@@ -182,10 +191,20 @@ export const Navbar = () => {
|
||||
}
|
||||
}, [subscription, isBillingPage, isModalIntrusive]);
|
||||
|
||||
const handleOrgChange = async (orgId: string) => {
|
||||
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||
organizationId: orgId
|
||||
});
|
||||
const handleOrgSelection = async ({
|
||||
organizationId,
|
||||
navigateTo,
|
||||
onSuccess
|
||||
}: {
|
||||
organizationId?: string;
|
||||
navigateTo?: string;
|
||||
onSuccess?: () => void | Promise<void>;
|
||||
}) => {
|
||||
if (!organizationId) return;
|
||||
|
||||
if (organizationId === currentOrg.id) return;
|
||||
|
||||
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
|
||||
|
||||
if (isMfaEnabled) {
|
||||
SecurityClient.setMfaToken(token);
|
||||
@@ -193,14 +212,58 @@ export const Navbar = () => {
|
||||
setRequiredMfaMethod(mfaMethod);
|
||||
}
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => () => handleOrgChange(orgId));
|
||||
setMfaSuccessCallback(() => async () => {
|
||||
await handleOrgSelection({ organizationId, onSuccess });
|
||||
});
|
||||
return;
|
||||
}
|
||||
await router.invalidate();
|
||||
await navigateUserToOrg(navigate, orgId);
|
||||
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
|
||||
|
||||
SecurityClient.setToken(token);
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
||||
queryClient.removeQueries({ queryKey: subOrgQuery.queryKey });
|
||||
|
||||
await queryClient.refetchQueries({ queryKey: authKeys.getAuthToken });
|
||||
|
||||
await navigateUserToOrg({ navigate, organizationId, navigateTo });
|
||||
queryClient.removeQueries({ queryKey: projectKeys.allProjectQueries() });
|
||||
|
||||
if (onSuccess) {
|
||||
await onSuccess();
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigateToRootOrgBilling = async () => {
|
||||
const navigateToBilling = () => {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/billing",
|
||||
params: { orgId: rootOrg.id }
|
||||
});
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
setShowCardDeclinedModal(false);
|
||||
};
|
||||
|
||||
if (isSubOrganization) {
|
||||
await handleOrgSelection({ organizationId: rootOrg.id, onSuccess });
|
||||
} else {
|
||||
await navigateToBilling();
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigateToAdminConsole = async () => {
|
||||
const navigateToAdminConsole = () => {
|
||||
navigate({
|
||||
to: "/admin"
|
||||
});
|
||||
};
|
||||
|
||||
if (isSubOrganization) {
|
||||
await handleOrgSelection({ organizationId: rootOrg.id, navigateTo: "/admin" });
|
||||
} else {
|
||||
navigateToAdminConsole();
|
||||
}
|
||||
};
|
||||
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
@@ -272,7 +335,7 @@ export const Navbar = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
handleOrgChange(org?.id);
|
||||
handleOrgSelection({ organizationId: org?.id });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -314,17 +377,20 @@ export const Navbar = () => {
|
||||
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: currentOrg.id }
|
||||
});
|
||||
if (isSubOrganization) {
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
await handleOrgSelection({
|
||||
organizationId: currentOrg.rootOrgId as string
|
||||
});
|
||||
} else {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: currentOrg.id }
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
|
||||
<span className="truncate">{currentOrg?.name}</span>
|
||||
<span className="truncate">{rootOrg?.name}</span>
|
||||
<Badge variant="org" className="hidden lg:inline-flex">
|
||||
Organization
|
||||
</Badge>
|
||||
@@ -397,13 +463,7 @@ export const Navbar = () => {
|
||||
</div>
|
||||
{subOrganizations.map((subOrg) => (
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: subOrg.id }
|
||||
});
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
}}
|
||||
onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
|
||||
className="cursor-pointer font-normal"
|
||||
key={subOrg.id}
|
||||
>
|
||||
@@ -458,79 +518,93 @@ export const Navbar = () => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{currentOrg.subOrganization && (
|
||||
{isSubOrganization && (
|
||||
<>
|
||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
||||
<DropdownMenu modal={false}>
|
||||
<Badge
|
||||
asChild
|
||||
isTruncatable
|
||||
variant="sub-org"
|
||||
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
|
||||
className={twMerge(
|
||||
"gap-x-1.5 text-sm",
|
||||
isProjectScope &&
|
||||
"min-w-6 bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-sub-org"
|
||||
)}
|
||||
>
|
||||
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
|
||||
<SubOrgIcon className="size-[12px]" />
|
||||
<span>{currentOrg.subOrganization.name}</span>
|
||||
</Link>
|
||||
</Badge>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="switch-org"
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
<ChevronRight size={18} className="mt-[3px] mr-3 text-mineshaft-400/70" />
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative flex min-w-16 items-center self-end rounded-t-md border-x border-t pt-1.5 pr-2 pb-2.5 pl-3",
|
||||
!isProjectScope && isSubOrganization
|
||||
? "border-sub-org/15 bg-gradient-to-b from-sub-org/10 to-sub-org/[0.075]"
|
||||
: "border-transparent"
|
||||
)}
|
||||
>
|
||||
{/* scott: the below is used to hide the top border from the org nav bar */}
|
||||
{!isProjectScope && isSubOrganization && (
|
||||
<div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
|
||||
<div className="h-full bg-sub-org/[0.075]" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="center"
|
||||
side="bottom"
|
||||
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "220px" }}
|
||||
>
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
|
||||
Sub-Organizations
|
||||
</div>
|
||||
{subOrganizations.map((subOrg) => (
|
||||
<DropdownMenuItem
|
||||
)}
|
||||
<DropdownMenu modal={false}>
|
||||
<div className="group mr-1 flex min-w-0 cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100">
|
||||
<button
|
||||
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: subOrg.id }
|
||||
params: { orgId: currentOrg.id }
|
||||
});
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
if (isSubOrganization) {
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer font-normal"
|
||||
key={subOrg.id}
|
||||
>
|
||||
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
|
||||
{currentOrg?.id === subOrg.id && (
|
||||
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
|
||||
)}
|
||||
<p className="truncate">{subOrg.name}</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{Boolean(subOrganizations.length) && (
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => setShowSubOrgForm(true)}
|
||||
<SubOrgIcon className={twMerge("size-[14px] shrink-0 text-sub-org")} />
|
||||
<span className="truncate">{currentOrg?.name}</span>
|
||||
<Badge variant="sub-org" className="hidden lg:inline-flex">
|
||||
Sub-Organization
|
||||
</Badge>
|
||||
</button>
|
||||
</div>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="switch-org"
|
||||
className="px-2 py-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="center"
|
||||
side="bottom"
|
||||
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "220px" }}
|
||||
>
|
||||
New Sub-Organization
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
|
||||
Sub-Organizations
|
||||
</div>
|
||||
{subOrganizations.map((subOrg) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleOrgSelection({ organizationId: subOrg.id })}
|
||||
className="cursor-pointer font-normal"
|
||||
key={subOrg.id}
|
||||
>
|
||||
<div className="flex w-full max-w-48 cursor-pointer items-center gap-x-2">
|
||||
{currentOrg?.id === subOrg.id && (
|
||||
<FontAwesomeIcon icon={faCheck} className="shrink-0 text-primary" />
|
||||
)}
|
||||
<p className="truncate">{subOrg.name}</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{Boolean(subOrganizations.length) && (
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => setShowSubOrgForm(true)}
|
||||
>
|
||||
New Sub-Organization
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isProjectScope && (
|
||||
@@ -550,11 +624,11 @@ export const Navbar = () => {
|
||||
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||
leftIcon={<FontAwesomeIcon icon={faInfinity} />}
|
||||
onClick={async () => {
|
||||
if (!subscription || !currentOrg) return;
|
||||
if (!subscription || !rootOrg) return;
|
||||
|
||||
// direct user to start pro trial
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg.id,
|
||||
orgId: rootOrg.id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
@@ -575,6 +649,7 @@ export const Navbar = () => {
|
||||
<Link
|
||||
className="mr-2 flex h-[34px] items-center rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
|
||||
to="/admin"
|
||||
onClick={handleNavigateToAdminConsole}
|
||||
>
|
||||
<InstanceIcon className="inline-block size-3.5" />
|
||||
<span className="ml-2 hidden md:inline-block">Server Console</span>
|
||||
@@ -612,7 +687,8 @@ export const Navbar = () => {
|
||||
text === "Email Support"
|
||||
? getUrl({
|
||||
org_id: currentOrg.id,
|
||||
domain: window.location.origin
|
||||
domain: window.location.origin,
|
||||
...(isSubOrganization && { root_org_id: rootOrg.id })
|
||||
})
|
||||
: getUrl();
|
||||
|
||||
@@ -772,19 +848,13 @@ export const Navbar = () => {
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="flex space-x-3">
|
||||
<Link
|
||||
to="/organizations/$orgId/billing"
|
||||
params={{ orgId: currentOrg.id }}
|
||||
className="inline-flex"
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={handleNavigateToRootOrgBilling}
|
||||
>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={() => setShowCardDeclinedModal(false)}
|
||||
>
|
||||
Update Payment Method
|
||||
</Button>
|
||||
</Link>
|
||||
Update Payment Method
|
||||
</Button>
|
||||
{!isModalIntrusive && (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
@@ -810,6 +880,7 @@ export const Navbar = () => {
|
||||
onClose={() => {
|
||||
setShowSubOrgForm(false);
|
||||
}}
|
||||
handleOrgSelection={handleOrgSelection}
|
||||
/>
|
||||
</div>
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useCreateSubOrganization } from "@app/hooks/api";
|
||||
import { selectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
handleOrgSelection: (params: { organizationId: string }) => void;
|
||||
};
|
||||
|
||||
const AddOrgSchema = z.object({
|
||||
@@ -18,7 +21,8 @@ const AddOrgSchema = z.object({
|
||||
|
||||
type FormData = z.infer<typeof AddOrgSchema>;
|
||||
|
||||
export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
||||
export const NewSubOrganizationForm = ({ onClose, handleOrgSelection }: ContentProps) => {
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const createSubOrg = useCreateSubOrganization();
|
||||
|
||||
const {
|
||||
@@ -32,10 +36,16 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
||||
resolver: zodResolver(AddOrgSchema)
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
|
||||
const onSubmit = async ({ name }: FormData) => {
|
||||
if (isSubOrganization && currentOrg.rootOrgId) {
|
||||
const { token } = await selectOrganization({
|
||||
organizationId: currentOrg.rootOrgId
|
||||
});
|
||||
|
||||
SecurityClient.setToken(token);
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
}
|
||||
|
||||
const { organization } = await createSubOrg.mutateAsync({
|
||||
name
|
||||
});
|
||||
@@ -46,11 +56,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
||||
});
|
||||
onClose();
|
||||
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: organization.id }
|
||||
});
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
await handleOrgSelection({ organizationId: organization.id });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Link, useLocation } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||
import { Tab, TabList, Tabs } from "@app/components/v2";
|
||||
@@ -21,7 +22,14 @@ export const OrgNavBar = ({ isHidden }: Props) => {
|
||||
return (
|
||||
<div className="bg-mineshaft-900">
|
||||
{!isHidden && (
|
||||
<div className="dark flex w-full flex-col overflow-x-hidden border-y border-t-org/15 border-b-org/5 bg-gradient-to-b from-org/[0.075] to-org/[0.025] px-4 pt-0.5">
|
||||
<div
|
||||
className={twMerge(
|
||||
"dark flex w-full flex-col overflow-x-hidden border-y bg-gradient-to-b px-4 pt-0.5",
|
||||
isRootOrganization
|
||||
? "border-t-org/15 border-b-org/5 from-org/[0.075] to-org/[0.025]"
|
||||
: "border-t-sub-org/15 border-b-sub-org/5 from-sub-org/[0.075] to-sub-org/[0.025]"
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
key="menu-org-items"
|
||||
initial={{ x: -150 }}
|
||||
|
||||
@@ -69,6 +69,23 @@ export const PamLayout = () => {
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Sessions</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/pam/$projectId/approvals"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<Tab
|
||||
value={
|
||||
isActive || location.pathname.match(/\/approvals\/|\/i/) ? "selected" : ""
|
||||
}
|
||||
>
|
||||
Approvals
|
||||
</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/pam/$projectId/access-management"
|
||||
params={{
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
|
||||
export const AssumePrivilegeModeBanner = () => {
|
||||
const { isSubOrganization, currentOrg } = useOrganization();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentProject } = useProject();
|
||||
const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege();
|
||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||
@@ -37,7 +37,7 @@ export const AssumePrivilegeModeBanner = () => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`;
|
||||
const url = getProjectHomePage(currentProject.type, currentProject.environments);
|
||||
window.location.assign(
|
||||
url.replace("$orgId", currentOrg.id).replace("$projectId", currentProject.id)
|
||||
);
|
||||
|
||||
@@ -157,20 +157,11 @@ const ProjectSelectInner = () => {
|
||||
params: {
|
||||
projectId: workspace.id,
|
||||
orgId: workspace.orgId
|
||||
},
|
||||
search: {
|
||||
subOrganization: currentOrg?.subOrganization?.name
|
||||
}
|
||||
});
|
||||
const urlInstance = new URL(
|
||||
`${window.location.origin}${url.to.replaceAll("$orgId", url.params.orgId).replaceAll("$projectId", url.params.projectId)}`
|
||||
);
|
||||
if (currentOrg?.subOrganization) {
|
||||
urlInstance.searchParams.set(
|
||||
"subOrganization",
|
||||
currentOrg.subOrganization.name
|
||||
);
|
||||
}
|
||||
window.location.assign(urlInstance);
|
||||
}}
|
||||
icon={
|
||||
|
||||
@@ -5,7 +5,17 @@ import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { queryClient } from "@app/hooks/api/reactQuery";
|
||||
import { userKeys } from "@app/hooks/api/users";
|
||||
|
||||
export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: string) => {
|
||||
type NavigateUserToOrgParams = {
|
||||
navigate: NavigateFn;
|
||||
organizationId?: string;
|
||||
navigateTo?: string;
|
||||
};
|
||||
|
||||
export const navigateUserToOrg = async ({
|
||||
navigate,
|
||||
organizationId,
|
||||
navigateTo
|
||||
}: NavigateUserToOrgParams) => {
|
||||
const userOrgs = await fetchOrganizations();
|
||||
|
||||
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
|
||||
@@ -13,7 +23,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
|
||||
if (organizationId) {
|
||||
localStorage.setItem("orgData.id", organizationId);
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
to: navigateTo || "/organizations/$orgId/projects",
|
||||
params: { orgId: organizationId }
|
||||
});
|
||||
return;
|
||||
@@ -24,7 +34,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
|
||||
const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id;
|
||||
localStorage.setItem("orgData.id", userOrg);
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
to: navigateTo || "/organizations/$orgId/projects",
|
||||
params: { orgId: userOrg }
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -115,7 +115,7 @@ export const PasswordStep = ({
|
||||
return;
|
||||
}
|
||||
|
||||
await navigateUserToOrg(navigate, organizationId);
|
||||
await navigateUserToOrg({ navigate, organizationId });
|
||||
};
|
||||
|
||||
await finishWithOrgWorkflow();
|
||||
@@ -131,7 +131,7 @@ export const PasswordStep = ({
|
||||
}
|
||||
// case: no orgs found, so we navigate the user to create an org
|
||||
else {
|
||||
await navigateUserToOrg(navigate);
|
||||
await navigateUserToOrg({ navigate });
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -233,7 +233,7 @@ export const PasswordStep = ({
|
||||
}
|
||||
// case: no orgs found, so we navigate the user to create an org
|
||||
else {
|
||||
await navigateUserToOrg(navigate);
|
||||
await navigateUserToOrg({ navigate });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -254,7 +254,7 @@ export const PasswordStep = ({
|
||||
|
||||
// case: organization ID is present from the provider auth token -- navigate directly to the org
|
||||
if (organizationId) {
|
||||
await navigateUserToOrg(navigate, organizationId);
|
||||
await navigateUserToOrg({ navigate, organizationId });
|
||||
}
|
||||
// case: no organization ID is present -- navigate to the select org page IF the user has any orgs
|
||||
// if the user has no orgs, navigate to the create org page
|
||||
@@ -264,7 +264,7 @@ export const PasswordStep = ({
|
||||
if (userOrgs.length > 0) {
|
||||
navigateToSelectOrganization(undefined, isAdminLogin);
|
||||
} else {
|
||||
await navigateUserToOrg(navigate);
|
||||
await navigateUserToOrg({ navigate });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +316,7 @@ export const PasswordStep = ({
|
||||
return (
|
||||
<EmailDuplicationConfirmation
|
||||
onRemoveDuplicateLater={() =>
|
||||
navigateUserToOrg(navigate, organizationId).catch(() =>
|
||||
navigateUserToOrg({ navigate, organizationId }).catch(() =>
|
||||
createNotification({ text: "Failed to navigate user", type: "error" })
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export const SelectOrganizationSection = () => {
|
||||
const orgId = queryParams.get("org_id");
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
const isAdminLogin = queryParams.get("is_admin_login") === "true";
|
||||
const mfaPending = queryParams.get("mfa_pending") === "true";
|
||||
const defaultSelectedOrg = organizations.data?.find((org) => org.id === orgId);
|
||||
|
||||
const logout = useLogoutUser(true);
|
||||
@@ -188,7 +189,7 @@ export const SelectOrganizationSection = () => {
|
||||
navigate({ to: "/cli-redirect" });
|
||||
// cli page
|
||||
} else {
|
||||
navigateUserToOrg(navigate, organization.id);
|
||||
navigateUserToOrg({ navigate, organizationId: organization.id });
|
||||
}
|
||||
},
|
||||
[selectOrg]
|
||||
@@ -201,7 +202,7 @@ export const SelectOrganizationSection = () => {
|
||||
const decodedJwt = jwtDecode(authToken) as any;
|
||||
|
||||
if (decodedJwt?.organizationId) {
|
||||
navigateUserToOrg(navigate, decodedJwt.organizationId);
|
||||
navigateUserToOrg({ navigate, organizationId: decodedJwt.organizationId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,10 +237,22 @@ export const SelectOrganizationSection = () => {
|
||||
}, [organizations.isPending, organizations.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mfaPending && defaultSelectedOrg) {
|
||||
const storedMfaToken = sessionStorage.getItem(SessionStorageKeys.MFA_TEMP_TOKEN);
|
||||
if (storedMfaToken) {
|
||||
sessionStorage.removeItem(SessionStorageKeys.MFA_TEMP_TOKEN);
|
||||
SecurityClient.setMfaToken(storedMfaToken);
|
||||
setIsInitialOrgCheckLoading(false);
|
||||
toggleShowMfa.on();
|
||||
setMfaSuccessCallback(() => () => handleSelectOrganization(defaultSelectedOrg));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultSelectedOrg) {
|
||||
handleSelectOrganization(defaultSelectedOrg);
|
||||
}
|
||||
}, [defaultSelectedOrg]);
|
||||
}, [defaultSelectedOrg, mfaPending]);
|
||||
|
||||
if (
|
||||
userLoading ||
|
||||
|
||||
@@ -8,7 +8,8 @@ export const SelectOrganizationPageQueryParams = z.object({
|
||||
org_id: z.string().optional().catch(""),
|
||||
callback_port: z.coerce.number().optional().catch(undefined),
|
||||
is_admin_login: z.boolean().optional().catch(false),
|
||||
force: z.boolean().optional()
|
||||
force: z.boolean().optional(),
|
||||
mfa_pending: z.boolean().optional().catch(false)
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/_restrict-login-signup/login/select-organization")({
|
||||
@@ -16,7 +17,12 @@ export const Route = createFileRoute("/_restrict-login-signup/login/select-organ
|
||||
validateSearch: zodValidator(SelectOrganizationPageQueryParams),
|
||||
search: {
|
||||
middlewares: [
|
||||
stripSearchParams({ org_id: "", callback_port: undefined, is_admin_login: false })
|
||||
stripSearchParams({
|
||||
org_id: "",
|
||||
callback_port: undefined,
|
||||
is_admin_login: false,
|
||||
mfa_pending: false
|
||||
})
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ const tabs = [
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
@@ -34,7 +34,8 @@ export const SettingsPage = () => {
|
||||
}}
|
||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||
>
|
||||
<InfoIcon size={12} /> Looking for organization settings?
|
||||
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
|
||||
settings?
|
||||
</Link>
|
||||
</PageHeader>
|
||||
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
||||
|
||||
@@ -19,7 +19,7 @@ const tabs = [
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
@@ -39,7 +39,8 @@ export const SettingsPage = () => {
|
||||
}}
|
||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||
>
|
||||
<InfoIcon size={12} /> Looking for organization settings?
|
||||
<InfoIcon size={12} /> Looking for {isSubOrganization ? "sub-" : ""}organization
|
||||
settings?
|
||||
</Link>
|
||||
</PageHeader>
|
||||
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
||||
|
||||
@@ -73,6 +73,11 @@ export const Route = createFileRoute("/_authenticate")({
|
||||
});
|
||||
});
|
||||
|
||||
return { organizationId: data.organizationId as string, isAuthenticated: true, user };
|
||||
const isSubOrganization = !!data.subOrganizationId;
|
||||
return {
|
||||
organizationId: isSubOrganization ? data.subOrganizationId : (data.organizationId as string),
|
||||
isAuthenticated: true,
|
||||
user
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, isRedirect, redirect } from "@tanstack/react-router";
|
||||
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import { SessionStorageKeys } from "@app/const";
|
||||
import { authKeys, fetchAuthToken, selectOrganization } from "@app/hooks/api/auth/queries";
|
||||
import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries";
|
||||
import { projectKeys } from "@app/hooks/api/projects";
|
||||
import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
|
||||
import { subOrganizationsQuery } from "@app/hooks/api/subOrganizations";
|
||||
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
|
||||
|
||||
// Route context to fill in organization's data like details, subscription etc
|
||||
@@ -15,6 +20,44 @@ export const Route = createFileRoute("/_authenticate/_inject-org-details")({
|
||||
organizationId = context.organizationId!;
|
||||
}
|
||||
|
||||
if ((params as { orgId?: string })?.orgId && context.organizationId) {
|
||||
const urlOrgId = (params as { orgId: string }).orgId;
|
||||
const currentTokenOrgId = context.organizationId;
|
||||
|
||||
if (urlOrgId !== currentTokenOrgId) {
|
||||
try {
|
||||
const { token, isMfaEnabled } = await selectOrganization({ organizationId: urlOrgId });
|
||||
|
||||
if (isMfaEnabled) {
|
||||
sessionStorage.setItem(SessionStorageKeys.MFA_TEMP_TOKEN, token);
|
||||
throw redirect({
|
||||
to: "/login/select-organization",
|
||||
search: { org_id: urlOrgId, mfa_pending: true }
|
||||
});
|
||||
}
|
||||
|
||||
if (!isMfaEnabled && token) {
|
||||
SecurityClient.setToken(token);
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
|
||||
context.queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||
context.queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
||||
context.queryClient.removeQueries({ queryKey: subOrganizationsQuery.allKey() });
|
||||
|
||||
await context.queryClient.fetchQuery({
|
||||
queryKey: authKeys.getAuthToken,
|
||||
queryFn: fetchAuthToken
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (isRedirect(error)) {
|
||||
throw error;
|
||||
}
|
||||
console.warn("Failed to automatically exchange token for organization:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await context.queryClient.ensureQueryData({
|
||||
queryKey: organizationKeys.getOrgById(organizationId),
|
||||
queryFn: () => fetchOrganizationById(organizationId)
|
||||
|
||||
@@ -117,9 +117,10 @@ export const Route = createFileRoute("/_restrict-login-signup")({
|
||||
return;
|
||||
throw redirect({ to: "/login/select-organization" });
|
||||
}
|
||||
const orgId = data.subOrganizationId || data.organizationId;
|
||||
throw redirect({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: data.organizationId }
|
||||
params: { orgId }
|
||||
});
|
||||
},
|
||||
component: AuthConsentWrapper
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
|
||||
import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan";
|
||||
import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
@@ -84,8 +85,20 @@ export const AccessManagementPage = () => {
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`}
|
||||
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources."
|
||||
/>
|
||||
description={`Manage fine-grained access for users, groups, roles, and machine identities within your ${isSubOrganization ? "sub-" : ""}organization resources.`}
|
||||
>
|
||||
{isSubOrganization && (
|
||||
<Link
|
||||
to="/organizations/$orgId/access-management"
|
||||
params={{
|
||||
orgId: currentOrg.rootOrgId ?? ""
|
||||
}}
|
||||
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||
>
|
||||
<InfoIcon size={12} /> Looking for root organization access control?
|
||||
</Link>
|
||||
)}
|
||||
</PageHeader>
|
||||
{!currentOrg.shouldUseNewPrivilegeSystem && (
|
||||
<div className="mt-4 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
|
||||
<div className="mb-1 flex items-center text-sm">
|
||||
|
||||
@@ -6,7 +6,12 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { DocumentationLinkBadge } from "@app/components/v3";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import {
|
||||
OrgPermissionGroupActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useDeleteGroup } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -15,6 +20,7 @@ import { OrgGroupsTable } from "./OrgGroupsTable";
|
||||
|
||||
export const OrgGroupsSection = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const { isSubOrganization } = useOrganization();
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
@@ -51,7 +57,9 @@ export const OrgGroupsSection = () => {
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Organization Groups</p>
|
||||
<p className="text-xl font-medium text-mineshaft-100">
|
||||
{isSubOrganization ? "Sub-" : ""}Organization Groups
|
||||
</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
|
||||
@@ -63,7 +71,7 @@ export const OrgGroupsSection = () => {
|
||||
onClick={() => handleAddGroupModal()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create Organization Group
|
||||
Create {isSubOrganization ? "Sub-" : ""}Organization Group
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user