Merge remote-tracking branch 'origin/main' into feat/gateway-v2

This commit is contained in:
Sheen Capadngan
2025-09-09 02:14:49 +08:00
188 changed files with 12002 additions and 6900 deletions
@@ -1,207 +0,0 @@
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, FormLabel, IconButton, Input, Spinner } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useCreateAuditLogStream,
useGetAuditLogStreamDetails,
useUpdateAuditLogStream
} from "@app/hooks/api";
type Props = {
id?: string;
onClose: () => void;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const formSchema = z.object({
url: z.string().url().min(1),
headers: z
.object({
key: z.string(),
value: z.string()
})
.array()
.optional()
});
type TForm = z.infer<typeof formSchema>;
export const AuditLogStreamForm = ({ id = "", onClose }: Props) => {
const isEdit = Boolean(id);
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const auditLogStream = useGetAuditLogStreamDetails(id);
const createAuditLogStream = useCreateAuditLogStream();
const updateAuditLogStream = useUpdateAuditLogStream();
const {
handleSubmit,
control,
setValue,
getValues,
formState: { isSubmitting }
} = useForm<TForm>({
values: auditLogStream?.data,
defaultValues: {
headers: [{ key: "", value: "" }]
}
});
const headerFields = useFieldArray({
control,
name: "headers"
});
const handleAuditLogStreamEdit = async ({ headers, url }: TForm) => {
if (!id) return;
try {
await updateAuditLogStream.mutateAsync({
id,
orgId,
headers,
url
});
createNotification({
type: "success",
text: "Successfully updated stream"
});
onClose();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to update stream"
});
}
};
const handleFormSubmit = async ({ headers = [], url }: TForm) => {
if (isSubmitting) return;
const sanitizedHeaders = headers.filter(({ key, value }) => Boolean(key) && Boolean(value));
const streamHeaders = sanitizedHeaders.length ? sanitizedHeaders : undefined;
if (isEdit) {
await handleAuditLogStreamEdit({ headers: streamHeaders, url });
return;
}
try {
await createAuditLogStream.mutateAsync({
orgId,
headers: streamHeaders,
url
});
createNotification({
type: "success",
text: "Successfully created stream"
});
onClose();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: (err as Error)?.message ?? "Failed to create stream"
});
}
};
if (isEdit && auditLogStream.isPending) {
return (
<div className="flex items-center justify-center p-8">
<Spinner size="lg" />
</div>
);
}
return (
<form onSubmit={handleSubmit(handleFormSubmit)} autoComplete="off">
<div>
<Controller
control={control}
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Endpoint URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<FormLabel label="Headers" isOptional />
{headerFields.fields.map(({ id: headerFieldId }, i) => (
<div key={headerFieldId} className="flex space-x-2">
<Controller
control={control}
name={`headers.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-1/3"
>
<Input {...field} placeholder="Authorization" />
</FormControl>
)}
/>
<Controller
control={control}
name={`headers.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="flex-grow"
>
<Input
{...field}
type="password"
placeholder="Bearer <token>"
autoComplete="new-password"
/>
</FormControl>
)}
/>
<IconButton
ariaLabel="delete key"
className="h-9"
variant="outline_bg"
onClick={() => {
const header = getValues("headers");
if (header && header?.length > 1) {
headerFields.remove(i);
} else {
setValue("headers", [{ key: "", value: "" }]);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => headerFields.append({ value: "", key: "" })}
>
Add Key
</Button>
</div>
</div>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isLoading={isSubmitting}>
{isEdit ? "Save" : "Create"}
</Button>
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel
</Button>
</div>
</form>
);
};
@@ -0,0 +1,125 @@
import { createNotification } from "@app/components/notifications";
import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams";
import { useCreateAuditLogStream, useUpdateAuditLogStream } from "@app/hooks/api";
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
import { TAuditLogStream } from "@app/hooks/api/types";
import { DiscriminativePick } from "@app/types";
import { AuditLogStreamHeader } from "../components/AuditLogStreamHeader";
import { CustomProviderAuditLogStreamForm } from "./CustomProviderAuditLogStreamForm";
import { DatadogProviderAuditLogStreamForm } from "./DatadogProviderAuditLogStreamForm";
import { SplunkProviderAuditLogStreamForm } from "./SplunkProviderAuditLogStreamForm";
type FormProps = {
onComplete: (auditLogStream: TAuditLogStream) => void;
};
type CreateFormProps = FormProps & { provider: LogProvider };
type UpdateFormProps = FormProps & {
auditLogStream: TAuditLogStream;
};
const CreateForm = ({ provider, onComplete }: CreateFormProps) => {
const createAuditLogStream = useCreateAuditLogStream();
const { name: providerName } = AUDIT_LOG_STREAM_PROVIDER_MAP[provider];
const onSubmit = async (
formData: DiscriminativePick<TAuditLogStream, "provider" | "credentials">
) => {
try {
const logStream = await createAuditLogStream.mutateAsync(formData);
createNotification({
text: `Successfully created ${providerName} Log Stream`,
type: "success"
});
onComplete(logStream);
} catch (err: any) {
console.error(err);
createNotification({
title: `Failed to create ${providerName} Log Stream`,
text: err.message,
type: "error"
});
}
};
switch (provider) {
case LogProvider.Custom:
return <CustomProviderAuditLogStreamForm onSubmit={onSubmit} />;
case LogProvider.Datadog:
return <DatadogProviderAuditLogStreamForm onSubmit={onSubmit} />;
case LogProvider.Splunk:
return <SplunkProviderAuditLogStreamForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled Provider: ${provider}`);
}
};
const UpdateForm = ({ auditLogStream, onComplete }: UpdateFormProps) => {
const updateAuditLogStream = useUpdateAuditLogStream();
const { name: providerName } = AUDIT_LOG_STREAM_PROVIDER_MAP[auditLogStream.provider];
const onSubmit = async (
formData: DiscriminativePick<TAuditLogStream, "provider" | "credentials">
) => {
try {
const connection = await updateAuditLogStream.mutateAsync({
auditLogStreamId: auditLogStream.id,
...formData
});
createNotification({
text: `Successfully updated ${providerName} Log Stream`,
type: "success"
});
onComplete(connection);
} catch (err: any) {
console.error(err);
createNotification({
title: `Failed to update ${providerName} Log Stream`,
text: err.message,
type: "error"
});
}
};
switch (auditLogStream.provider) {
case LogProvider.Custom:
return (
<CustomProviderAuditLogStreamForm onSubmit={onSubmit} auditLogStream={auditLogStream} />
);
case LogProvider.Datadog:
return (
<DatadogProviderAuditLogStreamForm onSubmit={onSubmit} auditLogStream={auditLogStream} />
);
case LogProvider.Splunk:
return (
<SplunkProviderAuditLogStreamForm onSubmit={onSubmit} auditLogStream={auditLogStream} />
);
default:
throw new Error(`Unhandled Provider: ${(auditLogStream as TAuditLogStream).provider}`);
}
};
type Props = { onBack?: () => void } & Pick<FormProps, "onComplete"> &
(
| { provider: LogProvider; auditLogStream?: undefined }
| { provider?: undefined; auditLogStream: TAuditLogStream }
);
export const AuditLogStreamForm = ({ onBack, ...props }: Props) => {
const { provider, auditLogStream } = props;
return (
<div>
<AuditLogStreamHeader
logStreamExists={Boolean(auditLogStream)}
provider={auditLogStream ? auditLogStream.provider : provider}
onBack={onBack}
/>
{auditLogStream ? (
<UpdateForm {...props} auditLogStream={auditLogStream} />
) : (
<CreateForm {...props} provider={provider} />
)}
</div>
);
};
@@ -0,0 +1,176 @@
import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, FormLabel, IconButton, Input, ModalClose } from "@app/components/v2";
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
import { TCustomProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/custom-provider";
type Props = {
auditLogStream?: TCustomProviderLogStream;
onSubmit: (formData: FormData) => void;
};
const formSchema = z.object({
provider: z.literal(LogProvider.Custom),
credentials: z.object({
url: z.string().url().trim().min(1).max(255),
headers: z
.object({
key: z.string().min(1),
value: z.string().min(1)
})
.array()
})
});
type FormData = z.infer<typeof formSchema>;
export const CustomProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => {
const isUpdate = Boolean(auditLogStream);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: auditLogStream ?? {
provider: LogProvider.Custom
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty },
getValues,
setValue
} = form;
const headerFields = useFieldArray({
control,
name: "credentials.headers"
});
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="credentials.url"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Endpoint URL"
>
<Input {...field} placeholder="https://example.com" />
</FormControl>
)}
/>
<FormLabel label="Headers" isOptional />
{headerFields.fields.map(({ id: headerFieldId }, i) => (
<div key={headerFieldId} className="flex space-x-2">
<Controller
control={control}
name={`credentials.headers.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-1/3"
>
<Input {...field} placeholder="Authorization" />
</FormControl>
)}
/>
<Controller
control={control}
name={`credentials.headers.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="flex-grow"
>
<Input
{...field}
type="password"
placeholder="Bearer <token>"
autoComplete="new-password"
onFocus={(e) => {
if (
auditLogStream &&
auditLogStream.credentials.headers[i] &&
auditLogStream.credentials.headers[i].value === "******" &&
field.value === "******"
) {
field.onChange("");
}
e.target.type = "text";
}}
onBlur={(e) => {
if (
auditLogStream &&
auditLogStream.credentials.headers[i] &&
auditLogStream.credentials.headers[i].value === "******" &&
field.value === ""
) {
field.onChange("******");
}
e.target.type = "password";
}}
/>
</FormControl>
)}
/>
<IconButton
ariaLabel="delete key"
className="h-9"
variant="outline_bg"
onClick={() => {
const header = getValues("credentials.headers");
if (header && header?.length > 1) {
headerFields.remove(i);
} else {
setValue("credentials.headers", [{ key: "", value: "" }]);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => headerFields.append({ value: "", key: "" })}
>
Add Key
</Button>
</div>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Create Log Stream"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,132 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
import { TDatadogProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/datadog-provider";
type Props = {
auditLogStream?: TDatadogProviderLogStream;
onSubmit: (formData: FormData) => void;
};
const formSchema = z.object({
provider: z.literal(LogProvider.Datadog),
credentials: z.object({
url: z.string().url().trim().min(1).max(255),
token: z
.string()
.trim()
.regex(/^[a-fA-F0-9]{32}$/, "Invalid Datadog API key format")
})
});
type FormData = z.infer<typeof formSchema>;
const DATADOG_ENDPOINTS = {
"Datadog US1": "https://http-intake.logs.datadoghq.com/api/v2/logs",
"Datadog US3": "https://http-intake.logs.us3.datadoghq.com/api/v2/logs",
"Datadog US5": "https://http-intake.logs.us5.datadoghq.com/api/v2/logs",
"Datadog EU": "https://http-intake.logs.datadoghq.eu/api/v2/logs",
"Datadog AP1": "https://http-intake.logs.ap1.datadoghq.com/api/v2/logs",
"Datadog AP2": "https://http-intake.logs.ap2.datadoghq.com/api/v2/logs",
"Datadog GovCloud (US1-FED)": "https://http-intake.logs.ddog-gov.com/api/v2/logs"
};
export const DatadogProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => {
const isUpdate = Boolean(auditLogStream);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: auditLogStream ?? {
provider: LogProvider.Datadog,
credentials: {
url: DATADOG_ENDPOINTS["Datadog US1"]
}
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="credentials.url"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Datadog Region"
helperText={value}
>
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.entries(DATADOG_ENDPOINTS).map(([k, v]) => {
return (
<SelectItem value={v} key={k}>
{k}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.token"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Datadog Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Create Log Stream"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,120 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, SecretInput } from "@app/components/v2";
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
import { TSplunkProviderLogStream } from "@app/hooks/api/auditLogStreams/types/providers/splunk-provider";
type Props = {
auditLogStream?: TSplunkProviderLogStream;
onSubmit: (formData: FormData) => void;
};
const formSchema = z.object({
provider: z.literal(LogProvider.Splunk),
credentials: z.object({
hostname: z
.string()
.trim()
.min(1)
.max(255)
.superRefine((val, ctx) => {
if (val.includes("://")) {
ctx.addIssue({
code: "custom",
message: "Hostname should not include protocol"
});
return;
}
try {
const url = new URL(`https://${val}`);
if (url.hostname !== val) {
ctx.addIssue({
code: "custom",
message: "Must be a valid hostname without port or path"
});
}
} catch {
ctx.addIssue({ code: "custom", message: "Invalid hostname" });
}
}),
token: z.string().uuid().trim().min(1)
})
});
type FormData = z.infer<typeof formSchema>;
export const SplunkProviderAuditLogStreamForm = ({ auditLogStream, onSubmit }: Props) => {
const isUpdate = Boolean(auditLogStream);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: auditLogStream ?? {
provider: LogProvider.Splunk
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="credentials.hostname"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Hostname"
>
<Input {...field} placeholder="splunk.example.com" />
</FormControl>
)}
/>
<Controller
name="credentials.token"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Splunk Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Create Log Stream"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -1,72 +1,24 @@
import { faPlug, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
THead,
Tr
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useDeleteAuditLogStream, useGetAuditLogStreams } from "@app/hooks/api";
import { AuditLogStreamForm } from "./AuditLogStreamForm";
import { AuditLogStreamTable } from "./components/AuditLogStreamTable";
import { AddAuditLogStreamModal } from "./components";
export const AuditLogStreamsTab = withPermission(
() => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"auditLogStreamForm",
"deleteAuditLogStream",
"upgradePlan"
] as const);
const { subscription } = useSubscription();
const { data: auditLogStreams, isPending: isAuditLogStreamsLoading } =
useGetAuditLogStreams(orgId);
// mutation
const { mutateAsync: deleteAuditLogStream } = useDeleteAuditLogStream();
const handleAuditLogStreamDelete = async () => {
try {
const auditLogStreamId = popUp?.deleteAuditLogStream?.data as string;
await deleteAuditLogStream({
id: auditLogStreamId,
orgId
});
handlePopUpClose("deleteAuditLogStream");
createNotification({
type: "success",
text: "Successfully deleted stream"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete stream"
});
}
};
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"auditLogStreamForm",
"upgradePlan"
] as const);
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
@@ -84,8 +36,10 @@ export const AuditLogStreamsTab = withPermission(
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
variant="outline_bg"
colorSchema="secondary"
>
Create
Add Log Stream
</Button>
)}
</OrgPermissionCan>
@@ -93,102 +47,15 @@ export const AuditLogStreamsTab = withPermission(
<p className="mb-8 text-gray-400">
Send audit logs from Infisical to external logging providers via HTTP
</p>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Td>URL</Td>
<Td className="text-right">Action</Td>
</Tr>
</THead>
<TBody>
{isAuditLogStreamsLoading && (
<TableSkeleton columns={2} innerKey="stream-loading" />
)}
{!isAuditLogStreamsLoading && auditLogStreams && auditLogStreams?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No audit log streams found" icon={faPlug} />
</Td>
</Tr>
)}
{!isAuditLogStreamsLoading &&
auditLogStreams?.map(({ id, url }) => (
<Tr key={id}>
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{url}
</Td>
<Td>
<div className="flex items-center justify-end space-x-2">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<Button
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("auditLogStreamForm", id)}
>
Edit
</Button>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<Button
variant="outline_bg"
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
colorSchema="danger"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("deleteAuditLogStream", id)}
>
Delete
</Button>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</div>
<Modal
<AuditLogStreamTable />
<AddAuditLogStreamModal
isOpen={popUp.auditLogStreamForm.isOpen}
onOpenChange={(isModalOpen) => {
handlePopUpToggle("auditLogStreamForm", isModalOpen);
}}
>
<ModalContent
title={`${popUp?.auditLogStreamForm?.data ? "Update" : "Create"} Audit Log Stream `}
subTitle="Continuously stream logs from Infisical to third-party logging providers."
>
<AuditLogStreamForm
id={popUp?.auditLogStreamForm?.data as string}
onClose={() => handlePopUpToggle("auditLogStreamForm")}
/>
</ModalContent>
</Modal>
onOpenChange={(isOpen) => handlePopUpToggle("auditLogStreamForm", isOpen)}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add audit log streams if you switch to Infisical's Enterprise plan."
/>
<DeleteActionModal
isOpen={popUp.deleteAuditLogStream.isOpen}
deleteKey="delete"
title="Are you sure you want to remove this stream?"
onChange={(isOpen) => handlePopUpToggle("deleteAuditLogStream", isOpen)}
onClose={() => handlePopUpClose("deleteAuditLogStream")}
onDeleteApproved={handleAuditLogStreamDelete}
text="You can add audit log streams if you switch to Infisical's Enterprise plan."
/>
</div>
);
@@ -0,0 +1,69 @@
import { Dispatch, SetStateAction, useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
import { TAuditLogStream } from "@app/hooks/api/types";
import { AuditLogStreamForm } from "../AuditLogStreamForm/AuditLogStreamForm";
import { LogStreamProviderSelect } from "./LogStreamProviderSelect";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
onComplete: (auditLogStream: TAuditLogStream) => void;
selectedProvider: LogProvider | null;
setSelectedProvider: Dispatch<SetStateAction<LogProvider | null>>;
};
const Content = ({ onComplete, selectedProvider, setSelectedProvider }: ContentProps) => {
if (selectedProvider) {
return (
<AuditLogStreamForm
onComplete={onComplete}
onBack={() => setSelectedProvider(null)}
provider={selectedProvider}
/>
);
}
return <LogStreamProviderSelect onSelect={setSelectedProvider} />;
};
export const AddAuditLogStreamModal = ({ isOpen, onOpenChange }: Props) => {
const [selectedProvider, setSelectedProvider] = useState<LogProvider | null>(null);
return (
<Modal
isOpen={isOpen}
onOpenChange={(e) => {
onOpenChange(e);
if (!e) setSelectedProvider(null);
}}
>
<ModalContent
className="max-w-2xl"
title="Log Provider"
subTitle=<>
Select a log provider or{" "}
<button
type="button"
className="underline"
onClick={() => setSelectedProvider(LogProvider.Custom)}
>
input a custom URL
</button>{" "}
to stream logs to.
</>
>
<Content
onComplete={() => onOpenChange(false)}
selectedProvider={selectedProvider}
setSelectedProvider={setSelectedProvider}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,69 @@
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams";
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
type Props = {
provider: LogProvider;
logStreamExists: boolean;
onBack?: () => void;
};
export const AuditLogStreamHeader = ({ provider, logStreamExists, onBack }: Props) => {
const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider];
return (
<div className="mb-4 flex w-full items-center gap-2 border-b border-mineshaft-500 pb-4">
<div className="relative">
{providerDetails.image ? (
<img
alt={providerDetails.name}
src={`/images/integrations/${providerDetails.image}`}
className="size-12 rounded-md bg-bunker-500 p-2"
/>
) : (
providerDetails.icon && (
<div className="size-12 rounded-md bg-bunker-500 p-2">
<FontAwesomeIcon
icon={providerDetails.icon}
className="h-full w-full text-mineshaft-300"
/>
</div>
)
)}
</div>
<div>
<div className="mb-1 flex items-center text-mineshaft-300">
{providerDetails.name}
<a
href="https://infisical.com/docs/documentation/platform/audit-log-streams/audit-log-streams#example-providers"
target="_blank"
className="ml-1"
rel="noopener noreferrer"
>
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mb-px mr-1 text-xs" />
<span>Docs</span>
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="mb-px ml-1 text-[10px]" />
</div>
</a>
</div>
<p className="text-sm leading-4 text-mineshaft-400">
{logStreamExists
? `${providerDetails.name} Log Stream`
: `Create a ${providerDetails.name} Log Stream`}
</p>
</div>
{onBack && (
<button
type="button"
className="ml-auto mt-1 text-xs text-mineshaft-400 underline underline-offset-2 hover:text-mineshaft-300"
onClick={onBack}
>
Select another provider
</button>
)}
</div>
);
};
@@ -0,0 +1,111 @@
import { faAsterisk, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionSubjects } from "@app/context";
import { OrgPermissionActions } from "@app/context/OrgPermissionContext/types";
import { AUDIT_LOG_STREAM_PROVIDER_MAP, getProviderUrl } from "@app/helpers/auditLogStreams";
import { TAuditLogStream } from "@app/hooks/api/types";
type Props = {
logStream: TAuditLogStream;
onDelete: (logStream: TAuditLogStream) => void;
onEditCredentials: (logStream: TAuditLogStream) => void;
};
export const AuditLogStreamRow = ({ logStream, onDelete, onEditCredentials }: Props) => {
const { id, provider } = logStream;
const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider];
const url = getProviderUrl(logStream);
return (
<Tr
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700")}
key={`log-stream-${id}`}
>
<Td>
<div className="flex items-center gap-2">
<div className="relative">
{providerDetails.image ? (
<img
alt={providerDetails.name}
src={`/images/integrations/${providerDetails.image}`}
className="size-5"
/>
) : (
providerDetails.icon && (
<FontAwesomeIcon
icon={providerDetails.icon}
className="size-5 text-mineshaft-300"
/>
)
)}
</div>
<span className="hidden lg:inline">{providerDetails.name}</span>
</div>
</Td>
<Td className="!min-w-[8rem] max-w-0">
<div className="flex w-full items-center">
<p className="truncate">{url}</p>
</div>
</Td>
<Td>
<div className="flex items-center justify-end gap-2">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faAsterisk} />}
onClick={() => onEditCredentials(logStream)}
>
Edit Credentials
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(logStream)}
>
Delete Stream
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</div>
</Td>
</Tr>
);
};
@@ -0,0 +1,304 @@
import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faFilter,
faMagnifyingGlass,
faPlug,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { AUDIT_LOG_STREAM_PROVIDER_MAP, getProviderUrl } from "@app/helpers/auditLogStreams";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useListAuditLogStreams } from "@app/hooks/api";
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TAuditLogStream } from "@app/hooks/api/types";
import { AuditLogStreamRow } from "./AuditLogStreamRow";
import { DeleteAuditLogStreamModal } from "./DeleteAuditLogStreamModal";
import { EditAuditLogStreamCredentialsModal } from "./EditAuditLogStreamCredentialsModal";
enum LogStreamsOrderBy {
Provider = "provider",
Url = "url"
}
type LogStreamFilters = {
providers: LogProvider[];
};
export const AuditLogStreamTable = () => {
const { isPending, data: logStreams = [] } = useListAuditLogStreams();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"delete",
"editCredentials"
] as const);
const [filters, setFilters] = useState<LogStreamFilters>({
providers: []
});
const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
toggleOrderDirection,
orderBy,
setOrderDirection,
setOrderBy
} = usePagination<LogStreamsOrderBy>(LogStreamsOrderBy.Provider, {
initPerPage: getUserTablePreference("logStreamsTable", PreferenceKey.PerPage, 20)
});
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("logStreamsTable", PreferenceKey.PerPage, newPerPage);
};
const filteredLogStreams = useMemo(
() =>
logStreams
.filter((stream) => {
const { provider } = stream;
if (filters.providers.length && !filters.providers.includes(provider)) return false;
const searchValue = search.trim().toLowerCase();
return AUDIT_LOG_STREAM_PROVIDER_MAP[provider].name.toLowerCase().includes(searchValue);
})
.sort((a, b) => {
const [one, two] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
switch (orderBy) {
case LogStreamsOrderBy.Url:
return getProviderUrl(one)
.toLowerCase()
.localeCompare(getProviderUrl(two).toLowerCase());
case LogStreamsOrderBy.Provider:
default:
return AUDIT_LOG_STREAM_PROVIDER_MAP[one.provider].name
.toLowerCase()
.localeCompare(AUDIT_LOG_STREAM_PROVIDER_MAP[two.provider].name.toLowerCase());
}
}),
[logStreams, orderDirection, search, orderBy, filters]
);
useResetPageHelper({
totalCount: filteredLogStreams.length,
offset,
setPage
});
const handleSort = (column: LogStreamsOrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
const getClassName = (col: LogStreamsOrderBy) =>
twMerge("ml-2", orderBy === col ? "" : "opacity-30");
const getColSortIcon = (col: LogStreamsOrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
const isTableFiltered = Boolean(filters.providers.length);
const handleDelete = (logStream: TAuditLogStream) => handlePopUpOpen("delete", logStream);
const handleEditCredentials = (logStream: TAuditLogStream) => {
handlePopUpOpen("editCredentials", logStream);
};
return (
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search audit log streams..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter Log Streams"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuLabel>Filter by Provider</DropdownMenuLabel>
{logStreams.length ? (
[...new Set(logStreams.map(({ provider }) => provider))].map((provider) => {
const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider];
return (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
providers: prev.providers.includes(provider)
? prev.providers.filter((a) => a !== provider)
: [...prev.providers, provider]
}));
}}
key={provider}
iconPos="right"
>
<div className="flex items-center gap-2">
{providerDetails.image ? (
<img
alt={providerDetails.name}
src={`/images/integrations/${providerDetails.image}`}
className="h-4 w-4"
/>
) : (
providerDetails.icon && (
<FontAwesomeIcon
icon={providerDetails.icon}
className="size-4 text-mineshaft-300"
/>
)
)}
<span>{providerDetails.name}</span>
</div>
</DropdownMenuItem>
);
})
) : (
<DropdownMenuItem isDisabled>No Providers Configured</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/4">
<div className="flex items-center">
Provider
<IconButton
variant="plain"
className={getClassName(LogStreamsOrderBy.Provider)}
ariaLabel="sort"
onClick={() => handleSort(LogStreamsOrderBy.Provider)}
>
<FontAwesomeIcon icon={getColSortIcon(LogStreamsOrderBy.Provider)} />
</IconButton>
</div>
</Th>
<Th className="w-1/3">
<div className="flex items-center">
Endpoint URL
<IconButton
variant="plain"
className={getClassName(LogStreamsOrderBy.Url)}
ariaLabel="sort"
onClick={() => handleSort(LogStreamsOrderBy.Url)}
>
<FontAwesomeIcon icon={getColSortIcon(LogStreamsOrderBy.Url)} />
</IconButton>
</div>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && (
<TableSkeleton
innerKey="audit-log-streams-table"
columns={3}
key="audit-log-streams"
/>
)}
{filteredLogStreams.slice(offset, perPage * page).map((stream) => (
<AuditLogStreamRow
logStream={stream}
key={stream.id}
onDelete={handleDelete}
onEditCredentials={handleEditCredentials}
/>
))}
</TBody>
</Table>
{Boolean(filteredLogStreams.length) && (
<Pagination
count={filteredLogStreams.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{!isPending && !filteredLogStreams?.length && (
<EmptyState
title={
logStreams.length
? "No log streams match search..."
: "No log streams have been configured"
}
icon={logStreams.length ? faSearch : faPlug}
/>
)}
</TableContainer>
<DeleteAuditLogStreamModal
isOpen={popUp.delete.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("delete", isOpen)}
auditLogStream={popUp.delete.data}
/>
<EditAuditLogStreamCredentialsModal
isOpen={popUp.editCredentials.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editCredentials", isOpen)}
auditLogStream={popUp.editCredentials.data}
/>
</div>
);
};
@@ -0,0 +1,54 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams";
import { useDeleteAuditLogStream } from "@app/hooks/api";
import { TAuditLogStream } from "@app/hooks/api/types";
type Props = {
auditLogStream?: TAuditLogStream;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const DeleteAuditLogStreamModal = ({ isOpen, onOpenChange, auditLogStream }: Props) => {
const deleteAuditLogStream = useDeleteAuditLogStream();
if (!auditLogStream) return null;
const { id: auditLogStreamId, provider } = auditLogStream;
const providerDetails = AUDIT_LOG_STREAM_PROVIDER_MAP[provider];
const handleDelete = async () => {
try {
await deleteAuditLogStream.mutateAsync({
auditLogStreamId,
provider
});
createNotification({
text: `Successfully deleted ${providerDetails.name} stream`,
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
createNotification({
text: `Failed to delete ${providerDetails.name} stream`,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
onChange={onOpenChange}
title="Are you sure you want to delete this log stream?"
deleteKey="delete"
onDeleteApproved={handleDelete}
/>
);
};
@@ -0,0 +1,34 @@
import { Modal, ModalContent } from "@app/components/v2";
import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams";
import { TAuditLogStream } from "@app/hooks/api/types";
import { AuditLogStreamForm } from "../AuditLogStreamForm/AuditLogStreamForm";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
auditLogStream?: TAuditLogStream;
};
export const EditAuditLogStreamCredentialsModal = ({
isOpen,
onOpenChange,
auditLogStream
}: Props) => {
if (!auditLogStream) return null;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
className="max-w-2xl"
title="Edit Log Stream Credentials"
subTitle={`Update the credentials for this ${AUDIT_LOG_STREAM_PROVIDER_MAP[auditLogStream.provider].name} Log Stream.`}
>
<AuditLogStreamForm
onComplete={() => onOpenChange(false)}
auditLogStream={auditLogStream}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,155 @@
import { useMemo } from "react";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, Spinner } from "@app/components/v2";
import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetAuditLogStreamOptions } from "@app/hooks/api";
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
type Props = {
onSelect: (provider: LogProvider) => void;
};
// TODO: When we have more than 1 page of providers, uncomment the search components
export const LogStreamProviderSelect = ({ onSelect }: Props) => {
const { isPending, data: logStreamOptions } = useGetAuditLogStreamOptions();
const { search, setPage, page, perPage, offset } = usePagination("", {
initPerPage: 16
});
const filteredOptions = useMemo(
() =>
(logStreamOptions || [])
.filter(
({ name, provider }) =>
name.toLowerCase().includes(search.trim().toLowerCase()) ||
provider.toLowerCase().includes(search.trim().toLowerCase())
)
.sort((a, b) => {
if (a.provider === LogProvider.Custom) return 1;
if (b.provider === LogProvider.Custom) return -1;
return 0;
}),
[logStreamOptions, search]
);
useResetPageHelper({
totalCount: filteredOptions.length,
offset,
setPage
});
if (isPending) {
return (
<div className="flex h-full flex-col items-center justify-center py-2.5">
<Spinner size="lg" className="text-mineshaft-500" />
<p className="mt-4 text-sm text-mineshaft-400">Loading options...</p>
</div>
);
}
return (
<div className="flex flex-col gap-4">
{/* <Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search options..."
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
/> */}
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
{filteredOptions.slice(offset, perPage * page)?.map((option) => {
const { image, icon, name, size = 50 } = AUDIT_LOG_STREAM_PROVIDER_MAP[option.provider];
return (
<button
type="button"
onClick={() => onSelect(option.provider)}
className={`group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 ${option.provider === LogProvider.Custom ? "bg-mineshaft-700/30 hover:bg-mineshaft-600/30" : "bg-mineshaft-700 hover:bg-mineshaft-600"} p-4 duration-200`}
>
{image && (
<div className="relative">
<img
src={`/images/integrations/${image}`}
style={{
width: `${size}px`
}}
className="mt-auto"
alt={`${name} logo`}
/>
</div>
)}
{icon && (
<FontAwesomeIcon className="mt-auto size-10 text-mineshaft-300" icon={icon} />
)}
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
{name}
</div>
</button>
);
})}
{!filteredOptions.length && (
<EmptyState
className="col-span-full mt-40"
title="No providers match search"
icon={faSearch}
/>
)}
</div>
{/* {Boolean(filteredOptions.length) && (
<Pagination
startAdornment={
<Tooltip
side="bottom"
className="max-w-sm py-4"
content={
<>
<p className="mb-2">Infisical is constantly adding support for more providers.</p>
<p>
{`If you don't see the third-party
provider you're looking for,`}{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://infisical.com/slack"
rel="noopener noreferrer"
>
let us know on Slack
</a>{" "}
or{" "}
<a
target="_blank"
className="underline hover:text-mineshaft-300"
href="https://github.com/Infisical/infisical/discussions"
rel="noopener noreferrer"
>
make a request on GitHub
</a>
.
</p>
</>
}
>
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
<span className="text-xs">
Don&#39;t see the third-party provider you&#39;re looking for?
</span>
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
</div>
</Tooltip>
}
count={filteredOptions.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
perPageList={[16]}
/>
)} */}
</div>
);
};
@@ -0,0 +1 @@
export * from "./AddAuditLogStreamModal";
@@ -15,7 +15,9 @@ import {
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { gatewaysQueryKeys } from "@app/hooks/api";
import { useHasCustomMigrationAvailable } from "@app/hooks/api/migration";
import { useImportVault } from "@app/hooks/api/migration/mutations";
import { ExternalMigrationProviders } from "@app/hooks/api/migration/types";
type Props = {
id?: string;
@@ -24,11 +26,13 @@ type Props = {
enum VaultMappingType {
KeyVault = "key-vault",
Namespace = "namespace"
Namespace = "namespace",
Custom = "custom"
}
const MAPPING_TYPE_MENU_ITEMS = [
{
isCustom: false,
value: VaultMappingType.KeyVault,
label: "Key Vaults",
tooltip: (
@@ -48,6 +52,7 @@ const MAPPING_TYPE_MENU_ITEMS = [
)
},
{
isCustom: false,
value: VaultMappingType.Namespace,
label: "Namespaces",
tooltip: (
@@ -63,10 +68,25 @@ const MAPPING_TYPE_MENU_ITEMS = [
</div>
</div>
)
},
{
isCustom: true,
value: VaultMappingType.Custom,
label: "Custom Migration",
tooltip: (
<div>
Custom migrations allow you to shape your Vault migration to your specific needs. Please
contact our sales team to get started with custom migrations.
</div>
)
}
];
export const VaultPlatformModal = ({ onClose }: Props) => {
const { data: isCustomMigrationAvailable } = useHasCustomMigrationAvailable(
ExternalMigrationProviders.Vault
);
const formSchema = z.object({
vaultUrl: z.string().min(1),
gatewayId: z.string().optional(),
@@ -230,31 +250,44 @@ export const VaultPlatformModal = ({ onClose }: Props) => {
errorText={error?.message}
className="flex-1"
>
<div className="mt-2 grid h-full w-full grid-cols-2 gap-4">
<div className="mt-2 grid grid-cols-2 gap-4">
{MAPPING_TYPE_MENU_ITEMS.map((el) => (
<div
key={el.value}
className={twMerge(
"flex w-full cursor-pointer flex-col items-center gap-2 rounded border border-mineshaft-600 p-4 opacity-75 transition-all",
field.value === el.value
? "border-primary-700 border-opacity-70 bg-mineshaft-600 opacity-100"
: "hover:border-primary-700 hover:bg-mineshaft-600"
? "border-primary-700 border-opacity-70 bg-mineshaft-700 opacity-100"
: "hover:border-primary-800/75 hover:bg-mineshaft-600",
el.isCustom && "col-span-2",
el.isCustom &&
!isCustomMigrationAvailable?.data?.enabled &&
"!cursor-not-allowed !border-mineshaft-600 !bg-mineshaft-600 !opacity-40"
)}
onClick={() => field.onChange(el.value)}
onClick={() => {
if (el.isCustom && !isCustomMigrationAvailable?.data?.enabled) return;
field.onChange(el.value);
}}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
if (el.isCustom && !isCustomMigrationAvailable?.data?.enabled) return;
field.onChange(el.value);
}}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter") {
field.onChange(el.value);
}
}}
>
<div className="flex items-center gap-1">
<div className="text-center text-sm">{el.label}</div>
{el.tooltip && (
<div className="text-center text-sm">
<Tooltip content={el.tooltip} className="max-w-96">
<FontAwesomeIcon className="opacity-60" icon={faQuestionCircle} />
<FontAwesomeIcon
size="sm"
className="text-mineshaft-400"
icon={faQuestionCircle}
/>
</Tooltip>
</div>
)}
@@ -272,7 +305,7 @@ export const VaultPlatformModal = ({ onClose }: Props) => {
isLoading={isLoading}
isDisabled={!isDirty || isSubmitting || isLoading || !isValid}
>
Import data
Import Data
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
@@ -204,9 +204,10 @@ export const OrgGeneralAuthSection = ({
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Enforce users to authenticate via Google to access this organization.
Enforce users to authenticate via Google OAuth SSO to access this organization.
<br />
When this is enabled your organization members will only be able to login with Google.
When this is enabled your organization members will only be able to login with Google
SSO (not Google SAML).
</p>
</div>
</div>