mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(audit-log-stream): frontend rework + some backend changes
This commit is contained in:
@@ -1,15 +1,18 @@
|
||||
import { LogProvider } from "@app/ee/services/audit-log-stream/audit-log-stream-enums";
|
||||
import {
|
||||
CustomProviderSchema,
|
||||
SanitizedCustomProviderSchema
|
||||
CreateCustomProviderLogStreamSchema,
|
||||
SanitizedCustomProviderSchema,
|
||||
UpdateCustomProviderLogStreamSchema
|
||||
} from "@app/ee/services/audit-log-stream/custom/custom-provider-schemas";
|
||||
import {
|
||||
DatadogProviderSchema,
|
||||
SanitizedDatadogProviderSchema
|
||||
CreateDatadogProviderLogStreamSchema,
|
||||
SanitizedDatadogProviderSchema,
|
||||
UpdateDatadogProviderLogStreamSchema
|
||||
} from "@app/ee/services/audit-log-stream/datadog/datadog-provider-schemas";
|
||||
import {
|
||||
CreateSplunkProviderLogStreamSchema,
|
||||
SanitizedSplunkProviderSchema,
|
||||
SplunkProviderSchema
|
||||
UpdateSplunkProviderLogStreamSchema
|
||||
} from "@app/ee/services/audit-log-stream/splunk/splunk-provider-schemas";
|
||||
|
||||
import { registerAuditLogStreamEndpoints } from "./audit-log-stream-endpoints";
|
||||
@@ -23,8 +26,8 @@ export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record<LogProvider, (server:
|
||||
server,
|
||||
provider: LogProvider.Custom,
|
||||
sanitizedResponseSchema: SanitizedCustomProviderSchema,
|
||||
createSchema: CustomProviderSchema,
|
||||
updateSchema: CustomProviderSchema
|
||||
createSchema: CreateCustomProviderLogStreamSchema,
|
||||
updateSchema: UpdateCustomProviderLogStreamSchema
|
||||
});
|
||||
},
|
||||
[LogProvider.Datadog]: async (server: FastifyZodProvider) => {
|
||||
@@ -32,8 +35,8 @@ export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record<LogProvider, (server:
|
||||
server,
|
||||
provider: LogProvider.Datadog,
|
||||
sanitizedResponseSchema: SanitizedDatadogProviderSchema,
|
||||
createSchema: DatadogProviderSchema,
|
||||
updateSchema: DatadogProviderSchema
|
||||
createSchema: CreateDatadogProviderLogStreamSchema,
|
||||
updateSchema: UpdateDatadogProviderLogStreamSchema
|
||||
});
|
||||
},
|
||||
[LogProvider.Splunk]: async (server: FastifyZodProvider) => {
|
||||
@@ -41,8 +44,8 @@ export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record<LogProvider, (server:
|
||||
server,
|
||||
provider: LogProvider.Splunk,
|
||||
sanitizedResponseSchema: SanitizedSplunkProviderSchema,
|
||||
createSchema: SplunkProviderSchema,
|
||||
updateSchema: SplunkProviderSchema
|
||||
createSchema: CreateSplunkProviderLogStreamSchema,
|
||||
updateSchema: UpdateSplunkProviderLogStreamSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getDatadogProviderListItem } from "./datadog/datadog-provider-fns";
|
||||
import { getSplunkProviderListItem } from "./splunk/splunk-provider-fns";
|
||||
|
||||
export const listProviderOptions = () => {
|
||||
return [getCustomProviderListItem(), getDatadogProviderListItem(), getSplunkProviderListItem()].sort((a, b) =>
|
||||
return [getDatadogProviderListItem(), getSplunkProviderListItem(), getCustomProviderListItem()].sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AuditLogStreamsSchema } from "@app/db/schemas";
|
||||
|
||||
export const BaseProviderSchema = AuditLogStreamsSchema.omit({
|
||||
encryptedCredentials: true,
|
||||
provider: true,
|
||||
|
||||
// Old "archived" values
|
||||
encryptedHeadersAlgorithm: true,
|
||||
encryptedHeadersCiphertext: true,
|
||||
encryptedHeadersIV: true,
|
||||
encryptedHeadersKeyEncoding: true,
|
||||
encryptedHeadersTag: true,
|
||||
url: true
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
import { BaseProviderSchema } from "../audit-log-stream-schemas";
|
||||
|
||||
export const CustomProviderCredentialsSchema = z.object({
|
||||
url: z.string().url().trim().min(1).max(255),
|
||||
@@ -12,13 +13,13 @@ export const CustomProviderCredentialsSchema = z.object({
|
||||
.array()
|
||||
});
|
||||
|
||||
export const CustomProviderSchema = z.object({
|
||||
provider: z.literal(LogProvider.Custom),
|
||||
const BaseCustomProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Custom) });
|
||||
|
||||
export const CustomProviderSchema = BaseCustomProviderSchema.extend({
|
||||
credentials: CustomProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedCustomProviderSchema = z.object({
|
||||
provider: z.literal(LogProvider.Custom),
|
||||
export const SanitizedCustomProviderSchema = BaseCustomProviderSchema.extend({
|
||||
credentials: z.object({
|
||||
url: CustomProviderCredentialsSchema.shape.url,
|
||||
// Only return header keys
|
||||
@@ -30,3 +31,11 @@ export const CustomProviderListItemSchema = z.object({
|
||||
name: z.literal("Custom"),
|
||||
provider: z.literal(LogProvider.Custom)
|
||||
});
|
||||
|
||||
export const CreateCustomProviderLogStreamSchema = z.object({
|
||||
credentials: CustomProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateCustomProviderLogStreamSchema = z.object({
|
||||
credentials: CustomProviderCredentialsSchema
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RawAxiosRequestHeaders } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
@@ -8,6 +9,20 @@ import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue";
|
||||
import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types";
|
||||
import { TDatadogProviderCredentials } from "./datadog-provider-types";
|
||||
|
||||
function createPayload(event: Record<string, unknown>) {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const ddtags = [`env:${appCfg.NODE_ENV || "unknown"}`].join(",");
|
||||
|
||||
return {
|
||||
...event,
|
||||
hostname: new URL(appCfg.SITE_URL || "http://infisical").hostname,
|
||||
ddsource: "infisical",
|
||||
service: "infisical",
|
||||
ddtags
|
||||
};
|
||||
}
|
||||
|
||||
export const DatadogProviderFactory = () => {
|
||||
const validateCredentials: TLogStreamFactoryValidateCredentials<TDatadogProviderCredentials> = async ({
|
||||
credentials
|
||||
@@ -19,15 +34,11 @@ export const DatadogProviderFactory = () => {
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token };
|
||||
|
||||
await request
|
||||
.post(
|
||||
url,
|
||||
{ ping: "ok" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
)
|
||||
.post(url, createPayload({ ping: "ok" }), {
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new BadRequestError({ message: `Failed to connect with Datadog: ${(err as Error)?.message}` });
|
||||
});
|
||||
@@ -42,15 +53,11 @@ export const DatadogProviderFactory = () => {
|
||||
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json", "DD-API-KEY": token };
|
||||
|
||||
await request.post(
|
||||
url,
|
||||
{ ...auditLog, ddsource: "infisical", service: "audit-logs" },
|
||||
{
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
}
|
||||
);
|
||||
await request.post(url, createPayload(auditLog), {
|
||||
headers: streamHeaders,
|
||||
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
import { BaseProviderSchema } from "../audit-log-stream-schemas";
|
||||
|
||||
export const DatadogProviderCredentialsSchema = z.object({
|
||||
url: z.string().url().trim().min(1).max(255),
|
||||
@@ -11,13 +12,13 @@ export const DatadogProviderCredentialsSchema = z.object({
|
||||
.refine((val) => new RE2(/^[a-fA-F0-9]{32}$/).test(val), "Invalid Datadog API key format")
|
||||
});
|
||||
|
||||
export const DatadogProviderSchema = z.object({
|
||||
provider: z.literal(LogProvider.Datadog),
|
||||
const BaseDatadogProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Datadog) });
|
||||
|
||||
export const DatadogProviderSchema = BaseDatadogProviderSchema.extend({
|
||||
credentials: DatadogProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedDatadogProviderSchema = z.object({
|
||||
provider: z.literal(LogProvider.Datadog),
|
||||
export const SanitizedDatadogProviderSchema = BaseDatadogProviderSchema.extend({
|
||||
credentials: DatadogProviderCredentialsSchema.pick({
|
||||
url: true
|
||||
})
|
||||
@@ -27,3 +28,11 @@ export const DatadogProviderListItemSchema = z.object({
|
||||
name: z.literal("Datadog"),
|
||||
provider: z.literal(LogProvider.Datadog)
|
||||
});
|
||||
|
||||
export const CreateDatadogProviderLogStreamSchema = z.object({
|
||||
credentials: DatadogProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateDatadogProviderLogStreamSchema = z.object({
|
||||
credentials: DatadogProviderCredentialsSchema
|
||||
});
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { LogProvider } from "../audit-log-stream-enums";
|
||||
import { BaseProviderSchema } from "../audit-log-stream-schemas";
|
||||
|
||||
export const SplunkProviderCredentialsSchema = z.object({
|
||||
hostname: z.string().url().trim().min(1).max(255),
|
||||
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)
|
||||
});
|
||||
|
||||
export const SplunkProviderSchema = z.object({
|
||||
provider: z.literal(LogProvider.Splunk),
|
||||
const BaseSplunkProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Splunk) });
|
||||
|
||||
export const SplunkProviderSchema = BaseSplunkProviderSchema.extend({
|
||||
credentials: SplunkProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedSplunkProviderSchema = z.object({
|
||||
provider: z.literal(LogProvider.Splunk),
|
||||
export const SanitizedSplunkProviderSchema = BaseSplunkProviderSchema.extend({
|
||||
credentials: SplunkProviderCredentialsSchema.pick({
|
||||
hostname: true
|
||||
})
|
||||
@@ -23,3 +49,11 @@ export const SplunkProviderListItemSchema = z.object({
|
||||
name: z.literal("Splunk"),
|
||||
provider: z.literal(LogProvider.Splunk)
|
||||
});
|
||||
|
||||
export const CreateSplunkProviderLogStreamSchema = z.object({
|
||||
credentials: SplunkProviderCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateSplunkProviderLogStreamSchema = z.object({
|
||||
credentials: SplunkProviderCredentialsSchema
|
||||
});
|
||||
|
||||
BIN
frontend/public/images/integrations/Datadog.png
Normal file
BIN
frontend/public/images/integrations/Datadog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
BIN
frontend/public/images/integrations/Splunk.png
Normal file
BIN
frontend/public/images/integrations/Splunk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
29
frontend/src/helpers/auditLogStreams.ts
Normal file
29
frontend/src/helpers/auditLogStreams.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { faCode, IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
|
||||
import { TAuditLogStream } from "@app/hooks/api/types";
|
||||
import { DiscriminativePick } from "@app/types";
|
||||
|
||||
export const AUDIT_LOG_STREAM_PROVIDER_MAP: Record<
|
||||
LogProvider,
|
||||
{ name: string; image?: string; icon?: IconDefinition; size?: number }
|
||||
> = {
|
||||
[LogProvider.Custom]: { name: "Custom", icon: faCode },
|
||||
[LogProvider.Datadog]: { name: "Datadog", image: "Datadog.png" },
|
||||
[LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 }
|
||||
};
|
||||
|
||||
export function getProviderUrl(
|
||||
logStream: DiscriminativePick<TAuditLogStream, "provider" | "credentials">
|
||||
) {
|
||||
switch (logStream.provider) {
|
||||
case LogProvider.Custom:
|
||||
return logStream.credentials.url;
|
||||
case LogProvider.Datadog:
|
||||
return logStream.credentials.url;
|
||||
case LogProvider.Splunk:
|
||||
return `https://${logStream.credentials.hostname}:8088/services/collector/event`;
|
||||
default:
|
||||
return "Not found";
|
||||
}
|
||||
}
|
||||
5
frontend/src/hooks/api/auditLogStreams/enums.ts
Normal file
5
frontend/src/hooks/api/auditLogStreams/enums.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum LogProvider {
|
||||
Datadog = "datadog",
|
||||
Splunk = "splunk",
|
||||
Custom = "custom"
|
||||
}
|
||||
@@ -1,6 +1,2 @@
|
||||
export {
|
||||
useCreateAuditLogStream,
|
||||
useDeleteAuditLogStream,
|
||||
useUpdateAuditLogStream
|
||||
} from "./mutations";
|
||||
export { useGetAuditLogStreamDetails, useGetAuditLogStreams } from "./queries";
|
||||
export * from "./mutations";
|
||||
export * from "./queries";
|
||||
|
||||
@@ -12,50 +12,54 @@ import {
|
||||
|
||||
export const useCreateAuditLogStream = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ auditLogStream: TAuditLogStream }, object, TCreateAuditLogStreamDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ provider, ...params }: TCreateAuditLogStreamDTO) => {
|
||||
const { data } = await apiRequest.post<{ auditLogStream: TAuditLogStream }>(
|
||||
"/api/v1/audit-log-streams",
|
||||
dto
|
||||
`/api/v1/audit-log-streams/${provider}`,
|
||||
{ ...params, provider }
|
||||
);
|
||||
return data;
|
||||
|
||||
return data.auditLogStream;
|
||||
},
|
||||
onSuccess: (_, { orgId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list(orgId) });
|
||||
}
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list() })
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateAuditLogStream = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ auditLogStream: TAuditLogStream }, object, TUpdateAuditLogStreamDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ auditLogStreamId, provider, ...params }: TUpdateAuditLogStreamDTO) => {
|
||||
const { data } = await apiRequest.patch<{ auditLogStream: TAuditLogStream }>(
|
||||
`/api/v1/audit-log-streams/${dto.id}`,
|
||||
dto
|
||||
`/api/v1/audit-log-streams/${provider}/${auditLogStreamId}`,
|
||||
params
|
||||
);
|
||||
return data;
|
||||
|
||||
return data.auditLogStream;
|
||||
},
|
||||
onSuccess: (_, { orgId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list(orgId) });
|
||||
onSuccess: (_, { auditLogStreamId, provider }) => {
|
||||
queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: auditLogStreamKeys.getById(provider, auditLogStreamId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteAuditLogStream = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ auditLogStream: TAuditLogStream }, object, TDeleteAuditLogStreamDTO>({
|
||||
mutationFn: async (dto) => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ auditLogStreamId, provider }: TDeleteAuditLogStreamDTO) => {
|
||||
const { data } = await apiRequest.delete<{ auditLogStream: TAuditLogStream }>(
|
||||
`/api/v1/audit-log-streams/${dto.id}`
|
||||
`/api/v1/audit-log-streams/${provider}/${auditLogStreamId}`
|
||||
);
|
||||
return data;
|
||||
|
||||
return data.auditLogStream;
|
||||
},
|
||||
onSuccess: (_, { orgId }) => {
|
||||
queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list(orgId) });
|
||||
onSuccess: (_, { auditLogStreamId, provider }) => {
|
||||
queryClient.invalidateQueries({ queryKey: auditLogStreamKeys.list() });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: auditLogStreamKeys.getById(provider, auditLogStreamId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,40 +1,89 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TAuditLogStream } from "./types";
|
||||
import { TAuditLogStreamProviderOption } from "./types/provider-options";
|
||||
import { LogProvider } from "./enums";
|
||||
import { TAuditLogStream, TAuditLogStreamProviderMap } from "./types";
|
||||
|
||||
export const auditLogStreamKeys = {
|
||||
list: (orgId: string) => ["audit-log-stream", { orgId }],
|
||||
getById: (id: string) => ["audit-log-stream-details", { id }]
|
||||
all: ["audit-log-stream"] as const,
|
||||
options: () => [...auditLogStreamKeys.all, "options"] as const,
|
||||
list: () => [...auditLogStreamKeys.all, "list"] as const,
|
||||
getById: (provider: string, id: string) =>
|
||||
[...auditLogStreamKeys.all, provider, "get-by-id", id] as const
|
||||
};
|
||||
|
||||
const fetchAuditLogStreams = async () => {
|
||||
const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>(
|
||||
"/api/v1/audit-log-streams"
|
||||
);
|
||||
export const useGetAuditLogStreamOptions = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TAuditLogStreamProviderOption[],
|
||||
unknown,
|
||||
TAuditLogStreamProviderOption[],
|
||||
ReturnType<typeof auditLogStreamKeys.options>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: auditLogStreamKeys.options(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ providerOptions: TAuditLogStreamProviderOption[] }>(
|
||||
"/api/v1/audit-log-streams/options"
|
||||
);
|
||||
|
||||
return data.auditLogStreams;
|
||||
};
|
||||
|
||||
export const useGetAuditLogStreams = (orgId: string) =>
|
||||
useQuery({
|
||||
queryKey: auditLogStreamKeys.list(orgId),
|
||||
queryFn: () => fetchAuditLogStreams(),
|
||||
enabled: Boolean(orgId)
|
||||
return data.providerOptions;
|
||||
},
|
||||
...options
|
||||
});
|
||||
|
||||
const fetchAuditLogStreamDetails = async (id: string) => {
|
||||
const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStream }>(
|
||||
`/api/v1/audit-log-streams/${id}`
|
||||
);
|
||||
|
||||
return data.auditLogStream;
|
||||
};
|
||||
|
||||
export const useGetAuditLogStreamDetails = (id: string) =>
|
||||
useQuery({
|
||||
queryKey: auditLogStreamKeys.getById(id),
|
||||
queryFn: () => fetchAuditLogStreamDetails(id),
|
||||
enabled: Boolean(id)
|
||||
export const useListAuditLogStreams = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TAuditLogStream[],
|
||||
unknown,
|
||||
TAuditLogStream[],
|
||||
ReturnType<typeof auditLogStreamKeys.list>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: auditLogStreamKeys.list(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>(
|
||||
"/api/v1/audit-log-streams"
|
||||
);
|
||||
|
||||
return data.auditLogStreams;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAuditLogStreamById = <T extends LogProvider>(
|
||||
provider: T,
|
||||
logStreamId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TAuditLogStreamProviderMap[T],
|
||||
unknown,
|
||||
TAuditLogStreamProviderMap[T],
|
||||
ReturnType<typeof auditLogStreamKeys.getById>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: auditLogStreamKeys.getById(provider, logStreamId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStreamProviderMap[T] }>(
|
||||
`/api/v1/audit-log-streams/${provider}/${logStreamId}`
|
||||
);
|
||||
|
||||
return data.auditLogStream;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
export type LogStreamHeaders = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TAuditLogStream = {
|
||||
id: string;
|
||||
url: string;
|
||||
headers?: LogStreamHeaders[];
|
||||
};
|
||||
|
||||
export type TCreateAuditLogStreamDTO = {
|
||||
url: string;
|
||||
headers?: LogStreamHeaders[];
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TUpdateAuditLogStreamDTO = {
|
||||
id: string;
|
||||
url?: string;
|
||||
headers?: LogStreamHeaders[];
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TDeleteAuditLogStreamDTO = {
|
||||
id: string;
|
||||
orgId: string;
|
||||
};
|
||||
25
frontend/src/hooks/api/auditLogStreams/types/index.ts
Normal file
25
frontend/src/hooks/api/auditLogStreams/types/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { LogProvider } from "../enums";
|
||||
import { TCustomProviderLogStream } from "./providers/custom-provider";
|
||||
import { TDatadogProviderLogStream } from "./providers/datadog-provider";
|
||||
import { TSplunkProviderLogStream } from "./providers/splunk-provider";
|
||||
|
||||
export type TAuditLogStream =
|
||||
| TCustomProviderLogStream
|
||||
| TDatadogProviderLogStream
|
||||
| TSplunkProviderLogStream;
|
||||
|
||||
export type TAuditLogStreamProviderMap = {
|
||||
[LogProvider.Custom]: TCustomProviderLogStream;
|
||||
[LogProvider.Datadog]: TDatadogProviderLogStream;
|
||||
[LogProvider.Splunk]: TSplunkProviderLogStream;
|
||||
};
|
||||
|
||||
export type TCreateAuditLogStreamDTO = Pick<TAuditLogStream, "provider" | "credentials">;
|
||||
export type TUpdateAuditLogStreamDTO = Pick<TAuditLogStream, "credentials"> & {
|
||||
provider: LogProvider;
|
||||
auditLogStreamId: string;
|
||||
};
|
||||
export type TDeleteAuditLogStreamDTO = {
|
||||
provider: LogProvider;
|
||||
auditLogStreamId: string;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LogProvider } from "../enums";
|
||||
|
||||
export type TAuditLogStreamProviderOptionBase = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TAuditLogStreamProviderOption = {
|
||||
[P in keyof typeof LogProvider]: TAuditLogStreamProviderOptionBase & {
|
||||
provider: (typeof LogProvider)[P];
|
||||
};
|
||||
}[keyof typeof LogProvider];
|
||||
@@ -0,0 +1,10 @@
|
||||
import { LogProvider } from "../../enums";
|
||||
import { TRootProviderLogStream } from "./root-provider";
|
||||
|
||||
export type TCustomProviderLogStream = TRootProviderLogStream & {
|
||||
provider: LogProvider.Custom;
|
||||
credentials: {
|
||||
url: string;
|
||||
headers: { key: string; value: string }[];
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { LogProvider } from "../../enums";
|
||||
import { TRootProviderLogStream } from "./root-provider";
|
||||
|
||||
export type TDatadogProviderLogStream = TRootProviderLogStream & {
|
||||
provider: LogProvider.Datadog;
|
||||
credentials: {
|
||||
url: string;
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export type TRootProviderLogStream = {
|
||||
id: string;
|
||||
orgId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { LogProvider } from "../../enums";
|
||||
import { TRootProviderLogStream } from "./root-provider";
|
||||
|
||||
export type TSplunkProviderLogStream = TRootProviderLogStream & {
|
||||
provider: LogProvider.Splunk;
|
||||
credentials: {
|
||||
hostname: string;
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useFetchServerStatus, useGetAuditLogStreams } from "@app/hooks/api";
|
||||
import { useFetchServerStatus, useListAuditLogStreams } from "@app/hooks/api";
|
||||
|
||||
import { OrgAlertBanner } from "../OrgAlertBanner";
|
||||
|
||||
export const AuditLogBanner = () => {
|
||||
const org = useOrganization();
|
||||
const { data: status, isLoading: isLoadingStatus } = useFetchServerStatus();
|
||||
const { data: streams, isLoading: isLoadingStreams } = useGetAuditLogStreams(org.currentOrg.id);
|
||||
const { data: streams, isLoading: isLoadingStreams } = useListAuditLogStreams();
|
||||
|
||||
if (isLoadingStreams || isLoadingStatus || !streams) return null;
|
||||
|
||||
|
||||
@@ -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,154 @@
|
||||
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"
|
||||
/>
|
||||
</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="Endpoint URL"
|
||||
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,54 @@
|
||||
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-start 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="flex items-center text-mineshaft-300">{providerDetails.name}</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={4}
|
||||
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't see the third-party provider you'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";
|
||||
Reference in New Issue
Block a user