mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 06:29:37 +00:00
feat(super-admin): Environment Overrides
This commit is contained in:
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ export type TServerConfig = {
|
||||
authConsentContent?: string;
|
||||
pageFrameContent?: string;
|
||||
invalidatingCache: boolean;
|
||||
envOverrides?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type TUpdateServerConfigDTO = {
|
||||
@@ -61,6 +62,7 @@ export type TUpdateServerConfigDTO = {
|
||||
gitHubAppConnectionSlug?: string;
|
||||
gitHubAppConnectionId?: string;
|
||||
gitHubAppConnectionPrivateKey?: string;
|
||||
envOverrides?: Record<string, string>;
|
||||
} & Partial<TServerConfig>;
|
||||
|
||||
export type TCreateAdminUserDTO = {
|
||||
@@ -138,3 +140,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 }[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ const generalTabs = [
|
||||
label: "Caching",
|
||||
icon: "note",
|
||||
link: "/admin/caching"
|
||||
},
|
||||
{
|
||||
label: "Environment",
|
||||
icon: "unlock",
|
||||
link: "/admin/environment"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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"
|
||||
description="Manage the environment for your Infisical instance."
|
||||
/>
|
||||
<EnvironmentPageForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faExclamationTriangle, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useGetEnvOverrides, useUpdateServerConfig } from "@app/hooks/api";
|
||||
|
||||
type TForm = Record<string, string>;
|
||||
|
||||
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) => {
|
||||
const value = formValues?.[field.key] ?? "";
|
||||
return (
|
||||
field.key.toLowerCase().includes(searchTerm) ||
|
||||
(field.description ?? "").toLowerCase().includes(searchTerm) ||
|
||||
value.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"
|
||||
});
|
||||
|
||||
reset(formData);
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update environment overrides"
|
||||
});
|
||||
}
|
||||
},
|
||||
[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>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-300">Override specific environment variables.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline_bg"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
Save Overrides
|
||||
</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 gap-4">
|
||||
{filteredData.map((group) => (
|
||||
<div key={group!.name}>
|
||||
<span className="mb-2 text-sm text-mineshaft-300">{group!.name}</span>
|
||||
<div className="rounded-lg border border-mineshaft-700 bg-mineshaft-800">
|
||||
<div className="flex flex-col">
|
||||
{group!.fields.map((field, i) => (
|
||||
<div
|
||||
key={field.key}
|
||||
className={`flex items-center justify-between gap-4 border-mineshaft-500 p-4 ${
|
||||
i === 0 ? "" : "border-t"
|
||||
}`}
|
||||
>
|
||||
<div className="flex max-w-lg flex-col">
|
||||
<span className="text-sm">{field.key}</span>
|
||||
<span className="text-sm text-mineshaft-400">{field.description}</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-red" />
|
||||
</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>
|
||||
</div>
|
||||
))}
|
||||
</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: async () => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Admin",
|
||||
link: linkOptions({ to: "/admin" })
|
||||
},
|
||||
{
|
||||
label: "Environment",
|
||||
link: linkOptions({
|
||||
to: "/admin/environment"
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -43,6 +43,7 @@ import { Route as authProviderSuccessPageRouteImport } from './pages/auth/Provid
|
||||
import { Route as authProviderErrorPageRouteImport } from './pages/auth/ProviderErrorPage/route'
|
||||
import { Route as userPersonalSettingsPageRouteImport } from './pages/user/PersonalSettingsPage/route'
|
||||
import { Route as adminIntegrationsPageRouteImport } from './pages/admin/IntegrationsPage/route'
|
||||
import { Route as adminEnvironmentPageRouteImport } from './pages/admin/EnvironmentPage/route'
|
||||
import { Route as adminEncryptionPageRouteImport } from './pages/admin/EncryptionPage/route'
|
||||
import { Route as adminCachingPageRouteImport } from './pages/admin/CachingPage/route'
|
||||
import { Route as adminAuthenticationPageRouteImport } from './pages/admin/AuthenticationPage/route'
|
||||
@@ -607,6 +608,12 @@ const adminIntegrationsPageRouteRoute = adminIntegrationsPageRouteImport.update(
|
||||
} as any,
|
||||
)
|
||||
|
||||
const adminEnvironmentPageRouteRoute = adminEnvironmentPageRouteImport.update({
|
||||
id: '/environment',
|
||||
path: '/environment',
|
||||
getParentRoute: () => adminLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const adminEncryptionPageRouteRoute = adminEncryptionPageRouteImport.update({
|
||||
id: '/encryption',
|
||||
path: '/encryption',
|
||||
@@ -2353,6 +2360,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof adminEncryptionPageRouteImport
|
||||
parentRoute: typeof adminLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/environment': {
|
||||
id: '/_authenticate/_inject-org-details/admin/_admin-layout/environment'
|
||||
path: '/environment'
|
||||
fullPath: '/admin/environment'
|
||||
preLoaderRoute: typeof adminEnvironmentPageRouteImport
|
||||
parentRoute: typeof adminLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/integrations': {
|
||||
id: '/_authenticate/_inject-org-details/admin/_admin-layout/integrations'
|
||||
path: '/integrations'
|
||||
@@ -4484,6 +4498,7 @@ interface adminLayoutRouteChildren {
|
||||
adminAuthenticationPageRouteRoute: typeof adminAuthenticationPageRouteRoute
|
||||
adminCachingPageRouteRoute: typeof adminCachingPageRouteRoute
|
||||
adminEncryptionPageRouteRoute: typeof adminEncryptionPageRouteRoute
|
||||
adminEnvironmentPageRouteRoute: typeof adminEnvironmentPageRouteRoute
|
||||
adminIntegrationsPageRouteRoute: typeof adminIntegrationsPageRouteRoute
|
||||
adminMachineIdentitiesResourcesPageRouteRoute: typeof adminMachineIdentitiesResourcesPageRouteRoute
|
||||
adminOrganizationResourcesPageRouteRoute: typeof adminOrganizationResourcesPageRouteRoute
|
||||
@@ -4495,6 +4510,7 @@ const adminLayoutRouteChildren: adminLayoutRouteChildren = {
|
||||
adminAuthenticationPageRouteRoute: adminAuthenticationPageRouteRoute,
|
||||
adminCachingPageRouteRoute: adminCachingPageRouteRoute,
|
||||
adminEncryptionPageRouteRoute: adminEncryptionPageRouteRoute,
|
||||
adminEnvironmentPageRouteRoute: adminEnvironmentPageRouteRoute,
|
||||
adminIntegrationsPageRouteRoute: adminIntegrationsPageRouteRoute,
|
||||
adminMachineIdentitiesResourcesPageRouteRoute:
|
||||
adminMachineIdentitiesResourcesPageRouteRoute,
|
||||
@@ -4697,6 +4713,7 @@ export interface FileRoutesByFullPath {
|
||||
'/admin/authentication': typeof adminAuthenticationPageRouteRoute
|
||||
'/admin/caching': typeof adminCachingPageRouteRoute
|
||||
'/admin/encryption': typeof adminEncryptionPageRouteRoute
|
||||
'/admin/environment': typeof adminEnvironmentPageRouteRoute
|
||||
'/admin/integrations': typeof adminIntegrationsPageRouteRoute
|
||||
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
|
||||
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
|
||||
@@ -4918,6 +4935,7 @@ export interface FileRoutesByTo {
|
||||
'/admin/authentication': typeof adminAuthenticationPageRouteRoute
|
||||
'/admin/caching': typeof adminCachingPageRouteRoute
|
||||
'/admin/encryption': typeof adminEncryptionPageRouteRoute
|
||||
'/admin/environment': typeof adminEnvironmentPageRouteRoute
|
||||
'/admin/integrations': typeof adminIntegrationsPageRouteRoute
|
||||
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
|
||||
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
|
||||
@@ -5139,6 +5157,7 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/authentication': typeof adminAuthenticationPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/caching': typeof adminCachingPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/encryption': typeof adminEncryptionPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/environment': typeof adminEnvironmentPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
|
||||
@@ -5371,6 +5390,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/authentication'
|
||||
| '/admin/caching'
|
||||
| '/admin/encryption'
|
||||
| '/admin/environment'
|
||||
| '/admin/integrations'
|
||||
| '/cert-manager/$projectId'
|
||||
| '/kms/$projectId'
|
||||
@@ -5591,6 +5611,7 @@ export interface FileRouteTypes {
|
||||
| '/admin/authentication'
|
||||
| '/admin/caching'
|
||||
| '/admin/encryption'
|
||||
| '/admin/environment'
|
||||
| '/admin/integrations'
|
||||
| '/cert-manager/$projectId'
|
||||
| '/kms/$projectId'
|
||||
@@ -5810,6 +5831,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout/authentication'
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout/caching'
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout/encryption'
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout/environment'
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout/integrations'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
|
||||
@@ -6267,6 +6289,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/authentication",
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/caching",
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/encryption",
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/environment",
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations",
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities",
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations",
|
||||
@@ -6309,6 +6332,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "admin/EncryptionPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/environment": {
|
||||
"filePath": "admin/EnvironmentPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/admin/_admin-layout/integrations": {
|
||||
"filePath": "admin/IntegrationsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/admin/_admin-layout"
|
||||
|
||||
@@ -8,6 +8,7 @@ const adminRoute = route("/admin", [
|
||||
index("admin/GeneralPage/route.tsx"),
|
||||
route("/encryption", "admin/EncryptionPage/route.tsx"),
|
||||
route("/authentication", "admin/AuthenticationPage/route.tsx"),
|
||||
route("/environment", "admin/EnvironmentPage/route.tsx"),
|
||||
route("/integrations", "admin/IntegrationsPage/route.tsx"),
|
||||
route("/caching", "admin/CachingPage/route.tsx"),
|
||||
route("/resources/organizations", "admin/OrganizationResourcesPage/route.tsx"),
|
||||
|
||||
Reference in New Issue
Block a user