mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 01:29:38 +00:00
Merge branch 'main' into feat/remindersImprovement
This commit is contained in:
@@ -65,6 +65,7 @@ export const createNotification = (
|
||||
toast(<NotificationContent {...myProps} />, {
|
||||
position: "bottom-right",
|
||||
...toastProps,
|
||||
autoClose: toastProps.autoClose || 15000,
|
||||
theme: "dark",
|
||||
type: myProps?.type || "info"
|
||||
});
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { MultiValue, SingleValue } from "react-select";
|
||||
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FilterableSelect, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
TBitbucketRepo,
|
||||
TBitbucketWorkspace,
|
||||
useBitbucketConnectionListRepositories,
|
||||
useBitbucketConnectionListWorkspaces
|
||||
} from "@app/hooks/api/appConnections/bitbucket";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { SecretScanningDataSourceConnectionField } from "../SecretScanningDataSourceConnectionField";
|
||||
|
||||
enum ScanMethod {
|
||||
AllRepositories = "all-repositories",
|
||||
SelectRepositories = "select-repositories"
|
||||
}
|
||||
|
||||
export const BitbucketDataSourceConfigFields = () => {
|
||||
const { control, watch, setValue } = useFormContext<
|
||||
TSecretScanningDataSourceForm & {
|
||||
type: SecretScanningDataSource.Bitbucket;
|
||||
}
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ control, name: "connection.id" });
|
||||
const isUpdate = Boolean(watch("id"));
|
||||
|
||||
const selectedWorkspaceSlug = useWatch({ control, name: "config.workspaceSlug" });
|
||||
|
||||
const { data: workspaces, isPending: areWorkspacesLoading } =
|
||||
useBitbucketConnectionListWorkspaces(connectionId, { enabled: Boolean(connectionId) });
|
||||
|
||||
const { data: repositories, isPending: areRepositoriesLoading } =
|
||||
useBitbucketConnectionListRepositories(connectionId, selectedWorkspaceSlug, {
|
||||
enabled: Boolean(connectionId) && Boolean(selectedWorkspaceSlug)
|
||||
});
|
||||
|
||||
const includeRepos = watch("config.includeRepos");
|
||||
|
||||
const scanMethod =
|
||||
!includeRepos || includeRepos[0] === "*"
|
||||
? ScanMethod.AllRepositories
|
||||
: ScanMethod.SelectRepositories;
|
||||
|
||||
useEffect(() => {
|
||||
if (!includeRepos) {
|
||||
setValue("config.includeRepos", ["*"]);
|
||||
}
|
||||
}, [includeRepos, setValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretScanningDataSourceConnectionField
|
||||
isUpdate={isUpdate}
|
||||
onChange={() => {
|
||||
if (scanMethod === ScanMethod.SelectRepositories) {
|
||||
setValue("config.workspaceSlug", "");
|
||||
setValue("config.includeRepos", []);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="config.workspaceSlug"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Workspace"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={<>Ensure that your connection has the correct permissions.</>}
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the workspaces you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={areWorkspacesLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={value ? { slug: value } : null}
|
||||
onChange={(newValue) => {
|
||||
onChange((newValue as SingleValue<TBitbucketWorkspace>)?.slug);
|
||||
if (scanMethod === ScanMethod.SelectRepositories) {
|
||||
setValue("config.includeRepos", []);
|
||||
}
|
||||
}}
|
||||
options={workspaces}
|
||||
placeholder="Select workspace..."
|
||||
getOptionLabel={(option) => option.slug}
|
||||
getOptionValue={(option) => option.slug}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<FormControl label="Scan Repositories">
|
||||
<Select
|
||||
value={scanMethod}
|
||||
onValueChange={(val) => {
|
||||
setValue("config.includeRepos", val === ScanMethod.AllRepositories ? ["*"] : []);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isDisabled={!connectionId}
|
||||
>
|
||||
{Object.values(ScanMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem className="capitalize" value={method} key={method}>
|
||||
{method.replace("-", " ")}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{scanMethod === ScanMethod.SelectRepositories && (
|
||||
<Controller
|
||||
name="config.includeRepos"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Include Repositories"
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={<>Ensure that your connection has the correct permissions.</>}
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the repository you're looking for?</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={areRepositoriesLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId || !selectedWorkspaceSlug}
|
||||
isMulti
|
||||
value={repositories?.filter((repository) =>
|
||||
(value as string[]).includes(repository.full_name)
|
||||
)}
|
||||
onChange={(newValue) => {
|
||||
onChange(
|
||||
newValue ? (newValue as MultiValue<TBitbucketRepo>).map((p) => p.full_name) : []
|
||||
);
|
||||
}}
|
||||
options={repositories}
|
||||
placeholder="Select repositories..."
|
||||
getOptionLabel={(option) => option.full_name}
|
||||
getOptionValue={(option) => option.full_name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
-1
@@ -5,10 +5,12 @@ import { RESOURCE_DESCRIPTION_HELPER } from "@app/helpers/secretScanningV2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { BitbucketDataSourceConfigFields } from "./BitbucketDataSourceConfigFields";
|
||||
import { GitHubDataSourceConfigFields } from "./GitHubDataSourceConfigFields";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields
|
||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceConfigFields,
|
||||
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceConfigFields
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceConfigFields = () => {
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { SecretScanningDataSourceConfigReviewSection } from "./shared";
|
||||
|
||||
export const BitbucketDataSourceReviewFields = () => {
|
||||
const { watch } = useFormContext<
|
||||
TSecretScanningDataSourceForm & {
|
||||
type: SecretScanningDataSource.Bitbucket;
|
||||
}
|
||||
>();
|
||||
|
||||
const [{ includeRepos, workspaceSlug }, connection] = watch(["config", "connection"]);
|
||||
const shouldScanAll = includeRepos[0] === "*";
|
||||
|
||||
return (
|
||||
<SecretScanningDataSourceConfigReviewSection>
|
||||
{connection && <GenericFieldLabel label="Connection">{connection.name}</GenericFieldLabel>}
|
||||
<GenericFieldLabel label="Workspace Slug">{workspaceSlug}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Scan Repositories">
|
||||
{shouldScanAll ? "All" : includeRepos.join(", ")}
|
||||
</GenericFieldLabel>
|
||||
</SecretScanningDataSourceConfigReviewSection>
|
||||
);
|
||||
};
|
||||
+3
-1
@@ -4,10 +4,12 @@ import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { TSecretScanningDataSourceForm } from "../schemas";
|
||||
import { BitbucketDataSourceReviewFields } from "./BitbucketDataSourceReviewFields";
|
||||
import { GitHubDataSourceReviewFields } from "./GitHubDataSourceReviewFields";
|
||||
|
||||
const COMPONENT_MAP: Record<SecretScanningDataSource, React.FC> = {
|
||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields
|
||||
[SecretScanningDataSource.GitHub]: GitHubDataSourceReviewFields,
|
||||
[SecretScanningDataSource.Bitbucket]: BitbucketDataSourceReviewFields
|
||||
};
|
||||
|
||||
export const SecretScanningDataSourceReviewFields = () => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretScanningDataSource } from "@app/hooks/api/secretScanningV2";
|
||||
|
||||
import { BaseSecretScanningDataSourceSchema } from "./base-secret-scanning-data-source-schema";
|
||||
|
||||
export const BitbucketDataSourceSchema = z
|
||||
.object({
|
||||
type: z.literal(SecretScanningDataSource.Bitbucket),
|
||||
config: z.object({
|
||||
workspaceSlug: z.string().min(1, "Workspace Required").max(128),
|
||||
includeRepos: z
|
||||
.string()
|
||||
.array()
|
||||
.min(1, "One or more repositories required")
|
||||
.max(100, "Cannot configure more than 100 repositories")
|
||||
})
|
||||
})
|
||||
.merge(BaseSecretScanningDataSourceSchema({ isConnectionRequired: true }));
|
||||
@@ -1,9 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BitbucketDataSourceSchema } from "./bitbucket-data-source-schema";
|
||||
import { GitHubDataSourceSchema } from "./github-data-source-schema";
|
||||
|
||||
export const SecretScanningDataSourceSchema = z.discriminatedUnion("type", [
|
||||
GitHubDataSourceSchema
|
||||
GitHubDataSourceSchema,
|
||||
BitbucketDataSourceSchema
|
||||
]);
|
||||
|
||||
export type TSecretScanningDataSourceForm = z.infer<typeof SecretScanningDataSourceSchema>;
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export const OnePassSyncFields = () => {
|
||||
menuPlacement="top"
|
||||
isLoading={isVaultsLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={vaults?.find((v) => v.id === value) ?? null}
|
||||
value={vaults?.find((v) => v.id === value) || null}
|
||||
onChange={(option) => onChange((option as SingleValue<TOnePassVault>)?.id ?? null)}
|
||||
options={vaults}
|
||||
placeholder="Select a vault..."
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ export const OCIVaultSyncFields = () => {
|
||||
menuPlacement="top"
|
||||
isLoading={isVaultsLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId || !selectedCompartment}
|
||||
value={vaults?.find((v) => v.id === value) ?? null}
|
||||
value={vaults?.find((v) => v.id === value) || null}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<{ id: string }>)?.id ?? null);
|
||||
setValue("destinationConfig.keyOcid", "");
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import {
|
||||
TRailwayProject,
|
||||
useRailwayConnectionListProjects
|
||||
} from "@app/hooks/api/appConnections/railway";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const RailwaySyncFields = () => {
|
||||
const { control, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.Railway }
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const projectId = useWatch({ name: "destinationConfig.projectId", control });
|
||||
|
||||
const { data: projects = [], isPending: isProjectsLoading } = useRailwayConnectionListProjects(
|
||||
connectionId,
|
||||
{
|
||||
enabled: Boolean(connectionId)
|
||||
}
|
||||
);
|
||||
|
||||
const environments = useMemo(() => {
|
||||
return projects.find((p) => p.id === projectId)?.environments ?? [];
|
||||
}, [projects, projectId]);
|
||||
|
||||
const services = useMemo(() => {
|
||||
return projects.find((p) => p.id === projectId)?.services ?? [];
|
||||
}, [projects, projectId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.environmentId", "");
|
||||
setValue("destinationConfig.projectId", "");
|
||||
setValue("destinationConfig.serviceId", "");
|
||||
setValue("destinationConfig.projectName", "");
|
||||
setValue("destinationConfig.environmentName", "");
|
||||
setValue("destinationConfig.serviceName", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.projectId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Select a project"
|
||||
tooltipClassName="max-w-md"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isProjectsLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={projects.find((p) => p.id === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const v = option as SingleValue<TRailwayProject>;
|
||||
onChange(v?.id ?? null);
|
||||
setValue("destinationConfig.projectName", v?.name ?? "");
|
||||
}}
|
||||
options={projects}
|
||||
placeholder="Select a project..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.environmentId"
|
||||
disabled={!connectionId || !projectId}
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Select an environment"
|
||||
tooltipClassName="max-w-md"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isProjectsLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={environments.find((p) => p.id === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const v = option as SingleValue<TRailwayProject>;
|
||||
onChange(v?.id ?? null);
|
||||
setValue("destinationConfig.environmentName", v?.name ?? "");
|
||||
}}
|
||||
options={environments}
|
||||
placeholder="Select an environment..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="destinationConfig.serviceId"
|
||||
disabled={!connectionId || !projectId}
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Select a service"
|
||||
tooltipClassName="max-w-md"
|
||||
tooltipText="By default secrets are created as shared variables in Railway."
|
||||
helperText="Scope your secrets to a specific service within the environment."
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isProjectsLoading && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={services.find((p) => p.id === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const v = option as SingleValue<TRailwayProject>;
|
||||
onChange(v?.id ?? null);
|
||||
setValue("destinationConfig.serviceName", v?.name ?? "");
|
||||
}}
|
||||
options={services}
|
||||
placeholder="Select a service..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+6
@@ -20,11 +20,13 @@ import { HCVaultSyncFields } from "./HCVaultSyncFields";
|
||||
import { HerokuSyncFields } from "./HerokuSyncFields";
|
||||
import { HumanitecSyncFields } from "./HumanitecSyncFields";
|
||||
import { OCIVaultSyncFields } from "./OCIVaultSyncFields";
|
||||
import { RailwaySyncFields } from "./RailwaySyncFields";
|
||||
import { RenderSyncFields } from "./RenderSyncFields";
|
||||
import { TeamCitySyncFields } from "./TeamCitySyncFields";
|
||||
import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields";
|
||||
import { VercelSyncFields } from "./VercelSyncFields";
|
||||
import { WindmillSyncFields } from "./WindmillSyncFields";
|
||||
import { ZabbixSyncFields } from "./ZabbixSyncFields";
|
||||
|
||||
export const SecretSyncDestinationFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm>();
|
||||
@@ -76,6 +78,10 @@ export const SecretSyncDestinationFields = () => {
|
||||
return <GitLabSyncFields />;
|
||||
case SecretSync.CloudflarePages:
|
||||
return <CloudflarePagesSyncFields />;
|
||||
case SecretSync.Zabbix:
|
||||
return <ZabbixSyncFields />;
|
||||
case SecretSync.Railway:
|
||||
return <RailwaySyncFields />;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Config Field: ${destination}`);
|
||||
}
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
|
||||
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
|
||||
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
TZabbixHost,
|
||||
useZabbixConnectionListHosts,
|
||||
ZABBIX_SYNC_SCOPES,
|
||||
ZabbixMacroType,
|
||||
ZabbixSyncScope
|
||||
} from "@app/hooks/api/appConnections/zabbix";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
|
||||
export const ZabbixSyncFields = () => {
|
||||
const { control, watch, setValue } = useFormContext<
|
||||
TSecretSyncForm & { destination: SecretSync.Zabbix }
|
||||
>();
|
||||
|
||||
const connectionId = useWatch({ name: "connection.id", control });
|
||||
const currentScope = watch("destinationConfig.scope");
|
||||
|
||||
const { data: hosts = [], isPending: isHostsPending } = useZabbixConnectionListHosts(
|
||||
connectionId,
|
||||
{
|
||||
enabled: Boolean(connectionId)
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretSyncConnectionField
|
||||
onChange={() => {
|
||||
setValue("destinationConfig.scope", ZabbixSyncScope.Global);
|
||||
setValue("destinationConfig.hostId", "");
|
||||
setValue("destinationConfig.hostName", "");
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="destinationConfig.scope"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Scope"
|
||||
tooltipClassName="max-w-lg py-3"
|
||||
tooltipText={
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Specify how Infisical should manage secrets from Zabbix. The following options are
|
||||
available:
|
||||
</p>
|
||||
<ul className="flex list-disc flex-col gap-3 pl-4">
|
||||
{Object.values(ZABBIX_SYNC_SCOPES).map(({ name, description }) => {
|
||||
return (
|
||||
<li key={name}>
|
||||
<p className="text-mineshaft-300">
|
||||
<span className="font-medium text-bunker-200">{name}</span>: {description}
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => {
|
||||
onChange(val);
|
||||
setValue("destinationConfig.hostId", "");
|
||||
setValue("destinationConfig.hostName", "");
|
||||
}}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select a scope..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(ZabbixSyncScope).map((scope) => (
|
||||
<SelectItem className="capitalize" value={scope} key={scope}>
|
||||
{scope.replace("-", " ")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{currentScope === ZabbixSyncScope.Host && (
|
||||
<Controller
|
||||
name="destinationConfig.hostId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Host">
|
||||
<FilterableSelect
|
||||
menuPlacement="top"
|
||||
isLoading={isHostsPending && Boolean(connectionId)}
|
||||
isDisabled={!connectionId}
|
||||
value={hosts.find((host) => host.hostId === value) ?? null}
|
||||
onChange={(option) => {
|
||||
const selectedOption = option as SingleValue<TZabbixHost>;
|
||||
onChange(selectedOption?.hostId ?? null);
|
||||
|
||||
if (selectedOption) {
|
||||
setValue("destinationConfig.hostName", selectedOption.host);
|
||||
} else {
|
||||
setValue("destinationConfig.hostName", "");
|
||||
}
|
||||
}}
|
||||
options={hosts}
|
||||
placeholder="Select a host..."
|
||||
getOptionLabel={(option) => option.host}
|
||||
getOptionValue={(option) => option.hostId}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="destinationConfig.macroType"
|
||||
defaultValue={ZabbixMacroType.Secret}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} label="Macro Type">
|
||||
<Select
|
||||
value={String(value)}
|
||||
onValueChange={(val) => onChange(parseInt(val, 10))}
|
||||
className="w-full border border-mineshaft-500 capitalize"
|
||||
position="popper"
|
||||
placeholder="Select a macro type..."
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
<SelectItem value={String(ZabbixMacroType.Text)} key="text">
|
||||
Text
|
||||
</SelectItem>
|
||||
<SelectItem value={String(ZabbixMacroType.Secret)} key="secret">
|
||||
Secret
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+6
-2
@@ -23,6 +23,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
const { control, watch } = useFormContext<TSecretSyncForm>();
|
||||
|
||||
const destination = watch("destination");
|
||||
const currentSyncOption = watch("syncOptions");
|
||||
|
||||
const destinationName = SECRET_SYNC_MAP[destination].name;
|
||||
|
||||
@@ -57,6 +58,8 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
case SecretSync.Flyio:
|
||||
case SecretSync.GitLab:
|
||||
case SecretSync.CloudflarePages:
|
||||
case SecretSync.Zabbix:
|
||||
case SecretSync.Railway:
|
||||
AdditionalSyncOptionsFieldsComponent = null;
|
||||
break;
|
||||
default:
|
||||
@@ -127,8 +130,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
{!syncOption?.canImportSecrets && (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} />
|
||||
{destinationName} only supports overwriting destination secrets. Secrets not present
|
||||
in Infisical will be removed from the destination.
|
||||
{destinationName} only supports overwriting destination secrets.{" "}
|
||||
{!currentSyncOption.disableSecretDeletion &&
|
||||
"Secrets not present in Infisical will be removed from the destination."}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { useRailwayConnectionListProjects } from "@app/hooks/api/appConnections/railway";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const RailwaySyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Railway }>();
|
||||
const connectionId = watch("connection.id");
|
||||
const projectId = watch("destinationConfig.projectId");
|
||||
const environmentId = watch("destinationConfig.environmentId");
|
||||
|
||||
const { data: projects = [] } = useRailwayConnectionListProjects(connectionId, {
|
||||
enabled: Boolean(connectionId)
|
||||
});
|
||||
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
const environment = project?.environments.find((e) => e.id === environmentId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Project">{project?.name ?? projectId}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Environment">
|
||||
{environment?.name ?? environmentId}
|
||||
</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+8
@@ -30,11 +30,13 @@ import { HerokuSyncReviewFields } from "./HerokuSyncReviewFields";
|
||||
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
|
||||
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
|
||||
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
|
||||
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
|
||||
import { RenderSyncReviewFields } from "./RenderSyncReviewFields";
|
||||
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
|
||||
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
|
||||
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
|
||||
import { WindmillSyncReviewFields } from "./WindmillSyncReviewFields";
|
||||
import { ZabbixSyncReviewFields } from "./ZabbixSyncReviewFields";
|
||||
|
||||
export const SecretSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm>();
|
||||
@@ -124,6 +126,12 @@ export const SecretSyncReviewFields = () => {
|
||||
case SecretSync.CloudflarePages:
|
||||
DestinationFieldsComponent = <CloudflarePagesSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.Zabbix:
|
||||
DestinationFieldsComponent = <ZabbixSyncReviewFields />;
|
||||
break;
|
||||
case SecretSync.Railway:
|
||||
DestinationFieldsComponent = <RailwaySyncReviewFields />;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
|
||||
import { GenericFieldLabel } from "@app/components/v2";
|
||||
import { ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
const isTextMacro = (macroType: number) => macroType === 0;
|
||||
|
||||
export const ZabbixSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Zabbix }>();
|
||||
const scope = watch("destinationConfig.scope");
|
||||
const hostId = watch("destinationConfig.hostId");
|
||||
const hostName = watch("destinationConfig.hostName");
|
||||
const macroType = watch("destinationConfig.macroType");
|
||||
|
||||
return (
|
||||
<>
|
||||
<GenericFieldLabel label="Scope">{scope}</GenericFieldLabel>
|
||||
{scope === ZabbixSyncScope.Host && (
|
||||
<>
|
||||
<GenericFieldLabel label="Host ID">{hostId}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Host Name">{hostName}</GenericFieldLabel>
|
||||
</>
|
||||
)}
|
||||
<GenericFieldLabel label="Macro Type">
|
||||
{isTextMacro(macroType) ? "Text" : "Secret"}
|
||||
</GenericFieldLabel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const RailwaySyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.Railway),
|
||||
destinationConfig: z.object({
|
||||
projectId: z.string().min(1, "Project ID is required"),
|
||||
projectName: z.string(),
|
||||
environmentName: z.string(),
|
||||
environmentId: z.string().min(1, "Environment is required"),
|
||||
serviceId: z.string().optional(),
|
||||
serviceName: z.string().optional()
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -17,11 +17,13 @@ import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema
|
||||
import { HerokuSyncDestinationSchema } from "./heroku-sync-destination-schema";
|
||||
import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema";
|
||||
import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema";
|
||||
import { RailwaySyncDestinationSchema } from "./railway-sync-destination-schema";
|
||||
import { RenderSyncDestinationSchema } from "./render-sync-destination-schema";
|
||||
import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema";
|
||||
import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema";
|
||||
import { VercelSyncDestinationSchema } from "./vercel-sync-destination-schema";
|
||||
import { WindmillSyncDestinationSchema } from "./windmill-sync-destination-schema";
|
||||
import { ZabbixSyncDestinationSchema } from "./zabbix-sync-destination-schema";
|
||||
|
||||
const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncDestinationSchema,
|
||||
@@ -45,7 +47,9 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
|
||||
RenderSyncDestinationSchema,
|
||||
FlyioSyncDestinationSchema,
|
||||
GitlabSyncDestinationSchema,
|
||||
CloudflarePagesSyncDestinationSchema
|
||||
CloudflarePagesSyncDestinationSchema,
|
||||
ZabbixSyncDestinationSchema,
|
||||
RailwaySyncDestinationSchema
|
||||
]);
|
||||
|
||||
export const SecretSyncFormSchema = SecretSyncUnionSchema;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
|
||||
import { ZabbixMacroType, ZabbixSyncScope } from "@app/hooks/api/appConnections/zabbix";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
export const ZabbixSyncDestinationSchema = BaseSecretSyncSchema().merge(
|
||||
z.object({
|
||||
destination: z.literal(SecretSync.Zabbix),
|
||||
destinationConfig: z.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(ZabbixSyncScope.Host),
|
||||
hostId: z.string().trim().min(1, "Host ID required"),
|
||||
hostName: z.string().trim().min(1, "Host name required"),
|
||||
macroType: z.nativeEnum(ZabbixMacroType, {
|
||||
errorMap: () => ({ message: "Macro type must be either 'text' or 'secret'" })
|
||||
})
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(ZabbixSyncScope.Global),
|
||||
macroType: z.nativeEnum(ZabbixMacroType, {
|
||||
errorMap: () => ({ message: "Macro type must be either 'text' or 'secret'" })
|
||||
})
|
||||
})
|
||||
])
|
||||
})
|
||||
);
|
||||
@@ -77,3 +77,91 @@ export const parseJson = (src: ArrayBuffer | string) => {
|
||||
});
|
||||
return env;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses simple flat YAML with support for multiline strings using |, |-, and >.
|
||||
* @param {ArrayBuffer | string} src
|
||||
* @returns {Record<string, { value: string, comments: string[] }>}
|
||||
*/
|
||||
export function parseYaml(src: ArrayBuffer | string) {
|
||||
const result: Record<string, { value: string; comments: string[] }> = {};
|
||||
|
||||
const content = src.toString().replace(/\r\n?/g, "\n");
|
||||
const lines = content.split("\n");
|
||||
|
||||
let i = 0;
|
||||
let comments: string[] = [];
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// Collect comment
|
||||
if (line.startsWith("#")) {
|
||||
comments.push(line.slice(1).trim());
|
||||
i += 1; // move to next line
|
||||
} else {
|
||||
// Match key: value or key: |, key: >, etc.
|
||||
const keyMatch = lines[i].match(/^([\w.-]+)\s*:\s*(.*)$/);
|
||||
if (keyMatch) {
|
||||
const [, key, rawValue] = keyMatch;
|
||||
let value = rawValue.trim();
|
||||
|
||||
// Multiline string handling
|
||||
if (value === "|-" || value === "|" || value === ">") {
|
||||
const isFolded = value === ">";
|
||||
|
||||
const baseIndent = lines[i + 1]?.match(/^(\s*)/)?.[1]?.length ?? 0;
|
||||
const collectedLines: string[] = [];
|
||||
|
||||
i += 1; // move to first content line
|
||||
|
||||
while (i < lines.length) {
|
||||
const current = lines[i];
|
||||
const currentIndent = current.match(/^(\s*)/)?.[1]?.length ?? 0;
|
||||
|
||||
if (current.trim() === "" || currentIndent >= baseIndent) {
|
||||
collectedLines.push(current.slice(baseIndent));
|
||||
i += 1; // move to next line
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFolded) {
|
||||
// Join lines with space for `>` folded style
|
||||
value = collectedLines.map((l) => l.trim()).join(" ");
|
||||
} else {
|
||||
// Keep lines with newlines for `|` and `|-`
|
||||
value = collectedLines.join("\n");
|
||||
}
|
||||
} else {
|
||||
// Inline value — strip quotes and inline comment
|
||||
const commentIndex = value.indexOf(" #");
|
||||
if (commentIndex !== -1) {
|
||||
value = value.slice(0, commentIndex).trim();
|
||||
}
|
||||
|
||||
// Remove surrounding quotes
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
i += 1; // advance to next line
|
||||
}
|
||||
|
||||
result[key] = {
|
||||
value,
|
||||
comments: [...comments]
|
||||
};
|
||||
comments = []; // reset
|
||||
} else {
|
||||
i += 1; // skip unknown line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ContentLoader = ({ text, frequency = 2000, className }: Props) => {
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Lottie icon="infisical_loading" className="h-32 w-32" />
|
||||
<Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
|
||||
{text && isTextArray && (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { MouseEventHandler } from "react";
|
||||
|
||||
export const HeaderResizer = ({
|
||||
onMouseDown,
|
||||
isActive,
|
||||
scrollOffset,
|
||||
heightOffset
|
||||
}: {
|
||||
onMouseDown: MouseEventHandler<HTMLDivElement>;
|
||||
isActive: boolean;
|
||||
scrollOffset: number;
|
||||
heightOffset: number;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
tabIndex={-1}
|
||||
role="button"
|
||||
className={`absolute left-0 z-40 h-0.5 w-full cursor-ns-resize hover:bg-blue-400/20 ${
|
||||
isActive ? "bg-blue-400/75" : "bg-transparent"
|
||||
}`}
|
||||
onMouseDown={onMouseDown}
|
||||
style={{
|
||||
transform: "translateY(50%)",
|
||||
top: heightOffset
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{ left: `calc(50% + ${scrollOffset}px)`, top: heightOffset }}
|
||||
className="pointer-events-none absolute z-30 -translate-x-1/2"
|
||||
>
|
||||
<div className="h-1 w-8 rounded bg-gray-400 opacity-50" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
export const HighlightText = ({
|
||||
text,
|
||||
highlight,
|
||||
highlightClassName
|
||||
}: {
|
||||
text: string | undefined | null;
|
||||
highlight: string;
|
||||
highlightClassName?: string;
|
||||
}) => {
|
||||
if (!text) return null;
|
||||
const searchTerm = highlight.toLowerCase().trim();
|
||||
|
||||
if (!searchTerm) return <span>{text}</span>;
|
||||
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(escapedSearchTerm, "gi");
|
||||
|
||||
text.replace(regex, (match: string, offset: number) => {
|
||||
if (offset > lastIndex) {
|
||||
parts.push(<span key={`pre-${lastIndex}`}>{text.substring(lastIndex, offset)}</span>);
|
||||
}
|
||||
|
||||
parts.push(
|
||||
<span key={`match-${offset}`} className={highlightClassName || "bg-yellow/30"}>
|
||||
{match}
|
||||
</span>
|
||||
);
|
||||
|
||||
lastIndex = offset + match.length;
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(<span key={`post-${lastIndex}`}>{text.substring(lastIndex)}</span>);
|
||||
}
|
||||
|
||||
return parts;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { HighlightText } from "./HighlightText";
|
||||
@@ -45,10 +45,14 @@ export const Table = ({ children, className }: TableProps): JSX.Element => (
|
||||
export type THeadProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export const THead = ({ children, className }: THeadProps): JSX.Element => (
|
||||
<thead className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}>
|
||||
export const THead = ({ children, className, style }: THeadProps): JSX.Element => (
|
||||
<thead
|
||||
className={twMerge("bg-mineshaft-800 text-xs uppercase text-bunker-300", className)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
@@ -96,14 +100,16 @@ export const Tr = ({
|
||||
export type ThProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export const Th = ({ children, className }: ThProps): JSX.Element => (
|
||||
export const Th = ({ children, className, style }: ThProps): JSX.Element => (
|
||||
<th
|
||||
className={twMerge(
|
||||
"border-b-2 border-mineshaft-600 bg-mineshaft-800 px-5 pb-3.5 pt-4 font-semibold",
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
|
||||
@@ -43,23 +43,25 @@ export const Tooltip = ({
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
|
||||
<TooltipPrimitive.Content
|
||||
side={position}
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"z-50 max-w-[15rem] select-none border border-mineshaft-600 bg-mineshaft-800 font-light text-bunker-200 shadow-md data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade",
|
||||
isDisabled && "!hidden",
|
||||
center && "text-center",
|
||||
size === "sm" && "rounded-sm px-2 py-1 text-xs",
|
||||
size === "md" && "rounded-md px-4 py-2 text-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
|
||||
</TooltipPrimitive.Content>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
side={position}
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"z-50 max-w-[15rem] select-none border border-mineshaft-600 bg-mineshaft-800 font-light text-bunker-200 shadow-md data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade",
|
||||
isDisabled && "!hidden",
|
||||
center && "text-center",
|
||||
size === "sm" && "rounded-sm px-2 py-1 text-xs",
|
||||
size === "md" && "rounded-md px-4 py-2 text-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-600" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
</TooltipPrimitive.Root>
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
|
||||
@@ -26,6 +26,7 @@ export const envConfig = {
|
||||
import.meta.env.VITE_TELEMETRY_CAPTURING_ENABLED === true
|
||||
);
|
||||
},
|
||||
|
||||
get PLATFORM_VERSION() {
|
||||
return import.meta.env.VITE_INFISICAL_PLATFORM_VERSION;
|
||||
}
|
||||
|
||||
@@ -37,10 +37,13 @@ import {
|
||||
TeamCityConnectionMethod,
|
||||
TerraformCloudConnectionMethod,
|
||||
VercelConnectionMethod,
|
||||
WindmillConnectionMethod
|
||||
WindmillConnectionMethod,
|
||||
ZabbixConnectionMethod
|
||||
} from "@app/hooks/api/appConnections/types";
|
||||
import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection";
|
||||
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
|
||||
import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection";
|
||||
import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection";
|
||||
import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection";
|
||||
|
||||
export const APP_CONNECTION_MAP: Record<
|
||||
@@ -88,7 +91,10 @@ export const APP_CONNECTION_MAP: Record<
|
||||
[AppConnection.Render]: { name: "Render", image: "Render.png" },
|
||||
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
|
||||
[AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" },
|
||||
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" }
|
||||
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
|
||||
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
|
||||
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
||||
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }
|
||||
};
|
||||
|
||||
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||
@@ -120,6 +126,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
case VercelConnectionMethod.ApiToken:
|
||||
case OnePassConnectionMethod.ApiToken:
|
||||
case CloudflareConnectionMethod.ApiToken:
|
||||
case BitbucketConnectionMethod.ApiToken:
|
||||
case ZabbixConnectionMethod.ApiToken:
|
||||
return { name: "API Token", icon: faKey };
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
@@ -140,6 +148,12 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
return { name: "Simple Bind", icon: faLink };
|
||||
case HerokuConnectionMethod.AuthToken:
|
||||
return { name: "Auth Token", icon: faKey };
|
||||
case RailwayConnectionMethod.AccountToken:
|
||||
return { name: "Account Token", icon: faKey };
|
||||
case RailwayConnectionMethod.TeamToken:
|
||||
return { name: "Team Token", icon: faKey };
|
||||
case RailwayConnectionMethod.ProjectToken:
|
||||
return { name: "Project Token", icon: faKey };
|
||||
case RenderConnectionMethod.ApiKey:
|
||||
return { name: "API Key", icon: faKey };
|
||||
default:
|
||||
|
||||
@@ -19,6 +19,11 @@ export const SECRET_SCANNING_DATA_SOURCE_MAP: Record<
|
||||
name: "GitHub",
|
||||
image: "GitHub.png",
|
||||
size: 45
|
||||
},
|
||||
[SecretScanningDataSource.Bitbucket]: {
|
||||
name: "Bitbucket",
|
||||
image: "Bitbucket.png",
|
||||
size: 45
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,7 +31,8 @@ export const SECRET_SCANNING_DATA_SOURCE_CONNECTION_MAP: Record<
|
||||
SecretScanningDataSource,
|
||||
AppConnection
|
||||
> = {
|
||||
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar
|
||||
[SecretScanningDataSource.GitHub]: AppConnection.GitHubRadar,
|
||||
[SecretScanningDataSource.Bitbucket]: AppConnection.Bitbucket
|
||||
};
|
||||
|
||||
export const RESOURCE_DESCRIPTION_HELPER: Record<
|
||||
@@ -45,6 +51,13 @@ export const RESOURCE_DESCRIPTION_HELPER: Record<
|
||||
singularNoun: "repository",
|
||||
pluralTitle: "Repositories",
|
||||
singularTitle: "Repository"
|
||||
},
|
||||
[SecretScanningDataSource.Bitbucket]: {
|
||||
verb: "push",
|
||||
pluralNoun: "repositories",
|
||||
singularNoun: "repository",
|
||||
pluralTitle: "Repositories",
|
||||
singularTitle: "Repository"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -81,6 +81,14 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
|
||||
[SecretSync.CloudflarePages]: {
|
||||
name: "Cloudflare Pages",
|
||||
image: "Cloudflare.png"
|
||||
},
|
||||
[SecretSync.Zabbix]: {
|
||||
name: "Zabbix",
|
||||
image: "Zabbix.png"
|
||||
},
|
||||
[SecretSync.Railway]: {
|
||||
name: "Railway",
|
||||
image: "Railway.png"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,7 +114,9 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Render]: AppConnection.Render,
|
||||
[SecretSync.Flyio]: AppConnection.Flyio,
|
||||
[SecretSync.GitLab]: AppConnection.Gitlab,
|
||||
[SecretSync.CloudflarePages]: AppConnection.Cloudflare
|
||||
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
|
||||
[SecretSync.Zabbix]: AppConnection.Zabbix,
|
||||
[SecretSync.Railway]: AppConnection.Railway
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<
|
||||
|
||||
@@ -170,7 +170,7 @@ export type TCreateAccessPolicyDTO = {
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
approvals?: number;
|
||||
secretPath?: string;
|
||||
secretPath: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
allowedSelfApprovals: boolean;
|
||||
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AdminGetUsersFilters,
|
||||
AdminIntegrationsConfig,
|
||||
OrganizationWithProjects,
|
||||
TGetEnvOverrides,
|
||||
TGetInvalidatingCacheStatus,
|
||||
TGetServerRootKmsEncryptionDetails,
|
||||
TServerConfig
|
||||
@@ -31,7 +32,8 @@ export const adminQueryKeys = {
|
||||
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
|
||||
getInvalidateCache: () => ["admin-invalidate-cache"] as const,
|
||||
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const
|
||||
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
|
||||
getEnvOverrides: () => ["env-overrides"] as const
|
||||
};
|
||||
|
||||
export const fetchServerConfig = async () => {
|
||||
@@ -163,3 +165,13 @@ export const useGetInvalidatingCacheStatus = (enabled = true) => {
|
||||
refetchInterval: (data) => (data ? 3000 : false)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetEnvOverrides = () => {
|
||||
return useQuery({
|
||||
queryKey: adminQueryKeys.getEnvOverrides(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TGetEnvOverrides>("/api/v1/admin/env-overrides");
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ export type TServerConfig = {
|
||||
trustLdapEmails: boolean;
|
||||
trustOidcEmails: boolean;
|
||||
isSecretScanningDisabled: boolean;
|
||||
kubernetesAutoFetchServiceAccountToken: boolean;
|
||||
defaultAuthOrgSlug: string | null;
|
||||
defaultAuthOrgId: string | null;
|
||||
defaultAuthOrgAuthMethod?: string | null;
|
||||
@@ -48,6 +49,7 @@ export type TServerConfig = {
|
||||
authConsentContent?: string;
|
||||
pageFrameContent?: string;
|
||||
invalidatingCache: boolean;
|
||||
envOverrides?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type TUpdateServerConfigDTO = {
|
||||
@@ -61,6 +63,7 @@ export type TUpdateServerConfigDTO = {
|
||||
gitHubAppConnectionSlug?: string;
|
||||
gitHubAppConnectionId?: string;
|
||||
gitHubAppConnectionPrivateKey?: string;
|
||||
envOverrides?: Record<string, string>;
|
||||
} & Partial<TServerConfig>;
|
||||
|
||||
export type TCreateAdminUserDTO = {
|
||||
@@ -138,3 +141,10 @@ export type TInvalidateCacheDTO = {
|
||||
export type TGetInvalidatingCacheStatus = {
|
||||
invalidating: boolean;
|
||||
};
|
||||
|
||||
export interface TGetEnvOverrides {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
fields: { key: string; value: string; hasEnvEntry: boolean; description?: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import {
|
||||
TBitbucketConnectionListRepositoriesResponse,
|
||||
TBitbucketConnectionListWorkspacesResponse,
|
||||
TBitbucketRepo,
|
||||
TBitbucketWorkspace
|
||||
} from "./types";
|
||||
|
||||
const bitbucketConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "bitbucket"] as const,
|
||||
listRepos: (connectionId: string, workspaceSlug: string) =>
|
||||
[...bitbucketConnectionKeys.all, "repos", connectionId, workspaceSlug] as const,
|
||||
listWorkspaces: (connectionId: string) =>
|
||||
[...bitbucketConnectionKeys.all, "workspaces", connectionId] as const
|
||||
};
|
||||
|
||||
export const useBitbucketConnectionListWorkspaces = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TBitbucketWorkspace[],
|
||||
unknown,
|
||||
TBitbucketWorkspace[],
|
||||
ReturnType<typeof bitbucketConnectionKeys.listWorkspaces>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: bitbucketConnectionKeys.listWorkspaces(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TBitbucketConnectionListWorkspacesResponse>(
|
||||
`/api/v1/app-connections/bitbucket/${connectionId}/workspaces`
|
||||
);
|
||||
|
||||
return data.workspaces;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
export const useBitbucketConnectionListRepositories = (
|
||||
connectionId: string,
|
||||
workspaceSlug: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TBitbucketRepo[],
|
||||
unknown,
|
||||
TBitbucketRepo[],
|
||||
ReturnType<typeof bitbucketConnectionKeys.listRepos>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: bitbucketConnectionKeys.listRepos(connectionId, workspaceSlug),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TBitbucketConnectionListRepositoriesResponse>(
|
||||
`/api/v1/app-connections/bitbucket/${connectionId}/repositories?workspaceSlug=${encodeURIComponent(workspaceSlug)}`
|
||||
);
|
||||
|
||||
return data.repositories;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
export type TBitbucketWorkspace = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type TBitbucketRepo = {
|
||||
uuid: string;
|
||||
slug: string;
|
||||
full_name: string; // workspace-slug/repo-slug
|
||||
};
|
||||
|
||||
export type TBitbucketConnectionListWorkspacesResponse = {
|
||||
workspaces: TBitbucketWorkspace[];
|
||||
};
|
||||
|
||||
export type TBitbucketConnectionListRepositoriesResponse = {
|
||||
repositories: TBitbucketRepo[];
|
||||
};
|
||||
@@ -27,5 +27,8 @@ export enum AppConnection {
|
||||
Render = "render",
|
||||
Flyio = "flyio",
|
||||
Gitlab = "gitlab",
|
||||
Cloudflare = "cloudflare"
|
||||
Cloudflare = "cloudflare",
|
||||
Bitbucket = "bitbucket",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { appConnectionKeys } from "@app/hooks/api/appConnections";
|
||||
|
||||
import { TRailwayProject } from "./types";
|
||||
|
||||
const railwayConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "railway"] as const,
|
||||
listSecretScopes: (connectionId: string) =>
|
||||
[...railwayConnectionKeys.all, "workspace-scopes", connectionId] as const
|
||||
};
|
||||
|
||||
export const useRailwayConnectionListProjects = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TRailwayProject[],
|
||||
unknown,
|
||||
TRailwayProject[],
|
||||
ReturnType<typeof railwayConnectionKeys.listSecretScopes>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: railwayConnectionKeys.listSecretScopes(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ projects: TRailwayProject[] }>(
|
||||
`/api/v1/app-connections/railway/${connectionId}/projects`
|
||||
);
|
||||
|
||||
return data.projects;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export type TRailwayProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
environments: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
services: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
@@ -132,6 +132,18 @@ export type TCloudflareConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Cloudflare;
|
||||
};
|
||||
|
||||
export type TBitbucketConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Bitbucket;
|
||||
};
|
||||
|
||||
export type TZabbixConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Zabbix;
|
||||
};
|
||||
|
||||
export type TRailwayConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Railway;
|
||||
};
|
||||
|
||||
export type TAppConnectionOption =
|
||||
| TAwsConnectionOption
|
||||
| TGitHubConnectionOption
|
||||
@@ -159,7 +171,10 @@ export type TAppConnectionOption =
|
||||
| TRenderConnectionOption
|
||||
| TFlyioConnectionOption
|
||||
| TGitlabConnectionOption
|
||||
| TCloudflareConnectionOption;
|
||||
| TCloudflareConnectionOption
|
||||
| TBitbucketConnectionOption
|
||||
| TZabbixConnectionOption
|
||||
| TRailwayConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
@@ -191,4 +206,7 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.Flyio]: TFlyioConnectionOption;
|
||||
[AppConnection.Gitlab]: TGitlabConnectionOption;
|
||||
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
||||
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
||||
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
||||
[AppConnection.Railway]: TRailwayConnectionOption;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum BitbucketConnectionMethod {
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
|
||||
export type TBitbucketConnection = TRootAppConnection & { app: AppConnection.Bitbucket } & {
|
||||
method: BitbucketConnectionMethod.ApiToken;
|
||||
credentials: {
|
||||
email: string;
|
||||
apiToken: string;
|
||||
};
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { TAzureAppConfigurationConnection } from "./azure-app-configuration-conn
|
||||
import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection";
|
||||
import { TAzureDevOpsConnection } from "./azure-devops-connection";
|
||||
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
|
||||
import { TBitbucketConnection } from "./bitbucket-connection";
|
||||
import { TCamundaConnection } from "./camunda-connection";
|
||||
import { TCloudflareConnection } from "./cloudflare-connection";
|
||||
import { TDatabricksConnection } from "./databricks-connection";
|
||||
@@ -24,11 +25,13 @@ import { TMySqlConnection } from "./mysql-connection";
|
||||
import { TOCIConnection } from "./oci-connection";
|
||||
import { TOracleDBConnection } from "./oracledb-connection";
|
||||
import { TPostgresConnection } from "./postgres-connection";
|
||||
import { TRailwayConnection } from "./railway-connection";
|
||||
import { TRenderConnection } from "./render-connection";
|
||||
import { TTeamCityConnection } from "./teamcity-connection";
|
||||
import { TTerraformCloudConnection } from "./terraform-cloud-connection";
|
||||
import { TVercelConnection } from "./vercel-connection";
|
||||
import { TWindmillConnection } from "./windmill-connection";
|
||||
import { TZabbixConnection } from "./zabbix-connection";
|
||||
|
||||
export * from "./1password-connection";
|
||||
export * from "./auth0-connection";
|
||||
@@ -37,6 +40,7 @@ export * from "./azure-app-configuration-connection";
|
||||
export * from "./azure-client-secrets-connection";
|
||||
export * from "./azure-devops-connection";
|
||||
export * from "./azure-key-vault-connection";
|
||||
export * from "./bitbucket-connection";
|
||||
export * from "./camunda-connection";
|
||||
export * from "./cloudflare-connection";
|
||||
export * from "./databricks-connection";
|
||||
@@ -59,6 +63,7 @@ export * from "./teamcity-connection";
|
||||
export * from "./terraform-cloud-connection";
|
||||
export * from "./vercel-connection";
|
||||
export * from "./windmill-connection";
|
||||
export * from "./zabbix-connection";
|
||||
|
||||
export type TAppConnection =
|
||||
| TAwsConnection
|
||||
@@ -89,7 +94,10 @@ export type TAppConnection =
|
||||
| TRenderConnection
|
||||
| TFlyioConnection
|
||||
| TGitLabConnection
|
||||
| TCloudflareConnection;
|
||||
| TCloudflareConnection
|
||||
| TBitbucketConnection
|
||||
| TZabbixConnection
|
||||
| TRailwayConnection;
|
||||
|
||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
|
||||
|
||||
@@ -146,4 +154,7 @@ export type TAppConnectionMap = {
|
||||
[AppConnection.Flyio]: TFlyioConnection;
|
||||
[AppConnection.Gitlab]: TGitLabConnection;
|
||||
[AppConnection.Cloudflare]: TCloudflareConnection;
|
||||
[AppConnection.Bitbucket]: TBitbucketConnection;
|
||||
[AppConnection.Zabbix]: TZabbixConnection;
|
||||
[AppConnection.Railway]: TRailwayConnection;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum RailwayConnectionMethod {
|
||||
AccountToken = "account-token",
|
||||
ProjectToken = "project-token",
|
||||
TeamToken = "team-token"
|
||||
}
|
||||
|
||||
export type TRailwayConnection = TRootAppConnection & { app: AppConnection.Railway } & {
|
||||
method: RailwayConnectionMethod;
|
||||
credentials: {
|
||||
apiToken: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum ZabbixConnectionMethod {
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
|
||||
export type TZabbixConnection = TRootAppConnection & { app: AppConnection.Zabbix } & {
|
||||
method: ZabbixConnectionMethod.ApiToken;
|
||||
credentials: {
|
||||
apiToken: string;
|
||||
instanceUrl: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TZabbixHost } from "./types";
|
||||
|
||||
const zabbixConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "zabbix"] as const,
|
||||
listHosts: (connectionId: string) => [...zabbixConnectionKeys.all, "hosts", connectionId] as const
|
||||
};
|
||||
|
||||
export const useZabbixConnectionListHosts = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TZabbixHost[],
|
||||
unknown,
|
||||
TZabbixHost[],
|
||||
ReturnType<typeof zabbixConnectionKeys.listHosts>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: zabbixConnectionKeys.listHosts(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TZabbixHost[]>(
|
||||
`/api/v1/app-connections/zabbix/${connectionId}/hosts`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
export type TZabbixHost = {
|
||||
host: string;
|
||||
hostId: string;
|
||||
};
|
||||
|
||||
export enum ZabbixSyncScope {
|
||||
Host = "host",
|
||||
Global = "global"
|
||||
}
|
||||
|
||||
export enum ZabbixMacroType {
|
||||
Text = 0,
|
||||
Secret = 1
|
||||
}
|
||||
|
||||
export const ZABBIX_SYNC_SCOPES = {
|
||||
[ZabbixSyncScope.Host]: {
|
||||
name: "Host",
|
||||
description: "Sync secrets to a specific host in Zabbix."
|
||||
},
|
||||
[ZabbixSyncScope.Global]: {
|
||||
name: "Global",
|
||||
description: "Sync secrets to a global scope in Zabbix."
|
||||
}
|
||||
};
|
||||
@@ -30,8 +30,8 @@ export const useGetAuditLogs = (
|
||||
params: {
|
||||
...filters,
|
||||
offset: pageParam,
|
||||
startDate: filters?.startDate?.toISOString(),
|
||||
endDate: filters?.endDate?.toISOString(),
|
||||
startDate: filters.startDate.toISOString(),
|
||||
endDate: filters.endDate.toISOString(),
|
||||
...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
|
||||
? {
|
||||
eventMetadata: Object.entries(filters.eventMetadata)
|
||||
|
||||
@@ -14,8 +14,8 @@ export type TGetAuditLogsFilter = {
|
||||
actor?: string; // user ID format
|
||||
secretPath?: string;
|
||||
secretKey?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
limit: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ export enum SqlProviders {
|
||||
|
||||
export enum DynamicSecretAwsIamAuth {
|
||||
AssumeRole = "assume-role",
|
||||
AccessKey = "access-key"
|
||||
AccessKey = "access-key",
|
||||
IRSA = "irsa"
|
||||
}
|
||||
|
||||
export type TDynamicSecretProvider =
|
||||
@@ -111,6 +112,14 @@ export type TDynamicSecretProvider =
|
||||
policyDocument?: string;
|
||||
userGroups?: string;
|
||||
policyArns?: string;
|
||||
}
|
||||
| {
|
||||
method: DynamicSecretAwsIamAuth.IRSA;
|
||||
region: string;
|
||||
awsPath?: string;
|
||||
policyDocument?: string;
|
||||
userGroups?: string;
|
||||
policyArns?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -4,7 +4,7 @@ export {
|
||||
useDeleteIntegrationAuth,
|
||||
useDeleteIntegrationAuths,
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthBitBucketWorkspaces,
|
||||
useGetIntegrationAuthBitbucketWorkspaces,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthChecklyGroups,
|
||||
useGetIntegrationAuthCircleCIOrganizations,
|
||||
|
||||
@@ -6,8 +6,8 @@ import { TReactQueryOptions } from "@app/types/reactQuery";
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import {
|
||||
App,
|
||||
BitBucketEnvironment,
|
||||
BitBucketWorkspace,
|
||||
BitbucketEnvironment,
|
||||
BitbucketWorkspace,
|
||||
ChecklyGroup,
|
||||
CircleCIOrganization,
|
||||
Environment,
|
||||
@@ -98,9 +98,9 @@ const integrationAuthKeys = {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
|
||||
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
|
||||
getIntegrationAuthBitbucketWorkspaces: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const,
|
||||
getIntegrationAuthBitBucketEnvironments: (
|
||||
getIntegrationAuthBitbucketEnvironments: (
|
||||
integrationAuthId: string,
|
||||
workspaceSlug: string,
|
||||
repoSlug: string
|
||||
@@ -446,23 +446,23 @@ const fetchIntegrationAuthRailwayServices = async ({
|
||||
return services;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string) => {
|
||||
const fetchIntegrationAuthBitbucketWorkspaces = async (integrationAuthId: string) => {
|
||||
const {
|
||||
data: { workspaces }
|
||||
} = await apiRequest.get<{ workspaces: BitBucketWorkspace[] }>(
|
||||
} = await apiRequest.get<{ workspaces: BitbucketWorkspace[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/workspaces`
|
||||
);
|
||||
return workspaces;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthBitBucketEnvironments = async (
|
||||
const fetchIntegrationAuthBitbucketEnvironments = async (
|
||||
integrationAuthId: string,
|
||||
workspaceSlug: string,
|
||||
repoSlug: string
|
||||
) => {
|
||||
const {
|
||||
data: { environments }
|
||||
} = await apiRequest.get<{ environments: BitBucketEnvironment[] }>(
|
||||
} = await apiRequest.get<{ environments: BitbucketEnvironment[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/environments`,
|
||||
{
|
||||
params: {
|
||||
@@ -833,10 +833,10 @@ export const useGetIntegrationAuthRailwayServices = ({
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: string) => {
|
||||
export const useGetIntegrationAuthBitbucketWorkspaces = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketWorkspaces(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthBitBucketWorkspaces(integrationAuthId),
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthBitbucketWorkspaces(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthBitbucketWorkspaces(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
@@ -858,7 +858,7 @@ export const useGetIntegrationAuthOctopusDeployScopeValues = (
|
||||
...options
|
||||
});
|
||||
|
||||
export const useGetIntegrationAuthBitBucketEnvironments = (
|
||||
export const useGetIntegrationAuthBitbucketEnvironments = (
|
||||
{
|
||||
integrationAuthId,
|
||||
workspaceSlug,
|
||||
@@ -871,13 +871,13 @@ export const useGetIntegrationAuthBitBucketEnvironments = (
|
||||
options?: TReactQueryOptions["options"]
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketEnvironments(
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthBitbucketEnvironments(
|
||||
integrationAuthId,
|
||||
workspaceSlug,
|
||||
repoSlug
|
||||
),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthBitBucketEnvironments(integrationAuthId, workspaceSlug, repoSlug),
|
||||
fetchIntegrationAuthBitbucketEnvironments(integrationAuthId, workspaceSlug, repoSlug),
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -78,13 +78,13 @@ export type Service = {
|
||||
serviceId: string;
|
||||
};
|
||||
|
||||
export type BitBucketWorkspace = {
|
||||
export type BitbucketWorkspace = {
|
||||
uuid: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type BitBucketEnvironment = {
|
||||
export type BitbucketEnvironment = {
|
||||
uuid: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
|
||||
@@ -21,7 +21,7 @@ export const useUpdateOIDCConfig = () => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
orgSlug,
|
||||
organizationId,
|
||||
manageGroupMemberships,
|
||||
jwtSignatureAlgorithm
|
||||
}: {
|
||||
@@ -36,7 +36,7 @@ export const useUpdateOIDCConfig = () => {
|
||||
clientSecret?: string;
|
||||
isActive?: boolean;
|
||||
configurationType?: string;
|
||||
orgSlug: string;
|
||||
organizationId: string;
|
||||
manageGroupMemberships?: boolean;
|
||||
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
|
||||
}) => {
|
||||
@@ -50,7 +50,7 @@ export const useUpdateOIDCConfig = () => {
|
||||
tokenEndpoint,
|
||||
userinfoEndpoint,
|
||||
clientId,
|
||||
orgSlug,
|
||||
organizationId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
manageGroupMemberships,
|
||||
@@ -60,7 +60,7 @@ export const useUpdateOIDCConfig = () => {
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, dto) {
|
||||
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) });
|
||||
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
|
||||
queryClient.invalidateQueries({ queryKey: organizationKeys.getUserOrganizations });
|
||||
}
|
||||
});
|
||||
@@ -81,7 +81,7 @@ export const useCreateOIDCConfig = () => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
orgSlug,
|
||||
organizationId,
|
||||
manageGroupMemberships,
|
||||
jwtSignatureAlgorithm
|
||||
}: {
|
||||
@@ -95,7 +95,7 @@ export const useCreateOIDCConfig = () => {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
isActive: boolean;
|
||||
orgSlug: string;
|
||||
organizationId: string;
|
||||
allowedEmailDomains?: string;
|
||||
manageGroupMemberships?: boolean;
|
||||
jwtSignatureAlgorithm?: OIDCJWTSignatureAlgorithm;
|
||||
@@ -112,7 +112,7 @@ export const useCreateOIDCConfig = () => {
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive,
|
||||
orgSlug,
|
||||
organizationId,
|
||||
manageGroupMemberships,
|
||||
jwtSignatureAlgorithm
|
||||
});
|
||||
@@ -120,7 +120,7 @@ export const useCreateOIDCConfig = () => {
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, dto) {
|
||||
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.orgSlug) });
|
||||
queryClient.invalidateQueries({ queryKey: oidcConfigKeys.getOIDCConfig(dto.organizationId) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,18 +5,18 @@ import { apiRequest } from "@app/config/request";
|
||||
import { OIDCConfigData } from "./types";
|
||||
|
||||
export const oidcConfigKeys = {
|
||||
getOIDCConfig: (orgSlug: string) => [{ orgSlug }, "organization-oidc"] as const,
|
||||
getOIDCConfig: (orgId: string) => [{ orgId }, "organization-oidc"] as const,
|
||||
getOIDCManageGroupMembershipsEnabled: (orgId: string) =>
|
||||
["oidc-manage-group-memberships", orgId] as const
|
||||
};
|
||||
|
||||
export const useGetOIDCConfig = (orgSlug: string) => {
|
||||
export const useGetOIDCConfig = (orgId: string) => {
|
||||
return useQuery({
|
||||
queryKey: oidcConfigKeys.getOIDCConfig(orgSlug),
|
||||
queryKey: oidcConfigKeys.getOIDCConfig(orgId),
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const { data } = await apiRequest.get<OIDCConfigData>(
|
||||
`/api/v1/sso/oidc/config?orgSlug=${orgSlug}`
|
||||
`/api/v1/sso/oidc/config?organizationId=${orgId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
|
||||
@@ -49,7 +49,7 @@ export type TCreateSecretPolicyDTO = {
|
||||
workspaceId: string;
|
||||
name?: string;
|
||||
environment: string;
|
||||
secretPath?: string | null;
|
||||
secretPath: string;
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
approvals?: number;
|
||||
@@ -62,7 +62,7 @@ export type TUpdateSecretPolicyDTO = {
|
||||
name?: string;
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
secretPath?: string | null;
|
||||
secretPath?: string;
|
||||
approvals?: number;
|
||||
allowedSelfApprovals?: boolean;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export enum SecretScanningDataSource {
|
||||
GitHub = "github"
|
||||
GitHub = "github",
|
||||
Bitbucket = "bitbucket"
|
||||
}
|
||||
|
||||
export enum SecretScanningScanStatus {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import { SecretScanningDataSource } from "../enums";
|
||||
import { TSecretScanningDataSourceBase } from "./shared";
|
||||
|
||||
export type TBitbucketDataSource = TSecretScanningDataSourceBase & {
|
||||
type: SecretScanningDataSource.Bitbucket;
|
||||
config: {
|
||||
workspaceSlug: string;
|
||||
includeRepos: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TBitbucketDataSourceOption = {
|
||||
name: string;
|
||||
type: SecretScanningDataSource.Bitbucket;
|
||||
connection: AppConnection.Bitbucket;
|
||||
};
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
SecretScanningScanStatus,
|
||||
SecretScanningScanType
|
||||
} from "../enums";
|
||||
import { TBitbucketDataSource, TBitbucketDataSourceOption } from "./bitbucket-data-source";
|
||||
import { TGitHubDataSource, TGitHubDataSourceOption } from "./github-data-source";
|
||||
|
||||
export type TSecretScanningDataSource = TGitHubDataSource;
|
||||
export type TSecretScanningDataSource = TGitHubDataSource | TBitbucketDataSource;
|
||||
|
||||
export type TSecretScanningDataSourceWithDetails = TSecretScanningDataSource & {
|
||||
lastScannedAt: string | null;
|
||||
@@ -23,7 +24,7 @@ export type TListSecretScanningDataSources = {
|
||||
dataSources: TSecretScanningDataSourceWithDetails[];
|
||||
};
|
||||
|
||||
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption;
|
||||
export type TSecretScanningDataSourceOption = TGitHubDataSourceOption | TBitbucketDataSourceOption;
|
||||
|
||||
export type TListSecretScanningDataSourceOptions = {
|
||||
dataSourceOptions: TSecretScanningDataSourceOption[];
|
||||
|
||||
@@ -20,7 +20,9 @@ export enum SecretSync {
|
||||
Render = "render",
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
CloudflarePages = "cloudflare-pages"
|
||||
CloudflarePages = "cloudflare-pages",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway"
|
||||
}
|
||||
|
||||
export enum SecretSyncStatus {
|
||||
|
||||
@@ -19,10 +19,12 @@ import { THCVaultSync } from "./hc-vault-sync";
|
||||
import { THerokuSync } from "./heroku-sync";
|
||||
import { THumanitecSync } from "./humanitec-sync";
|
||||
import { TOCIVaultSync } from "./oci-vault-sync";
|
||||
import { TRailwaySync } from "./railway-sync";
|
||||
import { TTeamCitySync } from "./teamcity-sync";
|
||||
import { TTerraformCloudSync } from "./terraform-cloud-sync";
|
||||
import { TVercelSync } from "./vercel-sync";
|
||||
import { TWindmillSync } from "./windmill-sync";
|
||||
import { TZabbixSync } from "./zabbix-sync";
|
||||
|
||||
export type TSecretSyncOption = {
|
||||
name: string;
|
||||
@@ -53,7 +55,9 @@ export type TSecretSync =
|
||||
| TRenderSync
|
||||
| TFlyioSync
|
||||
| TGitLabSync
|
||||
| TCloudflarePagesSync;
|
||||
| TCloudflarePagesSync
|
||||
| TZabbixSync
|
||||
| TRailwaySync;
|
||||
|
||||
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
export type TRailwaySync = TRootSecretSync & {
|
||||
destination: SecretSync.Railway;
|
||||
destinationConfig: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
|
||||
environmentName: string;
|
||||
environmentId: string;
|
||||
|
||||
serviceId?: string;
|
||||
serviceName?: string;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.Railway;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { SecretSync } from "@app/hooks/api/secretSyncs";
|
||||
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
|
||||
|
||||
import { ZabbixSyncScope } from "../../appConnections/zabbix";
|
||||
|
||||
export type TZabbixSync = TRootSecretSync & {
|
||||
destination: SecretSync.Zabbix;
|
||||
destinationConfig:
|
||||
| {
|
||||
scope: ZabbixSyncScope.Host;
|
||||
hostId: string;
|
||||
hostName: string;
|
||||
macroType: number;
|
||||
}
|
||||
| {
|
||||
scope: ZabbixSyncScope.Global;
|
||||
macroType: number;
|
||||
};
|
||||
connection: {
|
||||
app: AppConnection.Zabbix;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
@@ -5,6 +5,7 @@ export { usePagination } from "./usePagination";
|
||||
export { usePersistentState } from "./usePersistentState";
|
||||
export { usePopUp } from "./usePopUp";
|
||||
export { useResetPageHelper } from "./useResetPageHelper";
|
||||
export * from "./useResizableHeaderHeight";
|
||||
export { useSyntaxHighlight } from "./useSyntaxHighlight";
|
||||
export { useTimedReset } from "./useTimedReset";
|
||||
export { useToggle } from "./useToggle";
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
type Params = {
|
||||
minHeight: number;
|
||||
maxHeight: number;
|
||||
initialHeight: number;
|
||||
};
|
||||
|
||||
export const useResizableHeaderHeight = ({ minHeight, maxHeight, initialHeight }: Params) => {
|
||||
const [headerHeight, setHeaderHeight] = useState(initialHeight);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const startY = useRef(0);
|
||||
const startHeight = useRef(0);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsResizing(true);
|
||||
startY.current = e.clientY;
|
||||
startHeight.current = headerHeight;
|
||||
},
|
||||
[headerHeight]
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const deltaY = e.clientY - startY.current;
|
||||
const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight.current + deltaY));
|
||||
|
||||
setHeaderHeight(newHeight);
|
||||
},
|
||||
[isResizing]
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isResizing) {
|
||||
document.addEventListener(
|
||||
"mousemove",
|
||||
// @ts-expect-error native discrepancy
|
||||
handleMouseMove
|
||||
);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "ns-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"mousemove",
|
||||
// @ts-expect-error native discrepancy
|
||||
handleMouseMove
|
||||
);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
}, [isResizing, handleMouseMove, handleMouseUp]);
|
||||
|
||||
return {
|
||||
headerHeight,
|
||||
handleMouseDown,
|
||||
isResizing
|
||||
};
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useMatchRoute } from "@tanstack/react-router";
|
||||
|
||||
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||
|
||||
const generalTabs = [
|
||||
{
|
||||
@@ -29,6 +29,11 @@ const generalTabs = [
|
||||
label: "Caching",
|
||||
icon: "note",
|
||||
link: "/admin/caching"
|
||||
},
|
||||
{
|
||||
label: "Environment Variables",
|
||||
icon: "unlock",
|
||||
link: "/admin/environment"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -45,7 +50,7 @@ const resourceTabs = [
|
||||
},
|
||||
{
|
||||
label: "Machine Identities",
|
||||
icon: "key-user",
|
||||
icon: "wrench",
|
||||
link: "/admin/resources/machine-identities"
|
||||
}
|
||||
];
|
||||
@@ -56,30 +61,6 @@ export const AdminSidebar = () => {
|
||||
return (
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div className="flex-grow">
|
||||
<Menu>
|
||||
<MenuGroup title="General">
|
||||
{generalTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem isSelected={Boolean(isActive)}>{tab.label}</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Resources">
|
||||
{resourceTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem isSelected={Boolean(isActive)}>{tab.label}</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
@@ -96,6 +77,46 @@ export const AdminSidebar = () => {
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</Menu>
|
||||
<div className="flex-grow">
|
||||
<Menu>
|
||||
<MenuGroup title="General">
|
||||
{generalTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
leftIcon={
|
||||
<Lottie className="inline-block h-6 w-6 shrink-0" icon={tab.icon} />
|
||||
}
|
||||
isSelected={Boolean(isActive)}
|
||||
>
|
||||
{tab.label}
|
||||
</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MenuGroup>
|
||||
<MenuGroup title="Resources">
|
||||
{resourceTabs.map((tab) => {
|
||||
const isActive = matchRoute({ to: tab.link, fuzzy: false });
|
||||
return (
|
||||
<Link key={tab.link} to={tab.link}>
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden rounded-none"
|
||||
leftIcon={
|
||||
<Lottie className="inline-block h-6 w-6 shrink-0" icon={tab.icon} />
|
||||
}
|
||||
isSelected={Boolean(isActive)}
|
||||
>
|
||||
{tab.label}
|
||||
</MenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -54,33 +54,55 @@ const getPlan = (subscription: SubscriptionPlan) => {
|
||||
return "Free";
|
||||
};
|
||||
|
||||
const getFormattedSupportEmailLink = (variables: { org_id: string; domain: string }) => {
|
||||
const email = "[email protected]";
|
||||
|
||||
const body = `Hello Infisical Support Team,
|
||||
|
||||
Issue Details:
|
||||
[What you did]
|
||||
[What you expected to happen]
|
||||
[What actually happened]
|
||||
[Any error request IDs]
|
||||
[Any supporting screenshots or video recording of the issue/request at hand]
|
||||
|
||||
Account Info:
|
||||
- Organization ID: ${variables.org_id}
|
||||
- Domain: ${variables.domain}
|
||||
|
||||
Thank you,
|
||||
[Your Name]`;
|
||||
|
||||
return `mailto:${email}?body=${encodeURIComponent(body)}`;
|
||||
};
|
||||
|
||||
export const INFISICAL_SUPPORT_OPTIONS = [
|
||||
[
|
||||
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
|
||||
"Support Forum",
|
||||
"https://infisical.com/slack"
|
||||
() => "https://infisical.com/slack"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
|
||||
"Read Docs",
|
||||
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
() => "https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
|
||||
"GitHub Issues",
|
||||
"https://github.com/Infisical/infisical/issues"
|
||||
() => "https://github.com/Infisical/infisical/issues"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
|
||||
"Email Support",
|
||||
"mailto:[email protected]"
|
||||
getFormattedSupportEmailLink
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={5} className="pr-4 text-sm" icon={faUsers} />,
|
||||
"Instance Admins",
|
||||
"server-admins"
|
||||
() => "server-admins"
|
||||
]
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const Navbar = () => {
|
||||
const { user } = useUser();
|
||||
@@ -258,7 +280,15 @@ export const Navbar = () => {
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="bottom" className="mt-3 p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, getUrl]) => {
|
||||
const url =
|
||||
text === "Email Support"
|
||||
? getUrl({
|
||||
org_id: currentOrg.id,
|
||||
domain: window.location.origin
|
||||
})
|
||||
: getUrl();
|
||||
|
||||
if (url === "server-admins" && isInfisicalCloud()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faArrowLeft, faInfo, faMobile, faQuestion } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowLeft, faMobile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { WishForm } from "@app/components/features/WishForm";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/NavBar/Navbar";
|
||||
|
||||
export const PersonalSettingsLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -36,37 +28,6 @@ export const PersonalSettingsLayout = () => {
|
||||
<div className="relative mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faQuestion} className="mr-3 px-[0.1rem]" />
|
||||
Help & Support
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{envConfig.PLATFORM_VERSION && (
|
||||
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
|
||||
Version: {envConfig.PLATFORM_VERSION}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
</nav>
|
||||
|
||||
@@ -113,7 +113,7 @@ export const ProjectSelect = () => {
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faCube} className="text-xs" />
|
||||
</div>
|
||||
<Tooltip content={currentWorkspace.name}>
|
||||
<Tooltip content={currentWorkspace.name} className="max-w-96">
|
||||
<div className="max-w-32 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{currentWorkspace?.name}
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@ export const ProjectSelect = () => {
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-1 items-center justify-between overflow-hidden">
|
||||
<Tooltip content={workspace.name}>
|
||||
<Tooltip side="right" className="break-words" content={workspace.name}>
|
||||
<div className="max-w-40 overflow-hidden truncate whitespace-nowrap">
|
||||
{workspace.name}
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,7 @@ const router = createRouter({
|
||||
context: { serverConfig: null, queryClient },
|
||||
defaultPendingComponent: () => (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
|
||||
<Lottie icon="infisical_loading" className="h-32 w-32" />
|
||||
<Lottie isAutoPlay icon="infisical_loading" className="h-32 w-32" />
|
||||
</div>
|
||||
),
|
||||
defaultNotFoundComponent: NotFoundPage,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { EnvironmentPageForm } from "./components";
|
||||
|
||||
export const EnvironmentPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Admin" })}</title>
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Environment Variables"
|
||||
description="Manage the environment variables for your Infisical instance."
|
||||
/>
|
||||
<EnvironmentPageForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,264 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Control, Controller, useForm, useWatch } from "react-hook-form";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faChevronRight,
|
||||
faExclamationTriangle,
|
||||
faMagnifyingGlass
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, SecretInput, Tooltip } from "@app/components/v2";
|
||||
import { HighlightText } from "@app/components/v2/HighlightText";
|
||||
import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api";
|
||||
|
||||
type TForm = Record<string, string>;
|
||||
|
||||
export const GroupContainer = ({
|
||||
group,
|
||||
control,
|
||||
search
|
||||
}: {
|
||||
group: {
|
||||
fields: {
|
||||
key: string;
|
||||
value: string;
|
||||
hasEnvEntry: boolean;
|
||||
description?: string;
|
||||
}[];
|
||||
name: string;
|
||||
};
|
||||
control: Control<TForm, any, TForm>;
|
||||
search: string;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={group.name}
|
||||
className="overflow-clip border border-b-0 border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md last:border-b"
|
||||
>
|
||||
<div
|
||||
className="flex h-14 cursor-pointer items-center px-5 py-4 text-sm text-gray-300"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
setOpen((o) => !o);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className={`mr-8 transition-transform duration-100 ${open || search ? "rotate-90" : ""}`}
|
||||
icon={faChevronRight}
|
||||
/>
|
||||
|
||||
<div className="flex-grow select-none text-base">{group.name}</div>
|
||||
</div>
|
||||
|
||||
{(open || search) && (
|
||||
<div className="flex flex-col">
|
||||
{group.fields.map((field) => (
|
||||
<div
|
||||
key={field.key}
|
||||
className="flex items-center justify-between gap-4 border-t border-mineshaft-500 bg-mineshaft-700/50 p-4"
|
||||
>
|
||||
<div className="flex max-w-lg flex-col">
|
||||
<span className="text-sm">
|
||||
<HighlightText text={field.key} highlight={search} />
|
||||
</span>
|
||||
<span className="text-sm text-mineshaft-400">
|
||||
<HighlightText text={field.description} highlight={search} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex grow items-center justify-end gap-2">
|
||||
{field.hasEnvEntry && (
|
||||
<Tooltip
|
||||
content="Setting this value will override an existing environment variable"
|
||||
className="text-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} className="text-yellow" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name={field.key}
|
||||
render={({ field: formGenField, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 w-full max-w-sm"
|
||||
>
|
||||
<SecretInput
|
||||
{...formGenField}
|
||||
autoComplete="off"
|
||||
containerClassName="text-bunker-300 hover:border-mineshaft-400 border border-mineshaft-600 bg-bunker-600 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EnvironmentPageForm = () => {
|
||||
const { data: envOverrides } = useGetEnvOverrides();
|
||||
const { mutateAsync: updateServerConfig } = useUpdateServerConfig();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const allFields = useMemo(() => {
|
||||
if (!envOverrides) return [];
|
||||
return Object.values(envOverrides).flatMap((group) => group.fields);
|
||||
}, [envOverrides]);
|
||||
|
||||
const formSchema = useMemo(() => {
|
||||
return z.object(Object.fromEntries(allFields.map((field) => [field.key, z.string()])));
|
||||
}, [allFields]);
|
||||
|
||||
const defaultValues = useMemo(() => {
|
||||
const values: Record<string, string> = {};
|
||||
allFields.forEach((field) => {
|
||||
values[field.key] = field.value ?? "";
|
||||
});
|
||||
return values;
|
||||
}, [allFields]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues
|
||||
});
|
||||
|
||||
const formValues = useWatch({ control });
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!envOverrides) return [];
|
||||
|
||||
const searchTerm = search.toLowerCase().trim();
|
||||
if (!searchTerm) {
|
||||
return Object.values(envOverrides);
|
||||
}
|
||||
|
||||
return Object.values(envOverrides)
|
||||
.map((group) => {
|
||||
const filteredFields = group.fields.filter(
|
||||
(field) =>
|
||||
field.key.toLowerCase().includes(searchTerm) ||
|
||||
(field.description ?? "").toLowerCase().includes(searchTerm)
|
||||
);
|
||||
|
||||
if (filteredFields.length > 0) {
|
||||
return { ...group, fields: filteredFields };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}, [search, formValues, envOverrides]);
|
||||
|
||||
useEffect(() => {
|
||||
reset(defaultValues);
|
||||
}, [defaultValues, reset]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (formData: TForm) => {
|
||||
try {
|
||||
const filteredFormData = Object.fromEntries(
|
||||
Object.entries(formData).filter(([, value]) => value !== "")
|
||||
);
|
||||
await updateServerConfig({
|
||||
envOverrides: filteredFormData
|
||||
});
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Environment overrides updated successfully. It can take up to 5 minutes to take effect."
|
||||
});
|
||||
|
||||
reset(formData);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
(error as any)?.response?.data?.message ||
|
||||
(error as any)?.message ||
|
||||
"An unknown error occurred";
|
||||
createNotification({
|
||||
type: "error",
|
||||
title: "Failed to update environment overrides",
|
||||
text: errorMessage
|
||||
});
|
||||
}
|
||||
},
|
||||
[reset, updateServerConfig]
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-start gap-1">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Overrides</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/self-hosting/configuration/envars#environment-variable-overrides"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">
|
||||
Override specific environment variables. After saving, it may take up to 5 minutes for
|
||||
variables to propagate throughout every container.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline_bg"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search for keys, descriptions, and values..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
{filteredData.map((group) => (
|
||||
<GroupContainer group={group!} control={control} search={search} />
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { EnvironmentPageForm } from "./EnvironmentPageForm";
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { EnvironmentPage } from "./EnvironmentPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/environment"
|
||||
)({
|
||||
component: EnvironmentPage,
|
||||
beforeLoad: () => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Admin",
|
||||
link: linkOptions({ to: "/admin" })
|
||||
},
|
||||
{
|
||||
label: "Environment",
|
||||
link: linkOptions({
|
||||
to: "/admin/environment"
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -1,19 +1,33 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
export const EmailNotVerifiedPage = () => {
|
||||
return (
|
||||
<div className="flex flex-col justify-between bg-bunker-800 md:h-screen">
|
||||
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
|
||||
<Helmet>
|
||||
<title>Request a New Invite</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Helmet>
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center text-gray-200">
|
||||
<p className="text-6xl">Oops.</p>
|
||||
<p className="mb-1 mt-2 text-xl">Your email was not verified. </p>
|
||||
<p className="text-xl">Please try again.</p>
|
||||
<p className="text-md mt-8 max-w-sm text-center text-gray-600">
|
||||
Note: If it still doesn't work, please reach out to us at support@infisical.com
|
||||
<Link to="/">
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<img src="/images/gradientLogo.svg" className="h-[90px] w-[120px]" alt="Infisical Logo" />
|
||||
</div>
|
||||
</Link>
|
||||
<div className="mx-auto flex w-full flex-col items-center justify-center">
|
||||
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||
Your email was not verified
|
||||
</h1>
|
||||
<p className="w-max justify-center text-center text-sm text-gray-400">
|
||||
Please try again. <br /> Note: If it still doesn't work, please reach out to us at
|
||||
support@infisical.com
|
||||
</p>
|
||||
<div className="mt-6 flex flex-row text-sm text-bunker-400">
|
||||
<Link to="/login">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
Back to Login
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
|
||||
@@ -36,7 +36,12 @@ export const PasswordResetPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
|
||||
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
|
||||
<Link to="/">
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<img src="/images/gradientLogo.svg" className="h-[90px] w-[120px]" alt="Infisical Logo" />
|
||||
</div>
|
||||
</Link>
|
||||
{step === Steps.ConfirmEmail && (
|
||||
<ConfirmEmailStep
|
||||
onComplete={(verifyToken, userEncryptionVersion) => {
|
||||
|
||||
@@ -19,17 +19,21 @@ export const ConfirmEmailStep = ({ onComplete }: Props) => {
|
||||
isPending: isVerifyPasswordResetLoading
|
||||
} = useVerifyPasswordResetCode();
|
||||
return (
|
||||
<div className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 py-6 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<p className="mb-8 flex justify-center bg-gradient-to-br from-sky-400 to-primary bg-clip-text text-center text-4xl font-semibold text-transparent">
|
||||
<div className="mx-auto flex w-full flex-col items-center justify-center">
|
||||
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||
Confirm your email
|
||||
</h1>
|
||||
<p className="mb-8 w-max justify-center text-center text-sm text-gray-400">
|
||||
Reset password for <span className="italic">{email}</span>.
|
||||
</p>
|
||||
<img
|
||||
src="/images/envelope.svg"
|
||||
style={{ height: "262px", width: "410px" }}
|
||||
alt="verify email"
|
||||
/>
|
||||
<div className="mx-auto mb-2 mt-4 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
|
||||
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className="h-10"
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const response = await verifyPasswordResetCodeMutateAsync({
|
||||
@@ -44,7 +48,6 @@ export const ConfirmEmailStep = ({ onComplete }: Props) => {
|
||||
}
|
||||
}}
|
||||
isLoading={isVerifyPasswordResetLoading}
|
||||
size="lg"
|
||||
>
|
||||
Confirm Email
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
@@ -169,17 +169,15 @@ export const EnterPasswordStep = ({
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(resetPasswordHandler)}
|
||||
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
|
||||
className="mx-auto flex w-full flex-col items-center justify-center"
|
||||
>
|
||||
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||
Enter new password
|
||||
</h1>
|
||||
<p className="w-max justify-center text-center text-sm text-gray-400">
|
||||
Make sure you save it somewhere safe.
|
||||
</p>
|
||||
<div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
||||
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
|
||||
Make sure you save it somewhere safe.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
|
||||
<div className="mt-8 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
@@ -202,6 +200,20 @@ export const EnterPasswordStep = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className="h-10"
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
isLoading={isSubmitting || isLoading || isLoadingV2}
|
||||
isDisabled={isSubmitting || isLoading || isLoadingV2}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
{passwordErrorTooShort ||
|
||||
passwordErrorTooLong ||
|
||||
passwordErrorNoLetterChar ||
|
||||
@@ -210,33 +222,33 @@ export const EnterPasswordStep = ({
|
||||
passwordErrorEscapeChar ||
|
||||
passwordErrorLowEntropy ||
|
||||
passwordErrorBreached ? (
|
||||
<div className="mx-2 mb-2 mt-3 flex w-full max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
||||
<div className="mb-1 text-sm text-gray-400">Password should contain:</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
<div className="mt-4 rounded border border-mineshaft-600 bg-mineshaft-800 p-4 drop-shadow">
|
||||
<div className="mb-1 ml-2 text-sm text-gray-300">Password should contain:</div>
|
||||
<div className="ml-2 flex flex-row items-center justify-start">
|
||||
{passwordErrorTooShort ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
|
||||
)}
|
||||
<div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
|
||||
at least 14 characters
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
<div className="ml-2 flex flex-row items-center justify-start">
|
||||
{passwordErrorTooLong ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
|
||||
)}
|
||||
<div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}>
|
||||
at most 100 characters
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
<div className="ml-2 flex flex-row items-center justify-start">
|
||||
{passwordErrorNoLetterChar ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
|
||||
)}
|
||||
<div
|
||||
className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
|
||||
@@ -244,11 +256,11 @@ export const EnterPasswordStep = ({
|
||||
at least 1 letter character
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
<div className="ml-2 flex flex-row items-center justify-start">
|
||||
{passwordErrorNoNumOrSpecialChar ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
@@ -258,11 +270,11 @@ export const EnterPasswordStep = ({
|
||||
at least 1 number or special character
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
<div className="ml-2 flex flex-row items-center justify-start">
|
||||
{passwordErrorRepeatedChar ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
|
||||
)}
|
||||
<div
|
||||
className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
|
||||
@@ -270,11 +282,11 @@ export const EnterPasswordStep = ({
|
||||
at most 3 repeated, consecutive characters
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
<div className="ml-2 flex flex-row items-center justify-start">
|
||||
{passwordErrorEscapeChar ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
|
||||
)}
|
||||
<div
|
||||
className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
|
||||
@@ -282,11 +294,11 @@ export const EnterPasswordStep = ({
|
||||
No escape characters allowed.
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
<div className="ml-2 flex flex-row items-center justify-start">
|
||||
{passwordErrorLowEntropy ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
|
||||
)}
|
||||
<div
|
||||
className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
|
||||
@@ -294,32 +306,18 @@ export const EnterPasswordStep = ({
|
||||
Password contains personal info.
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 flex flex-row items-center justify-start">
|
||||
<div className="ml-2 flex flex-row items-center justify-start">
|
||||
{passwordErrorBreached ? (
|
||||
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2.5 text-lg text-red" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-green" />
|
||||
)}
|
||||
<div className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}>
|
||||
Password was found in a data breach.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-2" />
|
||||
)}
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting || isLoading || isLoadingV2}
|
||||
isDisabled={isSubmitting || isLoading || isLoadingV2}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,18 +43,15 @@ export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) =>
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(getEncryptedKeyHandler)}
|
||||
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
|
||||
className="mx-auto flex w-full flex-col items-center justify-center"
|
||||
>
|
||||
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100">
|
||||
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||
Enter your backup key
|
||||
</h1>
|
||||
<p className="w-max justify-center text-center text-sm text-gray-400">
|
||||
You can find it in your emergency kit you downloaded during signup.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
||||
<p className="flex w-full px-4 text-center text-sm text-gray-400 sm:max-w-md">
|
||||
You can find it in your emergency kit. You had to download the emergency kit during
|
||||
signup.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
|
||||
<div className="mt-8 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Controller
|
||||
control={control}
|
||||
name="backupKey"
|
||||
@@ -75,12 +72,17 @@ export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) =>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button type="submit" colorSchema="secondary">
|
||||
Submit Backup Key
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className="h-10"
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
>
|
||||
Submit Backup Key
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,7 @@ import { FormEvent, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import InputField from "@app/components/basic/InputField";
|
||||
import { Button, EmailServiceSetupModal } from "@app/components/v2";
|
||||
import { Button, EmailServiceSetupModal, Input } from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useSendPasswordResetEmail } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
@@ -44,9 +43,9 @@ export const VerifyEmailPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-start bg-bunker-800 px-6">
|
||||
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28">
|
||||
<Helmet>
|
||||
<title>Login</title>
|
||||
<title>Reset Password</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="Verify your email in Infisical" />
|
||||
@@ -56,66 +55,80 @@ export const VerifyEmailPage = () => {
|
||||
/>
|
||||
</Helmet>
|
||||
<Link to="/">
|
||||
<div className="mb-8 mt-20 flex cursor-pointer justify-center">
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<img
|
||||
src="/images/biglogo.png"
|
||||
src="/images/gradientLogo.svg"
|
||||
style={{
|
||||
height: "90px",
|
||||
width: "120px"
|
||||
}}
|
||||
alt="long logo"
|
||||
alt="Infisical Logo"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
{step === 1 && (
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl"
|
||||
className="mx-auto flex w-full flex-col items-center justify-center"
|
||||
>
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||
Forgot your password?
|
||||
</h1>
|
||||
<p className="w-max justify-center text-center text-sm text-gray-400">
|
||||
Enter your email to start the password reset process. <br /> You will receive an email
|
||||
with instructions.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
||||
<p className="flex w-max justify-center text-center text-sm text-gray-400">
|
||||
Enter your email to start the password reset process. You will receive an email with
|
||||
instructions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
<div className="mt-8 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Input
|
||||
value={email}
|
||||
placeholder=""
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
type="email"
|
||||
placeholder="Enter your email..."
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button type="submit" size="lg" onClick={() => {}} isLoading={loading}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className="h-10"
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
isLoading={loading}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-row text-sm text-bunker-400">
|
||||
<Link to="/login">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
Back to Login
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl">
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-semibold text-bunker-100 md:text-2xl">
|
||||
Look for an email in your inbox.
|
||||
<div className="mx-auto flex w-full flex-col items-center justify-center">
|
||||
<h1 className="mb-2 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||
Look for an email in your inbox
|
||||
</h1>
|
||||
<p className="w-max max-w-lg justify-center text-center text-sm text-gray-400">
|
||||
If the email is in our system, you will receive an email at{" "}
|
||||
<span className="italic">{email}</span> with instructions on how to reset your password.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
|
||||
<p className="w-max text-center text-sm text-gray-400">
|
||||
If the email is in our system, you will receive an email at{" "}
|
||||
<span className="italic">{email}</span> with instructions on how to reset your
|
||||
password.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-row text-sm text-bunker-400">
|
||||
<Link to="/login">
|
||||
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||
Back to Login
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||
|
||||
+15
@@ -16,6 +16,7 @@ import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConn
|
||||
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
|
||||
import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm";
|
||||
import { AzureKeyVaultConnectionForm } from "./AzureKeyVaultConnectionForm";
|
||||
import { BitbucketConnectionForm } from "./BitbucketConnectionForm";
|
||||
import { CamundaConnectionForm } from "./CamundaConnectionForm";
|
||||
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
@@ -33,11 +34,13 @@ import { MySqlConnectionForm } from "./MySqlConnectionForm";
|
||||
import { OCIConnectionForm } from "./OCIConnectionForm";
|
||||
import { OracleDBConnectionForm } from "./OracleDBConnectionForm";
|
||||
import { PostgresConnectionForm } from "./PostgresConnectionForm";
|
||||
import { RailwayConnectionForm } from "./RailwayConnectionForm";
|
||||
import { RenderConnectionForm } from "./RenderConnectionForm";
|
||||
import { TeamCityConnectionForm } from "./TeamCityConnectionForm";
|
||||
import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm";
|
||||
import { VercelConnectionForm } from "./VercelConnectionForm";
|
||||
import { WindmillConnectionForm } from "./WindmillConnectionForm";
|
||||
import { ZabbixConnectionForm } from "./ZabbixConnectionForm";
|
||||
|
||||
type FormProps = {
|
||||
onComplete: (appConnection: TAppConnection) => void;
|
||||
@@ -134,6 +137,12 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
return <GitLabConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Cloudflare:
|
||||
return <CloudflareConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Bitbucket:
|
||||
return <BitbucketConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Zabbix:
|
||||
return <ZabbixConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Railway:
|
||||
return <RailwayConnectionForm onSubmit={onSubmit} />;
|
||||
default:
|
||||
throw new Error(`Unhandled App ${app}`);
|
||||
}
|
||||
@@ -228,6 +237,12 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Cloudflare:
|
||||
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Bitbucket:
|
||||
return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Zabbix:
|
||||
return <ZabbixConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Railway:
|
||||
return <RailwayConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
default:
|
||||
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
|
||||
}
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { BitbucketConnectionMethod, TBitbucketConnection } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TBitbucketConnection;
|
||||
onSubmit: (formData: FormData) => void;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.Bitbucket)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(BitbucketConnectionMethod.ApiToken),
|
||||
credentials: z.object({
|
||||
email: z.string().email().trim().min(1, "Email required"),
|
||||
apiToken: z.string().trim().min(1, "API Token required")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const BitbucketConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.Bitbucket,
|
||||
method: BitbucketConnectionMethod.ApiToken
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Bitbucket].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(BitbucketConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.email"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error?.message)} label="Email">
|
||||
<Input {...field} placeholder="[email protected]" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.apiToken"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="API 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" : "Connect to Bitbucket"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
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 { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
RailwayConnectionMethod,
|
||||
TRailwayConnection
|
||||
} from "@app/hooks/api/appConnections/types/railway-connection";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TRailwayConnection;
|
||||
onSubmit: (formData: FormData) => void;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.Railway)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.nativeEnum(RailwayConnectionMethod),
|
||||
credentials: z.object({
|
||||
apiToken: z.string().trim().min(1, "Service API Token required")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const RailwayConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.Railway,
|
||||
method: RailwayConnectionMethod.AccountToken
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The type of token you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Railway].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Token Type"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(RailwayConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.apiToken"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Token Value"
|
||||
>
|
||||
<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" : "Connect to Railway"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { TZabbixConnection, ZabbixConnectionMethod } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TZabbixConnection;
|
||||
onSubmit: (formData: FormData) => void;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.Zabbix)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(ZabbixConnectionMethod.ApiToken),
|
||||
credentials: z.object({
|
||||
apiToken: z.string().trim().min(1, "API Token required"),
|
||||
instanceUrl: z.string().trim().url("Invalid instance URL")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ZabbixConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.Zabbix,
|
||||
method: ZabbixConnectionMethod.ApiToken
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.Zabbix].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(ZabbixConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.apiToken"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="API Token"
|
||||
>
|
||||
<Input {...field} type="password" placeholder="Enter your API Token" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.instanceUrl"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Instance URL"
|
||||
tooltipClassName="max-w-sm"
|
||||
>
|
||||
<Input {...field} placeholder="https://your-zabbix-instance.com" />
|
||||
</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" : "Connect to Zabbix"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, Tooltip } from "@app/components/v2";
|
||||
@@ -8,25 +10,41 @@ type Props = {
|
||||
label: string;
|
||||
onClear: () => void;
|
||||
children: React.ReactNode;
|
||||
tooltipText?: string;
|
||||
};
|
||||
|
||||
export const LogFilterItem = ({ label, onClear, hoverTooltip, children, className }: Props) => {
|
||||
export const LogFilterItem = ({
|
||||
label,
|
||||
onClear,
|
||||
hoverTooltip,
|
||||
children,
|
||||
className,
|
||||
tooltipText
|
||||
}: Props) => {
|
||||
return (
|
||||
<Tooltip className="relative top-4" content={hoverTooltip} isDisabled={!hoverTooltip}>
|
||||
<div className={twMerge("flex flex-col justify-between", className)}>
|
||||
<div className="flex items-center justify-between pr-1">
|
||||
<p className="text-xs opacity-60">{label}</p>
|
||||
<Button
|
||||
onClick={() => onClear()}
|
||||
variant="link"
|
||||
className="font-normal text-mineshaft-400 transition-all duration-75 hover:text-mineshaft-300"
|
||||
size="xs"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
{children}
|
||||
<div className={twMerge("flex flex-col justify-between", className)}>
|
||||
<div className="flex items-center pr-1">
|
||||
<p className="text-xs opacity-60">{label}</p>
|
||||
{tooltipText && (
|
||||
<Tooltip content={tooltipText} className="max-w-sm">
|
||||
<FontAwesomeIcon
|
||||
icon={faInfoCircle}
|
||||
className="-mt-[0.05rem] ml-1 text-[11px] text-mineshaft-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => onClear()}
|
||||
variant="link"
|
||||
className="ml-auto font-normal text-mineshaft-400 transition-all duration-75 hover:text-mineshaft-300"
|
||||
size="xs"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip className="relative top-4" content={hoverTooltip} isDisabled={!hoverTooltip}>
|
||||
<div>{children}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faArrowRight, faCalendar, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
import ms from "ms";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
|
||||
import {
|
||||
auditLogDateFilterFormSchema,
|
||||
AuditLogDateFilterType,
|
||||
TAuditLogDateFilterFormData
|
||||
} from "./types";
|
||||
|
||||
type Props = {
|
||||
setFilter: (data: TAuditLogDateFilterFormData) => void;
|
||||
filter: TAuditLogDateFilterFormData;
|
||||
};
|
||||
const RELATIVE_VALUES = ["5m", "30m", "1h", "3h", "12h"];
|
||||
export const LogsDateFilter = ({ setFilter, filter }: Props) => {
|
||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||
const [isPopupOpen, setIsPopOpen] = useState(false);
|
||||
|
||||
const { control, watch, handleSubmit, formState } = useForm<TAuditLogDateFilterFormData>({
|
||||
resolver: zodResolver(auditLogDateFilterFormSchema),
|
||||
values: filter
|
||||
});
|
||||
const selectType = watch("type");
|
||||
const isCustomRelative =
|
||||
filter.type === AuditLogDateFilterType.Relative &&
|
||||
!RELATIVE_VALUES.includes(filter.relativeModeValue || "");
|
||||
|
||||
const onSubmit = (data: TAuditLogDateFilterFormData) => {
|
||||
const endDate = data.type === AuditLogDateFilterType.Relative ? new Date() : data.endDate;
|
||||
const startDate =
|
||||
data.type === AuditLogDateFilterType.Relative && data.relativeModeValue
|
||||
? new Date(Number(new Date()) - ms(data.relativeModeValue))
|
||||
: data.startDate;
|
||||
setFilter({
|
||||
...data,
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
setIsPopOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isPopupOpen} onOpenChange={(el) => setIsPopOpen(el)}>
|
||||
<div className="mr-2 flex items-center">
|
||||
{filter.type === AuditLogDateFilterType.Relative ? (
|
||||
<>
|
||||
{RELATIVE_VALUES.map((el) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
className={twMerge(
|
||||
"rounded-none px-3 py-2 first:rounded-l-md",
|
||||
filter.type === AuditLogDateFilterType.Relative &&
|
||||
filter.relativeModeValue === el &&
|
||||
"border-primary/40 bg-primary/[0.1]"
|
||||
)}
|
||||
key={`${el}-relative`}
|
||||
onClick={() =>
|
||||
setFilter({
|
||||
relativeModeValue: el,
|
||||
type: AuditLogDateFilterType.Relative,
|
||||
endDate: new Date(),
|
||||
startDate: new Date(Number(new Date()) - ms(el))
|
||||
})
|
||||
}
|
||||
>
|
||||
{el}
|
||||
</Button>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-l-md border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
|
||||
{format(filter.startDate, "yyyy-MM-dd HH:mm")}
|
||||
</div>
|
||||
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</div>
|
||||
<div className="border border-transparent bg-mineshaft-600 px-3 py-2 text-sm text-bunker-200">
|
||||
{format(filter.endDate, "yyyy-MM-dd HH:mm")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
className={twMerge(
|
||||
"rounded-none rounded-r-md px-3 py-2",
|
||||
(filter.type === AuditLogDateFilterType.Absolute || isCustomRelative) &&
|
||||
"border-primary/40 bg-primary/[0.1]"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCalendar} />
|
||||
{filter.type === AuditLogDateFilterType.Relative && isCustomRelative && (
|
||||
<span className="ml-1">({filter.relativeModeValue})</span>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent className="min-w-80 p-4" align="center" alignOffset={16}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormControl className="mb-2 w-full">
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={(el) => field.onChange(el)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value={AuditLogDateFilterType.Relative}>Relative</SelectItem>
|
||||
<SelectItem value={AuditLogDateFilterType.Absolute}>Absolute</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectType === AuditLogDateFilterType.Relative && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="relativeModeValue"
|
||||
render={({ field }) => (
|
||||
<FormControl className="mb-0 w-full" helperText="Example: 1h, 1d, 2d">
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectType === AuditLogDateFilterType.Absolute && (
|
||||
<div className="mb-2 flex h-10 w-full items-center justify-between gap-2">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="relative top-2"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center -space-x-3">
|
||||
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
|
||||
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
|
||||
</div>
|
||||
<Controller
|
||||
name="endDate"
|
||||
control={control}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="relative top-2"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isEndDatePickerOpen,
|
||||
onOpenChange: setIsEndDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4">
|
||||
<Button size="xs" type="submit" isDisabled={!formState.isDirty}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +1,15 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
Control,
|
||||
Controller,
|
||||
UseFormGetFieldState,
|
||||
UseFormReset,
|
||||
UseFormResetField,
|
||||
UseFormSetValue,
|
||||
UseFormWatch
|
||||
} from "react-hook-form";
|
||||
import {
|
||||
faArrowRight,
|
||||
faCaretDown,
|
||||
faCheckCircle,
|
||||
faFilterCircleXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCaretDown, faCheckCircle, faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DatePicker,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -44,7 +31,7 @@ import { EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { UserAgentType } from "@app/hooks/api/auth/types";
|
||||
|
||||
import { LogFilterItem } from "./LogFilterItem";
|
||||
import { AuditLogFilterFormData, Presets } from "./types";
|
||||
import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types";
|
||||
|
||||
const eventTypes = Object.entries(eventToNameMap).map(([value, label]) => ({ label, value }));
|
||||
const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label]) => ({
|
||||
@@ -54,78 +41,54 @@ const userAgentTypes = Object.entries(userAgentTypeToNameMap).map(([value, label
|
||||
|
||||
type Props = {
|
||||
presets?: Presets;
|
||||
control: Control<AuditLogFilterFormData>;
|
||||
reset: UseFormReset<AuditLogFilterFormData>;
|
||||
resetField: UseFormResetField<AuditLogFilterFormData>;
|
||||
watch: UseFormWatch<AuditLogFilterFormData>;
|
||||
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>;
|
||||
setValue: UseFormSetValue<AuditLogFilterFormData>;
|
||||
setFilter: (data: TAuditLogFilterFormData) => void;
|
||||
filter: TAuditLogFilterFormData;
|
||||
};
|
||||
|
||||
const getActiveFilterCount = (
|
||||
getFieldState: UseFormGetFieldState<AuditLogFilterFormData>,
|
||||
watch: UseFormWatch<AuditLogFilterFormData>
|
||||
) => {
|
||||
const getActiveFilterCount = (filter: TAuditLogFilterFormData) => {
|
||||
const fields = [
|
||||
"actor",
|
||||
"project",
|
||||
"eventType",
|
||||
"startDate",
|
||||
"endDate",
|
||||
"environment",
|
||||
"secretPath",
|
||||
"userAgentType",
|
||||
"secretKey"
|
||||
] as Partial<keyof AuditLogFilterFormData>[];
|
||||
] as Partial<keyof TAuditLogFilterFormData>[];
|
||||
|
||||
let filterCount = 0;
|
||||
|
||||
// either start or end date should only be counted as one filter
|
||||
let dateProcessed = false;
|
||||
|
||||
fields.forEach((field) => {
|
||||
const fieldState = getFieldState(field);
|
||||
|
||||
if (
|
||||
field === "userAgentType" ||
|
||||
field === "environment" ||
|
||||
field === "secretKey" ||
|
||||
field === "secretPath"
|
||||
) {
|
||||
const value = watch(field);
|
||||
|
||||
if (value !== undefined && value !== "") {
|
||||
filterCount += 1;
|
||||
}
|
||||
} else if (fieldState.isDirty && !dateProcessed) {
|
||||
const value = filter?.[field];
|
||||
if (Array.isArray(value) ? value.length : value) {
|
||||
filterCount += 1;
|
||||
|
||||
if (field === "startDate" || field === "endDate") {
|
||||
dateProcessed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return filterCount;
|
||||
};
|
||||
|
||||
export const LogsFilter = ({
|
||||
presets,
|
||||
control,
|
||||
reset,
|
||||
resetField,
|
||||
watch,
|
||||
getFieldState,
|
||||
setValue
|
||||
}: Props) => {
|
||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||
|
||||
export const LogsFilter = ({ presets, setFilter, filter }: Props) => {
|
||||
const { data: workspaces = [] } = useGetUserWorkspaces();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
|
||||
|
||||
const { control, watch, resetField, setValue, handleSubmit, formState } =
|
||||
useForm<TAuditLogFilterFormData>({
|
||||
resolver: zodResolver(auditLogFilterFormSchema),
|
||||
defaultValues: {
|
||||
project: null,
|
||||
environment: undefined,
|
||||
secretKey: "",
|
||||
secretPath: "",
|
||||
actor: presets?.actorId,
|
||||
eventType: filter?.eventType || [],
|
||||
userAgentType: undefined
|
||||
},
|
||||
values: filter
|
||||
});
|
||||
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
||||
const selectedProject = watch("project");
|
||||
|
||||
@@ -140,408 +103,341 @@ export const LogsFilter = ({
|
||||
return workspacesInOrg.find((ws) => ws.id === selectedProject.id)?.environments ?? [];
|
||||
}, [selectedProject, workspacesInOrg]);
|
||||
|
||||
const activeFilterCount = getActiveFilterCount(getFieldState, watch);
|
||||
const activeFilterCount = getActiveFilterCount(filter);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline_bg" colorSchema="primary">
|
||||
<FontAwesomeIcon icon={faFilterCircleXmark} className="mr-3 px-[0.1rem]" />
|
||||
Filters
|
||||
<Button variant="outline_bg" colorSchema="primary" className="relative">
|
||||
<FontAwesomeIcon icon={faFilterCircleXmark} />
|
||||
{activeFilterCount > 0 && (
|
||||
<Badge className="ml-2 px-1.5 py-0.5" variant="primary">
|
||||
<Badge className="absolute bottom-0 right-0" variant="primary">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="mt-4 py-4">
|
||||
<div className="flex min-w-64 flex-col font-inter">
|
||||
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Filters</span>
|
||||
<Badge className="px-1.5 py-0.5" variant="primary">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
<form onSubmit={handleSubmit(setFilter)}>
|
||||
<div className="flex min-w-64 flex-col font-inter">
|
||||
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Filters</span>
|
||||
<Badge className="px-1.5 py-0.5" variant="primary">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFilter({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
userAgentType: undefined,
|
||||
project: null,
|
||||
secretPath: undefined,
|
||||
secretKey: undefined
|
||||
});
|
||||
}}
|
||||
variant="link"
|
||||
className="text-mineshaft-400"
|
||||
size="xs"
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
reset({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
project: null,
|
||||
secretPath: undefined,
|
||||
secretKey: undefined
|
||||
});
|
||||
</div>
|
||||
|
||||
<div className="px-3">
|
||||
<LogFilterItem
|
||||
label="Events"
|
||||
onClear={() => {
|
||||
resetField("eventType");
|
||||
}}
|
||||
variant="link"
|
||||
className="text-mineshaft-400"
|
||||
size="xs"
|
||||
>
|
||||
Clear filters
|
||||
<Controller
|
||||
control={control}
|
||||
name="eventType"
|
||||
render={({ field }) => (
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{selectedEventTypes?.length === 1
|
||||
? eventTypes.find(
|
||||
(eventType) => eventType.value === selectedEventTypes[0]
|
||||
)?.label
|
||||
: selectedEventTypes?.length === 0
|
||||
? "All events"
|
||||
: `${selectedEventTypes?.length} events selected`}
|
||||
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
|
||||
>
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{eventTypes && eventTypes.length > 0 ? (
|
||||
eventTypes.map((eventType) => {
|
||||
const isSelected = selectedEventTypes?.includes(
|
||||
eventType.value as EventType
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) =>
|
||||
eventTypes.length > 1 && event.preventDefault()
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
selectedEventTypes?.includes(eventType.value as EventType)
|
||||
) {
|
||||
field.onChange(
|
||||
selectedEventTypes?.filter(
|
||||
(e: string) => e !== eventType.value
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([
|
||||
...(selectedEventTypes || []),
|
||||
eventType.value
|
||||
]);
|
||||
}
|
||||
}}
|
||||
key={`event-type-${eventType.value}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{eventType.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
<LogFilterItem
|
||||
label="Source"
|
||||
onClear={() => {
|
||||
resetField("userAgentType");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="userAgentType"
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
value={value === undefined ? "all" : value}
|
||||
onValueChange={(e) => {
|
||||
if (e === "all") onChange(undefined);
|
||||
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
|
||||
}}
|
||||
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All sources
|
||||
</SelectItem>
|
||||
{userAgentTypes.map(({ label, value: userAgent }) => (
|
||||
<SelectItem value={userAgent} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
<LogFilterItem
|
||||
label="Project"
|
||||
onClear={() => {
|
||||
resetField("project");
|
||||
resetField("environment");
|
||||
setValue("secretPath", "");
|
||||
setValue("secretKey", "");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mb-0 w-full"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
isClearable
|
||||
onChange={(e) => {
|
||||
if (e === null) {
|
||||
setValue("secretPath", "");
|
||||
setValue("secretKey", "");
|
||||
}
|
||||
resetField("environment");
|
||||
onChange(e);
|
||||
}}
|
||||
placeholder="All projects"
|
||||
options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({
|
||||
name,
|
||||
id,
|
||||
type: defaultProduct
|
||||
}))}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
<AnimatePresence initial={false}>
|
||||
{showSecretsSection && (
|
||||
<motion.div
|
||||
className="overflow-hidden"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="mb-3 mt-2">
|
||||
<p className="text-xs opacity-60">Secrets</p>
|
||||
<div className="h-[1px] w-full rounded-full bg-mineshaft-500" />
|
||||
</div>
|
||||
<LogFilterItem
|
||||
label="Environment"
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by environment."
|
||||
: undefined
|
||||
}
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
onClear={() => {
|
||||
resetField("environment");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-full"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
key={value?.name || "filter-environment"}
|
||||
isClearable
|
||||
isDisabled={!selectedProject}
|
||||
onChange={(e) => onChange(e)}
|
||||
placeholder="All environments"
|
||||
options={availableEnvironments.map(({ name, slug }) => ({
|
||||
name,
|
||||
slug
|
||||
}))}
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
<LogFilterItem
|
||||
label="Secret Path"
|
||||
tooltipText="Enter the exact secret path (wildcards like * are not supported)"
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by secret path."
|
||||
: undefined
|
||||
}
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
onClear={() => {
|
||||
setValue("secretPath", "");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl className="w-full">
|
||||
<Input
|
||||
placeholder="Enter secret path"
|
||||
className="disabled:cursor-not-allowed"
|
||||
isDisabled={!selectedProject}
|
||||
{...field}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
|
||||
<LogFilterItem
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by secret key."
|
||||
: undefined
|
||||
}
|
||||
tooltipText="Enter the exact secret key name (wildcards like * are not supported)"
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
label="Secret Key"
|
||||
onClear={() => {
|
||||
setValue("secretKey", "");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretKey"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl className="w-full">
|
||||
<Input
|
||||
isDisabled={!selectedProject}
|
||||
{...field}
|
||||
placeholder="Enter secret key"
|
||||
className="disabled:cursor-not-allowed"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue("secretKey", e.target.value, { shouldDirty: true })
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className="mt-2 px-3">
|
||||
<Button size="xs" type="submit" isDisabled={!formState.isDirty}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3">
|
||||
<LogFilterItem
|
||||
label="Events"
|
||||
onClear={() => {
|
||||
resetField("eventType");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="eventType"
|
||||
render={({ field }) => (
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="thin-scrollbar inline-flex w-full cursor-pointer items-center justify-between whitespace-nowrap rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{selectedEventTypes?.length === 1
|
||||
? eventTypes.find(
|
||||
(eventType) => eventType.value === selectedEventTypes[0]
|
||||
)?.label
|
||||
: selectedEventTypes?.length === 0
|
||||
? "All events"
|
||||
: `${selectedEventTypes?.length} events selected`}
|
||||
<FontAwesomeIcon icon={faCaretDown} className="ml-2 text-xs" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
|
||||
>
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{eventTypes && eventTypes.length > 0 ? (
|
||||
eventTypes.map((eventType) => {
|
||||
const isSelected = selectedEventTypes?.includes(
|
||||
eventType.value as EventType
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) =>
|
||||
eventTypes.length > 1 && event.preventDefault()
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
selectedEventTypes?.includes(eventType.value as EventType)
|
||||
) {
|
||||
field.onChange(
|
||||
selectedEventTypes?.filter(
|
||||
(e: string) => e !== eventType.value
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([
|
||||
...(selectedEventTypes || []),
|
||||
eventType.value
|
||||
]);
|
||||
}
|
||||
}}
|
||||
key={`event-type-${eventType.value}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{eventType.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
<LogFilterItem
|
||||
label="Source"
|
||||
onClear={() => {
|
||||
resetField("userAgentType");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="userAgentType"
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-full"
|
||||
>
|
||||
<Select
|
||||
value={value === undefined ? "all" : value}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
if (e === "all") onChange(undefined);
|
||||
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
|
||||
}}
|
||||
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All sources
|
||||
</SelectItem>
|
||||
{userAgentTypes.map(({ label, value: userAgent }) => (
|
||||
<SelectItem value={userAgent} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
|
||||
<LogFilterItem
|
||||
label="Date"
|
||||
onClear={() => {
|
||||
resetField("startDate");
|
||||
resetField("endDate");
|
||||
}}
|
||||
>
|
||||
<div className="flex h-10 w-full items-center justify-between gap-2">
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="relative top-2"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isStartDatePickerOpen,
|
||||
onOpenChange: setIsStartDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex items-center -space-x-3">
|
||||
<div className="h-[2px] w-[20px] rounded-full bg-mineshaft-500" />
|
||||
<FontAwesomeIcon icon={faArrowRight} className="text-mineshaft-500" />
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="endDate"
|
||||
control={control}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="relative top-2"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<DatePicker
|
||||
value={field.value || undefined}
|
||||
onChange={onChange}
|
||||
dateFormat="P"
|
||||
popUpProps={{
|
||||
open: isEndDatePickerOpen,
|
||||
onOpenChange: setIsEndDatePickerOpen
|
||||
}}
|
||||
popUpContentProps={{}}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</LogFilterItem>
|
||||
<AnimatePresence initial={false}>
|
||||
{showSecretsSection && (
|
||||
<motion.div
|
||||
className="mt-2 overflow-hidden"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="mb-3 mt-2">
|
||||
<p className="text-xs opacity-60">Secrets</p>
|
||||
<div className="h-[1px] w-full rounded-full bg-mineshaft-500" />
|
||||
</div>
|
||||
|
||||
<LogFilterItem
|
||||
label="Project"
|
||||
onClear={() => {
|
||||
resetField("project");
|
||||
resetField("environment");
|
||||
setValue("secretPath", "");
|
||||
setValue("secretKey", "");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-full"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
isClearable
|
||||
onChange={(e) => {
|
||||
if (e === null) {
|
||||
setValue("secretPath", "");
|
||||
setValue("secretKey", "");
|
||||
}
|
||||
resetField("environment");
|
||||
onChange(e);
|
||||
}}
|
||||
placeholder="All projects"
|
||||
options={workspacesInOrg.map(({ name, id, defaultProduct }) => ({
|
||||
name,
|
||||
id,
|
||||
type: defaultProduct
|
||||
}))}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
|
||||
<LogFilterItem
|
||||
label="Environment"
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by environment."
|
||||
: undefined
|
||||
}
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
onClear={() => {
|
||||
resetField("environment");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-full"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
key={value?.name || "filter-environment"}
|
||||
isClearable
|
||||
isDisabled={!selectedProject}
|
||||
onChange={(e) => onChange(e)}
|
||||
placeholder="All environments"
|
||||
options={availableEnvironments.map(({ name, slug }) => ({
|
||||
name,
|
||||
slug
|
||||
}))}
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
<LogFilterItem
|
||||
label="Secret Path"
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by secret path."
|
||||
: undefined
|
||||
}
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
onClear={() => {
|
||||
setValue("secretPath", "");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl
|
||||
tooltipText="Filter audit logs related to events that occurred on a specific secret path."
|
||||
className="w-full"
|
||||
>
|
||||
<Input
|
||||
placeholder="Enter secret path"
|
||||
className="disabled:cursor-not-allowed"
|
||||
isDisabled={!selectedProject}
|
||||
{...field}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
|
||||
<LogFilterItem
|
||||
hoverTooltip={
|
||||
!selectedProject
|
||||
? "Select a project before filtering by secret key."
|
||||
: undefined
|
||||
}
|
||||
className={twMerge(!selectedProject && "opacity-50")}
|
||||
label="Secret Key"
|
||||
onClear={() => {
|
||||
setValue("secretKey", "");
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretKey"
|
||||
render={({ field: { onChange, value, ...field } }) => (
|
||||
<FormControl
|
||||
tooltipText="Filter audit logs related to a specific secret."
|
||||
className="w-full"
|
||||
>
|
||||
<Input
|
||||
isDisabled={!selectedProject}
|
||||
{...field}
|
||||
placeholder="Enter secret key"
|
||||
className="disabled:cursor-not-allowed"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setValue("secretKey", e.target.value, { shouldDirty: true })
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</LogFilterItem>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import ms from "ms";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LogsDateFilter } from "./LogsDateFilter";
|
||||
import { LogsFilter } from "./LogsFilter";
|
||||
import { LogsTable } from "./LogsTable";
|
||||
import { AuditLogFilterFormData, auditLogFilterFormSchema, Presets } from "./types";
|
||||
import {
|
||||
AuditLogDateFilterType,
|
||||
Presets,
|
||||
TAuditLogDateFilterFormData,
|
||||
TAuditLogFilterFormData
|
||||
} from "./types";
|
||||
|
||||
type Props = {
|
||||
presets?: Presets;
|
||||
@@ -24,74 +27,45 @@ export const LogsSection = withPermission(
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const { control, reset, watch, getFieldState, resetField, setValue } =
|
||||
useForm<AuditLogFilterFormData>({
|
||||
resolver: zodResolver(auditLogFilterFormSchema),
|
||||
defaultValues: {
|
||||
project: null,
|
||||
environment: undefined,
|
||||
secretKey: "",
|
||||
secretPath: "",
|
||||
actor: presets?.actorId,
|
||||
eventType: presets?.eventType || [],
|
||||
userAgentType: undefined,
|
||||
startDate: presets?.startDate ?? new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
endDate: presets?.endDate ?? new Date(new Date(Date.now()).setHours(23, 59, 59, 999))
|
||||
}
|
||||
});
|
||||
const [logFilter, setLogFilter] = useState<TAuditLogFilterFormData>({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId
|
||||
});
|
||||
const [dateFilter, setDateFilter] = useState<TAuditLogDateFilterFormData>({
|
||||
startDate: new Date(Number(new Date()) - ms("1h")),
|
||||
endDate: new Date(),
|
||||
type: AuditLogDateFilterType.Relative,
|
||||
relativeModeValue: "1h"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.auditLogs) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
const eventType = watch("eventType") as EventType[] | undefined;
|
||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||
const actor = watch("actor");
|
||||
const projectId = watch("project")?.id;
|
||||
const environment = watch("environment")?.slug;
|
||||
const secretPath = watch("secretPath");
|
||||
const secretKey = watch("secretKey");
|
||||
|
||||
const startDate = watch("startDate");
|
||||
const endDate = watch("endDate");
|
||||
|
||||
const [debouncedSecretPath] = useDebounce<string>(secretPath!, 500);
|
||||
const [debouncedSecretKey] = useDebounce<string>(secretKey!, 500);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex w-full justify-end">
|
||||
{showFilters && <LogsDateFilter filter={dateFilter} setFilter={setDateFilter} />}
|
||||
{showFilters && (
|
||||
<LogsFilter
|
||||
presets={presets}
|
||||
control={control}
|
||||
watch={watch}
|
||||
reset={reset}
|
||||
resetField={resetField}
|
||||
getFieldState={getFieldState}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<LogsFilter presets={presets} setFilter={setLogFilter} filter={logFilter} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
filter={{
|
||||
secretPath: debouncedSecretPath || undefined,
|
||||
secretKey: debouncedSecretKey || undefined,
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
projectId,
|
||||
secretPath: logFilter.secretPath || undefined,
|
||||
secretKey: logFilter.secretKey || undefined,
|
||||
eventMetadata: logFilter?.eventMetadata,
|
||||
projectId: logFilter?.project?.id,
|
||||
actorType: presets?.actorType,
|
||||
limit: 15,
|
||||
eventType,
|
||||
userAgentType,
|
||||
startDate,
|
||||
endDate,
|
||||
environment,
|
||||
actor
|
||||
eventType: logFilter?.eventType,
|
||||
userAgentType: logFilter?.userAgentType,
|
||||
startDate: dateFilter?.startDate,
|
||||
endDate: dateFilter?.endDate,
|
||||
environment: logFilter?.environment?.slug,
|
||||
actor: logFilter?.actor
|
||||
}}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
|
||||
@@ -23,11 +23,11 @@ import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
|
||||
import { LogsTableRow } from "./LogsTableRow";
|
||||
|
||||
type Props = {
|
||||
filter?: TGetAuditLogsFilter;
|
||||
filter: TGetAuditLogsFilter;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
const AUDIT_LOG_LIMIT = 15;
|
||||
const AUDIT_LOG_LIMIT = 30;
|
||||
|
||||
export const LogsTable = ({ filter, refetchInterval }: Props) => {
|
||||
// Determine the project ID for filtering
|
||||
|
||||
@@ -3,23 +3,33 @@ import { z } from "zod";
|
||||
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const auditLogFilterFormSchema = z
|
||||
export enum AuditLogDateFilterType {
|
||||
Relative = "relative",
|
||||
Absolute = "absolute"
|
||||
}
|
||||
|
||||
export const auditLogFilterFormSchema = z.object({
|
||||
eventMetadata: z.object({}).optional(),
|
||||
project: z
|
||||
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
|
||||
.optional()
|
||||
.nullable(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(),
|
||||
eventType: z.nativeEnum(EventType).array(),
|
||||
actor: z.string().optional(),
|
||||
userAgentType: z.nativeEnum(UserAgentType).optional(),
|
||||
secretPath: z.string().optional(),
|
||||
secretKey: z.string().optional(),
|
||||
page: z.coerce.number().optional(),
|
||||
perPage: z.coerce.number().optional()
|
||||
});
|
||||
|
||||
export const auditLogDateFilterFormSchema = z
|
||||
.object({
|
||||
eventMetadata: z.object({}).optional(),
|
||||
project: z
|
||||
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
|
||||
.optional()
|
||||
.nullable(),
|
||||
environment: z.object({ name: z.string(), slug: z.string() }).optional().nullable(),
|
||||
eventType: z.nativeEnum(EventType).array(),
|
||||
actor: z.string().optional(),
|
||||
userAgentType: z.nativeEnum(UserAgentType),
|
||||
secretPath: z.string().optional(),
|
||||
secretKey: z.string().optional(),
|
||||
startDate: z.date().optional(),
|
||||
endDate: z.date().optional(),
|
||||
page: z.coerce.number().optional(),
|
||||
perPage: z.coerce.number().optional()
|
||||
type: z.nativeEnum(AuditLogDateFilterType),
|
||||
relativeModeValue: z.string().optional(),
|
||||
startDate: z.date(),
|
||||
endDate: z.date()
|
||||
})
|
||||
.superRefine((el, ctx) => {
|
||||
if (el.endDate && el.startDate && el.endDate < el.startDate) {
|
||||
@@ -31,10 +41,11 @@ export const auditLogFilterFormSchema = z
|
||||
}
|
||||
});
|
||||
|
||||
export type AuditLogFilterFormData = z.infer<typeof auditLogFilterFormSchema>;
|
||||
export type TAuditLogFilterFormData = z.infer<typeof auditLogFilterFormSchema>;
|
||||
export type TAuditLogDateFilterFormData = z.infer<typeof auditLogDateFilterFormSchema>;
|
||||
|
||||
export type SetValueType = (
|
||||
name: keyof AuditLogFilterFormData,
|
||||
name: keyof TAuditLogFilterFormData,
|
||||
value: any,
|
||||
options?: {
|
||||
shouldValidate?: boolean;
|
||||
|
||||
@@ -105,7 +105,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
|
||||
const { mutateAsync: updateMutateAsync, isPending: updateIsLoading } = useUpdateOIDCConfig();
|
||||
const [isDeletePopupOpen, setIsDeletePopupOpen] = useToggle(false);
|
||||
|
||||
const { data } = useGetOIDCConfig(currentOrg?.slug ?? "");
|
||||
const { data } = useGetOIDCConfig(currentOrg?.id ?? "");
|
||||
|
||||
const { control, handleSubmit, reset, setValue, watch } = useForm<OIDCFormData>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -134,7 +134,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
isActive: false,
|
||||
orgSlug: currentOrg.slug
|
||||
organizationId: currentOrg.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -196,7 +196,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive: true,
|
||||
orgSlug: currentOrg.slug,
|
||||
organizationId: currentOrg.id,
|
||||
jwtSignatureAlgorithm
|
||||
});
|
||||
} else {
|
||||
@@ -212,7 +212,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
|
||||
clientId,
|
||||
clientSecret,
|
||||
isActive: true,
|
||||
orgSlug: currentOrg.slug,
|
||||
organizationId: currentOrg.id,
|
||||
jwtSignatureAlgorithm
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { data, isPending } = useGetOIDCConfig(currentOrg?.slug ?? "");
|
||||
const { data, isPending } = useGetOIDCConfig(currentOrg?.id ?? "");
|
||||
const { mutateAsync } = useUpdateOIDCConfig();
|
||||
const { mutateAsync: updateOrg } = useUpdateOrg();
|
||||
|
||||
@@ -41,7 +41,7 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgSlug: currentOrg?.slug,
|
||||
organizationId: currentOrg?.id,
|
||||
isActive: value
|
||||
});
|
||||
|
||||
@@ -114,7 +114,7 @@ export const OrgOIDCSection = (): JSX.Element => {
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
orgSlug: currentOrg?.slug,
|
||||
organizationId: currentOrg?.id,
|
||||
manageGroupMemberships: value
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export const OrgSsoTab = withPermission(
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { data: oidcConfig, isPending: isLoadingOidcConfig } = useGetOIDCConfig(
|
||||
currentOrg?.slug ?? ""
|
||||
currentOrg?.id ?? ""
|
||||
);
|
||||
const { data: samlConfig, isPending: isLoadingSamlConfig } = useGetSSOConfig(
|
||||
currentOrg?.id ?? ""
|
||||
|
||||
@@ -1,41 +1,37 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { BreadcrumbTypes } from '@app/components/v2'
|
||||
import { workspaceKeys } from '@app/hooks/api'
|
||||
import {
|
||||
fetchUserProjectPermissions,
|
||||
roleQueryKeys,
|
||||
} from '@app/hooks/api/roles/queries'
|
||||
import { fetchWorkspaceById } from '@app/hooks/api/workspace/queries'
|
||||
import { ProjectLayout } from '@app/layouts/ProjectLayout'
|
||||
import { ProjectSelect } from '@app/layouts/ProjectLayout/components/ProjectSelect'
|
||||
import { BreadcrumbTypes } from "@app/components/v2";
|
||||
import { workspaceKeys } from "@app/hooks/api";
|
||||
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
|
||||
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
|
||||
import { ProjectLayout } from "@app/layouts/ProjectLayout";
|
||||
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout',
|
||||
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout"
|
||||
)({
|
||||
component: ProjectLayout,
|
||||
beforeLoad: async ({ params, context }) => {
|
||||
const project = await context.queryClient.ensureQueryData({
|
||||
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
|
||||
queryFn: () => fetchWorkspaceById(params.projectId),
|
||||
})
|
||||
queryFn: () => fetchWorkspaceById(params.projectId)
|
||||
});
|
||||
|
||||
await context.queryClient.ensureQueryData({
|
||||
queryKey: roleQueryKeys.getUserProjectPermissions({
|
||||
workspaceId: params.projectId,
|
||||
workspaceId: params.projectId
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchUserProjectPermissions({ workspaceId: params.projectId }),
|
||||
})
|
||||
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
|
||||
});
|
||||
|
||||
return {
|
||||
project,
|
||||
breadcrumbs: [
|
||||
{
|
||||
type: BreadcrumbTypes.Component,
|
||||
component: ProjectSelect,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
component: ProjectSelect
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||
upgrade your subscription
|
||||
</a>
|
||||
</a>
|
||||
)}
|
||||
)}{" "}
|
||||
to view integration logs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { TRailwaySync } from "@app/hooks/api/secretSyncs/types/railway-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
secretSync: TRailwaySync;
|
||||
};
|
||||
|
||||
export const RailwaySyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
};
|
||||
+6
@@ -17,11 +17,13 @@ import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol";
|
||||
import { HerokuSyncDestinationCol } from "./HerokuSyncDestinationCol";
|
||||
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
|
||||
import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol";
|
||||
import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol";
|
||||
import { RenderSyncDestinationCol } from "./RenderSyncDestinationCol";
|
||||
import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol";
|
||||
import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol";
|
||||
import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol";
|
||||
import { WindmillSyncDestinationCol } from "./WindmillSyncDestinationCol";
|
||||
import { ZabbixSyncDestinationCol } from "./ZabbixSyncDestinationCol";
|
||||
|
||||
type Props = {
|
||||
secretSync: TSecretSync;
|
||||
@@ -73,6 +75,10 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
return <GitLabSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.CloudflarePages:
|
||||
return <CloudflarePagesSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.Zabbix:
|
||||
return <ZabbixSyncDestinationCol secretSync={secretSync} />;
|
||||
case SecretSync.Railway:
|
||||
return <RailwaySyncDestinationCol secretSync={secretSync} />;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { TZabbixSync } from "@app/hooks/api/secretSyncs/types/zabbix-sync";
|
||||
|
||||
import { getSecretSyncDestinationColValues } from "../helpers";
|
||||
import { SecretSyncTableCell } from "../SecretSyncTableCell";
|
||||
|
||||
type Props = {
|
||||
secretSync: TZabbixSync;
|
||||
};
|
||||
|
||||
export const ZabbixSyncDestinationCol = ({ secretSync }: Props) => {
|
||||
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
|
||||
|
||||
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user