diff --git a/api/server/handlers/datastore/get.go b/api/server/handlers/datastore/get.go index 26583e23a0..952483c038 100644 --- a/api/server/handlers/datastore/get.go +++ b/api/server/handlers/datastore/get.go @@ -142,6 +142,13 @@ func (c *GetDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound)) return } + connectedClusterIds := make([]uint, 0) + if matchingDatastore.ConnectedClusters != nil { + for _, cc := range matchingDatastore.ConnectedClusters.ConnectedClusterIds { + connectedClusterIds = append(connectedClusterIds, uint(cc)) + } + } + encoded, err := helpers.MarshalContractObject(ctx, matchingDatastore) if err != nil { err = telemetry.Error(ctx, span, err, "error marshaling datastore") @@ -159,6 +166,8 @@ func (c *GetDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) Status: string(datastoreRecord.Status), CloudProvider: SupportedDatastoreCloudProvider_AWS, CloudProviderCredentialIdentifier: datastoreRecord.CloudProviderCredentialIdentifier, + ConnectedClusterIds: connectedClusterIds, + OnManagementCluster: true, B64Proto: b64, } diff --git a/api/server/handlers/datastore/list.go b/api/server/handlers/datastore/list.go index 4a915774b3..a5893bf3e7 100644 --- a/api/server/handlers/datastore/list.go +++ b/api/server/handlers/datastore/list.go @@ -10,7 +10,6 @@ import ( "github.com/porter-dev/porter/api/server/authz" "github.com/porter-dev/porter/api/server/handlers" "github.com/porter-dev/porter/api/server/handlers/cloud_provider" - "github.com/porter-dev/porter/api/server/handlers/environment_groups" "github.com/porter-dev/porter/api/server/shared" "github.com/porter-dev/porter/api/server/shared/apierrors" "github.com/porter-dev/porter/api/server/shared/config" @@ -140,8 +139,6 @@ func Datastores(ctx context.Context, inp DatastoresInput) ([]datastore.Datastore CloudProvider: inp.CloudProvider.Type, CloudProviderAccountId: inp.CloudProvider.AccountID, Name: inp.Name, - IncludeEnvGroup: inp.IncludeEnvGroup, - IncludeMetadata: inp.IncludeMetadata, } if inp.Type != porterv1.EnumDatastore_ENUM_DATASTORE_UNSPECIFIED { message.Type = &inp.Type @@ -168,18 +165,10 @@ func Datastores(ctx context.Context, inp DatastoresInput) ([]datastore.Datastore Engine: datastoreRecord.Engine, CreatedAtUTC: datastoreRecord.CreatedAt, Status: string(datastoreRecord.Status), - Metadata: ds.Metadata, CloudProvider: datastoreRecord.CloudProvider, CloudProviderCredentialIdentifier: datastoreRecord.CloudProviderCredentialIdentifier, - } - if inp.IncludeEnvGroup && ds.Env != nil { - encodedDatastore.Env = environment_groups.EnvironmentGroupListItem{ - Name: ds.Env.Name, - LatestVersion: int(ds.Env.Version), - Variables: ds.Env.Variables, - SecretVariables: ds.Env.SecretVariables, - LinkedApplications: ds.Env.LinkedApplications, - } + ConnectedClusterIds: []uint{uint(ds.ConnectedClusterId)}, + OnManagementCluster: false, } datastores = append(datastores, encodedDatastore) } diff --git a/api/server/handlers/environment_groups/list.go b/api/server/handlers/environment_groups/list.go index 7301f8bb88..773dda10b0 100644 --- a/api/server/handlers/environment_groups/list.go +++ b/api/server/handlers/environment_groups/list.go @@ -193,6 +193,7 @@ func (c *ListEnvironmentGroupsHandler) ServeHTTP(w http.ResponseWriter, r *http. } var translateProtoTypeToEnvGroupType = map[porterv1.EnumEnvGroupProviderType]string{ - porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_DOPPLER: "doppler", - porterv1.EnumEnvGroupProviderType_ENUM_ENV_GROUP_PROVIDER_TYPE_PORTER: "porter", + 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_PORTER: "porter", } diff --git a/dashboard/src/lib/databases/types.ts b/dashboard/src/lib/databases/types.ts index ba053612b9..9bbdf8b01a 100644 --- a/dashboard/src/lib/databases/types.ts +++ b/dashboard/src/lib/databases/types.ts @@ -55,6 +55,8 @@ export const datastoreValidator = z.object({ cloud_provider: z.string().pipe(cloudProviderValidator.catch("UNKNOWN")), cloud_provider_credential_identifier: z.string(), credential: datastoreCredentialValidator, + connected_cluster_ids: z.number().array().optional().default([]), + on_management_cluster: z.boolean().default(false), }); export type SerializedDatastore = z.infer; diff --git a/dashboard/src/lib/env-groups/types.ts b/dashboard/src/lib/env-groups/types.ts index 208ece998d..bc681edfd2 100644 --- a/dashboard/src/lib/env-groups/types.ts +++ b/dashboard/src/lib/env-groups/types.ts @@ -18,7 +18,21 @@ export const envGroupFormValidator = z.object({ locked: z.boolean(), }) ) - .min(1, { message: "At least one environment variable is required" }) + .min(1, { message: "At least one environment variable is required" }), }); -export type EnvGroupFormData = z.infer; \ No newline at end of file +export type EnvGroupFormData = z.infer; + +export const envGroupValidator = z.object({ + name: z.string(), + variables: z.record(z.string()).optional().default({}), + secret_variables: z.record(z.string()).optional().default({}), + created_at: z.string(), + type: z + .string() + .pipe( + z.enum(["UNKNOWN", "datastore", "doppler", "porter"]).catch("UNKNOWN") + ), +}); + +export type ClientEnvGroup = z.infer; diff --git a/dashboard/src/lib/hooks/useDatabaseMethods.ts b/dashboard/src/lib/hooks/useDatabaseMethods.ts index bc6b8776df..4740e6ac3e 100644 --- a/dashboard/src/lib/hooks/useDatabaseMethods.ts +++ b/dashboard/src/lib/hooks/useDatabaseMethods.ts @@ -13,11 +13,9 @@ type DatastoreHook = { attachDatastoreToAppInstances: ({ name, appInstanceIds, - clusterId, }: { name: string; appInstanceIds: string[]; - clusterId: number; }) => Promise; }; type CreateDatastoreInput = { diff --git a/dashboard/src/lib/hooks/useEnvGroups.ts b/dashboard/src/lib/hooks/useEnvGroups.ts new file mode 100644 index 0000000000..6cd4ce6b98 --- /dev/null +++ b/dashboard/src/lib/hooks/useEnvGroups.ts @@ -0,0 +1,50 @@ +import { useContext } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { z } from "zod"; + +import { envGroupValidator, type ClientEnvGroup } from "lib/env-groups/types"; + +import api from "shared/api"; +import { Context } from "shared/Context"; + +type TUseEnvGroupList = { + envGroups: ClientEnvGroup[]; + isLoading: boolean; +}; +export const useEnvGroupList = ({ + clusterId, +}: { + clusterId?: number; +}): TUseEnvGroupList => { + const { currentProject } = useContext(Context); + + const envGroupReq = useQuery( + ["getEnvGroups", currentProject?.id], + async () => { + if (!currentProject?.id || currentProject.id === -1 || !clusterId) { + return; + } + + const res = await api.getAllEnvGroups( + "", + {}, + { + id: currentProject?.id, + cluster_id: clusterId, + } + ); + const parsed = await z + .object({ environment_groups: z.array(envGroupValidator) }) + .parseAsync(res.data); + return parsed.environment_groups; + }, + { + enabled: !!currentProject && currentProject.id !== -1, + } + ); + + return { + envGroups: envGroupReq.data ?? [], + isLoading: envGroupReq.isLoading, + }; +}; 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 63dd43f6fa..5698dc1070 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 @@ -1,10 +1,6 @@ -import React, {useContext, useMemo} from "react"; -import styled from "styled-components"; - +import React, { useContext, useMemo } from "react"; import { useHistory } from "react-router"; - -import doppler from "assets/doppler.png"; -import key from "assets/key.svg"; +import styled from "styled-components"; import Container from "components/porter/Container"; import Expandable from "components/porter/Expandable"; @@ -12,83 +8,117 @@ import Image from "components/porter/Image"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import EnvGroupArray from "main/home/env-dashboard/EnvGroupArray"; -import {envGroupPath} from "shared/util"; -import {Context} from "shared/Context"; + +import { Context } from "shared/Context"; +import { envGroupPath } from "shared/util"; +import database from "assets/database.svg"; +import doppler from "assets/doppler.png"; +import key from "assets/key.svg"; type Props = { onRemove: (name: string) => void; envGroup: { name: string; - id: number; type: string; - isActive: boolean; variables: Record; secret_variables: Record; }; + canDelete?: boolean; }; // TODO: support footer for consolidation w/ app services -const EnvGroupRow: React.FC = ({ envGroup, onRemove }) => { +const EnvGroupRow: React.FC = ({ + envGroup, + onRemove, + canDelete = true, +}) => { const { currentProject } = useContext(Context); const history = useHistory(); const variables = useMemo(() => { - const normalVariables = Object.entries( - envGroup.variables || {} - ).map(([key, value]) => ({ - key, - value, - hidden: value.includes("PORTERSECRET"), - locked: value.includes("PORTERSECRET"), - deleted: false, - })); - - const secretVariables = Object.entries( - envGroup.secret_variables || {} - ).map(([key, value]) => ({ - key, - value, - hidden: true, - locked: true, - deleted: false, - })); - + const normalVariables = Object.entries(envGroup.variables || {}).map( + ([key, value]) => ({ + key, + value, + hidden: value.includes("PORTERSECRET"), + locked: value.includes("PORTERSECRET"), + deleted: false, + }) + ); + + const secretVariables = Object.entries(envGroup.secret_variables || {}).map( + ([key, value]) => ({ + key, + value, + hidden: true, + locked: true, + deleted: false, + }) + ); + return [...normalVariables, ...secretVariables]; }, [envGroup]); return ( {envGroup.name} - { - history.push(envGroupPath(currentProject, `/${envGroup.name}/synced-apps`)) + { + history.push( + envGroupPath(currentProject, `/${envGroup.name}/synced-apps`) + ); }} - data-testid="geist-icon" fill="none" height="27px" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round" strokeLinejoin="round" stroke-width="2" viewBox="0 0 24 24" width="27px" data-darkreader-inline-stroke="" data-darkreader-inline-color=""> - - { onRemove(envGroup.name) }} + data-testid="geist-icon" + fill="none" + height="27px" + shape-rendering="geometricPrecision" + stroke="currentColor" + stroke-linecap="round" + strokeLinejoin="round" + stroke-width="2" + viewBox="0 0 24 24" + width="27px" + data-darkreader-inline-stroke="" + data-darkreader-inline-color="" > - delete - + + + + + {canDelete && ( + <> + + { + onRemove(envGroup.name); + }} + > + delete + + + )} - )} + } > - + ); }; @@ -113,4 +143,4 @@ const Svg = styled.svg` :hover { stroke: white; } -`; \ No newline at end of file +`; diff --git a/dashboard/src/main/home/database-dashboard/DatabaseTabs.tsx b/dashboard/src/main/home/database-dashboard/DatabaseTabs.tsx index 09a6765afc..5fb5c69cec 100644 --- a/dashboard/src/main/home/database-dashboard/DatabaseTabs.tsx +++ b/dashboard/src/main/home/database-dashboard/DatabaseTabs.tsx @@ -7,7 +7,7 @@ import TabSelector from "components/TabSelector"; import { useDatastoreContext } from "./DatabaseContextProvider"; import DatastoreProvisioningIndicator from "./DatastoreProvisioningIndicator"; -import ConnectedAppsTab from "./tabs/ConnectedAppsTab"; +import ConfigurationTab from "./tabs/ConfigurationTab"; import ConnectTab from "./tabs/ConnectTab"; import MetricsTab from "./tabs/MetricsTab"; import SettingsTab from "./tabs/SettingsTab"; @@ -43,9 +43,9 @@ const DatabaseTabs: React.FC = ({ tabParam }) => { const tabs = useMemo(() => { return [ - { label: "Connect", value: "connect" }, - { label: "Connected Apps", value: "connected-apps" }, - // { label: "Configuration", value: "configuration" }, + { label: "Connectivity", value: "connect" }, + // { label: "Connected Apps", value: "connected-apps" }, + { label: "Configuration", value: "configuration" }, { label: "Settings", value: "settings" }, ]; }, []); @@ -69,10 +69,9 @@ const DatabaseTabs: React.FC = ({ tabParam }) => { .with("connect", () => ) .with("settings", () => ) .with("metrics", () => ) - // .with("configuration", () => ) - .with("connected-apps", () => ) + .with("configuration", () => ) + // .with("connected-apps", () => ) .otherwise(() => null)} - ); }; diff --git a/dashboard/src/main/home/database-dashboard/shared/ConnectAppsModal.tsx b/dashboard/src/main/home/database-dashboard/shared/ConnectAppsModal.tsx index b39ac29950..4040ac8fdf 100644 --- a/dashboard/src/main/home/database-dashboard/shared/ConnectAppsModal.tsx +++ b/dashboard/src/main/home/database-dashboard/shared/ConnectAppsModal.tsx @@ -1,8 +1,10 @@ -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import axios from "axios"; import pluralize from "pluralize"; +import styled from "styled-components"; import { z } from "zod"; +import Loading from "components/Loading"; import Button from "components/porter/Button"; import Error from "components/porter/Error"; import Icon from "components/porter/Icon"; @@ -10,51 +12,111 @@ import Modal from "components/porter/Modal"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import SelectableAppList from "main/home/app-dashboard/apps/SelectableAppList"; -import { type AppRevisionWithSource } from "main/home/app-dashboard/apps/types"; +import { + appRevisionWithSourceValidator, + type AppRevisionWithSource, +} from "main/home/app-dashboard/apps/types"; +import EnvGroupRow from "main/home/app-dashboard/validate-apply/app-settings/EnvGroupRow"; +import { useDatastoreMethods } from "lib/hooks/useDatabaseMethods"; +import { useEnvGroupList } from "lib/hooks/useEnvGroups"; import { useIntercom } from "lib/hooks/useIntercom"; +import api from "shared/api"; import connect from "assets/connect.svg"; +import { useDatastoreContext } from "../DatabaseContextProvider"; + type Props = { closeModal: () => void; - apps: AppRevisionWithSource[]; - onSubmit: (appInstanceIds: string[]) => Promise; }; -const ConnectAppsModal: React.FC = ({ closeModal, apps, onSubmit }) => { - const [selectedAppInstanceIds, setSelectedAppInstanceIds] = useState< - string[] +const ConnectAppsModal: React.FC = ({ closeModal }) => { + const { datastore, projectId } = useDatastoreContext(); + const { attachDatastoreToAppInstances } = useDatastoreMethods(); + const { envGroups, isLoading } = useEnvGroupList({ + clusterId: datastore.connected_cluster_ids.length + ? datastore.connected_cluster_ids[0] + : undefined, + }); + const matchingEnvGroup = useMemo(() => { + return envGroups.find((eg) => eg.name === datastore.name); + }, [envGroups, datastore]); + const [clusterConnectedApps, setClusterConnectedApps] = useState< + AppRevisionWithSource[] + >([]); + + useEffect(() => { + const fetchClusterConnectedApps = async (): Promise => { + try { + const res = await Promise.all( + datastore.connected_cluster_ids.map(async (clusterId) => { + return await api.getLatestAppRevisions( + "", + { + deployment_target_id: undefined, + ignore_preview_apps: true, + }, + { cluster_id: clusterId, project_id: projectId } + ); + }) + ); + const apps = await Promise.all( + res.map(async (r) => { + const parsed = await z + .object({ + app_revisions: z.array(appRevisionWithSourceValidator), + }) + .parseAsync(r.data); + return parsed.app_revisions; + }) + ); + setClusterConnectedApps(apps.flat()); + } catch (err) { + // TODO: handle error + } + }; + void fetchClusterConnectedApps(); + }, [datastore.connected_cluster_ids, projectId]); + + const [selectedAppInstances, setSelectedAppInstances] = useState< + AppRevisionWithSource[] >([]); const [isSubmitting, setIsSubmitting] = useState(false); const [submitErrorMessage, setSubmitErrorMessage] = useState(""); const { showIntercomWithMessage } = useIntercom(); const append = useCallback( - (appInstanceId: string): void => { - if (!selectedAppInstanceIds.includes(appInstanceId)) { - setSelectedAppInstanceIds([...selectedAppInstanceIds, appInstanceId]); + (appInstance: AppRevisionWithSource): void => { + if ( + !selectedAppInstances + .map((s) => s.app_revision.app_instance_id) + .includes(appInstance.app_revision.app_instance_id) + ) { + setSelectedAppInstances([...selectedAppInstances, appInstance]); } }, - [selectedAppInstanceIds] + [selectedAppInstances] ); const remove = useCallback( - (appInstanceId: string): void => { - setSelectedAppInstanceIds( - selectedAppInstanceIds.filter((id) => id !== appInstanceId) + (appInstance: AppRevisionWithSource): void => { + setSelectedAppInstances( + selectedAppInstances.filter( + (a) => a.app_revision.app_instance_id !== appInstance.app_revision.id + ) ); }, - [selectedAppInstanceIds] - ); - const isSelected = useCallback( - (appInstanceId: string): boolean => { - return selectedAppInstanceIds.includes(appInstanceId); - }, - [selectedAppInstanceIds] + [selectedAppInstances] ); + const submit = useCallback(async () => { try { setIsSubmitting(true); - await onSubmit(selectedAppInstanceIds); + await attachDatastoreToAppInstances({ + name: datastore.name, + appInstanceIds: selectedAppInstances.map( + (a) => a.app_revision.app_instance_id + ), + }); closeModal(); } catch (err) { let message = "Please contact support."; @@ -73,7 +135,7 @@ const ConnectAppsModal: React.FC = ({ closeModal, apps, onSubmit }) => { } finally { setIsSubmitting(false); } - }, [onSubmit, selectedAppInstanceIds]); + }, [selectedAppInstances]); const submitButtonStatus = useMemo(() => { if (isSubmitting) { @@ -87,56 +149,111 @@ const ConnectAppsModal: React.FC = ({ closeModal, apps, onSubmit }) => { return ""; }, [isSubmitting, submitErrorMessage]); + if (isLoading) { + return ( + + Inject credentials into apps + + + + ); + } + if (datastore.connected_cluster_ids.length === 0) { + return ( + + Inject credentials into apps + + + No clusters are connected to this datastore. Please connect a cluster + first. + + + ); + } + + if (!matchingEnvGroup) { + return ( + + Inject credentials into apps + + + The env group for this datastore has not yet been created. Please add + credentials to your application environment variables manually. + + + ); + } + return ( - Select apps - - {apps.length === 0 && ( + + Inject credentials into apps + - No apps are available to connect. Please create an app first. + The following env group contains credentials for your datastore: - )} - {apps.length !== 0 && ( - <> - ({ - app: a, - key: a.source.name, - onSelect: () => { - append(a.app_revision.app_instance_id); - }, - onDeselect: () => { - remove(a.app_revision.app_instance_id); - }, - isSelected: isSelected(a.app_revision.app_instance_id), - }))} - /> - - - Click the button below to confirm the above selections. Newly - connected apps may take a few seconds to appear on the dashboard. + + ({})} + envGroup={matchingEnvGroup} + canDelete={false} + /> + + Select apps + + + Select the apps you want to link this env group to. + + + {clusterConnectedApps.length === 0 && ( + + No apps are available. Please create an app first. - - )} - - + )} + {clusterConnectedApps.length !== 0 && ( + <> + ({ + app: a, + key: a.source.name, + onSelect: () => { + append(a); + }, + onDeselect: () => { + remove(a); + }, + isSelected: selectedAppInstances + .map((s) => s.app_revision.app_instance_id) + .includes(a.app_revision.app_instance_id), + }))} + /> + + + + )} + ); }; export default ConnectAppsModal; + +const InnerModalContents = styled.div` + overflow-y: auto; + max-height: 80vh; +`; diff --git a/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx b/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx index 137649139f..095c05e313 100644 --- a/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx +++ b/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx @@ -22,93 +22,121 @@ const ConnectionInfo: React.FC = ({ connectionInfo, type }) => { return (
- Host - - {connectionInfo.host} - - Port - - {connectionInfo.port.toString()} - - {type === DATASTORE_TYPE_ELASTICACHE ? ( - <> - Auth token - - - {isPasswordHidden ? ( - <> - {connectionInfo.password} - - { - setIsPasswordHidden(false); - }} - > - Reveal - - - ) : ( - <> - - {connectionInfo.password} - - - { - setIsPasswordHidden(true); - }} - > - Hide - - - )} - - - ) : ( - <> - Database name - - - {connectionInfo.database_name} - - - Username - - {connectionInfo.username} - - Password - - - {isPasswordHidden ? ( - <> - {connectionInfo.password} - - { - setIsPasswordHidden(false); - }} - > - Reveal - - - ) : ( - <> - - {connectionInfo.password} - - - { - setIsPasswordHidden(true); - }} - > - Hide - - - )} - - - )} + + + + + + + + + + + {type === DATASTORE_TYPE_ELASTICACHE ? ( + + + + + ) : ( + <> + + + + + + + + + + + + + + )} + +
+ Host + + {connectionInfo.host} +
+ Port + + + {connectionInfo.port.toString()} + +
+ Auth token + + {isPasswordHidden ? ( + + {connectionInfo.password} + + { + setIsPasswordHidden(false); + }} + > + Reveal + + + ) : ( + + + {connectionInfo.password} + + + { + setIsPasswordHidden(true); + }} + > + Hide + + + )} +
+ Database name + + + {connectionInfo.database_name} + +
+ Username + + + {connectionInfo.username} + +
+ Password + + {isPasswordHidden ? ( + + {connectionInfo.password} + + { + setIsPasswordHidden(false); + }} + > + Reveal + + + ) : ( + + + {connectionInfo.password} + + + { + setIsPasswordHidden(true); + }} + > + Hide + + + )} +
); }; diff --git a/dashboard/src/main/home/database-dashboard/tabs/ConfigurationTab.tsx b/dashboard/src/main/home/database-dashboard/tabs/ConfigurationTab.tsx index 9bafff8f44..dd93be752e 100644 --- a/dashboard/src/main/home/database-dashboard/tabs/ConfigurationTab.tsx +++ b/dashboard/src/main/home/database-dashboard/tabs/ConfigurationTab.tsx @@ -1,36 +1,33 @@ -import React from "react"; -import styled from "styled-components"; +import React, { useMemo } from "react"; -import Fieldset from "components/porter/Fieldset"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; +import { ClusterList } from "main/home/infrastructure-dashboard/ClusterDashboard"; +import { useClusterList } from "lib/hooks/useCluster"; import { useDatastoreContext } from "../DatabaseContextProvider"; -import DatabaseHeaderItem from "../DatabaseHeaderItem"; const ConfigurationTab: React.FC = () => { const { datastore } = useDatastoreContext(); + const { clusters } = useClusterList(); + + const connectedClusters = useMemo(() => { + return clusters.filter((cluster) => { + return datastore.connected_cluster_ids.includes(cluster.id); + }); + }, [clusters, datastore.connected_cluster_ids]); return ( -
- Datastore details: +
+ Connected clusters - - {datastore.metadata !== undefined && datastore.metadata?.length > 0 && ( - - {datastore.metadata?.map((item, index) => ( - - ))} - - )} -
+ + Porter automatically manages connectivity between connected clusters and + this datastore. + + + + ); }; export default ConfigurationTab; - -const GridList = styled.div` - display: grid; - grid-column-gap: 25px; - grid-row-gap: 25px; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); -`; diff --git a/dashboard/src/main/home/database-dashboard/tabs/ConnectTab.tsx b/dashboard/src/main/home/database-dashboard/tabs/ConnectTab.tsx index f36b5c3681..d0c9043e37 100644 --- a/dashboard/src/main/home/database-dashboard/tabs/ConnectTab.tsx +++ b/dashboard/src/main/home/database-dashboard/tabs/ConnectTab.tsx @@ -1,87 +1,134 @@ -import React from "react"; +import React, { useState } from "react"; import styled from "styled-components"; import CopyToClipboard from "components/CopyToClipboard"; +import Banner from "components/porter/Banner"; import Container from "components/porter/Container"; import Link from "components/porter/Link"; +import ShowIntercomButton from "components/porter/ShowIntercomButton"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; import copy from "assets/copy-left.svg"; import { useDatastoreContext } from "../DatabaseContextProvider"; +import ConnectAppsModal from "../shared/ConnectAppsModal"; import ConnectionInfo from "../shared/ConnectionInfo"; const ConnectTab: React.FC = () => { const { datastore } = useDatastoreContext(); + const [showConnectAppsModal, setShowConnectAppsModal] = useState(false); + if (datastore.credential.host === "") { + return ( + + + Talk to support + + + } + > + Error reaching your datastore for credentials. Please contact support. + + + ); + } return ( - - - Application connection - - {datastore.credential.host !== "" && ( - <> - - - All apps deployed in your cluster can access this datastore using - the following credentials: - - - - - - For security, access to the datastore is restricted - connection - attempts from outside the cluster will not succeed. - - - - The datastore client of your application should use these - credentials to create a connection. - + +
+ + Application connection + + + + An application deployed in one of this datastore's connected + clusters can use the following credentials to access the datastore: + + + + + + For security, access to the datastore is restricted - connection + attempts from outside a connected cluster will not succeed. + + + + The datastore client of your application should use these credentials + to create a connection.{" "} {datastore.template.type.name === "ELASTICACHE" && ( - <> - - - Your datastore client must connect via SSL. - - + + The datastore client must connect via SSL. + )} - - )} - - Local connection - - - For local connection, you can create a temporary, secure tunnel to this - datastore using the{" "} - + + { + setShowConnectAppsModal(true); + }} > - Porter CLI - - - - - {`$ porter datastore connect ${datastore.name}`} - - - - - - - + add + Inject these credentials into an app + + {showConnectAppsModal && ( + { + setShowConnectAppsModal(false); + }} + /> + )} +
+
+ Local connection + + + For local connection, you can create a temporary, secure tunnel to + this datastore using the{" "} + + Porter CLI + + + + + {`$ porter datastore connect ${datastore.name}`} + + + + + + +
+
); }; export default ConnectTab; -const CredentialsTabContainer = styled.div` +const ConnectTabContainer = styled.div` width: 100%; + height: 100%; + display: flex; + flex-direction: row; `; const IdContainer = styled.div` @@ -115,3 +162,34 @@ const CopyIcon = styled.img` const Code = styled.span` font-family: monospace; `; + +const ConnectAppButton = styled.div` + color: #aaaabb; + background: ${({ theme }) => theme.fg}; + border: 1px solid #494b4f; + :hover { + border: 1px solid #7a7b80; + color: white; + } + display: flex; + align-items: center; + border-radius: 5px; + height: 40px; + font-size: 13px; + width: 100%; + padding-left: 10px; + cursor: pointer; + .add-icon { + width: 30px; + font-size: 20px; + } +`; + +const I = styled.i` + color: white; + font-size: 14px; + display: flex; + align-items: center; + margin-right: 7px; + justify-content: center; +`; diff --git a/dashboard/src/main/home/database-dashboard/tabs/SettingsTab.tsx b/dashboard/src/main/home/database-dashboard/tabs/SettingsTab.tsx index d37b477cf5..4dd5fd3b41 100644 --- a/dashboard/src/main/home/database-dashboard/tabs/SettingsTab.tsx +++ b/dashboard/src/main/home/database-dashboard/tabs/SettingsTab.tsx @@ -45,19 +45,17 @@ const SettingsTab: React.FC = () => { return ( - - Delete "{datastore.name}" - - - Delete this datastore and all of its resources. - - - - + Delete "{datastore.name}" + + + Delete this datastore and all of its resources. + + + ); }; @@ -76,16 +74,3 @@ const StyledTemplateComponent = styled.div` } } `; - -const InnerWrapper = styled.div<{ full?: boolean }>` - width: 100%; - height: ${(props) => (props.full ? "100%" : "calc(100% - 65px)")}; - padding: 30px; - padding-bottom: 15px; - position: relative; - overflow: auto; - margin-bottom: 30px; - border-radius: 5px; - background: ${(props) => props.theme.fg}; - border: 1px solid #494b4f; -`; diff --git a/dashboard/src/main/home/env-dashboard/EnvDashboard.tsx b/dashboard/src/main/home/env-dashboard/EnvDashboard.tsx index 02f5a61c2e..59b488f780 100644 --- a/dashboard/src/main/home/env-dashboard/EnvDashboard.tsx +++ b/dashboard/src/main/home/env-dashboard/EnvDashboard.tsx @@ -30,6 +30,7 @@ import key from "assets/key.svg"; import list from "assets/list.png"; import notFound from "assets/not-found.png"; import time from "assets/time.png"; +import database from "assets/database.svg"; import { envGroupPath } from "../../../shared/util"; @@ -201,7 +202,7 @@ const EnvDashboard: React.FC = (props) => { > @@ -227,7 +228,7 @@ const EnvDashboard: React.FC = (props) => { key={i} > - + {envGroup.name} diff --git a/dashboard/src/main/home/env-dashboard/ExpandedEnv.tsx b/dashboard/src/main/home/env-dashboard/ExpandedEnv.tsx index f6c6690ef1..65ecf07ddb 100644 --- a/dashboard/src/main/home/env-dashboard/ExpandedEnv.tsx +++ b/dashboard/src/main/home/env-dashboard/ExpandedEnv.tsx @@ -15,6 +15,7 @@ import TabSelector from "components/TabSelector"; import api from "shared/api"; import { Context } from "shared/Context"; import doppler from "assets/doppler.png"; +import database from "assets/database.svg"; import key from "assets/key.svg"; import notFound from "assets/not-found.png"; import time from "assets/time.png"; @@ -108,7 +109,7 @@ const ExpandedEnv: React.FC = () => { diff --git a/dashboard/src/main/home/env-dashboard/tabs/EnvVarsTab.tsx b/dashboard/src/main/home/env-dashboard/tabs/EnvVarsTab.tsx index 6fbe0260b8..882f311d99 100644 --- a/dashboard/src/main/home/env-dashboard/tabs/EnvVarsTab.tsx +++ b/dashboard/src/main/home/env-dashboard/tabs/EnvVarsTab.tsx @@ -198,7 +198,7 @@ const EnvVarsTab: React.FC = ({ envGroup, fetchEnvGroup }) => { secretOption={true} disabled={envGroup.type === "doppler"} /> - {envGroup.type !== "doppler" && ( + {envGroup.type !== "doppler" && envGroup.type !== "datastore" && ( <>