From b765d7e6ae396fae3297b19ec54b500776999ff8 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Tue, 23 Apr 2024 12:24:13 -0400 Subject: [PATCH] infisical ui updates (#4570) --- .../are_external_providers_enabled.go | 45 ++++- .../handlers/environment_groups/delete.go | 14 +- .../handlers/environment_groups/list.go | 1 + .../handlers/environment_groups/update.go | 58 +++++- dashboard/src/assets/infisical.svg | 65 ++++++ dashboard/src/lib/env-groups/types.ts | 5 +- .../app-settings/EnvGroupRow.tsx | 25 ++- .../env-groups/ExpandedEnvGroup.tsx | 2 +- .../main/home/env-dashboard/EnvDashboard.tsx | 98 +++++---- .../main/home/env-dashboard/EnvGroupArray.tsx | 6 +- .../main/home/env-dashboard/ExpandedEnv.tsx | 75 +++---- .../home/env-dashboard/tabs/EnvVarsTab.tsx | 45 ++--- .../home/env-dashboard/tabs/SettingsTab.tsx | 2 +- .../integrations/DopplerIntegrationList.tsx | 36 +++- .../integrations/IntegrationCategories.tsx | 9 +- .../main/home/integrations/Integrations.tsx | 4 +- .../infisical/AddInfisicalEnvModal.tsx | 162 +++++++++++++++ .../infisical/InfisicalIntegrationList.tsx | 187 ++++++++++++++++++ dashboard/src/shared/api.tsx | 4 + dashboard/src/shared/common.tsx | 6 + 20 files changed, 703 insertions(+), 146 deletions(-) create mode 100644 dashboard/src/assets/infisical.svg create mode 100644 dashboard/src/main/home/integrations/infisical/AddInfisicalEnvModal.tsx create mode 100644 dashboard/src/main/home/integrations/infisical/InfisicalIntegrationList.tsx diff --git a/api/server/handlers/environment_groups/are_external_providers_enabled.go b/api/server/handlers/environment_groups/are_external_providers_enabled.go index 6fac4efa4a..d0054fd0ff 100644 --- a/api/server/handlers/environment_groups/are_external_providers_enabled.go +++ b/api/server/handlers/environment_groups/are_external_providers_enabled.go @@ -34,8 +34,20 @@ func NewAreExternalProvidersEnabledHandler( } } -// AreExternalProvidersEnabledResponse is the response object for the /environment-group/are-external-providers-enabled endpoint -type AreExternalProvidersEnabledResponse struct { +// ExternalEnvGroupOperator is the type of external env group operator, which syncs secrets from external sources +type ExternalEnvGroupOperator string + +const ( + // ExternalEnvGroupOperator_ExternalSecrets is the external secrets operator + ExternalEnvGroupOperator_ExternalSecrets ExternalEnvGroupOperator = "external-secrets" + // ExternalEnvGroupOperator_Infisical is the infisical secrets operator + ExternalEnvGroupOperator_Infisical ExternalEnvGroupOperator = "infisical" +) + +// ExternalEnvGroupOperatorEnabledStatus is the status of an external env group operator +type ExternalEnvGroupOperatorEnabledStatus struct { + // Type is the type of external provider + Type ExternalEnvGroupOperator `json:"type"` // Enabled is true if external providers are enabled Enabled bool `json:"enabled"` // ReprovisionRequired is true if the cluster needs to be reprovisioned to enable external providers @@ -44,6 +56,11 @@ type AreExternalProvidersEnabledResponse struct { K8SUpgradeRequired bool `json:"k8s_upgrade_required"` } +// AreExternalProvidersEnabledResponse is the response object for the /environment-group/are-external-providers-enabled endpoint +type AreExternalProvidersEnabledResponse struct { + Operators []ExternalEnvGroupOperatorEnabledStatus `json:"operators"` +} + // ServeHTTP checks if external providers are enabled func (c *AreExternalProvidersEnabledHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx, span := telemetry.NewSpan(r.Context(), "serve-are-external-providers-enabled") @@ -62,9 +79,27 @@ func (c *AreExternalProvidersEnabledHandler) ServeHTTP(w http.ResponseWriter, r return } + var operators []ExternalEnvGroupOperatorEnabledStatus + for _, operator := range resp.Msg.Operators { + var operatorType ExternalEnvGroupOperator + switch operator.Operator { + case porterv1.EnumExternalEnvGroupOperatorType_ENUM_EXTERNAL_ENV_GROUP_OPERATOR_TYPE_EXTERNAL_SECRETS: + operatorType = ExternalEnvGroupOperator_ExternalSecrets + case porterv1.EnumExternalEnvGroupOperatorType_ENUM_EXTERNAL_ENV_GROUP_OPERATOR_TYPE_INFISICAL: + operatorType = ExternalEnvGroupOperator_Infisical + default: + continue + } + + operators = append(operators, ExternalEnvGroupOperatorEnabledStatus{ + Type: operatorType, + Enabled: operator.Enabled, + ReprovisionRequired: operator.ReprovisionRequired, + K8SUpgradeRequired: operator.K8SUpgradeRequired, + }) + } + c.WriteResult(w, r, &AreExternalProvidersEnabledResponse{ - Enabled: resp.Msg.Enabled, - ReprovisionRequired: resp.Msg.ReprovisionRequired, - K8SUpgradeRequired: resp.Msg.K8SUpgradeRequired, + Operators: operators, }) } diff --git a/api/server/handlers/environment_groups/delete.go b/api/server/handlers/environment_groups/delete.go index da89fe787b..33ee81ce13 100644 --- a/api/server/handlers/environment_groups/delete.go +++ b/api/server/handlers/environment_groups/delete.go @@ -61,11 +61,19 @@ func (c *DeleteEnvironmentGroupHandler) ServeHTTP(w http.ResponseWriter, r *http ) switch request.Type { - case "doppler": + case "doppler", "infisical": + var provider porterv1.EnumEnvGroupProviderType + switch request.Type { + case "doppler": + provider = porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER + case "infisical": + provider = porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_INFISICAL + } + _, err := c.Config().ClusterControlPlaneClient.DeleteEnvGroup(ctx, connect.NewRequest(&porterv1.DeleteEnvGroupRequest{ ProjectId: int64(cluster.ProjectID), ClusterId: int64(cluster.ID), - EnvGroupProviderType: porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER, + EnvGroupProviderType: provider, EnvGroupName: request.Name, })) if err != nil { @@ -88,4 +96,6 @@ func (c *DeleteEnvironmentGroupHandler) ServeHTTP(w http.ResponseWriter, r *http return } } + + c.WriteResult(w, r, nil) } diff --git a/api/server/handlers/environment_groups/list.go b/api/server/handlers/environment_groups/list.go index cc72583890..a7e7252b65 100644 --- a/api/server/handlers/environment_groups/list.go +++ b/api/server/handlers/environment_groups/list.go @@ -218,5 +218,6 @@ func (c *ListEnvironmentGroupsHandler) ServeHTTP(w http.ResponseWriter, r *http. var translateProtoTypeToEnvGroupType = map[porterv1.EnumEnvGroupProviderType]string{ porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DATASTORE: "datastore", porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER: "doppler", + porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_INFISICAL: "infisical", porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_PORTER: "porter", } diff --git a/api/server/handlers/environment_groups/update.go b/api/server/handlers/environment_groups/update.go index 7ff9678a18..ecfae256f0 100644 --- a/api/server/handlers/environment_groups/update.go +++ b/api/server/handlers/environment_groups/update.go @@ -34,6 +34,22 @@ func NewUpdateEnvironmentGroupHandler( } } +// EnvironmentGroupType is the env_groups-level environment group type +type EnvironmentGroupType string + +const ( + // EnvironmentGroupType_Unspecified is the nil environment group type + EnvironmentGroupType_Unspecified EnvironmentGroupType = "" + // EnvironmentGroupType_Doppler is the doppler environment group type + EnvironmentGroupType_Doppler EnvironmentGroupType = "doppler" + // EnvironmentGroupType_Porter is the porter environment group type + EnvironmentGroupType_Porter EnvironmentGroupType = "porter" + // EnvironmentGroupType_Datastore is the datastore environment group type + EnvironmentGroupType_Datastore EnvironmentGroupType = "datastore" + // EnvironmentGroupType_Infisical is the infisical environment group type + EnvironmentGroupType_Infisical EnvironmentGroupType = "infisical" +) + // EnvVariableDeletions is the set of keys to delete from the environment group type EnvVariableDeletions struct { // Variables is a set of variable keys to delete from the environment group @@ -42,12 +58,20 @@ type EnvVariableDeletions struct { Secrets []string `json:"secrets"` } +// InfisicalEnv is the Infisical environment to pull secret values from, only required for the Infisical external provider type +type InfisicalEnv struct { + // Slug is the slug referring to the Infisical environment to pull secret values from + Slug string `json:"slug"` + // Path is the relative path in the Infisical environment to pull secret values from + Path string `json:"path"` +} + type UpdateEnvironmentGroupRequest struct { // Name of the env group to create or update Name string `json:"name"` // Type of the env group to create or update - Type string `json:"type"` + Type EnvironmentGroupType `json:"type"` // AuthToken for the env group AuthToken string `json:"auth_token"` @@ -69,6 +93,9 @@ type UpdateEnvironmentGroupRequest struct { // SkipAppAutoDeploy is a flag to determine if the app should be auto deployed SkipAppAutoDeploy bool `json:"skip_app_auto_deploy"` + + // InfisicalEnv is the Infisical environment to pull secret values from, only required for the Infisical external provider type + InfisicalEnv InfisicalEnv `json:"infisical_env"` } type UpdateEnvironmentGroupResponse struct { // Name of the env group to create or update @@ -99,13 +126,38 @@ func (c *UpdateEnvironmentGroupHandler) ServeHTTP(w http.ResponseWriter, r *http ) switch request.Type { - case "doppler": + case EnvironmentGroupType_Doppler, EnvironmentGroupType_Infisical: + var provider porterv1.EnumEnvGroupProviderType + var infisicalEnv *porterv1.InfisicalEnv + if request.Type == EnvironmentGroupType_Doppler { + provider = porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER + } + if request.Type == EnvironmentGroupType_Infisical { + if request.InfisicalEnv.Slug == "" { + err := telemetry.Error(ctx, span, nil, "infisical env slug is required") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + if request.InfisicalEnv.Path == "" { + err := telemetry.Error(ctx, span, nil, "infisical env path is required") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + + provider = porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_INFISICAL + infisicalEnv = &porterv1.InfisicalEnv{ + EnvironmentSlug: request.InfisicalEnv.Slug, + EnvironmentPath: request.InfisicalEnv.Path, + } + } + _, err := c.Config().ClusterControlPlaneClient.CreateOrUpdateEnvGroup(ctx, connect.NewRequest(&porterv1.CreateOrUpdateEnvGroupRequest{ ProjectId: int64(cluster.ProjectID), ClusterId: int64(cluster.ID), - EnvGroupProviderType: porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER, + EnvGroupProviderType: provider, EnvGroupName: request.Name, EnvGroupAuthToken: request.AuthToken, + InfisicalEnv: infisicalEnv, })) if err != nil { err := telemetry.Error(ctx, span, err, "unable to create environment group") diff --git a/dashboard/src/assets/infisical.svg b/dashboard/src/assets/infisical.svg new file mode 100644 index 0000000000..efb4821c6b --- /dev/null +++ b/dashboard/src/assets/infisical.svg @@ -0,0 +1,65 @@ + + + + + + \ No newline at end of file diff --git a/dashboard/src/lib/env-groups/types.ts b/dashboard/src/lib/env-groups/types.ts index 36d50e2068..bbea0ea062 100644 --- a/dashboard/src/lib/env-groups/types.ts +++ b/dashboard/src/lib/env-groups/types.ts @@ -32,10 +32,13 @@ export const envGroupValidator = z.object({ variables: z.record(z.string()).optional().default({}), secret_variables: z.record(z.string()).optional().default({}), created_at: z.string(), + linked_applications: z.array(z.string()).optional().default([]), type: z .string() .pipe( - z.enum(["UNKNOWN", "datastore", "doppler", "porter"]).catch("UNKNOWN") + z + .enum(["UNKNOWN", "datastore", "doppler", "porter", "infisical"]) + .catch("UNKNOWN") ), }); diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroupRow.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroupRow.tsx index 7796746ce0..819f21ca9d 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroupRow.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/app-settings/EnvGroupRow.tsx @@ -14,6 +14,7 @@ import { Context } from "shared/Context"; import { envGroupPath } from "shared/util"; import database from "assets/database.svg"; import doppler from "assets/doppler.png"; +import infisical from "assets/infisical.svg"; import key from "assets/key.svg"; type Props = { @@ -65,22 +66,26 @@ const EnvGroupRow: React.FC = ({ return [...normalVariables, ...secretVariables]; }, [envGroup]); + const envGroupIcon = useMemo(() => { + if (envGroup.type === "doppler") { + return doppler; + } + if (envGroup.type === "datastore") { + return database; + } + if (envGroup.type === "infisical") { + return infisical; + } + return key; + }, [envGroup.type]); + return ( - + {envGroup.name} diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx index 5de5438038..8c640d625a 100644 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx +++ b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx @@ -613,7 +613,7 @@ export const ExpandedEnvGroupFC = ({ const linkedApp: string[] = currentEnvGroup?.linked_applications; // doppler env groups update themselves, and we don't want to increment the version - if (currentEnvGroup?.type !== "doppler") { + if (currentEnvGroup?.type !== "doppler" && currentEnvGroup.type !== "infisical") { await api.createEnvironmentGroups( "", { diff --git a/dashboard/src/main/home/env-dashboard/EnvDashboard.tsx b/dashboard/src/main/home/env-dashboard/EnvDashboard.tsx index 872bc64fe1..29e69cbd9d 100644 --- a/dashboard/src/main/home/env-dashboard/EnvDashboard.tsx +++ b/dashboard/src/main/home/env-dashboard/EnvDashboard.tsx @@ -1,8 +1,10 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; +import React, { useContext, useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import _ from "lodash"; import { withRouter, type RouteComponentProps } from "react-router"; import { Link } from "react-router-dom"; import styled from "styled-components"; +import { z } from "zod"; import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder"; import Loading from "components/Loading"; @@ -17,6 +19,7 @@ import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import Toggle from "components/porter/Toggle"; import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader"; +import { envGroupValidator, type ClientEnvGroup } from "lib/env-groups/types"; import api from "shared/api"; import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc"; @@ -27,6 +30,7 @@ import database from "assets/database.svg"; import doppler from "assets/doppler.png"; import envGroupGrad from "assets/env-group-grad.svg"; import grid from "assets/grid.png"; +import infisical from "assets/infisical.svg"; import key from "assets/key.svg"; import list from "assets/list.png"; import notFound from "assets/not-found.png"; @@ -40,10 +44,32 @@ const EnvDashboard: React.FC = (props) => { const { currentProject, currentCluster } = useContext(Context); const [searchValue, setSearchValue] = useState(""); - const [envGroups, setEnvGroups] = useState<[]>([]); - const [isLoading, setIsLoading] = useState(true); const [view, setView] = useState<"grid" | "list">("grid"); - const [hasError, setHasError] = useState(false); + + const { data: { environment_groups: envGroups = [] } = {}, status } = + useQuery( + ["envGroups", currentProject?.id, currentCluster?.id], + async () => { + if (!currentProject || !currentCluster) { + return { environment_groups: [] }; + } + const res = await api.getAllEnvGroups( + "", + {}, + { + id: currentProject?.id || -1, + cluster_id: currentCluster?.id || -1, + } + ); + + const data = await z + .object({ + environment_groups: z.array(envGroupValidator), + }) + .parseAsync(res.data); + return data; + } + ); const filteredEnvGroups = useMemo(() => { const filteredBySearch = search(envGroups, searchValue, { @@ -55,31 +81,18 @@ const EnvDashboard: React.FC = (props) => { return sortedFilteredBySearch; }, [envGroups, searchValue]); - const updateEnvGroups = async (): Promise => { - try { - const res = await api.getAllEnvGroups( - "", - {}, - { - id: currentProject?.id || -1, - cluster_id: currentCluster?.id || -1, - } - ); - setEnvGroups(res.data.environment_groups); - setIsLoading(false); - } catch (err) { - setHasError(true); - setIsLoading(false); + const getIconFromType = (type: ClientEnvGroup["type"]): string => { + if (type === "doppler") { + return doppler; + } else if (type === "datastore") { + return database; + } else if (type === "infisical") { + return infisical; + } else { + return key; } }; - useEffect(() => { - setIsLoading(true); - if ((currentProject?.id ?? -1) > -1 && (currentCluster?.id ?? -1) > -1) { - void updateEnvGroups(); - } - }, [currentProject, currentCluster]); - const renderContents = (): React.ReactNode => { if (currentProject?.sandbox_enabled) { return ( @@ -105,7 +118,11 @@ const EnvDashboard: React.FC = (props) => { return ; } - if (!isLoading && (!envGroups || envGroups.length === 0)) { + if (status === "loading") { + return ; + } + + if (envGroups.length === 0) { return ( No environment groups found @@ -178,7 +195,7 @@ const EnvDashboard: React.FC = (props) => { - {!isLoading && filteredEnvGroups.length === 0 ? ( + {status === "success" && filteredEnvGroups.length === 0 ? (
@@ -188,8 +205,6 @@ const EnvDashboard: React.FC = (props) => {
- ) : isLoading ? ( - ) : view === "grid" ? ( {(filteredEnvGroups ?? []).map((envGroup, i: number) => { @@ -199,16 +214,7 @@ const EnvDashboard: React.FC = (props) => { key={i} > - + {envGroup.name} @@ -225,22 +231,14 @@ const EnvDashboard: React.FC = (props) => { ) : ( - {(filteredEnvGroups ?? []).map((envGroup: any, i: number) => { + {(filteredEnvGroups ?? []).map((envGroup, i) => { return ( - + {envGroup.name} diff --git a/dashboard/src/main/home/env-dashboard/EnvGroupArray.tsx b/dashboard/src/main/home/env-dashboard/EnvGroupArray.tsx index 9db0b73eae..e8ccf8f192 100644 --- a/dashboard/src/main/home/env-dashboard/EnvGroupArray.tsx +++ b/dashboard/src/main/home/env-dashboard/EnvGroupArray.tsx @@ -21,14 +21,14 @@ export type KeyValueType = { type PropsType = { label?: string; values: KeyValueType[]; - setValues: (x: KeyValueType[]) => void; + setValues?: (x: KeyValueType[]) => void; disabled?: boolean; fileUpload?: boolean; secretOption?: boolean; setButtonDisabled?: (x: boolean) => void; }; -const EnvGroupArray = ({ +const EnvGroupArray: React.FC = ({ label, values, setValues = () => {}, @@ -36,7 +36,7 @@ const EnvGroupArray = ({ fileUpload, secretOption, setButtonDisabled, -}: PropsType): React.ReactElement => { +}) => { const [showEditorModal, setShowEditorModal] = useState(false); const blankValues = (): void => { const isAnyEnvVariableBlank = values.some( diff --git a/dashboard/src/main/home/env-dashboard/ExpandedEnv.tsx b/dashboard/src/main/home/env-dashboard/ExpandedEnv.tsx index 2818b4cca1..dbbb0c4f74 100644 --- a/dashboard/src/main/home/env-dashboard/ExpandedEnv.tsx +++ b/dashboard/src/main/home/env-dashboard/ExpandedEnv.tsx @@ -1,7 +1,9 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; +import React, { useContext, useEffect, useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; import { useHistory, useParams } from "react-router"; import styled from "styled-components"; import { match } from "ts-pattern"; +import { z } from "zod"; import Loading from "components/Loading"; import Back from "components/porter/Back"; @@ -11,11 +13,13 @@ import Link from "components/porter/Link"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import TabSelector from "components/TabSelector"; +import { envGroupValidator } from "lib/env-groups/types"; import api from "shared/api"; import { Context } from "shared/Context"; import database from "assets/database.svg"; import doppler from "assets/doppler.png"; +import infisical from "assets/infisical.svg"; import key from "assets/key.svg"; import notFound from "assets/not-found.png"; import time from "assets/time.png"; @@ -43,9 +47,6 @@ const ExpandedEnv: React.FC = () => { }>(); const history = useHistory(); - const [isLoading, setIsLoading] = useState(true); - const [envGroup, setEnvGroup] = useState(null); - const tabs = useMemo(() => { return [ { label: "Environment variables", value: "env-vars" }, @@ -54,30 +55,34 @@ const ExpandedEnv: React.FC = () => { ]; }, []); - const fetchEnvGroup = async () => { - try { + const { + data: envGroup, + isLoading, + refetch, + } = useQuery( + ["envGroups", currentProject?.id, currentCluster?.id, envGroupName], + async () => { + if (!currentProject || !currentCluster) { + return null; + } const res = await api.getAllEnvGroups( "", {}, { - id: currentProject?.id ?? -1, - cluster_id: currentCluster?.id ?? -1, + id: currentProject?.id || -1, + cluster_id: currentCluster?.id || -1, } ); - const matchedEnvGroup = res.data.environment_groups.find((x) => { - return x.name === envGroupName; - }); - setIsLoading(false); - setEnvGroup(matchedEnvGroup); - } catch (err) { - setIsLoading(false); - } - }; - useEffect(() => { - setIsLoading(true); - void fetchEnvGroup(); - }, [currentProject, currentCluster, envGroupName]); + const data = await z + .object({ + environment_groups: z.array(envGroupValidator), + }) + .parseAsync(res.data); + + return data.environment_groups.find((eg) => eg.name === envGroupName); + } + ); useEffect(() => { if (!tab) { @@ -85,6 +90,19 @@ const ExpandedEnv: React.FC = () => { } }, [tab]); + const envGroupIcon = useMemo(() => { + if (envGroup?.type === "doppler") { + return doppler; + } + if (envGroup?.type === "datastore") { + return database; + } + if (envGroup?.type === "infisical") { + return infisical; + } + return key; + }, [envGroup?.type]); + return ( <> {isLoading && } @@ -108,16 +126,7 @@ const ExpandedEnv: React.FC = () => { - + {envGroupName} @@ -145,9 +154,7 @@ const ExpandedEnv: React.FC = () => { {match(tab) .with("env-vars", () => { - return ( - - ); + return ; }) .with("synced-apps", () => ) .with("settings", () => ) diff --git a/dashboard/src/main/home/env-dashboard/tabs/EnvVarsTab.tsx b/dashboard/src/main/home/env-dashboard/tabs/EnvVarsTab.tsx index ffb4e76c1f..df23c70c03 100644 --- a/dashboard/src/main/home/env-dashboard/tabs/EnvVarsTab.tsx +++ b/dashboard/src/main/home/env-dashboard/tabs/EnvVarsTab.tsx @@ -17,7 +17,7 @@ import { import api from "shared/api"; import { Context } from "shared/Context"; -import EnvGroupArray from "../EnvGroupArray"; +import EnvGroupArray, { type KeyValueType } from "../EnvGroupArray"; type Props = { envGroup: { @@ -46,13 +46,7 @@ const EnvVarsTab: React.FC = ({ envGroup, fetchEnvGroup }) => { resolver: zodResolver(envGroupFormValidator), reValidateMode: "onSubmit", }); - const { - formState: { isValidating, isSubmitting }, - watch, - trigger, - handleSubmit, - setValue, - } = envGroupFormMethods; + const { watch, trigger, handleSubmit, setValue } = envGroupFormMethods; const [submitErrorMessage, setSubmitErrorMessage] = useState(""); const [isValid, setIsValid] = useState(false); @@ -108,6 +102,14 @@ const EnvVarsTab: React.FC = ({ envGroup, fetchEnvGroup }) => { setValue("envFiles", envGroup.files || []); }, [envGroup]); + const isUpdatable = useMemo(() => { + return ( + envGroup.type !== "doppler" && + envGroup.type !== "datastore" && + envGroup.type !== "infisical" + ); + }, [envGroup.type]); + const onSubmit = handleSubmit(async (data) => { setButtonStatus("loading"); setSubmitErrorMessage(""); @@ -149,7 +151,7 @@ const EnvVarsTab: React.FC = ({ envGroup, fetchEnvGroup }) => { apiEnvVariables[envVar.key] = envVar.value; }); - if (envGroup?.type !== "doppler") { + if (envGroup?.type !== "doppler" && envGroup?.type !== "infisical") { await api.createEnvironmentGroups( "", { @@ -178,24 +180,14 @@ const EnvVarsTab: React.FC = ({ envGroup, fetchEnvGroup }) => { } }); - const submitButtonStatus = useMemo(() => { - if (isSubmitting || isValidating) { - return "loading"; - } - if (submitErrorMessage) { - return ; - } - return undefined; - }, [isSubmitting, submitErrorMessage, isValidating]); - return ( <> Environment variables - {envGroup.type === "doppler" ? ( + {envGroup.type === "doppler" || envGroup.type === "infisical" ? ( - Doppler environment variables can only be updated from the Doppler - dashboard. + {envGroup.type === "doppler" ? "Doppler" : "Infisical"} environment + variables can only be updated from the Doppler dashboard. ) : ( @@ -216,7 +208,7 @@ const EnvVarsTab: React.FC = ({ envGroup, fetchEnvGroup }) => { }} fileUpload={true} secretOption={true} - disabled={envGroup.type === "doppler"} + disabled={!isUpdatable} /> Environment files @@ -232,7 +224,7 @@ const EnvVarsTab: React.FC = ({ envGroup, fetchEnvGroup }) => { setValue("envFiles", x); }} /> - {envGroup.type !== "doppler" && envGroup.type !== "datastore" && ( + {isUpdatable ? ( <> - )} + ) : null} diff --git a/dashboard/src/main/home/env-dashboard/tabs/SettingsTab.tsx b/dashboard/src/main/home/env-dashboard/tabs/SettingsTab.tsx index 3b89b44807..886ff59160 100644 --- a/dashboard/src/main/home/env-dashboard/tabs/SettingsTab.tsx +++ b/dashboard/src/main/home/env-dashboard/tabs/SettingsTab.tsx @@ -47,7 +47,7 @@ const SettingsTab: React.FC = ({ envGroup }) => { }; const handleDeletionSubmit = async (): Promise => { - if (envGroup?.linked_applications) { + if (envGroup?.linked_applications.length) { setButtonStatus( ); diff --git a/dashboard/src/main/home/integrations/DopplerIntegrationList.tsx b/dashboard/src/main/home/integrations/DopplerIntegrationList.tsx index e1ae75384f..ecdac7b676 100644 --- a/dashboard/src/main/home/integrations/DopplerIntegrationList.tsx +++ b/dashboard/src/main/home/integrations/DopplerIntegrationList.tsx @@ -43,6 +43,10 @@ const DopplerIntegrationList: React.FC = (_) => { currentCluster?.id, ], async () => { + if (!currentProject || !currentCluster) { + return; + } + const res = await api.areExternalEnvGroupProvidersEnabled( "", {}, @@ -50,13 +54,26 @@ const DopplerIntegrationList: React.FC = (_) => { ); const externalEnvGroupProviderStatus = await z .object({ - enabled: z.boolean(), - reprovision_required: z.boolean(), - k8s_upgrade_required: z.boolean(), + operators: z.array( + z.object({ + type: z.enum(["infisical", "external-secrets"]), + enabled: z.boolean(), + reprovision_required: z.boolean(), + k8s_upgrade_required: z.boolean(), + }) + ), }) .parseAsync(res.data); - return externalEnvGroupProviderStatus; + return ( + externalEnvGroupProviderStatus.operators.find( + (o) => o.type === "external-secrets" + ) || { + enabled: false, + reprovision_required: true, + k8s_upgrade_required: false, + } + ); }, { enabled: !!currentProject && !!currentCluster, @@ -114,7 +131,7 @@ const DopplerIntegrationList: React.FC = (_) => { ) .then(() => { setShowServiceTokenModal(false); - history.push("/env-groups"); + history.push("/environment-groups"); }) .catch((err) => { let message = @@ -148,7 +165,14 @@ const DopplerIntegrationList: React.FC = (_) => { ) : externalProviderStatus?.reprovision_required ? ( To enable integration with Doppler, - + re-provision your cluster . diff --git a/dashboard/src/main/home/integrations/IntegrationCategories.tsx b/dashboard/src/main/home/integrations/IntegrationCategories.tsx index 7b0241c43b..8f18e108aa 100644 --- a/dashboard/src/main/home/integrations/IntegrationCategories.tsx +++ b/dashboard/src/main/home/integrations/IntegrationCategories.tsx @@ -14,6 +14,7 @@ import TitleSection from "components/TitleSection"; import GitlabIntegrationList from "./GitlabIntegrationList"; import leftArrow from "assets/left-arrow.svg"; import Spacer from "components/porter/Spacer"; +import InfisicalIntegrationList from "./infisical/InfisicalIntegrationList"; type Props = RouteComponentProps & { category: string; @@ -103,7 +104,7 @@ const IntegrationCategories: React.FC = (props) => { useEffect(() => { getIntegrationsForCategory(props.category); - if (props.category === "doppler") { + if (props.category === "doppler" || props.category === "infisical") { setLoading(false); } }, [props.category]); @@ -131,14 +132,14 @@ const IntegrationCategories: React.FC = (props) => { {label} - {props.category === "doppler" ? null : ( + {props.category === "doppler" || props.category === "infisical" ? null : ( + + + ); +}; diff --git a/dashboard/src/main/home/integrations/infisical/InfisicalIntegrationList.tsx b/dashboard/src/main/home/integrations/infisical/InfisicalIntegrationList.tsx new file mode 100644 index 0000000000..6597678b7e --- /dev/null +++ b/dashboard/src/main/home/integrations/infisical/InfisicalIntegrationList.tsx @@ -0,0 +1,187 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { z } from "zod"; + +import Loading from "components/Loading"; +import Placeholder from "components/Placeholder"; +import Banner from "components/porter/Banner"; +import Button from "components/porter/Button"; +import Link from "components/porter/Link"; +import Spacer from "components/porter/Spacer"; +import ToggleRow from "components/porter/ToggleRow"; + +import api from "shared/api"; +import { Context } from "shared/Context"; + +import { AddInfisicalEnvModal } from "./AddInfisicalEnvModal"; + +const InfisicalIntegrationList: React.FC = (_) => { + const [infisicalToggled, setInfisicalToggled] = useState(false); + const [infisicalEnabled, setInfisicalEnabled] = useState(false); + const [showServiceTokenModal, setShowServiceTokenModal] = + useState(false); + + const { currentCluster, currentProject } = useContext(Context); + + const { + data: externalProviderStatus, + isLoading: isExternalProviderStatusLoading, + } = useQuery( + [ + "areExternalEnvGroupProvidersEnabled", + currentProject?.id, + currentCluster?.id, + ], + async () => { + if (!currentProject || !currentCluster) { + return; + } + + const res = await api.areExternalEnvGroupProvidersEnabled( + "", + {}, + { id: currentProject?.id, cluster_id: currentCluster?.id } + ); + const externalEnvGroupProviderStatus = await z + .object({ + operators: z.array( + z.object({ + type: z.enum(["infisical", "external-secrets"]), + enabled: z.boolean(), + reprovision_required: z.boolean(), + k8s_upgrade_required: z.boolean(), + }) + ), + }) + .parseAsync(res.data); + + return ( + externalEnvGroupProviderStatus.operators.find( + (o) => o.type === "infisical" + ) || { + enabled: false, + reprovision_required: true, + k8s_upgrade_required: false, + } + ); + }, + { + enabled: !!currentProject && !!currentCluster, + refetchInterval: 5000, + refetchOnWindowFocus: false, + } + ); + + useEffect(() => { + if (externalProviderStatus) { + setInfisicalToggled(externalProviderStatus.enabled); + setInfisicalEnabled(externalProviderStatus.enabled); + } + }, [externalProviderStatus]); + + const installInfisical = async (): Promise => { + if (!currentCluster || !currentProject) { + return; + } + + try { + setInfisicalToggled(true); + + await api.enableExternalEnvGroupProviders( + "", + {}, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } + ); + } catch (err) { + setInfisicalToggled(false); + } + }; + + if (!infisicalEnabled) { + return ( + <> + {isExternalProviderStatusLoading ? ( + + + + ) : externalProviderStatus?.k8s_upgrade_required ? ( + + Cluster must be upgraded to Kubernetes v1.27 to integrate with + Infisical. + + ) : externalProviderStatus?.reprovision_required ? ( + + To enable integration with Infisical, + + re-provision your cluster + + . + + ) : ( + <> + + + {infisicalToggled + ? "Enabling Infisical integration . . ." + : "Enable Infisical integration"} + + + + + Enable the Infisical integration to add environment groups from + Infisical. + + + )} + + ); + } + + return ( + <> + + + {infisicalToggled + ? infisicalEnabled + ? "Infisical integration enabled" + : "Enabling Infisical integration . . ." + : "Enable Infisical integration"} + + + + + + {showServiceTokenModal && ( + + )} + + ); +}; + +export default InfisicalIntegrationList; diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx index cd90f0ef76..d83975f022 100644 --- a/dashboard/src/shared/api.tsx +++ b/dashboard/src/shared/api.tsx @@ -2313,6 +2313,10 @@ const createEnvironmentGroups = baseApi< type?: string; auth_token?: string; is_env_override?: boolean; + infisical_env?: { + slug: string; + path: string; + } }, { id: number; diff --git a/dashboard/src/shared/common.tsx b/dashboard/src/shared/common.tsx index e71ad9f770..1278abd741 100644 --- a/dashboard/src/shared/common.tsx +++ b/dashboard/src/shared/common.tsx @@ -4,6 +4,7 @@ import gcp from "../assets/gcp.png"; import github from "../assets/github.png"; import azure from "assets/azure.png"; import doppler from "assets/doppler.png"; +import infisical from "assets/infisical.svg"; export const infraNames: any = { ecr: "Elastic Container Registry (ECR)", @@ -20,6 +21,11 @@ export const integrationList: any = { label: "Doppler", buttonText: "Add a service token", }, + infisical: { + icon: infisical, + label: "Infisical", + buttonText: "Add an API key", + }, kubernetes: { icon: "https://upload.wikimedia.org/wikipedia/labs/thumb/b/ba/Kubernetes-icon-color.svg/2110px-Kubernetes-icon-color.svg.png",