mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 12:38:36 +00:00
Merge remote-tracking branch 'origin/main' into misc/privilege-management-v2-transition
This commit is contained in:
@@ -34,6 +34,7 @@ import { EncryptionPanel } from "./components/EncryptionPanel";
|
||||
import { IntegrationPanel } from "./components/IntegrationPanel";
|
||||
import { RateLimitPanel } from "./components/RateLimitPanel";
|
||||
import { UserPanel } from "./components/UserPanel";
|
||||
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
||||
|
||||
enum TabSections {
|
||||
Settings = "settings",
|
||||
@@ -42,6 +43,7 @@ enum TabSections {
|
||||
RateLimit = "rate-limit",
|
||||
Integrations = "integrations",
|
||||
Users = "users",
|
||||
Identities = "identities",
|
||||
Kmip = "kmip"
|
||||
}
|
||||
|
||||
@@ -164,6 +166,7 @@ export const OverviewPage = () => {
|
||||
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
|
||||
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
||||
<Tab value={TabSections.Users}>Users</Tab>
|
||||
<Tab value={TabSections.Identities}>Identities</Tab>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Settings}>
|
||||
@@ -409,6 +412,9 @@ export const OverviewPage = () => {
|
||||
<TabPanel value={TabSections.Users}>
|
||||
<UserPanel />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Identities}>
|
||||
<IdentityPanel />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useState } from "react";
|
||||
import { faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
Input,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useAdminGetIdentities } from "@app/hooks/api/admin/queries";
|
||||
|
||||
const IdentityPanelTable = () => {
|
||||
const [searchIdentityFilter, setSearchIdentityFilter] = useState("");
|
||||
const [debouncedSearchTerm] = useDebounce(searchIdentityFilter, 500);
|
||||
|
||||
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetIdentities(
|
||||
{
|
||||
limit: 20,
|
||||
searchTerm: debouncedSearchTerm
|
||||
}
|
||||
);
|
||||
|
||||
const isEmpty = !isPending && !data?.pages?.[0].length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={searchIdentityFilter}
|
||||
onChange={(e) => setSearchIdentityFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search identities by name..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={2} innerKey="identities" />}
|
||||
{!isPending &&
|
||||
data?.pages?.map((identities) =>
|
||||
identities.map(({ name, id }) => (
|
||||
<Tr key={`identity-${id}`} className="w-full">
|
||||
<Td>{name}</Td>
|
||||
</Tr>
|
||||
))
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && isEmpty && <EmptyState title="No identities found" icon={faServer} />}
|
||||
</TableContainer>
|
||||
{!isEmpty && (
|
||||
<Button
|
||||
className="mt-4 py-3 text-sm"
|
||||
isFullWidth
|
||||
variant="star"
|
||||
isLoading={isFetchingNextPage}
|
||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||
onClick={() => fetchNextPage()}
|
||||
>
|
||||
{hasNextPage ? "Load More" : "End of list"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const IdentityPanel = () => (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||
</div>
|
||||
<IdentityPanelTable />
|
||||
</div>
|
||||
);
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const deleteUserUpgradePlanMessage = "Deleting users via Admin UI";
|
||||
const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions";
|
||||
|
||||
const UserPanelTable = ({
|
||||
@@ -61,12 +60,12 @@ const UserPanelTable = ({
|
||||
const [adminsOnly, setAdminsOnly] = useState(false);
|
||||
const { user } = useUser();
|
||||
const userId = user?.id || "";
|
||||
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
|
||||
limit: 20,
|
||||
searchTerm: debounedSearchTerm,
|
||||
searchTerm: debouncedSearchTerm,
|
||||
adminsOnly
|
||||
});
|
||||
|
||||
@@ -156,14 +155,6 @@ const UserPanelTable = ({
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!subscription?.instanceUserManagement) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
username,
|
||||
id,
|
||||
message: deleteUserUpgradePlanMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
handlePopUpOpen("removeUser", { username, id });
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -28,7 +28,14 @@ type Props = {
|
||||
|
||||
const AUDIT_LOG_LIMIT = 15;
|
||||
|
||||
const TABLE_HEADERS = ["Timestamp (MM/DD/YYYY)", "Event", "Project", "Actor", "Source", "Metadata"] as const;
|
||||
const TABLE_HEADERS = [
|
||||
"Timestamp (MM/DD/YYYY)",
|
||||
"Event",
|
||||
"Project",
|
||||
"Actor",
|
||||
"Source",
|
||||
"Metadata"
|
||||
] as const;
|
||||
export type TAuditLogTableHeader = (typeof TABLE_HEADERS)[number];
|
||||
|
||||
export const LogsTable = ({
|
||||
|
||||
+2
-1
@@ -14,9 +14,10 @@ import {
|
||||
} from "@app/context";
|
||||
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||
import { useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
||||
import { GenericResourceNameSchema } from "@app/lib/schemas";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().max(64, "Too long, maximum length is 64 characters"),
|
||||
name: GenericResourceNameSchema,
|
||||
slug: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens"),
|
||||
|
||||
+5
-2
@@ -45,8 +45,11 @@ export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }:
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<p className="mt-2 text-gray-300">Conditions</p>
|
||||
<p className="mb-2 text-sm text-mineshaft-400">
|
||||
When this policy should apply (always if no conditions are added).
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||
</p>
|
||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||
All conditions must evaluate to true for the policy to take effect.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
{items.fields.map((el, index) => {
|
||||
|
||||
+5
-2
@@ -39,8 +39,11 @@ export const IdentityManagementPermissionConditions = ({ position = 0, isDisable
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<p className="mt-2 text-gray-300">Conditions</p>
|
||||
<p className="mb-2 text-sm text-mineshaft-400">
|
||||
When this policy should apply (always if no conditions are added).
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||
</p>
|
||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||
All conditions must evaluate to true for the policy to take effect.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
{items.fields.map((el, index) => {
|
||||
|
||||
+107
-87
@@ -1,10 +1,13 @@
|
||||
import { useMemo } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability";
|
||||
import { faPlus, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { AccessTree } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@@ -13,6 +16,8 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
|
||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||
|
||||
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||
@@ -115,94 +120,109 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const permissions = form.watch("permissions");
|
||||
|
||||
const formattedPermissions = useMemo(
|
||||
() =>
|
||||
evaluatePermissionsAbility(
|
||||
formRolePermission2API(permissions) as RawRuleOf<
|
||||
MongoAbility<ProjectPermissionSet, MongoQuery>
|
||||
>[]
|
||||
),
|
||||
[JSON.stringify(permissions)]
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isCustomRole && (
|
||||
<>
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
New policy
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
|
||||
{Object.keys(PROJECT_PERMISSION_OBJECT)
|
||||
.sort((a, b) =>
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
a as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
b as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title.toLowerCase()
|
||||
)
|
||||
)
|
||||
.map((subject) => (
|
||||
<DropdownMenuItem
|
||||
key={`permission-create-${subject}`}
|
||||
className="py-3"
|
||||
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
|
||||
>
|
||||
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="w-full">
|
||||
<AccessTree permissions={formattedPermissions} />
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isCustomRole && (
|
||||
<>
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
className="h-10 rounded-l-none"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
New policy
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
|
||||
{Object.keys(PROJECT_PERMISSION_OBJECT)
|
||||
.sort((a, b) =>
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
a as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title
|
||||
.toLowerCase()
|
||||
.localeCompare(
|
||||
PROJECT_PERMISSION_OBJECT[
|
||||
b as keyof typeof PROJECT_PERMISSION_OBJECT
|
||||
].title.toLowerCase()
|
||||
)
|
||||
)
|
||||
.map((subject) => (
|
||||
<DropdownMenuItem
|
||||
key={`permission-create-${subject}`}
|
||||
className="py-3"
|
||||
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
|
||||
>
|
||||
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
{!isPending && <PermissionEmptyState />}
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
<div className="py-4">
|
||||
{!isPending && <PermissionEmptyState />}
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-2
@@ -43,8 +43,11 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<p className="mt-2 text-gray-300">Conditions</p>
|
||||
<p className="mb-2 text-sm text-mineshaft-400">
|
||||
When this policy should apply (always if no conditions are added).
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||
</p>
|
||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||
All conditions must evaluate to true for the policy to take effect.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
{items.fields.map((el, index) => {
|
||||
|
||||
@@ -120,7 +120,14 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="API Key" type="text" autoComplete="off" autoCorrect="off" spellCheck="false" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="API Key"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@@ -155,7 +162,16 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
errorText={error?.message}
|
||||
isOptional
|
||||
>
|
||||
<Input {...field} placeholder="Password" type="password" autoComplete="new-password" autoCorrect="off" spellCheck="false" aria-autocomplete="none" data-form-type="other" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
aria-autocomplete="none"
|
||||
data-form-type="other"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -228,7 +228,8 @@ export const OverviewPage = () => {
|
||||
setPage
|
||||
});
|
||||
|
||||
const { folderNamesAndDescriptions, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders);
|
||||
const { folderNamesAndDescriptions, getFolderByNameAndEnv, isFolderPresentInEnv } =
|
||||
useFolderOverview(folders);
|
||||
|
||||
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
|
||||
useDynamicSecretOverview(dynamicSecrets);
|
||||
@@ -251,7 +252,7 @@ export const OverviewPage = () => {
|
||||
"updateFolder"
|
||||
] as const);
|
||||
|
||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||
const promises = userAvailableEnvs.map((env) => {
|
||||
const environment = env.slug;
|
||||
return createFolder({
|
||||
@@ -1029,7 +1030,7 @@ export const OverviewPage = () => {
|
||||
)}
|
||||
{!isOverviewLoading && visibleEnvs.length > 0 && (
|
||||
<>
|
||||
{folderNamesAndDescriptions.map(({name: folderName, description}, index) => (
|
||||
{folderNamesAndDescriptions.map(({ name: folderName, description }, index) => (
|
||||
<SecretOverviewFolderRow
|
||||
folderName={folderName}
|
||||
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||
@@ -1161,7 +1162,9 @@ export const OverviewPage = () => {
|
||||
<FolderForm
|
||||
isEdit
|
||||
defaultFolderName={(popUp.updateFolder?.data as Pick<TSecretFolder, "name">)?.name}
|
||||
defaultDescription={(popUp.updateFolder?.data as Pick<TSecretFolder, "description">)?.description}
|
||||
defaultDescription={
|
||||
(popUp.updateFolder?.data as Pick<TSecretFolder, "description">)?.description
|
||||
}
|
||||
onUpdateFolder={handleFolderUpdate}
|
||||
showDescriptionOverwriteWarning
|
||||
/>
|
||||
|
||||
+1
-1
@@ -16,10 +16,10 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import { useCreateFolder, useCreateSecretV3, useCreateWsTag, useGetWsTags } from "@app/hooks/api";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
const typeSchema = z
|
||||
.object({
|
||||
|
||||
+204
-5
@@ -23,6 +23,27 @@ import { useWorkspace } from "@app/context";
|
||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const passwordRequirementsSchema = z
|
||||
.object({
|
||||
length: z.number().min(1).max(250),
|
||||
required: z
|
||||
.object({
|
||||
lowercase: z.number().min(0),
|
||||
uppercase: z.number().min(0),
|
||||
digits: z.number().min(0),
|
||||
symbols: z.number().min(0)
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||
return total <= 250;
|
||||
}, "Sum of required characters cannot exceed 250"),
|
||||
allowedSymbols: z.string().optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length");
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
client: z.nativeEnum(SqlProviders),
|
||||
@@ -31,6 +52,7 @@ const formSchema = z.object({
|
||||
database: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
passwordRequirements: passwordRequirementsSchema.optional(),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
@@ -133,11 +155,24 @@ export const SqlDatabaseInputForm = ({
|
||||
control,
|
||||
setValue,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
handleSubmit,
|
||||
watch
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: getSqlStatements(SqlProviders.Postgres)
|
||||
provider: {
|
||||
...getSqlStatements(SqlProviders.Postgres),
|
||||
passwordRequirements: {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -174,6 +209,10 @@ export const SqlDatabaseInputForm = ({
|
||||
setValue("provider.renewStatement", sqlStatment.renewStatement);
|
||||
setValue("provider.revocationStatement", sqlStatment.revocationStatement);
|
||||
setValue("provider.port", getDefaultPort(type));
|
||||
|
||||
// Update password requirements based on provider
|
||||
const length = type === SqlProviders.Oracle ? 30 : 48;
|
||||
setValue("provider.passwordRequirements.length", length);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -197,6 +236,7 @@ export const SqlDatabaseInputForm = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -386,10 +426,15 @@ export const SqlDatabaseInputForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<Accordion type="multiple" className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advanced">
|
||||
<AccordionTrigger>
|
||||
Creation, Revocation & Renew Statements (optional)
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="mb-4 text-sm text-mineshaft-300">
|
||||
Customize SQL statements for managing database user lifecycle
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
@@ -450,6 +495,160 @@ export const SqlDatabaseInputForm = ({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="password-config">
|
||||
<AccordionTrigger>Password Configuration (optional)</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="mb-4 text-sm text-mineshaft-300">
|
||||
Set constraints on the generated database password
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.length"
|
||||
defaultValue={48}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password Length"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={250}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Minimum Required Character Counts</h4>
|
||||
<div className="text-sm text-gray-500">
|
||||
{(() => {
|
||||
const total = Object.values(
|
||||
watch("provider.passwordRequirements.required") || {}
|
||||
).reduce((sum, count) => sum + Number(count || 0), 0);
|
||||
const length = watch("provider.passwordRequirements.length") || 0;
|
||||
const isError = total > length;
|
||||
return (
|
||||
<span className={isError ? "text-red-500" : ""}>
|
||||
Total required characters: {total}{" "}
|
||||
{isError ? `(exceeds length of ${length})` : ""}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.required.lowercase"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Lowercase Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of lowercase letters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.required.uppercase"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Uppercase Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of uppercase letters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.required.digits"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Digit Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of digits"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.required.symbols"
|
||||
defaultValue={0}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Symbol Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of symbols"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Allowed Symbols</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.passwordRequirements.allowedSymbols"
|
||||
defaultValue="-_.~!*"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Symbols to use in password"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Default: -_.~!*"
|
||||
>
|
||||
<Input {...field} placeholder="-_.~!*" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+30
-29
@@ -15,7 +15,8 @@ type Props = {
|
||||
showDescriptionOverwriteWarning?: boolean;
|
||||
};
|
||||
|
||||
const descriptionOverwriteWarningMessage = "Warning: Any changes made here will overwrite any custom edits in individual environment folders."
|
||||
const descriptionOverwriteWarningMessage =
|
||||
"Warning: Any changes made here will overwrite any custom edits in individual environment folders.";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
@@ -25,9 +26,7 @@ const formSchema = z.object({
|
||||
/^[a-zA-Z0-9-_]+$/,
|
||||
"Folder name can only contain letters, numbers, dashes, and underscores"
|
||||
),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
description: z.string().optional()
|
||||
});
|
||||
type TFormData = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -59,7 +58,7 @@ export const FolderForm = ({
|
||||
if (textarea) {
|
||||
const lines = textarea.value.split("\n");
|
||||
const maxDescriptionLines = 10;
|
||||
|
||||
|
||||
if (lines.length > maxDescriptionLines) {
|
||||
textarea.value = lines.slice(0, maxDescriptionLines).join("\n");
|
||||
}
|
||||
@@ -90,30 +89,32 @@ export const FolderForm = ({
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Folder Description"
|
||||
isError={Boolean(error)}
|
||||
tooltipText={showDescriptionOverwriteWarning ? descriptionOverwriteWarningMessage : undefined}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Folder description"
|
||||
{...field}
|
||||
rows={3}
|
||||
ref={descriptionRef}
|
||||
onInput={handleInput}
|
||||
className="thin-scrollbar w-full !resize-none bg-mineshaft-900"
|
||||
maxLength={255}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
control={control}
|
||||
name="description"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Folder Description"
|
||||
isError={Boolean(error)}
|
||||
tooltipText={
|
||||
showDescriptionOverwriteWarning ? descriptionOverwriteWarningMessage : undefined
|
||||
}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Folder description"
|
||||
{...field}
|
||||
rows={3}
|
||||
ref={descriptionRef}
|
||||
onInput={handleInput}
|
||||
className="thin-scrollbar w-full !resize-none bg-mineshaft-900"
|
||||
maxLength={255}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||
{isEdit ? "Save" : "Create"}
|
||||
|
||||
+202
-4
@@ -23,6 +23,27 @@ import { useWorkspace } from "@app/context";
|
||||
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const passwordRequirementsSchema = z
|
||||
.object({
|
||||
length: z.number().min(1).max(250),
|
||||
required: z
|
||||
.object({
|
||||
lowercase: z.number().min(0),
|
||||
uppercase: z.number().min(0),
|
||||
digits: z.number().min(0),
|
||||
symbols: z.number().min(0)
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||
return total <= 250; // Sanity check for individual validation
|
||||
}, "Sum of required characters cannot exceed 250"),
|
||||
allowedSymbols: z.string().optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length");
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
.object({
|
||||
@@ -32,6 +53,7 @@ const formSchema = z.object({
|
||||
database: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
passwordRequirements: passwordRequirementsSchema.optional(),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
@@ -82,6 +104,17 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const getDefaultPasswordRequirements = (provider: SqlProviders) => ({
|
||||
length: provider === SqlProviders.Oracle ? 30 : 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
@@ -94,10 +127,16 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
...(dynamicSecret.inputs as TForm["inputs"]),
|
||||
passwordRequirements:
|
||||
(dynamicSecret.inputs as TForm["inputs"])?.passwordRequirements ||
|
||||
getDefaultPasswordRequirements(
|
||||
(dynamicSecret.inputs as TForm["inputs"])?.client || SqlProviders.Postgres
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
|
||||
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
|
||||
@@ -347,10 +386,15 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="multiple" className="w-full bg-mineshaft-700">
|
||||
<AccordionItem value="modify-sql-statement">
|
||||
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advanced">
|
||||
<AccordionTrigger>
|
||||
Creation, Revocation & Renew Statements (optional)
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="mb-4 text-sm text-mineshaft-300">
|
||||
Customize SQL statements for managing database user lifecycle
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
@@ -418,6 +462,160 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Accordion type="multiple" className="mb-2 mt-4 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="password-config">
|
||||
<AccordionTrigger>Password Configuration (optional)</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="mb-4 text-sm text-mineshaft-300">
|
||||
Set constraints on the generated database password
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.length"
|
||||
defaultValue={48}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password Length"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={250}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Minimum Required Character Counts</h4>
|
||||
<div className="text-sm text-gray-500">
|
||||
{(() => {
|
||||
const total = Object.values(
|
||||
watch("inputs.passwordRequirements.required") || {}
|
||||
).reduce((sum, count) => sum + Number(count || 0), 0);
|
||||
const length = watch("inputs.passwordRequirements.length") || 0;
|
||||
const isError = total > length;
|
||||
return (
|
||||
<span className={isError ? "text-red-500" : ""}>
|
||||
Total required characters: {total}{" "}
|
||||
{isError ? `(exceeds length of ${length})` : ""}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.required.lowercase"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Lowercase Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of lowercase letters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.required.uppercase"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Uppercase Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of uppercase letters"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.required.digits"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Digit Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of digits"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.required.symbols"
|
||||
defaultValue={0}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Symbol Count"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Minimum number of symbols"
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Allowed Symbols</h4>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.passwordRequirements.allowedSymbols"
|
||||
defaultValue="-_.~!*"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Symbols to use in password"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
helperText="Default: -_.~!*"
|
||||
>
|
||||
<Input {...field} placeholder="-_.~!*" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+6
-7
@@ -1,17 +1,17 @@
|
||||
import { subject } from "@casl/ability";
|
||||
import { faClose, faFolder, faPencilSquare, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClose, faFolder, faInfoCircle, faPencilSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, IconButton, Modal, ModalContent } from "@app/components/v2";
|
||||
import { Tooltip } from "@app/components/v2/Tooltip/Tooltip";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||
import { Tooltip } from "@app/components/v2/Tooltip/Tooltip";
|
||||
|
||||
import { FolderForm } from "../ActionBar/FolderForm";
|
||||
|
||||
@@ -118,16 +118,15 @@ export const FolderListView = ({
|
||||
onClick={() => handleFolderClick(name)}
|
||||
>
|
||||
{name}
|
||||
{
|
||||
description &&
|
||||
{description && (
|
||||
<Tooltip
|
||||
position="right"
|
||||
className="flex items-center space-x-4 max-w-lg py-4 whitespace-pre-wrap"
|
||||
className="flex max-w-lg items-center space-x-4 whitespace-pre-wrap py-4"
|
||||
content={description}
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400 ml-1" />
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-1 text-mineshaft-400" />
|
||||
</Tooltip>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export const PitDrawer = ({
|
||||
<div>
|
||||
{(() => {
|
||||
const distance = formatDistance(new Date(createdAt), new Date());
|
||||
return distance.charAt(0).toUpperCase() + distance.slice(1) + " ago";
|
||||
return `${distance.charAt(0).toUpperCase() + distance.slice(1)} ago`;
|
||||
})()}
|
||||
</div>
|
||||
<div>{getButtonLabel(i === 0 && index === 0, snapshotId === id)}</div>
|
||||
|
||||
+1
-1
@@ -58,10 +58,10 @@ import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
|
||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { camelCaseToSpaces } from "@app/lib/fn/string";
|
||||
|
||||
import { CreateReminderForm } from "./CreateReminderForm";
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
import { camelCaseToSpaces } from "@app/lib/fn/string";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
|
||||
+9
-9
@@ -281,7 +281,7 @@ export const SecretItem = memo(
|
||||
isVisible={isVisible}
|
||||
isReadOnly={isReadOnly}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
containerClassName="py-1.5 rounded-md transition-all"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -301,19 +301,19 @@ export const SecretItem = memo(
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
defaultValue={secretValueHidden ? "" : undefined}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
containerClassName="py-1.5 rounded-md transition-all"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all">
|
||||
<div key="actions" className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2">
|
||||
<Tooltip content="Copy secret">
|
||||
<IconButton
|
||||
isDisabled={secret.secretValueHidden}
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
@@ -339,7 +339,7 @@ export const SecretItem = memo(
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<IconButton
|
||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="reference-tree"
|
||||
@@ -390,7 +390,7 @@ export const SecretItem = memo(
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-5",
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5 data-[state=open]:w-5",
|
||||
hasTagsApplied && "w-5 text-primary"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
@@ -473,7 +473,7 @@ export const SecretItem = memo(
|
||||
size="sm"
|
||||
onClick={handleOverrideClick}
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5",
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5",
|
||||
isOverriden && "w-5 text-primary"
|
||||
)}
|
||||
>
|
||||
@@ -498,7 +498,7 @@ export const SecretItem = memo(
|
||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5",
|
||||
hasComment && "w-5 text-primary"
|
||||
)}
|
||||
variant="plain"
|
||||
@@ -518,7 +518,7 @@ export const SecretItem = memo(
|
||||
</ProjectPermissionCan>
|
||||
<IconButton
|
||||
isDisabled={secret.secretValueHidden}
|
||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="share-secret"
|
||||
|
||||
+7
-2
@@ -206,8 +206,13 @@ export const DatabricksConfigurePage = () => {
|
||||
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||
</div>
|
||||
<span className="mt-4 text-sm text-mineshaft-300">
|
||||
After creating an integration, your secrets will start syncing immediately. This might
|
||||
cause an unexpected override of current secrets in Databricks with secrets from Infisical.
|
||||
When integrating with Databricks, Infisical is intended to be the source of truth for the
|
||||
secrets in the configured Databricks scope.
|
||||
</span>
|
||||
<span className="mt-4 text-sm text-mineshaft-300">
|
||||
Any secrets not present in Infisical will be removed from the specified scope. To prevent
|
||||
removal of secrets not managed by Infisical, Infisical recommends creating a designated
|
||||
secret scope for your integration.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -13,7 +14,6 @@ import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { useUser } from "@app/context";
|
||||
import { useResetUserPasswordV2, useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
|
||||
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
type Errors = {
|
||||
tooShort?: string;
|
||||
|
||||
Reference in New Issue
Block a user