mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
chore: refactor frontend and UI improvements
This commit is contained in:
36
frontend/src/components/features/FormLabelToolTip.tsx
Normal file
36
frontend/src/components/features/FormLabelToolTip.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { FormLabel, Tooltip } from "../v2";
|
||||||
|
|
||||||
|
// To give users example of possible values of TTL
|
||||||
|
export const FormLabelToolTip = ({ label, linkToMore, content }: { label: string, linkToMore: string, content: string }) => (
|
||||||
|
<div>
|
||||||
|
<FormLabel
|
||||||
|
label={label}
|
||||||
|
icon={
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<span>
|
||||||
|
{content}{" "}
|
||||||
|
<a
|
||||||
|
href={linkToMore}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary-700"
|
||||||
|
>
|
||||||
|
More
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faQuestionCircle}
|
||||||
|
size="sm"
|
||||||
|
className="relative bottom-1 right-1"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -1,36 +1,12 @@
|
|||||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
import { FormLabelToolTip } from "./FormLabelToolTip";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { FormLabel, Tooltip } from "../v2";
|
|
||||||
|
|
||||||
// To give users example of possible values of TTL
|
// To give users example of possible values of TTL
|
||||||
export const TtlFormLabel = ({ label }: { label: string }) => (
|
export const TtlFormLabel = ({ label }: { label: string }) => (
|
||||||
<div>
|
<div>
|
||||||
<FormLabel
|
<FormLabelToolTip
|
||||||
label={label}
|
label={label}
|
||||||
icon={
|
content="1m, 2h, 3d. "
|
||||||
<Tooltip
|
linkToMore="https://github.com/vercel/ms?tab=readme-ov-file#examples"
|
||||||
content={
|
|
||||||
<span>
|
|
||||||
1m, 2h, 3d.{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/vercel/ms?tab=readme-ov-file#examples"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary-700"
|
|
||||||
>
|
|
||||||
More
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faQuestionCircle}
|
|
||||||
size="sm"
|
|
||||||
className="relative bottom-1 right-1"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
import { faCheckCircle, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import ListBoxMultiple from "@app/components/basic/ListboxMultiple";
|
|
||||||
import { TtlFormLabel } from "@app/components/features";
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
|
import { FormLabelToolTip } from "@app/components/features/FormLabelToolTip";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -15,11 +15,19 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/v2/Dropdown/Dropdown";
|
||||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||||
import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries";
|
import { useGetDynamicSecretProviderData } from "@app/hooks/api/dynamicSecret/queries";
|
||||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
|
import { AzureEntraIdSetup } from "./AzureEntraIdSetup";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
|
selectedUsers: z.array(z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
name: z.string().min(1),
|
||||||
|
email: z.string().min(1),
|
||||||
|
})),
|
||||||
provider: z.object({
|
provider: z.object({
|
||||||
tenantId: z.string().min(1),
|
tenantId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
@@ -55,12 +63,6 @@ type Props = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EntraIDUser = {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AzureEntraIdInputForm = ({
|
export const AzureEntraIdInputForm = ({
|
||||||
onCompleted,
|
onCompleted,
|
||||||
onCancel,
|
onCancel,
|
||||||
@@ -77,25 +79,11 @@ export const AzureEntraIdInputForm = ({
|
|||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
const tenantId = watch("provider.tenantId");
|
const tenantId = watch("provider.tenantId");
|
||||||
const [selectedUsers, setSelectedUsers] = useState<EntraIDUser[]>([]);
|
const [onSetup, setOnSetup] = useState(true);
|
||||||
const { data, isLoading, isFetched, isError, isFetching } = useGetDynamicSecretProviderData({ dataFetchType: "Users", provider: {type: DynamicSecretProviders.AzureEntraId, inputs: { userId : "unused", email: "unused", tenantId }}, enabled: !!tenantId });
|
const { data, isLoading, isFetched, isError, isFetching } = useGetDynamicSecretProviderData({ dataFetchType: "Users", provider: { type: DynamicSecretProviders.AzureEntraId, inputs: { userId: "unused", email: "unused", tenantId } }, enabled: !!tenantId });
|
||||||
const mappedUserList = data?.map((user) => ({
|
|
||||||
primaryText: user.name,
|
|
||||||
secondaryText: user.email,
|
|
||||||
id: user.id,
|
|
||||||
}));
|
|
||||||
const handleChange = (selected: { primaryText: string; secondaryText: string; id: string }[]) => {
|
|
||||||
// Map back to EntraIDUser based on the id or other attributes
|
|
||||||
const selectedMappedUsers = data?.filter(user =>
|
|
||||||
selected.some(selectedItem => selectedItem.id === user.id)
|
|
||||||
);
|
|
||||||
if(selectedMappedUsers){
|
|
||||||
setSelectedUsers(selectedMappedUsers);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const createDynamicSecret = useCreateDynamicSecret();
|
const createDynamicSecret = useCreateDynamicSecret();
|
||||||
|
|
||||||
const handleCreateDynamicSecret = async ({ name, provider, maxTTL, defaultTTL }: TForm) => {
|
const handleCreateDynamicSecret = async ({ name, selectedUsers, provider, maxTTL, defaultTTL }: TForm) => {
|
||||||
// wait till previous request is finished
|
// wait till previous request is finished
|
||||||
if (createDynamicSecret.isLoading) return;
|
if (createDynamicSecret.isLoading) return;
|
||||||
try {
|
try {
|
||||||
@@ -121,7 +109,12 @@ export const AzureEntraIdInputForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
{onSetup && <AzureEntraIdSetup
|
||||||
|
onCompleted={() => { setOnSetup(false); }}
|
||||||
|
onCancel={onCancel}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{!onSetup && <form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
@@ -192,7 +185,7 @@ export const AzureEntraIdInputForm = ({
|
|||||||
<Input {...field} placeholder="Tenant Id from Azure Entra ID App installation" />
|
<Input {...field} placeholder="Tenant Id from Azure Entra ID App installation" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,15 +195,55 @@ export const AzureEntraIdInputForm = ({
|
|||||||
Select Users
|
Select Users
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-4">
|
||||||
{
|
{
|
||||||
tenantId && !isError && !isFetching && isFetched && data && mappedUserList &&
|
tenantId && !isError && !isFetching && isFetched && data &&
|
||||||
<ListBoxMultiple
|
<Controller
|
||||||
isSelected={mappedUserList.filter(user =>
|
control={control}
|
||||||
selectedUsers.some(selectedUser => selectedUser.id === user.id)
|
name="selectedUsers"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<FormLabelToolTip content="We create a secret for each user" label="Select Users" linkToMore=""/>}
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="w-72">
|
||||||
|
<Input
|
||||||
|
isReadOnly
|
||||||
|
value={value?.length ? `${value.length} selected` : "None"}
|
||||||
|
className="text-left"
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start"
|
||||||
|
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
||||||
|
>
|
||||||
|
{data.map((user) => {
|
||||||
|
const ids = value?.map((selectedUser) => selectedUser.id)
|
||||||
|
const isChecked = ids?.includes(user.id);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
onChange(
|
||||||
|
isChecked
|
||||||
|
? value?.filter((el) => el.id !== user.id)
|
||||||
|
: [...(value || []), user]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
key={`create-policy-members-${user.id}`}
|
||||||
|
iconPos="right"
|
||||||
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
>
|
||||||
|
{user.name} <br /> {`(${user.email})`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
onChange={handleChange}
|
|
||||||
data={mappedUserList}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -230,11 +263,15 @@ export const AzureEntraIdInputForm = ({
|
|||||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isLoading || isError}>
|
<Button type="submit" isLoading={isSubmitting} isDisabled={isLoading || isError}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={() => { setOnSetup(true); }}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
<Button variant="outline_bg" onClick={onCancel}>
|
<Button variant="outline_bg" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { DiRedis } from "react-icons/di";
|
import { DiRedis } from "react-icons/di";
|
||||||
import { SiApachecassandra, SiElasticsearch, SiMongodb, SiRabbitmq } from "react-icons/si";
|
import { SiApachecassandra, SiElasticsearch, SiMicrosoftazure, SiMongodb, SiRabbitmq } from "react-icons/si";
|
||||||
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
import { faAws } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
|
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -12,7 +12,6 @@ import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
|||||||
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
|
import { AwsElastiCacheInputForm } from "./AwsElastiCacheInputForm";
|
||||||
import { AwsIamInputForm } from "./AwsIamInputForm";
|
import { AwsIamInputForm } from "./AwsIamInputForm";
|
||||||
import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm";
|
import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm";
|
||||||
import { AzureEntraIdSetup } from "./AzureEntraIdSetup";
|
|
||||||
import { CassandraInputForm } from "./CassandraInputForm";
|
import { CassandraInputForm } from "./CassandraInputForm";
|
||||||
import { ElasticSearchInputForm } from "./ElasticSearchInputForm";
|
import { ElasticSearchInputForm } from "./ElasticSearchInputForm";
|
||||||
import { MongoAtlasInputForm } from "./MongoAtlasInputForm";
|
import { MongoAtlasInputForm } from "./MongoAtlasInputForm";
|
||||||
@@ -31,7 +30,6 @@ type Props = {
|
|||||||
|
|
||||||
enum WizardSteps {
|
enum WizardSteps {
|
||||||
SelectProvider = "select-provider",
|
SelectProvider = "select-provider",
|
||||||
ProviderSetup = "provider-setup",
|
|
||||||
ProviderInputs = "provider-inputs"
|
ProviderInputs = "provider-inputs"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,10 +80,9 @@ const DYNAMIC_SECRET_LIST = [
|
|||||||
title: "RabbitMQ"
|
title: "RabbitMQ"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <SiRabbitmq size="1.5rem" />,
|
icon: <SiMicrosoftazure size="1.5rem" />,
|
||||||
provider: DynamicSecretProviders.AzureEntraId,
|
provider: DynamicSecretProviders.AzureEntraId,
|
||||||
title: "Azure Entra ID",
|
title: "Azure Entra ID",
|
||||||
hasSetupStep: true
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -123,7 +120,7 @@ export const CreateDynamicSecretForm = ({
|
|||||||
>
|
>
|
||||||
<div className="mb-4 text-mineshaft-300">Select a service to connect to:</div>
|
<div className="mb-4 text-mineshaft-300">Select a service to connect to:</div>
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
{DYNAMIC_SECRET_LIST.map(({ icon, provider, title, hasSetupStep }) => (
|
{DYNAMIC_SECRET_LIST.map(({ icon, provider, title }) => (
|
||||||
<div
|
<div
|
||||||
key={`dynamic-secret-provider-${provider}`}
|
key={`dynamic-secret-provider-${provider}`}
|
||||||
className="flex h-32 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
className="flex h-32 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||||
@@ -131,22 +128,13 @@ export const CreateDynamicSecretForm = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedProvider(provider);
|
setSelectedProvider(provider);
|
||||||
if(hasSetupStep){
|
setWizardStep(WizardSteps.ProviderInputs);
|
||||||
setWizardStep(WizardSteps.ProviderSetup);
|
|
||||||
} else {
|
|
||||||
setWizardStep(WizardSteps.ProviderInputs);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
if (evt.key === "Enter") {
|
if (evt.key === "Enter") {
|
||||||
setSelectedProvider(provider);
|
setSelectedProvider(provider);
|
||||||
setWizardStep(WizardSteps.ProviderInputs);
|
setWizardStep(WizardSteps.ProviderInputs);
|
||||||
}
|
}
|
||||||
if(hasSetupStep){
|
|
||||||
setWizardStep(WizardSteps.ProviderSetup);
|
|
||||||
} else {
|
|
||||||
setWizardStep(WizardSteps.ProviderInputs);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
@@ -156,23 +144,6 @@ export const CreateDynamicSecretForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
{
|
|
||||||
wizardStep === WizardSteps.ProviderSetup && selectedProvider === DynamicSecretProviders.AzureEntraId
|
|
||||||
&& (
|
|
||||||
<motion.div
|
|
||||||
key="dynamic-azure-entra-id-setup"
|
|
||||||
transition={{ duration: 0.1 }}
|
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
|
||||||
exit={{ opacity: 0, translateX: -30 }}
|
|
||||||
>
|
|
||||||
<AzureEntraIdSetup
|
|
||||||
onCompleted={() => { setWizardStep(WizardSteps.ProviderInputs) }}
|
|
||||||
onCancel={handleFormReset}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{wizardStep === WizardSteps.ProviderInputs &&
|
{wizardStep === WizardSteps.ProviderInputs &&
|
||||||
selectedProvider === DynamicSecretProviders.SqlDatabase && (
|
selectedProvider === DynamicSecretProviders.SqlDatabase && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
Reference in New Issue
Block a user