From ab50fa56cc912dbce215b188c3b87fbd202350e7 Mon Sep 17 00:00:00 2001 From: sdess09 <37374498+sdess09@users.noreply.github.com> Date: Wed, 6 Dec 2023 11:16:49 -0500 Subject: [PATCH] Db front end (#4047) --- .../expanded-chart/ExpandedChart.tsx | 104 +++++-- .../database-dashboard/DatabaseDashboard.tsx | 254 ++++++++++++------ dashboard/src/main/home/sidebar/Sidebar.tsx | 109 ++++---- dashboard/src/shared/api.tsx | 124 ++++----- dashboard/src/shared/types.tsx | 4 + 5 files changed, 391 insertions(+), 204 deletions(-) diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx index 42d1901e7b..3eb621be0b 100644 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx +++ b/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx @@ -28,6 +28,9 @@ import BuildSettingsTab from "./build-settings/BuildSettingsTab"; import { DisabledNamespacesForIncidents } from "./incidents/DisabledNamespaces"; import { useStackEnvGroups } from "./useStackEnvGroups"; import DeployStatusSection from "./deploy-status-section/DeployStatusSection"; +import Banner from "components/porter/Banner"; +import Spacer from "components/porter/Spacer"; + type Props = { namespace: string; @@ -48,6 +51,12 @@ const getReadableDate = (s: string) => { return `${time} on ${date}`; }; +const templateWhitelist = [ + "elasticache-redis", + "rds-postgresql", + "rds-postgresql-aurora", +]; + const ExpandedChart: React.FC = (props) => { const [currentChart, setCurrentChart] = useState( props.currentChart @@ -79,6 +88,7 @@ const ExpandedChart: React.FC = (props) => { const [logData, setLogData] = useState({}); const [overrideCurrentTab, setOverrideCurrentTab] = useState(""); const [isAgentInstalled, setIsAgentInstalled] = useState(false); + const [databaseStatus, setDatabaseStatus] = useState(true); const { isStack, @@ -105,6 +115,26 @@ const ExpandedChart: React.FC = (props) => { setOverrideCurrentTab("logs"); }; + const updateDatabaseStatuses = async (): Promise => { + try { + + const statusRes = await api.getDatabaseStatus("", { + name: currentChart.name, + type: currentChart.chart.metadata.name + }, { + project_id: currentProject?.id ?? 0, + cluster_id: currentCluster?.id ?? 0, + }); + if (statusRes.data.status === "available") { + setDatabaseStatus(true); + } + else { + setDatabaseStatus(false); + } + } catch (err) { + setDatabaseStatus(false); + } + }; // Retrieve full chart data (includes form and values) const getChartData = async (chart: ChartType) => { setIsLoadingChartData(true); @@ -192,7 +222,7 @@ const ExpandedChart: React.FC = (props) => { if ( oldControllers && oldControllers[object.metadata.uid]?.status?.conditions == - object.status?.conditions + object.status?.conditions ) { return oldControllers; } @@ -463,7 +493,7 @@ const ExpandedChart: React.FC = (props) => { being deployed {props.currentChart.git_action_config && - props.currentChart.git_action_config.gitlab_integration_id ? ( + props.currentChart.git_action_config.gitlab_integration_id ? ( <> Navigate to the{" "} = (props) => { }); }, [currentChart]); + + useEffect(() => { if (logData.revision) { api @@ -818,7 +850,9 @@ const ExpandedChart: React.FC = (props) => { }); }); }); - + if (templateWhitelist.includes(currentChart.chart.metadata.name)) { + void updateDatabaseStatuses() + } return () => { closeAllWebsockets(); }; @@ -881,6 +915,7 @@ const ExpandedChart: React.FC = (props) => { } }) .catch(console.log); + return () => (isSubscribed = false); }, [components, currentCluster, currentProject, currentChart]); @@ -899,7 +934,7 @@ const ExpandedChart: React.FC = (props) => { isFullscreen={true} setIsFullscreen={setIsFullscreen} currentChart={currentChart} - setInitData={() => {}} + setInitData={() => { }} /> ) : ( @@ -937,15 +972,24 @@ const ExpandedChart: React.FC = (props) => { margin_left={"0px"} /> */} - - - Last deployed - {" " + getReadableDate(currentChart.info.last_deployed)} - + {!templateWhitelist.includes(currentChart.chart.metadata.name) && + <> + Last deployed + {" " + getReadableDate(currentChart.info.last_deployed)} + + } + + {!databaseStatus && + <> + + + Database is being created + + + } {deleting ? ( <> @@ -976,7 +1020,7 @@ const ExpandedChart: React.FC = (props) => { shouldUpdate={ currentChart.latest_version && currentChart.latest_version !== - currentChart.chart.metadata.version + currentChart.chart.metadata.version } latestVersion={currentChart.latest_version} upgradeVersion={handleUpgradeVersion} @@ -1056,7 +1100,8 @@ const ExpandedChart: React.FC = (props) => { )} - )} + ) + } ); }; @@ -1186,11 +1231,11 @@ const TabButton = styled.div` border-radius: 20px; text-shadow: 0px 0px 8px ${(props: { devOpsMode: boolean }) => - props.devOpsMode ? "#ffffff66" : "none"}; + props.devOpsMode ? "#ffffff66" : "none"}; cursor: pointer; :hover { color: ${(props: { devOpsMode: boolean }) => - props.devOpsMode ? "" : "#aaaabb99"}; + props.devOpsMode ? "" : "#aaaabb99"}; } > i { @@ -1279,3 +1324,30 @@ const A = styled.a` text-decoration: underline; cursor: pointer; `; + + +const BannerContents = styled.div` + display: flex; + flex-direction: column; + row-gap: 0.5rem; +`; + +const CloseButton = styled.div` + display: block; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + border-radius: 50%; + cursor: pointer; + :hover { + background-color: #ffffff11; + } + + > i { + font-size: 20px; + color: #aaaabb; + } +`; \ No newline at end of file diff --git a/dashboard/src/main/home/database-dashboard/DatabaseDashboard.tsx b/dashboard/src/main/home/database-dashboard/DatabaseDashboard.tsx index 3716941f44..f2c8d57680 100644 --- a/dashboard/src/main/home/database-dashboard/DatabaseDashboard.tsx +++ b/dashboard/src/main/home/database-dashboard/DatabaseDashboard.tsx @@ -22,7 +22,6 @@ import api from "shared/api"; import { hardcodedIcons } from "shared/hardcodedNameDict"; import { search } from "shared/search"; -import Loading from "components/Loading"; import Button from "components/porter/Button"; import Container from "components/porter/Container"; import Fieldset from "components/porter/Fieldset"; @@ -37,6 +36,7 @@ import { readableDate } from "shared/string_utils"; import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder"; import DashboardPlaceholder from "components/porter/DashboardPlaceholder"; import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader"; +import loading from "assets/loading.gif"; type Props = {}; @@ -46,7 +46,7 @@ const templateWhitelist = [ "rds-postgresql-aurora", ]; -const Apps: React.FC = ({ +const Apps: React.FC = ({ }) => { const { currentProject, currentCluster } = useContext(Context); @@ -57,6 +57,7 @@ const Apps: React.FC = ({ // Placeholder (replace w useQuery) const [databases, setDatabases] = useState([]); const [status, setStatus] = useState(""); + const [databaseStatuses, setDatabaseStatuses] = useState({}); const filteredDatabases = useMemo(() => { const filteredBySearch = search( @@ -71,6 +72,36 @@ const Apps: React.FC = ({ return _.sortBy(filteredBySearch); }, [databases, searchValue]); + const updateDatabaseStatuses = async (): Promise => { + const newStatuses = {}; + for (const db of filteredDatabases) { + try { + if (databaseStatuses[db.name] !== "available") { + console.log(db) + const statusRes = await api.getDatabaseStatus("", { + name: db.name, + type: db.chart.metadata.name + }, { + project_id: currentProject?.id ?? 0, + cluster_id: currentCluster?.id ?? 0, + }); + if (statusRes.data.status === "available") { + newStatuses[db.name] = statusRes.data.status; + } + else { + newStatuses[db.name] = "updating"; + } + }// Assuming status is returned in this field + } catch (err) { + console.error("Error fetching database status for", db.name, err); + newStatuses[db.name] = "error"; // Or some error state + } + + } + setDatabaseStatuses(newStatuses); + }; + + const getExpandedChartLinkURL = useCallback((x: any) => { const params = new Proxy(new URLSearchParams(window.location.search), { get: (searchParams, prop: string) => searchParams.get(prop), @@ -126,13 +157,54 @@ const Apps: React.FC = ({ }; }; + useEffect(() => { + // Call once when the component mounts + void updateDatabaseStatuses(); + + // Set up the interval for polling every 5 minutes + const intervalId = setInterval(() => { + void updateDatabaseStatuses(); + }, 60000); // 60000 milliseconds = 5 minutes + + // Clear interval on component unmount + return () => clearInterval(intervalId); + }, [filteredDatabases]); + useEffect(() => { // currentCluster sometimes returns as -1 and passes null check + if (currentProject?.id >= 0 && currentCluster?.id >= 0) { getAddOns(); } }, [currentCluster, currentProject]); + const renderStatusIcon = (dbName: string): JSX.Element => { + const status: string = databaseStatuses[dbName]; + switch (status) { + case "available": + return ; + case "": + return <>; + case "error": + return + + + {"Creating database"} + + + case "updating": + return + + + {"Creating database"} + + + default: + return <>; + } + }; + + const renderContents = () => { if (currentCluster?.status === "UPDATING_UNAVAILABLE") { return ; @@ -248,7 +320,7 @@ const Apps: React.FC = ({ {app.name} - + {renderStatusIcon(app.name)} @@ -309,109 +381,137 @@ const Apps: React.FC = ({ export default Apps; const MidIcon = styled.img<{ height?: string }>` - height: ${props => props.height || "18px"}; - margin-right: 11px; -`; + height: ${props => props.height || "18px"}; + margin-right: 11px; + `; const Row = styled(Link) <{ isAtBottom?: boolean }>` - cursor: pointer; - display: block; - padding: 15px; - border-bottom: ${props => props.isAtBottom ? "none" : "1px solid #494b4f"}; - background: ${props => props.theme.clickable.bg}; - position: relative; - border: 1px solid #494b4f; - border-radius: 5px; - margin-bottom: 15px; - animation: fadeIn 0.3s 0s; -`; + cursor: pointer; + display: block; + padding: 15px; + border-bottom: ${props => props.isAtBottom ? "none" : "1px solid #494b4f"}; + background: ${props => props.theme.clickable.bg}; + position: relative; + border: 1px solid #494b4f; + border-radius: 5px; + margin-bottom: 15px; + animation: fadeIn 0.3s 0s; + `; const List = styled.div` - overflow: hidden; -`; + overflow: hidden; + `; const SmallIcon = styled.img<{ opacity?: string }>` - margin-left: 2px; - height: 14px; - opacity: ${props => props.opacity || 1}; - margin-right: 10px; -`; + margin-left: 2px; + height: 14px; + opacity: ${props => props.opacity || 1}; + margin-right: 10px; + `; const StatusIcon = styled.img` - position: absolute; - top: 20px; - right: 20px; - height: 18px; -`; + position: absolute; + top: 20px; + right: 20px; + height: 18px; + `; const Icon = styled.img` - height: 20px; - margin-right: 13px; -`; + height: 20px; + margin-right: 13px; + `; const Block = styled(Link)` - height: 110px; - flex-direction: column; - display: flex; - justify-content: space-between; - cursor: pointer; - padding: 20px; - color: ${props => props.theme.text.primary}; - position: relative; - border-radius: 5px; - background: ${props => props.theme.clickable.bg}; - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; + height: 110px; + flex-direction: column; + display: flex; + justify-content: space-between; + cursor: pointer; + padding: 20px; + color: ${props => props.theme.text.primary}; + position: relative; + border-radius: 5px; + background: ${props => props.theme.clickable.bg}; + border: 1px solid #494b4f; + :hover { + border: 1px solid #7a7b80; } - animation: fadeIn 0.3s 0s; - @keyframes fadeIn { - from { - opacity: 0; + animation: fadeIn 0.3s 0s; + @keyframes fadeIn { + from { + opacity: 0; } - to { - opacity: 1; + to { + opacity: 1; } } -`; + `; const GridList = styled.div` - display: grid; - grid-column-gap: 25px; - grid-row-gap: 25px; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); -`; + display: grid; + grid-column-gap: 25px; + grid-row-gap: 25px; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + `; const PlaceholderIcon = styled.img` - height: 13px; - margin-right: 12px; - opacity: 0.65; -`; + height: 13px; + margin-right: 12px; + opacity: 0.65; + `; const ToggleIcon = styled.img` - height: 12px; - margin: 0 5px; - min-width: 12px; -`; + height: 12px; + margin: 0 5px; + min-width: 12px; + `; const I = styled.i` - color: white; - font-size: 14px; + color: white; + font-size: 14px; + display: flex; + align-items: center; + margin-right: 5px; + justify-content: center; + `; + +const StyledAppDashboard = styled.div` + width: 100%; + height: 100%; + `; + +const StatusText = styled.div` + position: absolute; + top: 20px; + right: 20px; display: flex; align-items: center; - margin-right: 5px; justify-content: center; `; -const StyledAppDashboard = styled.div` - width: 100%; - height: 100%; -`; - -const CentralContainer = styled.div` +const StatusWrapper = styled.div<{ + success?: boolean; +}>` display: flex; - flex-direction: column; - justify-content: left; - align-items: left; + line-height: 1.5; + align-items: center; + font-family: "Work Sans", sans-serif; + font-size: 13px; + color: #ffffff55; + margin-left: 15px; + text-overflow: ellipsis; + animation-fill-mode: forwards; + > i { + font-size: 18px; + margin-right: 10px; + float: left; + color: ${(props) => (props.success ? "#4797ff" : "#fcba03")}; + } `; +const Loading = styled.img` + width: 15px; + height: 15px; + margin-right: 9px; + margin-bottom: 0px; +`; \ No newline at end of file diff --git a/dashboard/src/main/home/sidebar/Sidebar.tsx b/dashboard/src/main/home/sidebar/Sidebar.tsx index bd866775f7..a360cb5791 100644 --- a/dashboard/src/main/home/sidebar/Sidebar.tsx +++ b/dashboard/src/main/home/sidebar/Sidebar.tsx @@ -148,21 +148,21 @@ class Sidebar extends Component { "update", "delete", ]) && ( - - - Integrations - - )} + + + Integrations + + )} {this.props.isAuthorized("settings", "", [ "get", "update", "delete", ]) && ( - - - Project settings - - )} + + + Project settings + + )}
@@ -189,22 +189,22 @@ class Sidebar extends Component { "update", "delete", ]) && ( - - - Project settings - - )} + + + Project settings + + )} {this.props.isAuthorized("integrations", "", [ "get", "create", "update", "delete", ]) && ( - - - Integrations - - )} + + + Integrations + + )} {currentCluster && ( <> @@ -218,6 +218,15 @@ class Sidebar extends Component { Applications + {currentProject.db_enabled && ( + + + Databases + + )} { "update", "delete", ]) && ( - - - Infrastructure - - )} + + + Infrastructure + + )} {currentProject.preview_envs_enabled && ( @@ -304,16 +313,16 @@ class Sidebar extends Component { "update", "delete", ]) && ( - - - Infrastructure - - )} + + + Infrastructure + + )} {currentProject.preview_envs_enabled && ( @@ -328,22 +337,22 @@ class Sidebar extends Component { "update", "delete", ]) && ( - - - Integrations - - )} + + + Integrations + + )} {this.props.isAuthorized("settings", "", [ "get", "update", "delete", ]) && ( - - - Project settings - - )} + + + Project settings + + )} {/* Hacky workaround for setting currentCluster with legacy method */} ("GET", (pathParams) => { const { project_id, cluster_id, stack_name, page } = pathParams; - return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${ - page || 1 - }`; + return `/api/projects/${project_id}/clusters/${cluster_id}/applications/${stack_name}/events?page=${page || 1 + }`; }); const createEnvironment = baseApi< @@ -798,11 +797,9 @@ const detectBuildpack = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`; }); const detectGitlabBuildpack = baseApi< @@ -833,11 +830,9 @@ const getBranchContents = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/contents`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/contents`; }); const getProcfileContents = baseApi< @@ -853,11 +848,9 @@ const getProcfileContents = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/procfile`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/procfile`; }); const getPorterYamlContents = baseApi< @@ -873,11 +866,9 @@ const getPorterYamlContents = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/porteryaml`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/porteryaml`; }); const parsePorterYaml = baseApi< @@ -925,11 +916,9 @@ const getBranchHead = baseApi< branch: string; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.project_id}/gitrepos/${ - pathParams.git_repo_id - }/repos/${pathParams.kind}/${pathParams.owner}/${ - pathParams.name - }/${encodeURIComponent(pathParams.branch)}/head`; + return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id + }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name + }/${encodeURIComponent(pathParams.branch)}/head`; }); const validatePorterApp = baseApi< @@ -961,23 +950,23 @@ const validatePorterApp = baseApi< const createApp = baseApi< | { - name: string; - deployment_target_id: string; - type: "github"; - git_repo_id: number; - git_branch: string; - git_repo_name: string; - porter_yaml_path: string; - } + name: string; + deployment_target_id: string; + type: "github"; + git_repo_id: number; + git_branch: string; + git_repo_name: string; + porter_yaml_path: string; + } | { - name: string; - deployment_target_id: string; - type: "docker-registry"; - image: { - repository: string; - tag: string; - }; - }, + name: string; + deployment_target_id: string; + type: "docker-registry"; + image: { + repository: string; + tag: string; + }; + }, { project_id: number; cluster_id: number; @@ -1038,17 +1027,17 @@ const updateApp = baseApi< }); const appRun = baseApi< - { - deployment_target_id: string; - service_name: string; - }, - { - project_id: number; - cluster_id: number; - porter_app_name: string; - } + { + deployment_target_id: string; + service_name: string; + }, + { + project_id: number; + cluster_id: number; + porter_app_name: string; + } >("POST", (pathParams) => { - return `/api/projects/${pathParams.project_id}/clusters/${pathParams.cluster_id}/apps/${pathParams.porter_app_name}/run`; + return `/api/projects/${pathParams.project_id}/clusters/${pathParams.cluster_id}/apps/${pathParams.porter_app_name}/run`; }); const updateBuildSettings = baseApi< @@ -1142,7 +1131,7 @@ const getRevision = baseApi< const porterYamlFromRevision = baseApi< { - should_format_for_export: boolean; + should_format_for_export: boolean; }, { project_id: number; @@ -1880,6 +1869,20 @@ const updateDatabaseStatus = baseApi< >("POST", (pathParams) => { return `/api/projects/${pathParams.project_id}/infras/${pathParams.infra_id}/database`; }); +// GET /api/projects/{project_id}/clusters/{cluster_id}/datastore/status +const getDatabaseStatus = baseApi< + { + name: string; + type: string + }, + { + project_id: number; + cluster_id: number; + + } +>("GET", (pathParams) => { + return `/api/projects/${pathParams.project_id}/clusters/${pathParams.cluster_id}/datastore/status`; +}); const getRepoIntegrations = baseApi("GET", "/api/integrations/repo"); @@ -2139,11 +2142,9 @@ const getEnvGroup = baseApi< version?: number; } >("GET", (pathParams) => { - return `/api/projects/${pathParams.id}/clusters/${ - pathParams.cluster_id - }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${ - pathParams.version ? "&version=" + pathParams.version : "" - }`; + return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id + }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : "" + }`; }); const getConfigMap = baseApi< @@ -3201,7 +3202,7 @@ const removeStackEnvGroup = baseApi< `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}` ); -const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`); +const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`); const createSecretAndOpenGitHubPullRequest = baseApi< { @@ -3498,4 +3499,5 @@ export default { // STATUS getGithubStatus, + getDatabaseStatus }; diff --git a/dashboard/src/shared/types.tsx b/dashboard/src/shared/types.tsx index 188c7e2c08..3bd39acbed 100644 --- a/dashboard/src/shared/types.tsx +++ b/dashboard/src/shared/types.tsx @@ -32,6 +32,9 @@ export type DetailedIngressError = { message: string; error: string; }; +export type Annotations = { + category: string; +} export type ChartType = { stack_id: string; @@ -54,6 +57,7 @@ export type ChartType = { description: string; icon: string; apiVersion: string; + annotations?: Annotations; }; files?: Array<{ data: string;