Merge branch 'main' of https://github.com/Infisical/infisical into feat/pagination-in-resource-overview-tables

This commit is contained in:
Piyush Gupta
2025-10-09 19:13:27 +05:30
26 changed files with 289 additions and 147 deletions

View File

@@ -75,7 +75,7 @@ class RailwayPublicClient {
async send<T extends TRailwayResponse>(
query: string,
options: RailwaySendReqOptions,
variables: Record<string, string | Record<string, string>> = {},
variables: Record<string, unknown> = {},
retryAttempt: number = 0
): Promise<T["data"] | undefined> {
const body = {
@@ -117,6 +117,25 @@ class RailwayPublicClient {
}
}
async getDeployments(
config: RailwaySendReqOptions,
variables: { input: { serviceId: string; environmentId: string }; first?: number }
) {
return this.send<TRailwayResponse<{ deployments: { edges: { node: { id: string } }[] } }>>(
`query deployments($input: DeploymentListInput!, $first: Int) { deployments(first: $first, input: $input) { edges { node { id } } } }`,
config,
variables
);
}
async redeployDeployment(config: RailwaySendReqOptions, variables: { input: { deploymentId: string } }) {
return this.send<TRailwayResponse<{ deploymentRedeploy: { id: string } }>>(
`mutation deploymentRedeploy($deploymentId: String!) { deploymentRedeploy(id: $deploymentId) { id } }`,
config,
{ deploymentId: variables.input.deploymentId }
);
}
async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) {
const res = await this.send(
`query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`,
@@ -213,7 +232,9 @@ class RailwayPublicClient {
async deleteVariable(
config: RailwaySendReqOptions,
variables: { input: { projectId: string; environmentId: string; name: string; serviceId?: string } }
variables: {
input: { projectId: string; environmentId: string; name: string; skipDeploys?: boolean; serviceId?: string };
}
) {
await this.send<TRailwayResponse<{ variables: Record<string, string> }>>(
`mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`,
@@ -222,6 +243,26 @@ class RailwayPublicClient {
);
}
async upsertCollection(
config: RailwaySendReqOptions,
variables: {
input: {
projectId: string;
environmentId: string;
variables: Record<string, string>;
skipDeploys?: boolean;
serviceId?: string;
replace?: boolean;
};
}
) {
return this.send<TRailwayResponse<boolean>>(
`mutation variableCollectionUpsert($input: VariableCollectionUpsertInput!) { variableCollectionUpsert(input: $input) }`,
config,
variables
);
}
async upsertVariable(
config: RailwaySendReqOptions,
variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } }

View File

@@ -12,6 +12,8 @@ export const RailwaySyncFns = {
async getSecrets(secretSync: TRailwaySyncWithCredentials): Promise<TSecretMap> {
try {
const config = secretSync.destinationConfig;
const { keySchema } = secretSync.syncOptions;
const { environment } = secretSync;
const variables = await RailwayPublicAPI.getVariables(secretSync.connection, {
projectId: config.projectId,
@@ -26,6 +28,10 @@ export const RailwaySyncFns = {
// eslint-disable-next-line no-continue
if (key.startsWith("RAILWAY_")) continue;
// Check if key matches the schema
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
entries[key] = {
value
};
@@ -40,60 +46,73 @@ export const RailwaySyncFns = {
}
},
/**
* Syncs secrets to Railway and redeploys the service if needed.
*
* Gets existing Railway vars, merges with new secrets (keeping Railway vars if deletion is disabled),
* then replaces every variable with the new values, if variable is not in the secretMap, it is deleted.
* If there's a service, triggers a redeploy to pick up the changes.
*/
async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {
const {
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const railwaySecrets = await this.getSecrets(secretSync);
const config = secretSync.destinationConfig;
try {
const {
syncOptions: { disableSecretDeletion }
} = secretSync;
const railwaySecrets = await this.getSecrets(secretSync);
const config = secretSync.destinationConfig;
for await (const key of Object.keys(secretMap)) {
try {
const existing = railwaySecrets[key];
const railwaySecretsMap = Object.fromEntries(
Object.entries(railwaySecrets).map(([key, secret]) => [key, secret.value])
);
const secretMapMap = Object.fromEntries(Object.entries(secretMap).map(([key, secret]) => [key, secret.value]));
if (existing === undefined || existing.value !== secretMap[key].value) {
await RailwayPublicAPI.upsertVariable(secretSync.connection, {
input: {
projectId: config.projectId,
environmentId: config.environmentId,
serviceId: config.serviceId || undefined,
name: key,
value: secretMap[key].value ?? ""
}
});
const toReplace = disableSecretDeletion ? { ...railwaySecretsMap, ...secretMapMap } : secretMapMap;
const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, {
input: {
projectId: config.projectId,
environmentId: config.environmentId,
serviceId: config.serviceId || undefined,
skipDeploys: true,
variables: toReplace,
replace: true
}
} catch (error) {
});
if (!upserted)
throw new SecretSyncError({
error,
secretKey: key
message: "Failed to upsert secrets to Railway"
});
}
}
if (disableSecretDeletion) return;
if (!config.serviceId) return;
for await (const key of Object.keys(railwaySecrets)) {
try {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
const latestDeployment = await RailwayPublicAPI.getDeployments(secretSync.connection, {
input: {
serviceId: config.serviceId,
environmentId: config.environmentId
},
first: 1
});
if (!secretMap[key]) {
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
input: {
projectId: config.projectId,
environmentId: config.environmentId,
serviceId: config.serviceId || undefined,
name: key
}
});
const latestDeploymentId = latestDeployment?.deployments.edges[0].node.id;
if (!latestDeploymentId)
throw new SecretSyncError({
message: "Failed to get latest deployment from Railway"
});
await RailwayPublicAPI.redeployDeployment(secretSync.connection, {
input: {
deploymentId: latestDeploymentId
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
});
} catch (error) {
if (error instanceof SecretSyncError) throw error;
throw new SecretSyncError({
error,
message: "Failed to sync secrets to Railway"
});
}
},
@@ -101,24 +120,37 @@ export const RailwaySyncFns = {
const existing = await this.getSecrets(secretSync);
const config = secretSync.destinationConfig;
for await (const secret of Object.keys(existing)) {
try {
if (secret in secretMap) {
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
input: {
projectId: config.projectId,
environmentId: config.environmentId,
serviceId: config.serviceId || undefined,
name: secret
}
});
// Create a new variables object excluding secrets that exist in secretMap
const remainingVariables = Object.fromEntries(
Object.entries(existing)
.filter(([key]) => !(key in secretMap))
.map(([key, secret]) => [key, secret.value])
);
try {
const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, {
input: {
projectId: config.projectId,
environmentId: config.environmentId,
serviceId: config.serviceId || undefined,
skipDeploys: true,
variables: remainingVariables,
replace: true
}
} catch (error) {
});
if (!upserted) {
throw new SecretSyncError({
error,
secretKey: secret
message: "Failed to remove secrets from Railway"
});
}
} catch (error) {
if (error instanceof SecretSyncError) throw error;
throw new SecretSyncError({
error,
message: "Failed to remove secrets from Railway"
});
}
}
};

View File

@@ -221,6 +221,7 @@
]
},
"documentation/platform/sso/auth0-oidc",
"documentation/platform/sso/pingone-oidc",
{
"group": "General OIDC",
"pages": [

View File

@@ -95,7 +95,7 @@ To successfully deploy an Infisical Gateway for use, follow these steps in order
<Step title="Set Up a Relay Server">
Ensure a relay server is running and accessible before you deploy any gateways. You have two options:
- **Managed relay (Infisical Cloud, US/EU only):** Managed relays are only available for Infisical Cloud instances in the US and EU regions. If you are using Infisical Cloud in these regions, you can use the provided managed relay.
- **Self-hosted relay:** For all other cases, including all self-hosted and dedicated enterprise instances of Infisical, you must deploy your own relay server. You can also choose to deploy your own relay server when using Infisical Cloud if you require reduced geographic proximity to your target resources for lower latency or to reduce network congestion. For setup instructions, see the <a href="/documentation/platform/gateways/relay-deployment">Relay Deployment Guide</a>.
- **Self-hosted relay:** For all other cases, including all self-hosted and dedicated enterprise instances of Infisical, you must deploy your own relay server. You can also choose to deploy your own relay server when using Infisical Cloud if you require reduced geographic proximity to your target resources for lower latency or to reduce network congestion. For setup instructions, see the [Relay Deployment Guide](/documentation/platform/gateways/relay-deployment).
</Step>
<Step title="Install the Infisical CLI">
Make sure the Infisical CLI is installed on the machine or environment where you plan to deploy the gateway. The CLI is required for gateway installation and management.

View File

@@ -6,7 +6,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
<Info>
Auth0 OIDC SSO is a paid feature. If you're using Infisical Cloud, then it is
available under the **Pro Tier**. If you're self-hosting Infisical, then you
should contact sales@infisical.com to purchase an enterprise license to use
should contact sales@infisical.com to purchase a self-hosted license to use
it.
</Info>
@@ -55,7 +55,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
<Step title="Enable OIDC in Infisical">
Enabling OIDC allows members in your organization to log into Infisical via Auth0.
![OIDC auth0 enable OIDC](../../../images/sso/auth0-oidc/enable-oidc.png)
![OIDC auth0 enable OIDC](../../../images/sso/enable-oidc.png)
</Step>
<Step title="Enforce OIDC SSO in Infisical">

View File

@@ -7,7 +7,7 @@ description: "Learn how to configure OIDC for Infisical SSO with any OIDC-compli
<Info>
OIDC SSO is a paid feature. If you're using Infisical Cloud, then it is
available under the **Pro Tier**. If you're self-hosting Infisical, then you
should contact sales@infisical.com to purchase an enterprise license to use
should contact sales@infisical.com to purchase a self-hosted license to use
it.
</Info>

View File

@@ -7,7 +7,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
<Info>
Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it
is available under the **Pro Tier**. If you're self-hosting Infisical, then
you should contact sales@infisical.com to purchase an enterprise license to
you should contact sales@infisical.com to purchase a self-hosted license to
use it.
</Info>
@@ -82,7 +82,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
<Step title="Enable OIDC SSO in Infisical">
Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
![OIDC keycloak enable OIDC](/images/sso/keycloak-oidc/enable-oidc.png)
![OIDC keycloak enable OIDC](/images/sso/enable-oidc.png)
</Step>
<Step title="Enforce OIDC SSO in Infisical">

View File

@@ -0,0 +1,108 @@
---
title: "PingOne OIDC"
description: "Learn how to configure PingOne OIDC for Infisical SSO."
---
<Info>
PingOne OIDC SSO is a paid feature. If you're using Infisical Cloud, then it is
available under the **Pro Tier**. If you're self-hosting Infisical, then you
should contact sales@infisical.com to purchase a self-hosted license to use
it.
</Info>
<Steps>
<Step title="Setup application in PingOne">
1.1. From the Application's Page, create a new OIDC Web App application.
![OIDC pingone create application](../../../images/sso/pingone-oidc/pingone-create-application.png)
1.2. Enable the application by pressing the "Enable" toggle.
![OIDC PingOne Enable Application](../../../images/sso/pingone-oidc/pingone-enable-application.png)
1.3. In the Application "Configuration" tab, press the "Edit" pencil icon to configure the application callback URI.
![OIDC PingOne Edit Application Configuration](../../../images/sso/pingone-oidc/pingone-edit-application-configuration.png)
1.4 Set the Redirect URL to `https://app.infisical.com/api/v1/sso/oidc/callback` and press the "Save" button.
![OIDC PingOne Edit Redirect URI](../../../images/sso/pingone-oidc/pingone-edit-application-redirect-uri.png)
<Info>
If you're self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain.
</Info>
1.5 After configuring the redirect URL, go to the "Attribute Mappings" tab and press the "Edit" pencil icon to configure the attribute mappings.
![OIDC PingOne Edit Attribute Mappings](../../../images/sso/pingone-oidc/pingone-edit-application-attribute-mappings.png)
1.6 Map the following attributes:
- `email` -> `Email Address`
- `name` -> `Username`
![OIDC PingOne Edit Attribute Mappings](../../../images/sso/pingone-oidc/pingone-edit-application-attribute-mappings-2.png)
Once done, press the "Save" button.
</Step>
<Step title="Retrieve Identity Provider (IdP) Information from PingOne">
2.1. Open the "Overview" tab and copy the **Client ID** and **Client Secret**.
![OIDC PingOne Application Credential](../../../images/sso/pingone-oidc/pingone-overview-credentials.png)
2.2. Still in the "Overview" tab, scroll down to the Connection Details section and retrieve the **OIDC Discovery Endpoint**.
![OIDC PingOne OIDC Discovery Endpoint](../../../images/sso/pingone-oidc/pingone-overview-oidc-discovery-endpoint.png)
Keep these values handy as we will need them in the next steps.
</Step>
<Step title="Finish configuring OIDC in Infisical">
3.1. Back in Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **OIDC**.
![OIDC SSO Connect](../../../images/sso/connect-oidc.png)
3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2.
![OIDC PingOne paste values into Infisical](../../../images/sso/pingone-oidc/infisical-configure-oidc.png)
<Info>
Currently, the following JWT signature algorithms are supported: RS256, RS512, HS256, and EdDSA
</Info>
Once you've done that, press **Update** to complete the required configuration.
</Step>
<Step title="Enable OIDC in Infisical">
Enabling OIDC allows members in your organization to log into Infisical via PingOne
![OIDC PingOne enable OIDC](../../../images/sso/enable-oidc.png)
</Step>
<Step title="Enforce OIDC SSO in Infisical">
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
by logging into the organization via PingOne.
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one PingOne user with Infisical.
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
<Warning>
We recommend ensuring that your account is provisioned using the application in PingOne
prior to enforcing OIDC SSO to prevent any unintended issues.
</Warning>
<Info>
In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin.
</Info>
</Step>
</Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
</Tip>
<Note>
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
work:
<div class="height:1px;"/>
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
can be a random 32-byte base64 string generated with `openssl rand -base64
32`.
<div class="height:1px;"/>
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
</Note>

View File

Before

Width:  |  Height:  |  Size: 797 KiB

After

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,9 +1,8 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "@tanstack/react-router";
import axios from "axios";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { useSendVerificationEmail } from "@app/hooks/api";
import { Button, Input } from "../v2";
@@ -35,11 +34,10 @@ export default function EnterEmailStep({
* Verifies if the entered email "looks" correct
*/
const emailCheck = async () => {
const isValid = z.string().email().safeParse(email);
let emailCheckBool = false;
if (!email) {
setEmailError(true);
emailCheckBool = true;
} else if (!email.includes("@") || !email.includes(".") || !/[a-z]/.test(email)) {
if (!isValid.success) {
setEmailError(true);
emailCheckBool = true;
} else {
@@ -48,19 +46,9 @@ export default function EnterEmailStep({
// If everything is correct, go to the next step
if (!emailCheckBool) {
try {
await mutateAsync({ email: email.toLowerCase() });
setEmail(email.toLowerCase());
incrementStep();
} catch (e) {
if (axios.isAxiosError(e)) {
const { message = "Something went wrong" } = e.response?.data as { message: string };
createNotification({
type: "error",
text: message
});
}
}
await mutateAsync({ email: email.toLowerCase() });
setEmail(email.toLowerCase());
incrementStep();
}
};

View File

@@ -12,7 +12,8 @@ export const AUDIT_LOG_STREAM_PROVIDER_MAP: Record<
[LogProvider.Cribl]: { name: "Cribl", image: "Cribl.png", size: 60 },
[LogProvider.Custom]: { name: "Custom", icon: faCode },
[LogProvider.Datadog]: { name: "Datadog", image: "Datadog.png" },
[LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 }
[LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 },
[LogProvider.QRadar]: { name: "IBM QRadar", image: "IBM.png" }
};
// Strictly for showing to the client in the front-end

View File

@@ -3,5 +3,6 @@ export enum LogProvider {
Cribl = "cribl",
Custom = "custom",
Datadog = "datadog",
Splunk = "splunk"
Splunk = "splunk",
QRadar = "qradar"
}

View File

@@ -3,6 +3,7 @@ import { TAzureProviderLogStream } from "./providers/azure-provider";
import { TCriblProviderLogStream } from "./providers/cribl-provider";
import { TCustomProviderLogStream } from "./providers/custom-provider";
import { TDatadogProviderLogStream } from "./providers/datadog-provider";
import { TQRadarProviderLogStream } from "./providers/qradar-provider";
import { TSplunkProviderLogStream } from "./providers/splunk-provider";
export type TAuditLogStream =
@@ -18,6 +19,7 @@ export type TAuditLogStreamProviderMap = {
[LogProvider.Custom]: TCustomProviderLogStream;
[LogProvider.Datadog]: TDatadogProviderLogStream;
[LogProvider.Splunk]: TSplunkProviderLogStream;
[LogProvider.QRadar]: TQRadarProviderLogStream;
};
export type TCreateAuditLogStreamDTO = Pick<TAuditLogStream, "provider" | "credentials">;

View File

@@ -0,0 +1,7 @@
import { LogProvider } from "../../enums";
import { TRootProviderLogStream } from "./root-provider";
export type TQRadarProviderLogStream = TRootProviderLogStream & {
provider: LogProvider.QRadar;
// credentials: {};
};

View File

@@ -2,9 +2,10 @@ import { useMemo } from "react";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { EmptyState, Spinner } from "@app/components/v2";
import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useGetAuditLogStreamOptions } from "@app/hooks/api";
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
@@ -15,6 +16,8 @@ type Props = {
// TODO: When we have more than 1 page of providers, uncomment the search components
export const LogStreamProviderSelect = ({ onSelect }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
const { isPending, data: logStreamOptions } = useGetAuditLogStreamOptions();
const { search, setPage, page, perPage, offset } = usePagination("", {
@@ -23,7 +26,11 @@ export const LogStreamProviderSelect = ({ onSelect }: Props) => {
const filteredOptions = useMemo(
() =>
(logStreamOptions || [])
[
...(logStreamOptions || []),
// QRadar is a planned provider
{ name: "IBM QRadar", provider: LogProvider.QRadar }
]
.filter(
({ name, provider }) =>
name.toLowerCase().includes(search.trim().toLowerCase()) ||
@@ -54,13 +61,6 @@ export const LogStreamProviderSelect = ({ onSelect }: Props) => {
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];
@@ -68,7 +68,13 @@ export const LogStreamProviderSelect = ({ onSelect }: Props) => {
return (
<button
type="button"
onClick={() => onSelect(option.provider)}
onClick={() => {
if (option.provider === LogProvider.QRadar) {
handlePopUpOpen("upgradePlan");
} else {
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 && (
@@ -100,56 +106,11 @@ export const LogStreamProviderSelect = ({ onSelect }: Props) => {
/>
)}
</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]}
/>
)} */}
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="This audit log stream provider requires an enterprise license."
/>
</div>
);
};