From d430473aef50a9237690a8f478a6dd187e5f62c8 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Thu, 2 May 2024 14:27:32 -0400 Subject: [PATCH 1/8] remove files only used in v1 --- dashboard/src/main/home/Home.tsx | 37 +- .../main/home/app-dashboard/AppDashboard.tsx | 474 ------- .../events/cards/AppEventCard.tsx | 104 -- .../app-dashboard/expanded-app/AppEvents.tsx | 43 - .../expanded-app/ChangeLogComponent.tsx | 195 --- .../expanded-app/ChangeLogModal.tsx | 240 ---- .../expanded-app/DiffViewModal.tsx | 308 ----- .../expanded-app/DisabledNamespaces.ts | 9 - .../expanded-app/ExpandedApp.tsx | 1090 ----------------- .../expanded-app/HelmValuesTab.tsx | 84 -- .../expanded-app/ImageSettingsTab.tsx | 119 -- .../app-dashboard/expanded-app/JobRuns.tsx | 557 --------- .../expanded-app/PorterAppRevisionSection.tsx | 548 --------- .../expanded-app/SettingsTab.tsx | 57 - .../expanded-app/StatusFooter.tsx | 508 -------- .../activity-feed/ActivityFeed.tsx | 301 ----- .../events/cards/AppEventCard.tsx | 99 -- .../events/cards/BuildEventCard.tsx | 113 -- .../events/cards/DeployEventCard.tsx | 229 ---- .../activity-feed/events/cards/EventCard.tsx | 61 - .../events/cards/PreDeployEventCard.tsx | 90 -- .../events/cards/ServiceStatusDetail.tsx | 126 -- .../BuildFailureEventFocusView.tsx | 278 ----- .../focus-views/DeployEventFocusView.tsx | 71 -- .../events/focus-views/EventFocusView.tsx | 129 -- .../focus-views/PredeployEventFocusView.tsx | 70 -- .../activity-feed/events/types.ts | 44 - .../activity-feed/events/utils.ts | 91 -- .../expanded-app/env-vars/EnvGroupModal.tsx | 344 ------ .../expanded-app/env-vars/EnvVariablesTab.tsx | 332 ----- .../env-vars/ExpandableEnvGroup.tsx | 262 ---- .../expanded-app/expanded-job/ExpandedJob.tsx | 221 ---- .../expanded-job/ExpandedJobRun.tsx | 559 --------- .../expanded-app/logs/LogFilterComponent.tsx | 45 - .../expanded-app/logs/LogFilterContainer.tsx | 72 -- .../expanded-app/logs/LogSection.tsx | 572 --------- .../expanded-app/logs/StyledLogs.tsx | 186 --- .../app-dashboard/expanded-app/logs/types.ts | 93 -- .../app-dashboard/expanded-app/logs/utils.ts | 536 -------- .../expanded-app/metrics/MetricsSection.tsx | 370 ------ .../expanded-app/status/AppEventModal.tsx | 81 -- .../status/ConnectToLogsInstructionModal.tsx | 52 - .../expanded-app/status/ControllerTab.tsx | 448 ------- .../status/ExpandedIncidentLogs.tsx | 157 --- .../expanded-app/status/GHALogsModal.tsx | 231 ---- .../expanded-app/status/Logs.tsx | 398 ------ .../expanded-app/status/LogsModal.tsx | 50 - .../expanded-app/status/PodRow.tsx | 234 ---- .../expanded-app/status/StatusSection.tsx | 278 ----- .../expanded-app/status/types.ts | 19 - .../expanded-app/status/useLogs.ts | 218 ---- .../app-dashboard/new-app-flow/NewAppFlow.tsx | 781 ------------ .../new-app-flow/ServiceContainer.tsx | 325 ----- .../app-dashboard/new-app-flow/Services.tsx | 248 ---- .../new-app-flow/serviceTypes.ts | 695 ----------- .../src/main/home/sidebar/ProjectButton.tsx | 34 +- dashboard/src/shared/util.ts | 4 +- 57 files changed, 23 insertions(+), 13897 deletions(-) delete mode 100644 dashboard/src/main/home/app-dashboard/AppDashboard.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/AppEventCard.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/AppEvents.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogComponent.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogModal.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/DiffViewModal.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/DisabledNamespaces.ts delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/HelmValuesTab.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/ImageSettingsTab.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/PorterAppRevisionSection.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/SettingsTab.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/AppEventCard.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/BuildEventCard.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/DeployEventCard.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/EventCard.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/PreDeployEventCard.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/ServiceStatusDetail.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/DeployEventFocusView.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/EventFocusView.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/PredeployEventFocusView.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/types.ts delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvGroupModal.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvVariablesTab.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/env-vars/ExpandableEnvGroup.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJob.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJobRun.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterComponent.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterContainer.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/logs/LogSection.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/logs/StyledLogs.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/logs/utils.ts delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/metrics/MetricsSection.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/AppEventModal.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/ConnectToLogsInstructionModal.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/ControllerTab.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/ExpandedIncidentLogs.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/GHALogsModal.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/Logs.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/PodRow.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/StatusSection.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/types.ts delete mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/status/useLogs.ts delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts diff --git a/dashboard/src/main/home/Home.tsx b/dashboard/src/main/home/Home.tsx index ecc5695cff..7e9af365a9 100644 --- a/dashboard/src/main/home/Home.tsx +++ b/dashboard/src/main/home/Home.tsx @@ -486,37 +486,21 @@ const Home: React.FC = (props) => { - {currentProject?.validate_apply_v2 ? ( - - - - ) : ( - - )} + + + - {currentProject?.validate_apply_v2 ? ( - - ) : ( - - )} + - {currentProject?.validate_apply_v2 ? ( - - ) : ( - - )} + - {currentProject?.validate_apply_v2 ? ( - - ) : ( - - )} + @@ -616,9 +600,6 @@ const Home: React.FC = (props) => { "/jobs", "/env-groups", "/datastores", - ...(!currentProject?.validate_apply_v2 - ? ["/preview-environments"] - : []), "/stacks", ]} render={() => { diff --git a/dashboard/src/main/home/app-dashboard/AppDashboard.tsx b/dashboard/src/main/home/app-dashboard/AppDashboard.tsx deleted file mode 100644 index 5ae693ea99..0000000000 --- a/dashboard/src/main/home/app-dashboard/AppDashboard.tsx +++ /dev/null @@ -1,474 +0,0 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import _ from "lodash"; -import { Link, LinkProps } from "react-router-dom"; -import styled from "styled-components"; - -import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder"; -import Loading from "components/Loading"; -import Button from "components/porter/Button"; -import Container from "components/porter/Container"; -import DashboardPlaceholder from "components/porter/DashboardPlaceholder"; -import Fieldset from "components/porter/Fieldset"; -import Icon from "components/porter/Icon"; -import PorterLink from "components/porter/Link"; -import SearchBar from "components/porter/SearchBar"; -import Spacer from "components/porter/Spacer"; -import Text from "components/porter/Text"; -import Toggle from "components/porter/Toggle"; - -import api from "shared/api"; -import { Context } from "shared/Context"; -import { search } from "shared/search"; -import { readableDate } from "shared/string_utils"; -import applications from "assets/applications.svg"; -import box from "assets/box.png"; -import calendar from "assets/calendar-number.svg"; -import github from "assets/github.png"; -import grid from "assets/grid.png"; -import list from "assets/list.png"; -import notFound from "assets/not-found.png"; -import healthy from "assets/status-healthy.png"; -import time from "assets/time.png"; -import letter from "assets/vector.svg"; - -import DashboardHeader from "../cluster-dashboard/DashboardHeader"; - -type Props = {}; - -const icons = [ - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/ruby/ruby-plain.svg", - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/nodejs/nodejs-plain.svg", - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/python/python-plain.svg", - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/go/go-original-wordmark.svg", - applications, -]; - -const namespaceBlacklist = [ - "cert-manager", - "default", - "ingress-nginx", - "kube-node-lease", - "kube-public", - "kube-system", - "monitoring", -]; - -const AppDashboard: React.FC = ({}) => { - const { currentProject, currentCluster, setFeaturePreview } = - useContext(Context); - const [apps, setApps] = useState([]); - const [charts, setCharts] = useState([]); - const [error, setError] = useState(null); - const [searchValue, setSearchValue] = useState(""); - const [view, setView] = useState("grid"); - const [sort, setSort] = useState<"calendar" | "letter">("calendar"); - - const [isLoading, setIsLoading] = useState(true); - const [shouldLoadTime, setShouldLoadTime] = useState(true); - - const filteredApps = useMemo(() => { - const filteredBySearch = search(apps ?? [], searchValue, { - keys: ["name"], - isCaseSensitive: false, - }); - - if (sort === "letter") { - return _.sortBy(filteredBySearch, ["name"]); - } else if (sort === "calendar") { - return _.sortBy(filteredBySearch, ["last_deployed"]).reverse(); // Assuming that the latest date should come first. - } - - return filteredBySearch; // default - }, [apps, searchValue, sort]); - - const getApps = async () => { - setIsLoading(true); - try { - const res = await api.getPorterApps( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - const apps = res.data; - const timeRes = await Promise.all( - apps.map(async (app: any) => { - return await api.getCharts( - "", - { - limit: 1, - skip: 0, - byDate: false, - statusFilter: [ - "deployed", - "uninstalled", - "pending", - "pending-install", - "pending-upgrade", - "pending-rollback", - "failed", - ], - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - namespace: `porter-stack-${app.name}`, - } - ); - }) - ); - apps.forEach((app: any, i: number) => { - if (timeRes?.[i]?.data?.[0]?.info?.last_deployed != null) { - app.last_deployed = readableDate( - timeRes[i].data[0].info.last_deployed - ); - } - }); - setApps(apps.reverse()); - setIsLoading(false); - } catch (err) { - setError(err); - setIsLoading(false); - } - }; - - useEffect(() => { - if (currentProject?.id > 0 && currentCluster?.id > 0) { - getApps(); - } - }, [currentCluster, currentProject]); - - const renderSource = (app: any) => { - return ( - <> - {app.repo_name ? ( - - - - {app.repo_name} - - - ) : ( - - - - {app.image_repo_uri} - - - )} - - ); - }; - - const updateStackStartedStep = async () => { - try { - await api.updateStackStep( - "", - { - step: "stack-launch-start", - }, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - } - ); - } catch (err) { - // TODO: handle error - } - }; - - const renderIcon = (b: string, size?: string) => { - let src = box; - if (b) { - const bp = b.split(",")[0]?.split("/")[1]; - switch (bp) { - case "ruby": - src = icons[0]; - break; - case "nodejs": - src = icons[1]; - break; - case "python": - src = icons[2]; - break; - case "go": - src = icons[3]; - break; - default: - break; - } - } - return ( - <> - {size === "larger" ? ( - - ) : ( - - )} - - ); - }; - - return ( - - - {currentCluster?.status === "UPDATING_UNAVAILABLE" ? ( - - ) : apps.length === 0 ? ( - isLoading ? ( - - ) : ( - - No apps have been deployed yet - - Get started by deploying your app. - - - - - - ) - ) : ( - <> - - { - if (x === "open_sesame") { - setFeaturePreview(true); - } - setSearchValue(x); - }} - placeholder="Search applications . . ." - width="100%" - /> - - , value: "calendar" }, - { label: , value: "letter" }, - ]} - active={sort} - setActive={setSort} - /> - - - , value: "grid" }, - { label: , value: "list" }, - ]} - active={view} - setActive={setView} - /> - - - - - - - - - {filteredApps.length === 0 ? ( -
- - - No matching apps were found. - -
- ) : isLoading ? ( - - ) : view === "grid" ? ( - - {(filteredApps ?? []).map((app: any, i: number) => { - if (!namespaceBlacklist.includes(app.name)) { - return ( - - - - {renderIcon(app.buildpacks)} - - {app.name} - - - - {renderSource(app)} - - - - {app.last_deployed} - - - - - ); - } - })} - - ) : ( - - {(filteredApps ?? []).map((app: any, i: number) => { - if (!namespaceBlacklist.includes(app.name)) { - return ( - - - - - {renderIcon(app.buildpacks, "larger")} - - {app.name} - - - - - - {renderSource(app)} - - - - {app.last_deployed} - - - - - ); - } - })} - - )} - - )} - -
- ); -}; - -export default AppDashboard; - -const PlaceholderIcon = styled.img` - height: 13px; - margin-right: 12px; - opacity: 0.65; -`; - -const Row = styled.div<{ isAtBottom?: boolean }>` - cursor: pointer; - 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; -`; - -const ToggleIcon = styled.img` - height: 12px; - margin: 0 5px; - min-width: 12px; -`; - -const StatusIcon = styled.img` - position: absolute; - top: 20px; - right: 20px; - height: 18px; -`; - -const SmallIcon = styled.img<{ opacity?: string; height?: string }>` - margin-left: 2px; - height: ${(props) => props.height || "14px"}; - opacity: ${(props) => props.opacity || 1}; - filter: grayscale(100%); - margin-right: 10px; -`; - -const Block = styled.div` - height: 150px; - 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; - } - 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)); -`; - -const I = styled.i` - 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 CentralContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: left; - align-items: left; -`; diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/AppEventCard.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/AppEventCard.tsx deleted file mode 100644 index 379a29cb7a..0000000000 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/AppEventCard.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from "react"; - -import app_event from "assets/app_event.png"; -import Text from "components/porter/Text"; -import Container from "components/porter/Container"; -import Spacer from "components/porter/Spacer"; -import Link from "components/porter/Link"; -import Icon from "components/porter/Icon"; - -import { StyledEventCard } from "./EventCard"; -import { readableDate } from "shared/string_utils"; -import dayjs from "dayjs"; -import Anser from "anser"; -import api from "shared/api"; -import { PorterAppAppEvent } from "../types"; -import { Direction } from "main/home/app-dashboard/expanded-app/logs/types"; -import AppEventModal from "main/home/app-dashboard/expanded-app/status/AppEventModal"; - -type Props = { - event: PorterAppAppEvent; - deploymentTargetId: string; - projectId: number; - clusterId: number; - appName: string; -}; - -const AppEventCard: React.FC = ({ event, deploymentTargetId, projectId, clusterId, appName }) => { - const [showModal, setShowModal] = useState(false); - const [logs, setLogs] = useState([]); - - const getAppLogs = async () => { - setShowModal(true); - try { - const logResp = await api.appLogs( - "", - { - start_range: dayjs(event.created_at).subtract(1, 'minute').toISOString(), - end_range: dayjs(event.updated_at).add(1, 'minute').toISOString(), - app_id: event.porter_app_id, - service_name: event.metadata.service_name, - deployment_target_id: deploymentTargetId, - limit: 1000, - direction: Direction.forward, - }, - { - project_id: projectId, - cluster_id: clusterId, - porter_app_name: appName, - } - ) - - if (logResp.data?.logs != null) { - const updatedLogs = logResp.data.logs.map((l: { line: string; timestamp: string; }, index: number) => { - try { - return { - line: JSON.parse(l.line)?.log ?? Anser.ansiToJson(l.line), - lineNumber: index + 1, - timestamp: l.timestamp, - } - } catch (err) { - return { - line: Anser.ansiToJson(l.line), - lineNumber: index + 1, - timestamp: l.timestamp, - } - } - }); - setLogs(updatedLogs); - } - } catch (error) { - console.log(error); - } - }; - - return ( - - - - - - {event.metadata.summary} - - - - - - View details - - - {showModal && ( - - )} - - ); -}; - -export default AppEventCard; - diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/AppEvents.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/AppEvents.tsx deleted file mode 100644 index 3cb9fba7d3..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/AppEvents.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import Loading from "components/Loading"; -import Fieldset from "components/porter/Fieldset"; -import Link from "components/porter/Link"; -import Spacer from "components/porter/Spacer"; -import Text from "components/porter/Text"; -import React, { useEffect, useState } from "react"; -import styled from "styled-components"; - -type Props = { - repoName: string; - branchName: string; -}; - -const AppEvents: React.FC = ({ - repoName, - branchName, -}) => { - const [isExpanded, setIsExpanded] = useState(false); - - useEffect(() => { - // Do something - }, []); - - return ( - -
- - Dream on - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. - - -
-
- ); -}; - -export default AppEvents; - -const StyledAppEvents = styled.div` -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogComponent.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogComponent.tsx deleted file mode 100644 index 9fc6a3b06a..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogComponent.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import React, { FC } from 'react'; -import * as Diff from "deep-diff"; -import styled from 'styled-components'; -import Text from 'components/porter/Text'; -import { flatMapDepth } from 'lodash'; -import Link from 'components/porter/Link'; - -const createCompareLink = (repoId: string, oldTag: string, newTag: string) => { - const baseUrl = 'https://github.com'; - const link = `${baseUrl}/${repoId}/compare/${oldTag}...${newTag}`; - return link; -} - -const getTagsFromChange = (changeString: string) => { - const tagPattern = /"global image tag: "([^"]*)" -> "([^"]*)""/; - const match = changeString.match(tagPattern); - if (match) { - return { oldTag: match[1], newTag: match[2] }; - } - return null; -} - -type Props = { - oldYaml: any; - newYaml: any; - appData: any; -}; - -const ChangeLogComponent: FC = ({ oldYaml, newYaml, appData }) => { - const diff = Diff.diff(oldYaml, newYaml); - const changes: JSX.Element[] = []; - // Define the regex pattern to match service creation - const servicePattern = /^[a-zA-Z0-9\-]*-[a-zA-Z0-9]*[^\.]$/; - diff?.forEach((difference: any) => { - let path = difference.path?.join(" "); - switch (difference.kind) { - case "N": - // Check if the added item is a service by testing the path against the regex pattern - if (path?.includes('container env normal')) { - const appName = path.split(' ')[0]; - const keyName = path.split(' ')[4]; - changes.push( - {`${appName} added env var ${keyName} = ${difference.rhs}`} - ); - } else if (servicePattern.test(path)) { - changes.push({`${path} created`}); - } else { - // If not, display the full message - changes.push( - {`${path} added: ${JSON.stringify( - difference.rhs - )}`} - ); - } - break; - case "D": - if (servicePattern.test(path)) { - // If so, display a simplified message - changes.push( - {`${path} deleted`} - ); - } else { - - changes.push( - {`${path} removed`} - ); - } - break; - case "E": - if (path === "global image tag") { - const oldCommit = difference.lhs; - const newCommit = difference.rhs; - if (appData?.app?.repo_name) { - const commitDiffLink = `https://github.com/${appData.app.repo_name}/compare/${oldCommit}...${newCommit}`; - changes.push( - - {`Tag updated: ${oldCommit} -> ${newCommit}. `} - - - View commit diff - - - ); - } else { - - {`Tag updated: ${oldCommit} -> ${newCommit}. `} - - } - } else { - changes.push( - - {`${path}: ${JSON.stringify(difference.lhs)} -> ${JSON.stringify(difference.rhs)}`} - - ); - } - break; - case "A": - path = path + `[${difference.index}]`; - if (difference.item.kind === "N") { - if (path.includes('container env synced')) { - const appName = path.split(' ')[0]; - if (path.includes('keys')) { - // This is an addition of a key in an existing env group - const keyName = difference.item.rhs?.name; - changes.push( - {`${appName} synced env-group key ${keyName} added`} - ); - } else { - // This is an addition of a whole new env group - const groupName = difference.item.rhs?.name; - changes.push( - {`${appName} synced env-group ${groupName} added`} - ); - } - } else { - changes.push( - {`${path} added: ${JSON.stringify(difference.item.rhs)}`} - ); - } - } - if (difference.item.kind === "D") { - if (path.includes('container env synced')) { - const appName = path.split(' ')[0]; - if (path.includes('keys')) { - // This is a deletion of a key in an existing env group - const keyName = difference.item.lhs?.name; - changes.push( - {`${appName} synced env-group key ${keyName} removed`} - ); - } else { - // This is a deletion of a whole env group - const groupName = difference.item.lhs?.name; - changes.push( - {`${appName} synced env-group ${groupName} removed`} - ); - } - } else { - changes.push( - {`${path} removed: ${JSON.stringify(difference.item.lhs)}`} - ); - } - } - if (difference.item.kind === "E") - changes.push( - - {`${path} updated: ${JSON.stringify( - difference.item.lhs - )} -> ${JSON.stringify(difference.item.rhs)}`} - - ); - break; - } - }); - if (changes.length === 0) { - changes.push( - - {`No changes detected`} - - ) - } - - return {changes} - -}; - -export default ChangeLogComponent; - -const ChangeLog = styled.div` - display: flex; - flex-direction: column; - border-radius: 8px; - overflow: hidden; -`; - -type BoxProps = { - type: string, - children?: React.ReactNode, -}; - -const ChangeBox = styled.div` - padding: 10px; - background-color: ${({ type }) => - type === "N" - ? "#034a53" - : type === "D" - ? "#632f34" - : type === "E" - ? "#272831" - : "#fff"}; - color: "#fff"; -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogModal.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogModal.tsx deleted file mode 100644 index 43450b326e..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/ChangeLogModal.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; -import styled from "styled-components"; -import Modal from "components/porter/Modal"; -import Loading from "components/Loading"; -import Text from "components/porter/Text"; -import yaml from "js-yaml"; -import DiffViewer, { DiffMethod } from "react-diff-viewer"; -import Button from "components/porter/Button"; -import ConfirmOverlay from "components/porter/ConfirmOverlay"; -import Spacer from "components/porter/Spacer"; -import Checkbox from "components/porter/Checkbox"; -import { ChartType } from "shared/types"; -import * as Diff from "deep-diff"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import ChangeLogComponent from "./ChangeLogComponent"; - -type Props = { - modalVisible: boolean; - setModalVisible: (x: boolean) => void; - revision: number; - currentChart: ChartType; - revertModal?: boolean; - appData: any; - diffContent: boolean; - setDiffContent: (x: boolean) => void; -}; - -const ChangeLogModal: React.FC = ({ - revision, - appData, - currentChart, - modalVisible, - revertModal, - setModalVisible, -}) => { - const [values, setValues] = useState(""); - const [chartEvent, setChartEvent] = useState(null); - const [eventValues, setEventValues] = useState(""); - const [prevChartEvent, setPrevChartEvent] = useState(null); - const [prevEventValues, setPrevEventValues] = useState(""); - const [showRawDiff, setShowRawDiff] = useState(false); - const [showOverlay, setShowOverlay] = useState(false); - const [changesConfig, setChangesConfig] = useState(true); - - const [loading, setLoading] = useState(false); - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - useEffect(() => { - let values = "# Nothing here yet"; - if (currentChart.config) { - values = yaml.dump(currentChart.config); - } - setValues(values); - }, [currentChart.config]); // It will run this effect whenever currentChart.config changes - - const getChartData = async (chart: ChartType) => { - setLoading(true); - const res = await api.getChart( - "", - {}, - { - name: chart.name, - namespace: chart.namespace, - cluster_id: currentCluster.id, - revision: revision, - id: currentProject.id, - } - ); - const updatedChart = res.data; - setLoading(false); - return updatedChart; - }; - - const revertToRevision = async (revision: number) => { - setLoading(true); - try { - await api - .rollbackPorterApp( - "", - { - revision, - }, - { - project_id: appData.app.project_id, - stack_name: appData.app.name, - cluster_id: appData.app.cluster_id, - } - ) - window.location.reload(); - } catch (err) { - setLoading(false); - console.log(err) - } - } - - const getPrevChartData = async (chart: ChartType) => { - setLoading(true); - const prevRevision = revision - 1; - const res = await api.getChart( - "", - {}, - { - name: chart.name, - namespace: chart.namespace, - cluster_id: currentCluster.id, - revision: prevRevision, - id: currentProject.id, - } - ); - const updatedChart = res.data; - setLoading(false); - return updatedChart; - }; - - useEffect(() => { - const fetchData = async () => { - // Fetch the chart data - const updatedChart = await getChartData(currentChart); - const prevChart = await getPrevChartData(currentChart); - - // Now that we've waited for getChartData to finish, process the result - let eventValues = "# Nothing here yet"; - if (updatedChart?.config) { - eventValues = yaml.dump(updatedChart?.config); - } - let prevEventValues = "# Nothing here yet"; - if (prevChart?.config) { - prevEventValues = yaml.dump(prevChart?.config); - } - setEventValues(eventValues); - setChartEvent(updatedChart); - setPrevEventValues(prevEventValues); - setPrevChartEvent(prevChart); - }; - - fetchData(); - }, [currentChart.config]); - - return ( - <> - setModalVisible(false)} width={"800px"}> - {revertModal ? Revert to version no. {revision} : Changes for version no. {revision}} - - {loading ? ( - // <-- Render loading state - ) : ( - revertModal ? (<> -
- -
- ) : - (<> - {showRawDiff ? ( - <> -
- -
- - ) : ( -
- {revertModal ? - - - : - - } -
- )} - - {changesConfig && (<> - -
- - setShowRawDiff(!showRawDiff)} - > - Show raw diff - -
)} - )) - } - - {revertModal && ( - <> - - - - )} - {showOverlay && ( - - revertToRevision(revision)} - onNo={() => setShowOverlay(false)} - /> - - )} - -
- - ); -}; - -export default ChangeLogModal; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/DiffViewModal.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/DiffViewModal.tsx deleted file mode 100644 index 099e73c68e..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/DiffViewModal.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import styled from "styled-components"; -import Modal from "components/porter/Modal"; -import TitleSection from "components/TitleSection"; -import Loading from "components/Loading"; -import Text from "components/porter/Text"; -import danger from "assets/danger.svg"; -import Anser, { AnserJsonEntry } from "anser"; -import web from "assets/web-bold.png"; -import settings from "assets/settings-bold.png"; -import sliders from "assets/sliders.svg"; - -import dayjs from "dayjs"; -import Link from "components/porter/Link"; -import Spacer from "components/porter/Spacer"; -import Checkbox from "components/porter/Checkbox"; -import { NavLink } from "react-router-dom"; -import SidebarLink from "main/home/sidebar/SidebarLink"; -import { EnvVariablesTab } from "./env-vars/EnvVariablesTab"; -type Props = { - modalVisible: boolean; - setModalVisible: (x: boolean) => void; - serviceChild: any; -}; - -const DiffViewModal: React.FC = ({ - serviceChild, - setModalVisible, -}) => { - const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true); - const [currentView, setCurrentView] = useState("overview"); - - return ( - setModalVisible(false)} width={"1100px"}> - Compare Diff - - - - - - setCurrentView("overview")}> - - Overview - - setCurrentView("environment")}> - - Environment - - setCurrentView("buildSettings")}> - - Build settings - - - - - - {currentView === "overview" && ( - - - Current - {serviceChild} - - - - - Revision No.5 - - {serviceChild} - - - )} - {currentView === "environment" &&
} - {currentView === "buildSettings" && ( -
-

Build Settings

-

Dummy content for build settings.

-
- )} -
-
-
- ); -}; - -export default DiffViewModal; -const ScrollWrapper = styled.div` - overflow-y: auto; - padding-bottom: 25px; - max-height: calc(100vh - 95px); -`; - -const ProjectPlaceholder = styled.div` - background: #ffffff11; - border-radius: 5px; - margin: 0 15px; - display: flex; - align-items: center; - justify-content: center; - height: calc(100% - 100px); - font-size: 13px; - color: #aaaabb; - padding-bottom: 80px; - - > img { - width: 17px; - margin-right: 10px; - } -`; - -const NavButton = styled(SidebarLink)` - display: flex; - align-items: center; - border-radius: 5px; - position: relative; - text-decoration: none; - height: 34px; - margin: 5px 15px; - padding: 0 30px 2px 6px; - font-size: 13px; - color: ${(props) => props.theme.text.primary}; - cursor: ${(props: { disabled?: boolean }) => - props.disabled ? "not-allowed" : "pointer"}; - - background: ${(props: any) => (props.active ? "#ffffff11" : "")}; - - :hover { - background: ${(props: any) => (props.active ? "#ffffff11" : "#ffffff08")}; - } - - &.active { - background: #ffffff11; - - :hover { - background: #ffffff11; - } - } - - :hover { - background: #ffffff08; - } - - > i { - font-size: 18px; - border-radius: 3px; - margin-left: 2px; - margin-right: 10px; - } -`; - -const Img = styled.img<{ enlarge?: boolean }>` - padding: ${(props) => (props.enlarge ? "0 0 0 1px" : "4px")}; - height: 22px; - padding-top: 4px; - border-radius: 3px; - margin-right: 8px; - opacity: 0.8; -`; - -const SidebarBg = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - background-color: ${(props) => props.theme.bg}; - height: 100%; - z-index: -1; - border-right: 1px solid #383a3f; -`; - -const SidebarLabel = styled.div` - color: ${(props) => props.theme.text.primary}; - padding: 5px 23px; - margin-bottom: 5px; - font-size: 13px; - z-index: 1; -`; - -const PullTab = styled.div` - position: fixed; - width: 30px; - height: 50px; - background: #7a838f77; - top: calc(50vh - 60px); - left: 0; - z-index: 1; - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; - cursor: pointer; - - :hover { - background: #99a5af77; - } - - > i { - color: #ffffff77; - font-size: 18px; - position: absolute; - top: 15px; - left: 4px; - } -`; - -const Tooltip = styled.div` - position: absolute; - right: -60px; - top: 34px; - min-width: 67px; - height: 18px; - padding-bottom: 2px; - background: #383842dd; - display: flex; - align-items: center; - justify-content: center; - flex: 1; - color: white; - font-size: 12px; - outline: 1px solid #ffffff55; - opacity: 0; - animation: faded-in 0.2s 0.15s; - animation-fill-mode: forwards; - @keyframes faded-in { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const CollapseButton = styled.div` - position: absolute; - right: 0; - top: 8px; - height: 23px; - width: 23px; - background: #525563aa; - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; - cursor: pointer; - - :hover { - background: #636674; - } - - > i { - color: #ffffff77; - font-size: 14px; - transform: rotate(180deg); - position: absolute; - top: 4px; - right: 5px; - } -`; - -const StyledSidebar = styled.section` - width: 240px; - position: relative; - padding-top: 20px; - height: 75vh; - z-index: 2; - animation: ${(props: { showSidebar: boolean }) => - props.showSidebar ? "showSidebar 0.4s" : "hideSidebar 0.4s"}; - animation-fill-mode: forwards; - @keyframes showSidebar { - from { - margin-left: -240px; - } - to { - margin-left: 0px; - } - } - @keyframes hideSidebar { - from { - margin-left: 0px; - } - to { - margin-left: -240px; - } - } -`; -const ContentView = styled.div` - flex 1; - overflow: auto; - padding: 20px; -`; - -const ContentWrapper = styled.div` - display: flex; - flex-direction: row; - height: 75vh; -`; -const ServiceChildContainer = styled.div` - display: flex; - height: 100%; - justify-content: space-between; - align-items: flex-start; // align top -`; - -const ServiceChild = styled.div` - width: calc(50% - 0.5px); -`; - -const Divider = styled.div` - width: 8px; - - background-color: white; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/DisabledNamespaces.ts b/dashboard/src/main/home/app-dashboard/expanded-app/DisabledNamespaces.ts deleted file mode 100644 index 055b94cc7c..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/DisabledNamespaces.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const DisabledNamespacesForIncidents = [ - "cert-manager", - "ingress-nginx", - "kube-node-lease", - "kube-public", - "kube-system", - "monitoring", - "porter-agent-system", -]; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx deleted file mode 100644 index 136c6f386f..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx +++ /dev/null @@ -1,1090 +0,0 @@ -import React, { useEffect, useState, useContext } from "react"; -import { type RouteComponentProps, useHistory, useLocation, useParams, withRouter } from "react-router"; -import styled from "styled-components"; -import yaml from "js-yaml"; - -import notFound from "assets/not-found.png"; -import web from "assets/web.png"; -import box from "assets/box.png"; -import github from "assets/github-white.png"; -import pr_icon from "assets/pull_request_icon.svg"; -import loadingImg from "assets/loading.gif"; -import refresh from "assets/refresh.png"; -import save from "assets/save-01.svg"; - -import api from "shared/api"; -import { Context } from "shared/Context"; -import Error from "components/porter/Error"; - -import Banner from "components/porter/Banner"; -import Loading from "components/Loading"; -import Text from "components/porter/Text"; -import Container from "components/porter/Container"; -import Spacer from "components/porter/Spacer"; -import Link from "components/porter/Link"; -import Back from "components/porter/Back"; -import TabSelector from "components/TabSelector"; -import Icon from "components/porter/Icon"; -import { type ChartType, type CreateUpdatePorterAppOptions } from "shared/types"; -import BuildSettingsTab from "../build-settings/BuildSettingsTab"; -import Button from "components/porter/Button"; -import Services from "../new-app-flow/Services"; -import { ImageInfo, Service } from "../new-app-flow/serviceTypes"; -import Fieldset from "components/porter/Fieldset"; -import { type PorterJson, createFinalPorterYaml , PorterYamlSchema } from "../new-app-flow/schema"; -import { type KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray"; -import { EnvVariablesTab } from "./env-vars/EnvVariablesTab"; -import GHABanner from "./GHABanner"; -import LogSection from "./logs/LogSection"; -import ActivityFeed from "./activity-feed/ActivityFeed"; -import MetricsSection from "./metrics/MetricsSection"; -import StatusSectionFC from "./status/StatusSection"; -import ExpandedJob from "./expanded-job/ExpandedJob"; -import _ from "lodash"; -import AnimateHeight from "react-animate-height"; -import { type NewPopulatedEnvGroup } from "../../../../components/porter-form/types"; -import { type BuildMethod, PorterApp } from "../types/porterApp"; -import EventFocusView from "./activity-feed/events/focus-views/EventFocusView"; -import HelmValuesTab from "./HelmValuesTab"; -import SettingsTab from "./SettingsTab"; -import PorterAppRevisionSection from "./PorterAppRevisionSection"; -import ImageSettingsTab from "./ImageSettingsTab"; - -type Props = RouteComponentProps & {}; - -const icons = [ - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/ruby/ruby-plain.svg", - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/nodejs/nodejs-plain.svg", - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/python/python-plain.svg", - "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/go/go-original-wordmark.svg", - web, -]; - -const validTabs = [ - "activity", - "events", - "overview", - "logs", - "metrics", - "debug", - "environment", - "build-settings", - "image-settings", - "settings", - "helm-values", - "job-history", -] as const; -const DEFAULT_TAB = "activity"; -type ValidTab = typeof validTabs[number]; -type Params = { - eventId?: string; - tab?: ValidTab; -} - -const ExpandedApp: React.FC = ({ ...props }) => { - const { - currentCluster, - currentProject, - setCurrentError, - user, - } = useContext(Context); - const [isLoading, setIsLoading] = useState(true); - const [deleting, setDeleting] = useState(false); - const [appData, setAppData] = useState(null); - const [workflowCheckPassed, setWorkflowCheckPassed] = useState( - false - ); - const [githubWorkflowFilename, setGithubWorkflowFilename] = useState(""); - const [hasBuiltImage, setHasBuiltImage] = useState(false); - - const [forceRefreshRevisions, setForceRefreshRevisions] = useState( - false - ); - - const [showRevisions, setShowRevisions] = useState(false); - - // this is what we read from their porter.yaml in github - const [porterJson, setPorterJson] = useState(undefined); - // this is what we use to update the release. the above is a subset of this - const [porterYaml, setPorterYaml] = useState({} as PorterJson); - const [showUnsavedChangesBanner, setShowUnsavedChangesBanner] = useState(false); - - const [expandedJob, setExpandedJob] = useState(null); - const [services, setServices] = useState([]); - const [envVars, setEnvVars] = useState([]); - const [buttonStatus, setButtonStatus] = useState(""); - const [subdomain, setSubdomain] = useState(""); - const [syncedEnvGroups, setSyncedEnvGroups] = useState([]) - const [deletedEnvGroups, setDeleteEnvGroups] = useState([]) - const [porterApp, setPorterApp] = useState(); - - // this is the version of the porterApp that is being edited. on save, we set the real porter app to be this version - const [tempPorterApp, setTempPorterApp] = useState(PorterApp.empty()); - const [buildView, setBuildView] = useState("docker"); - - const history = useHistory(); - - const { tab } = useParams(); - const { search } = useLocation(); - const queryParams = new URLSearchParams(search); - const queryParamOpts = { - revision: queryParams.get('version'), - output_stream: queryParams.get('output_stream'), - service: queryParams.get('service'), - } - const eventId = queryParams.get('event_id'); - const selectedTab: ValidTab = tab != null && validTabs.includes(tab) ? tab : DEFAULT_TAB; - useEffect(() => { - if (!_.isEqual(_.omitBy(porterApp, _.isEmpty), _.omitBy(tempPorterApp, _.isEmpty))) { - setButtonStatus(""); - setShowUnsavedChangesBanner(true); - } else { - setShowUnsavedChangesBanner(false); - } - }, [tempPorterApp, porterApp]); - - useEffect(() => { - const { appName } = props.match.params as any; - if (currentCluster && appName && currentProject) { - getPorterApp({ revision: 0 }); - } - }, [currentCluster]); - - // this method fetches and reconstructs the porter yaml as well as the DB info (stored in PorterApp) - const getPorterApp = async ({ revision }: { revision: number }) => { - const { appName } = props.match.params as any; - try { - if (!currentCluster || !currentProject) { - return; - } - const resPorterApp = await api.getPorterApp( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - name: appName, - } - ); - const resChartData = await api.getChart( - "", - {}, - { - id: currentProject.id, - namespace: `porter-stack-${appName}`, - cluster_id: currentCluster.id, - name: appName, - revision, - } - ); - let preDeployChartData; - // get the pre-deploy chart - try { - preDeployChartData = await api.getChart( - "", - {}, - { - id: currentProject.id, - namespace: `porter-stack-${appName}`, - cluster_id: currentCluster.id, - name: `${appName}-r`, - // this is always latest because we do not tie the pre-deploy chart to the umbrella chart - revision: 0, - } - ); - } catch (err) { - // that's ok if there's an error, just means there is no pre-deploy chart - } - // update apps and release - const newAppData = { - app: resPorterApp?.data, - chart: resChartData?.data, - releaseChart: preDeployChartData?.data, - }; - const porterJson = await fetchPorterYamlContent( - resPorterApp?.data?.porter_yaml_path ?? "porter.yaml", - newAppData - ); - - const envGroups: NewPopulatedEnvGroup[] = await api - .getAllEnvGroups( - "", - {}, - { - id: currentProject?.id, - cluster_id: currentCluster?.id, - } - ) - .then((res) => res?.data?.environment_groups) - .catch((error) => { - console.error("Failed to fetch environment groups:", error); - return []; - }); - let filteredEnvGroups: NewPopulatedEnvGroup[] = []; - - if (envGroups) { - filteredEnvGroups = envGroups?.filter(envGroup => - envGroup?.linked_applications?.length > 0 && envGroup?.linked_applications?.includes(appName) - ); - } - - setSyncedEnvGroups(filteredEnvGroups || []); - setPorterJson(porterJson); - setAppData(newAppData); - const globalImage = resChartData.data.config?.global?.image - const hasBuiltImage = globalImage?.repository != null && - globalImage.tag != null && - !(globalImage.repository === ImageInfo.BASE_IMAGE.repository && - globalImage.tag === ImageInfo.BASE_IMAGE.tag) - // annoying that we have to parse buildpacks like this but alas - const parsedPorterApp = { ...resPorterApp?.data, buildpacks: newAppData.app.buildpacks?.split(",") ?? [] }; - if (parsedPorterApp.image_repo_uri && hasBuiltImage) { - parsedPorterApp.image_info = { repository: globalImage.repository, tag: globalImage.tag }; - } - setPorterApp(parsedPorterApp); - setTempPorterApp(parsedPorterApp); - setBuildView(!_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks") - const [newServices, newEnvVars] = updateServicesAndEnvVariables( - resChartData?.data, - preDeployChartData?.data, - porterJson, - ); - const finalPorterYaml = createFinalPorterYaml( - newServices, - newEnvVars, - porterJson, - // if we are using a heroku buildpack, inject a PORT env variable - newAppData.app.builder?.includes("heroku") - ); - setPorterYaml(finalPorterYaml); - // Only check GHA status if no built image is set - if (hasBuiltImage || !resPorterApp.data.repo_name) { - setWorkflowCheckPassed(true); - setHasBuiltImage(true); - } else { - try { - await api.getBranchContents( - "", - { - dir: `./.github/workflows/porter_stack_${resPorterApp.data.name}.yml`, - }, - { - project_id: currentProject.id, - git_repo_id: resPorterApp.data.git_repo_id, - kind: "github", - owner: resPorterApp.data.repo_name.split("/")[0], - name: resPorterApp.data.repo_name.split("/")[1], - branch: resPorterApp.data.git_branch, - } - ); - setWorkflowCheckPassed(true); - setGithubWorkflowFilename(`porter_stack_${resPorterApp.data.name}.yml`); - } catch (err) { - // Handle unmerged PR - if (err.response?.status === 404) { - try { - // Check for user-copied porter.yml as fallback - await api.getBranchContents( - "", - { dir: `./.github/workflows/porter.yml` }, - { - project_id: currentProject.id, - git_repo_id: resPorterApp.data.git_repo_id, - kind: "github", - owner: resPorterApp.data.repo_name.split("/")[0], - name: resPorterApp.data.repo_name.split("/")[1], - branch: resPorterApp.data.git_branch, - } - ); - setWorkflowCheckPassed(true); - setGithubWorkflowFilename(`porter.yml`); - } catch (err) { - setWorkflowCheckPassed(false); - } - } - } - } - } catch (err) { - // TODO: handle error - } finally { - setIsLoading(false); - } - }; - - const deletePorterApp = async (deleteGHWorkflowFile?: boolean) => { - setDeleting(true); - const { appName } = props.match.params as any; - try { - await api.deletePorterApp( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - name: appName, - } - ); - } catch (err) { - // TODO: handle error - } - try { - await api.deleteNamespace( - "", - {}, - { - cluster_id: currentCluster.id, - id: currentProject.id, - namespace: `porter-stack-${appName}`, - } - ); - } catch (err) { - // TODO: handle error - } - - let deleteWorkflowFile = false; - - if (deleteGHWorkflowFile && githubWorkflowFilename !== "" && appData?.app != null) { - try { - const res = await api.createSecretAndOpenGitHubPullRequest( - "", - { - github_app_installation_id: appData.app.git_repo_id, - github_repo_owner: appData.app.repo_name.split("/")[0], - github_repo_name: appData.app.repo_name.split("/")[1], - branch: appData.app.git_branch, - delete_workflow_filename: githubWorkflowFilename, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - stack_name: appData.app.name, - } - ); - if (res.data?.url) { - window.open(res.data.url, "_blank", "noreferrer"); - } - deleteWorkflowFile = true; - } catch (err) { - // TODO: handle error - } - } - - // intentionally do not await this promise - api.updateStackStep( - "", - { - step: "stack-deletion", - stack_name: appName, - delete_workflow_file: deleteWorkflowFile, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - - props.history.push("/apps"); - }; - - const updatePorterApp = async (options: Partial) => { - try { - setButtonStatus("loading"); - if ( - appData != null && - currentCluster != null && - currentProject != null && - appData.app != null && - tempPorterApp != null - ) { - const finalPorterYaml = createFinalPorterYaml( - services, - envVars, - porterJson, - // if we are using a heroku buildpack, inject a PORT env variable - appData.app.builder?.includes("heroku") - ); - const yamlString = yaml.dump(finalPorterYaml); - const base64Encoded = btoa(yamlString); - let updatedPorterApp = { - porter_yaml: base64Encoded, - override_release: true, - ...PorterApp.empty(), - build_context: tempPorterApp.build_context, - repo_name: tempPorterApp.repo_name, - git_branch: tempPorterApp.git_branch, - buildpacks: "", - environment_groups: syncedEnvGroups?.map((env) => env.name), - user_update: true, - ...options, - } - if (buildView === "docker") { - updatedPorterApp.dockerfile = tempPorterApp.dockerfile; - updatedPorterApp.builder = "null"; - updatedPorterApp.buildpacks = "null"; - } else { - updatedPorterApp.builder = tempPorterApp.builder; - updatedPorterApp.buildpacks = tempPorterApp.buildpacks.join(","); - updatedPorterApp.dockerfile = "null"; - } - if (tempPorterApp.image_info?.repository && tempPorterApp.image_info?.tag) { - updatedPorterApp = { ...updatedPorterApp, image_info: tempPorterApp.image_info, image_repo_uri: tempPorterApp.image_info.repository } - } - - await api.createPorterApp( - "", - updatedPorterApp, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - stack_name: appData.app.name, - } - ); - - - setPorterYaml(finalPorterYaml); - setPorterApp(tempPorterApp); - setButtonStatus("success"); - setShowUnsavedChangesBanner(false); - getPorterApp({ revision: 0 }); - } else { - setButtonStatus(); - } - // redirect to the default tab - history.push(`/apps/${appData.app.name}/${DEFAULT_TAB}`); - } catch (err) { - // TODO: better error handling - const errMessage = - err?.response?.data?.error ?? - err?.toString() ?? - "An error occurred while deploying your app. Please try again."; - setButtonStatus(); - } - }; - - const fetchPorterYamlContent = async ( - porterYaml: string, - appData: any - ): Promise => { - try { - const res = await api.getPorterYamlContents( - "", - { - path: porterYaml, - }, - { - project_id: appData.app.project_id, - git_repo_id: appData.app.git_repo_id, - owner: appData.app.repo_name?.split("/")[0], - name: appData.app.repo_name?.split("/")[1], - kind: "github", - branch: appData.app.git_branch, - } - ); - if (res.data == null || res.data == "") { - return undefined; - } - const parsedYaml = yaml.load(atob(res.data)); - const parsedData = PorterYamlSchema.parse(parsedYaml); - const porterYamlToJson = parsedData ; - return porterYamlToJson; - } catch (err) { - // TODO: handle error - } - }; - - const renderIcon = (b: string, size?: string) => { - let src = box; - if (b) { - const bp = b.split(",")[0]?.split("/")[1]; - switch (bp) { - case "ruby": - src = icons[0]; - break; - case "nodejs": - src = icons[1]; - break; - case "python": - src = icons[2]; - break; - case "go": - src = icons[3]; - break; - default: - break; - } - } - return ; - }; - - const updateServicesAndEnvVariables = ( - currentChart?: ChartType, - releaseChart?: ChartType, - porterJson?: PorterJson, - ): [Service[], KeyValueType[]] => { - // handle normal chart - const helmValues = currentChart?.config; - const defaultValues = (currentChart?.chart as any)?.values; - let newServices: Service[] = []; - let envVars: KeyValueType[] = []; - - if ( - (defaultValues && Object.keys(defaultValues).length > 0) || - (helmValues && Object.keys(helmValues).length > 0) - ) { - newServices = Service.deserialize(helmValues, defaultValues, porterJson); - const { global, ...helmValuesWithoutGlobal } = helmValues; - if (Object.keys(helmValuesWithoutGlobal).length > 0) { - envVars = Service.retrieveEnvFromHelmValues(helmValuesWithoutGlobal); - setEnvVars(envVars); - const subdomain = Service.retrieveSubdomainFromHelmValues( - newServices, - helmValuesWithoutGlobal - ); - setSubdomain(subdomain); - } - } - - // handle release chart - if (releaseChart?.config || porterJson?.release) { - const release = Service.deserializeRelease(releaseChart?.config, porterJson); - newServices.push(release); - } - - setServices(newServices); - - return [newServices, envVars]; - }; - - const setRevision = (chart: ChartType, isCurrent?: boolean) => { - getPorterApp({ revision: isCurrent ? 0 : chart.version }); - }; - - const getReadableDate = (s: string) => { - const ts = new Date(s); - const date = ts.toLocaleDateString(); - const time = ts.toLocaleTimeString([], { - hour: "numeric", - minute: "2-digit", - }); - return `${time} on ${date}`; - }; - - const onAppUpdate = (services: Service[], envVars: KeyValueType[]) => { - const newPorterYaml = createFinalPorterYaml( - services, - envVars, - porterJson, - // if we are using a heroku buildpack, inject a PORT env variable - appData.app.builder?.includes("heroku") - ); - if (!_.isEqual(porterYaml, newPorterYaml)) { - setButtonStatus(""); - setShowUnsavedChangesBanner(true); - } else { - setShowUnsavedChangesBanner(false); - } - }; - - const renderTabContents = () => { - switch (selectedTab) { - case "activity": - return ; - case "events": - if (eventId != null && eventId !== "") { - return ; - } - return ; - case "overview": - return ( - <> - {/* pre-deploy stuff - only if this is from github! */} - {!isLoading && appData?.app?.git_repo_id != null && ( - <> - Pre-deploy job - - { - if (buttonStatus !== "") { - setButtonStatus(""); - } - const nonRelease = services.filter(Service.isNonRelease) - const newServices = [...nonRelease, ...release] - setServices(newServices) - onAppUpdate(newServices, envVars) - }} - chart={appData.releaseChart} - services={services.filter(Service.isRelease)} - limitOne={true} - prePopulateService={Service.default("pre-deploy", "release", porterJson)} - addNewText={"Add a new pre-deploy job"} - defaultExpanded={false} - /> - - - )} - Application services - - {!isLoading && services.length === 0 && ( - <> -
- - - No services were found. - -
- - - )} - { - if (buttonStatus !== "") { - setButtonStatus(""); - } - const release = services.filter(Service.isRelease) - const newServices = [...svcs, ...release] - setServices(newServices); - onAppUpdate(newServices, envVars); - }} - services={services.filter(Service.isNonRelease)} - chart={appData.chart} - addNewText={"Add a new service"} - setExpandedJob={(x: string) => { setExpandedJob(x); }} - appName={appData.app.name} - /> - - - - ); - case "build-settings": - return ( - ) => { setTempPorterApp(PorterApp.setAttributes(tempPorterApp, attrs)); }} - clearStatus={() => { setButtonStatus(""); }} - updatePorterApp={updatePorterApp} - buildView={buildView} - setBuildView={setBuildView} - /> - ); - case "image-settings": - return ( - ) => { setTempPorterApp(PorterApp.setAttributes(tempPorterApp, attrs)); }} - updatePorterApp={updatePorterApp} - /> - ) - case "settings": - return ; - case "logs": - return Service.isNonRelease(svc))} - appName={appData.app.name} - filterOpts={queryParamOpts} - />; - case "metrics": - return ; - case "debug": - return ; - case "environment": - return ( - { - setEnvVars(envVars); - // onAppUpdate(services, envVars.filter((e) => e.key !== "" || e.value !== "")); - }} - setShowUnsavedChangesBanner={setShowUnsavedChangesBanner} - syncedEnvGroups={syncedEnvGroups} - status={buttonStatus} - updatePorterApp={updatePorterApp} - clearStatus={() => { setButtonStatus(""); }} - setSyncedEnvGroups={setSyncedEnvGroups} - appData={appData} - deletedEnvGroups={deletedEnvGroups} - setDeletedEnvGroups={setDeleteEnvGroups} - /> - ); - case "helm-values": - return ; - case "job-history": - return { setExpandedJob(null); }} - />; - default: - return ; - } - }; - - return ( - <> - {isLoading && } - {!isLoading && appData == null && ( - - - - - No application matching "{(props.match.params as any).appName}" - was found. - - - - Return to dashboard - - )} - {!isLoading && appData?.app != null && ( - - - - {renderIcon(appData.app?.buildpacks)} - - {appData.app.name} - {appData.app.repo_name && ( - <> - - - - - {appData.app.repo_name} - - - - )} - {appData.app.git_branch && ( - <> - - - Branch - - - {appData.app.git_branch} - - - - )} - {!appData.app.repo_name && appData.app.image_repo_uri && ( - <> - - - - - {appData.app.image_repo_uri} - - - - )} - - - {subdomain && ( - <> - - - - {subdomain} - - - - - - )} - - Last deployed {getReadableDate(appData.chart.info.last_deployed)} - - - {deleting ? ( -
- - Deleting "{appData.app.name}" - - - - You will be automatically redirected after deletion is complete. - -
- ) : ( - <> - {!workflowCheckPassed ? ( - isLoading ? ( - - - - ) : ( - - ) - ) : !hasBuiltImage ? ( - isLoading ? ( - - - - ) : ( - - { window.location.reload(); }} - > - - Refresh - - - } - > - Your GitHub repo has not been built yet. - - - Check status - - - ) - ) : ( - <> - - { - setShowRevisions(!showRevisions); - }} - chart={appData.chart} - setRevision={setRevision} - forceRefreshRevisions={forceRefreshRevisions} - refreshRevisionsOff={() => { setForceRefreshRevisions(false); }} - shouldUpdate={ - appData.chart.latest_version && - appData.chart.latest_version !== - appData.chart.chart.metadata.version - } - updatePorterApp={updatePorterApp} - latestVersion={appData.chart.latest_version} - appName={appData.app.name} - /> - - - )} - - - - - - } - > - Changes you are currently previewing have not been saved. - - - - x)} - currentTab={selectedTab} - setCurrentTab={(tab: string) => { - if (buttonStatus !== "") { - setButtonStatus(""); - } - props.history.push(`/apps/${appData.app.name}/${tab}`); - }} - /> - - {renderTabContents()} - - - )} -
- )} - - ); -}; - -export default withRouter(ExpandedApp); - -const A = styled.a` - display: flex; - align-items: center; -`; - -const RefreshButton = styled.div` - color: #ffffff; - display: flex; - align-items: center; - cursor: pointer; - :hover { - color: #ffffff; - > img { - opacity: 1; - } - } - - > img { - display: flex; - align-items: center; - justify-content: center; - height: 11px; - margin-right: 10px; - } -`; - -const Spinner = styled.img` - width: 15px; - height: 15px; - margin-right: 12px; - margin-bottom: -2px; -`; - -const DarkMatter = styled.div<{ antiHeight?: string }>` - width: 100%; - margin-top: ${(props) => props.antiHeight || "-20px"}; -`; - -const TagWrapper = styled.div` - height: 20px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - color: #ffffff44; - border: 1px solid #ffffff44; - border-radius: 3px; - padding-left: 6px; -`; - -const BranchTag = styled.div` - height: 20px; - margin-left: 6px; - color: #aaaabb; - background: #ffffff22; - border-radius: 3px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - padding: 0px 6px; - padding-left: 7px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - -const SmallIcon = styled.img<{ opacity?: string; height?: string }>` - height: ${(props) => props.height || "15px"}; - opacity: ${(props) => props.opacity || 1}; - margin-right: 10px; -`; - -const BranchIcon = styled.img` - height: 14px; - opacity: 0.65; - margin-right: 5px; -`; - -const PlaceholderIcon = styled.img` - height: 13px; - margin-right: 12px; - opacity: 0.65; -`; - -const Placeholder = styled.div` - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - font-size: 13px; -`; - -const StyledExpandedApp = styled.div` - width: 100%; - height: 100%; - - animation: fadeIn 0.5s 0s; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/HelmValuesTab.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/HelmValuesTab.tsx deleted file mode 100644 index f30209fafc..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/HelmValuesTab.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import yaml from "js-yaml"; -import _ from "lodash"; - -import { ChartType, CreateUpdatePorterAppOptions } from "shared/types"; - -import YamlEditor from "components/YamlEditor"; -import Button from "components/porter/Button"; -import Spacer from "components/porter/Spacer"; -import Text from "components/porter/Text"; - -type Props = { - currentChart: ChartType; - updatePorterApp: (options: Partial) => Promise; - buttonStatus: any; -}; - -const HelmValuesTab: React.FC = ({ - currentChart, - updatePorterApp, - buttonStatus, -}) => { - const [values, setValues] = React.useState(yaml.dump(currentChart.config)); - - const handleSaveValues = async () => { - await updatePorterApp({ full_helm_values: values }) - }; - - - return ( - - - - - - Note: any unsaved service changes from the Overview tab will be lost. - - - - ); - -} - -export default HelmValuesTab; - -const Wrapper = styled.div` - overflow: auto; - border-radius: 8px; - border: 1px solid #ffffff33; -`; - -const StyledValuesYaml = styled.div` - display: flex; - flex-direction: column; - width: 100%; - height: calc(100vh - 350px); - font-size: 13px; - overflow: hidden; - border-radius: 8px; - animation: floatIn 0.3s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - @keyframes floatIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0px); - } - } -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/ImageSettingsTab.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/ImageSettingsTab.tsx deleted file mode 100644 index c34efc9987..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/ImageSettingsTab.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React, { useContext, useState } from "react"; -import Spacer from "components/porter/Spacer"; -import Button from "components/porter/Button"; -import Error from "components/porter/Error"; -import styled from "styled-components"; -import copy from "assets/copy-left.svg" -import CopyToClipboard from "components/CopyToClipboard"; -import Link from "components/porter/Link"; -import Text from "components/porter/Text"; -import ImageSettings from "../image-settings/ImageSettings"; -import { Context } from "shared/Context"; -import { CreateUpdatePorterAppOptions } from "shared/types"; -import { PorterApp } from "../types/porterApp"; - -type Props = { - porterApp: PorterApp; - updatePorterApp: (options: Partial) => Promise; - setTempPorterApp: (app: PorterApp) => void; -} -const ImageSettingsTab: React.FC = ({ - porterApp, - updatePorterApp, - setTempPorterApp, -}) => { - const { currentProject } = useContext(Context); - - const [buttonStatus, setButtonStatus] = useState< - "loading" | "success" | string - >(""); - - const saveConfig = async () => { - try { - await updatePorterApp({}); - } catch (err) { - console.log(err); - } - }; - - const handleSave = async () => { - setButtonStatus("loading"); - - try { - await saveConfig(); - setButtonStatus("success"); - } catch (error) { - setButtonStatus("Something went wrong"); - console.log(error); - } - }; - - return ( - <> - setTempPorterApp({ ...porterApp, image_info: { ...porterApp.image_info, repository: uri } })} - imageTag={porterApp.image_info?.tag ?? ""} - setImageTag={(tag: string) => setTempPorterApp({ ...porterApp, image_info: { ...porterApp.image_info, tag: tag } })} - resetImageInfo={() => setTempPorterApp({ ...porterApp, image_info: { ...porterApp.image_info, repository: "", tag: "" } })} - /> - - - - Update command - - If you have the Porter CLI installed, you can update your application image tag by running the following command: - - - {`$ porter app update-tag ${porterApp.name} --tag latest`} - - - - - - - - ); -}; - -export default ImageSettingsTab; - -const Code = styled.span` - font-family: monospace; -`; - -const IdContainer = styled.div` - background: #000000; - border-radius: 5px; - padding: 10px; - display: flex; - width: 100%; - border-radius: 5px; - border: 1px solid ${({ theme }) => theme.border}; - align-items: center; -`; - -const CopyContainer = styled.div` - display: flex; - align-items: center; - margin-left: auto; -`; - -const CopyIcon = styled.img` - cursor: pointer; - margin-left: 5px; - margin-right: 5px; - width: 15px; - height: 15px; - :hover { - opacity: 0.8; - } -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx deleted file mode 100644 index 06dc148c21..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx +++ /dev/null @@ -1,557 +0,0 @@ -import DynamicLink from "components/DynamicLink"; -import Loading from "components/Loading"; -import Table from "components/OldTable"; -import Placeholder from "components/Placeholder"; -import Fieldset from "components/porter/Fieldset"; -import Spacer from "components/porter/Spacer"; -import Text from "components/porter/Text"; -import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { CellProps, Column, Row } from "react-table"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets"; -import { useRouting } from "shared/routing"; -import { relativeDate, timeFrom } from "shared/string_utils"; -import styled from "styled-components"; - -type Props = { - lastRunStatus: "failed" | "succeeded" | "active" | "all"; - namespace: string; - sortType: "Newest" | "Oldest" | "Alphabetical"; - releaseName?: string; - jobName?: string; - setExpandedRun?: any; -}; - -const runnedFor = (start: string | number, end?: string | number) => { - const duration = timeFrom(start, end); - - const unit = - duration.time === 1 - ? duration.unitOfTime.substring(0, duration.unitOfTime.length - 1) - : duration.unitOfTime; - - return `${duration.time} ${unit}`; -}; - -const JobRuns: React.FC = ({ - lastRunStatus, - namespace, - sortType, - releaseName, - jobName, - setExpandedRun, -}) => { - const { currentCluster, currentProject } = useContext(Context); - const [jobRuns, setJobRuns] = useState(null); - const [hasError, setHasError] = useState(false); - const tmpJobRuns = useRef([]); - const lastStreamStatus = useRef(""); - const { openWebsocket, newWebsocket, closeAllWebsockets } = useWebsockets(); - - const getJobRuns = () => { - closeAllWebsockets(); - tmpJobRuns.current = []; - lastStreamStatus.current = ""; - setJobRuns(null); - setHasError(false); - const websocketId = `job-runs-for-all-charts-ws`; - const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/jobs/stream`; - - const config: NewWebsocketOptions = { - onopen: console.log, - onmessage: (message) => { - const data = JSON.parse(message.data); - - if (data.streamStatus === "finished") { - setHasError(false); - setJobRuns(tmpJobRuns.current); - lastStreamStatus.current = data.streamStatus; - return; - } - - if (data.streamStatus === "errored") { - setHasError(true); - tmpJobRuns.current = []; - setJobRuns([]); - return; - } - - tmpJobRuns.current = [...tmpJobRuns.current, data]; - }, - onclose: (event) => { - // console.log(event); - closeAllWebsockets(); - }, - onerror: (error) => { - setHasError(true); - console.log(error); - closeAllWebsockets(); - }, - }; - newWebsocket(websocketId, endpoint, config); - openWebsocket(websocketId); - }; - - useEffect(() => { - if (!namespace) { - return; - } - - getJobRuns(); - }, [currentCluster, currentProject, namespace]); - - useEffect(() => { - return () => { - closeAllWebsockets(); - }; - }, []); - - const columns = useMemo[]>( - () => [ - { - Header: "Started", - accessor: (originalRow) => relativeDate(originalRow?.status.startTime), - }, - { - Header: "Run for", - accessor: (originalRow) => { - if (originalRow?.status?.completionTime) { - return originalRow?.status?.completionTime; - } else if ( - Array.isArray(originalRow?.status?.conditions) && - originalRow?.status?.conditions[0]?.lastTransitionTime - ) { - return originalRow?.status?.conditions[0]?.lastTransitionTime; - } else { - return "Still running..."; - } - }, - Cell: ({ row }) => { - if (row.original?.status?.completionTime) { - return runnedFor( - row.original?.status?.startTime, - row.original?.status?.completionTime - ); - } else if ( - Array.isArray(row.original?.status?.conditions) && - row.original?.status?.conditions[0]?.lastTransitionTime - ) { - return runnedFor( - row.original?.status?.startTime, - row.original?.status?.conditions[0]?.lastTransitionTime - ); - } else { - return "Still running..."; - } - }, - styles: { - padding: "10px", - }, - }, - { - Header: "Status", - id: "status", - Cell: ({ row }: CellProps) => { - if (row.original?.status?.succeeded >= 1) { - return Succeeded; - } - - if (row.original?.status?.failed >= 1) { - return Failed; - } - - return Running; - }, - }, - { - Header: "Commit tag", - id: "commit_or_image_tag", - accessor: (originalRow) => { - const container = originalRow.spec?.template?.spec?.containers[0]; - return container?.image?.split(":")[1] || "N/A"; - }, - Cell: ({ row }: any) => { - const container = row.original.spec?.template?.spec?.containers[0]; - - const tag = container?.image?.split(":")[1]; - return tag; - }, - }, - { - id: "expand", - Cell: ({ row }: CellProps) => { - /** - * project_id: currentProject.id, - chart_revision: 0, - job: row.original?.metadata?.name, - */ - const urlParams = new URLSearchParams(); - urlParams.append("project_id", String(currentProject.id)); - urlParams.append("chart_revision", String(0)); - urlParams.append("job", row.original.metadata.name); - if (!setExpandedRun) { - return ( - - open_in_new - - ); - } else { - return ( - { - setExpandedRun(row.original); - }} - > - open_in_new - - ) - } - }, - maxWidth: 40, - }, - ], - [] - ); - - const data = useMemo(() => { - if (jobRuns === null) { - return []; - } - let tmp = [...tmpJobRuns.current]; - const filter = new JobRunsFilter(tmp); - switch (lastRunStatus) { - case "active": - tmp = filter.filterByActive(); - break; - case "failed": - tmp = filter.filterByFailed(); - break; - case "succeeded": - tmp = filter.filterBySucceded(); - break; - default: - tmp = filter.dontFilter(releaseName, jobName, namespace); - break; - } - - const sorter = new JobRunsSorter(tmp); - switch (sortType) { - case "Alphabetical": - tmp = sorter.sortByAlphabetical(); - break; - case "Newest": - tmp = sorter.sortByNewest(); - break; - case "Oldest": - tmp = sorter.sortByOldest(); - break; - default: - break; - } - - return tmp; - }, [jobRuns, lastRunStatus, sortType]); - - if (hasError && lastStreamStatus.current !== "finished") { - return ( - - Couldn't retrieve jobs, please try again.{" "} - getJobRuns()}>Retry - - ); - } - - if (jobRuns === null) { - return ; - } - - if (!jobRuns?.length) { - return ( -
- No job runs found - - - There are no jobs runs with the provided filters. - -
- ); - } - - return ( - - ); -}; - -export default JobRuns; - -const RetryButton = styled.button` - margin-left: 10px; - border: none; - background: #5460c6; - color: white; - padding: 5px 10px; - border-radius: 25px; - min-height: 35px; - min-width: 65px; - cursor: pointer; -`; - -const ErrorWrapper = styled.div` - display: flex; - align-items: center; - justify-content: center; - min-height: 300px; - width: 100%; - color: #ffffff88; -`; - -const Status = styled.div<{ color: string }>` - padding: 5px 10px; - background: ${(props) => props.color}; - font-size: 13px; - border-radius: 3px; - display: flex; - align-items: center; - justify-content: center; - width: min-content; - height: 25px; - min-width: 90px; -`; - -const CommandString = styled.div` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 160px; - color: #ffffff55; - margin-right: 27px; - font-family: monospace; -`; - -const RedirectButton = styled(DynamicLink)` - user-select: none; - display: flex; - align-items: center; - justify-content: flex-end; - > i { - border-radius: 20px; - font-size: 18px; - padding: 5px; - margin: 0 5px; - color: #ffffff44; - :hover { - background: #ffffff11; - } - } -`; - -const ExpandButton = styled.div` - user-select: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: flex-end; - > i { - border-radius: 20px; - font-size: 18px; - padding: 5px; - margin: 0 5px; - color: #ffffff44; - :hover { - background: #ffffff11; - } - } -`; - -type JobRun = { - metadata: { - name: string; - namespace: string; - selfLink: string; - uid: string; - resourceVersion: string; - creationTimestamp: string; - labels: { - [key: string]: string; - "app.kubernetes.io/instance": string; - "app.kubernetes.io/managed-by": string; - "app.kubernetes.io/version": string; - "helm.sh/chart": string; - "helm.sh/revision": string; - "meta.helm.sh/release-name": string; - }; - ownerReferences: { - apiVersion: string; - kind: string; - name: string; - uid: string; - controller: boolean; - blockOwnerDeletion: boolean; - }[]; - managedFields: unknown[]; - }; - spec: { - [key: string]: unknown; - parallelism: number; - completions: number; - backOffLimit?: number; - selector: { - [key: string]: unknown; - matchLabels: { - [key: string]: unknown; - "controller-uid": string; - }; - }; - template: { - [key: string]: unknown; - metadata: { - creationTimestamp: string | null; - labels: { - [key: string]: unknown; - "controller-uid": string; - "job-name": string; - }; - }; - spec: { - containers: { - name: string; - image: string; - command: string[]; - env?: { - [key: string]: unknown; - name: string; - value?: string; - valueFrom?: { - secretKeyRef?: { name: string; key: string }; - configMapKeyRef?: { name: string; key: string }; - }; - }[]; - resources: { - [key: string]: unknown; - limits: { [key: string]: unknown; memory: string }; - requests: { [key: string]: unknown; cpu: string; memory: string }; - }; - terminationMessagePath: string; - terminationMessagePolicy: string; - imagePullPolicy: string; - }[]; - - restartPolicy: string; - terminationGracePeriodSeconds: number; - dnsPolicy: string; - shareProcessNamespace: boolean; - securityContext: unknown; - schedulerName: string; - tolerations: { - [key: string]: unknown; - key: string; - operator: string; - value: string; - effect: string; - }[]; - }; - }; - }; - status: { - [key: string]: unknown; - conditions: { - [key: string]: unknown; - type: string; - status: string; - lastProbeTime: string; - lastTransitionTime: string; - }[]; - startTime: string; - completionTime: string | undefined | null; - succeeded?: number; - failed?: number; - active?: number; - }; -}; - -class JobRunsFilter { - jobRuns: JobRun[]; - - constructor(newJobRuns: JobRun[]) { - this.jobRuns = newJobRuns; - } - - // TODO: to support this filter, add appName filter (see dontFilter()) - filterByFailed() { - return this.jobRuns.filter((jobRun) => jobRun?.status?.failed); - } - - filterByActive() { - return this.jobRuns.filter((jobRun) => jobRun?.status?.active); - } - - filterBySucceded() { - return this.jobRuns.filter( - (jobRun) => - jobRun?.status?.succeeded && - !jobRun?.status?.active && - !jobRun?.status?.failed - ); - } - - dontFilter(releaseName?: string, jobName?: string, namespace?: string) { - if (releaseName) { - const filteredJobs = this.jobRuns.filter(x => { - return releaseName === x?.metadata?.labels["meta.helm.sh/release-name"]; - }); - return filteredJobs; - } else if (jobName) { - const filteredJobs = this.jobRuns.filter(x => { - let name = x?.metadata?.name; - let appName = namespace.split("porter-stack-")[1]; - return name.startsWith(`${appName}-${jobName}`) && name.split(`${appName}-${jobName}-`).length > 1 && name.split(`${appName}-${jobName}-`)[1].split("-").length === 2; - }); - return filteredJobs; - } - return this.jobRuns; - } -} - -class JobRunsSorter { - jobRuns: JobRun[]; - - constructor(newJobRuns: JobRun[]) { - this.jobRuns = newJobRuns; - } - - sortByNewest() { - return this.jobRuns.sort((a, b) => { - return Date.parse(a?.metadata?.creationTimestamp) > - Date.parse(b?.metadata?.creationTimestamp) - ? -1 - : 1; - }); - } - - sortByOldest() { - return this.jobRuns.sort((a, b) => { - return Date.parse(a?.metadata?.creationTimestamp) > - Date.parse(b?.metadata?.creationTimestamp) - ? 1 - : -1; - }); - } - - sortByAlphabetical() { - return this.jobRuns.sort((a, b) => - a?.metadata?.name > b?.metadata?.name ? 1 : -1 - ); - } -} diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/PorterAppRevisionSection.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/PorterAppRevisionSection.tsx deleted file mode 100644 index c1a8bd3a6f..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/PorterAppRevisionSection.tsx +++ /dev/null @@ -1,548 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; -import loading from "assets/loading.gif"; - -import api from "shared/api"; -import { Context } from "shared/Context"; -import { ChartType, CreateUpdatePorterAppOptions, StorageType } from "shared/types"; - -import ConfirmOverlay from "components/ConfirmOverlay"; -import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc"; - -import Modal from "main/home/modals/Modal"; -import UpgradeChartModal from "main/home/modals/UpgradeChartModal"; -import { readableDate } from "shared/string_utils"; -import { createPortal } from "react-dom"; -import yaml from "js-yaml"; - -type PropsType = WithAuthProps & { - chart: ChartType; - refreshChart: () => void; - setRevision: (x: ChartType, isCurrent?: boolean) => void; - forceRefreshRevisions: boolean; - refreshRevisionsOff: () => void; - shouldUpdate: boolean; - upgradeVersion: (version: string, cb: () => void) => void; - latestVersion: string; - showRevisions?: boolean; - toggleShowRevisions?: () => void; - updatePorterApp: (options: Partial) => Promise; - appName: string; -}; - -type StateType = { - revisions: ChartType[]; - rollbackRevision: number | null; - upgradeVersion: string; - loading: boolean; - maxVersion: number; - expandRevisions: boolean; -}; - -// TODO: refactor this component it's so gross -class PorterAppRevisionSection extends Component { - state = { - revisions: [] as ChartType[], - rollbackRevision: null as number | null, - upgradeVersion: "", - loading: false, - maxVersion: 0, // Track most recent version even when previewing old revisions - expandRevisions: false, - }; - - ws: WebSocket | null = null; - - refreshHistory = () => { - let { chart } = this.props; - let { currentCluster, currentProject } = this.context; - - return api - .getRevisions( - "", - {}, - { - id: currentProject.id, - namespace: chart.namespace, - cluster_id: currentCluster.id, - name: chart.name, - } - ) - .then((res) => { - res.data.sort((a: ChartType, b: ChartType) => { - return -(a.version - b.version); - }); - this.setState({ - revisions: res.data, - maxVersion: res.data[0].version, - }); - }) - .catch(console.log); - }; - - componentDidMount() { - this.refreshHistory(); - this.connectToLiveUpdates(); - } - - componentWillUnmount() { - if (this.ws) { - this.ws.close(); // Close the WebSocket connection - } - } - - connectToLiveUpdates() { - let { chart } = this.props; - let { currentCluster, currentProject } = this.context; - - const apiPath = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/helm_release?charts=${chart.name}`; - const protocol = window.location.protocol == "https:" ? "wss" : "ws"; - const url = `${protocol}://${window.location.host}`; - - this.ws = new WebSocket(`${url}${apiPath}`); - - this.ws.onopen = () => { - console.log("connected to chart live updates websocket"); - }; - - this.ws.onmessage = (evt: MessageEvent) => { - let event = JSON.parse(evt.data); - - if (event.event_type == "UPDATE") { - let object = event.Object; - - this.setState( - (prevState) => { - const { revisions: oldRevisions } = prevState; - // Copy old array to clean up references - const prevRevisions = [...oldRevisions]; - - // Check if it's an update of a revision or if it's a new one - const revisionIndex = prevRevisions.findIndex((rev) => { - if (rev.version === object.version) { - return true; - } - }); - - // Place new one at top of the array or update the old one - if (revisionIndex > -1) { - prevRevisions.splice(revisionIndex, 1, object); - } else { - return { ...prevState, revisions: [object, ...prevRevisions] }; - } - - return { ...prevState, revisions: prevRevisions, maxVersion: Math.max(...prevRevisions.map(rev => rev.version)) }; - }, - () => { - this.props.setRevision(this.state.revisions[0], true); - } - ); - } - }; - - this.ws.onclose = () => { - console.log("closing chart live updates websocket"); - }; - - this.ws.onerror = (err: ErrorEvent) => { - console.log(err); - this.ws.close(); - }; - } - - // Handle update of values.yaml - componentDidUpdate(prevProps: PropsType) { - if (this.props.forceRefreshRevisions) { - this.props.refreshRevisionsOff(); - - // Force refresh occurs on submit -> set current to newest - this.refreshHistory().then(() => { - this.props.setRevision(this.state.revisions[0], true); - }); - } else if (this.props.chart !== prevProps.chart) { - this.refreshHistory(); - } - } - - handleRollback = async () => { - let { setCurrentError, currentCluster, currentProject } = this.context; - - let revisionNumber = this.state.rollbackRevision; - if (revisionNumber == null) { - return; - } - this.setState({ loading: true, rollbackRevision: null }); - - try { - await api.rollbackPorterApp( - "", - { - revision: revisionNumber, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - stack_name: this.props.appName, - } - ); - } catch { - // TODO: handle error better - setCurrentError(err.response.data); - } finally { - this.setState({ loading: false }); - } - }; - - handleClickRevision = (revision: ChartType) => { - this.props.setRevision( - revision, - revision.version === this.state.maxVersion - ); - }; - - renderRevisionList = () => { - return this.state.revisions.map((revision: ChartType, i: number) => { - let isCurrent = revision.version === this.state.maxVersion; - const isGithubApp = !!this.props.chart.git_action_config; - const imageTag = revision.config?.image?.tag || revision.config?.global?.image?.tag; - - const parsedImageTag = isGithubApp - ? String(imageTag).slice(0, 7) - : imageTag; - - const isStack = !!this.props.chart.stack_id; - - return ( - this.handleClickRevision(revision)} - selected={this.props.chart.version === revision.version} - > - - - - - - - ); - }); - }; - - renderExpanded = () => { - if (this.state.expandRevisions) { - return ( - - - - - - - - - - - {this.renderRevisionList()} - - - - ); - } - }; - - renderContents = () => { - if (this.state.loading) { - return ( - - - Updating . . . - - - ); - } - - let isCurrent = - this.props.chart.version === this.state.maxVersion || - this.state.maxVersion === 0; - return ( -
- {this.state.upgradeVersion && ( - this.setState({ upgradeVersion: "" })} - width="500px" - height="450px" - > - { - this.setState({ upgradeVersion: "" }); - }} - onSubmit={() => { - this.props.upgradeVersion(this.state.upgradeVersion, () => { - this.setState({ loading: false }); - }); - this.setState({ upgradeVersion: "", loading: true }); - }} - /> - - )} - { - if (typeof this.props.toggleShowRevisions === "function") { - this.props.toggleShowRevisions(); - } - this.setState((prev) => ({ - ...prev, - expandRevisions: !prev.expandRevisions, - })); - }} - > - - arrow_drop_down - {isCurrent - ? `Current version` - : `Previewing revision (not deployed)`}{" "} - - No. {this.props.chart.version} - - - {this.renderExpanded()} -
- ); - }; - - render() { - return ( - - {this.renderContents()} - {createPortal( - this.setState({ rollbackRevision: null })} - />, - document.body - )} - - ); - } -} - -PorterAppRevisionSection.contextType = Context; - -export default withAuth(PorterAppRevisionSection); - -const TableWrapper = styled.div` - padding-bottom: 20px; -`; - -const LoadingPlaceholder = styled.div` - height: 40px; - display: flex; - align-items: center; - padding-left: 20px; -`; - -const LoadingGif = styled.img` - width: 15px; - height: 15px; - margin-right: ${(props: { revision: boolean }) => - props.revision ? "0px" : "9px"}; - margin-left: ${(props: { revision: boolean }) => - props.revision ? "10px" : "0px"}; - margin-bottom: ${(props: { revision: boolean }) => - props.revision ? "-2px" : "0px"}; -`; - -const StatusWrapper = styled.div` - display: flex; - align-items: center; - font-family: "Work Sans", sans-serif; - font-size: 13px; - color: #ffffff55; - margin-right: 25px; -`; - -const RevisionList = styled.div` - overflow-y: auto; - max-height: 215px; -`; - -const RollbackButton = styled.div` - cursor: ${(props: { disabled: boolean }) => - props.disabled ? "not-allowed" : "pointer"}; - display: flex; - border-radius: 3px; - align-items: center; - justify-content: center; - font-weight: 500; - height: 21px; - font-size: 13px; - width: 70px; - background: ${(props: { disabled: boolean }) => - props.disabled ? "#aaaabbee" : "#616FEEcc"}; - :hover { - background: ${(props: { disabled: boolean }) => - props.disabled ? "" : "#405eddbb"}; - } -`; - -const Tr = styled.tr` - line-height: 2.2em; - cursor: ${(props: { disableHover?: boolean; selected?: boolean }) => - props.disableHover ? "" : "pointer"}; - background: ${(props: { disableHover?: boolean; selected?: boolean }) => - props.selected ? "#ffffff11" : ""}; - :hover { - background: ${(props: { disableHover?: boolean; selected?: boolean }) => - props.disableHover ? "" : "#ffffff22"}; - } -`; - -const Td = styled.td` - font-size: 13px; - color: #ffffff; - padding-left: 32px; -`; - -const Th = styled.td` - font-size: 13px; - font-weight: 500; - color: #aaaabb; - padding-left: 32px; -`; - -const RevisionsTable = styled.table` - width: 100%; - margin-top: 5px; - padding-left: 32px; - padding-bottom: 20px; - min-width: 500px; - border-collapse: collapse; -`; - -const Revision = styled.div` - color: #ffffff; - margin-left: 5px; -`; - -const RevisionHeader = styled.div` - color: ${(props: { showRevisions: boolean; isCurrent: boolean }) => - props.isCurrent ? "#ffffff66" : "#f5cb42"}; - display: flex; - justify-content: space-between; - align-items: center; - height: 40px; - font-size: 13px; - width: 100%; - padding-left: 10px; - cursor: pointer; - background: ${({ theme }) => theme.fg}; - :hover { - background: ${(props) => props.showRevisions && props.theme.fg2}; - } - - > div > i { - margin-right: 8px; - font-size: 20px; - cursor: pointer; - border-radius: 20px; - transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) => - props.showRevisions ? "" : "rotate(-90deg)"}; - transition: transform 0.1s ease; - } -`; - -const StyledRevisionSection = styled.div` - width: 100%; - max-height: ${(props: { showRevisions: boolean }) => - props.showRevisions ? "255px" : "40px"}; - margin: 20px 0px 18px; - overflow: hidden; - border-radius: 5px; - background: ${props => props.theme.fg}; - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - } - animation: ${(props: { showRevisions: boolean }) => - props.showRevisions ? "expandRevisions 0.3s" : ""}; - animation-timing-function: ease-out; - @keyframes expandRevisions { - from { - max-height: 40px; - } - to { - max-height: 250px; - } - } -`; - -const RevisionPreview = styled.div` - display: flex; - align-items: center; -`; - -const RevisionUpdateMessage = styled.div` - color: white; - display: flex; - align-items: center; - padding: 4px 10px; - border-radius: 5px; - margin-right: 10px; - - :hover { - border: 1px solid white; - padding: 3px 9px; - } - - > i { - margin-right: 6px; - font-size: 20px; - cursor: pointer; - border-radius: 20px; - transform: none; - } -`; - -const A = styled.a` - color: #8590ff; - text-decoration: underline; - cursor: pointer; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/SettingsTab.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/SettingsTab.tsx deleted file mode 100644 index 170e682fda..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/SettingsTab.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import Button from "components/porter/Button"; -import Spacer from "components/porter/Spacer"; -import Text from "components/porter/Text"; - -import React, { useEffect, useState } from "react"; -import styled from "styled-components"; -import DeleteApplicationModal from "./DeleteApplicationModal"; - -type Props = { - appName: string; - githubWorkflowFilename: string; - deleteApplication: (deleteWorkflowFile?: boolean) => void; -}; - -const SettingsTab: React.FC = ({ - appName, - githubWorkflowFilename, - deleteApplication -}) => { - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - - useEffect(() => { - // Do something - }, []); - - return ( - - Delete "{appName}" - - - Delete this application and all of its resources. - - - - {isDeleteModalOpen && - setIsDeleteModalOpen(false)} - githubWorkflowFilename={githubWorkflowFilename} - deleteApplication={deleteApplication} - /> - } - - ); -}; - -export default SettingsTab; - -const StyledSettingsTab = styled.div` -width: 100%; -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx deleted file mode 100644 index edaaa1ca28..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx +++ /dev/null @@ -1,508 +0,0 @@ -import React, { useEffect, useState, useContext, useMemo } from "react"; -import styled from "styled-components"; - -import api from "shared/api"; -import { Context } from "shared/Context"; - -import Text from "components/porter/Text"; -import Container from "components/porter/Container"; -import Button from "components/porter/Button"; -import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets"; -import { - getAvailability, - getAvailabilityStacks, -} from "../../cluster-dashboard/expanded-chart/deploy-status-section/util"; -import Spacer from "components/porter/Spacer"; -import { pushFiltered } from "shared/routing"; -import { RouteComponentProps, useLocation, withRouter } from "react-router"; -import { timeFormat } from "d3-time-format"; -import AnimateHeight, { Height } from "react-animate-height"; -import { ControllerTabPodType } from "./status/ControllerTab"; -import _ from "lodash"; -import Link from "components/porter/Link"; - -type Props = RouteComponentProps & { - chart: any; - service: any; - setExpandedJob: any; -}; - -interface ErrorMessage { - revision: string; - message: string; -} - -const StatusFooter: React.FC = ({ - chart, - service, - setExpandedJob, - ...props -}) => { - const { currentProject, currentCluster } = useContext(Context); - const [controller, setController] = React.useState(null); - const [available, setAvailable] = React.useState(0); - const [total, setTotal] = React.useState(0); - const [stale, setStale] = React.useState(0); - const location = useLocation(); - const [unavailable, setUnavailable] = React.useState(0); - const [height, setHeight] = useState(0); - const [expanded, setExpanded] = useState(false); - const [pods, setPods] = useState([]); - - const { - newWebsocket, - openWebsocket, - closeAllWebsockets, - closeWebsocket, - } = useWebsockets(); - - const selectors = useMemo(() => { - let ml = - controller?.spec?.selector?.matchLabels || controller?.spec?.selector; - let i = 1; - let selector = ""; - for (var key in ml) { - selector += key + "=" + ml[key]; - if (i != Object.keys(ml).length) { - selector += ","; - } - i += 1; - } - return selector; - }, [controller]); - - useEffect(() => { - updatePods(); - if (selectors.length > 0) { - // updatePods(); - [controller?.kind, "pod"].forEach((kind) => { - setupWebsocket(kind, controller?.metadata?.uid, selectors); - }); - return () => closeAllWebsockets(); - } - }, [controller]); - - const getName = (service: any) => { - const name = chart.name + "-" + service.name; - - switch (service.type) { - case "web": - return name + "-web"; - case "worker": - return name + "-wkr"; - case "job": - return name + "job"; - } - }; - - useEffect(() => { - if (chart) { - api - .getChartControllers( - "", - {}, - { - namespace: chart.namespace, - cluster_id: currentCluster.id, - id: currentProject.id, - name: chart.name, - revision: chart.version, - } - ) - .then((res: any) => { - const controllers = - chart.chart.metadata.name == "job" - ? res.data[0]?.status.active - : res.data; - const filteredControllers = controllers.filter((controller: any) => { - const name = getName(service); - return name == controller.metadata.name; - }); - if (filteredControllers.length == 1) { - setController(filteredControllers[0]); - } - }) - .catch((err) => { - console.log(err); - }); - } - }, [chart]); - - const setupWebsocket = ( - kind: string, - controllerUid: string, - selectors: string - ) => { - let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status?`; - if (kind == "pod" && selectors) { - apiEndpoint += `selectors=${selectors}`; - } - - const options: NewWebsocketOptions = {}; - options.onopen = () => { }; - - options.onmessage = async (evt: MessageEvent) => { - let event = JSON.parse(evt.data); - let object = event.Object; - object.metadata.kind = event.Kind; - - // Make a new API call to update pods only when the event type is UPDATE - if (event.event_type !== "UPDATE") { - return; - } - // update pods no matter what if ws message is a pod event. - // If controller event, check if ws message corresponds to the designated controller in props. - if (event.Kind != "pod" && object.metadata.uid !== controllerUid) { - return; - } - - if (event.Kind === "deployment") { - let [available, total, stale, unavailable] = getAvailabilityStacks( - object - ); - - setAvailable(available); - setTotal(total); - setStale(stale); - setUnavailable(unavailable); - return; - } - await updatePods(); - }; - - options.onclose = () => { }; - - options.onerror = (err: ErrorEvent) => { - console.log(err); - closeWebsocket(kind); - }; - - newWebsocket(kind, apiEndpoint, options); - openWebsocket(kind); - }; - - const replicaSetArray = useMemo(() => { - setExpanded(false); - setHeight(0); - const podsDividedByReplicaSet = _.sortBy(pods, ["revisionNumber"]) - .reverse() - .reduce>>(function ( - prev, - currentPod, - i - ) { - if ( - !i || - prev[prev.length - 1][0].replicaSetName !== currentPod.replicaSetName - ) { - return prev.concat([[currentPod]]); - } - prev[prev.length - 1].push(currentPod); - return prev; - }, - []); - - return podsDividedByReplicaSet; - }, [pods]); - - const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y"); - - const updatePods = async () => { - try { - const res = await api.getMatchingPods( - "", - { - namespace: controller?.metadata?.namespace, - selectors: [selectors], - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - const data = res?.data as any[]; - let newPods = data - // Parse only data that we need - .map((pod: any) => { - const replicaSetName = - Array.isArray(pod?.metadata?.ownerReferences) && - pod?.metadata?.ownerReferences[0]?.name; - const containerStatus = - Array.isArray(pod?.status?.containerStatuses) && - pod?.status?.containerStatuses[0]; - - const restartCount = containerStatus - ? containerStatus.restartCount - : "N/A"; - - const podAge = formatCreationTimestamp( - new Date(pod?.metadata?.creationTimestamp) - ); - - const failing = containerStatus?.state?.waiting?.reason === "CrashLoopBackOff" ?? false; - const crashLoopReason = containerStatus?.lastState?.terminated?.message ?? ""; - - return { - namespace: pod?.metadata?.namespace, - name: pod?.metadata?.name, - phase: pod?.status?.phase, - status: pod?.status, - replicaSetName, - restartCount, - containerStatus, - podAge: pod?.metadata?.creationTimestamp ? podAge : "N/A", - revisionNumber: - pod?.metadata?.annotations?.["helm.sh/revision"] || "N/A", - crashLoopReason, - failing - }; - }); - - setPods(newPods); - } catch (error) { - // TODO: handle error - } - }; - - if (service.type === "job") { - return ( - - {service.type === "job" && ( - - {/* - check - - Last run succeeded at 12:39 PM on 4/13/23 - - */} - - - - - )} - - ); - } - - return ( - <> - {replicaSetArray != null && - replicaSetArray.length > 0 && - replicaSetArray.map((replicaSet, i) => { - return ( - <> - - - {replicaSet.some((r) => r.crashLoopReason != "") || replicaSet.some((r) => r.failing) ? ( - <> - - - - {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s" - } ${replicaSet.length === 1 ? "is" : "are" - } failing to run Version ${replicaSet[0].revisionNumber - }`} - - - {replicaSet.some((r) => r.crashLoopReason != "") && - - } - - ) : // check if there are more recent replicasets and if the previous replicaset has a crashloop reason - i > 0 && - !replicaSetArray[i - 1].some( - (p) => p.crashLoopReason != "" - ) ? ( - - - - {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s" - } ${replicaSet.length === 1 ? "is" : "are" - } still running at Version ${replicaSet[0].revisionNumber - }. Spinning down...`} - - - ) : ( - - {replicaSet.length ? ( - - ) : ( - - )} - - {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s" - } ${replicaSet.length === 1 ? "is" : "are" - } running at Version ${replicaSet[0].revisionNumber}`} - - - )} - - - {replicaSet.some((r) => r.crashLoopReason != "") && ( - - - - { - replicaSet.find((r) => r.crashLoopReason != "") - ?.crashLoopReason - } - - - - )} - - ); - })} - - ); -}; - -export default withRouter(StatusFooter); - -const StatusDot = styled.div<{ color?: string }>` - min-width: 7px; - max-width: 7px; - height: 7px; - border-radius: 50%; - margin-right: 10px; - background: ${(props) => props.color || "#38a88a"}; - - box-shadow: 0 0 0 0 rgba(0, 0, 0, 1); - transform: scale(1); - animation: pulse 2s infinite; - @keyframes pulse { - 0% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7); - } - - 70% { - transform: scale(1); - box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); - } - - 100% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); - } - } -`; - -const Mi = styled.i` - font-size: 16px; - margin-right: 7px; - margin-top: -1px; - color: rgb(56, 168, 138); -`; - -const I = styled.i` - font-size: 14px; - margin-right: 5px; -`; - -const StatusCircle = styled.div<{ - percentage?: any; - dashed?: boolean; -}>` - width: 16px; - height: 16px; - border-radius: 50%; - margin-right: 10px; - background: conic-gradient( - from 0deg, - #ffffff33 ${(props) => props.percentage}, - #ffffffaa 0% ${(props) => props.percentage} - ); - border: ${(props) => (props.dashed ? "1px dashed #ffffff55" : "none")}; -`; - -const Running = styled.div` - display: flex; - align-items: center; -`; - -const StyledStatusFooter = styled.div` - width: 100%; - padding: 10px 15px; - background: ${(props) => props.theme.fg2}; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; - border: 1px solid #494b4f; - border-top: 0; - overflow: hidden; - display: flex; - align-items: stretch; - flex-direction: row; - animation: fadeIn 0.5s; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const StyledStatusFooterTop = styled(StyledStatusFooter) <{ - expanded: boolean; -}>` - height: 40px; - border-bottom: ${({ expanded }) => expanded && "0px"}; - border-bottom-left-radius: ${({ expanded }) => expanded && "0px"}; - border-bottom-right-radius: ${({ expanded }) => expanded && "0px"}; -`; - -const Message = styled.div` - padding: 20px; - background: #000000; - border-radius: 5px; - line-height: 1.5em; - border: 1px solid #aaaabb33; - font-family: monospace; - font-size: 13px; - display: flex; - align-items: center; - > img { - width: 13px; - margin-right: 20px; - } - width: 100%; - height: 101px; - overflow: hidden; -`; - -const StyledContainer = styled.div<{ - row: boolean; - spaced: boolean; -}>` - display: ${(props) => (props.row ? "flex" : "block")}; - align-items: center; - justify-content: ${(props) => - props.spaced ? "space-between" : "flex-start"}; - width: 100%; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx deleted file mode 100644 index b90f7b2165..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import React, { useEffect, useState, useContext } from "react"; -import styled from "styled-components"; - -import api from "shared/api"; -import { Context } from "shared/Context"; - -import Text from "components/porter/Text"; - -import EventCard from "./events/cards/EventCard"; -import Loading from "components/Loading"; -import Spacer from "components/porter/Spacer"; -import Fieldset from "components/porter/Fieldset"; - -import { feedDate } from "shared/string_utils"; -import Pagination from "components/porter/Pagination"; -import _ from "lodash"; -import Button from "components/porter/Button"; -import { PorterAppEvent, PorterAppEventType } from "./events/types"; - -type Props = { - chart: any; - stackName: string; - appData: any; -}; - -const EVENTS_POLL_INTERVAL = 5000; // poll every 5 seconds - -const ActivityFeed: React.FC = ({ chart, stackName, appData }) => { - const { currentProject, currentCluster } = useContext(Context); - - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [page, setPage] = useState(1); - const [numPages, setNumPages] = useState(0); - const [hasPorterAgent, setHasPorterAgent] = useState(false); - const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false); - const [shouldAnimate, setShouldAnimate] = useState(true); - - // remove this filter when https://linear.app/porter/issue/POR-1676/disable-porter-agent-code-for-cpu-alerts is resolved - const isNotFilteredAppEvent = (event: PorterAppEvent) => { - return !(event.type === PorterAppEventType.APP_EVENT && - ( - event.metadata?.short_summary?.includes("requesting more memory than is available") - || event.metadata?.short_summary?.includes("requesting more CPU than is available") - || event.metadata?.short_summary?.includes("non-zero exit code") - ) - ); - } - - const getEvents = async () => { - setLoading(true) - if (!currentProject || !currentCluster) { - setError(true); - setLoading(false); - return; - } - try { - const res = await api.getFeedEvents( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - stack_name: stackName, - page, - } - ); - - setNumPages(res.data.num_pages); - setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(isNotFilteredAppEvent) ?? []); - } catch (err) { - setError(err); - } finally { - setLoading(false); - setShouldAnimate(false); - } - }; - - const getLatestDeployEventIndex = () => { - const deployEvents = events.filter((event) => event.type === PorterAppEventType.DEPLOY); - if (deployEvents.length === 0) { - return -1; - } - return events.indexOf(deployEvents[0]); - }; - - const updateEvents = async () => { - if (!currentProject || !currentCluster) { - return; - } - try { - const res = await api.getFeedEvents( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - stack_name: stackName, - page, - } - ); - setError(undefined) - setNumPages(res.data.num_pages); - setEvents(res.data.events?.map((event: any) => PorterAppEvent.toPorterAppEvent(event)).filter(isNotFilteredAppEvent) ?? []); - } catch (err) { - setError(err); - } - } - - useEffect(() => { - const checkForAgent = async () => { - const project_id = currentProject?.id; - const cluster_id = currentCluster?.id; - if (project_id == null || cluster_id == null) { - setError(true); - return; - } - try { - const res = await api.detectPorterAgent("", {}, { project_id, cluster_id }); - const hasAgent = res.data?.version === "v3"; - setHasPorterAgent(hasAgent); - } catch (err) { - if (err.response?.status === 404) { - setHasPorterAgent(false); - } - } finally { - setLoading(false); - } - }; - - if (!hasPorterAgent) { - checkForAgent(); - } else { - const intervalId = setInterval(updateEvents, EVENTS_POLL_INTERVAL); - getEvents(); - return () => clearInterval(intervalId); - } - - }, [currentProject, currentCluster, hasPorterAgent, page]); - - const installAgent = async () => { - const project_id = currentProject?.id; - const cluster_id = currentCluster?.id; - - setIsPorterAgentInstalling(true); - try { - await api.installPorterAgent("", {}, { project_id, cluster_id }); - window.location.reload(); - } catch (err) { - setIsPorterAgentInstalling(false); - console.log(err); - } - }; - - if (isPorterAgentInstalling) { - return ( -
- Installing agent... - - If you are not redirected automatically after a minute, you may need to refresh this page. -
- ); - } - - if (error) { - return ( -
- Error retrieving events - - An unexpected error occurred. -
- ); - } - - if (loading) { - return ( -
- - -
- ); - } - - if (!loading && !hasPorterAgent) { - return ( -
- - We couldn't detect the Porter agent on your cluster - - - - In order to use the Activity tab, you need to install the Porter agent. - - - -
- ); - } - - if (!loading && events?.length === 0) { - return ( -
- No events found for "{stackName}" - - - This application currently has no associated events. - -
- ); - } - - return ( - - {events.map((event, i) => { - return ( - - {i !== events.length - 1 && events.length > 1 && } - - - - - ); - })} - {numPages > 1 && ( - <> - - - - )} - - ); -}; - -export default ActivityFeed; - -const I = styled.i` - font-size: 14px; - margin-right: 5px; -`; - -const Time = styled.div<{ shouldAnimate: boolean }>` - opacity: ${(props) => props.shouldAnimate ? "0" : "1"}; - ${(props) => props.shouldAnimate && "animation: fadeIn 0.3s 0.1s;"} - ${(props) => props.shouldAnimate && "animation-fill-mode: forwards;"} - width: 90px; -`; - -const Line = styled.div<{ shouldAnimate: boolean }>` - width: 1px; - height: calc(100% + 30px); - background: #414141; - position: absolute; - left: 3px; - top: 36px; - opacity: ${(props) => props.shouldAnimate ? "0" : "1"}; - ${(props) => props.shouldAnimate && "animation: fadeIn 0.3s 0.1s;"} - ${(props) => props.shouldAnimate && "animation-fill-mode: forwards;"} -`; - -const Dot = styled.div<{ shouldAnimate: boolean }>` - width: 7px; - height: 7px; - background: #fff; - border-radius: 50%; - margin-left: -29px; - margin-right: 20px; - z-index: 1; - opacity: ${(props) => props.shouldAnimate ? "0" : "1"}; - ${(props) => props.shouldAnimate && "animation: fadeIn 0.3s 0.1s;"} - ${(props) => props.shouldAnimate && "animation-fill-mode: forwards;"} -`; - -const EventWrapper = styled.div<{ - isLast: boolean; -}>` - padding-left: 30px; - display: flex; - align-items: center; - position: relative; - margin-bottom: ${(props) => (props.isLast ? "" : "25px")}; -`; - -const StyledActivityFeed = styled.div<{ shouldAnimate: boolean }>` - width: 100%; - ${(props) => props.shouldAnimate && "animation: fadeIn 0.3s 0s;"} - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/AppEventCard.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/AppEventCard.tsx deleted file mode 100644 index 37c8123920..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/AppEventCard.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useState } from "react"; - -import app_event from "assets/app_event.png"; -import Text from "components/porter/Text"; -import Container from "components/porter/Container"; -import Spacer from "components/porter/Spacer"; -import Link from "components/porter/Link"; -import Icon from "components/porter/Icon"; - -import { StyledEventCard } from "./EventCard"; -import AppEventModal from "../../../status/AppEventModal"; -import { readableDate } from "shared/string_utils"; -import dayjs from "dayjs"; -import Anser from "anser"; -import api from "shared/api"; -import { Direction } from "../../../logs/types"; -import { PorterAppEvent } from "../types"; - -type Props = { - event: PorterAppEvent; - appData: any; -}; - -const AppEventCard: React.FC = ({ event, appData }) => { - const [showModal, setShowModal] = useState(false); - const [logs, setLogs] = useState([]); - - const getAppLogs = async () => { - setShowModal(true); - try { - const logResp = await api.getLogsWithinTimeRange( - "", - { - namespace: appData.chart.namespace, - start_range: dayjs(event.created_at).subtract(1, 'minute').toISOString(), - end_range: dayjs(event.updated_at).add(1, 'minute').toISOString(), - pod_selector: event.metadata.pod_name.endsWith(".*") ? event.metadata.pod_name : event.metadata.pod_name + ".*", - limit: 1000, - direction: Direction.forward, - }, - { - project_id: appData.app.project_id, - cluster_id: appData.app.cluster_id, - } - ) - - if (logResp.data?.logs != null) { - const updatedLogs = logResp.data.logs.map((l: { line: string; timestamp: string; }, index: number) => { - try { - return { - line: JSON.parse(l.line)?.log ?? Anser.ansiToJson(l.line), - lineNumber: index + 1, - timestamp: l.timestamp, - } - } catch (err) { - return { - line: Anser.ansiToJson(l.line), - lineNumber: index + 1, - timestamp: l.timestamp, - } - } - }); - setLogs(updatedLogs); - } - } catch (error) { - console.log(error); - } - }; - - return ( - - - - - - {event.metadata.summary} - - - - - - View details - - - {showModal && ( - - )} - - ); -}; - -export default AppEventCard; - diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/BuildEventCard.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/BuildEventCard.tsx deleted file mode 100644 index 03b266c8bd..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/BuildEventCard.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useState } from "react"; -import styled from "styled-components"; - -import build from "assets/build.png"; - -import run_for from "assets/run_for.png"; -import refresh from "assets/refresh.png"; - -import Text from "components/porter/Text"; -import Container from "components/porter/Container"; -import Spacer from "components/porter/Spacer"; -import Link from "components/porter/Link"; -import Icon from "components/porter/Icon"; -import api from "shared/api"; -import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs"; -import JSZip from "jszip"; -import Anser, { AnserJsonEntry } from "anser"; -import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils'; -import { StyledEventCard } from "./EventCard"; -import document from "assets/document.svg"; -import { PorterAppEvent } from "../types"; - -type Props = { - event: PorterAppEvent; - appData: any; -}; - -const BuildEventCard: React.FC = ({ event, appData }) => { - const renderStatusText = (event: PorterAppEvent) => { - switch (event.status) { - case "SUCCESS": - return Build succeeded; - case "FAILED": - return Build failed; - default: - return Build in progress...; - } - }; - - const renderInfoCta = (event: PorterAppEvent) => { - switch (event.status) { - case "SUCCESS": - return null; - case "FAILED": - return ( - - - - - - View details - - - - triggerWorkflow(appData)}> - - - - Retry - - - - ); - default: - return ( - - - View live logs - - - - ); - } - }; - - return ( - - - - - - Application build - - - - - {getDuration(event)} - - - - - - - - {renderStatusText(event)} - - {renderInfoCta(event)} - - - - - ); -}; - -export default BuildEventCard; - -const Wrapper = styled.div` - margin-top: -3px; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/DeployEventCard.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/DeployEventCard.tsx deleted file mode 100644 index 3cea80a1fc..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/DeployEventCard.tsx +++ /dev/null @@ -1,229 +0,0 @@ -import React, { useState } from "react"; -import deploy from "assets/deploy.png"; -import Text from "components/porter/Text"; -import Container from "components/porter/Container"; -import Spacer from "components/porter/Spacer"; -import Icon from "components/porter/Icon"; -import { getStatusColor, getStatusIcon } from '../utils'; -import { StyledEventCard } from "./EventCard"; -import styled from "styled-components"; -import Link from "components/porter/Link"; -import ChangeLogModal from "../../../ChangeLogModal"; -import { PorterAppDeployEvent } from "../types"; -import AnimateHeight from "react-animate-height"; -import ServiceStatusDetail from "./ServiceStatusDetail"; - -type Props = { - event: PorterAppDeployEvent; - appData: any; - showServiceStatusDetail?: boolean; -}; - -const DeployEventCard: React.FC = ({ event, appData, showServiceStatusDetail = false }) => { - const [diffModalVisible, setDiffModalVisible] = useState(false); - const [revertModalVisible, setRevertModalVisible] = useState(false); - const [serviceStatusVisible, setServiceStatusVisible] = useState(showServiceStatusDetail); - - const renderStatusText = () => { - switch (event.status) { - case "SUCCESS": - return event.metadata.image_tag != null ? - event.metadata.service_deployment_metadata != null ? - - - Deployed {event.metadata.image_tag} to - - - {renderServiceDropdownCta(Object.keys(event.metadata.service_deployment_metadata).length, getStatusColor(event.status))} - - : - - Deployed {event.metadata.image_tag} - - : - - Deployment successful - ; - case "FAILED": - if (event.metadata.service_deployment_metadata != null) { - let failedServices = 0; - for (const key in event.metadata.service_deployment_metadata) { - if (event.metadata.service_deployment_metadata[key].status === "FAILED") { - failedServices++; - } - } - return ( - - - Failed to deploy {event.metadata.image_tag} to - - - {renderServiceDropdownCta(failedServices, getStatusColor(event.status))} - - ); - } else { - return ( - - Deployment failed - - ); - } - case "CANCELED": - if (event.metadata.service_deployment_metadata != null) { - let canceledServices = 0; - for (const key in event.metadata.service_deployment_metadata) { - if (event.metadata.service_deployment_metadata[key].status === "CANCELED") { - canceledServices++; - } - } - return ( - - - Canceled deploy of {event.metadata.image_tag} to - - - {renderServiceDropdownCta(canceledServices, getStatusColor(event.status))} - - ); - } else { - return ( - - Deployment canceled - - ); - } - default: - if (event.metadata.service_deployment_metadata != null) { - return ( - - - Deploying {event.metadata.image_tag} to - - - {renderServiceDropdownCta(Object.keys(event.metadata.service_deployment_metadata).length, getStatusColor(event.status))} - - ); - } else { - return ( - - Deploying {event.metadata.image_tag}... - - ); - } - } - }; - - const renderServiceDropdownCta = (numServices: number, color?: string) => { - return ( - - setServiceStatusVisible(!serviceStatusVisible)}> - arrow_drop_down - {numServices} service{numServices === 1 ? "" : "s"} - - - ) - } - - return ( - - - - - - Application version no. {event.metadata?.revision} - - - - - - - - {renderStatusText()} - {appData?.chart?.version !== event.metadata.revision && ( - <> - - - setRevertModalVisible(true)}> - Revert to version {event.metadata.revision} - - - - - )} - - - {event.metadata.revision != 1 && ( setDiffModalVisible(true)}> - View changes - )} - {diffModalVisible && ( - - )} - {revertModalVisible && ( - - )} - - - - {event.metadata.service_deployment_metadata != null && - - - - - } - - ); -}; - -export default DeployEventCard; - -// TODO: remove after fixing v-align -const TempWrapper = styled.div` - margin-top: -3px; -`; - -const Code = styled.span` - font-family: monospace; -`; - -const ServiceStatusDropdownCtaContainer = styled.div` - display: flex; - justify-content: center; - cursor: pointer; - padding: 3px 5px; - border-radius: 5px; - :hover { - background: #ffffff11; - } -`; - -const ServiceStatusDropdownIcon = styled.i` - margin-left: -5px; - font-size: 20px; - border-radius: 20px; - transform: ${(props: { serviceStatusVisible: boolean }) => - props.serviceStatusVisible ? "" : "rotate(-90deg)"}; - transition: transform 0.1s ease; -` - -const StatusTextContainer = styled.div` - display: flex; - align-items: center; - flex-direction: row; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/EventCard.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/EventCard.tsx deleted file mode 100644 index 5cf00207e2..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/EventCard.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from "react"; -import styled from "styled-components"; - -import BuildEventCard from "./BuildEventCard"; -import PreDeployEventCard from "./PreDeployEventCard"; -import AppEventCard from "./AppEventCard"; -import DeployEventCard from "./DeployEventCard"; -import { PorterAppDeployEvent, PorterAppEvent, PorterAppEventType } from "../types"; - -type Props = { - event: PorterAppEvent; - appData: any; - isLatestDeployEvent?: boolean; -}; - -const EventCard: React.FC = ({ event, appData, isLatestDeployEvent }) => { - const renderEventCard = (event: PorterAppEvent) => { - switch (event.type) { - case PorterAppEventType.APP_EVENT: - return ; - case PorterAppEventType.BUILD: - return ; - case PorterAppEventType.DEPLOY: - return ; - case PorterAppEventType.PRE_DEPLOY: - return ; - default: - return null; - }; - }; - - return renderEventCard(event); -}; - -export default EventCard; - -export const StyledEventCard = styled.div<{ row?: boolean }>` - width: 100%; - padding: 15px; - display: flex; - flex-direction: ${({ row }) => row ? "row" : "column"}; - justify-content: space-between; - border-radius: 5px; - background: ${({ theme }) => theme.fg}; - border: 1px solid ${({ theme }) => theme.border}; - opacity: 0; - animation: slideIn 0.5s 0s; - animation-fill-mode: forwards; - @keyframes slideIn { - from { - margin-left: -10px; - opacity: 0; - margin-right: 10px; - } - to { - margin-left: 0; - opacity: 1; - margin-right: 0; - } - } -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/PreDeployEventCard.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/PreDeployEventCard.tsx deleted file mode 100644 index 88283b4733..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/PreDeployEventCard.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useState } from "react"; -import styled from "styled-components"; - -import pre_deploy from "assets/pre_deploy.png"; - -import run_for from "assets/run_for.png"; -import refresh from "assets/refresh.png"; - -import Text from "components/porter/Text"; -import Container from "components/porter/Container"; -import Spacer from "components/porter/Spacer"; -import Icon from "components/porter/Icon"; - -import { getDuration, getStatusColor, getStatusIcon, triggerWorkflow } from '../utils'; -import { StyledEventCard } from "./EventCard"; -import Link from "components/porter/Link"; -import document from "assets/document.svg"; -import { PorterAppEvent } from "../types"; - -type Props = { - event: PorterAppEvent; - appData: any; -}; - -const PreDeployEventCard: React.FC = ({ event, appData }) => { - const renderStatusText = (event: PorterAppEvent) => { - switch (event.status) { - case "SUCCESS": - return Pre-deploy succeeded; - case "FAILED": - return Pre-deploy failed; - default: - return Pre-deploy in progress...; - } - }; - - return ( - - - - - - Application pre-deploy - - - - - {getDuration(event)} - - - - - - - - {renderStatusText(event)} - {(event.status !== "SUCCESS") && - <> - - - - - - - View details - - - - triggerWorkflow(appData)}> - - - - Retry - - - - - } - - - - - ); -}; - -export default PreDeployEventCard; - -const Wrapper = styled.div` - margin-top: -3px; -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/ServiceStatusDetail.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/ServiceStatusDetail.tsx deleted file mode 100644 index eb8a84bac0..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/ServiceStatusDetail.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import Icon from 'components/porter/Icon'; -import Spacer from 'components/porter/Spacer'; -import Text from 'components/porter/Text'; -import React from 'react' -import styled from 'styled-components'; -import { getStatusColor, getStatusIcon } from '../utils'; -import Link from 'components/porter/Link'; -import { PorterAppDeployEvent } from "../types"; -import { Service } from 'main/home/app-dashboard/new-app-flow/serviceTypes'; - -type Props = { - serviceDeploymentMetadata: PorterAppDeployEvent["metadata"]["service_deployment_metadata"]; - appName: string; - revision: number; -} - -const ServiceStatusDetail: React.FC = ({ - serviceDeploymentMetadata, - appName, - revision, -}) => { - const convertEventStatusToCopy = (status: string) => { - switch (status) { - case "PROGRESSING": - return "DEPLOYING"; - case "SUCCESS": - return "DEPLOYED"; - case "FAILED": - return "FAILED"; - case "CANCELED": - return "CANCELED"; - default: - return "UNKNOWN"; - } - }; - - return ( - -
- {Object.keys(serviceDeploymentMetadata).map((key) => { - const deploymentMetadata = serviceDeploymentMetadata[key]; - return ( - - - {key} - - - - - {convertEventStatusToCopy(serviceDeploymentMetadata[key].status)} - - - {deploymentMetadata.type !== "job" && - <> - - Logs - - - - Metrics - - - } - {deploymentMetadata.type === "job" && - <> - - History - - - } - {deploymentMetadata.external_uri !== "" && - <> - - - External link - - - } - - - ); - })} - - - ) -} - -export default ServiceStatusDetail; - -const ServiceStatusTable = styled.table` - border-collapse: collapse; - width: 100%; -`; - -const ServiceStatusTableRow = styled.tr` - display: flex; - align-items: center; -`; - -const ServiceStatusTableData = styled.td` - padding: 8px; - display: flex; - align-items: center; - ${(props) => props.width && `width: ${props.width};`} - - &:not(:last-child) { - border-right: 2px solid #ffffff11; - } -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx deleted file mode 100644 index 44a1383b55..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/BuildFailureEventFocusView.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import Loading from "components/Loading"; -import Spacer from "components/porter/Spacer"; -import React, { useEffect, useRef, useState } from "react"; -import api from "shared/api"; -import styled from "styled-components"; -import Anser, { AnserJsonEntry } from "anser"; -import JSZip from "jszip"; -import dayjs from "dayjs"; -import Text from "components/porter/Text"; -import { readableDate } from "shared/string_utils"; -import { getDuration } from "../utils"; -import Link from "components/porter/Link"; -import { PorterLog } from "../../../logs/types"; -import { PorterAppEvent } from "../types"; - -type Props = { - event: PorterAppEvent; - appData: any; -}; - -const BuildFailureEventFocusView: React.FC = ({ - event, - appData, -}) => { - const [logs, setLogs] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const scrollToBottomRef = useRef(null); - - useEffect(() => { - if (!isLoading && scrollToBottomRef.current) { - scrollToBottomRef.current.scrollIntoView({ - behavior: "smooth", - block: "end", - }); - } - }, [isLoading, logs, scrollToBottomRef]); - - const getBuildLogs = async () => { - if (event == null) { - return; - } - try { - setLogs([]); - - const res = await api.getGHWorkflowLogById( - "", - {}, - { - project_id: appData.app.project_id, - cluster_id: appData.app.cluster_id, - git_installation_id: appData.app.git_repo_id, - owner: appData.app.repo_name?.split("/")[0], - name: appData.app.repo_name?.split("/")[1], - filename: "porter_stack_" + appData.chart.name + ".yml", - run_id: event.metadata.action_run_id, - } - ); - let logs: PorterLog[] = []; - if (res.data != null) { - // Fetch the logs - const logsResponse = await fetch(res.data); - - // Ensure that the response body is only read once - const logsBlob = await logsResponse.blob(); - - if (logsResponse.headers.get("Content-Type") === "application/zip") { - const zip = await JSZip.loadAsync(logsBlob); - const promises: any[] = []; - - zip.forEach(function (relativePath, zipEntry) { - promises.push( - (async function () { - const fileData = await zip - .file(relativePath) - ?.async("string"); - - if ( - fileData && - fileData.includes("Run porter-dev/porter-cli-action@v0.1.0") - ) { - const lines = fileData.split("\n"); - const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes("Post job cleanup.")) { - break; - } - const lineWithoutTimestamp = line.replace(timestampPattern, "").trimStart(); - const anserLine: AnserJsonEntry[] = Anser.ansiToJson(lineWithoutTimestamp); - if (lineWithoutTimestamp.toLowerCase().includes("error")) { - anserLine[0].fg = "238,75,43"; - } - - const log: PorterLog = { - line: anserLine, - lineNumber: i + 1, - timestamp: line.match(timestampPattern)?.[0], - }; - - logs.push(log); - } - } - })() - ); - }); - - await Promise.all(promises); - setLogs(logs); - } - } - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - getBuildLogs(); - }, []); - - return ( - <> - Build failed - - Started {readableDate(event.created_at)} and ran for {getDuration(event)}. - - - {isLoading ? ( - - ) : logs.length == 0 ? ( - <> - - No logs found. - - - ) : ( - <> - {logs?.map((log, i) => { - return ( - - {log.lineNumber}. - - {log.timestamp - ? dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss") - : "-"} - - - {log.line?.map((ansi, j) => { - if (ansi.clearLine) { - return null; - } - - return ( - - {ansi.content.replace(/ /g, "\u00a0")} - - ); - })} - - - ); - })} - - )} -
- - - - View full build logs - - - ); -}; - -export default BuildFailureEventFocusView; - -const StyledLogsSection = styled.div` - width: 100%; - min-height: 600px; - height: calc(100vh - 460px); - display: flex; - flex-direction: column; - position: relative; - font-size: 13px; - border-radius: 8px; - border: 1px solid #ffffff33; - background: #000000; - animation: floatIn 0.3s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - overflow-y: auto; - overflow-wrap: break-word; - position: relative; - @keyframes floatIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0px); - } - } -`; - -const Message = styled.div` - display: flex; - height: 100%; - width: calc(100% - 150px); - align-items: center; - justify-content: center; - margin-left: 75px; - text-align: center; - color: #ffffff44; - font-size: 13px; -`; - -const Log = styled.div` - font-family: monospace; - user-select: text; - display: flex; - align-items: flex-end; - gap: 8px; - width: 100%; - & > * { - padding-block: 5px; - } - & > .line-timestamp { - height: 100%; - color: #949effff; - opacity: 0.5; - font-family: monospace; - min-width: fit-content; - padding-inline-end: 5px; - } - & > .line-number { - height: 100%; - background: #202538; - display: inline-block; - text-align: right; - min-width: 45px; - padding-inline-end: 5px; - opacity: 0.3; - font-family: monospace; - } -`; - -const LogOuter = styled.div` - display: inline-block; - word-wrap: anywhere; - flex-grow: 1; - font-family: monospace, sans-serif; - font-size: 12px; -`; - -const LogInnerSpan = styled.span` - font-family: monospace, sans-serif; - font-size: 12px; - font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) => - props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"}; - color: ${(props: { ansi: Anser.AnserJsonEntry }) => - props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"}; - background-color: ${(props: { ansi: Anser.AnserJsonEntry }) => - props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"}; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/DeployEventFocusView.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/DeployEventFocusView.tsx deleted file mode 100644 index 69c8cc925b..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/DeployEventFocusView.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import Spacer from "components/porter/Spacer"; -import React from "react"; -import dayjs from "dayjs"; -import Text from "components/porter/Text"; -import { readableDate } from "shared/string_utils"; -import { getDuration } from "../utils"; -import LogSection from "../../../logs/LogSection"; -import { AppearingView } from "./EventFocusView"; -import Icon from "components/porter/Icon"; -import loading from "assets/loading.gif"; -import Container from "components/porter/Container"; -import { PorterAppDeployEvent } from "../types"; -import { LogFilterQueryParamOpts } from "../../../logs/types"; - -type Props = { - event: PorterAppDeployEvent; - appData: any; - filterOpts?: LogFilterQueryParamOpts -}; - -const DeployEventFocusView: React.FC = ({ - event, - appData, - filterOpts, -}) => { - const renderHeaderText = () => { - switch (event.status) { - case "SUCCESS": - return Deploy succeeded; - case "FAILED": - return Deploy failed; - case "CANCELED": - return Deploy canceled; - default: - return ( - - - - Deploy in progress... - - ); - } - }; - - const renderDurationText = () => { - switch (event.status) { - case "PROGRESSING": - return Started {readableDate(event.created_at)}. - default: - return Started {readableDate(event.created_at)} and ran for {getDuration(event)}.; - } - } - - return ( - <> - - {renderHeaderText()} - - - {renderDurationText()} - - - - ); -}; - -export default DeployEventFocusView; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/EventFocusView.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/EventFocusView.tsx deleted file mode 100644 index 961d03ffe6..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/EventFocusView.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import Loading from "components/Loading"; -import Spacer from "components/porter/Spacer"; -import React, { useContext, useEffect, useState } from "react"; -import { Context } from "shared/Context"; -import api from "shared/api"; -import styled from "styled-components"; -import Link from "components/porter/Link"; -import BuildFailureEventFocusView from "./BuildFailureEventFocusView"; -import PreDeployEventFocusView from "./PredeployEventFocusView"; -import _ from "lodash"; -import { PorterAppDeployEvent, PorterAppEvent } from "../types"; -import DeployEventFocusView from "./DeployEventFocusView"; -import { LogFilterQueryParamOpts } from "../../../logs/types"; - -type Props = { - eventId: string; - appData: any; - filterOpts?: LogFilterQueryParamOpts; -}; - -const EVENT_POLL_INTERVAL = 5000; // poll every 5 seconds - -const EventFocusView: React.FC = ({ - eventId, - appData, - filterOpts, -}) => { - const { currentProject, currentCluster } = useContext(Context); - const [event, setEvent] = useState(null); - - useEffect(() => { - const getEvent = async () => { - if (currentProject == null || currentCluster == null) { - return; - } - try { - const eventResp = await api.getPorterAppEvent( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - event_id: eventId, - } - ) - const newEvent = PorterAppEvent.toPorterAppEvent(eventResp.data.event); - setEvent(newEvent); - if (newEvent.metadata.end_time != null) { - clearInterval(intervalId); - } - } catch (err) { - console.log(err); - } - } - const intervalId = setInterval(getEvent, EVENT_POLL_INTERVAL); - getEvent(); - return () => clearInterval(intervalId); - }, []); - - const getEventFocusView = (event: PorterAppEvent, appData: any) => { - switch (event.type) { - case "BUILD": - return - case "PRE_DEPLOY": - return - case "DEPLOY": - return - default: - return null - } - } - - return ( - - - - keyboard_backspace - Activity feed - - - - {event == null && } - {event != null && getEventFocusView(event, appData)} - - ); -}; - -export default EventFocusView; - -export const AppearingView = styled.div` - width: 100%; - animation: fadeIn 0.3s 0s; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const BackButton = styled.div` - display: flex; - align-items: center; - max-width: fit-content; - cursor: pointer; - font-size: 11px; - max-height: fit-content; - padding: 5px 13px; - border: 1px solid #ffffff55; - border-radius: 100px; - color: white; - background: #ffffff11; - - :hover { - background: #ffffff22; - } - - > i { - color: white; - font-size: 16px; - margin-right: 6px; - } -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/PredeployEventFocusView.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/PredeployEventFocusView.tsx deleted file mode 100644 index 2bdbfccebf..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/focus-views/PredeployEventFocusView.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import Spacer from "components/porter/Spacer"; -import React from "react"; -import dayjs from "dayjs"; -import Text from "components/porter/Text"; -import { readableDate } from "shared/string_utils"; -import { getDuration } from "../utils"; -import LogSection from "../../../logs/LogSection"; -import { AppearingView } from "./EventFocusView"; -import Icon from "components/porter/Icon"; -import loading from "assets/loading.gif"; -import Container from "components/porter/Container"; -import { PorterAppEvent } from "../types"; - -type Props = { - event: PorterAppEvent; - appData: any; -}; - -const PreDeployEventFocusView: React.FC = ({ - event, - appData, -}) => { - const renderHeaderText = () => { - switch (event.status) { - case "SUCCESS": - return Pre-deploy succeeded; - case "FAILED": - return Pre-deploy failed; - default: - return ( - - - - Pre-deploy in progress... - - ); - } - }; - - const renderDurationText = () => { - switch (event.status) { - case "PROGRESSING": - return Started {readableDate(event.created_at)}. - default: - return Started {readableDate(event.created_at)} and ran for {getDuration(event)}.; - } - } - - return ( - <> - - {renderHeaderText()} - - - {renderDurationText()} - - - - ); -}; - -export default PreDeployEventFocusView; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/types.ts b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/types.ts deleted file mode 100644 index 6c928970d2..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/types.ts +++ /dev/null @@ -1,44 +0,0 @@ -export enum PorterAppEventType { - BUILD = "BUILD", - DEPLOY = "DEPLOY", - APP_EVENT = "APP_EVENT", - PRE_DEPLOY = "PRE_DEPLOY", -} -export interface PorterAppEvent { - created_at: string; - updated_at: string; - id: string; - status: string; - type: PorterAppEventType; - type_source: string; - porter_app_id: number; - metadata: any; -} -export const PorterAppEvent = { - toPorterAppEvent: (data: any): PorterAppEvent => { - return { - created_at: data.created_at ?? "", - updated_at: data.updated_at ?? "", - id: data.id ?? "", - status: data.status ?? "", - type: data.type ?? "", - type_source: data.type_source ?? "", - porter_app_id: data.porter_app_id ?? "", - metadata: data.metadata ?? {}, - }; - } -} - -interface PorterAppServiceDeploymentMetadata { - status: string; - external_uri: string; - type: string; -} -export interface PorterAppDeployEvent extends PorterAppEvent { - type: PorterAppEventType.DEPLOY; - metadata: { - image_tag: string; - revision: number; - service_deployment_metadata: Record; - }; -} \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts b/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts deleted file mode 100644 index 35dfe3bc86..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts +++ /dev/null @@ -1,91 +0,0 @@ -import healthy from "assets/status-healthy.png"; -import failure from "assets/failure.svg"; -import loading from "assets/loading.gif"; -import canceled from "assets/canceled.svg" -import api from "shared/api"; -import { PorterAppEvent } from "./types"; - -export const getDuration = (event: PorterAppEvent): string => { - const startTimeStamp = new Date(event.metadata.start_time ?? event.created_at).getTime(); - const endTimeStamp = new Date(event.metadata.end_time ?? event.updated_at).getTime(); - - const timeDifferenceMilliseconds = endTimeStamp - startTimeStamp; - - const seconds = Math.floor(timeDifferenceMilliseconds / 1000); - const weeks = Math.floor(seconds / 604800); - const remainingDays = Math.floor((seconds % 604800) / 86400); - const remainingHours = Math.floor((seconds % 86400) / 3600); - const remainingMinutes = Math.floor((seconds % 3600) / 60); - const remainingSeconds = seconds % 60; - - if (weeks > 0) { - return `${weeks}w ${remainingDays}d`; - } - - if (remainingDays > 0) { - return `${remainingDays}d ${remainingHours}h`; - } - - if (remainingHours > 0) { - return `${remainingHours}h ${remainingMinutes}m`; - } - - if (remainingMinutes > 0) { - return `${remainingMinutes}m ${remainingSeconds}s`; - } - - return `${remainingSeconds}s`; -}; - -export const getStatusIcon = (status: string) => { - switch (status) { - case "SUCCESS": - return healthy; - case "FAILED": - return failure; - case "PROGRESSING": - return loading; - case "CANCELED": - return canceled; - default: - return loading; - } -}; - -export const getStatusColor = (status: string) => { - switch (status) { - case "SUCCESS": - return "#68BF8B"; - case "FAILED": - return "#FF6060"; - case "PROGRESSING": - return "#6e9df5"; - case "CANCELED": - return "#FFBF00"; - default: - return "#6e9df5"; - } -}; - -export const triggerWorkflow = async (appData: any) => { - try { - const res = await api.reRunGHWorkflow( - "", - {}, - { - project_id: appData.app.project_id, - cluster_id: appData.app.cluster_id, - git_installation_id: appData.app.git_repo_id, - owner: appData.app.repo_name?.split("/")[0], - name: appData.app.repo_name?.split("/")[1], - branch: appData.app.branch_name, - filename: "porter_stack_" + appData.chart.name + ".yml", - } - ); - if (res.data != null) { - window.open(res.data, "_blank", "noreferrer"); - } - } catch (error) { - console.log(error); - } -}; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvGroupModal.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvGroupModal.tsx deleted file mode 100644 index 29cda2d534..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvGroupModal.tsx +++ /dev/null @@ -1,344 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import { isEmpty, isObject } from "lodash"; -import AceEditor from "react-ace"; -import { withRouter, type RouteComponentProps } from "react-router"; -import styled, { css } from "styled-components"; -import { set } from "zod"; - -import Loading from "components/Loading"; -import { - type NewPopulatedEnvGroup, - type PartialEnvGroup, - type PopulatedEnvGroup, -} from "components/porter-form/types"; -import Button from "components/porter/Button"; -import Checkbox from "components/porter/Checkbox"; -import Container from "components/porter/Container"; -import Error from "components/porter/Error"; -import Modal from "components/porter/Modal"; -import Spacer from "components/porter/Spacer"; -import Text from "components/porter/Text"; -import YamlEditor from "components/YamlEditor"; - -import api from "shared/api"; -import sliders from "assets/sliders.svg"; - -import { Context } from "../../../../../shared/Context"; -import { - EnvGroupData, - formattedEnvironmentValue, -} from "../../../cluster-dashboard/env-groups/EnvGroup"; -import { type KeyValueType } from "../../../cluster-dashboard/env-groups/EnvGroupArray"; -import { getGithubAction } from "./utils"; - -type Props = RouteComponentProps & { - closeModal: () => void; - availableEnvGroups?: PartialEnvGroup[]; - setValues: (x: KeyValueType[]) => void; - values: KeyValueType[]; - syncedEnvGroups: NewPopulatedEnvGroup[]; - setSyncedEnvGroups: (values: NewPopulatedEnvGroup[]) => void; - namespace: string; - newApp?: boolean; -}; - -const EnvGroupModal: React.FC = ({ - closeModal, - setValues, - availableEnvGroups, - syncedEnvGroups, - setSyncedEnvGroups, - values, - namespace, - newApp, -}) => { - const { currentCluster, currentProject } = useContext(Context); - const [envGroups, setEnvGroups] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [shouldSync, setShouldSync] = useState(true); - const [selectedEnvGroup, setSelectedEnvGroup] = - useState(null); - const [cloneSuccess, setCloneSuccess] = useState(false); - - const updateEnvGroups = async () => { - let populatedEnvGroups: any[] = []; - try { - populatedEnvGroups = await api - .getAllEnvGroups( - "", - {}, - { - id: currentProject?.id, - cluster_id: currentCluster?.id, - } - ) - .then((res) => res.data?.environment_groups); - } catch (error) { - setLoading(false); - setError(true); - return; - } - - try { - setEnvGroups(populatedEnvGroups); - setLoading(false); - } catch (error) { - setLoading(false); - setError(true); - } - }; - - useEffect(() => { - if (!values) { - setValues([]); - } - }, [values]); - - useEffect(() => { - setLoading(true); - if (Array.isArray(availableEnvGroups)) { - setEnvGroups(availableEnvGroups); - setLoading(false); - return; - } - updateEnvGroups(); - }, []); - - const renderEnvGroupList = () => { - if (loading) { - return ( - - - - ); - } else { - const sortedEnvGroups = envGroups - ?.slice() - .sort((a, b) => a.name.localeCompare(b.name)); - - return sortedEnvGroups - ?.filter((envGroup) => { - if (!Array.isArray(syncedEnvGroups)) { - return true; - } - return !syncedEnvGroups?.find( - (syncedEnvGroup) => syncedEnvGroup?.name === envGroup?.name - ); - }) - .map((envGroup: any, i: number) => { - return ( - { - setSelectedEnvGroup(envGroup); - }} - > - - {envGroup?.name} - - ); - }); - } - }; - - const onSubmit = () => { - if (shouldSync) { - syncedEnvGroups.push(selectedEnvGroup); - setSyncedEnvGroups(syncedEnvGroups); - } else { - const _values = [...values]; - - Object.entries(selectedEnvGroup?.variables || {}).map(([key, value]) => - _values.push({ - key, - value, - hidden: false, - locked: false, - deleted: false, - }) - ); - setValues(_values); - } - closeModal(); - }; - - return ( - - Load env group - - - - {syncedEnvGroups?.length != envGroups?.length ? ( - <> - - Select an Env Group to load into your application. - - - - - {renderEnvGroupList()} - - {selectedEnvGroup && ( - <> - - - {isObject(selectedEnvGroup?.variables) || - isObject(selectedEnvGroup?.secret_variables) ? ( - <> - {[ - ...Object.entries( - selectedEnvGroup?.variables || {} - ).map(([key, value]) => ({ - source: "variables", - key, - value, - })), - ...Object.entries( - selectedEnvGroup?.secret_variables || {} - ).map(([key, value]) => ({ - source: "secret_variables", - key, - value, - })), - ].map(({ key, value, source }, index) => ( -
- {key} = - - {formattedEnvironmentValue( - source === "secret_variables" - ? "****" - : value - )} - -
- ))} - - ) : ( - <>This environment group has no variables - )} -
-
- - )} -
- - - - - ) : loading ? ( - - - - ) : ( - No selectable Env Groups - )} -
-
- - - -
- ); -}; - -export default withRouter(EnvGroupModal); - -const LoadingWrapper = styled.div` - height: 150px; -`; -const Placeholder = styled.div` - width: 100%; - height: 150px; - display: flex; - align-items: center; - justify-content: center; - color: #aaaabb; - font-size: 13px; -`; - -const EnvGroupRow = styled.div<{ lastItem?: boolean; isSelected: boolean }>` - display: flex; - width: 100%; - font-size: 13px; - border-bottom: 1px solid - ${(props) => (props.lastItem ? "#00000000" : "#606166")}; - color: #ffffff; - user-select: none; - align-items: center; - padding: 10px 0px; - cursor: pointer; - background: ${(props) => (props.isSelected ? "#ffffff11" : "")}; - :hover { - background: #ffffff11; - } - - > img, - i { - width: 16px; - height: 18px; - margin-left: 12px; - margin-right: 12px; - font-size: 20px; - } -`; -const EnvGroupList = styled.div` - width: 100%; - border-radius: 3px; - background: #ffffff11; - border: 1px solid #ffffff44; - overflow-y: auto; -`; - -const SidebarSection = styled.section<{ $expanded?: boolean }>` - height: 100%; - overflow-y: auto; - ${(props) => - props.$expanded && - css` - grid-column: span 2; - `} -`; - -const GroupEnvPreview = styled.pre` - font-family: monospace; - margin: 0 0 10px 0; - white-space: pre-line; - word-break: break-word; - user-select: text; - .key { - color: white; - } - .value { - color: #3a48ca; - } -`; -const GroupModalSections = styled.div` - margin-top: 20px; - width: 100%; - height: 100%; - display: grid; - gap: 10px; - grid-template-columns: 1fr 1fr; - max-height: 365px; -`; -const ColumnContainer = styled.div` - display: flex; - flex-direction: column; - align-items: stretch; -`; - -const ScrollableContainer = styled.div` - flex: 1; - overflow-y: auto; - max-height: 300px; -`; - -const SubmitButtonContainer = styled.div` - margin-top: 10px; - text-align: right; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvVariablesTab.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvVariablesTab.tsx deleted file mode 100644 index 154730451e..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvVariablesTab.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import Button from "components/porter/Button"; -import Spacer from "components/porter/Spacer"; -import EnvGroupArrayStacks from "main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks"; -import React, { useContext, useEffect, useRef, useState } from "react"; -import styled, { keyframes } from "styled-components"; -import Text from "components/porter/Text"; -import Error from "components/porter/Error"; -import sliders from "assets/sliders.svg"; -import EnvGroupModal from "./EnvGroupModal"; -import ExpandableEnvGroup from "./ExpandableEnvGroup"; -import { - PopulatedEnvGroup, - PartialEnvGroup, - type NewPopulatedEnvGroup, -} from "../../../../../components/porter-form/types"; -import _, { isObject, differenceBy, omit } from "lodash"; -import api from "../../../../../shared/api"; -import { Context } from "../../../../../shared/Context"; -import yaml from "js-yaml"; - -type EnvVariablesTabProps = { - envVars: any; - setEnvVars: (x: any) => void; - status: React.ReactNode; - updatePorterApp: any; - syncedEnvGroups: NewPopulatedEnvGroup[]; - setSyncedEnvGroups: (values: NewPopulatedEnvGroup[]) => void; - clearStatus: () => void; - appData: any; - deletedEnvGroups: NewPopulatedEnvGroup[]; - setShowUnsavedChangesBanner: (x: boolean) => void; - setDeletedEnvGroups: (values: NewPopulatedEnvGroup[]) => void; -} - -export const EnvVariablesTab: React.FC = ({ - envVars, - setEnvVars, - setShowUnsavedChangesBanner, - status, - updatePorterApp, - syncedEnvGroups, - setSyncedEnvGroups, - deletedEnvGroups, - setDeletedEnvGroups, - clearStatus, - appData, -}) => { - const [hovered, setHovered] = useState(false); - - const [showEnvModal, setShowEnvModal] = useState(false); - const [envGroups, setEnvGroups] = useState([]); - const { currentCluster, currentProject } = useContext(Context); - - const [values, setValues] = React.useState( - yaml.dump(appData.chart.config) - ); - const initialMount = useRef(true); - - useEffect(() => { - if (initialMount.current) { - initialMount.current = false; - } else { - setShowUnsavedChangesBanner(true); - setEnvVars(envVars); - } - }, [envVars]); - useEffect(() => { - updateEnvGroups(); - }, []); - - const updateEnvGroups = async () => { - let populateEnvGroupsPromises: NewPopulatedEnvGroup[] = []; - try { - populateEnvGroupsPromises = await api - .getAllEnvGroups( - "", - {}, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => res?.data?.environment_groups); - } catch (error) { - return; - } - - try { - const populatedEnvGroups = await Promise.all(populateEnvGroupsPromises); - setEnvGroups(populatedEnvGroups); - const filteredEnvGroups = populatedEnvGroups?.filter( - (envGroup) => - envGroup.linked_applications && - envGroup.linked_applications.includes(appData.chart.name) - ); - setSyncedEnvGroups(filteredEnvGroups); - } catch (error) { - - } - }; - - const deleteEnvGroup = (envGroup: NewPopulatedEnvGroup) => { - setDeletedEnvGroups([...deletedEnvGroups, envGroup]); - setSyncedEnvGroups( - syncedEnvGroups?.filter((env) => env.name !== envGroup.name) - ); - }; - const maxEnvGroupsReached = syncedEnvGroups.length >= 4; - - return ( - <> - Environment variables - - Shared among all services. - { - if (status !== "") { - clearStatus(); - } - setEnvVars(x); - }} - fileUpload={true} - syncedEnvGroups={syncedEnvGroups} - /> - - <> - { setHovered(true); }} - onMouseOut={() => { setHovered(false); }} - > - { !maxEnvGroupsReached && setShowEnvModal(true); }} - > - Load from Env Group - - - Max 4 Env Groups allowed - - - - {showEnvModal && ( - { - if (status !== "") { - clearStatus(); - } - setEnvVars(x); - }} - values={envVars} - closeModal={() => { setShowEnvModal(false); }} - syncedEnvGroups={syncedEnvGroups} - setSyncedEnvGroups={setSyncedEnvGroups} - namespace={appData.chart.namespace} - /> - )} - {!!syncedEnvGroups?.length && ( - <> - - Synced environment groups - {syncedEnvGroups?.map((envGroup: any) => { - return ( - { - deleteEnvGroup(envGroup); - }} - /> - ); - })} - - )} - - - - - - - ); -}; - -const AddRowButton = styled.div` - display: flex; - align-items: center; - width: 270px; - font-size: 13px; - color: #aaaabb; - height: 32px; - border-radius: 3px; - cursor: pointer; - background: #ffffff11; - :hover { - background: #ffffff22; - } - - > i { - color: #ffffff44; - font-size: 16px; - margin-left: 8px; - margin-right: 10px; - display: flex; - align-items: center; - justify-content: center; - } -`; - -const LoadButton = styled(AddRowButton)<{ disabled?: boolean }>` - background: ${(props) => (props.disabled ? "#aaaaaa55" : "none")}; - border: 1px solid ${(props) => (props.disabled ? "#aaaaaa55" : "#ffffff55")}; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; - - > i { - color: ${(props) => (props.disabled ? "#aaaaaa44" : "#ffffff44")}; - font-size: 16px; - margin-left: 8px; - margin-right: 10px; - display: flex; - align-items: center; - justify-content: center; - } - > img { - width: 14px; - margin-left: 10px; - margin-right: 12px; - opacity: ${(props) => (props.disabled ? "0.5" : "1")}; - } -`; - -type InputProps = { - disabled?: boolean; - width: string; - borderColor?: string; -}; - -const KeyInput = styled.input` - outline: none; - border: none; - margin-bottom: 5px; - font-size: 13px; - background: #ffffff11; - border: 1px solid - ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")}; - border-radius: 3px; - width: ${(props) => (props.width ? props.width : "270px")}; - color: ${(props) => (props.disabled ? "#ffffff44" : "white")}; - padding: 5px 10px; - height: 35px; -`; - -export const MultiLineInput = styled.textarea` - outline: none; - border: none; - margin-bottom: 5px; - font-size: 13px; - background: #ffffff11; - border: 1px solid - ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")}; - border-radius: 3px; - min-width: ${(props) => (props.width ? props.width : "270px")}; - max-width: ${(props) => (props.width ? props.width : "270px")}; - color: ${(props) => (props.disabled ? "#ffffff44" : "white")}; - padding: 8px 10px 5px 10px; - min-height: 35px; - max-height: 100px; - white-space: nowrap; - - ::-webkit-scrollbar { - width: 8px; - :horizontal { - height: 8px; - } - } - - ::-webkit-scrollbar-corner { - width: 10px; - background: #ffffff11; - color: white; - } - - ::-webkit-scrollbar-track { - width: 10px; - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - } - - ::-webkit-scrollbar-thumb { - background-color: darkgrey; - outline: 1px solid slategrey; - } -`; - -const fadeIn = keyframes` - from { - opacity: 0; - } - to { - opacity: 1; - } -`; - -const TooltipWrapper = styled.div` - position: relative; - display: inline-block; -`; - -const TooltipText = styled.span<{ visible: boolean }>` - visibility: ${(props) => (props.visible ? "visible" : "hidden")}; - width: 240px; - color: #fff; - text-align: center; - padding: 5px 0; - border-radius: 6px; - position: absolute; - z-index: 1; - bottom: 100%; - left: 50%; - margin-left: -120px; - opacity: ${(props) => (props.visible ? "1" : "0")}; - transition: opacity 0.3s; - font-size: 12px; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/env-vars/ExpandableEnvGroup.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/env-vars/ExpandableEnvGroup.tsx deleted file mode 100644 index 069f793c6b..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/env-vars/ExpandableEnvGroup.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import Button from "components/porter/Button"; -import Spacer from "components/porter/Spacer"; -import React, { useEffect, useState } from "react"; -import styled, { keyframes } from "styled-components"; -import { type PopulatedEnvGroup } from "components/porter-form/types"; -import _, { isObject, differenceBy, omit } from "lodash"; - - -const ExpandableEnvGroup: React.FC<{ - envGroup: PopulatedEnvGroup; - onDelete: () => void; -}> = ({ envGroup, onDelete }) => { - const [isExpanded, setIsExpanded] = useState(false); - return ( - <> - - - - - {envGroup.name} - - - - { onDelete(); }}> - delete - - { setIsExpanded((prev) => !prev); }}> - - {isExpanded ? "arrow_drop_up" : "arrow_drop_down"} - - - - - {isExpanded && ( - <> - {isObject(envGroup.variables) || isObject(envGroup.secret_variables) ? ( - <> - {[ - ...Object.entries(envGroup?.variables || {}).map(([key, value]) => ({ - key, - value, - source: 'variables', - })), - ...Object.entries(envGroup?.secret_variables || {}).map(([key, value]) => ({ - key, - value, - source: 'secret_variables', - })), - ].map(({ key, value, source }, i: number) => { - // Preprocess non-string env values set via raw Helm values - if (typeof value === "object") { - value = JSON.stringify(value); - } else { - value = String(value); - } - - return ( - - - - {source === 'secret_variables' ? ( - - ) : ( - - )} - - ); - })} - - ) : ( - - This env group has no variables yet - - )} - - )} - - - ); -}; - -export default ExpandableEnvGroup; -const InputWrapper = styled.div` - display: flex; - align-items: center; - margin-top: 5px; -`; - -type InputProps = { - disabled?: boolean; - width: string; - borderColor?: string; -}; - -const KeyInput = styled.input` - outline: none; - border: none; - margin-bottom: 5px; - font-size: 13px; - background: #ffffff11; - border: 1px solid - ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")}; - border-radius: 3px; - width: ${(props) => (props.width ? props.width : "270px")}; - color: ${(props) => (props.disabled ? "#ffffff44" : "white")}; - padding: 5px 10px; - height: 35px; -`; - -export const MultiLineInput = styled.textarea` - outline: none; - border: none; - margin-bottom: 5px; - font-size: 13px; - background: #ffffff11; - border: 1px solid - ${(props) => (props.borderColor ? props.borderColor : "#ffffff55")}; - border-radius: 3px; - min-width: ${(props) => (props.width ? props.width : "270px")}; - max-width: ${(props) => (props.width ? props.width : "270px")}; - color: ${(props) => (props.disabled ? "#ffffff44" : "white")}; - padding: 8px 10px 5px 10px; - min-height: 35px; - max-height: 100px; - white-space: nowrap; - - ::-webkit-scrollbar { - width: 8px; - :horizontal { - height: 8px; - } - } - - ::-webkit-scrollbar-corner { - width: 10px; - background: #ffffff11; - color: white; - } - - ::-webkit-scrollbar-track { - width: 10px; - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - } - - ::-webkit-scrollbar-thumb { - background-color: darkgrey; - outline: 1px solid slategrey; - } -`; - -const Label = styled.div` - color: #ffffff; - margin-bottom: 10px; -`; - -const StyledInputArray = styled.div` - margin-bottom: 15px; - margin-top: 22px; -`; - -const fadeIn = keyframes` - from { - opacity: 0; - } - to { - opacity: 1; - } -`; - -const StyledCard = styled.div` - border: 1px solid #ffffff44; - background: #ffffff11; - margin-bottom: 5px; - border-radius: 8px; - margin-top: 15px; - padding: 10px 14px; - overflow: hidden; - font-size: 13px; - animation: ${fadeIn} 0.5s; -`; - -const Flex = styled.div` - display: flex; - height: 25px; - align-items: center; - justify-content: space-between; -`; - -const ContentContainer = styled.div` - display: flex; - height: 40px; - width: 100%; - align-items: center; -`; - -const EventInformation = styled.div` - display: flex; - flex-direction: column; - justify-content: space-around; - height: 100%; -`; - -const EventName = styled.div` - font-family: "Work Sans", sans-serif; - font-weight: 500; - color: #ffffff; -`; - -const ActionContainer = styled.div` - display: flex; - align-items: center; - white-space: nowrap; - height: 100%; -`; - -const ActionButton = styled.button` - position: relative; - border: none; - background: none; - color: white; - padding: 5px; - width: 30px; - height: 30px; - margin-left: 5px; - display: flex; - justify-content: center; - align-items: center; - border-radius: 50%; - cursor: pointer; - color: #aaaabb; - border: 1px solid #ffffff00; - - :hover { - background: #ffffff11; - border: 1px solid #ffffff44; - } - - > span { - font-size: 20px; - } -`; - -const NoVariablesTextWrapper = styled.div` - display: flex; - align-items: center; - justify-content: center; - color: #ffffff99; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJob.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJob.tsx deleted file mode 100644 index e112380ae3..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJob.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import React, { useEffect, useState, useContext, useCallback } from "react"; -import { RouteComponentProps, useLocation, withRouter } from "react-router"; -import styled from "styled-components"; - -import history from "assets/history.png"; -import loadingImg from "assets/loading.gif"; -import refresh from "assets/refresh.png"; - -import api from "shared/api"; -import { Context } from "shared/Context"; -import Error from "components/porter/Error"; - -import Banner from "components/porter/Banner"; -import Loading from "components/Loading"; -import Text from "components/porter/Text"; -import Container from "components/porter/Container"; -import Spacer from "components/porter/Spacer"; -import Link from "components/porter/Link"; -import Back from "components/porter/Back"; -import TabSelector from "components/TabSelector"; -import RevisionSection from "main/home/cluster-dashboard/expanded-chart/RevisionSection"; -import ConfirmOverlay from "components/porter/ConfirmOverlay"; -import Fieldset from "components/porter/Fieldset"; -import JobRuns from "../JobRuns"; -import ExpandedJobRun from "./ExpandedJobRun"; - -type Props = RouteComponentProps & { - appName: string; - jobName: string; - goBack: () => void; -}; - -const ExpandedJob: React.FC = ({ - appName, - jobName, - goBack, - ...props -}) => { - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [expandedRun, setExpandedRun] = useState(null); - - return ( - <> - {isLoading && } - {!isLoading && expandedRun && ( - setExpandedRun(null)} - /> - )} - {!isLoading && !expandedRun && ( - - - - Run history for "{jobName}" - - - - This job runs under the "{appName}" app. - - - {currentCluster?.id && currentProject?.id && ( - setExpandedRun(x)} - /> - )} - - )} - - ); -}; - -export default withRouter(ExpandedJob); - -const RefreshButton = styled.div` - color: #ffffff44; - display: flex; - align-items: center; - cursor: pointer; - :hover { - color: #ffffff; - > img { - opacity: 1; - } - } - - > img { - display: flex; - align-items: center; - justify-content: center; - height: 11px; - margin-right: 10px; - opacity: 0.3; - } -`; - -const Spinner = styled.img` - width: 15px; - height: 15px; - margin-right: 12px; - margin-bottom: -2px; -`; - -const DarkMatter = styled.div<{ antiHeight?: string }>` - width: 100%; - margin-top: ${(props) => props.antiHeight || "-20px"}; -`; - -const TagWrapper = styled.div` - height: 20px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - color: #ffffff44; - border: 1px solid #ffffff44; - border-radius: 3px; - padding-left: 6px; -`; - -const BranchTag = styled.div` - height: 20px; - margin-left: 6px; - color: #aaaabb; - background: #ffffff22; - border-radius: 3px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - padding: 0px 6px; - padding-left: 7px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - -const BranchSection = styled.div` - background: ${(props) => props.theme.fg}; - border: 1px solid #494b4f; -`; - -const SmallIcon = styled.img<{ opacity?: string; height?: string }>` - height: ${(props) => props.height || "15px"}; - opacity: ${(props) => props.opacity || 1}; - margin-right: 10px; -`; - -const BranchIcon = styled.img` - height: 14px; - opacity: 0.65; - margin-right: 5px; -`; - -const Icon = styled.img` - height: 24px; - margin-right: 15px; -`; - -const PlaceholderIcon = styled.img` - height: 13px; - margin-right: 12px; - opacity: 0.65; -`; - -const Placeholder = styled.div` - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - font-size: 13px; -`; - -const StyledExpandedApp = styled.div` - width: 100%; - height: 100%; - - animation: fadeIn 0.5s 0s; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const HeaderWrapper = styled.div` - position: relative; -`; -const LastDeployed = styled.div` - font-size: 13px; - margin-left: 8px; - margin-top: -1px; - display: flex; - align-items: center; - color: #aaaabb66; -`; -const Dot = styled.div` - margin-right: 16px; -`; -const InfoWrapper = styled.div` - display: flex; - align-items: center; - margin-left: 3px; - margin-top: 22px; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJobRun.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJobRun.tsx deleted file mode 100644 index 5bd68f0815..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/expanded-job/ExpandedJobRun.tsx +++ /dev/null @@ -1,559 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import { get, isEmpty } from "lodash"; -import styled from "styled-components"; - -import job from "assets/job.png"; -import leftArrow from "assets/left-arrow.svg"; -import KeyValueArray from "components/form-components/KeyValueArray"; -import Loading from "components/Loading"; -import TabRegion, { TabOption } from "components/TabRegion"; -import TitleSection from "components/TitleSection"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { ChartType } from "shared/types"; -import DeploymentType from "main/home/cluster-dashboard/expanded-chart/DeploymentType"; -import Logs from "../status/Logs"; -import { useRouting } from "shared/routing"; -import LogsSection, { InitLogData } from "main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection"; -import EventsTab from "main/home/cluster-dashboard/expanded-chart/events/EventsTab"; -import { getPodStatus } from "main/home/cluster-dashboard/expanded-chart/deploy-status-section/util"; -import { capitalize } from "shared/string_utils"; -import { usePods } from "shared/hooks/usePods"; -import Container from "components/porter/Container"; -import Text from "components/porter/Text"; -import Spacer from "components/porter/Spacer"; - -const readableDate = (s: string) => { - let ts = new Date(s); - let date = ts.toLocaleDateString(); - let time = ts.toLocaleTimeString([], { - hour: "numeric", - minute: "2-digit", - }); - return `${time} on ${date}`; -}; - -const getLatestPod = (pods: any[]) => { - if (!Array.isArray(pods)) { - return undefined; - } - - return [...pods] - .sort((a: any, b: any) => { - if (!a?.metadata?.creationTimestamp) { - return 1; - } - - if (!b?.metadata?.creationTimestamp) { - return -1; - } - - return ( - new Date(b?.metadata?.creationTimestamp).getTime() - - new Date(a?.metadata?.creationTimestamp).getTime() - ); - }) - .shift(); -}; - -export const isRunning = (deleting: boolean, job: any, pod: any) => { - if (deleting) { - return false; - } - - if (job.status?.succeeded >= 1) { - return false; - } - - if (job.status?.conditions) { - if (job.status?.conditions[0]?.reason == "DeadlineExceeded") { - return false; - } - } - - if (job.status?.failed >= 1) { - return false; - } - - if (job.status?.active >= 1) { - // determine the status from the pod - return pod ? pod.status.startTime : false; - } - - return true; -}; - -export const renderStatus = ( - deleting: boolean, - job: any, - pod: any, - time?: string -) => { - if (deleting) { - return Deleting; - } - - if (job.status?.succeeded >= 1) { - if (time) { - return Succeeded at {time}; - } - - return Succeeded; - } - - if (job.status?.conditions) { - if (job.status?.conditions[0]?.reason == "DeadlineExceeded") { - return Timed Out; - } - } - - if (job.status?.failed >= 1) { - return Failed; - } - - if (job.status?.active >= 1) { - // determine the status from the pod - return pod ? ( - {capitalize(getPodStatus(pod?.status))} - ) : ( - Running - ); - } - - return Running; -}; - -type ExpandedJobRunTabs = "events" | "logs" | "config" | string; - -const ExpandedJobRun = ({ - currentChart, - jobRun, - onClose, -}: { - currentChart: ChartType; - jobRun: any; - onClose: () => void; -}) => { - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - const [currentTab, setCurrentTab] = useState( - currentCluster.agent_integration_enabled ? "events" : "logs" - ); - const { pushQueryParams } = useRouting(); - const [useDeprecatedLogs, setUseDeprecatedLogs] = useState(false); - - const [pods, isLoading] = usePods({ - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: jobRun.metadata?.namespace, - selectors: [`job-name=${jobRun.metadata?.name}`], - controller_kind: "job", - controller_name: jobRun.metadata?.name, - subscribed: true, - }); - - let chart = currentChart; - let run = jobRun; - - useEffect(() => { - return () => { - pushQueryParams({}, ["job"]); - }; - }, []); - - const renderConfigSection = (job: any) => { - let commandString = job?.spec?.template?.spec?.containers[0]?.command?.join( - " " - ); - let envArray = job?.spec?.template?.spec?.containers[0]?.env; - let envObject = {} as any; - envArray && - envArray.forEach((env: any, i: number) => { - const secretName = get(env, "valueFrom.secretKeyRef.name"); - envObject[env.name] = secretName - ? `PORTERSECRET_${secretName}` - : env.value; - }); - - // Handle no config to show - if (!commandString && isEmpty(envObject)) { - return No config was found.; - } - - let tag = job.spec.template.spec.containers[0].image.split(":")[1]; - return ( - - {commandString ? ( - <> - Command: {commandString} - - ) : ( - - )} - - Image Tag: {tag} - - {!isEmpty(envObject) && ( - <> - - - - )} - - ); - }; - - const renderEventsSection = () => { - return ( - setCurrentTab("logs")} - /> - ); - }; - - const renderLogsSection = () => { - if (useDeprecatedLogs || !currentCluster.agent_integration_enabled) { - return ( - - - - ); - } - - let initData: InitLogData = {}; - - if (run.status.completionTime) { - initData.timestamp = run.status.completionTime; - } - - return ( - - - Not seeing your logs? Switch back to{" "} - { - setUseDeprecatedLogs(true); - }} - > - {" "} - deprecated logging. - - - { }} - overridingPodSelector={pods[0]?.metadata?.name || jobRun.metadata?.name} - currentChart={currentChart} - initData={initData} - /> - - ); - }; - - if (isLoading) { - return ; - } - - let options: TabOption[] = []; - - if (currentCluster.agent_integration_enabled) { - options.push({ - label: "Events", - value: "events", - }); - } - - options.push( - { - label: "Logs", - value: "logs", - }, - { - label: "Config", - value: "config", - } - ); - - return ( - - - - - Back - - - - - - - {jobRun.metadata?.name.split('-').slice(1, -2).join('-')} - - - - at {run.status.completionTime ? readableDate(run.status.completionTime) : ""} - - - - - - {renderStatus( - false, - run, - pods[0], - run.status.completionTime - ? readableDate(run.status.completionTime) - : "" - )} - - - - - - { - setCurrentTab(newTab); - }} - options={options} - > - {currentTab === "events" && renderEventsSection()} - {currentTab === "logs" && renderLogsSection()} - {currentTab === "config" && <>{renderConfigSection(run)}} - - - - ); -}; - -export default ExpandedJobRun; - -const Icon = styled.img` - height: 24px; - margin-right: 15px; -`; - -const ArrowIcon = styled.img` - width: 15px; - margin-right: 8px; - opacity: 50%; -`; - -const BreadcrumbRow = styled.div` - width: 100%; - display: flex; - justify-content: flex-start; -`; - -const Breadcrumb = styled.div` - color: #aaaabb88; - font-size: 13px; - margin-bottom: 15px; - display: flex; - align-items: center; - margin-top: -10px; - z-index: 999; - padding: 5px; - padding-right: 7px; - border-radius: 5px; - cursor: pointer; - :hover { - background: #ffffff11; - } -`; - -const Wrap = styled.div` - z-index: 999; -`; - -const Row = styled.div` - margin-top: 20px; -`; - -const DarkMatter = styled.div<{ size?: string }>` - width: 100%; - margin-bottom: ${(props) => props.size || "-13px"}; -`; - -const Command = styled.span` - font-family: monospace; - color: #aaaabb; - margin-left: 7px; -`; - -const ConfigSection = styled.div` - padding: 20px 30px 30px; - font-size: 13px; - font-weight: 500; - width: 100%; - border-radius: 8px; - background: #ffffff08; -`; - -const JobLogsWrapper = styled.div` - min-height: 450px; - height: fit-content; - width: 100%; - border-radius: 8px; -`; - -const Status = styled.div<{ color: string }>` - padding: 5px 10px; - background: ${(props) => props.color}; - font-size: 13px; - border-radius: 3px; - height: 25px; - color: #ffffff; - margin-bottom: -3px; - display: flex; - align-items: center; - justify-content: center; -`; - -const Gray = styled.div` - color: #ffffff44; - margin-left: 15px; - font-weight: 400; - font-size: 18px; -`; - -const BackButton = styled.div` - position: absolute; - top: 0px; - right: 0px; - display: flex; - width: 36px; - cursor: pointer; - height: 36px; - align-items: center; - justify-content: center; - border: 1px solid #ffffff55; - border-radius: 100px; - background: #ffffff11; - - :hover { - background: #ffffff22; - > img { - opacity: 1; - } - } -`; - -const BackButtonImg = styled.img` - width: 16px; - opacity: 0.75; -`; - -const Placeholder = styled.div` - min-height: 400px; - height: 50vh; - padding: 30px; - padding-bottom: 70px; - font-size: 13px; - color: #ffffff44; - width: 100%; - display: flex; - align-items: center; - justify-content: center; -`; - -const BodyWrapper = styled.div` - position: relative; - overflow: hidden; -`; - -const HeaderWrapper = styled.div` - position: relative; -`; - -const InfoWrapper = styled.div` - display: flex; - align-items: center; - height: 20px; -`; - -const LastDeployed = styled.div` - font-size: 13px; - margin-left: 0; - display: flex; - align-items: center; - color: #aaaabb66; -`; - -const TagWrapper = styled.div` - height: 25px; - font-size: 12px; - display: flex; - margin-left: 20px; - margin-bottom: -3px; - align-items: center; - font-weight: 400; - justify-content: center; - color: #ffffff44; - border: 1px solid #ffffff44; - border-radius: 3px; - padding-left: 5px; - background: #26282e; -`; - -const NamespaceTag = styled.div` - height: 100%; - margin-left: 6px; - color: #aaaabb; - background: #43454a; - border-radius: 3px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - padding: 0px 6px; - padding-left: 7px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; -`; - -const StyledExpandedChart = styled.div` - width: 100%; - z-index: 0; - animation: fadeIn 0.3s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - display: flex; - overflow-y: auto; - padding-bottom: 120px; - flex-direction: column; - overflow: visible; - - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const DeprecatedWarning = styled.div` - font-size: 12px; - color: #ccc; - text-align: right; - width: 100%; - margin-bottom: 20px; -`; - -const DeprecatedSelect = styled.span` - cursor: pointer; - color: #949effff; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterComponent.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterComponent.tsx deleted file mode 100644 index d0a07bba4e..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/logs/LogFilterComponent.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import Text from "components/porter/Text"; -import React from "react"; -import styled from "styled-components"; -import { GenericFilter } from "./types"; -import Spacer from "components/porter/Spacer"; -import Select from "components/porter/Select"; - -type Props = { - filter: GenericFilter; - selectedValue: string; -}; - -const LogFilterComponent: React.FC = ({ - filter, - selectedValue, -}) => { - return ( - - {filter.displayName} - - {}} - /> - Scroll to bottom - - {Array.isArray(previousLogs) && previousLogs.length > 0 && ( - { - setShowPreviousLogs(!showPreviousLogs); - }} - > - {}} - /> - Show previous logs - - )} - refresh()}> - autorenew - Refresh - - - - ); - - if (!containers?.length) { - return null; - } - - if (rawText) { - return {renderContent()}; - } - - return {renderContent()}; -}; - -export default LogsFC; - -const Highlight = styled.div` - display: flex; - align-items: center; - justify-content: center; - margin-left: 8px; - color: #8590ff; - cursor: pointer; - - > i { - font-size: 16px; - margin-right: 3px; - } -`; - -const Scroll = styled.div` - align-items: center; - display: flex; - cursor: pointer; - width: max-content; - height: 100%; - - :hover { - background: #2468d6; - } - - > input { - width: 18px; - margin-left: 10px; - margin-right: 6px; - pointer-events: none; - } -`; - -const Tab = styled.div` - background: ${(props: { clicked: boolean }) => - props.clicked ? "#503559" : "#7c548a"}; - padding: 0px 10px; - margin: 0px 7px 0px 0px; - align-items: center; - display: flex; - cursor: pointer; - height: 100%; - border-radius: 8px 8px 0px 0px; - - :hover { - background: #503559; - } -`; - -const Refresh = styled.div` - display: flex; - align-items: center; - width: 87px; - user-select: none; - cursor: pointer; - height: 100%; - - > i { - margin-left: 6px; - font-size: 17px; - margin-right: 6px; - } - - :hover { - background: #2468d6; - } -`; - -const LogTabs = styled.div` - width: 100%; - height: 25px; - margin-top: -25px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; -`; - -const Options = styled.div` - width: 100%; - height: 25px; - background: #397ae3; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; -`; - -const Wrapper = styled.div` - width: 100%; - height: 100%; - overflow: auto; - padding: 25px 30px; -`; - -const LogStream = styled.div` - display: flex; - flex-direction: column; - flex: 1; - float: right; - height: 100%; - font-size: 13px; - background: #000000; - user-select: text; - max-width: 65%; - overflow-y: auto; - overflow-wrap: break-word; -`; - -const LogStreamAlt = styled(LogStream)` - width: 100%; - max-width: 100%; -`; - -const Message = styled.div` - display: flex; - height: 100%; - width: calc(100% - 150px); - align-items: center; - justify-content: center; - margin-left: 75px; - text-align: center; - color: #ffffff44; - font-size: 13px; -`; - -const Log = styled.div` - font-family: monospace; -`; - -const LogSpan = styled.span` - font-family: monospace, sans-serif; - font-size: 12px; - font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) => - props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"}; - color: ${(props: { ansi: Anser.AnserJsonEntry }) => - props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"}; - background-color: ${(props: { ansi: Anser.AnserJsonEntry }) => - props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"}; -`; - -const CLIModalIconWrapper = styled.div` - max-width: 200px; - height: 35px; - margin: 10px; - font-size: 13px; - font-weight: 500; - font-family: "Work Sans", sans-serif; - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 20px 6px 10px; - text-align: left; - border: 1px solid #ffffff55; - border-radius: 8px; - background: #ffffff11; - color: #ffffffdd; - cursor: pointer; - :hover { - cursor: pointer; - background: #ffffff22; - > path { - fill: #ffffff77; - } - } - - > path { - fill: #ffffff99; - } -`; - -const CLIModalIcon = styled(CommandLineIcon)` - width: 32px; - height: 32px; - padding: 8px; - - > path { - fill: #ffffff99; - } -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx deleted file mode 100644 index 2f5cb2a364..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/status/LogsModal.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useEffect, useRef } from "react"; - -import Modal from "components/porter/Modal"; -import Text from "components/porter/Text"; -import TitleSection from "components/TitleSection"; - -import danger from "assets/danger.svg"; - -import { type PorterLog } from "../logs/types"; -import ExpandedIncidentLogs from "./ExpandedIncidentLogs"; - -type LogsModalProps = { - logs: PorterLog[]; - setModalVisible: (x: boolean) => void; - logsName: string; -}; -const LogsModal: React.FC = ({ - logs, - logsName, - setModalVisible, -}) => { - const scrollToBottomRef = useRef(null); - const scrollToBottom = () => { - if (scrollToBottomRef.current) { - scrollToBottomRef.current.scrollIntoView({ - behavior: "smooth", - block: "end", - }); - } - }; - useEffect(() => { - scrollToBottom(); - }, [scrollToBottomRef]); - - return ( - { - setModalVisible(false); - }} - width={"800px"} - > - - Logs for {logsName} - - - - ); -}; - -export default LogsModal; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/status/PodRow.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/status/PodRow.tsx deleted file mode 100644 index d2a20abe51..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/status/PodRow.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import React, { useState } from "react"; -import styled from "styled-components"; -import { ControllerTabPodType } from "./ControllerTab"; - -type PodRowProps = { - pod: ControllerTabPodType; - isSelected: boolean; - isLastItem: boolean; - onTabClick: any; - onDeleteClick: any; - podStatus: string; -}; - -const PodRow: React.FunctionComponent = ({ - pod, - isSelected, - onTabClick, - onDeleteClick, - isLastItem, - podStatus, -}) => { - const [showTooltip, setShowTooltip] = useState(false); - - return ( - - - - - - - { - setShowTooltip(true); - }} - onMouseOut={() => { - setShowTooltip(false); - }} - > - {pod?.name} - - {showTooltip && ( - - {pod?.name} - Restart count: {pod.restartCount} - Created on: {pod.podAge} - {podStatus === "failed" ? ( - - - Failure Reason: {pod?.containerStatus?.state?.waiting?.reason} - - {pod?.containerStatus?.state?.waiting?.message} - - ) : null} - - )} - - - - {podStatus} - {podStatus === "failed" && ( - - close - - )} - - - ); -}; - -export default PodRow; - -const InfoIcon = styled.div` - width: 22px; -`; - -const Grey = styled.div` - margin-top: 5px; - color: #aaaabb; -`; - -const FailedStatusContainer = styled.div` - width: 100%; - border: 1px solid hsl(0deg, 100%, 30%); - padding: 5px; - margin-block: 5px; -`; - -const Tooltip = styled.div` - position: absolute; - left: 35px; - word-wrap: break-word; - top: 38px; - min-height: 18px; - max-width: calc(100% - 75px); - padding: 5px 7px; - background: #272731; - z-index: 999; - display: flex; - flex-direction: column; - justify-content: center; - flex: 1; - color: white; - text-transform: none; - font-size: 12px; - font-family: "Work Sans", sans-serif; - outline: 1px solid #ffffff55; - opacity: 0; - animation: faded-in 0.2s 0.15s; - animation-fill-mode: forwards; - @keyframes faded-in { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const CloseIcon = styled.i` - font-size: 14px; - display: flex; - font-weight: bold; - align-items: center; - justify-content: center; - border-radius: 5px; - background: #ffffff22; - width: 18px; - height: 18px; - margin-right: -6px; - margin-left: 10px; - cursor: pointer; - :hover { - background: #ffffff44; - } -`; - -const Tab = styled.div` - width: 100%; - height: 50px; - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - color: ${(props: { selected: boolean }) => - props.selected ? "white" : "#ffffff66"}; - background: ${(props: { selected: boolean }) => - props.selected ? "#ffffff18" : ""}; - font-size: 13px; - padding: 20px 19px 20px 42px; - text-shadow: 0px 0px 8px none; - overflow: visible; - cursor: pointer; - :hover { - color: white; - background: #ffffff18; - } -`; - -const Rail = styled.div` - width: 2px; - background: ${(props: { lastTab?: boolean }) => - props.lastTab ? "" : "#52545D"}; - height: 50%; -`; - -const Circle = styled.div` - min-width: 10px; - min-height: 2px; - margin-bottom: -2px; - margin-left: 8px; - background: #52545d; -`; - -const Gutter = styled.div` - position: absolute; - top: 0px; - left: 10px; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - overflow: visible; -`; - -const Status = styled.div` - display: flex; - font-size: 12px; - text-transform: capitalize; - margin-left: 5px; - justify-content: flex-end; - align-items: center; - font-family: "Work Sans", sans-serif; - color: #aaaabb; - animation: fadeIn 0.5s; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const StatusColor = styled.div` - margin-right: 7px; - width: 7px; - min-width: 7px; - height: 7px; - background: ${(props: { status: string }) => - props.status === "running" - ? "#4797ff" - : props.status === "failed" - ? "#ed5f85" - : props.status === "completed" - ? "#00d12a" - : "#f5cb42"}; - border-radius: 20px; -`; - -const Name = styled.div` - overflow: hidden; - text-overflow: ellipsis; - line-height: 1.5em; - display: -webkit-box; - overflow-wrap: anywhere; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/status/StatusSection.tsx b/dashboard/src/main/home/app-dashboard/expanded-app/status/StatusSection.tsx deleted file mode 100644 index 8578636927..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/status/StatusSection.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import styled from "styled-components"; - -import api from "shared/api"; -import { Context } from "shared/Context"; -import { ChartType } from "shared/types"; -import Loading from "components/Loading"; - -import Logs from "./Logs"; -import ControllerTab from "./ControllerTab"; -import Banner from "components/porter/Banner"; -import Spacer from "components/porter/Spacer"; - -type Props = { - selectors?: string[]; - currentChart: ChartType; - fullscreen?: boolean; - setFullScreenLogs?: any; -}; - -const StatusSectionFC: React.FunctionComponent = ({ - currentChart, - fullscreen, - setFullScreenLogs, - selectors, -}) => { - const [selectedPod, setSelectedPod] = useState({}); - const [controllers, setControllers] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [podError, setPodError] = useState(""); - - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - - useEffect(() => { - let isSubscribed = true; - api - .getChartControllers( - "", - {}, - { - namespace: currentChart.namespace, - cluster_id: currentCluster.id, - id: currentProject.id, - name: currentChart.name, - revision: currentChart.version, - } - ) - .then((res: any) => { - if (!isSubscribed) { - return; - } - let controllers = - currentChart.chart.metadata.name == "job" - ? res.data[0]?.status.active - : res.data; - setControllers(controllers); - setIsLoading(false); - }) - .catch((err) => { - if (!isSubscribed) { - return; - } - setCurrentError(JSON.stringify(err)); - setControllers([]); - setIsLoading(false); - }); - return () => { - isSubscribed = false; - }; - }, [currentProject, currentCluster, setCurrentError, currentChart]); - - const renderLogs = () => { - return ( - - ); - }; - - const renderTabs = () => { - return controllers.map((c, i) => { - return ( - setPodError(x)} - /> - ); - }); - }; - - const renderStatusSection = () => { - if (isLoading) { - return ( - - - - ); - } - if (controllers?.length > 0) { - return ( - - {renderTabs()} - {renderLogs()} - - ); - } - - if (currentChart?.chart?.metadata?.name === "job") { - return ( - - category - There are no jobs currently running. - - ); - } - - return ( - - category - No objects to display. This might happen while your app is still - deploying. - - ); - }; - - return ( - <> - - {renderStatusSection()} - - - ); -}; - -export default StatusSectionFC; - -const MyLink = styled.a` - cursor: pointer; - color: #ffffff; - text-decoration: underline; -`; - -const FullScreenButton = styled.div<{ top?: string }>` - position: absolute; - top: ${(props) => props.top || "10px"}; - right: 10px; - width: 24px; - height: 24px; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - border-radius: 5px; - background: #ffffff11; - border: 1px solid #aaaabb; - - :hover { - background: #ffffff22; - } - - > i { - font-size: 14px; - } -`; - -const BackButton = styled.div` - display: flex; - width: 30px; - z-index: 999; - cursor: pointer; - height: 30px; - align-items: center; - margin-right: 15px; - justify-content: center; - cursor: pointer; - border: 1px solid #ffffff55; - border-radius: 100px; - background: #ffffff11; - - > i { - font-size: 18px; - } - - :hover { - background: #ffffff22; - > img { - opacity: 1; - } - } -`; - -const AbsoluteTitle = styled.div` - position: absolute; - top: 0px; - left: 0px; - width: 100%; - height: 60px; - display: flex; - align-items: center; - padding-left: 20px; - font-size: 18px; - font-weight: 500; - user-select: text; -`; - -const TabWrapper = styled.div` - width: 35%; - min-width: 250px; - height: 100%; - overflow-y: auto; -`; - -const StyledStatusSection = styled.div` - padding: 0px; - user-select: text; - overflow: hidden; - width: 100%; - min-height: 400px; - height: calc(100vh - 400px); - font-size: 13px; - overflow: hidden; - border-radius: 8px; - animation: floatIn 0.3s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - @keyframes floatIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0px); - } - } -`; - -const FullScreen = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - padding-top: 60px; -`; - -const Wrapper = styled.div` - width: 100%; - height: 100%; - display: flex; -`; - -const NoControllers = styled.div` - padding-top: 20%; - position: relative; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - color: #ffffff44; - font-size: 14px; - - > i { - font-size: 18px; - margin-right: 12px; - } -`; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/status/types.ts b/dashboard/src/main/home/app-dashboard/expanded-app/status/types.ts deleted file mode 100644 index ffcfd4597d..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/status/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type SelectedPodType = { - spec: { - [key: string]: any; - containers: { - [key: string]: any; - name: string; - }[]; - }; - metadata: { - name: string; - namespace: string; - labels: { - [key: string]: string; - }; - }; - status: { - phase: string; - }; -}; diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/status/useLogs.ts b/dashboard/src/main/home/app-dashboard/expanded-app/status/useLogs.ts deleted file mode 100644 index defd9b4bde..0000000000 --- a/dashboard/src/main/home/app-dashboard/expanded-app/status/useLogs.ts +++ /dev/null @@ -1,218 +0,0 @@ -import Anser from "anser"; -import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets"; -import { SelectedPodType } from "./types"; - -const MAX_LOGS = 250; - -export const useLogs = ( - currentPod: SelectedPodType, - scroll?: (smooth: boolean) => void -) => { - const currentPodName = useRef(); - - const { currentCluster, currentProject } = useContext(Context); - const [containers, setContainers] = useState([]); - const [currentContainer, setCurrentContainer] = useState(""); - const [logs, setLogs] = useState<{ - [key: string]: Anser.AnserJsonEntry[][]; - }>({}); - - const [prevLogs, setPrevLogs] = useState<{ - [key: string]: Anser.AnserJsonEntry[][]; - }>({}); - - const { - newWebsocket, - openWebsocket, - closeAllWebsockets, - getWebsocket, - closeWebsocket, - } = useWebsockets(); - - const getSystemLogs = async () => { - const events = await api - .getPodEvents( - "", - {}, - { - name: currentPod?.metadata?.name, - namespace: currentPod?.metadata?.namespace, - cluster_id: currentCluster?.id, - id: currentProject?.id, - } - ) - .then((res) => res.data); - - let processedLogs = [] as Anser.AnserJsonEntry[][]; - - events.items.forEach((evt: any) => { - let ansiEvtType = evt.type == "Warning" ? "\u001b[31m" : "\u001b[32m"; - let ansiLog = Anser.ansiToJson( - `${ansiEvtType}${evt.type}\u001b[0m \t \u001b[43m\u001b[34m\t${evt.reason} \u001b[0m \t ${evt.message}` - ); - processedLogs.push(ansiLog); - }); - - // SET LOGS FOR SYSTEM - setLogs((prevState) => ({ - ...prevState, - system: processedLogs, - })); - }; - - const getContainerPreviousLogs = async (containerName: string) => { - try { - const logs = await api - .getPreviousLogsForContainer<{ previous_logs: string[] }>( - "", - { - container_name: containerName, - }, - { - pod_name: currentPod?.metadata?.name, - namespace: currentPod?.metadata?.namespace, - cluster_id: currentCluster?.id, - project_id: currentProject?.id, - } - ) - .then((res) => res.data); - // Process logs - const processedLogs: Anser.AnserJsonEntry[][] = logs.previous_logs.map( - (currentLog) => { - let ansiLog = Anser.ansiToJson(currentLog); - return ansiLog; - } - ); - - setPrevLogs((pl) => ({ - ...pl, - [containerName]: processedLogs, - })); - } catch (error) { } - }; - - const setupWebsocket = (containerName: string, websocketKey: string) => { - if (!currentPod?.metadata?.name) return; - - const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${currentPod?.metadata?.namespace}/pod/${currentPod?.metadata?.name}/logs?container_name=${containerName}`; - - const config: NewWebsocketOptions = { - onopen: () => { - console.log("Opened websocket:", websocketKey); - }, - onmessage: (evt: MessageEvent) => { - let ansiLog = Anser.ansiToJson(evt.data); - setLogs((logs) => { - const tmpLogs = { ...logs }; - let containerLogs = tmpLogs[containerName] || []; - - containerLogs.push(ansiLog); - // this is technically not as efficient as things could be - // if there are performance issues, a deque can be used in place of a list - // for storing logs - if (containerLogs.length > MAX_LOGS) { - containerLogs.shift(); - } - if (typeof scroll === "function") { - scroll(true); - } - return { - ...logs, - [containerName]: containerLogs, - }; - }); - }, - onclose: () => { - console.log("Closed websocket:", websocketKey); - }, - }; - - newWebsocket(websocketKey, endpoint, config); - openWebsocket(websocketKey); - }; - - const refresh = () => { - const websocketKey = `${currentPodName.current}-${currentContainer}-websocket`; - closeWebsocket(websocketKey); - - setPrevLogs((prev) => ({ ...prev, [currentContainer]: [] })); - setLogs((prev) => ({ ...prev, [currentContainer]: [] })); - - if (!Array.isArray(containers)) { - return; - } - - if (currentContainer === "system") { - getSystemLogs(); - } else { - getContainerPreviousLogs(currentContainer); - setupWebsocket(currentContainer, websocketKey); - } - }; - - useEffect(() => { - // console.log("Selected pod updated"); - if (currentPod?.metadata?.name === currentPodName.current) { - return () => { }; - } - currentPodName.current = currentPod?.metadata?.name; - const currentContainers = - currentPod?.spec?.containers?.map((container) => container?.name) || []; - - setContainers(currentContainers); - setCurrentContainer(currentContainers[0]); - }, [currentPod]); - - // Retrieve all previous logs for containers - useEffect(() => { - if (!Array.isArray(containers)) { - return; - } - - closeAllWebsockets(); - - setPrevLogs({}); - setLogs({}); - - getSystemLogs(); - containers.forEach((containerName) => { - const websocketKey = `${currentPodName.current}-${containerName}-websocket`; - - getContainerPreviousLogs(containerName); - - if (!getWebsocket(websocketKey)) { - setupWebsocket(containerName, websocketKey); - } - }); - - return () => { - closeAllWebsockets(); - }; - }, [containers]); - - useEffect(() => { - return () => { - closeAllWebsockets(); - }; - }, []); - - const currentLogs = useMemo(() => { - return logs[currentContainer] || []; - }, [currentContainer, logs]); - - const currentPreviousLogs = useMemo(() => { - return prevLogs[currentContainer] || []; - }, [currentContainer, prevLogs]); - - return { - containers, - currentContainer, - setCurrentContainer, - logs: currentLogs, - previousLogs: currentPreviousLogs, - refresh, - }; -}; diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx deleted file mode 100644 index d837c3dfba..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx +++ /dev/null @@ -1,781 +0,0 @@ -import React, { useState, useContext, useEffect } from "react"; -import styled from "styled-components"; -import { type RouteComponentProps, withRouter } from "react-router"; -import _ from "lodash"; -import yaml from "js-yaml"; - -import { Context } from "shared/Context"; -import api from "shared/api"; -import web from "assets/web.png"; -import sliders from "assets/sliders.svg"; - -import Back from "components/porter/Back"; -import DashboardHeader from "../../cluster-dashboard/DashboardHeader"; -import Text from "components/porter/Text"; -import Spacer from "components/porter/Spacer"; -import Input from "components/porter/Input"; -import VerticalSteps from "components/porter/VerticalSteps"; -import Button from "components/porter/Button"; -import SourceSelector, { type SourceType } from "./SourceSelector"; -import Container from "components/porter/Container"; - -import SourceSettings from "./SourceSettings"; -import Services from "./Services"; -import { type KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray"; -import GithubActionModal from "./GithubActionModal"; -import Error from "components/porter/Error"; -import { type PorterJson, PorterYamlSchema, createFinalPorterYaml } from "./schema"; -import { ImageInfo, Service } from "./serviceTypes"; -import GithubConnectModal from "./GithubConnectModal"; -import Link from "components/porter/Link"; -import { type BuildMethod, PorterApp } from "../types/porterApp"; -import { type NewPopulatedEnvGroup, PartialEnvGroup, type PopulatedEnvGroup } from "components/porter-form/types"; -import EnvGroupArrayStacks from "main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks"; -import EnvGroupModal from "../expanded-app/env-vars/EnvGroupModal"; -import ExpandableEnvGroup from "../expanded-app/env-vars/ExpandableEnvGroup"; - -type Props = RouteComponentProps & {}; - -type FormState = { - applicationName: string; - selectedSourceType: SourceType | undefined; - serviceList: Service[]; - envVariables: KeyValueType[]; -} - -const INITIAL_STATE: FormState = { - applicationName: "", - selectedSourceType: undefined, - serviceList: [], - envVariables: [], -}; - -const Validators: { - [key in keyof FormState]: (value: FormState[key]) => boolean; -} = { - applicationName: (value: string) => value.trim().length > 0, - selectedSourceType: (value: SourceType | undefined) => value !== undefined, - serviceList: (value: Service[]) => value.length > 0, - envVariables: (value: KeyValueType[]) => true, -}; - -type Detected = { - detected: boolean; - message: string; -}; -type GithubAppAccessData = { - username?: string; - accounts?: string[]; -} - -type PorterJsonWithPath = { - porterYamlPath: string; - porterJson: PorterJson; -} - -const NewAppFlow: React.FC = ({ ...props }) => { - const [porterApp, setPorterApp] = useState(PorterApp.empty()); - const [hovered, setHovered] = useState(false); - - const [imageTag, setImageTag] = useState(""); - const { currentCluster, currentProject } = useContext(Context); - const [deploying, setDeploying] = useState(false); - const [deploymentError, setDeploymentError] = useState(undefined); - const [currentStep, setCurrentStep] = useState(0); - const [existingStep, setExistingStep] = useState(0); - const [formState, setFormState] = useState(INITIAL_STATE); - const [porterYaml, setPorterYaml] = useState(""); - const [showGHAModal, setShowGHAModal] = useState(false); - const [showGithubConnectModal, setShowGithubConnectModal] = useState( - false - ); - - const [showConnectModal, setConnectModal] = useState(true); - const [hasClickedDoNotConnect, setHasClickedDoNotConnect] = useState(() => - JSON.parse(localStorage.getItem("hasClickedDoNotConnect") || "false") - ); - const [accessLoading, setAccessLoading] = useState(true); - const [accessError, setAccessError] = useState(false); - const [accessData, setAccessData] = useState({}); - const [hasProviders, setHasProviders] = useState(true); - - const [porterJsonWithPath, setPorterJsonWithPath] = useState(undefined); - const [detected, setDetected] = useState(undefined); - const [buildView, setBuildView] = useState("buildpacks"); - - const [existingApps, setExistingApps] = useState([]); - const [appNameInputError, setAppNameInputError] = useState(undefined); - - const [syncedEnvGroups, setSyncedEnvGroups] = useState([]); - const [showEnvModal, setShowEnvModal] = useState(false); - const [deletedEnvGroups, setDeleteEnvGroups] = useState([]) - - // this advances the step in the case that a user chooses a repo that doesn't have a porter.yaml - useEffect(() => { - if (porterApp.git_branch !== "") { - setCurrentStep(Math.max(currentStep, 2)); - } - }, [porterApp.git_branch]); - - useEffect(() => { - let isSubscribed = true; - - if (currentProject == null) { - return; - } - - api - .getGitProviders("", {}, { project_id: currentProject?.id }) - .then((res) => { - const data = res.data; - if (!isSubscribed) { - return; - } - - if (!Array.isArray(data)) { - setHasProviders(false); - - } - }) - .catch((err) => { - setHasProviders(false); - }); - - return () => { - isSubscribed = false; - }; - }, [currentProject]); - - useEffect(() => { - const getApps = async () => { - try { - const res = await api.getPorterApps( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - if (res?.data != null) { - setExistingApps(res.data.map((app: PorterApp) => app.name)); - } - } catch (err) { - } - }; - getApps(); - }, []) - - useEffect(() => { - setFormState({ ...formState, serviceList: [] }); - setDetected(undefined); - }, [porterApp.git_branch]); - - const handleSetAccessData = (data: GithubAppAccessData) => { - setAccessData(data); - setShowGithubConnectModal( - !hasClickedDoNotConnect && - (accessError || !data.accounts || data.accounts?.length === 0) - ); - }; - - const handleSetAccessError = (error: boolean) => { - setAccessError(error); - setShowGithubConnectModal( - !hasClickedDoNotConnect && - (error || !accessData.accounts || accessData.accounts?.length === 0) - ); - }; - - const updateStackStep = async (step: string, errorMessage: string = "") => { - try { - if (currentCluster?.id == null || currentProject?.id == null) { - throw "Unable to capture analytics, project or cluster not found"; - } - await api.updateStackStep( - "", - { - step, - stack_name: porterApp.name, - error_message: errorMessage, - }, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - } - ); - } catch (err) { - // TODO: handle analytics error - } - }; - - const validateAndSetPorterYaml = (yamlString: string, filename: string) => { - let parsedYaml; - try { - parsedYaml = yaml.load(yamlString); - const parsedData = PorterYamlSchema.parse(parsedYaml); - const porterYamlToJson = parsedData ; - setPorterJsonWithPath({ porterJson: porterYamlToJson, porterYamlPath: filename }); - const newServices = []; - const existingServices = formState.serviceList.map((s) => s.name); - for (const [name, app] of Object.entries(porterYamlToJson.apps)) { - if (!existingServices.includes(name)) { - if (app.type) { - newServices.push(Service.default(name, app.type, porterYamlToJson)); - } else if (name.includes("web")) { - newServices.push(Service.default(name, "web", porterYamlToJson)); - } else { - newServices.push(Service.default(name, "worker", porterYamlToJson)); - } - } - } - if (porterYamlToJson.release != null && !existingServices.includes("pre-deploy")) { - newServices.push(Service.default("pre-deploy", "release", porterYamlToJson)); - } - const newServiceList = [...formState.serviceList, ...newServices]; - if (Validators.serviceList(newServiceList)) { - setCurrentStep(Math.max(currentStep, 5)); - } - setFormState({ - ...formState, - serviceList: newServiceList, - }); - if ( - porterYamlToJson && - porterYamlToJson.apps && - Object.keys(porterYamlToJson.apps).length > 0 - ) { - setDetected({ - detected: true, - message: `Detected ${Object.keys(porterYamlToJson.apps).length - } service${Object.keys(porterYamlToJson.apps).length === 1 ? "" : "s"} from porter.yaml`, - }); - } else { - setDetected({ - detected: false, - message: - "Could not detect any services from porter.yaml. Make sure it exists in the root of your repo.", - }); - } - } catch (error) { - console.log("Error converting porter yaml file to input: " + error); - } - }; - - const handleAppNameChange = (name: string) => { - setPorterApp(PorterApp.setAttribute(porterApp, "name", name)); - const appNameInputError = getAppNameInputError(name); - if (appNameInputError == null) { - setCurrentStep(Math.max(Math.max(currentStep, 1), existingStep)); - } else { - setExistingStep(Math.max(currentStep, existingStep)); - setCurrentStep(0); - } - setAppNameInputError(appNameInputError); - }; - - const handleDoNotConnect = () => { - setHasClickedDoNotConnect(true); - localStorage.setItem("hasClickedDoNotConnect", "true"); - }; - - const getAppNameInputError = (name: string) => { - const regex = /^[a-z0-9-]{1,61}$/; - if (name === "") { - return undefined; - } else if (!regex.test(name)) { - return 'Lowercase letters, numbers, and "-" only.'; - } else if (name.length > 30) { - return "Maximum 30 characters allowed."; - } else if (existingApps.includes(name)) { - return "An app with this name already exists."; - } - return undefined; - }; - - const deleteEnvGroup = (envGroup: PopulatedEnvGroup) => { - setDeleteEnvGroups([...deletedEnvGroups, envGroup]); - setSyncedEnvGroups(syncedEnvGroups?.filter( - (env) => env.name !== envGroup.name - )) - } - - const deployPorterApp = async () => { - try { - setDeploying(true); - setDeploymentError(undefined); - - // log analytics event that we started form submission - updateStackStep("stack-launch-complete"); - - if (currentProject?.id == null || currentCluster?.id == null) { - throw "Project or cluster not found"; - } - - // validate form data - const finalPorterYaml = createFinalPorterYaml( - formState.serviceList, - formState.envVariables, - porterJsonWithPath?.porterJson, - // if we are using a heroku buildpack, inject a PORT env variable - porterApp.builder.includes("heroku") - ); - - const yamlString = yaml.dump(finalPorterYaml); - const base64Encoded = btoa(yamlString); - const imageInfo: ImageInfo = ImageInfo.BASE_IMAGE; - - const porterAppRequest = { - porter_yaml: base64Encoded, - override_release: true, - ...PorterApp.empty(), - image_info: imageInfo, - buildpacks: "", - // for some reason I couldn't get the path to update the porterApp object correctly here so I just grouped it with the porter json :/ - porter_yaml_path: porterJsonWithPath?.porterYamlPath, - repo_name: porterApp.repo_name, - git_branch: porterApp.git_branch, - git_repo_id: porterApp.git_repo_id, - build_context: porterApp.build_context, - image_repo_uri: porterApp.image_repo_uri, - environment_groups: syncedEnvGroups?.map((env: NewPopulatedEnvGroup) => env.name), - user_update: true, - } - if (porterApp.image_repo_uri && imageTag) { - porterAppRequest.image_info = { - repository: porterApp.image_repo_uri, - tag: imageTag, - }; - porterAppRequest.repo_name = ""; - porterAppRequest.git_branch = ""; - porterAppRequest.git_repo_id = 0; - } else if (buildView === "docker") { - if (porterApp.dockerfile === "") { - porterAppRequest.dockerfile = "./Dockerfile"; - } else { - if (!porterApp.dockerfile.startsWith("./") && !porterApp.dockerfile.startsWith("/")) { - porterAppRequest.dockerfile = `./${porterApp.dockerfile}`; - } else { - porterAppRequest.dockerfile = porterApp.dockerfile; - } - } - } else { - porterAppRequest.builder = porterApp.builder; - porterAppRequest.buildpacks = porterApp.buildpacks.join(","); - } - - await api.createPorterApp( - "", - porterAppRequest, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - stack_name: porterApp.name, - } - ); - - if (porterAppRequest.repo_name === "") { - props.history.push(`/apps/${porterApp.name}`); - } - - // log analytics event that we successfully deployed - updateStackStep("stack-launch-success"); - - return true; - } catch (err: any) { - // TODO: better error handling - const errMessage = - err?.response?.data?.error ?? - err?.toString() ?? - "An error occurred while deploying your app. Please try again."; - setDeploymentError(errMessage); - updateStackStep("stack-launch-failure", errMessage); - return false; - } finally { - setDeploying(false); - } - }; - const maxEnvGroupsReached = syncedEnvGroups.length >= 4; - - - return ( - -
- {showConnectModal && !hasProviders && ( - { setConnectModal(false); }} - hasClickedDoNotConnect={hasClickedDoNotConnect} - handleDoNotConnect={handleDoNotConnect} - accessData={accessData} - setAccessLoading={setAccessLoading} - accessError={accessError} - setAccessData={handleSetAccessData} - setAccessError={handleSetAccessError} - /> - )} - - - } - title="Deploy a new application" - capitalize={false} - disableLineBreak - /> - - - Application name - - - Lowercase letters, numbers, and "-" only. - - - - , - <> - Deployment method - - - Deploy from a Git repository or a Docker registry. - - - Learn more - - - - { - setPorterYaml(""); - setFormState({ ...formState, selectedSourceType: type }); - }} - /> - { - validateAndSetPorterYaml(newYaml, filename); - }} - porterApp={porterApp} - setPorterApp={setPorterApp} - imageUrl={porterApp.image_repo_uri} - setImageUrl={(url: string) => { - setPorterApp(PorterApp.setAttribute(porterApp, "image_repo_uri", url)); - setCurrentStep(Math.max(currentStep, 2)); - }} - imageTag={imageTag} - setImageTag={setImageTag} - buildView={buildView} - setBuildView={setBuildView} - projectId={currentProject?.id ?? 0} - resetImageInfo={() => { - setPorterApp(PorterApp.setAttribute(porterApp, "image_repo_uri", "")); - setImageTag(""); - }} - /> - , - <> - - - Application services{" "} - - {detected && formState.serviceList.length > 0 && ( - - {detected.detected ? ( - check - ) : ( - error - )} - - {detected.message} - - - )} - - - { - const release = formState.serviceList.filter(Service.isRelease) - setFormState({ ...formState, serviceList: [...services, ...release] }); - if (Validators.serviceList(services)) { - setCurrentStep(Math.max(currentStep, 5)); - } - }} - services={formState.serviceList.filter(Service.isNonRelease)} - defaultExpanded={true} - addNewText={"Add a new service"} - appName={porterApp.name} - /> - , - <> - Environment variables (optional) - - - Specify environment variables shared among all services. - - { - setFormState({ ...formState, envVariables: x }); - }} - fileUpload={true} - syncedEnvGroups={syncedEnvGroups} - /> - - <> - { setHovered(true); }} - onMouseOut={() => { setHovered(false); }}> - { !maxEnvGroupsReached && setShowEnvModal(true); }} - > - Load from Env Group - - Max 4 Env Groups allowed - - - {showEnvModal && { - setFormState({ ...formState, envVariables: x }); - }} - values={formState.envVariables} - closeModal={() => { setShowEnvModal(false); }} - syncedEnvGroups={syncedEnvGroups} - setSyncedEnvGroups={setSyncedEnvGroups} - namespace={"porter-stack-" + porterApp.name} - newApp={true} - />} - {!!syncedEnvGroups?.length && ( - <> - - Synced environment groups - {syncedEnvGroups?.map((envGroup: any) => { - return ( - { - deleteEnvGroup(envGroup); - }} - /> - ); - })} - - )} - - - , - formState.selectedSourceType == "github" && - <> - Pre-deploy job (optional) - - - After your application is built each time, your pre-deploy command will run before your services - are deployed. Use this for operations like a database migration. - - - { - const nonRelease = formState.serviceList.filter(Service.isNonRelease) - setFormState({ ...formState, serviceList: [...nonRelease, ...release] }); - }} - services={formState.serviceList.filter(Service.isRelease)} - limitOne={true} - addNewText={"Add a new pre-deploy job"} - prePopulateService={Service.default("pre-deploy", "release", porterJsonWithPath?.porterJson)} - appName={porterApp.name} - /> - , - , - ].filter((x) => x)} - /> - - -
- {showGHAModal && currentCluster != null && currentProject != null && ( - { setShowGHAModal(false); }} - githubAppInstallationID={porterApp.git_repo_id} - githubRepoOwner={porterApp.repo_name.split("/")[0]} - githubRepoName={porterApp.repo_name.split("/")[1]} - branch={porterApp.git_branch} - stackName={porterApp.name} - projectId={currentProject.id} - clusterId={currentCluster.id} - deployPorterApp={deployPorterApp} - deploymentError={deploymentError} - porterYamlPath={porterJsonWithPath?.porterYamlPath} - /> - )} -
- ); -}; - -export default withRouter(NewAppFlow); - -const I = styled.i` - font-size: 18px; - margin-right: 5px; -`; - -const Div = styled.div` - width: 100%; - max-width: 900px; -`; - -const CenterWrapper = styled.div` - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -`; - -const DarkMatter = styled.div` - width: 100%; - margin-top: -5px; -`; - -const Icon = styled.img` - margin-right: 15px; - height: 28px; - animation: floatIn 0.5s; - animation-fill-mode: forwards; - - @keyframes floatIn { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0px); - } - } -`; - -const AppearingDiv = styled.div<{ color?: string }>` - animation: floatIn 0.5s; - animation-fill-mode: forwards; - display: flex; - align-items: center; - color: ${(props) => props.color || "#ffffff44"}; - margin-left: 10px; - @keyframes floatIn { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0px); - } - } -`; - -const StyledConfigureTemplate = styled.div` - height: 100%; -`; - - -const AddRowButton = styled.div` - display: flex; - align-items: center; - width: 270px; - font-size: 13px; - color: #aaaabb; - height: 32px; - border-radius: 3px; - cursor: pointer; - background: #ffffff11; - :hover { - background: #ffffff22; - } - - > i { - color: #ffffff44; - font-size: 16px; - margin-left: 8px; - margin-right: 10px; - display: flex; - align-items: center; - justify-content: center; - } -`; -const LoadButton = styled(AddRowButton) <{ disabled?: boolean }>` - background: ${(props) => (props.disabled ? "#aaaaaa55" : "none")}; - border: 1px solid ${(props) => (props.disabled ? "#aaaaaa55" : "#ffffff55")}; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; - - > i { - color: ${(props) => (props.disabled ? "#aaaaaa44" : "#ffffff44")}; - font-size: 16px; - margin-left: 8px; - margin-right: 10px; - display: flex; - align-items: center; - justify-content: center; - } - > img { - width: 14px; - margin-left: 10px; - margin-right: 12px; - opacity: ${(props) => (props.disabled ? "0.5" : "1")}; - } -`; - -const TooltipWrapper = styled.div` - position: relative; - display: inline-block; -`; - -const TooltipText = styled.span` - visibility: ${(props) => (props.visible ? 'visible' : 'hidden')}; - width: 240px; - color: #fff; - text-align: center; - padding: 5px 0; - border-radius: 6px; - position: absolute; - z-index: 1; - bottom: 100%; - left: 50%; - margin-left: -120px; - opacity: ${(props) => (props.visible ? '1' : '0')}; - transition: opacity 0.3s; - font-size: 12px; -`; - diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx deleted file mode 100644 index 1c0ee9affb..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import AnimateHeight, { Height } from "react-animate-height"; -import styled from "styled-components"; -import _, { set } from "lodash"; - -import web from "assets/web.png"; -import worker from "assets/worker.png"; -import job from "assets/job.png"; - -import Spacer from "components/porter/Spacer"; -import WebTabs from "./tabs/WebTabs"; -import WorkerTabs from "./tabs/WorkerTabs"; -import JobTabs from "./tabs/JobTabs"; -import { Service } from "./serviceTypes"; -import StatusFooter from "../expanded-app/StatusFooter"; -import ReleaseTabs from "./tabs/ReleaseTabs"; -import { Context } from "shared/Context"; -import { AWS_INSTANCE_LIMITS } from "./tabs/utils"; -import api from "shared/api"; - -interface ServiceProps { - service: Service; - chart?: any; - editService: (service: Service) => void; - deleteService: () => void; - setExpandedJob?: (x: string) => void; -} - -const ServiceContainer: React.FC = ({ - service, - chart, - deleteService, - editService, - setExpandedJob, -}) => { - const [height, setHeight] = React.useState("auto"); - const [applicationNodeCount, setApplicationNodeCount] = useState(1); - const [maxCPU, setMaxCPU] = useState(2); //default is set to a t3 medium - const [maxRAM, setMaxRAM] = useState(4); //default is set to a t3 medium - const context = useContext(Context); - - useEffect(() => { - const { currentCluster, currentProject } = context; - if (!currentCluster || !currentProject) { - return; - } - var instanceType = ""; - - - if (service) { - const serviceName = service.name; - - //first check if there is a nodeSelector for the given application (Can be null) - if (chart?.config?.[`${serviceName}-${service.type}`]?.nodeSelector?.["beta.kubernetes.io/instance-type"]) { - instanceType = chart?.config?.[`${serviceName}-${service.type}`]?.nodeSelector?.["beta.kubernetes.io/instance-type"] - const [instanceClass, instanceSize] = instanceType.split('.'); - const currentInstance = AWS_INSTANCE_LIMITS[instanceClass][instanceSize]; - setMaxCPU(currentInstance.vCPU); - setMaxRAM(currentInstance.RAM); - } - } - //Query the given nodes if no instance type is specified - if (instanceType == "") { - - api - .getClusterNodes( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - } - ) - .then(({ data }) => { - if (data) { - var nodeCount = 0 - let largestInstanceType = { - vCPUs: 2, - RAM: 4, - }; - - // TODO: type this response - data.forEach((node: any) => { - if (node.labels['porter.run/workload-kind'] == "application") { - nodeCount += 1 - var instanceType: string = node.labels['beta.kubernetes.io/instance-type']; - const [instanceClass, instanceSize] = instanceType.split('.'); - if (instanceClass && instanceSize) { - if (AWS_INSTANCE_LIMITS[instanceClass] && AWS_INSTANCE_LIMITS[instanceClass][instanceSize]) { - let currentInstance = AWS_INSTANCE_LIMITS[instanceClass][instanceSize]; - largestInstanceType.vCPUs = currentInstance.vCPU; - largestInstanceType.RAM = currentInstance.RAM; - } - } - } - }); - setApplicationNodeCount(nodeCount); - setMaxCPU(largestInstanceType.vCPUs); - setMaxRAM(largestInstanceType.RAM); - } - }).catch((error) => { - - }); - } - }, []); - // TODO: calculate heights instead of hardcoding them - const renderTabs = (service: Service) => { - switch (service.type) { - case "web": - return ( - - ); - case "worker": - return ( - - ); - case "job": - return ( - - ); - case "release": - return ( - - ); - } - }; - - const renderIcon = (service: Service) => { - switch (service.type) { - case "web": - return ; - case "worker": - return ; - case "job": - return ; - case "release": - return ; - } - }; - - const getHasBuiltImage = () => { - if (chart?.chart?.values == null) { - return false; - } - return !_.isEmpty((Object.values(chart.chart.values)[0] as any)?.global); - }; - - return ( - <> - editService({ ...service, expanded: !service.expanded })} - chart={chart} - bordersRounded={!getHasBuiltImage() && !service.expanded} - > - - - arrow_drop_down - - {renderIcon(service)} - {service.name.trim().length > 0 ? service.name : "New Service"} - - {service.canDelete && ( - { - e.stopPropagation(); - deleteService(); - }}> - delete - - )} - - - - {renderTabs(service)} - - - {chart && - service && - // Check if has built image - getHasBuiltImage() && ( - - )} - - - ); -}; - -export default ServiceContainer; - -const ServiceTitle = styled.div` - display: flex; - align-items: center; -`; - -const StyledSourceBox = styled.div<{ - showExpanded: boolean; - chart: any; - hasFooter?: boolean; -}>` - width: 100%; - color: #ffffff; - padding: 14px 25px 30px; - position: relative; - font-size: 13px; - background: ${(props) => props.theme.fg}; - border: 1px solid #494b4f; - border-top: 0; - border-bottom-left-radius: ${(props) => (props.hasFooter ? "0" : "5px")}; - border-bottom-right-radius: ${(props) => (props.hasFooter ? "0" : "5px")}; -`; - -const ActionButton = styled.button` - position: relative; - border: none; - background: none; - padding: 5px; - display: flex; - justify-content: center; - align-items: center; - border-radius: 50%; - cursor: pointer; - color: #aaaabb; - :hover { - color: white; - } - - > span { - font-size: 20px; - } - margin-right: 5px; -`; - -const ServiceHeader = styled.div<{ - showExpanded: boolean; - chart: any; - bordersRounded?: boolean; -}>` - flex-direction: row; - display: flex; - height: 60px; - 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; - } - - border-bottom-left-radius: ${(props) => (props.bordersRounded ? "" : "0")}; - border-bottom-right-radius: ${(props) => (props.bordersRounded ? "" : "0")}; - - .dropdown { - font-size: 30px; - cursor: pointer; - border-radius: 20px; - margin-left: -10px; - transform: ${(props: { showExpanded: boolean; chart: any }) => - props.showExpanded ? "" : "rotate(-90deg)"}; - } - - animation: fadeIn 0.3s 0s; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const Icon = styled.img` - height: 18px; - margin-right: 15px; - - animation: fadeIn 0.3s 0s; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx deleted file mode 100644 index 8dc04badba..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import ServiceContainer from "./ServiceContainer"; -import styled from "styled-components"; -import Spacer from "components/porter/Spacer"; -import Modal from "components/porter/Modal"; -import Text from "components/porter/Text"; -import Select from "components/porter/Select"; -import Input from "components/porter/Input"; -import Container from "components/porter/Container"; -import Button from "components/porter/Button"; - -import web from "assets/web.png"; -import worker from "assets/worker.png"; -import job from "assets/job.png"; -import { Service, ServiceType } from "./serviceTypes"; - -interface ServicesProps { - services: Service[]; - appName: string; - setServices: (services: Service[]) => void; - addNewText: string; - defaultExpanded?: boolean; - chart?: any; - limitOne?: boolean; - prePopulateService?: Service; - setExpandedJob?: (x: string) => void; -} - -const Services: React.FC = ({ - appName, - services, - setServices, - addNewText, - chart, - limitOne = false, - setExpandedJob, - prePopulateService, -}) => { - const [showAddServiceModal, setShowAddServiceModal] = useState( - false - ); - const [serviceName, setServiceName] = useState(""); - const [serviceType, setServiceType] = useState("web"); - const isServiceNameValid = (name: string) => { - const regex = /^[a-z0-9-]+$/; - - return regex.test(name); - }; - const isServiceNameDuplicate = (name: string) => { - const serviceNames = services.map((service) => service.name); - return serviceNames.includes(name); - }; - const isServiceNameTooLong = (name: string) => { - // k8s pod name limit is 63 characters: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set - // the pod name is the appName-serviceName-web/wkr/job-random_4_char_string, so the max limit is 53 - return name.length + appName.length > 53; - }; - - const maybeGetError = (): string | undefined => { - if (serviceName.length > 30) { - return "Must be 30 characters or less."; - } else if (serviceName != "" && !isServiceNameValid(serviceName)) { - return "Lowercase letters, numbers, and '-' only."; - } else if (isServiceNameDuplicate(serviceName)) { - return "Service name is duplicate!"; - } else if (isServiceNameTooLong(serviceName)) { - return "Service name is too long!"; - } else { - return undefined; - } - }; - - const maybeRenderAddServicesButton = () => { - if (limitOne && services.length > 0) { - return null; - } - return ( - <> - { - if (prePopulateService == null) { - setShowAddServiceModal(true); - setServiceType("web"); - } else { - const newServices = [ - ...services, - prePopulateService, - ] - setServices(newServices); - } - }} - > - add_icon - {addNewText} - - - - ); - }; - - return ( - <> - {services.length > 0 && ( - - {services.map((service, index) => { - return ( - { - const newServices = services.map((s, i) => (i === index ? newService : s)); - setServices(newServices); - }} - deleteService={() => { - const newServices = services.filter((_, i) => i !== index); - setServices(newServices); - }} - /> - ); - })} - - )} - {maybeRenderAddServicesButton()} - {showAddServiceModal && ( - { - setShowAddServiceModal(false) - setServiceName("") - setServiceType("web") - }} - width="500px" - > - {addNewText} - - Select a service type: - - - - {serviceType === "web" && } - {serviceType === "worker" && } - {serviceType === "job" && } - - - - - - )} - - ); -}; - -export default Services; - -const ServiceIcon = styled.div` - border: 1px solid #494b4f; - display: flex; - align-items: center; - justify-content: center; - height: 35px; - width: 35px; - min-width: 35px; - margin-right: 10px; - overflow: hidden; - border-radius: 5px; - > img { - height: 18px; - animation: floatIn 0.5s 0s; - @keyframes floatIn { - from { - opacity: 0; - transform: translateY(7px); - } - to { - opacity: 1; - transform: translateY(0px); - } - } - } -`; - -const I = styled.i` - color: white; - font-size: 14px; - display: flex; - align-items: center; - margin-right: 7px; - justify-content: center; -`; - -const ServicesContainer = styled.div``; - -const AddServiceButton = 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; - } -`; diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts b/dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts deleted file mode 100644 index 846a3173cd..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts +++ /dev/null @@ -1,695 +0,0 @@ -import _ from "lodash"; -import { overrideObjectValues } from "./utils"; -import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray"; -import { PorterJson } from "./schema"; - -export type ImageInfo = { - repository: string; - tag: string; -} -export const ImageInfo = { - BASE_IMAGE: { - repository: "ghcr.io/porter-dev/porter/hello-porter", - tag: "latest", - } as const, -} - - -export type Service = WorkerService | WebService | JobService | ReleaseService; -export type ServiceType = 'web' | 'worker' | 'job' | 'release'; - -export type ServiceString = { - readOnly: boolean; - value: string; -} -type ServiceBoolean = { - readOnly: boolean; - value: boolean; -} -export type ServiceArray = T[]; -const ServiceArray = { - serialize: (serviceArray: ServiceArray) => { - return serviceArray.map((service) => service.value).filter((val) => val !== ''); - } -} -export type ServiceKeyValueArray = { - key: string; - value: T; -}[]; -const ServiceKeyValueArray = { - serialize: (serviceKeyValueArray: ServiceKeyValueArray) => { - const map: Record = {}; - serviceKeyValueArray.map(({ key, value }: { - key: string; - value: T; - }) => { - if (key != '') { - map[key] = value.value.toString(); - } - }); - return map; - } -} - -type Ingress = { - enabled: ServiceBoolean; - customDomains: ServiceArray; - hosts: ServiceArray; - porterHosts: ServiceString; - annotations: ServiceKeyValueArray; -} -type Autoscaling = { - enabled: ServiceBoolean, - minReplicas: ServiceString, - maxReplicas: ServiceString, - targetCPUUtilizationPercentage: ServiceString, - targetMemoryUtilizationPercentage: ServiceString, -} -type LivenessProbe = { - enabled: ServiceBoolean, - failureThreshold: ServiceString, - path: ServiceString, - periodSeconds: ServiceString, -} -type ReadinessProbe = { - enabled: ServiceBoolean, - failureThreshold: ServiceString, - path: ServiceString, - initialDelaySeconds: ServiceString, -} -type StartUpProbe = { - enabled: ServiceBoolean, - failureThreshold: ServiceString, - path: ServiceString, - periodSeconds: ServiceString, -} -type Health = { - livenessProbe: LivenessProbe, - startupProbe: StartUpProbe, - readinessProbe: ReadinessProbe, -} -type CloudSql = { - enabled: ServiceBoolean, - connectionName: ServiceString, - dbPort: ServiceString, - serviceAccountJSON: ServiceString, -} - - -const ServiceField = { - string: (defaultValue: string, overrideValue?: string): ServiceString => { - return { - readOnly: overrideValue != null, - value: overrideValue ?? defaultValue, - } - }, - boolean: (defaultValue: boolean, overrideValue?: boolean): ServiceBoolean => { - return { - readOnly: overrideValue != null, - value: overrideValue ?? defaultValue, - } - }, - array: (defaultValues: string[], overrideValues?: string[]): ServiceArray => { - const serviceMap: Record = {}; - for (const val of defaultValues) { - serviceMap[val] = ServiceField.string(val); - } - for (const val of overrideValues ?? []) { - serviceMap[val] = ServiceField.string('', val); - } - if (Object.keys(serviceMap).length == 0) { - return []; - } - return Object.values(serviceMap); - }, - keyValueArray: (defaultMap: Record, overrideMap?: Record): ServiceKeyValueArray => { - const serviceMap: Record = {}; - for (const key in defaultMap) { - serviceMap[key] = ServiceField.string(defaultMap[key]); - } - for (const key in overrideMap) { - serviceMap[key] = ServiceField.string('', overrideMap[key]); - } - if (Object.keys(serviceMap).length == 0) { - return []; - } - return Object.keys(serviceMap).map((key) => ({ - key, - value: serviceMap[key], - })); - } -} - -type SharedServiceParams = { - name: string; - cpu: ServiceString; - ram: ServiceString; - startCommand: ServiceString; - type: ServiceType; - canDelete: boolean; - expanded: boolean; - smartOptimization: boolean; - cloudsql: CloudSql; -} - -export type WebService = SharedServiceParams & Omit & { - type: 'web'; - port: ServiceString; - ingress: Ingress; - health: Health; -} -const WebService = { - default: (name: string, porterJson?: PorterJson): WebService => ({ - name, - smartOptimization: true, - expanded: true, - cpu: ServiceField.string('187.5', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined), - ram: ServiceField.string('384', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined), - startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run), - type: 'web', - replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount), - autoscaling: { - enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled), - minReplicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas), - maxReplicas: ServiceField.string('10', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas), - targetCPUUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage), - targetMemoryUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage), - }, - ingress: { - enabled: ServiceField.boolean(true, porterJson?.apps?.[name]?.config?.ingress?.enabled), - customDomains: ServiceField.array([], porterJson?.apps?.[name]?.config?.ingress?.hosts), - hosts: ServiceField.array([], porterJson?.apps?.[name]?.config?.ingress?.hosts), - porterHosts: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.porter_hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.porter_hosts[0] : undefined), - annotations: ServiceField.keyValueArray({}, porterJson?.apps?.[name]?.config?.ingress?.annotations) - }, - port: ServiceField.string('3000', porterJson?.apps?.[name]?.config?.container?.port), - canDelete: porterJson?.apps?.[name] == null, - health: { - startupProbe: { - enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.startupProbe?.enabled), - failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.startupProbe?.failureThreshold), - path: ServiceField.string('/startupz', porterJson?.apps?.[name]?.config?.health?.startupProbe?.path), - periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.startupProbe?.periodSeconds), - }, - readinessProbe: { - enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.readinessProbe?.enabled), - failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.failureThreshold), - path: ServiceField.string('/readyz', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.path), - initialDelaySeconds: ServiceField.string('0', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.initialDelaySeconds), - }, - livenessProbe: { - enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.enabled), - failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.failureThreshold), - path: ServiceField.string('/livez', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.path), - periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.periodSeconds), - }, - }, - cloudsql: { - enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled), - connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName), - dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort), - serviceAccountJSON: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON), - }, - }), - serialize: (service: WebService) => { - return { - smartOptimization: service.smartOptimization, - replicaCount: service.replicas.value, - resources: { - requests: { - cpu: service.cpu.value + 'm', - memory: service.ram.value + 'Mi', - } - }, - container: { - command: service.startCommand.value, - port: service.port.value, - }, - autoscaling: { - enabled: service.autoscaling.enabled.value, - minReplicas: service.autoscaling.minReplicas.value, - maxReplicas: service.autoscaling.maxReplicas.value, - targetCPUUtilizationPercentage: service.autoscaling.targetCPUUtilizationPercentage.value, - targetMemoryUtilizationPercentage: service.autoscaling.targetMemoryUtilizationPercentage.value, - }, - ingress: { - enabled: service.ingress.enabled.value, - custom_domain: service.ingress.customDomains.length ? true : false, - hosts: ServiceArray.serialize(service.ingress.customDomains), - porter_hosts: service.ingress.porterHosts.value ? [service.ingress.porterHosts.value] : [], - annotations: ServiceKeyValueArray.serialize(service.ingress.annotations), - }, - service: { - port: service.port.value, - }, - health: { - startupProbe: { - enabled: service.health.startupProbe.enabled.value, - failureThreshold: service.health.startupProbe.failureThreshold.value, - path: service.health.startupProbe.path.value, - periodSeconds: service.health.startupProbe.periodSeconds.value, - }, - readinessProbe: { - enabled: service.health.readinessProbe.enabled.value, - failureThreshold: service.health.readinessProbe.failureThreshold.value, - path: service.health.readinessProbe.path.value, - initialDelaySeconds: service.health.readinessProbe.initialDelaySeconds.value, - }, - livenessProbe: { - enabled: service.health.livenessProbe.enabled.value, - failureThreshold: service.health.livenessProbe.failureThreshold.value, - path: service.health.livenessProbe.path.value, - periodSeconds: service.health.livenessProbe.periodSeconds.value, - }, - }, - cloudsql: { - enabled: service.cloudsql.enabled.value, - connectionName: service.cloudsql.connectionName.value, - dbPort: service.cloudsql.dbPort.value, - serviceAccountJSON: service.cloudsql.serviceAccountJSON.value, - }, - } - }, - deserialize: (name: string, values: any, porterJson?: PorterJson): WebService => { - return { - name, - expanded: false, - smartOptimization: values.smartOptimization, - cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined), - ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined), - startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run), - type: 'web', - replicas: ServiceField.string(values.replicaCount ?? '', porterJson?.apps?.[name]?.config?.replicaCount), - autoscaling: { - enabled: ServiceField.boolean(values.autoscaling?.enabled ?? false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled), - minReplicas: ServiceField.string(values.autoscaling?.minReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas), - maxReplicas: ServiceField.string(values.autoscaling?.maxReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas), - targetCPUUtilizationPercentage: ServiceField.string(values.autoscaling?.targetCPUUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage), - targetMemoryUtilizationPercentage: ServiceField.string(values.autoscaling?.targetMemoryUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage), - }, - ingress: { - enabled: ServiceField.boolean(values.ingress?.enabled ?? false, porterJson?.apps?.[name]?.config?.ingress?.enabled), - customDomains: ServiceField.array(values.ingress?.hosts ?? [], porterJson?.apps?.[name]?.config?.ingress?.hosts), - hosts: ServiceField.array(values.ingress?.hosts ?? [], porterJson?.apps?.[name]?.config?.ingress?.hosts), - porterHosts: ServiceField.string(values.ingress?.porter_hosts?.length ? values.ingress.porter_hosts[0] : '', porterJson?.apps?.[name]?.config?.ingress?.porter_hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.porter_hosts[0] : undefined), - annotations: ServiceField.keyValueArray(values.ingress?.annotations ?? {}, porterJson?.apps?.[name]?.config?.ingress?.annotations), - }, - port: ServiceField.string(values.container?.port ?? '', porterJson?.apps?.[name]?.config?.container?.port), - canDelete: porterJson?.apps?.[name] == null, - health: { - startupProbe: { - enabled: ServiceField.boolean(values.health?.startupProbe?.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.startupProbe?.enabled), - failureThreshold: ServiceField.string(values.health?.startupProbe?.failureThreshold ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.failureThreshold), - path: ServiceField.string(values.health?.startupProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.path), - periodSeconds: ServiceField.string(values.health?.startupProbe?.periodSeconds ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.periodSeconds), - }, - readinessProbe: { - enabled: ServiceField.boolean(values.health?.readinessProbe?.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.readinessProbe?.enabled), - failureThreshold: ServiceField.string(values.health?.readinessProbe?.failureThreshold ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.failureThreshold), - path: ServiceField.string(values.health?.readinessProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.path), - initialDelaySeconds: ServiceField.string(values.health?.readinessProbe?.initialDelaySeconds ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.initialDelaySeconds), - }, - livenessProbe: { - enabled: ServiceField.boolean(values.health?.livenessProbe?.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.enabled), - failureThreshold: ServiceField.string(values.health?.livenessProbe?.failureThreshold ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.failureThreshold), - path: ServiceField.string(values.health?.livenessProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.path), - periodSeconds: ServiceField.string(values.health?.livenessProbe?.periodSeconds ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.periodSeconds), - }, - }, - cloudsql: { - enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled), - connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName), - dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort), - serviceAccountJSON: ServiceField.string(values.cloudsql?.serviceAccountJSON ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON), - }, - } - }, -} - -export type WorkerService = SharedServiceParams & { - type: 'worker'; - replicas: ServiceString; - autoscaling: Autoscaling; -} -const WorkerService = { - default: (name: string, porterJson?: PorterJson): WorkerService => ({ - name, - expanded: true, - smartOptimization: true, - cpu: ServiceField.string('187.5', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined), - ram: ServiceField.string('384', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined), - startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run), - type: 'worker', - replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount), - autoscaling: { - enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled), - minReplicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas), - maxReplicas: ServiceField.string('10', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas), - targetCPUUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage), - targetMemoryUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage), - }, - canDelete: porterJson?.apps?.[name] == null, - cloudsql: { - enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled), - connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName), - dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort), - serviceAccountJSON: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON), - }, - }), - serialize: (service: WorkerService) => { - return { - replicaCount: service.replicas.value, - container: { - command: service.startCommand.value, - }, - resources: { - requests: { - cpu: service.cpu.value + 'm', - memory: service.ram.value + 'Mi', - } - }, - autoscaling: { - enabled: service.autoscaling.enabled.value, - minReplicas: service.autoscaling.minReplicas.value, - maxReplicas: service.autoscaling.maxReplicas.value, - targetCPUUtilizationPercentage: service.autoscaling.targetCPUUtilizationPercentage.value, - targetMemoryUtilizationPercentage: service.autoscaling.targetMemoryUtilizationPercentage.value, - }, - cloudsql: { - enabled: service.cloudsql.enabled.value, - connectionName: service.cloudsql.connectionName.value, - dbPort: service.cloudsql.dbPort.value, - serviceAccountJSON: service.cloudsql.serviceAccountJSON.value, - }, - smartOptimization: service.smartOptimization, - } - }, - deserialize: (name: string, values: any, porterJson?: PorterJson): WorkerService => { - return { - name, - expanded: false, - smartOptimization: values.smartOptimization, - cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined), - ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined), - startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run), - type: 'worker', - replicas: ServiceField.string(values.replicaCount ?? '', porterJson?.apps?.[name]?.config?.replicaCount), - autoscaling: { - enabled: ServiceField.boolean(values.autoscaling?.enabled ?? false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled), - minReplicas: ServiceField.string(values.autoscaling?.minReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas), - maxReplicas: ServiceField.string(values.autoscaling?.maxReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas), - targetCPUUtilizationPercentage: ServiceField.string(values.autoscaling?.targetCPUUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage), - targetMemoryUtilizationPercentage: ServiceField.string(values.autoscaling?.targetMemoryUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage), - }, - canDelete: porterJson?.apps?.[name] == null, - cloudsql: { - enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled), - connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName), - dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort), - serviceAccountJSON: ServiceField.string(values.cloudsql?.serviceAccountJSON ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON), - }, - } - }, -} - -export type JobService = SharedServiceParams & { - type: 'job'; - jobsExecuteConcurrently: ServiceBoolean; - cronSchedule: ServiceString; -} -const JobService = { - default: (name: string, porterJson?: PorterJson): JobService => ({ - name, - expanded: true, - smartOptimization: true, - cpu: ServiceField.string('187.', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined), - ram: ServiceField.string('384', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined), - startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run), - type: 'job', - jobsExecuteConcurrently: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.allowConcurrent), - cronSchedule: ServiceField.string('*/10 * * * *', porterJson?.apps?.[name]?.config?.schedule?.value), - canDelete: porterJson?.apps?.[name] == null, - cloudsql: { - enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled), - connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName), - dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort), - serviceAccountJSON: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON), - }, - }), - serialize: (service: JobService) => { - return { - smartOptimization: service.smartOptimization, - allowConcurrent: service.jobsExecuteConcurrently.value, - container: { - command: service.startCommand.value, - }, - resources: { - requests: { - cpu: service.cpu.value + 'm', - memory: service.ram.value + 'Mi', - } - }, - schedule: { - enabled: service.cronSchedule.value ? true : false, - value: service.cronSchedule.value, - }, - paused: true, - cloudsql: { - enabled: service.cloudsql.enabled.value, - connectionName: service.cloudsql.connectionName.value, - dbPort: service.cloudsql.dbPort.value, - serviceAccountJSON: service.cloudsql.serviceAccountJSON.value, - }, - } - }, - deserialize: (name: string, values: any, porterJson?: PorterJson): JobService => { - return { - name, - expanded: false, - smartOptimization: values.smartOptimization, - cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined), - ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined), - startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run), - type: 'job', - jobsExecuteConcurrently: ServiceField.boolean(values.allowConcurrent ?? false, porterJson?.apps?.[name]?.config?.allowConcurrent), - cronSchedule: ServiceField.string(values.schedule?.value ?? '', porterJson?.apps?.[name]?.config?.schedule?.value), - canDelete: porterJson?.apps?.[name] == null, - cloudsql: { - enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled), - connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName), - dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort), - serviceAccountJSON: ServiceField.string(values.cloudsql?.serviceAccountJSON ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON), - }, - } - }, -} - -export type ReleaseService = SharedServiceParams & { - type: 'release'; -}; -const ReleaseService = { - default: (name: string, porterJson?: PorterJson): ReleaseService => ({ - name, - expanded: true, - smartOptimization: true, - cpu: ServiceField.string('187.5', porterJson?.release?.config?.resources?.requests?.cpu ? porterJson?.release?.config?.resources?.requests?.cpu.replace('m', '') : undefined), - ram: ServiceField.string('384', porterJson?.release?.config?.resources?.requests?.memory ? porterJson?.release?.config?.resources?.requests?.memory.replace('Mi', '') : undefined), - startCommand: ServiceField.string('', porterJson?.release?.run), - type: 'release', - canDelete: porterJson?.release == null, - cloudsql: { - enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled), - connectionName: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName), - dbPort: ServiceField.string('5432', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort), - serviceAccountJSON: ServiceField.string('', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON), - }, - }), - - serialize: (service: ReleaseService) => { - return { - smartOptimization: service.smartOptimization, - container: { - command: service.startCommand.value, - }, - resources: { - requests: { - cpu: service.cpu.value + 'm', - memory: service.ram.value + 'Mi', - } - }, - paused: true, // this makes sure the release isn't run immediately. it is flipped when the porter apply runs the release in the GHA - cloudsql: { - enabled: service.cloudsql.enabled.value, - connectionName: service.cloudsql.connectionName.value, - dbPort: service.cloudsql.dbPort.value, - serviceAccountJSON: service.cloudsql.serviceAccountJSON.value, - }, - } - }, - - deserialize: (name: string, values: any, porterJson?: PorterJson): ReleaseService => { - return { - name, - expanded: false, - smartOptimization: values.smartOptimization, - cpu: ServiceField.string(values?.resources?.requests?.cpu?.replace('m', ''), porterJson?.release?.config?.resources?.requests?.cpu ? porterJson?.release?.config?.resources?.requests?.cpu.replace('m', '') : undefined), - ram: ServiceField.string(values?.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.release?.config?.resources?.requests?.memory ? porterJson?.release?.config?.resources?.requests?.memory.replace('Mi', '') : undefined), - startCommand: ServiceField.string(values?.container?.command ?? '', porterJson?.release?.run), - type: 'release', - canDelete: porterJson?.release == null, - cloudsql: { - enabled: ServiceField.boolean(values.cloudsql?.enabled ?? false, porterJson?.apps?.[name]?.config?.cloudsql?.enabled), - connectionName: ServiceField.string(values.cloudsql?.connectionName ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.connectionName), - dbPort: ServiceField.string(values.cloudsql?.dbPort ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.dbPort), - serviceAccountJSON: ServiceField.string(values.cloudsql?.serviceAccountJSON ?? '', porterJson?.apps?.[name]?.config?.cloudsql?.serviceAccountJSON), - }, - } - }, -} - - -const TYPE_TO_SUFFIX: Record = { - 'web': '-web', - 'worker': '-wkr', - 'job': '-job', - 'release': '', -} -const SUFFIX_TO_TYPE: Record = { - '-web': 'web', - '-wkr': 'worker', - '-job': 'job', -} - -export const Service = { - // populates an empty service - default: (name: string, type: ServiceType, porterJson?: PorterJson) => { - switch (type) { - case 'web': - return WebService.default(name, porterJson); - case 'worker': - return WorkerService.default(name, porterJson); - case 'job': - return JobService.default(name, porterJson); - case 'release': - return ReleaseService.default(name, porterJson); - } - }, - - // converts a service to a helm values object - serialize: (service: Service) => { - switch (service.type) { - case 'web': - return WebService.serialize(service); - case 'worker': - return WorkerService.serialize(service); - case 'job': - return JobService.serialize(service); - case 'release': - return ReleaseService.serialize(service); - } - }, - - // converts a helm values object and porter json (from their repo) to a service - deserialize: (helmValues: any, defaultValues: any, porterJson?: PorterJson): Service[] => { - if (defaultValues == null) { - return []; - } - return Object.keys(defaultValues).map((name: string) => { - const suffix = name.slice(-4); - if (suffix in SUFFIX_TO_TYPE) { - const type = SUFFIX_TO_TYPE[suffix]; - const appName = name.slice(0, -4); - const coalescedValues = overrideObjectValues( - defaultValues[name], - helmValues?.[name] ?? {} - ); - switch (type) { - case 'web': - return WebService.deserialize(appName, coalescedValues, porterJson); - case 'worker': - return WorkerService.deserialize(appName, coalescedValues, porterJson); - case 'job': - return JobService.deserialize(appName, coalescedValues, porterJson); - } - } - }).filter((service: Service | undefined): service is Service => service != null) as Service[]; - }, - // TODO: consolidate these - deserializeRelease: (helmValues: any, porterJson?: PorterJson): ReleaseService => { - return ReleaseService.deserialize('pre-deploy', helmValues, porterJson); - }, - - // standard typeguards - isWeb: (service: Service): service is WebService => service.type === 'web', - isWorker: (service: Service): service is WorkerService => service.type === 'worker', - isJob: (service: Service): service is JobService => service.type === 'job', - isRelease: (service: Service): service is ReleaseService => service.type === 'release', - isNonRelease: (service: Service): service is Exclude => service.type !== 'release', - - // required because of https://github.com/helm/helm/issues/9214 - toHelmName: (service: Service): string => { - return service.name + TYPE_TO_SUFFIX[service.type] - }, - - retrieveEnvFromHelmValues: (helmValues: any): KeyValueType[] => { - const firstService = Object.keys(helmValues)[0]; - const env = helmValues[firstService]?.container?.env?.normal; - if (env == null) { - return []; - } - try { - return Object.keys(env).map((key: string) => ({ - key, - value: env[key], - hidden: false, - locked: false, - deleted: false, - })); - } catch (err) { - // TODO: handle error - return []; - } - }, - - retrieveSubdomainFromHelmValues: (services: Service[], helmValues: any): string => { - const webServices = services.filter(Service.isWeb); - if (webServices.length == 0) { - return ""; - } - - let matchedWebCount = 0; - let matchedWebHost = ""; - - for (const web of webServices) { - const values = helmValues[Service.toHelmName(web)]; - if (values == null || values.ingress == null || !values.ingress.enabled) { - continue; - } - if (values.ingress.porter_hosts?.length > 0 || (values.ingress.custom_domain && values.ingress.hosts?.length > 0)) { - if (values.ingress.custom_domain && values.ingress.hosts?.length === 1) { - // if they have a single custom domain, use that - matchedWebHost = values.ingress.hosts[0]; - } else { - // otherwise, use their porter domain - matchedWebHost = values.ingress.porter_hosts[0]; - } - matchedWebCount++; - } - } - - // if multiple web services have a subdomain, return nothing - if (matchedWebCount > 1) { - return ""; - } - - return matchedWebHost; - }, - - prefixSubdomain: (subdomain: string) => { - if (subdomain.startsWith('https://') || subdomain.startsWith('http://')) { - return subdomain; - } - return 'https://' + subdomain; - }, -} - diff --git a/dashboard/src/main/home/sidebar/ProjectButton.tsx b/dashboard/src/main/home/sidebar/ProjectButton.tsx index 8d5d3c56af..ee9d08656f 100644 --- a/dashboard/src/main/home/sidebar/ProjectButton.tsx +++ b/dashboard/src/main/home/sidebar/ProjectButton.tsx @@ -3,7 +3,6 @@ import { withRouter, type RouteComponentProps } from "react-router"; import styled from "styled-components"; import Spacer from "components/porter/Spacer"; -import Tooltip from "components/porter/Tooltip"; import { Context } from "shared/Context"; import { pushFiltered } from "shared/routing"; @@ -59,27 +58,20 @@ const ProjectButton: React.FC = (props) => { )} {user.isPorterUser && currentProject.simplified_view_enabled ? ( - { + (props.projects.length > 1 || user.isPorterUser) && + setShowModal(true); + }} > - { - (props.projects.length > 1 || user.isPorterUser) && - setShowModal(true); - }} - > - - - {currentProject.name[0].toUpperCase()} - - {currentProject.name} - - + + + {currentProject.name[0].toUpperCase()} + + {currentProject.name} + ) : ( Date: Thu, 2 May 2024 14:42:47 -0400 Subject: [PATCH 2/8] more dependencies --- .../src/components/porter/InputSlider.tsx | 18 +- .../events/cards/ServiceStatusDetail.tsx | 4 +- .../new-app-flow/tabs/CustomDomains.tsx | 102 -- .../tabs/IngressCustomAnnotations.tsx | 116 -- .../new-app-flow/tabs/JobTabs.tsx | 325 ---- .../new-app-flow/tabs/NodeInfoModal.tsx | 35 - .../new-app-flow/tabs/ReleaseTabs.tsx | 275 --- .../new-app-flow/tabs/SmartOptModal.tsx | 50 - .../new-app-flow/tabs/WebTabs.tsx | 914 ---------- .../new-app-flow/tabs/WorkerTabs.tsx | 402 ----- .../app-dashboard/new-app-flow/tabs/utils.ts | 88 - .../tabs/IntelligentSlider.tsx | 4 - .../services-settings/tabs/Resources.tsx | 7 +- .../env-groups/CreateEnvGroup.tsx | 459 ----- .../cluster-dashboard/env-groups/EnvGroup.tsx | 211 --- .../env-groups/EnvGroupArrayStacks.tsx | 404 ----- .../env-groups/EnvGroupDashboard.tsx | 273 --- .../env-groups/EnvGroupList.tsx | 163 -- .../env-groups/ExpandedEnvGroup.tsx | 1562 ----------------- .../env-groups/ExpandedEnvGroupDashboard.tsx | 198 --- 20 files changed, 9 insertions(+), 5601 deletions(-) delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/tabs/CustomDomains.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/tabs/IngressCustomAnnotations.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/tabs/JobTabs.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/tabs/ReleaseTabs.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/tabs/SmartOptModal.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WebTabs.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WorkerTabs.tsx delete mode 100644 dashboard/src/main/home/app-dashboard/new-app-flow/tabs/utils.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx diff --git a/dashboard/src/components/porter/InputSlider.tsx b/dashboard/src/components/porter/InputSlider.tsx index 086a9cbf29..9f6a290f0e 100644 --- a/dashboard/src/components/porter/InputSlider.tsx +++ b/dashboard/src/components/porter/InputSlider.tsx @@ -1,13 +1,11 @@ import React, { useEffect, useState } from 'react'; -import Slider, { Mark } from '@material-ui/core/Slider'; +import Slider, { type Mark } from '@material-ui/core/Slider'; import Tooltip from '@material-ui/core/Tooltip'; import Typography from '@material-ui/core/Typography'; import styled from 'styled-components'; import { withStyles } from '@material-ui/core/styles'; import Text from './Text'; import Spacer from './Spacer'; -import SmartOptModal from 'main/home/app-dashboard/new-app-flow/tabs/SmartOptModal'; -import NodeInfoModal from 'main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal'; type InputSliderProps = { label?: string; @@ -68,9 +66,9 @@ const InputSlider: React.FC = ({ label: max.toString(), }, ]; - var isExceedingLimit = false; - var displayOptimalText = false; - //Optimal Marks only give useful information to user if they are using more than 2 nodes + let isExceedingLimit = false; + let displayOptimalText = false; + // Optimal Marks only give useful information to user if they are using more than 2 nodes // if (optimal != 0 && nodeCount && nodeCount > 2) { // marks.push({ // value: optimal, @@ -127,10 +125,6 @@ const InputSlider: React.FC = ({ > help_outline } - {showNeedHelpModal && - } {isExceedingLimit && <>} @@ -168,7 +162,7 @@ const InputSlider: React.FC = ({ valueLabelDisplay={smartLimit && Number(value) > smartLimit ? "off" : "auto"} disabled={disabled} marks={marks} - step={(step ? step : 1)} + step={(step || 1)} style={{ color: disabled ? "gray" : color, }} @@ -257,7 +251,7 @@ const MaxedOutToolTip = withStyles(theme => ({ const StyledSlider = withStyles({ root: { - height: '8px', //height of the track + height: '8px', // height of the track }, mark: { backgroundColor: '#fff', // mark color diff --git a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/ServiceStatusDetail.tsx b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/ServiceStatusDetail.tsx index c837188702..a9055c5ea4 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/ServiceStatusDetail.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/cards/ServiceStatusDetail.tsx @@ -8,8 +8,8 @@ import Spacer from "components/porter/Spacer"; import Tag from "components/porter/Tag"; import Text from "components/porter/Text"; import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext"; -import { Service } from "main/home/app-dashboard/new-app-flow/serviceTypes"; import { isClientServiceNotification } from "lib/porter-apps/notification"; +import { prefixSubdomain } from "lib/porter-apps/services"; import alert from "assets/alert-warning.svg"; import metrics from "assets/bar-group-03.svg"; @@ -168,7 +168,7 @@ const ServiceStatusDetail: React.FC = ({ diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/CustomDomains.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/CustomDomains.tsx deleted file mode 100644 index 09b01c51db..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/CustomDomains.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { ServiceArray, ServiceString } from '../serviceTypes'; -import Button from 'components/porter/Button'; -import styled from 'styled-components'; -import Input from 'components/porter/Input'; -import Spacer from 'components/porter/Spacer'; - -interface Props { - customDomains: ServiceArray; - onChange: (customDomains: ServiceArray) => void; -} - -const CustomDomains: React.FC = ({ customDomains, onChange }) => { - const renderInputs = () => { - return customDomains.map((customDomain, i) => { - return ( - <> - - { - const newCustomDomains = [...customDomains]; - newCustomDomains[i] = { readOnly: false, value: e }; - onChange(newCustomDomains); - }} - disabled={customDomain.readOnly} - width="275px" - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - { - //remove customDomain at the index - const newCustomDomains = [...customDomains]; - newCustomDomains.splice(i, 1); - onChange(newCustomDomains); - }} - > - cancel - - - - - ); - }); - }; - - return ( - - {customDomains.length !== 0 && - <> - {renderInputs()} - - - } - - - ) -}; - -export default CustomDomains; - -const CustomDomainsContainer = styled.div` -`; - -const AnnotationContainer = styled.div` - display: flex; - align-items: center; - gap: 5px; -` - -const DeleteButton = styled.div` - width: 15px; - height: 15px; - display: flex; - align-items: center; - margin-left: 8px; - margin-top: -3px; - justify-content: center; - - > i { - font-size: 17px; - color: #ffffff44; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - :hover { - color: #ffffff88; - } - } -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/IngressCustomAnnotations.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/IngressCustomAnnotations.tsx deleted file mode 100644 index c7e8c1111c..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/IngressCustomAnnotations.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import { ServiceKeyValueArray, ServiceString } from '../serviceTypes'; -import Button from 'components/porter/Button'; -import styled from 'styled-components'; -import Input from 'components/porter/Input'; -import Spacer from 'components/porter/Spacer'; - -interface Props { - annotations: ServiceKeyValueArray; - onChange: (annotations: ServiceKeyValueArray) => void; -} - -const IngressCustomAnnotations: React.FC = ({ annotations, onChange }) => { - const renderInputs = () => { - return annotations.map(({ key: annotationKey, value: annotationValue }, i) => { - return ( - <> - - { - const newAnnotations = [...annotations]; - newAnnotations[i].key = e; - onChange(newAnnotations); - }} - disabled={annotationValue.readOnly} - width="275px" - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - { - const newAnnotations = [...annotations]; - newAnnotations[i].value = { readOnly: false, value: e }; - onChange(newAnnotations); - }} - disabled={annotationValue.readOnly} - width="275px" - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - { - //remove annotation at the index - const newAnnotations = [...annotations]; - newAnnotations.splice(i, 1); - onChange(newAnnotations); - }} - > - cancel - - - - - ); - }); - }; - - return ( - - {annotations.length !== 0 && - <> - {renderInputs()} - - - } - - - ) -}; - -export default IngressCustomAnnotations; - -const IngressCustomAnnotationsContainer = styled.div` -`; - -const AnnotationContainer = styled.div` - display: flex; - align-items: center; - gap: 5px; -` - -const DeleteButton = styled.div` - width: 15px; - height: 15px; - display: flex; - align-items: center; - margin-left: 8px; - margin-top: -3px; - justify-content: center; - - > i { - font-size: 17px; - color: #ffffff44; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - :hover { - color: #ffffff88; - } - } -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/JobTabs.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/JobTabs.tsx deleted file mode 100644 index 837032dc0e..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/JobTabs.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import Input from "components/porter/Input"; -import React, { useContext, useState } from "react" -import Text from "components/porter/Text"; -import Spacer from "components/porter/Spacer"; -import TabSelector from "components/TabSelector"; -import Checkbox from "components/porter/Checkbox"; -import { JobService } from "../serviceTypes"; -import AnimateHeight, { Height } from "react-animate-height"; -import cronstrue from 'cronstrue'; -import Link from "components/porter/Link"; -import { Context } from "shared/Context"; -import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_ALLOCATION_RAM, UPPER_BOUND_SMART } from "./utils"; -import InputSlider from "components/porter/InputSlider"; -import SmartOptModal from "./SmartOptModal"; -import { Switch } from "@material-ui/core"; -import styled from "styled-components"; - -interface Props { - service: JobService; - editService: (service: JobService) => void; - setHeight: (height: Height) => void; - maxRAM: number; - maxCPU: number; - nodeCount: number; -} - -const JobTabs: React.FC = ({ - service, - editService, - setHeight, - maxRAM, - maxCPU, - nodeCount, -}) => { - const [currentTab, setCurrentTab] = React.useState('main'); - const { currentCluster } = useContext(Context); - const [showNeedHelpModal, setShowNeedHelpModal] = useState(false); - const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART - const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART - const handleSwitch = (event: React.ChangeEvent) => { - if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) { - - editService({ - ...service, - cpu: { - readOnly: false, - value: (smartLimitCPU * MILI_TO_CORE).toString() - }, - ram: { - readOnly: false, - value: (smartLimitRAM * MIB_TO_GIB).toString() - }, - smartOptimization: !service.smartOptimization - }) - } - else { - editService({ - ...service, - smartOptimization: !service.smartOptimization - }) - } - - }; - const getScheduleDescription = () => { - try { - return This job runs: {cronstrue.toString(service.cronSchedule.value)}; - } catch (err) { - return - Invalid cron schedule.{" "} - - Need help? - - ; - } - } - - const renderMain = () => { - setHeight(276); - return ( - <> - - { editService({ ...service, startCommand: { readOnly: false, value: e } }) }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - /> - - { editService({ ...service, cronSchedule: { readOnly: false, value: e } }) }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - /> - - {getScheduleDescription()} - - ) - }; - - const renderResources = () => { - setHeight(316); - return ( - <> - -
- { - setShowNeedHelpModal(true) - }} - > - help_outline - - Smart Optimization - -
- {showNeedHelpModal && - } - <> - { - service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) : - editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } }); - }} - step={0.1} - disabled={false} - disabledTooltip={"You may only edit this field in your porter.yaml."} /> - - - - { - service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) : - editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } }); - }} - - disabled={service.ram.readOnly} - step={0.1} - disabledTooltip={"You may only edit this field in your porter.yaml."} /> - - - ) - }; - - const renderDatabase = () => { - setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED) - return ( - <> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - enabled: { - readOnly: false, - value: !service.cloudsql.enabled.value, - }, - }, - }); - }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Securely connect to Google Cloud SQL - - - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - connectionName: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - dbPort: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - serviceAccountJSON: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - - ); - } - - const renderAdvanced = () => { - setHeight(118); - return ( - <> - - { editService({ ...service, jobsExecuteConcurrently: { readOnly: false, value: !service.jobsExecuteConcurrently.value } }) }} - disabled={service.jobsExecuteConcurrently.readOnly} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Allow jobs to execute concurrently - - - ); - }; - - return ( - <> - - {currentTab === 'main' && renderMain()} - {currentTab === 'resources' && renderResources()} - {currentTab === 'advanced' && renderAdvanced()} - {currentTab === "database" && renderDatabase()} - - ) -} - -export default JobTabs; - -const StyledIcon = styled.i` - cursor: pointer; - font-size: 16px; - margin-right : 5px; - &:hover { - color: #666; - } -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal.tsx deleted file mode 100644 index cb85069d07..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal.tsx +++ /dev/null @@ -1,35 +0,0 @@ - -import React, { useEffect, useRef, useState } from "react"; -import Modal from "components/porter/Modal"; - -import Text from "components/porter/Text"; - -import Spacer from "components/porter/Spacer"; -import Step from "components/porter/Step"; -import Link from "components/porter/Link"; - -type Props = { - setModalVisible: (x: boolean) => void; -}; - -const NodeInfoModal: React.FC = ({ - setModalVisible, -}) => { - return ( - setModalVisible(false)} width={"800px"}> - Resource Optimization on Porter - - - Using the recommended marks ensures that your service runs cost-efficiently on Porter. - - - - - For more information about Kubernetes resource management, visit our docs. - - - - - ); -}; -export default NodeInfoModal; diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/ReleaseTabs.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/ReleaseTabs.tsx deleted file mode 100644 index c46291016f..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/ReleaseTabs.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import Input from "components/porter/Input"; -import React, { useContext, useState } from "react" -import Spacer from "components/porter/Spacer"; -import TabSelector from "components/TabSelector"; -import { ReleaseService } from "../serviceTypes"; -import AnimateHeight, { Height } from "react-animate-height"; -import { Context } from "shared/Context"; -import Checkbox from "components/porter/Checkbox"; -import Text from "components/porter/Text"; -import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_ALLOCATION_RAM, UPPER_BOUND_SMART } from "./utils"; -import InputSlider from "components/porter/InputSlider"; -import SmartOptModal from "./SmartOptModal"; -import { Switch } from "@material-ui/core"; -import styled from "styled-components"; - -interface Props { - service: ReleaseService; - editService: (service: ReleaseService) => void; - setHeight: (height: Height) => void; - maxRAM: number; - maxCPU: number; - nodeCount?: number; -} - -const ReleaseTabs: React.FC = ({ - service, - editService, - setHeight, - maxRAM, - maxCPU, - nodeCount, -}) => { - const [currentTab, setCurrentTab] = React.useState('main'); - const { currentCluster } = useContext(Context); - const [showNeedHelpModal, setShowNeedHelpModal] = useState(false); - const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART - const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART - const handleSwitch = (event: React.ChangeEvent) => { - if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) { - - editService({ - ...service, - cpu: { - readOnly: false, - value: (smartLimitCPU * MILI_TO_CORE).toString() - }, - ram: { - readOnly: false, - value: (smartLimitRAM * MIB_TO_GIB).toString() - }, - smartOptimization: !service.smartOptimization - }) - } - else { - editService({ - ...service, - smartOptimization: !service.smartOptimization - }) - } - - }; - const renderMain = () => { - setHeight(159); - return ( - <> - - { editService({ ...service, startCommand: { readOnly: false, value: e } }) }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - /> - - ) - }; - - const renderResources = () => { - setHeight(316); - return ( - <> - -
- { - setShowNeedHelpModal(true) - }} - > - help_outline - - Smart Optimization - -
- {showNeedHelpModal && - } - <> - { - service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) : - editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } }); - }} - step={0.1} - disabled={false} - disabledTooltip={"You may only edit this field in your porter.yaml."} /> - - - - { - service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) : - editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } }); - }} - - disabled={service.ram.readOnly} - step={0.1} - disabledTooltip={"You may only edit this field in your porter.yaml."} /> - - - ) - }; - - const renderDatabase = () => { - setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED) - return ( - <> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - enabled: { - readOnly: false, - value: !service.cloudsql.enabled.value, - }, - }, - }); - }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Securely connect to Google Cloud SQL - - - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - connectionName: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - dbPort: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - serviceAccountJSON: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - - ); - } - - - return ( - <> - - {currentTab === 'main' && renderMain()} - {currentTab === 'resources' && renderResources()} - {currentTab === "database" && renderDatabase()} - - ) -} - -export default ReleaseTabs; - -const StyledIcon = styled.i` - cursor: pointer; - font-size: 16px; - margin-right : 5px; - &:hover { - color: #666; - } -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/SmartOptModal.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/SmartOptModal.tsx deleted file mode 100644 index 401fa72192..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/SmartOptModal.tsx +++ /dev/null @@ -1,50 +0,0 @@ - -import React from "react"; -import Modal from "components/porter/Modal"; - -import Text from "components/porter/Text"; - -import Spacer from "components/porter/Spacer"; -import Step from "components/porter/Step"; -import Link from "components/porter/Link"; - -type Props = { - setModalVisible: (x: boolean) => void; -}; - -const SmartOptModal: React.FC = ({ - setModalVisible, -}) => { - return ( - setModalVisible(false)} width={"800px"}> - Resource Optimization on Porter - - - Smart Optimization ensures that your app runs smoothly while minimizing costs. Smart Optimization performs the following: - - - - Maintains a consistent ratio between RAM and CPU based on the instance type. - - - - Enforces limits so that your app does not consume resources beyond the instance type's limits. - - - Determines an optimal resource threshold to save cost. - - - - Turning off Smart Optimization will allow you to specify your own resource values. This is not recommended unless you are familiar with Kubernetes resource management. - - - - - For more information about Kubernetes resource management, visit our docs. - - - - - ); -}; -export default SmartOptModal; diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WebTabs.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WebTabs.tsx deleted file mode 100644 index 4f8c6593bc..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WebTabs.tsx +++ /dev/null @@ -1,914 +0,0 @@ -import Input from "components/porter/Input"; -import React, { useContext, useEffect, useState } from "react"; -import Text from "components/porter/Text"; -import Spacer from "components/porter/Spacer"; -import TabSelector from "components/TabSelector"; -import Checkbox from "components/porter/Checkbox"; -import { Service, WebService } from "../serviceTypes"; -import AnimateHeight, { Height } from "react-animate-height"; -import { Context } from "shared/Context"; -import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING, AWS_INSTANCE_LIMITS, MILI_TO_CORE, MIB_TO_GIB, UPPER_BOUND_SMART, UPPER_BOUND_REG, RESOURCE_ALLOCATION, RESOURCE_ALLOCATION_CPU, RESOURCE_ALLOCATION_RAM } from "./utils"; -import IngressCustomAnnotations from "./IngressCustomAnnotations"; -import CustomDomains from "./CustomDomains"; -import InputSlider from "components/porter/InputSlider"; -import api from "shared/api"; -import Toggle from "components/porter/Toggle"; -import Container from "components/porter/Container"; -import { FormControlLabel, Switch } from "@material-ui/core"; -import DialToggle from "components/porter/DialToggle"; -import { max } from "lodash"; -import styled from "styled-components"; -import Step from "components/porter/Step"; -import Link from "components/porter/Link"; -import Modal from "components/porter/Modal"; -import SmartOptModal from "./SmartOptModal"; -interface Props { - service: WebService; - editService: (service: WebService) => void; - setHeight: (height: Height) => void; - chart?: any; - maxRAM: number; - maxCPU: number; - nodeCount: number; -} - -const NETWORKING_HEIGHT_WITHOUT_INGRESS = 204; -const NETWORKING_HEIGHT_WITH_INGRESS = 395; -const ADVANCED_BASE_HEIGHT = 215; -const PROBE_INPUTS_HEIGHT = 230; -const CUSTOM_ANNOTATION_HEIGHT = 44; - -const WebTabs: React.FC = ({ - service, - editService, - setHeight, - maxRAM, - maxCPU, - nodeCount, -}) => { - const [currentTab, setCurrentTab] = React.useState("main"); - const { currentCluster } = useContext(Context); - const [showNeedHelpModal, setShowNeedHelpModal] = useState(false); - const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART - const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART - - const handleSwitch = (event: React.ChangeEvent) => { - if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) { - - editService({ - ...service, - cpu: { - readOnly: false, - value: (smartLimitCPU * MILI_TO_CORE).toString() - }, - ram: { - readOnly: false, - value: (smartLimitRAM * MIB_TO_GIB).toString() - }, - smartOptimization: !service.smartOptimization - }) - } - else { - editService({ - ...service, - smartOptimization: !service.smartOptimization - }) - } - - }; - const renderMain = () => { - setHeight(159); - return ( - <> - - - // Start command - // {!service.startCommand.readOnly && service.startCommand.value.includes("/cnb/lifecycle/launcher") && - // - //  (?) - // - // } - // } - placeholder="ex: sh start.sh" - value={service.startCommand.value} - width="300px" - disabled={service.startCommand.readOnly} - setValue={(e) => { - editService({ - ...service, - startCommand: { readOnly: false, value: e }, - }); - }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - /> - - ); - }; - - const renderNetworking = () => { - setHeight(service.ingress.enabled.value ? calculateNetworkingHeight() : NETWORKING_HEIGHT_WITHOUT_INGRESS) - return ( - <> - - { - editService({ ...service, port: { readOnly: false, value: e } }); - }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - /> - - { - editService({ - ...service, - ingress: { - ...service.ingress, - enabled: { - readOnly: false, - value: !service.ingress.enabled.value, - }, - }, - }); - }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Expose to external traffic - - - - {getApplicationURLText()} - - - Custom domains - -  (?) - - - - { - editService({ ...service, ingress: { ...service.ingress, customDomains: customDomains } }); - setHeight(calculateNetworkingHeight()); - }} - /> - - - Ingress Custom Annotations - -  (?) - - - - { - editService({ ...service, ingress: { ...service.ingress, annotations: annotations } }); - setHeight(calculateNetworkingHeight()); - }} - /> - - - ); - } - - const renderDatabase = () => { - setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED) - return ( - <> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - enabled: { - readOnly: false, - value: !service.cloudsql.enabled.value, - }, - }, - }); - }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Securely connect to Google Cloud SQL - - - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - connectionName: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - dbPort: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - serviceAccountJSON: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - - ); - } - - const renderResources = () => { - setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITH_AUTOSCALING : RESOURCE_HEIGHT_WITHOUT_AUTOSCALING) - return ( - <> - -
- { - setShowNeedHelpModal(true) - }} - > - help_outline - - Smart Optimization - -
- {showNeedHelpModal && - } - <> - { - service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) : - editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } }); - }} - step={0.1} - disabled={false} - disabledTooltip={"You may only edit this field in your porter.yaml."} /> - - - - { - service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) : - editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } }); - }} - - disabled={service.ram.readOnly} - step={0.1} - disabledTooltip={"You may only edit this field in your porter.yaml."} /> - - - - { - editService({ - ...service, - replicas: { readOnly: false, value: e }, - }); - }} - disabledTooltip={service.replicas.readOnly - ? "You may only edit this field in your porter.yaml." - : "Disable autoscaling to specify replicas."} /> { - editService({ - ...service, - autoscaling: { - ...service.autoscaling, - enabled: { - readOnly: false, - value: !service.autoscaling.enabled.value, - }, - }, - }); - setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITHOUT_AUTOSCALING : RESOURCE_HEIGHT_WITH_AUTOSCALING); - }} - disabled={service.autoscaling.enabled.readOnly} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Enable autoscaling (overrides replicas) - - - { - editService({ - ...service, - autoscaling: { - ...service.autoscaling, - minReplicas: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={service.autoscaling.minReplicas.readOnly - ? "You may only edit this field in your porter.yaml." - : "Enable autoscaling to specify min replicas."} /> - - { - editService({ - ...service, - autoscaling: { - ...service.autoscaling, - maxReplicas: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={service.autoscaling.maxReplicas.readOnly - ? "You may only edit this field in your porter.yaml." - : "Enable autoscaling to specify max replicas."} /> - - { - editService({ - ...service, - autoscaling: { - ...service.autoscaling, - targetCPUUtilizationPercentage: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={service.autoscaling.targetCPUUtilizationPercentage.readOnly - ? "You may only edit this field in your porter.yaml." - : "Enable autoscaling to specify target CPU utilization."} /> - - { - editService({ - ...service, - autoscaling: { - ...service.autoscaling, - targetMemoryUtilizationPercentage: { - readOnly: false, - value: e, - }, - }, - }); - }} - disabledTooltip={service.autoscaling.targetMemoryUtilizationPercentage.readOnly - ? "You may only edit this field in your porter.yaml." - : "Enable autoscaling to specify target RAM utilization."} /> - - ); - }; - - const calculateHealthHeight = () => { - let height = ADVANCED_BASE_HEIGHT; - if (service.health.livenessProbe.enabled.value) { - height += PROBE_INPUTS_HEIGHT; - } - if (service.health.startupProbe.enabled.value) { - height += PROBE_INPUTS_HEIGHT; - } - if (service.health.readinessProbe.enabled.value) { - height += PROBE_INPUTS_HEIGHT; - } - return height; - }; - - const calculateNetworkingHeight = () => { - return NETWORKING_HEIGHT_WITH_INGRESS + (service.ingress.annotations.length * CUSTOM_ANNOTATION_HEIGHT) + (service.ingress.customDomains.length * CUSTOM_ANNOTATION_HEIGHT); - } - - const renderAdvanced = () => { - setHeight(calculateHealthHeight()); - return ( - <> - - - <> - Health checks - -  (?) - - - - - { - editService({ - ...service, - health: { - ...service.health, - livenessProbe: { - ...service.health.livenessProbe, - enabled: { - readOnly: false, - value: !service.health.livenessProbe.enabled.value, - }, - }, - }, - }); - setHeight(calculateHealthHeight() + (service.health.livenessProbe.enabled.value ? -PROBE_INPUTS_HEIGHT : PROBE_INPUTS_HEIGHT)); - }} - disabled={service.health.livenessProbe.enabled.readOnly} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Enable Liveness Probe - - - - { - editService({ - ...service, - health: { - ...service.health, - livenessProbe: { - ...service.health.livenessProbe, - path: { - readOnly: false, - value: e, - }, - }, - }, - }); - }} - disabled={service.health.livenessProbe.path.readOnly} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - health: { - ...service.health, - livenessProbe: { - ...service.health.livenessProbe, - failureThreshold: { - readOnly: false, - value: e, - }, - }, - }, - }); - }} - disabled={ - service.health.livenessProbe.failureThreshold.readOnly - } - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - health: { - ...service.health, - livenessProbe: { - ...service.health.livenessProbe, - periodSeconds: { - readOnly: false, - value: e, - }, - }, - }, - }); - }} - disabled={service.health.livenessProbe.periodSeconds.readOnly} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - - - { - editService({ - ...service, - health: { - ...service.health, - startupProbe: { - ...service.health.startupProbe, - enabled: { - readOnly: false, - value: !service.health.startupProbe.enabled.value, - }, - }, - }, - }); - setHeight(calculateHealthHeight() + (service.health.startupProbe.enabled.value ? -PROBE_INPUTS_HEIGHT : PROBE_INPUTS_HEIGHT)); - }} - disabled={service.health.startupProbe.enabled.readOnly} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Enable Start Up Probe - - - - { - editService({ - ...service, - health: { - ...service.health, - startupProbe: { - ...service.health.startupProbe, - path: { - readOnly: false, - value: e, - }, - }, - }, - }); - }} - disabled={service.health.startupProbe.path.readOnly} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - health: { - ...service.health, - startupProbe: { - ...service.health.startupProbe, - failureThreshold: { - readOnly: false, - value: e, - }, - }, - }, - }); - }} - disabled={service.health.startupProbe.failureThreshold.readOnly} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - health: { - ...service.health, - startupProbe: { - ...service.health.startupProbe, - periodSeconds: { - readOnly: false, - value: e, - }, - }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - - - { - editService({ - ...service, - health: { - ...service.health, - readinessProbe: { - ...service.health.readinessProbe, - enabled: { - readOnly: false, - value: !service.health.readinessProbe.enabled.value, - }, - }, - }, - }); - setHeight(calculateHealthHeight() + (service.health.readinessProbe.enabled.value ? -PROBE_INPUTS_HEIGHT : PROBE_INPUTS_HEIGHT)); - }} - disabled={service.health.readinessProbe.enabled.readOnly} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Enable Readiness Probe - - - - { - editService({ - ...service, - health: { - ...service.health, - readinessProbe: { - ...service.health.readinessProbe, - path: { - readOnly: false, - value: e, - }, - }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - health: { - ...service.health, - readinessProbe: { - ...service.health.readinessProbe, - failureThreshold: { - readOnly: false, - value: e, - }, - }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - health: { - ...service.health, - readinessProbe: { - ...service.health.readinessProbe, - initialDelaySeconds: { - readOnly: false, - value: e, - }, - }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - - - ); - }; - - const getApplicationURLText = () => { - if (service.ingress.hosts.length !== 0) { - return ( - {`Application URL${service.ingress.hosts.length === 1 ? "" : "s"}: `} - {service.ingress.hosts.map((host, i) => { - return ( - - {host.value} - {i !== service.ingress.hosts.length - 1 && ", "} - - ) - })} - - ) - } else if (service.ingress.porterHosts.value !== "") { - return ( - Application URL:{" "} - - {service.ingress.porterHosts.value} - - - ) - } else if (service.ingress.customDomains.length !== 0) { - return ( - - {`Application URL${service.ingress.customDomains.length === 1 ? "" : "s"}: Your application will be available at the specified custom domain${service.ingress.customDomains.length === 1 ? "" : "s"} on next deploy.`} - - ) - } else { - return ( - - Application URL: Not generated yet. Porter will generate a URL for you on next deploy. - - ) - } - } - - return ( - <> - - {currentTab === "main" && renderMain()} - {currentTab === "resources" && renderResources()} - {currentTab === "networking" && renderNetworking()} - {currentTab === "database" && renderDatabase()} - {currentTab === "advanced" && renderAdvanced()} - - ); -}; - -export default WebTabs; - -const StyledIcon = styled.i` - cursor: pointer; - font-size: 16px; - margin-right : 5px; - &:hover { - color: #666; - } -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WorkerTabs.tsx b/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WorkerTabs.tsx deleted file mode 100644 index bdc894bbcd..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WorkerTabs.tsx +++ /dev/null @@ -1,402 +0,0 @@ -import Input from "components/porter/Input"; -import React, { useContext, useState } from "react" -import Text from "components/porter/Text"; -import Spacer from "components/porter/Spacer"; -import TabSelector from "components/TabSelector"; -import Checkbox from "components/porter/Checkbox"; -import { WorkerService } from "../serviceTypes"; -import AnimateHeight, { Height } from "react-animate-height"; -import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_ALLOCATION_RAM, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING, UPPER_BOUND_SMART } from "./utils"; -import { Context } from "shared/Context"; -import InputSlider from "components/porter/InputSlider"; -import styled from "styled-components"; -import { Switch } from "@material-ui/core"; -import SmartOptModal from "./SmartOptModal"; - -interface Props { - service: WorkerService; - editService: (service: WorkerService) => void; - setHeight: (height: Height) => void; - maxRAM: number; - maxCPU: number; - nodeCount: number; -} - -const WorkerTabs: React.FC = ({ - service, - editService, - setHeight, - maxCPU, - maxRAM, - nodeCount, -}) => { - const [currentTab, setCurrentTab] = React.useState('main'); - const { currentCluster } = useContext(Context); - const [showNeedHelpModal, setShowNeedHelpModal] = useState(false); - const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART - const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART - const handleSwitch = (event: React.ChangeEvent) => { - if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) { - - editService({ - ...service, - cpu: { - readOnly: false, - value: (smartLimitCPU * MILI_TO_CORE).toString() - }, - ram: { - readOnly: false, - value: (smartLimitRAM * MIB_TO_GIB).toString() - }, - smartOptimization: !service.smartOptimization - }) - } - else { - editService({ - ...service, - smartOptimization: !service.smartOptimization - }) - } - - }; - const renderMain = () => { - setHeight(159); - return ( - <> - - { editService({ ...service, startCommand: { readOnly: false, value: e } }) }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - /> - - ) - }; - - const renderResources = () => { - setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITH_AUTOSCALING : RESOURCE_HEIGHT_WITHOUT_AUTOSCALING) - return ( - <> - -
- { - setShowNeedHelpModal(true) - }} - > - help_outline - - Smart Optimization - -
- {showNeedHelpModal && - } - <> - { - service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) : - editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } }); - }} - step={0.1} - disabled={false} - disabledTooltip={"You may only edit this field in your porter.yaml."} /> - - - - { - service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) : - editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } }); - }} - - disabled={service.ram.readOnly} - step={0.1} - disabledTooltip={"You may only edit this field in your porter.yaml."} /> - - - { editService({ ...service, replicas: { readOnly: false, value: e } }) }} - disabledTooltip={service.replicas.readOnly ? "You may only edit this field in your porter.yaml." : "Disable autoscaling to specify replicas."} - /> - - { editService({ ...service, autoscaling: { ...service.autoscaling, enabled: { readOnly: false, value: !service.autoscaling.enabled.value } } }) }} - disabled={service.autoscaling.enabled.readOnly} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Enable autoscaling (overrides replicas) - - - - { - editService({ - ...service, - autoscaling: { - ...service.autoscaling, - minReplicas: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - service.autoscaling.minReplicas.readOnly - ? "You may only edit this field in your porter.yaml." - : "Enable autoscaling to specify min replicas." - } - /> - - { - editService({ - ...service, - autoscaling: { - ...service.autoscaling, - maxReplicas: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - service.autoscaling.maxReplicas.readOnly - ? "You may only edit this field in your porter.yaml." - : "Enable autoscaling to specify max replicas." - } - /> - - { - editService({ - ...service, - autoscaling: { - ...service.autoscaling, - targetCPUUtilizationPercentage: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - service.autoscaling.targetCPUUtilizationPercentage.readOnly - ? "You may only edit this field in your porter.yaml." - : "Enable autoscaling to specify target CPU utilization." - } - /> - - { - editService({ - ...service, - autoscaling: { - ...service.autoscaling, - targetMemoryUtilizationPercentage: { - readOnly: false, - value: e, - }, - }, - }); - }} - disabledTooltip={ - service.autoscaling.targetMemoryUtilizationPercentage.readOnly - ? "You may only edit this field in your porter.yaml." - : "Enable autoscaling to specify target RAM utilization." - } - /> - - - ) - }; - - const renderDatabase = () => { - setHeight(service.cloudsql.enabled.value ? DATABASE_HEIGHT_ENABLED : DATABASE_HEIGHT_DISABLED) - return ( - <> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - enabled: { - readOnly: false, - value: !service.cloudsql.enabled.value, - }, - }, - }); - }} - disabledTooltip={"You may only edit this field in your porter.yaml."} - > - Securely connect to Google Cloud SQL - - - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - connectionName: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - dbPort: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - { - editService({ - ...service, - cloudsql: { - ...service.cloudsql, - serviceAccountJSON: { readOnly: false, value: e }, - }, - }); - }} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - /> - - - ); - } - - return ( - <> - - {currentTab === 'main' && renderMain()} - {currentTab === 'resources' && renderResources()} - {currentTab === 'database' && renderDatabase()} - - ) -} - -export default WorkerTabs; - -const StyledIcon = styled.i` - cursor: pointer; - font-size: 16px; - margin-right : 5px; - &:hover { - color: #666; - } -`; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/utils.ts b/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/utils.ts deleted file mode 100644 index 2f61fd23e7..0000000000 --- a/dashboard/src/main/home/app-dashboard/new-app-flow/tabs/utils.ts +++ /dev/null @@ -1,88 +0,0 @@ -export const DATABASE_HEIGHT_ENABLED = 374; -export const DATABASE_HEIGHT_DISABLED = 119; -export const RESOURCE_HEIGHT_WITHOUT_AUTOSCALING = 446; -export const RESOURCE_HEIGHT_WITH_AUTOSCALING = 833; -export const MIB_TO_GIB = 1024; -export const MILI_TO_CORE = 1000; -interface InstanceDetails { - vCPU: number; - "RAM": number; -} - -interface InstanceTypes { - [key: string]: { - [size: string]: InstanceDetails; - }; -} - -export const AWS_INSTANCE_LIMITS: InstanceTypes = { - "t3a": { - "nano": { "vCPU": 2, "RAM": 0.5 }, - "micro": { "vCPU": 2, "RAM": 1 }, - "small": { "vCPU": 2, "RAM": 2 }, - "medium": { "vCPU": 2, "RAM": 4 }, - "large": { "vCPU": 2, "RAM": 8 }, - "xlarge": { "vCPU": 4, "RAM": 16 }, - "2xlarge": { "vCPU": 8, "RAM": 32 } - }, - "t3": { - "nano": { "vCPU": 2, "RAM": 0.5 }, - "micro": { "vCPU": 2, "RAM": 1 }, - "small": { "vCPU": 2, "RAM": 2 }, - "medium": { "vCPU": 2, "RAM": 4 }, - "large": { "vCPU": 2, "RAM": 8 }, - "xlarge": { "vCPU": 4, "RAM": 16 }, - "2xlarge": { "vCPU": 8, "RAM": 32 } - }, - "t2": { - "nano": { "vCPU": 1, "RAM": 0.5 }, - "micro": { "vCPU": 1, "RAM": 1 }, - "small": { "vCPU": 1, "RAM": 2 }, - "medium": { "vCPU": 2, "RAM": 4 }, - "large": { "vCPU": 2, "RAM": 8 }, - "xlarge": { "vCPU": 4, "RAM": 16 }, - "2xlarge": { "vCPU": 8, "RAM": 32 } - }, - "c6i": { - "large": { "vCPU": 2, "RAM": 4 }, - "xlarge": { "vCPU": 4, "RAM": 8 }, - "2xlarge": { "vCPU": 8, "RAM": 16 }, - "4xlarge": { "vCPU": 16, "RAM": 32 }, - "8xlarge": { "vCPU": 32, "RAM": 64 }, - "12xlarge": { "vCPU": 48, "RAM": 96 }, - }, - "g4dn": { - "xlarge": { "vCPU": 4, "RAM": 16 }, - "2xlarge": { "vCPU": 8, "RAM": 32 }, - "4xlarge": { "vCPU": 16, "RAM": 64 }, - "8xlarge": { "vCPU": 32, "RAM": 128 }, - }, - "r6a": { - "large": { "vCPU": 2, "RAM": 16 }, - "xlarge": { "vCPU": 4, "RAM": 32 }, - "2xlarge": { "vCPU": 8, "RAM": 64 }, - "4xlarge": { "vCPU": 16, "RAM": 128 }, - "8xlarge": { "vCPU": 32, "RAM": 256 }, - }, - "c5": { - "large": { "vCPU": 2, "RAM": 4 }, - "xlarge": { "vCPU": 4, "RAM": 8 }, - "2xlarge": { "vCPU": 8, "RAM": 16 }, - "4xlarge": { "vCPU": 16, "RAM": 32 }, - }, - "m5": { - "large": { "vCPU": 2, "RAM": 8 }, - "xlarge": { "vCPU": 4, "RAM": 16 }, - "2xlarge": { "vCPU": 8, "RAM": 32 }, - "4xlarge": { "vCPU": 16, "RAM": 64 }, - }, - "x2gd": { - "medium": { "vCPU": 1, "RAM": 16 }, - "large": { "vCPU": 2, "RAM": 32 }, - "xlarge": { "vCPU": 4, "RAM": 64 }, - } -} - - -export const UPPER_BOUND_SMART = .5; -export const RESOURCE_ALLOCATION_RAM = 1; \ No newline at end of file diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider.tsx index fe24075e84..9dd61d4866 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider.tsx @@ -5,7 +5,6 @@ import Tooltip from "@material-ui/core/Tooltip"; import styled from "styled-components"; import Spacer from "components/porter/Spacer"; -import NodeInfoModal from "main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal"; const SMART_LIMIT_FRACTION = 0.5; @@ -151,9 +150,6 @@ const IntelligentSlider: React.FC = ({ )} - {showNeedHelpModal && ( - - )} {isExceedingLimit && ( <> diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx index e8f4ef5009..fd184204d7 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useMemo, useState } from "react"; +import React, { useContext, useMemo } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { match } from "ts-pattern"; @@ -7,7 +7,6 @@ import { ControlledInput } from "components/porter/ControlledInput"; import InputSlider from "components/porter/InputSlider"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; -import SmartOptModal from "main/home/app-dashboard/new-app-flow/tabs/SmartOptModal"; import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider"; import { type PorterAppFormData } from "lib/porter-apps"; import { @@ -33,7 +32,6 @@ const Resources: React.FC = ({ }) => { const { control, register, watch } = useFormContext(); const { currentProject } = useContext(Context); - const [showNeedHelpModal, setShowNeedHelpModal] = useState(false); const { nodes } = useClusterContext(); const { maxRamMegabytes, maxCpuCores } = useMemo(() => { return getServiceResourceAllowances(nodes, currentProject?.sandbox_enabled); @@ -64,9 +62,6 @@ const Resources: React.FC = ({ return ( <> - {showNeedHelpModal && ( - - )} void; - currentCluster: ClusterType; -}; - -const CreateEnvGroup = ({ goBack, currentCluster }: PropsType) => { - const [envGroupName, setEnvGroupName] = useState(''); - const [selectedNamespace, setSelectedNamespace] = useState('default'); - const [namespaceOptions, setNamespaceOptions] = useState([]); - const [envVariables, setEnvVariables] = useState([]); - const [submitStatus, setSubmitStatus] = useState(''); - - const context = useContext(Context); - - useEffect(() => { - updateNamespaces(); - }, []); - - const isDisabled = (): boolean => { - const isEnvGroupNameInvalid = - !isAlphanumeric(envGroupName) || - envGroupName === '' || - envGroupName.length > 60; - - const isAnyEnvVariableBlank = envVariables.some( - (envVar) => !envVar.key.trim() || !envVar.value.trim() - ); - - - - return isEnvGroupNameInvalid || isAnyEnvVariableBlank; - }; - - const onSubmit = (): void => { - setSubmitStatus("loading") - - const apiEnvVariables: Record = {}; - const secretEnvVariables: Record = {}; - - const envVariable = envVariables; - - if (context.currentProject.simplified_view_enabled) { - api - .createNamespace( - "", - { - name: "porter-env-group", - }, - { - id: context.currentProject.id, - cluster_id: currentCluster.id, - } - ) - .catch((error) => { - if (error.response && error.response.status === 412) { - // do nothing - } else { - // do nothing still - } - }); - } - envVariable - .filter((envVar: KeyValueType, index: number, self: KeyValueType[]) => { - // remove any collisions that are marked as deleted and are duplicates - const numCollisions = self.reduce((n, _envVar: KeyValueType) => { - return n + (_envVar.key === envVar.key ? 1 : 0); - }, 0); - - if (numCollisions === 1) { - return true; - } else { - return ( - index === - self.findIndex( - (_envVar: KeyValueType) => - _envVar.key === envVar.key && !_envVar.deleted - ) - ); - } - }) - .forEach((envVar: KeyValueType) => { - if (!envVar.deleted) { - if (envVar.hidden) { - secretEnvVariables[envVar.key] = envVar.value; - } else { - apiEnvVariables[envVar.key] = envVar.value; - } - } - }); - - api - .createEnvGroup( - "", - { - name: envGroupName, - variables: apiEnvVariables, - secret_variables: secretEnvVariables, - }, - { - id: context.currentProject.id, - cluster_id: currentCluster.id, - namespace: context.currentProject.simplified_view_enabled ? "porter-env-group" : selectedNamespace, - } - ) - .then((res) => { - setSubmitStatus("successful"); - // console.log(res); - goBack(); - }) - .catch((err) => { - setSubmitStatus("Could not create"); - }); - }; - - const createEnv = () => { - setSubmitStatus("loading") - - const apiEnvVariables: Record = {}; - const secretEnvVariables: Record = {}; - - const envVariable = envVariables; - envVariable - .filter((envVar: KeyValueType, index: number, self: KeyValueType[]) => { - // remove any collisions that are marked as deleted and are duplicates - const numCollisions = self.reduce((n, _envVar: KeyValueType) => { - return n + (_envVar.key === envVar.key ? 1 : 0); - }, 0); - - if (numCollisions === 1) { - return true; - } else { - return ( - index === - self.findIndex( - (_envVar: KeyValueType) => - _envVar.key === envVar.key && !_envVar.deleted - ) - ); - } - }) - .forEach((envVar: KeyValueType) => { - if (!envVar.deleted) { - if (envVar.hidden) { - secretEnvVariables[envVar.key] = envVar.value; - } else { - apiEnvVariables[envVar.key] = envVar.value; - } - } - }); - - api - .createEnvironmentGroups( - "", - { - name: envGroupName, - variables: apiEnvVariables, - secret_variables: secretEnvVariables, - }, - { - id: context.currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - setSubmitStatus("successful"); - // console.log(res); - goBack(); - }) - .catch((err) => { - if (err) { - setSubmitStatus("Could not create"); - } - }); - }; - - const updateNamespaces = () => { - const { currentProject } = context; - api - .getNamespaces( - "", - {}, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - if (res.data) { - const availableNamespaces = res.data.filter((namespace: any) => { - return namespace.status !== "Terminating"; - }); - const namespaceOptions = availableNamespaces.map( - (x: { name: string }) => { - return { label: x.name, value: x.name }; - } - ); - if (availableNamespaces.length > 0) { - setNamespaceOptions(namespaceOptions); - } - } - }) - .catch(console.log); - }; - - - return ( - <> - - - - Create an environment group - - - - Name - - 60) && - envGroupName !== "" - } - > - Lowercase letters, numbers, and "-" only. Maximum 60 characters. - - - - { setEnvGroupName(x) }} - placeholder="ex: my-env-group" - width="100%" - /> - {!context?.currentProject?.simplified_view_enabled && (<> - Destination - - Specify the namespace you would like to create this environment - group in. - - - - view_listNamespace - - { setSelectedNamespace(namespace) }} - options={namespaceOptions} - width="250px" - dropdownWidth="335px" - closeOverlay={true} - /> - - - ) - } - Environment variables - - Set environment variables for your secrets and environment-specific - configuration. - - { setEnvVariables(x); }} - fileUpload={true} - secretOption={true} - /> - - - - - - ); - -} - -export default CreateEnvGroup; - -const Wrapper = styled.div` - padding: 30px; - padding-bottom: 25px; - border-radius: 5px; - margin-top: -15px; - background: ${props => props.theme.fg}; - border: 1px solid #494b4f; - margin-bottom: 30px; -`; - -const Buffer = styled.div` - width: 100%; - height: 150px; -`; - -const StyledCreateEnvGroup = styled.div` - padding-bottom: 70px; - position: relative; -`; - -const NamespaceLabel = styled.div` - margin-right: 10px; - display: flex; - align-items: center; - > i { - font-size: 16px; - margin-right: 6px; - } -`; - -const DestinationSection = styled.div` - display: flex; - align-items: center; - color: #ffffff; - font-family: "Work Sans", sans-serif; - font-size: 14px; - margin-top: 2px; - font-weight: 500; - margin-bottom: 32px; - - > i { - font-size: 25px; - color: #ffffff44; - margin-right: 13px; - } -`; - -const Button = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - font-size: 13px; - cursor: pointer; - font-family: "Work Sans", sans-serif; - border-radius: 20px; - color: white; - height: 35px; - margin-left: -2px; - padding: 0px 8px; - padding-bottom: 1px; - font-weight: 500; - padding-right: 15px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - cursor: pointer; - border: 2px solid #969fbbaa; - :hover { - background: #ffffff11; - } - - > i { - color: white; - width: 18px; - height: 18px; - color: #969fbbaa; - font-weight: 600; - font-size: 14px; - border-radius: 20px; - display: flex; - align-items: center; - margin-right: 5px; - justify-content: center; - } -`; - -const DarkMatter = styled.div<{ antiHeight?: string }>` - width: 100%; - margin-top: ${(props) => props.antiHeight || "-15px"}; -`; - -const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>` - color: ${(props) => (props.highlight ? "#f5cb42" : "")}; - margin-left: ${(props) => (props.makeFlush ? "" : "5px")}; -`; - -const Subtitle = styled.div` - padding: 11px 0px 16px; - font-family: "Work Sans", sans-serif; - font-size: 13px; - color: #aaaabb; - line-height: 1.6em; - display: flex; - align-items: center; -`; - -const Title = styled.div` - font-size: 20px; - font-weight: 500; - font-family: "Work Sans", sans-serif; - margin-left: 15px; - border-radius: 2px; - color: #ffffff; -`; - -const HeaderSection = styled.div` - display: flex; - align-items: center; - margin-bottom: 40px; - - > i { - cursor: pointer; - font-size: 20px; - color: #969fbbaa; - padding: 2px; - border: 2px solid #969fbbaa; - border-radius: 100px; - :hover { - background: #ffffff11; - } - } - - > img { - width: 20px; - margin-left: 17px; - margin-right: 7px; - } -`; - -const Heading = styled.div<{ isAtTop?: boolean }>` - color: white; - font-weight: 500; - font-size: 16px; - margin-bottom: 5px; - margin-top: ${(props) => (props.isAtTop ? "10px" : "30px")}; - display: flex; - align-items: center; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx deleted file mode 100644 index 44ea49b16e..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; - -import sliders from "assets/sliders.svg"; -import doppler from "assets/doppler.png"; - -import { Context } from "shared/Context"; -import { readableDate } from "shared/string_utils"; -import { Link } from "react-router-dom"; -import _ from "lodash"; - -export type EnvGroupData = { - name: string; - type?: string; - namespace: string; - created_at?: string; - version: number; -}; - -type PropsType = { - envGroup: EnvGroupData; -}; - -type StateType = { - update: any[]; -}; - -export default class EnvGroup extends Component { - state = { - update: [] as any[], - }; - - render() { - const { envGroup } = this.props; - const name = envGroup?.name; - const timestamp = envGroup?.created_at; - const namespace = envGroup?.namespace; - const version = this.context?.currentProject.simplified_view_enabled ? envGroup?.latest_version : envGroup?.version ; - - return ( - - - - <IconWrapper> - <Icon src={envGroup.type === "doppler" ? doppler : sliders} /> - </IconWrapper> - {name} - - - - - - Last updated {readableDate(timestamp)} - - - - {!this.context?.currentProject.simplified_view_enabled && - Namespace - {namespace.startsWith("porter-stack-") ? namespace.replace("porter-stack-", "") : namespace} - } - - - v{version} - - - ); - } -} - -export function formattedEnvironmentValue(value: string) { - if (value.startsWith("PORTERSECRET_")) { - return "••••"; - } - return value; -} - -EnvGroup.contextType = Context; - -const BottomWrapper = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - padding-right: 11px; - margin-top: 3px; -`; - -const Version = styled.div` - position: absolute; - top: 12px; - right: 12px; - font-size: 12px; - color: #aaaabb; -`; - -const Dot = styled.div` - margin-right: 9px; -`; - -const InfoWrapper = styled.div` - display: flex; - align-items: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-right: 8px; -`; - -const LastDeployed = styled.div` - font-size: 13px; - margin-left: 14px; - margin-bottom: -1px; - display: flex; - align-items: center; - color: #aaaabb66; -`; - -const TagWrapper = styled.div` - height: 20px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - color: #ffffff44; - border: 1px solid #ffffff44; - border-radius: 3px; - padding-left: 5px; -`; - -const NamespaceTag = styled.div` - height: 20px; - margin-left: 6px; - color: #aaaabb; - background: #ffffff22; - border-radius: 3px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - padding: 0px 6px; - padding-left: 7px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - -const Icon = styled.img` - width: 100%; -`; - -const IconWrapper = styled.div` - color: #efefef; - background: none; - font-size: 16px; - top: 11px; - left: 14px; - height: 20px; - width: 20px; - display: flex; - justify-content: center; - align-items: center; - border-radius: 3px; - position: absolute; - - > i { - font-size: 17px; - margin-top: -1px; - } -`; - -const Title = styled.div` - position: relative; - text-decoration: none; - padding: 12px 35px 12px 45px; - font-size: 14px; - font-family: "Work Sans", sans-serif; - font-weight: 500; - color: #ffffff; - width: 80%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - animation: fadeIn 0.5s; - - > img { - background: none; - top: 12px; - left: 13px; - - padding: 5px 4px; - width: 24px; - position: absolute; - } -`; - -const StyledEnvGroup = styled.div` - cursor: pointer; - margin-bottom: 15px; - padding-top: 2px; - padding-bottom: 13px; - position: relative; - width: calc(100% + 2px); - height: calc(100% + 2px); - border-radius: 5px; - background: ${props => props.theme.clickable.bg}; - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks.tsx deleted file mode 100644 index aa4c58c563..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArrayStacks.tsx +++ /dev/null @@ -1,404 +0,0 @@ -import React, { useEffect, useState } from "react"; -import styled from "styled-components"; -import Modal from "main/home/modals/Modal"; -import EnvEditorModal from "main/home/modals/EnvEditorModal"; - -import upload from "assets/upload.svg"; -import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray"; -import { dotenv_parse } from "shared/string_utils"; -import { NewPopulatedEnvGroup, PopulatedEnvGroup } from "components/porter-form/types"; -import Text from "components/porter/Text"; -import Spacer from "components/porter/Spacer"; -export type KeyValueType = { - key: string; - value: string; - hidden: boolean; - locked: boolean; - deleted: boolean; -}; - -type PropsType = { - label?: string; - values: KeyValueType[]; - setValues: (x: KeyValueType[]) => void; - disabled?: boolean; - fileUpload?: boolean; - secretOption?: boolean; - syncedEnvGroups?: NewPopulatedEnvGroup[]; -}; - -const EnvGroupArray = ({ - label, - values, - setValues, - disabled, - fileUpload, - secretOption, - syncedEnvGroups -}: PropsType) => { - const [showEditorModal, setShowEditorModal] = useState(false); - - useEffect(() => { - if (!values) { - setValues([]); - } - }, [values]); - const isKeyOverriding = (key: string) => { - if (!syncedEnvGroups || !values) return false; - return syncedEnvGroups?.some(envGroup => { - if (!envGroup || !envGroup.variables) return false; - return key in envGroup.variables || (envGroup.secret_variables && key in envGroup.secret_variables); - }); - }; - - const readFile = (env: string) => { - const envObj = dotenv_parse(env); - const _values = [...values]; - - for (const key in envObj) { - let push = true; - - for (let i = 0; i < values.length; i++) { - const existingKey = values[i]["key"]; - const isExistingKeyDeleted = values[i]["deleted"]; - if (key === existingKey && !isExistingKeyDeleted) { - _values[i]["value"] = envObj[key]; - push = false; - } - } - - if (push) { - _values.push({ - key, - value: envObj[key], - hidden: false, - locked: false, - deleted: false, - }); - } - } - - setValues(_values); - }; - - if (!values) { - return null; - } - - return ( - <> - - - {!!values?.length && - values.map((entry: KeyValueType, i: number) => { - if (!entry.deleted) { - return ( - - { - const _values = [...values]; - _values[i].key = e.target.value; - setValues(_values); - }} - disabled={disabled || entry.locked} - spellCheck={false} - override={isKeyOverriding(entry.key)} - /> - < Spacer x={.5} inline /> - {entry.hidden ? ( - entry.value?.includes("PORTERSECRET") ? ( - ) : ( - { - const _values = [...values]; - _values[i].value = e.target.value; - setValues(_values); - }} - disabled={disabled || entry.locked} - type={entry.hidden ? "password" : "text"} - spellCheck={false} - override={isKeyOverriding(entry.key)} - - />) - ) : ( - entry.value?.includes("PORTERSECRET") ? ( - ) : ( - { - const _values = [...values]; - _values[i].value = e.target.value; - setValues(_values); - }} - rows={entry.value?.split("\n").length} - disabled={disabled || entry.locked} - spellCheck={false} - override={isKeyOverriding(entry.key)} - /> - )) - } - {secretOption && ( - { - if (!entry.locked) { - const _values = [...values]; - _values[i].hidden = !_values[i].hidden; - setValues(_values); - } - }} - disabled={entry.locked} - > - {entry.hidden ? ( - lock - ) : ( - lock_open - )} - - )} - - {!disabled && ( - { - setValues(values.filter((val, index) => index !== i)); - }} - > - cancel - - )} - - {isKeyOverriding(entry.key) && <> Key is overriding value in a environment group} - - ); - } - })} - {!disabled && ( - - { - const _values = [ - ...values, - { - key: "", - value: "", - hidden: false, - locked: false, - deleted: false, - }, - ]; - setValues(_values); - }} - > - add Add Row - - - {fileUpload && ( - { - setShowEditorModal(true); - }} - > - Copy from File - - )} - - )} - - {showEditorModal && ( - setShowEditorModal(false)} - width="60%" - height="650px" - > - setShowEditorModal(false)} - setEnvVariables={(envFile: string) => readFile(envFile)} - /> - - )} - - ); -}; - -export default EnvGroupArray; - - -const AddRowButton = styled.div` - display: flex; - align-items: center; - width: 270px; - font-size: 13px; - color: #aaaabb; - height: 32px; - border-radius: 3px; - cursor: pointer; - background: #ffffff11; - :hover { - background: #ffffff22; - } - - > i { - color: #ffffff44; - font-size: 16px; - margin-left: 8px; - margin-right: 10px; - display: flex; - align-items: center; - justify-content: center; - } -`; - -const UploadButton = styled(AddRowButton)` - background: none; - position: relative; - border: 1px solid #ffffff55; - > i { - color: #ffffff44; - font-size: 16px; - margin-left: 8px; - margin-right: 10px; - display: flex; - align-items: center; - justify-content: center; - } - > img { - width: 14px; - margin-left: 10px; - margin-right: 12px; - } -`; - -const DeleteButton = styled.div` - width: 15px; - height: 15px; - display: flex; - align-items: center; - margin-left: 8px; - margin-top: -3px; - justify-content: center; - - > i { - font-size: 17px; - color: #ffffff44; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - :hover { - color: #ffffff88; - } - } -`; - -const HideButton = styled(DeleteButton)` - margin-top: -5px; - > i { - font-size: 19px; - cursor: ${(props: { disabled: boolean }) => - props.disabled ? "default" : "pointer"}; - :hover { - color: ${(props: { disabled: boolean }) => - props.disabled ? "#ffffff44" : "#ffffff88"}; - } - } -`; - -const InputWrapper = styled.div` - display: flex; - align-items: center; - margin-top: 5px; - -`; - -type InputProps = { - disabled?: boolean; - width: string; - override?: boolean; -}; - -const Input = styled.input` - outline: none; - border: none; - margin-bottom: 5px; - font-size: 13px; - background: #ffffff11; - border: ${(props) => (props.override ? '2px solid #6b74d6' : ' 1px solid #ffffff55')}; - border-radius: 3px; - width: ${(props) => props.width ? props.width : "270px"}; - color: ${(props) => props.disabled ? "#ffffff44" : "white"}; - padding: 5px 10px; - height: 35px; -`; -const Label = styled.div` - color: #ffffff; - margin-bottom: 10px; -`; - -const StyledInputArray = styled.div` - margin-bottom: 15px; - margin-top: 22px; -`; - -export const MultiLineInputer = styled.textarea` - outline: none; - border: none; - margin-bottom: 5px; - font-size: 13px; - background: #ffffff11; - border: ${(props) => (props.override ? '2px solid #6b74d6' : ' 1px solid #ffffff55')}; - border-radius: 3px; - min-width: ${(props) => (props.width ? props.width : "270px")}; - max-width: ${(props) => (props.width ? props.width : "270px")}; - color: ${(props) => (props.disabled ? "#ffffff44" : "white")}; - padding: 8px 10px 5px 10px; - min-height: 35px; - max-height: 100px; - white-space: nowrap; - - ::-webkit-scrollbar { - width: 8px; - :horizontal { - height: 8px; - } - } - - ::-webkit-scrollbar-corner { - width: 10px; - background: #ffffff11; - color: white; - } - - ::-webkit-scrollbar-track { - width: 10px; - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - } - - ::-webkit-scrollbar-thumb { - background-color: darkgrey; - outline: 1px solid slategrey; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx deleted file mode 100644 index ecf6741b4e..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import React, { Component, useContext, useEffect, useState } from "react"; -import { withRouter, type RouteComponentProps } from "react-router"; -import styled from "styled-components"; - -import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder"; -import PorterButton from "components/porter/Button"; -import DashboardPlaceholder from "components/porter/DashboardPlaceholder"; -import PorterLink from "components/porter/Link"; -import Spacer from "components/porter/Spacer"; -import Text from "components/porter/Text"; - -import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc"; -import { Context } from "shared/Context"; -import { getQueryParam, pushFiltered, pushQueryParams } from "shared/routing"; -import { type ClusterType } from "shared/types"; -import sliders from "assets/env-groups.svg"; - -import DashboardHeader from "../DashboardHeader"; -import { NamespaceSelector } from "../NamespaceSelector"; -import SortSelector from "../SortSelector"; -import CreateEnvGroup from "./CreateEnvGroup"; -import EnvGroupList from "./EnvGroupList"; -import ExpandedEnvGroup from "./ExpandedEnvGroup"; - -type PropsType = RouteComponentProps & - WithAuthProps & { - currentCluster: ClusterType; - }; - -type StateType = { - expand: boolean; - update: any[]; - sortType: string; - expandedEnvGroup: any; - namespace: string; - createEnvMode: boolean; -}; - -const EnvGroupDashboard = (props: PropsType) => { - const [state, setState] = useState({ - expand: false, - update: [] as any[], - namespace: null as string, - expandedEnvGroup: null as any, - createEnvMode: false, - sortType: localStorage.getItem("SortType") - ? localStorage.getItem("SortType") - : "Newest", - }); - - const { currentProject } = useContext(Context); - - const setNamespace = (namespace: string) => { - setState((state) => ({ ...state, namespace })); - pushQueryParams(props, { - namespace: currentProject.simplified_view_enabled - ? "porter-env-group" - : namespace ?? "ALL", - }); - }; - - const setSortType = (sortType: string) => { - setState((state) => ({ ...state, sortType })); - }; - - const toggleCreateEnvMode = () => { - setState((state) => ({ - ...state, - createEnvMode: !state.createEnvMode, - })); - }; - - const setExpandedEnvGroup = (envGroup: any | null) => { - setState((state) => ({ ...state, expandedEnvGroup: envGroup })); - }; - - const closeExpanded = () => { - pushQueryParams(props, {}, ["selected_env_group"]); - const redirectUrlOnClose = getQueryParam(props, "redirect_url"); - if (redirectUrlOnClose) { - props.history.push(redirectUrlOnClose); - return; - } - setExpandedEnvGroup(null); - }; - - const renderBody = () => { - if (props.currentCluster.status === "UPDATING_UNAVAILABLE") { - return ; - } - - if (currentProject?.sandbox_enabled) { - return ( - - Environment groups are not enabled on the Porter Cloud. - - - Eject to your own cloud account to enable environment groups. - - - - - Request ejection - - - - ); - } - - const goBack = () => { - setState((state) => ({ ...state, createEnvMode: false })); - }; - - if (state.createEnvMode) { - return ( - - ); - } else { - const isAuthorizedToAdd = props.isAuthorized("env_group", "", [ - "get", - "create", - ]); - - return ( - <> - - - - - {!currentProject.simplified_view_enabled && ( - - )} - - - {isAuthorizedToAdd && ( - - )} - - - - - - ); - } - }; - - const renderContents = () => { - if (state.expandedEnvGroup) { - return ( - { - closeExpanded(); - }} - /> - ); - } else { - return ( - <> - - {renderBody()} - - ); - } - }; - - return <>{renderContents()}; -}; - -export default withRouter(withAuth(EnvGroupDashboard)); - -const Flex = styled.div` - display: flex; - align-items: center; - border-bottom: 30px solid transparent; -`; - -const SortFilterWrapper = styled.div` - display: flex; - justify-content: space-between; - border-bottom: 30px solid transparent; - > div:not(:first-child) { - } -`; - -const ControlRow = styled.div` - display: flex; - justify-content: ${(props: { hasMultipleChilds: boolean }) => { - if (props.hasMultipleChilds) { - return "space-between"; - } - return "flex-end"; - }}; - align-items: center; - flex-wrap: wrap; -`; - -const Button = styled.div` - display: flex; - margin-left: 10px; - flex-direction: row; - align-items: center; - justify-content: space-between; - font-size: 13px; - cursor: pointer; - font-family: "Work Sans", sans-serif; - border-radius: 5px; - color: white; - height: 30px; - padding: 0 8px; - min-width: 155px; - padding-right: 13px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - cursor: ${(props: { disabled?: boolean }) => - props.disabled ? "not-allowed" : "pointer"}; - - background: ${(props: { disabled?: boolean }) => - props.disabled ? "#aaaabbee" : "#616FEEcc"}; - :hover { - background: ${(props: { disabled?: boolean }) => - props.disabled ? "" : "#505edddd"}; - } - - > i { - color: white; - width: 18px; - height: 18px; - font-weight: 600; - font-size: 12px; - border-radius: 20px; - display: flex; - align-items: center; - margin-right: 5px; - justify-content: center; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx deleted file mode 100644 index 7fc32f4259..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import styled from "styled-components"; - -import { Context } from "shared/Context"; -import api from "shared/api"; -import { ClusterType } from "shared/types"; - -import EnvGroup from "./EnvGroup"; -import Loading from "components/Loading"; -import { getQueryParam, pushQueryParams } from "shared/routing"; -import { RouteComponentProps, withRouter } from "react-router"; - -import Placeholder from "components/Placeholder"; - -type Props = RouteComponentProps & { - currentCluster: ClusterType; - namespace: string; - sortType: string; - setExpandedEnvGroup: (envGroup: any) => void; -}; - -type State = { - envGroups: any[]; - loading: boolean; - error: boolean; -}; - -const EnvGroupList: React.FunctionComponent = (props) => { - const context = useContext(Context); - - const { currentCluster, namespace, sortType, setExpandedEnvGroup } = props; - - const [envGroups, setEnvGroups] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [hasError, setHasError] = useState(false); - - const updateEnvGroups = async () => { - let { currentProject, currentCluster } = context; - try { - let envGroups: any[] = [] - if (currentProject?.simplified_view_enabled) { - envGroups = await api - .getAllEnvGroups( - "", - {}, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - return res.data?.environment_groups; - }); - } else { - envGroups = await api - .listEnvGroups( - "", - {}, - { - id: currentProject.id, - namespace: namespace, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - return res.data; - }); - } - let sortedGroups = envGroups; - if (sortedGroups) { - switch (sortType) { - case "Oldest": - sortedGroups.sort((a: any, b: any) => - Date.parse(a.created_at) > Date.parse(b.created_at) ? 1 : -1 - ); - break; - case "Alphabetical": - sortedGroups.sort((a: any, b: any) => (a.name > b.name ? 1 : -1)); - break; - default: - sortedGroups.sort((a: any, b: any) => - Date.parse(a.created_at) > Date.parse(b.created_at) ? -1 : 1 - ); - } - } - return sortedGroups; - } catch (error) { - console.log(error) - setIsLoading(false); - setHasError(true); - } - }; - - useEffect(() => { - // Prevents reload when opening ClusterConfigModal - (namespace || namespace === "") && - updateEnvGroups().then((envGroups) => { - const selectedEnvGroup = getQueryParam(props, "selected_env_group"); - - setEnvGroups(envGroups); - if (envGroups && envGroups.length > 0) { - setHasError(false); - } - setIsLoading(false); - - if (selectedEnvGroup) { - // find env group by selectedEnvGroup - const envGroup = envGroups.find( - (envGroup: any) => envGroup.name === selectedEnvGroup - ); - if (envGroup) { - setExpandedEnvGroup(envGroup); - } else { - pushQueryParams(props, {}, ["selected_env_group"]); - } - } - }); - }, [currentCluster, namespace, sortType]); - - const renderEnvGroupList = () => { - if (isLoading || (!namespace && namespace !== "")) { - return ( - - - - ); - } else if (hasError) { - return ( - - error Error connecting to cluster. - - ); - } else if (!envGroups || envGroups.length === 0) { - return ( - - category - No environment groups found with the given filters. - - ); - } - - return envGroups.map((envGroup: any, i: number) => { - return ( - - ); - }); - }; - - return {renderEnvGroupList()}; -}; - -export default withRouter(EnvGroupList); - -const LoadingWrapper = styled.div` - padding-top: 100px; -`; - -const StyledEnvGroupList = styled.div` - padding-bottom: 85px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx deleted file mode 100644 index 8c640d625a..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx +++ /dev/null @@ -1,1562 +0,0 @@ -import React, { - Component, - useContext, - useEffect, - useMemo, - useState, -} from "react"; -import yaml from "js-yaml"; -import { createFinalPorterYaml, PorterYamlSchema } from "../../app-dashboard/new-app-flow/schema" -import styled, { keyframes } from "styled-components"; -import backArrow from "assets/back_arrow.png"; -import key from "assets/key.svg"; -import loading from "assets/loading.gif"; -import leftArrow from "assets/left-arrow.svg"; - -import { type ChartType, type ClusterType, CreateUpdatePorterAppOptions } from "shared/types"; -import { Context } from "shared/Context"; -import { isAlphanumeric } from "shared/common"; -import api from "shared/api"; - -import TitleSection from "components/TitleSection"; -import SaveButton from "components/SaveButton"; -import TabRegion from "components/TabRegion"; -import EnvGroupArray, { type KeyValueType } from "./EnvGroupArray"; -import Heading from "components/form-components/Heading"; -import Helper from "components/form-components/Helper"; -import InputRow from "components/form-components/InputRow"; -import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc"; -import _, { flatMapDepth, remove, update } from "lodash"; -import { type NewPopulatedEnvGroup, type PopulatedEnvGroup } from "components/porter-form/types"; -import { isAuthorized } from "shared/auth/authorization-helpers"; -import useAuth from "shared/auth/useAuth"; -import { fillWithDeletedVariables } from "components/porter-form/utils"; -import DynamicLink from "components/DynamicLink"; -import DocsHelper from "components/DocsHelper"; -import Spacer from "components/porter/Spacer"; -import EnvGroups from "../stacks/ExpandedStack/components/EnvGroups"; -import { type PorterJson } from "main/home/app-dashboard/new-app-flow/schema"; -import { BuildMethod, PorterApp } from "main/home/app-dashboard/types/porterApp"; -import { Service } from "main/home/app-dashboard/new-app-flow/serviceTypes"; -import { consoleSandbox } from "@sentry/utils"; - -type PropsType = WithAuthProps & { - namespace: string; - envGroup: any; - currentCluster: ClusterType; - closeExpanded: () => void; - allEnvGroups?: NewPopulatedEnvGroup[]; -}; - -type StateType = { - loading: boolean; - currentTab: string | null; - deleting: boolean; - saveValuesStatus: string | null; - envGroup: EnvGroup; - tabOptions: Array<{ value: string; label: string }>; - newEnvGroupName: string; -}; - -type EnvGroup = { - name: string; - // timestamp: string; - variables: KeyValueType[]; - version: number; -}; - -// export default withAuth(ExpandedEnvGroup); - -type EditableEnvGroup = Omit & { - variables: KeyValueType[]; - linked_applications?: string[]; - secret_variables?: KeyValueType[]; -}; - -export const ExpandedEnvGroupFC = ({ - envGroup, - namespace, - closeExpanded, - allEnvGroups, -}: PropsType) => { - const { - currentProject, - currentCluster, - setCurrentOverlay, - setCurrentError, - } = useContext(Context); - const [isAuthorized] = useAuth(); - - const [workflowCheckPassed, setWorkflowCheckPassed] = useState( - false - ); - const [isLoading, setIsLoading] = useState(true); - - const [currentTab, setCurrentTab] = useState("variables-editor"); - const [isDeleting, setIsDeleting] = useState(false); - const [buttonStatus, setButtonStatus] = useState(""); - const [services, setServices] = useState([]); - const [envVars, setEnvVars] = useState([]); - const [subdomain, setSubdomain] = useState(""); - - - const [currentEnvGroup, setCurrentEnvGroup] = useState( - null - ); - const [hasBuiltImage, setHasBuiltImage] = useState(false); - - const [originalEnvVars, setOriginalEnvVars] = useState< - Array<{ - key: string; - value: string; - }> - >(); - - - const fetchPorterYamlContent = async ( - porterYaml: string, - appData: any - ) => { - try { - if (porterYaml && appData?.app?.git_repo_id) { - const res = await api.getPorterYamlContents( - "", - { - path: porterYaml, - }, - { - project_id: appData.app.project_id, - git_repo_id: appData.app.git_repo_id, - owner: appData.app.repo_name?.split("/")[0], - name: appData.app.repo_name?.split("/")[1], - kind: "github", - branch: appData.app.git_branch, - } - ); - if (res.data == null || res.data == "") { - return undefined; - } - const parsedYaml = yaml.load(atob(res.data)); - - return parsedYaml - } - } catch (err) { - // TODO: handle error - console.log("No Porter Yaml") - - } - }; - - const tabOptions = useMemo(() => { - if (!isAuthorized("env_group", "", ["get", "delete"])) { - return [{ value: "variables-editor", label: "Environment variables" }]; - } - if ( - !isAuthorized("env_group", "", ["get", "delete"]) && - (currentProject?.simplified_view_enabled ? currentEnvGroup?.linked_applications?.length : currentEnvGroup?.applications?.length) - ) { - return [ - { value: "variables-editor", label: "Environment variables" }, - { value: "applications", label: "Linked applications" }, - ]; - } - - if (currentProject?.simplified_view_enabled ? currentEnvGroup?.linked_applications?.length : currentEnvGroup?.applications?.length) { - return [ - { value: "variables-editor", label: "Environment variables" }, - { value: "applications", label: "Linked applications" }, - { value: "settings", label: "Settings" }, - ]; - } - - return [ - { value: "variables-editor", label: "Environment variables" }, - { value: "settings", label: "Settings" }, - ]; - }, [currentEnvGroup]); - const populateEnvGroup = async () => { - - // apply v2 already supplies the full env group - if (currentProject?.validate_apply_v2) { - updateEnvGroup(envGroup); - } else if (currentProject?.simplified_view_enabled) { - try { - const populatedEnvGroup = await api - .getAllEnvGroups( - "", - {}, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => res.data.environment_groups); - updateEnvGroup(populatedEnvGroup.find((i: any) => i.name === envGroup.name)); - } catch (error) { - console.log(error); - } - } else { - try { - const populatedEnvGroup = await api - .getEnvGroup( - "", - {}, - { - name: envGroup.name, - id: currentProject.id, - namespace, - cluster_id: currentCluster.id, - } - ) - .then((res) => res.data); - updateEnvGroup(populatedEnvGroup); - } catch (error) { - console.log(error); - } - } - }; - - const updateEnvGroup = (populatedEnvGroup: NewPopulatedEnvGroup) => { - - - if (currentProject?.simplified_view_enabled) { - const normal_variables: KeyValueType[] = Object.entries( - populatedEnvGroup.variables || {} - ).map(([key, value]) => ({ - key, - value, - hidden: value.includes("PORTERSECRET"), - locked: value.includes("PORTERSECRET"), - deleted: false, - })); - const secret_variables: KeyValueType[] = Object.entries( - populatedEnvGroup.secret_variables || {} - ).map(([key, value]) => ({ - key, - value, - hidden: true, - locked: true, - deleted: false, - })); - const variables = [...normal_variables, ...secret_variables]; - - - setOriginalEnvVars( - Object.entries({ - ...(populatedEnvGroup?.variables || {}), - ...(populatedEnvGroup.secret_variables || {}), - }).map(([key, value]) => ({ - key, - value, - })) - ); - - setCurrentEnvGroup({ - ...populatedEnvGroup, - variables, - }); - - } else { - const variables: KeyValueType[] = Object.entries( - populatedEnvGroup.variables || {} - ).map(([key, value]) => ({ - key, - value, - hidden: value.includes("PORTERSECRET"), - locked: value.includes("PORTERSECRET"), - deleted: false, - })); - - setOriginalEnvVars( - Object.entries(populatedEnvGroup?.variables || {}).map(([key, value]) => ({ - key, - value, - })) - ); - - setCurrentEnvGroup({ - ...populatedEnvGroup, - variables, - }); - - } - }; - - const deleteEnvGroup = async () => { - const { name, stack_id, type } = currentEnvGroup; - if (currentProject?.simplified_view_enabled) { - return await api.deleteNewEnvGroup( - "", - { - name, - type, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - } - - if (stack_id?.length) { - return await api.removeStackEnvGroup( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace, - stack_id, - env_group_name: name, - } - ); - } - - - return await api.deleteEnvGroup( - "", - { - name, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - namespace, - } - ); - }; - - const handleDeleteEnvGroup = () => { - setIsDeleting(true); - setCurrentOverlay(null); - - deleteEnvGroup() - .then(() => { - closeExpanded(); - setIsDeleting(true); - }) - .catch(() => { - setIsDeleting(true); - }); - }; - - const getPorterApp = async ({ appName }: { appName: string }) => { - try { - if (!currentCluster || !currentProject) { - return; - } - const resPorterApp = await api.getPorterApp( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - name: appName, - } - ); - const resChartData = await api.getChart( - "", - {}, - { - id: currentProject.id, - namespace: `porter-stack-${appName}`, - cluster_id: currentCluster.id, - name: appName, - revision: 0, - } - ); - - let preDeployChartData; - // get the pre-deploy chart - try { - preDeployChartData = await api.getChart( - "", - {}, - { - id: currentProject.id, - namespace: `porter-stack-${appName}`, - cluster_id: currentCluster.id, - name: `${appName}-r`, - // this is always latest because we do not tie the pre-deploy chart to the umbrella chart - revision: 0, - } - ); - } catch (err) { - // that's ok if there's an error, just means there is no pre-deploy chart - } - - // update apps and release - const newAppData = { - app: resPorterApp?.data, - chart: resChartData?.data, - releaseChart: preDeployChartData?.data, - }; - const porterJson = await fetchPorterYamlContent( - resPorterApp?.data?.porter_yaml_path ?? "porter.yaml", - newAppData - ); - - let filteredEnvGroups: NewPopulatedEnvGroup[] = [] - filteredEnvGroups = allEnvGroups?.filter(envGroup => - envGroup.linked_applications && envGroup.linked_applications.includes(appName) - ); - - const parsedPorterApp = { ...resPorterApp?.data, buildpacks: newAppData.app.buildpacks?.split(",") ?? [] }; - const buildView = !_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks" - - const [newServices, newEnvVars] = updateServicesAndEnvVariables( - resChartData?.data, - preDeployChartData?.data, - porterJson, - ); - const finalPorterYaml = createFinalPorterYaml( - newServices, - newEnvVars, - porterJson, - // if we are using a heroku buildpack, inject a PORT env variable - newAppData.app.builder?.includes("heroku") - ); - - - // Only check GHA status if no built image is set - const hasBuiltImage = !!resChartData.data.config?.global?.image - ?.repository; - if (hasBuiltImage || !resPorterApp.data.repo_name) { - setWorkflowCheckPassed(true); - setHasBuiltImage(true); - } else { - try { - await api.getBranchContents( - "", - { - dir: `./.github/workflows/porter_stack_${resPorterApp.data.name}.yml`, - }, - { - project_id: currentProject.id, - git_repo_id: resPorterApp.data.git_repo_id, - kind: "github", - owner: resPorterApp.data.repo_name.split("/")[0], - name: resPorterApp.data.repo_name.split("/")[1], - branch: resPorterApp.data.git_branch, - } - ); - setWorkflowCheckPassed(true); - - } catch (err) { - // Handle unmerged PR - if (err.response?.status === 404) { - try { - // Check for user-copied porter.yml as fallback - const resPorterYml = await api.getBranchContents( - "", - { dir: `./.github/workflows/porter.yml` }, - { - project_id: currentProject.id, - git_repo_id: resPorterApp.data.git_repo_id, - kind: "github", - owner: resPorterApp.data.repo_name.split("/")[0], - name: resPorterApp.data.repo_name.split("/")[1], - branch: resPorterApp.data.git_branch, - } - ); - setWorkflowCheckPassed(true); - } catch (err) { - setWorkflowCheckPassed(false); - } - } - } - } - - if ( - currentCluster != null && - currentProject != null - ) { - - const yamlString = yaml.dump(finalPorterYaml); - const base64Encoded = btoa(yamlString); - - const updatedPorterApp = { - porter_yaml: base64Encoded, - override_release: true, - ...PorterApp.empty(), - build_context: newAppData?.build_context, - repo_name: newAppData?.repo_name, - git_branch: newAppData?.git_branch, - buildpacks: "", - // full_helm_values: yaml.dump(values), - environment_groups: filteredEnvGroups?.map((env) => env.name), - user_update: true, - } - - if (buildView === "docker") { - updatedPorterApp.dockerfile = newAppData?.dockerfile; - updatedPorterApp.builder = "null"; - updatedPorterApp.buildpacks = "null"; - } else { - updatedPorterApp.builder = newAppData?.builder; - updatedPorterApp.buildpacks = newAppData?.buildpacks?.join(","); - updatedPorterApp.dockerfile = "null"; - } - - await api.createPorterApp( - "", - updatedPorterApp, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - stack_name: appName, - } - ); - } else { - setButtonStatus("error"); - } - } catch (err) { - // TODO: handle error - } finally { - setIsLoading(false); - } - }; - - const updateServicesAndEnvVariables = ( - currentChart?: ChartType, - releaseChart?: ChartType, - porterJson?: PorterJson, - ): [Service[], KeyValueType[]] => { - // handle normal chart - const helmValues = currentChart?.config; - const defaultValues = (currentChart?.chart as any)?.values; - let newServices: Service[] = []; - let envVars: KeyValueType[] = []; - - if ( - (defaultValues && Object.keys(defaultValues).length > 0) || - (helmValues && Object.keys(helmValues).length > 0) - ) { - newServices = Service.deserialize(helmValues, defaultValues, porterJson); - const { global, ...helmValuesWithoutGlobal } = helmValues; - if (Object.keys(helmValuesWithoutGlobal).length > 0) { - envVars = Service.retrieveEnvFromHelmValues(helmValuesWithoutGlobal); - setEnvVars(envVars); - const subdomain = Service.retrieveSubdomainFromHelmValues( - newServices, - helmValuesWithoutGlobal - ); - setSubdomain(subdomain); - } - } - - // handle release chart - if (releaseChart?.config || porterJson?.release) { - const release = Service.deserializeRelease(releaseChart?.config, porterJson); - newServices.push(release); - } - - setServices(newServices); - - return [newServices, envVars]; - }; - - const handleUpdateValues = async () => { - setButtonStatus("loading"); - const name = currentEnvGroup.name; - const variables = currentEnvGroup?.variables; - if (currentEnvGroup.meta_version === 2 || currentProject?.simplified_view_enabled) { - - const secretVariables = remove(variables, (envVar) => { - return !envVar.value.includes("PORTERSECRET") && envVar.hidden; - }).reduce( - (acc, variable) => ({ - ...acc, - [variable.key]: variable?.value, - }), - {} - ); - - const normalVariables = variables?.reduce( - (acc, variable) => ({ - ...acc, - [variable.key]: variable?.value, - }), - {} - ); - - if (currentProject?.simplified_view_enabled) { - try { - - const normal_variables: KeyValueType[] = Object.entries( - normalVariables || {} - ).map(([key, value]) => ({ - key, - value, - hidden: value.includes("PORTERSECRET"), - locked: value.includes("PORTERSECRET"), - deleted: false, - })); - - const secret_variables: KeyValueType[] = Object.entries( - secretVariables || {} - ).map(([key, value]) => ({ - key, - value, - hidden: true, - locked: true, - deleted: false, - })); - const variables = [...normal_variables, ...secret_variables]; - - - setCurrentEnvGroup({ - ...currentEnvGroup, - variables, - }); - - - const linkedApp: string[] = currentEnvGroup?.linked_applications; - // doppler env groups update themselves, and we don't want to increment the version - if (currentEnvGroup?.type !== "doppler" && currentEnvGroup.type !== "infisical") { - await api.createEnvironmentGroups( - "", - { - name, - variables: normalVariables, - secret_variables: secretVariables, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - } - if (!currentProject.validate_apply_v2) { - if (linkedApp) { - const promises = linkedApp.map(async appName => { - if (!currentProject.validate_apply_v2) { - await getPorterApp({ appName }); - } - }); - await Promise.all(promises); - } - } else { - try { - const res = await api.updateAppsLinkedToEnvironmentGroup( - "", - { - name: currentEnvGroup?.name, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - } catch (error) { - setCurrentError(error); - } - } - - const populatedEnvGroup = await api.getAllEnvGroups("", {}, { - id: currentProject.id, - cluster_id: currentCluster.id, - }).then(res => res.data.environment_groups); - - const newEnvGroup = populatedEnvGroup.find((i: any) => i.name === name); - - updateEnvGroup(newEnvGroup); - setButtonStatus("successful"); - } catch (error) { - setButtonStatus("Couldn't update successfully"); - setCurrentError(error); - setTimeout(() => { setButtonStatus(""); }, 1000); - } - } else { - try { - const updatedEnvGroup = await api - .updateEnvGroup( - "", - { - name, - variables: normalVariables, - secret_variables: secretVariables, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace, - } - ) - .then((res) => res.data); - if (!currentProject?.simplified_view_enabled) { - setButtonStatus("successful"); - } - updateEnvGroup(updatedEnvGroup); - - setTimeout(() => { setButtonStatus(""); }, 1000); - } - catch (error) { - setButtonStatus("Couldn't update successfully"); - setCurrentError(error); - setTimeout(() => { setButtonStatus(""); }, 1000); - } - } - } - else { - // SEPARATE THE TWO KINDS OF VARIABLES - let secret = variables.filter( - (variable) => - variable.hidden && !variable.value.includes("PORTERSECRET") - ); - - let normal = variables.filter( - (variable) => - !variable.hidden && !variable.value.includes("PORTERSECRET") - ); - - // Filter variables that weren't updated - normal = normal.reduce((acc, variable) => { - const originalVar = originalEnvVars.find( - (orgVar) => orgVar.key === variable.key - ); - - // Remove variables that weren't updated - if (variable.value === originalVar?.value) { - return acc; - } - - // add the variable that's going to be updated - return [...acc, variable]; - }, []); - - secret = secret.reduce((acc, variable) => { - const originalVar = originalEnvVars.find( - (orgVar) => orgVar.key === variable.key - ); - - // Remove variables that weren't updated - if (variable.value === originalVar?.value) { - return acc; - } - - // add the variable that's going to be updated - return [...acc, variable]; - }, []); - - // Check through the original env vars to see if there's a missing variable, if it is, then means it was removed - const removedNormal = originalEnvVars.reduce((acc, orgVar) => { - if (orgVar.value.includes("PORTERSECRET")) { - return acc; - } - - const variableFound = variables.find( - (variable) => orgVar.key === variable.key - ); - if (variableFound) { - return acc; - } - return [ - ...acc, - { - key: orgVar.key, - value: null, - }, - ]; - }, []); - - const removedSecret = originalEnvVars.reduce((acc, orgVar) => { - if (!orgVar.value.includes("PORTERSECRET")) { - return acc; - } - - const variableFound = variables.find( - (variable) => orgVar.key === variable.key - ); - if (variableFound) { - return acc; - } - return [ - ...acc, - { - key: orgVar.key, - value: null, - }, - ]; - }, []); - - normal = [...normal, ...removedNormal]; - secret = [...secret, ...removedSecret]; - - const normalObject = normal.reduce((acc, val) => { - return { - ...acc, - [val.key]: val.value, - }; - }, {}); - - const secretObject = secret.reduce((acc, val) => { - return { - ...acc, - [val.key]: val.value, - }; - }, {}); - - try { - const updatedEnvGroup = await api - .updateConfigMap( - "", - { - name, - variables: normalObject, - secret_variables: secretObject, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - namespace, - } - ) - .then((res) => res.data); - setButtonStatus("successful"); - updateEnvGroup(updatedEnvGroup); - setTimeout(() => { setButtonStatus(""); }, 1000); - } catch (error) { - setButtonStatus("Couldn't update successfully"); - setCurrentError(error); - setTimeout(() => { setButtonStatus(""); }, 1000); - } - } - }; - - const renderTabContents = () => { - const { variables, secret_variables } = currentEnvGroup; - - // const mergeVar = variables.concat(secret_variables); - - switch (currentTab) { - case "variables-editor": - return ( - { setCurrentEnvGroup((prev) => ({ ...prev, variables: x })); } - } - handleUpdateValues={handleUpdateValues} - variables={variables} - buttonStatus={buttonStatus} - setButtonStatus={setButtonStatus} - /> - ); - case "applications": - return ; - default: - return ( - - ); - } - }; - - useEffect(() => { - populateEnvGroup(); - }, [envGroup]); - - if (!currentEnvGroup) { - return null; - } - - return ( - - - - - Back - - - - - {envGroup.name} - {!currentProject?.simplified_view_enabled && - Namespace {currentProject?.capi_provisioner_enabled && namespace.startsWith("porter-stack-") ? namespace.replace("porter-stack-", "") : namespace} - } - - - - - - {isDeleting ? ( - <> - - - -
- Deleting "{currentEnvGroup.name}" -
- You will be automatically redirected after deletion is complete. -
-
- - ) : ( - { setCurrentTab(x); }} - options={tabOptions} - color={null} - > - {renderTabContents()} - - )} -
- ); -}; - -export default ExpandedEnvGroupFC; - -const EnvGroupVariablesEditor = ({ - onChange, - handleUpdateValues, - variables, - buttonStatus, - setButtonStatus, -}: { - variables: KeyValueType[]; - buttonStatus: any; - onChange: (newValues: any) => void; - handleUpdateValues: () => void; - setButtonStatus: (status: string) => void; -}) => { - const [isAuthorized] = useAuth(); - const [buttonDisabled, setButtonDisabled] = useState(false) - - return ( - - - Environment variables - - Set environment variables for your secrets and environment-specific - configuration. - - { - onChange(x); - }} - fileUpload={true} - secretOption={true} - disabled={ - !isAuthorized("env_group", "", [ - "get", - "create", - "delete", - "update", - ]) - } - /> - - {isAuthorized("env_group", "", ["get", "update"]) && ( - { handleUpdateValues(); }} - status={buttonStatus} - disabled={buttonStatus == "loading" || buttonDisabled} - makeFlush={true} - clearPosition={true} - statusPosition="right" - /> - )} - - ); -}; - -const EnvGroupSettings = ({ - envGroup, - handleDeleteEnvGroup, - namespace, -}: { - envGroup: EditableEnvGroup; - handleDeleteEnvGroup: () => void; - namespace?: string; -}) => { - const { - setCurrentOverlay, - currentProject, - currentCluster, - setCurrentError, - } = useContext(Context); - const [isAuthorized] = useAuth(); - - // When cloning an env group, append "-2" for the default name - // (i.e. my-env-group-2) - const [name, setName] = useState( - envGroup.name + "-2" - ); - const [cloneNamespace, setCloneNamespace] = useState("default"); - const [cloneSuccess, setCloneSuccess] = useState(false); - - const canDelete = useMemo(() => { - // add a case for when applications is null - in this case this is a deprecated env group version - if (currentProject?.simplified_view_enabled) { - if (!envGroup?.linked_applications) { - return true; - } - - return envGroup?.linked_applications?.length === 0; - } else { - if (!envGroup?.applications) { - return true; - } - - return envGroup?.applications?.length === 0; - } - }, [envGroup]); - - const cloneEnvGroup = async () => { - setCloneSuccess(false); - try { - await api.cloneEnvGroup( - "", - { - name: envGroup.name, - namespace: cloneNamespace, - clone_name: name, - version: envGroup.version, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - namespace, - } - ); - setCloneSuccess(true); - } catch (error) { - console.log(error); - } - }; - - return ( - - {isAuthorized("env_group", "", ["get", "delete"]) && ( - - Manage environment group - - Permanently delete this set of environment variables. This action - cannot be undone. - - {!canDelete && ( - - Applications are still synced to this env group. Navigate to - "Linked applications" and remove this env group from all - applications to delete. - - )} - - {!currentProject?.simplified_view_enabled && ( - <> - - Clone environment group - - Clone this set of environment variables into a new env group. - - { setName(x); }} - label="New env group name" - placeholder="ex: my-cloned-env-group" - /> - { setCloneNamespace(x); }} - label="New env group namespace" - placeholder="ex: default" - /> - - - {cloneSuccess && ( - - done - Successfully cloned - - )} - - - )} - - )} - - ); -}; - -const ApplicationsList = ({ envGroup }: { envGroup: EditableEnvGroup }) => { - const { currentCluster, currentProject } = useContext(Context); - - return ( - <> - - Linked applications: - - - {currentProject?.simplified_view_enabled ? ( - envGroup.linked_applications.map((appName) => { - return ( - - - - - {appName} - - - - {currentProject?.simplified_view_enabled ? ( - - open_in_new - - ) : ( - - open_in_new - - )} - - - - ); - }) - ) : ( - envGroup.applications.map((appName) => { - return ( - - - - - {appName} - - - - {currentProject?.simplified_view_enabled ? ( - - open_in_new - - ) : ( - - open_in_new - - )} - - - - ); - }) - )} - - ); -}; - -const FlexAlt = styled.div` - display: flex; - align-items: center; - margin-top: 20px; -`; - -const StatusTextWrapper = styled.p` - display: -webkit-box; - line-clamp: 2; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - line-height: 19px; - margin: 0; -`; - -const StatusWrapper = styled.div<{ - successful: boolean; - position: "right" | "left"; -}>` - display: flex; - align-items: center; - max-width: 170px; - font-family: "Work Sans", sans-serif; - font-size: 13px; - color: #ffffff55; - overflow: hidden; - text-overflow: ellipsis; - margin-top: 5px; - margin-bottom: 30px; - height: 35px; - margin-left: 15px; - - > i { - font-size: 18px; - margin-right: 10px; - float: left; - color: ${(props) => (props.successful ? "#4797ff" : "#fcba03")}; - } - - animation-fill-mode: forwards; - - @keyframes statusFloatIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0px); - } - } -`; - -const DarkMatter = styled.div` - width: 100%; - height: 1px; - margin-top: -20px; -`; - -const ArrowIcon = styled.img` - width: 15px; - margin-right: 8px; - opacity: 50%; -`; - -const BreadcrumbRow = styled.div` - width: 100%; - display: flex; - justify-content: flex-start; -`; - -const Breadcrumb = styled.div` - color: #aaaabb88; - font-size: 13px; - margin-bottom: 15px; - display: flex; - align-items: center; - margin-top: -10px; - z-index: 999; - padding: 5px; - padding-right: 7px; - border-radius: 5px; - cursor: pointer; - :hover { - background: #ffffff11; - } -`; - -const Wrap = styled.div` - z-index: 999; -`; - -const HeadingWrapper = styled.div` - display: flex; - margin-bottom: 15px; -`; - -const Header = styled.div` - font-weight: 500; - color: #aaaabb; - font-size: 16px; - margin-bottom: 15px; -`; - -const Placeholder = styled.div` - min-height: 400px; - height: 50vh; - padding: 30px; - padding-bottom: 90px; - font-size: 13px; - color: #ffffff44; - width: 100%; - display: flex; - align-items: center; - justify-content: center; -`; - -const Spinner = styled.img` - width: 15px; - height: 15px; - margin-right: 12px; - margin-bottom: -2px; -`; - -const TextWrap = styled.div``; - -const LineBreak = styled.div` - width: calc(100% - 0px); - height: 1px; - background: #494b4f; - margin: 15px 0px 55px; -`; - -const HeaderWrapper = styled.div` - position: relative; -`; - -const BackButton = styled.div` - position: absolute; - top: 0px; - right: 0px; - display: flex; - width: 36px; - cursor: pointer; - height: 36px; - align-items: center; - justify-content: center; - border: 1px solid #ffffff55; - border-radius: 100px; - background: #ffffff11; - - :hover { - background: #ffffff22; - > img { - opacity: 1; - } - } -`; - -const BackButtonImg = styled.img` - width: 16px; - opacity: 0.75; -`; - -const Button = styled.button` - height: 35px; - font-size: 13px; - margin-top: 5px; - margin-bottom: 30px; - font-weight: 500; - font-family: "Work Sans", sans-serif; - color: white; - padding: 6px 20px 7px 20px; - text-align: left; - border: 0; - border-radius: 5px; - background: ${(props) => (!props.disabled ? props.color : "#aaaabb")}; - cursor: ${(props) => (!props.disabled ? "pointer" : "default")}; - user-select: none; - :focus { - outline: 0; - } - :hover { - filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")}; - } -`; - -const CloneButton = styled(Button)` - display: flex; - width: fit-content; - align-items: center; - justify-content: center; - background-color: #ffffff11; - :hover { - background-color: #ffffff18; - } -`; - -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; -`; - -const TabWrapper = styled.div` - height: 100%; - width: 100%; - padding-bottom: 65px; - overflow: hidden; -`; - -const InfoWrapper = styled.div` - display: flex; - align-items: center; - margin: 10px 0px 17px 0px; - height: 20px; -`; - -const LastDeployed = styled.div` - font-size: 13px; - margin-left: 0; - margin-top: -1px; - display: flex; - align-items: center; - color: #aaaabb66; -`; - -const TagWrapper = styled.div` - height: 20px; - font-size: 12px; - display: flex; - margin-left: 20px; - margin-bottom: -3px; - align-items: center; - font-weight: 400; - justify-content: center; - color: #ffffff44; - border: 1px solid #ffffff44; - border-radius: 3px; - padding-left: 5px; - background: #26282e; -`; - -const NamespaceTag = styled.div` - height: 20px; - margin-left: 6px; - color: #aaaabb; - background: #43454a; - border-radius: 3px; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - padding: 0px 6px; - padding-left: 7px; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; -`; - -const StyledExpandedChart = styled.div` - width: 100%; - z-index: 0; - animation: fadeIn 0.3s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - display: flex; - overflow-y: auto; - padding-bottom: 120px; - flex-direction: column; - overflow: visible; - - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>` - color: ${(props) => (props.highlight ? "#f5cb42" : "")}; - margin-left: ${(props) => (props.makeFlush ? "" : "5px")}; -`; - -const Subtitle = styled.div` - padding: 11px 0px 16px; - font-family: "Work Sans", sans-serif; - font-size: 13px; - color: #aaaabb; - line-height: 1.6em; - display: flex; - align-items: center; -`; - -const fadeIn = keyframes` - from { - opacity: 0; - } - to { - opacity: 1; - } -`; - -const StyledCard = styled.div` - border-radius: 8px; - padding: 10px 18px; - overflow: hidden; - font-size: 13px; - animation: ${fadeIn} 0.5s; - - background: #2b2e3699; - margin-bottom: 15px; - overflow: hidden; - border: 1px solid #ffffff0a; -`; - -const Flex = styled.div` - display: flex; - align-items: center; - justify-content: space-between; -`; - -const ContentContainer = styled.div` - display: flex; - height: 100%; - width: 100%; - align-items: center; -`; - -const EventInformation = styled.div` - display: flex; - flex-direction: column; - justify-content: space-around; - height: 100%; -`; - -const EventName = styled.div` - font-family: "Work Sans", sans-serif; - font-weight: 500; - color: #ffffff; -`; - -const ActionContainer = styled.div` - display: flex; - align-items: center; - white-space: nowrap; - height: 100%; -`; - -const ActionButton = styled(DynamicLink)` - position: relative; - border: none; - background: none; - color: white; - padding: 5px; - display: flex; - justify-content: center; - align-items: center; - border-radius: 50%; - cursor: pointer; - color: #aaaabb; - border: 1px solid #ffffff00; - - :hover { - background: #ffffff11; - border: 1px solid #ffffff44; - } - - > span { - font-size: 20px; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx deleted file mode 100644 index 2034628077..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import React, { Component, useContext, useEffect, useState } from "react"; -import styled from "styled-components"; - -import sliders from "assets/sliders.svg"; - -import { Context } from "shared/Context"; -import { ClusterType } from "shared/types"; - -import ExpandedEnvGroup from "./ExpandedEnvGroup"; -import { RouteComponentProps, useParams, withRouter } from "react-router"; -import { getQueryParam, pushQueryParams } from "shared/routing"; -import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc"; -import { useQuery } from "@tanstack/react-query"; -import api from "shared/api"; -import Loading from "components/Loading"; -import Placeholder from "components/Placeholder"; - -type PropsType = RouteComponentProps & - WithAuthProps & { - currentCluster: ClusterType; - }; - -const EnvGroupDashboard = (props: PropsType) => { - const namespace = (currentProject?.simplified_view_enabled && currentProject?.capi_provisioner_enabled) ? "porter-env-group" : getQueryParam(props, "namespace"); - const params = useParams<{ name: string }>(); - const { currentProject } = useContext(Context); - const [expandedEnvGroup, setExpandedEnvGroup] = useState(); - const isTabActive = () => { - return !document.hidden; - }; - - const { - data: envGroups, - isLoading: listEnvGroupsLoading, - isError, - refetch, - } = useQuery( - ["envGroupList", currentProject.id, namespace, props.currentCluster.id], - async () => { - try { - if (!namespace) { - if (!currentProject?.simplified_view_enabled) { - return []; - } - } - let res: any[] = []; - if (currentProject?.simplified_view_enabled) { - res = await api.getAllEnvGroups( - "", - {}, - { - id: currentProject.id, - cluster_id: props.currentCluster.id, - } - ); - } else { - - res = await api.listEnvGroups( - "", - {}, - { - id: currentProject.id, - namespace: currentProject?.simplified_view_enabled ? "porter-env-group" : namespace, - cluster_id: props.currentCluster.id, - } - ); - } - return currentProject?.simplified_view_enabled ? res.data?.environment_groups : res.data; - } catch (err) { - throw err; - } - }, - { - enabled: false, // Initially disable the query - } - ); - - useEffect(() => { - const name = params.name; - - if (!envGroups || !isTabActive()) { - return; - } - - const envGroup = envGroups.find((envGroup) => envGroup.name === name); - setExpandedEnvGroup(envGroup); - }, [envGroups, params]); - - useEffect(() => { - if (isTabActive()) { - refetch(); // Run the query when the component mounts and the tab is active - } - }, []); - if (listEnvGroupsLoading) { - return ( - - - - ); - } - - const renderContents = () => { - if (!expandedEnvGroup) { - return null; - } - - return ( - props.history.push("/env-groups")} - /> - ); - }; - - if (listEnvGroupsLoading) { - return ( - - - - ); - } - - return <>{renderContents()}; -}; - -export default withRouter(withAuth(EnvGroupDashboard)); - -const Flex = styled.div` - display: flex; - align-items: center; - border-bottom: 30px solid transparent; -`; - -const SortFilterWrapper = styled.div` - display: flex; - justify-content: space-between; - border-bottom: 30px solid transparent; - > div:not(:first-child) { - } -`; - -const ControlRow = styled.div` - display: flex; - justify-content: ${(props: { hasMultipleChilds: boolean }) => { - if (props.hasMultipleChilds) { - return "space-between"; - } - return "flex-end"; - }}; - align-items: center; - flex-wrap: wrap; -`; - -const Button = styled.div` - display: flex; - margin-left: 10px; - flex-direction: row; - align-items: center; - justify-content: space-between; - font-size: 13px; - cursor: pointer; - font-family: "Work Sans", sans-serif; - border-radius: 5px; - color: white; - height: 30px; - padding: 0 8px; - min-width: 155px; - padding-right: 13px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - cursor: ${(props: { disabled?: boolean }) => - props.disabled ? "not-allowed" : "pointer"}; - - background: ${(props: { disabled?: boolean }) => - props.disabled ? "#aaaabbee" : "#616FEEcc"}; - :hover { - background: ${(props: { disabled?: boolean }) => - props.disabled ? "" : "#505edddd"}; - } - - > i { - color: white; - width: 18px; - height: 18px; - font-weight: 600; - font-size: 12px; - border-radius: 20px; - display: flex; - align-items: center; - margin-right: 5px; - justify-content: center; - } -`; From 5c538a8cea8430a4701032acfa4235deb5ad9967 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Thu, 2 May 2024 16:42:50 -0400 Subject: [PATCH 3/8] fix dependencies --- .../app-dashboard/expanded-app/logs/types.ts | 93 +++++++++++++++++++ .../cluster-dashboard/DashboardRouter.tsx | 38 ++------ 2 files changed, 102 insertions(+), 29 deletions(-) create mode 100644 dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts diff --git a/dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts b/dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts new file mode 100644 index 0000000000..97c9333bf6 --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts @@ -0,0 +1,93 @@ +import { z } from "zod"; +import { type AnserJsonEntry } from "anser"; + +export enum Direction { + forward = "forward", + backward = "backward", +} + +export type PorterLog = { + line: AnserJsonEntry[]; + lineNumber: number; + timestamp?: string; + metadata?: z.infer; +} + +export type PaginationInfo = { + previousCursor: string | null; + nextCursor: string | null; +} + +const rawLabelsValidator = z.object({ + porter_run_absolute_name: z.string().optional(), + porter_run_app_id: z.string().optional(), + porter_run_app_name: z.string().optional(), + porter_run_app_revision_id: z.string().optional(), + porter_run_service_name: z.string().optional(), + porter_run_service_type: z.string().optional(), + porter_run_deployment_target_id: z.string().optional(), + job_name: z.string().optional(), + controller_uid: z.string().optional(), +}); +export type RawLabels = z.infer; + +const agentLogMetadataValidator = z.object({ + pod_name: z.string(), + pod_namespace: z.string(), + revision: z.string(), + output_stream: z.string(), + app_name: z.string(), + raw_labels: rawLabelsValidator.nullish(), +}); + +export const agentLogValidator = z.object({ + line: z.string(), + timestamp: z.string(), + metadata: agentLogMetadataValidator.optional(), +}); +export type AgentLog = z.infer; + +export type GenericFilterOption = { + label: string; + value: string; +} +export const GenericFilterOption = { + of: (label: string, value: string): GenericFilterOption => { + return { label, value }; + } +} +export type FilterName = 'revision' | 'output_stream' | 'pod_name' | 'service_name' | 'revision_id'; +export type GenericFilter = { + name: FilterName; + displayName: string; + default: GenericFilterOption | undefined; + options: GenericFilterOption[]; + setValue: (value: string) => void; +} +export const GenericFilter = { + isDefault: (filter: GenericFilter, value: string) => { + return filter.default && filter.default.value === value; + }, + + getDefaultOption: (filterName: FilterName) => { + switch (filterName) { + case 'service_name': + return GenericFilterOption.of('All', 'all'); + case 'revision': // refers to number + return GenericFilterOption.of('All', 'all'); + case 'revision_id': + return GenericFilterOption.of('All', 'all'); + case 'output_stream': + return GenericFilterOption.of('All', 'all'); + case 'pod_name': + return GenericFilterOption.of('All', 'all'); + default: + return GenericFilterOption.of('All', 'all'); + } + }, +} +export type LogFilterQueryParamOpts = { + revision: string | null; + output_stream: string | null; + service: string | null; +} diff --git a/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx b/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx index d9958ea8bb..f2c406ca0f 100644 --- a/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx +++ b/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx @@ -1,16 +1,16 @@ import React, { useState, useContext, useEffect } from "react"; import styled from "styled-components"; import loadable from "@loadable/component"; -import { RouteComponentProps, withRouter } from "react-router"; +import { type RouteComponentProps, withRouter } from "react-router"; import { Route, Switch } from "react-router-dom"; import api from "shared/api"; import { Context } from "shared/Context"; -import { WithAuthProps, withAuth } from "shared/auth/AuthorizationHoc"; -import { ClusterType } from "shared/types"; +import { type WithAuthProps, withAuth } from "shared/auth/AuthorizationHoc"; +import { type ClusterType } from "shared/types"; import { getQueryParam, - PorterUrl, + type PorterUrl, pushQueryParams, } from "shared/routing"; @@ -20,20 +20,18 @@ import DashboardRoutes from "./dashboard/Routes"; import GuardedRoute from "shared/auth/RouteGuard"; import AppDashboard from "./apps/AppDashboard"; import JobDashboard from "./jobs/JobDashboard"; -import ExpandedEnvGroupDashboard from "./env-groups/ExpandedEnvGroupDashboard"; -import EnvGroupDashboard from "./env-groups/EnvGroupDashboard"; const LazyPreviewEnvironmentsRoutes = loadable( - // @ts-ignore - () => import("./preview-environments/routes.tsx"), + // @ts-expect-error + async () => await import("./preview-environments/routes.tsx"), { fallback: , } ); const LazyStackRoutes = loadable( - // @ts-ignore - () => import("./stacks/routes.tsx"), + // @ts-expect-error + async () => await import("./stacks/routes.tsx"), { fallback: , } @@ -84,7 +82,7 @@ const DashboardRouter: React.FC = ({ // Reset namespace filter and close expanded chart on cluster change useEffect(() => { let namespace = "default"; - let localStorageNamespace = localStorage.getItem( + const localStorageNamespace = localStorage.getItem( `${currentProject.id}-${currentCluster.id}-namespace` ); if (localStorageNamespace) { @@ -148,24 +146,6 @@ const DashboardRouter: React.FC = ({ sortType={sortType} /> - - - - - - From b797638a16411140b0b05b5f44450b35c24d005d Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Thu, 2 May 2024 16:50:11 -0400 Subject: [PATCH 4/8] fix import --- dashboard/src/main/home/modals/LoadEnvGroupModal.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dashboard/src/main/home/modals/LoadEnvGroupModal.tsx b/dashboard/src/main/home/modals/LoadEnvGroupModal.tsx index 96bcd3c928..f1a4d162aa 100644 --- a/dashboard/src/main/home/modals/LoadEnvGroupModal.tsx +++ b/dashboard/src/main/home/modals/LoadEnvGroupModal.tsx @@ -9,10 +9,6 @@ import { Context } from "shared/Context"; import Loading from "components/Loading"; import SaveButton from "components/SaveButton"; import { KeyValue } from "components/form-components/KeyValueArray"; -import { - EnvGroupData, - formattedEnvironmentValue, -} from "../cluster-dashboard/env-groups/EnvGroup"; import CheckboxRow from "components/form-components/CheckboxRow"; import { PartialEnvGroup, @@ -21,6 +17,7 @@ import { import Helper from "components/form-components/Helper"; import DocsHelper from "components/DocsHelper"; import { isEmpty, isObject } from "lodash"; +import { formattedEnvironmentValue } from "../env-dashboard/EnvGroup"; type PropsType = { namespace: string; From 55635bac3d412ae5d767a1065abe6d7ea87afc03 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Thu, 2 May 2024 16:55:13 -0400 Subject: [PATCH 5/8] undo unintended changes --- dashboard/src/components/porter/InputSlider.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dashboard/src/components/porter/InputSlider.tsx b/dashboard/src/components/porter/InputSlider.tsx index 9f6a290f0e..90576530a5 100644 --- a/dashboard/src/components/porter/InputSlider.tsx +++ b/dashboard/src/components/porter/InputSlider.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import Slider, { type Mark } from '@material-ui/core/Slider'; +import Slider, { Mark } from '@material-ui/core/Slider'; import Tooltip from '@material-ui/core/Tooltip'; import Typography from '@material-ui/core/Typography'; import styled from 'styled-components'; @@ -66,9 +66,9 @@ const InputSlider: React.FC = ({ label: max.toString(), }, ]; - let isExceedingLimit = false; - let displayOptimalText = false; - // Optimal Marks only give useful information to user if they are using more than 2 nodes + var isExceedingLimit = false; + var displayOptimalText = false; + //Optimal Marks only give useful information to user if they are using more than 2 nodes // if (optimal != 0 && nodeCount && nodeCount > 2) { // marks.push({ // value: optimal, @@ -162,7 +162,7 @@ const InputSlider: React.FC = ({ valueLabelDisplay={smartLimit && Number(value) > smartLimit ? "off" : "auto"} disabled={disabled} marks={marks} - step={(step || 1)} + step={(step ? step : 1)} style={{ color: disabled ? "gray" : color, }} @@ -251,7 +251,7 @@ const MaxedOutToolTip = withStyles(theme => ({ const StyledSlider = withStyles({ root: { - height: '8px', // height of the track + height: '8px', //height of the track }, mark: { backgroundColor: '#fff', // mark color From 00cd0de8e9445bb6c6021645c1ed16a389fc3cd0 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Thu, 2 May 2024 16:59:49 -0400 Subject: [PATCH 6/8] re-add stuff --- .../cluster-dashboard/DashboardRouter.tsx | 18 +- .../env-groups/EnvGroupDashboard.tsx | 273 +++ .../env-groups/ExpandedEnvGroup.tsx | 1562 +++++++++++++++++ .../env-groups/ExpandedEnvGroupDashboard.tsx | 198 +++ 4 files changed, 2042 insertions(+), 9 deletions(-) create mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx create mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx create mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx diff --git a/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx b/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx index f2c406ca0f..48b09dd6a8 100644 --- a/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx +++ b/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx @@ -1,16 +1,16 @@ import React, { useState, useContext, useEffect } from "react"; import styled from "styled-components"; import loadable from "@loadable/component"; -import { type RouteComponentProps, withRouter } from "react-router"; +import { RouteComponentProps, withRouter } from "react-router"; import { Route, Switch } from "react-router-dom"; import api from "shared/api"; import { Context } from "shared/Context"; -import { type WithAuthProps, withAuth } from "shared/auth/AuthorizationHoc"; -import { type ClusterType } from "shared/types"; +import { WithAuthProps, withAuth } from "shared/auth/AuthorizationHoc"; +import { ClusterType } from "shared/types"; import { getQueryParam, - type PorterUrl, + PorterUrl, pushQueryParams, } from "shared/routing"; @@ -22,16 +22,16 @@ import AppDashboard from "./apps/AppDashboard"; import JobDashboard from "./jobs/JobDashboard"; const LazyPreviewEnvironmentsRoutes = loadable( - // @ts-expect-error - async () => await import("./preview-environments/routes.tsx"), + // @ts-ignore + () => import("./preview-environments/routes.tsx"), { fallback: , } ); const LazyStackRoutes = loadable( - // @ts-expect-error - async () => await import("./stacks/routes.tsx"), + // @ts-ignore + () => import("./stacks/routes.tsx"), { fallback: , } @@ -82,7 +82,7 @@ const DashboardRouter: React.FC = ({ // Reset namespace filter and close expanded chart on cluster change useEffect(() => { let namespace = "default"; - const localStorageNamespace = localStorage.getItem( + let localStorageNamespace = localStorage.getItem( `${currentProject.id}-${currentCluster.id}-namespace` ); if (localStorageNamespace) { diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx new file mode 100644 index 0000000000..ecf6741b4e --- /dev/null +++ b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx @@ -0,0 +1,273 @@ +import React, { Component, useContext, useEffect, useState } from "react"; +import { withRouter, type RouteComponentProps } from "react-router"; +import styled from "styled-components"; + +import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder"; +import PorterButton from "components/porter/Button"; +import DashboardPlaceholder from "components/porter/DashboardPlaceholder"; +import PorterLink from "components/porter/Link"; +import Spacer from "components/porter/Spacer"; +import Text from "components/porter/Text"; + +import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc"; +import { Context } from "shared/Context"; +import { getQueryParam, pushFiltered, pushQueryParams } from "shared/routing"; +import { type ClusterType } from "shared/types"; +import sliders from "assets/env-groups.svg"; + +import DashboardHeader from "../DashboardHeader"; +import { NamespaceSelector } from "../NamespaceSelector"; +import SortSelector from "../SortSelector"; +import CreateEnvGroup from "./CreateEnvGroup"; +import EnvGroupList from "./EnvGroupList"; +import ExpandedEnvGroup from "./ExpandedEnvGroup"; + +type PropsType = RouteComponentProps & + WithAuthProps & { + currentCluster: ClusterType; + }; + +type StateType = { + expand: boolean; + update: any[]; + sortType: string; + expandedEnvGroup: any; + namespace: string; + createEnvMode: boolean; +}; + +const EnvGroupDashboard = (props: PropsType) => { + const [state, setState] = useState({ + expand: false, + update: [] as any[], + namespace: null as string, + expandedEnvGroup: null as any, + createEnvMode: false, + sortType: localStorage.getItem("SortType") + ? localStorage.getItem("SortType") + : "Newest", + }); + + const { currentProject } = useContext(Context); + + const setNamespace = (namespace: string) => { + setState((state) => ({ ...state, namespace })); + pushQueryParams(props, { + namespace: currentProject.simplified_view_enabled + ? "porter-env-group" + : namespace ?? "ALL", + }); + }; + + const setSortType = (sortType: string) => { + setState((state) => ({ ...state, sortType })); + }; + + const toggleCreateEnvMode = () => { + setState((state) => ({ + ...state, + createEnvMode: !state.createEnvMode, + })); + }; + + const setExpandedEnvGroup = (envGroup: any | null) => { + setState((state) => ({ ...state, expandedEnvGroup: envGroup })); + }; + + const closeExpanded = () => { + pushQueryParams(props, {}, ["selected_env_group"]); + const redirectUrlOnClose = getQueryParam(props, "redirect_url"); + if (redirectUrlOnClose) { + props.history.push(redirectUrlOnClose); + return; + } + setExpandedEnvGroup(null); + }; + + const renderBody = () => { + if (props.currentCluster.status === "UPDATING_UNAVAILABLE") { + return ; + } + + if (currentProject?.sandbox_enabled) { + return ( + + Environment groups are not enabled on the Porter Cloud. + + + Eject to your own cloud account to enable environment groups. + + + + + Request ejection + + + + ); + } + + const goBack = () => { + setState((state) => ({ ...state, createEnvMode: false })); + }; + + if (state.createEnvMode) { + return ( + + ); + } else { + const isAuthorizedToAdd = props.isAuthorized("env_group", "", [ + "get", + "create", + ]); + + return ( + <> + + + + + {!currentProject.simplified_view_enabled && ( + + )} + + + {isAuthorizedToAdd && ( + + )} + + + + + + ); + } + }; + + const renderContents = () => { + if (state.expandedEnvGroup) { + return ( + { + closeExpanded(); + }} + /> + ); + } else { + return ( + <> + + {renderBody()} + + ); + } + }; + + return <>{renderContents()}; +}; + +export default withRouter(withAuth(EnvGroupDashboard)); + +const Flex = styled.div` + display: flex; + align-items: center; + border-bottom: 30px solid transparent; +`; + +const SortFilterWrapper = styled.div` + display: flex; + justify-content: space-between; + border-bottom: 30px solid transparent; + > div:not(:first-child) { + } +`; + +const ControlRow = styled.div` + display: flex; + justify-content: ${(props: { hasMultipleChilds: boolean }) => { + if (props.hasMultipleChilds) { + return "space-between"; + } + return "flex-end"; + }}; + align-items: center; + flex-wrap: wrap; +`; + +const Button = styled.div` + display: flex; + margin-left: 10px; + flex-direction: row; + align-items: center; + justify-content: space-between; + font-size: 13px; + cursor: pointer; + font-family: "Work Sans", sans-serif; + border-radius: 5px; + color: white; + height: 30px; + padding: 0 8px; + min-width: 155px; + padding-right: 13px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: ${(props: { disabled?: boolean }) => + props.disabled ? "not-allowed" : "pointer"}; + + background: ${(props: { disabled?: boolean }) => + props.disabled ? "#aaaabbee" : "#616FEEcc"}; + :hover { + background: ${(props: { disabled?: boolean }) => + props.disabled ? "" : "#505edddd"}; + } + + > i { + color: white; + width: 18px; + height: 18px; + font-weight: 600; + font-size: 12px; + border-radius: 20px; + display: flex; + align-items: center; + margin-right: 5px; + justify-content: center; + } +`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx new file mode 100644 index 0000000000..8c640d625a --- /dev/null +++ b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx @@ -0,0 +1,1562 @@ +import React, { + Component, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import yaml from "js-yaml"; +import { createFinalPorterYaml, PorterYamlSchema } from "../../app-dashboard/new-app-flow/schema" +import styled, { keyframes } from "styled-components"; +import backArrow from "assets/back_arrow.png"; +import key from "assets/key.svg"; +import loading from "assets/loading.gif"; +import leftArrow from "assets/left-arrow.svg"; + +import { type ChartType, type ClusterType, CreateUpdatePorterAppOptions } from "shared/types"; +import { Context } from "shared/Context"; +import { isAlphanumeric } from "shared/common"; +import api from "shared/api"; + +import TitleSection from "components/TitleSection"; +import SaveButton from "components/SaveButton"; +import TabRegion from "components/TabRegion"; +import EnvGroupArray, { type KeyValueType } from "./EnvGroupArray"; +import Heading from "components/form-components/Heading"; +import Helper from "components/form-components/Helper"; +import InputRow from "components/form-components/InputRow"; +import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc"; +import _, { flatMapDepth, remove, update } from "lodash"; +import { type NewPopulatedEnvGroup, type PopulatedEnvGroup } from "components/porter-form/types"; +import { isAuthorized } from "shared/auth/authorization-helpers"; +import useAuth from "shared/auth/useAuth"; +import { fillWithDeletedVariables } from "components/porter-form/utils"; +import DynamicLink from "components/DynamicLink"; +import DocsHelper from "components/DocsHelper"; +import Spacer from "components/porter/Spacer"; +import EnvGroups from "../stacks/ExpandedStack/components/EnvGroups"; +import { type PorterJson } from "main/home/app-dashboard/new-app-flow/schema"; +import { BuildMethod, PorterApp } from "main/home/app-dashboard/types/porterApp"; +import { Service } from "main/home/app-dashboard/new-app-flow/serviceTypes"; +import { consoleSandbox } from "@sentry/utils"; + +type PropsType = WithAuthProps & { + namespace: string; + envGroup: any; + currentCluster: ClusterType; + closeExpanded: () => void; + allEnvGroups?: NewPopulatedEnvGroup[]; +}; + +type StateType = { + loading: boolean; + currentTab: string | null; + deleting: boolean; + saveValuesStatus: string | null; + envGroup: EnvGroup; + tabOptions: Array<{ value: string; label: string }>; + newEnvGroupName: string; +}; + +type EnvGroup = { + name: string; + // timestamp: string; + variables: KeyValueType[]; + version: number; +}; + +// export default withAuth(ExpandedEnvGroup); + +type EditableEnvGroup = Omit & { + variables: KeyValueType[]; + linked_applications?: string[]; + secret_variables?: KeyValueType[]; +}; + +export const ExpandedEnvGroupFC = ({ + envGroup, + namespace, + closeExpanded, + allEnvGroups, +}: PropsType) => { + const { + currentProject, + currentCluster, + setCurrentOverlay, + setCurrentError, + } = useContext(Context); + const [isAuthorized] = useAuth(); + + const [workflowCheckPassed, setWorkflowCheckPassed] = useState( + false + ); + const [isLoading, setIsLoading] = useState(true); + + const [currentTab, setCurrentTab] = useState("variables-editor"); + const [isDeleting, setIsDeleting] = useState(false); + const [buttonStatus, setButtonStatus] = useState(""); + const [services, setServices] = useState([]); + const [envVars, setEnvVars] = useState([]); + const [subdomain, setSubdomain] = useState(""); + + + const [currentEnvGroup, setCurrentEnvGroup] = useState( + null + ); + const [hasBuiltImage, setHasBuiltImage] = useState(false); + + const [originalEnvVars, setOriginalEnvVars] = useState< + Array<{ + key: string; + value: string; + }> + >(); + + + const fetchPorterYamlContent = async ( + porterYaml: string, + appData: any + ) => { + try { + if (porterYaml && appData?.app?.git_repo_id) { + const res = await api.getPorterYamlContents( + "", + { + path: porterYaml, + }, + { + project_id: appData.app.project_id, + git_repo_id: appData.app.git_repo_id, + owner: appData.app.repo_name?.split("/")[0], + name: appData.app.repo_name?.split("/")[1], + kind: "github", + branch: appData.app.git_branch, + } + ); + if (res.data == null || res.data == "") { + return undefined; + } + const parsedYaml = yaml.load(atob(res.data)); + + return parsedYaml + } + } catch (err) { + // TODO: handle error + console.log("No Porter Yaml") + + } + }; + + const tabOptions = useMemo(() => { + if (!isAuthorized("env_group", "", ["get", "delete"])) { + return [{ value: "variables-editor", label: "Environment variables" }]; + } + if ( + !isAuthorized("env_group", "", ["get", "delete"]) && + (currentProject?.simplified_view_enabled ? currentEnvGroup?.linked_applications?.length : currentEnvGroup?.applications?.length) + ) { + return [ + { value: "variables-editor", label: "Environment variables" }, + { value: "applications", label: "Linked applications" }, + ]; + } + + if (currentProject?.simplified_view_enabled ? currentEnvGroup?.linked_applications?.length : currentEnvGroup?.applications?.length) { + return [ + { value: "variables-editor", label: "Environment variables" }, + { value: "applications", label: "Linked applications" }, + { value: "settings", label: "Settings" }, + ]; + } + + return [ + { value: "variables-editor", label: "Environment variables" }, + { value: "settings", label: "Settings" }, + ]; + }, [currentEnvGroup]); + const populateEnvGroup = async () => { + + // apply v2 already supplies the full env group + if (currentProject?.validate_apply_v2) { + updateEnvGroup(envGroup); + } else if (currentProject?.simplified_view_enabled) { + try { + const populatedEnvGroup = await api + .getAllEnvGroups( + "", + {}, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } + ) + .then((res) => res.data.environment_groups); + updateEnvGroup(populatedEnvGroup.find((i: any) => i.name === envGroup.name)); + } catch (error) { + console.log(error); + } + } else { + try { + const populatedEnvGroup = await api + .getEnvGroup( + "", + {}, + { + name: envGroup.name, + id: currentProject.id, + namespace, + cluster_id: currentCluster.id, + } + ) + .then((res) => res.data); + updateEnvGroup(populatedEnvGroup); + } catch (error) { + console.log(error); + } + } + }; + + const updateEnvGroup = (populatedEnvGroup: NewPopulatedEnvGroup) => { + + + if (currentProject?.simplified_view_enabled) { + const normal_variables: KeyValueType[] = Object.entries( + populatedEnvGroup.variables || {} + ).map(([key, value]) => ({ + key, + value, + hidden: value.includes("PORTERSECRET"), + locked: value.includes("PORTERSECRET"), + deleted: false, + })); + const secret_variables: KeyValueType[] = Object.entries( + populatedEnvGroup.secret_variables || {} + ).map(([key, value]) => ({ + key, + value, + hidden: true, + locked: true, + deleted: false, + })); + const variables = [...normal_variables, ...secret_variables]; + + + setOriginalEnvVars( + Object.entries({ + ...(populatedEnvGroup?.variables || {}), + ...(populatedEnvGroup.secret_variables || {}), + }).map(([key, value]) => ({ + key, + value, + })) + ); + + setCurrentEnvGroup({ + ...populatedEnvGroup, + variables, + }); + + } else { + const variables: KeyValueType[] = Object.entries( + populatedEnvGroup.variables || {} + ).map(([key, value]) => ({ + key, + value, + hidden: value.includes("PORTERSECRET"), + locked: value.includes("PORTERSECRET"), + deleted: false, + })); + + setOriginalEnvVars( + Object.entries(populatedEnvGroup?.variables || {}).map(([key, value]) => ({ + key, + value, + })) + ); + + setCurrentEnvGroup({ + ...populatedEnvGroup, + variables, + }); + + } + }; + + const deleteEnvGroup = async () => { + const { name, stack_id, type } = currentEnvGroup; + if (currentProject?.simplified_view_enabled) { + return await api.deleteNewEnvGroup( + "", + { + name, + type, + }, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } + ); + } + + if (stack_id?.length) { + return await api.removeStackEnvGroup( + "", + {}, + { + project_id: currentProject.id, + cluster_id: currentCluster.id, + namespace, + stack_id, + env_group_name: name, + } + ); + } + + + return await api.deleteEnvGroup( + "", + { + name, + }, + { + id: currentProject.id, + cluster_id: currentCluster.id, + namespace, + } + ); + }; + + const handleDeleteEnvGroup = () => { + setIsDeleting(true); + setCurrentOverlay(null); + + deleteEnvGroup() + .then(() => { + closeExpanded(); + setIsDeleting(true); + }) + .catch(() => { + setIsDeleting(true); + }); + }; + + const getPorterApp = async ({ appName }: { appName: string }) => { + try { + if (!currentCluster || !currentProject) { + return; + } + const resPorterApp = await api.getPorterApp( + "", + {}, + { + cluster_id: currentCluster.id, + project_id: currentProject.id, + name: appName, + } + ); + const resChartData = await api.getChart( + "", + {}, + { + id: currentProject.id, + namespace: `porter-stack-${appName}`, + cluster_id: currentCluster.id, + name: appName, + revision: 0, + } + ); + + let preDeployChartData; + // get the pre-deploy chart + try { + preDeployChartData = await api.getChart( + "", + {}, + { + id: currentProject.id, + namespace: `porter-stack-${appName}`, + cluster_id: currentCluster.id, + name: `${appName}-r`, + // this is always latest because we do not tie the pre-deploy chart to the umbrella chart + revision: 0, + } + ); + } catch (err) { + // that's ok if there's an error, just means there is no pre-deploy chart + } + + // update apps and release + const newAppData = { + app: resPorterApp?.data, + chart: resChartData?.data, + releaseChart: preDeployChartData?.data, + }; + const porterJson = await fetchPorterYamlContent( + resPorterApp?.data?.porter_yaml_path ?? "porter.yaml", + newAppData + ); + + let filteredEnvGroups: NewPopulatedEnvGroup[] = [] + filteredEnvGroups = allEnvGroups?.filter(envGroup => + envGroup.linked_applications && envGroup.linked_applications.includes(appName) + ); + + const parsedPorterApp = { ...resPorterApp?.data, buildpacks: newAppData.app.buildpacks?.split(",") ?? [] }; + const buildView = !_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks" + + const [newServices, newEnvVars] = updateServicesAndEnvVariables( + resChartData?.data, + preDeployChartData?.data, + porterJson, + ); + const finalPorterYaml = createFinalPorterYaml( + newServices, + newEnvVars, + porterJson, + // if we are using a heroku buildpack, inject a PORT env variable + newAppData.app.builder?.includes("heroku") + ); + + + // Only check GHA status if no built image is set + const hasBuiltImage = !!resChartData.data.config?.global?.image + ?.repository; + if (hasBuiltImage || !resPorterApp.data.repo_name) { + setWorkflowCheckPassed(true); + setHasBuiltImage(true); + } else { + try { + await api.getBranchContents( + "", + { + dir: `./.github/workflows/porter_stack_${resPorterApp.data.name}.yml`, + }, + { + project_id: currentProject.id, + git_repo_id: resPorterApp.data.git_repo_id, + kind: "github", + owner: resPorterApp.data.repo_name.split("/")[0], + name: resPorterApp.data.repo_name.split("/")[1], + branch: resPorterApp.data.git_branch, + } + ); + setWorkflowCheckPassed(true); + + } catch (err) { + // Handle unmerged PR + if (err.response?.status === 404) { + try { + // Check for user-copied porter.yml as fallback + const resPorterYml = await api.getBranchContents( + "", + { dir: `./.github/workflows/porter.yml` }, + { + project_id: currentProject.id, + git_repo_id: resPorterApp.data.git_repo_id, + kind: "github", + owner: resPorterApp.data.repo_name.split("/")[0], + name: resPorterApp.data.repo_name.split("/")[1], + branch: resPorterApp.data.git_branch, + } + ); + setWorkflowCheckPassed(true); + } catch (err) { + setWorkflowCheckPassed(false); + } + } + } + } + + if ( + currentCluster != null && + currentProject != null + ) { + + const yamlString = yaml.dump(finalPorterYaml); + const base64Encoded = btoa(yamlString); + + const updatedPorterApp = { + porter_yaml: base64Encoded, + override_release: true, + ...PorterApp.empty(), + build_context: newAppData?.build_context, + repo_name: newAppData?.repo_name, + git_branch: newAppData?.git_branch, + buildpacks: "", + // full_helm_values: yaml.dump(values), + environment_groups: filteredEnvGroups?.map((env) => env.name), + user_update: true, + } + + if (buildView === "docker") { + updatedPorterApp.dockerfile = newAppData?.dockerfile; + updatedPorterApp.builder = "null"; + updatedPorterApp.buildpacks = "null"; + } else { + updatedPorterApp.builder = newAppData?.builder; + updatedPorterApp.buildpacks = newAppData?.buildpacks?.join(","); + updatedPorterApp.dockerfile = "null"; + } + + await api.createPorterApp( + "", + updatedPorterApp, + { + cluster_id: currentCluster.id, + project_id: currentProject.id, + stack_name: appName, + } + ); + } else { + setButtonStatus("error"); + } + } catch (err) { + // TODO: handle error + } finally { + setIsLoading(false); + } + }; + + const updateServicesAndEnvVariables = ( + currentChart?: ChartType, + releaseChart?: ChartType, + porterJson?: PorterJson, + ): [Service[], KeyValueType[]] => { + // handle normal chart + const helmValues = currentChart?.config; + const defaultValues = (currentChart?.chart as any)?.values; + let newServices: Service[] = []; + let envVars: KeyValueType[] = []; + + if ( + (defaultValues && Object.keys(defaultValues).length > 0) || + (helmValues && Object.keys(helmValues).length > 0) + ) { + newServices = Service.deserialize(helmValues, defaultValues, porterJson); + const { global, ...helmValuesWithoutGlobal } = helmValues; + if (Object.keys(helmValuesWithoutGlobal).length > 0) { + envVars = Service.retrieveEnvFromHelmValues(helmValuesWithoutGlobal); + setEnvVars(envVars); + const subdomain = Service.retrieveSubdomainFromHelmValues( + newServices, + helmValuesWithoutGlobal + ); + setSubdomain(subdomain); + } + } + + // handle release chart + if (releaseChart?.config || porterJson?.release) { + const release = Service.deserializeRelease(releaseChart?.config, porterJson); + newServices.push(release); + } + + setServices(newServices); + + return [newServices, envVars]; + }; + + const handleUpdateValues = async () => { + setButtonStatus("loading"); + const name = currentEnvGroup.name; + const variables = currentEnvGroup?.variables; + if (currentEnvGroup.meta_version === 2 || currentProject?.simplified_view_enabled) { + + const secretVariables = remove(variables, (envVar) => { + return !envVar.value.includes("PORTERSECRET") && envVar.hidden; + }).reduce( + (acc, variable) => ({ + ...acc, + [variable.key]: variable?.value, + }), + {} + ); + + const normalVariables = variables?.reduce( + (acc, variable) => ({ + ...acc, + [variable.key]: variable?.value, + }), + {} + ); + + if (currentProject?.simplified_view_enabled) { + try { + + const normal_variables: KeyValueType[] = Object.entries( + normalVariables || {} + ).map(([key, value]) => ({ + key, + value, + hidden: value.includes("PORTERSECRET"), + locked: value.includes("PORTERSECRET"), + deleted: false, + })); + + const secret_variables: KeyValueType[] = Object.entries( + secretVariables || {} + ).map(([key, value]) => ({ + key, + value, + hidden: true, + locked: true, + deleted: false, + })); + const variables = [...normal_variables, ...secret_variables]; + + + setCurrentEnvGroup({ + ...currentEnvGroup, + variables, + }); + + + const linkedApp: string[] = currentEnvGroup?.linked_applications; + // doppler env groups update themselves, and we don't want to increment the version + if (currentEnvGroup?.type !== "doppler" && currentEnvGroup.type !== "infisical") { + await api.createEnvironmentGroups( + "", + { + name, + variables: normalVariables, + secret_variables: secretVariables, + }, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } + ); + } + if (!currentProject.validate_apply_v2) { + if (linkedApp) { + const promises = linkedApp.map(async appName => { + if (!currentProject.validate_apply_v2) { + await getPorterApp({ appName }); + } + }); + await Promise.all(promises); + } + } else { + try { + const res = await api.updateAppsLinkedToEnvironmentGroup( + "", + { + name: currentEnvGroup?.name, + }, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } + ) + } catch (error) { + setCurrentError(error); + } + } + + const populatedEnvGroup = await api.getAllEnvGroups("", {}, { + id: currentProject.id, + cluster_id: currentCluster.id, + }).then(res => res.data.environment_groups); + + const newEnvGroup = populatedEnvGroup.find((i: any) => i.name === name); + + updateEnvGroup(newEnvGroup); + setButtonStatus("successful"); + } catch (error) { + setButtonStatus("Couldn't update successfully"); + setCurrentError(error); + setTimeout(() => { setButtonStatus(""); }, 1000); + } + } else { + try { + const updatedEnvGroup = await api + .updateEnvGroup( + "", + { + name, + variables: normalVariables, + secret_variables: secretVariables, + }, + { + project_id: currentProject.id, + cluster_id: currentCluster.id, + namespace, + } + ) + .then((res) => res.data); + if (!currentProject?.simplified_view_enabled) { + setButtonStatus("successful"); + } + updateEnvGroup(updatedEnvGroup); + + setTimeout(() => { setButtonStatus(""); }, 1000); + } + catch (error) { + setButtonStatus("Couldn't update successfully"); + setCurrentError(error); + setTimeout(() => { setButtonStatus(""); }, 1000); + } + } + } + else { + // SEPARATE THE TWO KINDS OF VARIABLES + let secret = variables.filter( + (variable) => + variable.hidden && !variable.value.includes("PORTERSECRET") + ); + + let normal = variables.filter( + (variable) => + !variable.hidden && !variable.value.includes("PORTERSECRET") + ); + + // Filter variables that weren't updated + normal = normal.reduce((acc, variable) => { + const originalVar = originalEnvVars.find( + (orgVar) => orgVar.key === variable.key + ); + + // Remove variables that weren't updated + if (variable.value === originalVar?.value) { + return acc; + } + + // add the variable that's going to be updated + return [...acc, variable]; + }, []); + + secret = secret.reduce((acc, variable) => { + const originalVar = originalEnvVars.find( + (orgVar) => orgVar.key === variable.key + ); + + // Remove variables that weren't updated + if (variable.value === originalVar?.value) { + return acc; + } + + // add the variable that's going to be updated + return [...acc, variable]; + }, []); + + // Check through the original env vars to see if there's a missing variable, if it is, then means it was removed + const removedNormal = originalEnvVars.reduce((acc, orgVar) => { + if (orgVar.value.includes("PORTERSECRET")) { + return acc; + } + + const variableFound = variables.find( + (variable) => orgVar.key === variable.key + ); + if (variableFound) { + return acc; + } + return [ + ...acc, + { + key: orgVar.key, + value: null, + }, + ]; + }, []); + + const removedSecret = originalEnvVars.reduce((acc, orgVar) => { + if (!orgVar.value.includes("PORTERSECRET")) { + return acc; + } + + const variableFound = variables.find( + (variable) => orgVar.key === variable.key + ); + if (variableFound) { + return acc; + } + return [ + ...acc, + { + key: orgVar.key, + value: null, + }, + ]; + }, []); + + normal = [...normal, ...removedNormal]; + secret = [...secret, ...removedSecret]; + + const normalObject = normal.reduce((acc, val) => { + return { + ...acc, + [val.key]: val.value, + }; + }, {}); + + const secretObject = secret.reduce((acc, val) => { + return { + ...acc, + [val.key]: val.value, + }; + }, {}); + + try { + const updatedEnvGroup = await api + .updateConfigMap( + "", + { + name, + variables: normalObject, + secret_variables: secretObject, + }, + { + id: currentProject.id, + cluster_id: currentCluster.id, + namespace, + } + ) + .then((res) => res.data); + setButtonStatus("successful"); + updateEnvGroup(updatedEnvGroup); + setTimeout(() => { setButtonStatus(""); }, 1000); + } catch (error) { + setButtonStatus("Couldn't update successfully"); + setCurrentError(error); + setTimeout(() => { setButtonStatus(""); }, 1000); + } + } + }; + + const renderTabContents = () => { + const { variables, secret_variables } = currentEnvGroup; + + // const mergeVar = variables.concat(secret_variables); + + switch (currentTab) { + case "variables-editor": + return ( + { setCurrentEnvGroup((prev) => ({ ...prev, variables: x })); } + } + handleUpdateValues={handleUpdateValues} + variables={variables} + buttonStatus={buttonStatus} + setButtonStatus={setButtonStatus} + /> + ); + case "applications": + return ; + default: + return ( + + ); + } + }; + + useEffect(() => { + populateEnvGroup(); + }, [envGroup]); + + if (!currentEnvGroup) { + return null; + } + + return ( + + + + + Back + + + + + {envGroup.name} + {!currentProject?.simplified_view_enabled && + Namespace {currentProject?.capi_provisioner_enabled && namespace.startsWith("porter-stack-") ? namespace.replace("porter-stack-", "") : namespace} + } + + + + + + {isDeleting ? ( + <> + + + +
+ Deleting "{currentEnvGroup.name}" +
+ You will be automatically redirected after deletion is complete. +
+
+ + ) : ( + { setCurrentTab(x); }} + options={tabOptions} + color={null} + > + {renderTabContents()} + + )} +
+ ); +}; + +export default ExpandedEnvGroupFC; + +const EnvGroupVariablesEditor = ({ + onChange, + handleUpdateValues, + variables, + buttonStatus, + setButtonStatus, +}: { + variables: KeyValueType[]; + buttonStatus: any; + onChange: (newValues: any) => void; + handleUpdateValues: () => void; + setButtonStatus: (status: string) => void; +}) => { + const [isAuthorized] = useAuth(); + const [buttonDisabled, setButtonDisabled] = useState(false) + + return ( + + + Environment variables + + Set environment variables for your secrets and environment-specific + configuration. + + { + onChange(x); + }} + fileUpload={true} + secretOption={true} + disabled={ + !isAuthorized("env_group", "", [ + "get", + "create", + "delete", + "update", + ]) + } + /> + + {isAuthorized("env_group", "", ["get", "update"]) && ( + { handleUpdateValues(); }} + status={buttonStatus} + disabled={buttonStatus == "loading" || buttonDisabled} + makeFlush={true} + clearPosition={true} + statusPosition="right" + /> + )} + + ); +}; + +const EnvGroupSettings = ({ + envGroup, + handleDeleteEnvGroup, + namespace, +}: { + envGroup: EditableEnvGroup; + handleDeleteEnvGroup: () => void; + namespace?: string; +}) => { + const { + setCurrentOverlay, + currentProject, + currentCluster, + setCurrentError, + } = useContext(Context); + const [isAuthorized] = useAuth(); + + // When cloning an env group, append "-2" for the default name + // (i.e. my-env-group-2) + const [name, setName] = useState( + envGroup.name + "-2" + ); + const [cloneNamespace, setCloneNamespace] = useState("default"); + const [cloneSuccess, setCloneSuccess] = useState(false); + + const canDelete = useMemo(() => { + // add a case for when applications is null - in this case this is a deprecated env group version + if (currentProject?.simplified_view_enabled) { + if (!envGroup?.linked_applications) { + return true; + } + + return envGroup?.linked_applications?.length === 0; + } else { + if (!envGroup?.applications) { + return true; + } + + return envGroup?.applications?.length === 0; + } + }, [envGroup]); + + const cloneEnvGroup = async () => { + setCloneSuccess(false); + try { + await api.cloneEnvGroup( + "", + { + name: envGroup.name, + namespace: cloneNamespace, + clone_name: name, + version: envGroup.version, + }, + { + id: currentProject.id, + cluster_id: currentCluster.id, + namespace, + } + ); + setCloneSuccess(true); + } catch (error) { + console.log(error); + } + }; + + return ( + + {isAuthorized("env_group", "", ["get", "delete"]) && ( + + Manage environment group + + Permanently delete this set of environment variables. This action + cannot be undone. + + {!canDelete && ( + + Applications are still synced to this env group. Navigate to + "Linked applications" and remove this env group from all + applications to delete. + + )} + + {!currentProject?.simplified_view_enabled && ( + <> + + Clone environment group + + Clone this set of environment variables into a new env group. + + { setName(x); }} + label="New env group name" + placeholder="ex: my-cloned-env-group" + /> + { setCloneNamespace(x); }} + label="New env group namespace" + placeholder="ex: default" + /> + + + {cloneSuccess && ( + + done + Successfully cloned + + )} + + + )} + + )} + + ); +}; + +const ApplicationsList = ({ envGroup }: { envGroup: EditableEnvGroup }) => { + const { currentCluster, currentProject } = useContext(Context); + + return ( + <> + + Linked applications: + + + {currentProject?.simplified_view_enabled ? ( + envGroup.linked_applications.map((appName) => { + return ( + + + + + {appName} + + + + {currentProject?.simplified_view_enabled ? ( + + open_in_new + + ) : ( + + open_in_new + + )} + + + + ); + }) + ) : ( + envGroup.applications.map((appName) => { + return ( + + + + + {appName} + + + + {currentProject?.simplified_view_enabled ? ( + + open_in_new + + ) : ( + + open_in_new + + )} + + + + ); + }) + )} + + ); +}; + +const FlexAlt = styled.div` + display: flex; + align-items: center; + margin-top: 20px; +`; + +const StatusTextWrapper = styled.p` + display: -webkit-box; + line-clamp: 2; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-height: 19px; + margin: 0; +`; + +const StatusWrapper = styled.div<{ + successful: boolean; + position: "right" | "left"; +}>` + display: flex; + align-items: center; + max-width: 170px; + font-family: "Work Sans", sans-serif; + font-size: 13px; + color: #ffffff55; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 5px; + margin-bottom: 30px; + height: 35px; + margin-left: 15px; + + > i { + font-size: 18px; + margin-right: 10px; + float: left; + color: ${(props) => (props.successful ? "#4797ff" : "#fcba03")}; + } + + animation-fill-mode: forwards; + + @keyframes statusFloatIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0px); + } + } +`; + +const DarkMatter = styled.div` + width: 100%; + height: 1px; + margin-top: -20px; +`; + +const ArrowIcon = styled.img` + width: 15px; + margin-right: 8px; + opacity: 50%; +`; + +const BreadcrumbRow = styled.div` + width: 100%; + display: flex; + justify-content: flex-start; +`; + +const Breadcrumb = styled.div` + color: #aaaabb88; + font-size: 13px; + margin-bottom: 15px; + display: flex; + align-items: center; + margin-top: -10px; + z-index: 999; + padding: 5px; + padding-right: 7px; + border-radius: 5px; + cursor: pointer; + :hover { + background: #ffffff11; + } +`; + +const Wrap = styled.div` + z-index: 999; +`; + +const HeadingWrapper = styled.div` + display: flex; + margin-bottom: 15px; +`; + +const Header = styled.div` + font-weight: 500; + color: #aaaabb; + font-size: 16px; + margin-bottom: 15px; +`; + +const Placeholder = styled.div` + min-height: 400px; + height: 50vh; + padding: 30px; + padding-bottom: 90px; + font-size: 13px; + color: #ffffff44; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +`; + +const Spinner = styled.img` + width: 15px; + height: 15px; + margin-right: 12px; + margin-bottom: -2px; +`; + +const TextWrap = styled.div``; + +const LineBreak = styled.div` + width: calc(100% - 0px); + height: 1px; + background: #494b4f; + margin: 15px 0px 55px; +`; + +const HeaderWrapper = styled.div` + position: relative; +`; + +const BackButton = styled.div` + position: absolute; + top: 0px; + right: 0px; + display: flex; + width: 36px; + cursor: pointer; + height: 36px; + align-items: center; + justify-content: center; + border: 1px solid #ffffff55; + border-radius: 100px; + background: #ffffff11; + + :hover { + background: #ffffff22; + > img { + opacity: 1; + } + } +`; + +const BackButtonImg = styled.img` + width: 16px; + opacity: 0.75; +`; + +const Button = styled.button` + height: 35px; + font-size: 13px; + margin-top: 5px; + margin-bottom: 30px; + font-weight: 500; + font-family: "Work Sans", sans-serif; + color: white; + padding: 6px 20px 7px 20px; + text-align: left; + border: 0; + border-radius: 5px; + background: ${(props) => (!props.disabled ? props.color : "#aaaabb")}; + cursor: ${(props) => (!props.disabled ? "pointer" : "default")}; + user-select: none; + :focus { + outline: 0; + } + :hover { + filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")}; + } +`; + +const CloneButton = styled(Button)` + display: flex; + width: fit-content; + align-items: center; + justify-content: center; + background-color: #ffffff11; + :hover { + background-color: #ffffff18; + } +`; + +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; +`; + +const TabWrapper = styled.div` + height: 100%; + width: 100%; + padding-bottom: 65px; + overflow: hidden; +`; + +const InfoWrapper = styled.div` + display: flex; + align-items: center; + margin: 10px 0px 17px 0px; + height: 20px; +`; + +const LastDeployed = styled.div` + font-size: 13px; + margin-left: 0; + margin-top: -1px; + display: flex; + align-items: center; + color: #aaaabb66; +`; + +const TagWrapper = styled.div` + height: 20px; + font-size: 12px; + display: flex; + margin-left: 20px; + margin-bottom: -3px; + align-items: center; + font-weight: 400; + justify-content: center; + color: #ffffff44; + border: 1px solid #ffffff44; + border-radius: 3px; + padding-left: 5px; + background: #26282e; +`; + +const NamespaceTag = styled.div` + height: 20px; + margin-left: 6px; + color: #aaaabb; + background: #43454a; + border-radius: 3px; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + padding: 0px 6px; + padding-left: 7px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; +`; + +const StyledExpandedChart = styled.div` + width: 100%; + z-index: 0; + animation: fadeIn 0.3s; + animation-timing-function: ease-out; + animation-fill-mode: forwards; + display: flex; + overflow-y: auto; + padding-bottom: 120px; + flex-direction: column; + overflow: visible; + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } +`; + +const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>` + color: ${(props) => (props.highlight ? "#f5cb42" : "")}; + margin-left: ${(props) => (props.makeFlush ? "" : "5px")}; +`; + +const Subtitle = styled.div` + padding: 11px 0px 16px; + font-family: "Work Sans", sans-serif; + font-size: 13px; + color: #aaaabb; + line-height: 1.6em; + display: flex; + align-items: center; +`; + +const fadeIn = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; + +const StyledCard = styled.div` + border-radius: 8px; + padding: 10px 18px; + overflow: hidden; + font-size: 13px; + animation: ${fadeIn} 0.5s; + + background: #2b2e3699; + margin-bottom: 15px; + overflow: hidden; + border: 1px solid #ffffff0a; +`; + +const Flex = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const ContentContainer = styled.div` + display: flex; + height: 100%; + width: 100%; + align-items: center; +`; + +const EventInformation = styled.div` + display: flex; + flex-direction: column; + justify-content: space-around; + height: 100%; +`; + +const EventName = styled.div` + font-family: "Work Sans", sans-serif; + font-weight: 500; + color: #ffffff; +`; + +const ActionContainer = styled.div` + display: flex; + align-items: center; + white-space: nowrap; + height: 100%; +`; + +const ActionButton = styled(DynamicLink)` + position: relative; + border: none; + background: none; + color: white; + padding: 5px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + cursor: pointer; + color: #aaaabb; + border: 1px solid #ffffff00; + + :hover { + background: #ffffff11; + border: 1px solid #ffffff44; + } + + > span { + font-size: 20px; + } +`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx new file mode 100644 index 0000000000..2034628077 --- /dev/null +++ b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroupDashboard.tsx @@ -0,0 +1,198 @@ +import React, { Component, useContext, useEffect, useState } from "react"; +import styled from "styled-components"; + +import sliders from "assets/sliders.svg"; + +import { Context } from "shared/Context"; +import { ClusterType } from "shared/types"; + +import ExpandedEnvGroup from "./ExpandedEnvGroup"; +import { RouteComponentProps, useParams, withRouter } from "react-router"; +import { getQueryParam, pushQueryParams } from "shared/routing"; +import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc"; +import { useQuery } from "@tanstack/react-query"; +import api from "shared/api"; +import Loading from "components/Loading"; +import Placeholder from "components/Placeholder"; + +type PropsType = RouteComponentProps & + WithAuthProps & { + currentCluster: ClusterType; + }; + +const EnvGroupDashboard = (props: PropsType) => { + const namespace = (currentProject?.simplified_view_enabled && currentProject?.capi_provisioner_enabled) ? "porter-env-group" : getQueryParam(props, "namespace"); + const params = useParams<{ name: string }>(); + const { currentProject } = useContext(Context); + const [expandedEnvGroup, setExpandedEnvGroup] = useState(); + const isTabActive = () => { + return !document.hidden; + }; + + const { + data: envGroups, + isLoading: listEnvGroupsLoading, + isError, + refetch, + } = useQuery( + ["envGroupList", currentProject.id, namespace, props.currentCluster.id], + async () => { + try { + if (!namespace) { + if (!currentProject?.simplified_view_enabled) { + return []; + } + } + let res: any[] = []; + if (currentProject?.simplified_view_enabled) { + res = await api.getAllEnvGroups( + "", + {}, + { + id: currentProject.id, + cluster_id: props.currentCluster.id, + } + ); + } else { + + res = await api.listEnvGroups( + "", + {}, + { + id: currentProject.id, + namespace: currentProject?.simplified_view_enabled ? "porter-env-group" : namespace, + cluster_id: props.currentCluster.id, + } + ); + } + return currentProject?.simplified_view_enabled ? res.data?.environment_groups : res.data; + } catch (err) { + throw err; + } + }, + { + enabled: false, // Initially disable the query + } + ); + + useEffect(() => { + const name = params.name; + + if (!envGroups || !isTabActive()) { + return; + } + + const envGroup = envGroups.find((envGroup) => envGroup.name === name); + setExpandedEnvGroup(envGroup); + }, [envGroups, params]); + + useEffect(() => { + if (isTabActive()) { + refetch(); // Run the query when the component mounts and the tab is active + } + }, []); + if (listEnvGroupsLoading) { + return ( + + + + ); + } + + const renderContents = () => { + if (!expandedEnvGroup) { + return null; + } + + return ( + props.history.push("/env-groups")} + /> + ); + }; + + if (listEnvGroupsLoading) { + return ( + + + + ); + } + + return <>{renderContents()}; +}; + +export default withRouter(withAuth(EnvGroupDashboard)); + +const Flex = styled.div` + display: flex; + align-items: center; + border-bottom: 30px solid transparent; +`; + +const SortFilterWrapper = styled.div` + display: flex; + justify-content: space-between; + border-bottom: 30px solid transparent; + > div:not(:first-child) { + } +`; + +const ControlRow = styled.div` + display: flex; + justify-content: ${(props: { hasMultipleChilds: boolean }) => { + if (props.hasMultipleChilds) { + return "space-between"; + } + return "flex-end"; + }}; + align-items: center; + flex-wrap: wrap; +`; + +const Button = styled.div` + display: flex; + margin-left: 10px; + flex-direction: row; + align-items: center; + justify-content: space-between; + font-size: 13px; + cursor: pointer; + font-family: "Work Sans", sans-serif; + border-radius: 5px; + color: white; + height: 30px; + padding: 0 8px; + min-width: 155px; + padding-right: 13px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: ${(props: { disabled?: boolean }) => + props.disabled ? "not-allowed" : "pointer"}; + + background: ${(props: { disabled?: boolean }) => + props.disabled ? "#aaaabbee" : "#616FEEcc"}; + :hover { + background: ${(props: { disabled?: boolean }) => + props.disabled ? "" : "#505edddd"}; + } + + > i { + color: white; + width: 18px; + height: 18px; + font-weight: 600; + font-size: 12px; + border-radius: 20px; + display: flex; + align-items: center; + margin-right: 5px; + justify-content: center; + } +`; From 602a1737cd1c00d3d38f8ae61a3bbd79c0153bf1 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Thu, 2 May 2024 17:03:04 -0400 Subject: [PATCH 7/8] re-add stuff --- .../cluster-dashboard/DashboardRouter.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx b/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx index 48b09dd6a8..d9958ea8bb 100644 --- a/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx +++ b/dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx @@ -20,6 +20,8 @@ import DashboardRoutes from "./dashboard/Routes"; import GuardedRoute from "shared/auth/RouteGuard"; import AppDashboard from "./apps/AppDashboard"; import JobDashboard from "./jobs/JobDashboard"; +import ExpandedEnvGroupDashboard from "./env-groups/ExpandedEnvGroupDashboard"; +import EnvGroupDashboard from "./env-groups/EnvGroupDashboard"; const LazyPreviewEnvironmentsRoutes = loadable( // @ts-ignore @@ -146,6 +148,24 @@ const DashboardRouter: React.FC = ({ sortType={sortType} /> + + + + + + From 84839533a1908f40dfc8138e8ac90d4e0d483119 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Thu, 2 May 2024 17:25:49 -0400 Subject: [PATCH 8/8] re-add stuff --- dashboard/src/main/home/Home.tsx | 47 +- .../env-groups/CreateEnvGroup.tsx | 459 ++++++++++++ .../cluster-dashboard/env-groups/EnvGroup.tsx | 211 ++++++ .../env-groups/EnvGroupList.tsx | 163 +++++ .../env-groups/ExpandedEnvGroup.tsx | 662 ++++++------------ 5 files changed, 1055 insertions(+), 487 deletions(-) create mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx create mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx create mode 100644 dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx diff --git a/dashboard/src/main/home/Home.tsx b/dashboard/src/main/home/Home.tsx index 45504fc5cb..177a806714 100644 --- a/dashboard/src/main/home/Home.tsx +++ b/dashboard/src/main/home/Home.tsx @@ -221,7 +221,7 @@ const Home: React.FC = (props) => { } else { setHasFinishedOnboarding(true); } - } catch (error) { } + } catch (error) {} }; useEffect(() => { @@ -508,8 +508,8 @@ const Home: React.FC = (props) => { {currentProject?.capi_provisioner_enabled && - currentProject?.simplified_view_enabled && - currentProject?.beta_features_enabled ? ( + currentProject?.simplified_view_enabled && + currentProject?.beta_features_enabled ? ( ) : ( @@ -523,8 +523,8 @@ const Home: React.FC = (props) => { {currentProject?.capi_provisioner_enabled && - currentProject?.simplified_view_enabled && - currentProject?.beta_features_enabled ? ( + currentProject?.simplified_view_enabled && + currentProject?.beta_features_enabled ? ( ) : ( @@ -606,28 +606,21 @@ const Home: React.FC = (props) => { path={"/project-settings"} render={() => } /> - {currentProject?.validate_apply_v2 && ( - <> - - - - - - - - - - - - - - - - - )} + + + + + + + + + + + + + + + } /> diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx new file mode 100644 index 0000000000..7c1799929b --- /dev/null +++ b/dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx @@ -0,0 +1,459 @@ +import React, { useState, useEffect, useContext } from 'react'; +import styled from 'styled-components'; +import api from 'shared/api'; + +import { Context } from 'shared/Context'; +import { type ClusterType } from 'shared/types'; + +import InputRow from 'components/form-components/InputRow'; +import EnvGroupArray, { type KeyValueType } from './EnvGroupArray'; +import Selector from 'components/Selector'; +import Helper from 'components/form-components/Helper'; +import SaveButton from 'components/SaveButton'; +import { isAlphanumeric } from 'shared/common'; + +type PropsType = { + goBack: () => void; + currentCluster: ClusterType; +}; + +const CreateEnvGroup = ({ goBack, currentCluster }: PropsType) => { + const [envGroupName, setEnvGroupName] = useState(''); + const [selectedNamespace, setSelectedNamespace] = useState('default'); + const [namespaceOptions, setNamespaceOptions] = useState([]); + const [envVariables, setEnvVariables] = useState([]); + const [submitStatus, setSubmitStatus] = useState(''); + + const context = useContext(Context); + + useEffect(() => { + updateNamespaces(); + }, []); + + const isDisabled = (): boolean => { + const isEnvGroupNameInvalid = + !isAlphanumeric(envGroupName) || + envGroupName === '' || + envGroupName.length > 60; + + const isAnyEnvVariableBlank = envVariables.some( + (envVar) => !envVar.key.trim() || !envVar.value.trim() + ); + + + + return isEnvGroupNameInvalid || isAnyEnvVariableBlank; + }; + + const onSubmit = (): void => { + setSubmitStatus("loading") + + const apiEnvVariables: Record = {}; + const secretEnvVariables: Record = {}; + + const envVariable = envVariables; + + if (context.currentProject.simplified_view_enabled) { + api + .createNamespace( + "", + { + name: "porter-env-group", + }, + { + id: context.currentProject.id, + cluster_id: currentCluster.id, + } + ) + .catch((error) => { + if (error.response && error.response.status === 412) { + // do nothing + } else { + // do nothing still + } + }); + } + envVariable + .filter((envVar: KeyValueType, index: number, self: KeyValueType[]) => { + // remove any collisions that are marked as deleted and are duplicates + const numCollisions = self.reduce((n, _envVar: KeyValueType) => { + return n + (_envVar.key === envVar.key ? 1 : 0); + }, 0); + + if (numCollisions === 1) { + return true; + } else { + return ( + index === + self.findIndex( + (_envVar: KeyValueType) => + _envVar.key === envVar.key && !_envVar.deleted + ) + ); + } + }) + .forEach((envVar: KeyValueType) => { + if (!envVar.deleted) { + if (envVar.hidden) { + secretEnvVariables[envVar.key] = envVar.value; + } else { + apiEnvVariables[envVar.key] = envVar.value; + } + } + }); + + api + .createEnvGroup( + "", + { + name: envGroupName, + variables: apiEnvVariables, + secret_variables: secretEnvVariables, + }, + { + id: context.currentProject.id, + cluster_id: currentCluster.id, + namespace: context.currentProject.simplified_view_enabled ? "porter-env-group" : selectedNamespace, + } + ) + .then((res) => { + setSubmitStatus("successful"); + // console.log(res); + goBack(); + }) + .catch((err) => { + setSubmitStatus("Could not create"); + }); + }; + + const createEnv = () => { + setSubmitStatus("loading") + + const apiEnvVariables: Record = {}; + const secretEnvVariables: Record = {}; + + const envVariable = envVariables; + envVariable + .filter((envVar: KeyValueType, index: number, self: KeyValueType[]) => { + // remove any collisions that are marked as deleted and are duplicates + const numCollisions = self.reduce((n, _envVar: KeyValueType) => { + return n + (_envVar.key === envVar.key ? 1 : 0); + }, 0); + + if (numCollisions === 1) { + return true; + } else { + return ( + index === + self.findIndex( + (_envVar: KeyValueType) => + _envVar.key === envVar.key && !_envVar.deleted + ) + ); + } + }) + .forEach((envVar: KeyValueType) => { + if (!envVar.deleted) { + if (envVar.hidden) { + secretEnvVariables[envVar.key] = envVar.value; + } else { + apiEnvVariables[envVar.key] = envVar.value; + } + } + }); + + api + .createEnvironmentGroups( + "", + { + name: envGroupName, + variables: apiEnvVariables, + secret_variables: secretEnvVariables, + }, + { + id: context.currentProject.id, + cluster_id: currentCluster.id, + } + ) + .then((res) => { + setSubmitStatus("successful"); + // console.log(res); + goBack(); + }) + .catch((err) => { + if (err) { + setSubmitStatus("Could not create"); + } + }); + }; + + const updateNamespaces = () => { + const { currentProject } = context; + api + .getNamespaces( + "", + {}, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } + ) + .then((res) => { + if (res.data) { + const availableNamespaces = res.data.filter((namespace: any) => { + return namespace.status !== "Terminating"; + }); + const namespaceOptions = availableNamespaces.map( + (x: { name: string }) => { + return { label: x.name, value: x.name }; + } + ); + if (availableNamespaces.length > 0) { + setNamespaceOptions(namespaceOptions); + } + } + }) + .catch(console.log); + }; + + + return ( + <> + + + + Create an environment group + + + + Name + + 60) && + envGroupName !== "" + } + > + Lowercase letters, numbers, and "-" only. Maximum 60 characters. + + + + { setEnvGroupName(x) }} + placeholder="ex: my-env-group" + width="100%" + /> + {!context?.currentProject?.simplified_view_enabled && (<> + Destination + + Specify the namespace you would like to create this environment + group in. + + + + view_listNamespace + + { setSelectedNamespace(namespace) }} + options={namespaceOptions} + width="250px" + dropdownWidth="335px" + closeOverlay={true} + /> + + + ) + } + Environment variables + + Set environment variables for your secrets and environment-specific + configuration. + + { setEnvVariables(x); }} + fileUpload={true} + secretOption={true} + /> + + + + + + ); + +} + +export default CreateEnvGroup; + +const Wrapper = styled.div` + padding: 30px; + padding-bottom: 25px; + border-radius: 5px; + margin-top: -15px; + background: ${props => props.theme.fg}; + border: 1px solid #494b4f; + margin-bottom: 30px; +`; + +const Buffer = styled.div` + width: 100%; + height: 150px; +`; + +const StyledCreateEnvGroup = styled.div` + padding-bottom: 70px; + position: relative; +`; + +const NamespaceLabel = styled.div` + margin-right: 10px; + display: flex; + align-items: center; + > i { + font-size: 16px; + margin-right: 6px; + } +`; + +const DestinationSection = styled.div` + display: flex; + align-items: center; + color: #ffffff; + font-family: "Work Sans", sans-serif; + font-size: 14px; + margin-top: 2px; + font-weight: 500; + margin-bottom: 32px; + + > i { + font-size: 25px; + color: #ffffff44; + margin-right: 13px; + } +`; + +const Button = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + font-size: 13px; + cursor: pointer; + font-family: "Work Sans", sans-serif; + border-radius: 20px; + color: white; + height: 35px; + margin-left: -2px; + padding: 0px 8px; + padding-bottom: 1px; + font-weight: 500; + padding-right: 15px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; + border: 2px solid #969fbbaa; + :hover { + background: #ffffff11; + } + + > i { + color: white; + width: 18px; + height: 18px; + color: #969fbbaa; + font-weight: 600; + font-size: 14px; + border-radius: 20px; + display: flex; + align-items: center; + margin-right: 5px; + justify-content: center; + } +`; + +const DarkMatter = styled.div<{ antiHeight?: string }>` + width: 100%; + margin-top: ${(props) => props.antiHeight || "-15px"}; +`; + +const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>` + color: ${(props) => (props.highlight ? "#f5cb42" : "")}; + margin-left: ${(props) => (props.makeFlush ? "" : "5px")}; +`; + +const Subtitle = styled.div` + padding: 11px 0px 16px; + font-family: "Work Sans", sans-serif; + font-size: 13px; + color: #aaaabb; + line-height: 1.6em; + display: flex; + align-items: center; +`; + +const Title = styled.div` + font-size: 20px; + font-weight: 500; + font-family: "Work Sans", sans-serif; + margin-left: 15px; + border-radius: 2px; + color: #ffffff; +`; + +const HeaderSection = styled.div` + display: flex; + align-items: center; + margin-bottom: 40px; + + > i { + cursor: pointer; + font-size: 20px; + color: #969fbbaa; + padding: 2px; + border: 2px solid #969fbbaa; + border-radius: 100px; + :hover { + background: #ffffff11; + } + } + + > img { + width: 20px; + margin-left: 17px; + margin-right: 7px; + } +`; + +const Heading = styled.div<{ isAtTop?: boolean }>` + color: white; + font-weight: 500; + font-size: 16px; + margin-bottom: 5px; + margin-top: ${(props) => (props.isAtTop ? "10px" : "30px")}; + display: flex; + align-items: center; +`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx new file mode 100644 index 0000000000..44ea49b16e --- /dev/null +++ b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx @@ -0,0 +1,211 @@ +import React, { Component } from "react"; +import styled from "styled-components"; + +import sliders from "assets/sliders.svg"; +import doppler from "assets/doppler.png"; + +import { Context } from "shared/Context"; +import { readableDate } from "shared/string_utils"; +import { Link } from "react-router-dom"; +import _ from "lodash"; + +export type EnvGroupData = { + name: string; + type?: string; + namespace: string; + created_at?: string; + version: number; +}; + +type PropsType = { + envGroup: EnvGroupData; +}; + +type StateType = { + update: any[]; +}; + +export default class EnvGroup extends Component { + state = { + update: [] as any[], + }; + + render() { + const { envGroup } = this.props; + const name = envGroup?.name; + const timestamp = envGroup?.created_at; + const namespace = envGroup?.namespace; + const version = this.context?.currentProject.simplified_view_enabled ? envGroup?.latest_version : envGroup?.version ; + + return ( + + + + <IconWrapper> + <Icon src={envGroup.type === "doppler" ? doppler : sliders} /> + </IconWrapper> + {name} + + + + + + Last updated {readableDate(timestamp)} + + + + {!this.context?.currentProject.simplified_view_enabled && + Namespace + {namespace.startsWith("porter-stack-") ? namespace.replace("porter-stack-", "") : namespace} + } + + + v{version} + + + ); + } +} + +export function formattedEnvironmentValue(value: string) { + if (value.startsWith("PORTERSECRET_")) { + return "••••"; + } + return value; +} + +EnvGroup.contextType = Context; + +const BottomWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding-right: 11px; + margin-top: 3px; +`; + +const Version = styled.div` + position: absolute; + top: 12px; + right: 12px; + font-size: 12px; + color: #aaaabb; +`; + +const Dot = styled.div` + margin-right: 9px; +`; + +const InfoWrapper = styled.div` + display: flex; + align-items: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-right: 8px; +`; + +const LastDeployed = styled.div` + font-size: 13px; + margin-left: 14px; + margin-bottom: -1px; + display: flex; + align-items: center; + color: #aaaabb66; +`; + +const TagWrapper = styled.div` + height: 20px; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + color: #ffffff44; + border: 1px solid #ffffff44; + border-radius: 3px; + padding-left: 5px; +`; + +const NamespaceTag = styled.div` + height: 20px; + margin-left: 6px; + color: #aaaabb; + background: #ffffff22; + border-radius: 3px; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + padding: 0px 6px; + padding-left: 7px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +const Icon = styled.img` + width: 100%; +`; + +const IconWrapper = styled.div` + color: #efefef; + background: none; + font-size: 16px; + top: 11px; + left: 14px; + height: 20px; + width: 20px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 3px; + position: absolute; + + > i { + font-size: 17px; + margin-top: -1px; + } +`; + +const Title = styled.div` + position: relative; + text-decoration: none; + padding: 12px 35px 12px 45px; + font-size: 14px; + font-family: "Work Sans", sans-serif; + font-weight: 500; + color: #ffffff; + width: 80%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + animation: fadeIn 0.5s; + + > img { + background: none; + top: 12px; + left: 13px; + + padding: 5px 4px; + width: 24px; + position: absolute; + } +`; + +const StyledEnvGroup = styled.div` + cursor: pointer; + margin-bottom: 15px; + padding-top: 2px; + padding-bottom: 13px; + position: relative; + width: calc(100% + 2px); + height: calc(100% + 2px); + border-radius: 5px; + background: ${props => props.theme.clickable.bg}; + border: 1px solid #494b4f; + :hover { + border: 1px solid #7a7b80; + } +`; diff --git a/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx new file mode 100644 index 0000000000..7fc32f4259 --- /dev/null +++ b/dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx @@ -0,0 +1,163 @@ +import React, { useContext, useEffect, useMemo, useState } from "react"; +import styled from "styled-components"; + +import { Context } from "shared/Context"; +import api from "shared/api"; +import { ClusterType } from "shared/types"; + +import EnvGroup from "./EnvGroup"; +import Loading from "components/Loading"; +import { getQueryParam, pushQueryParams } from "shared/routing"; +import { RouteComponentProps, withRouter } from "react-router"; + +import Placeholder from "components/Placeholder"; + +type Props = RouteComponentProps & { + currentCluster: ClusterType; + namespace: string; + sortType: string; + setExpandedEnvGroup: (envGroup: any) => void; +}; + +type State = { + envGroups: any[]; + loading: boolean; + error: boolean; +}; + +const EnvGroupList: React.FunctionComponent = (props) => { + const context = useContext(Context); + + const { currentCluster, namespace, sortType, setExpandedEnvGroup } = props; + + const [envGroups, setEnvGroups] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + + const updateEnvGroups = async () => { + let { currentProject, currentCluster } = context; + try { + let envGroups: any[] = [] + if (currentProject?.simplified_view_enabled) { + envGroups = await api + .getAllEnvGroups( + "", + {}, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } + ) + .then((res) => { + return res.data?.environment_groups; + }); + } else { + envGroups = await api + .listEnvGroups( + "", + {}, + { + id: currentProject.id, + namespace: namespace, + cluster_id: currentCluster.id, + } + ) + .then((res) => { + return res.data; + }); + } + let sortedGroups = envGroups; + if (sortedGroups) { + switch (sortType) { + case "Oldest": + sortedGroups.sort((a: any, b: any) => + Date.parse(a.created_at) > Date.parse(b.created_at) ? 1 : -1 + ); + break; + case "Alphabetical": + sortedGroups.sort((a: any, b: any) => (a.name > b.name ? 1 : -1)); + break; + default: + sortedGroups.sort((a: any, b: any) => + Date.parse(a.created_at) > Date.parse(b.created_at) ? -1 : 1 + ); + } + } + return sortedGroups; + } catch (error) { + console.log(error) + setIsLoading(false); + setHasError(true); + } + }; + + useEffect(() => { + // Prevents reload when opening ClusterConfigModal + (namespace || namespace === "") && + updateEnvGroups().then((envGroups) => { + const selectedEnvGroup = getQueryParam(props, "selected_env_group"); + + setEnvGroups(envGroups); + if (envGroups && envGroups.length > 0) { + setHasError(false); + } + setIsLoading(false); + + if (selectedEnvGroup) { + // find env group by selectedEnvGroup + const envGroup = envGroups.find( + (envGroup: any) => envGroup.name === selectedEnvGroup + ); + if (envGroup) { + setExpandedEnvGroup(envGroup); + } else { + pushQueryParams(props, {}, ["selected_env_group"]); + } + } + }); + }, [currentCluster, namespace, sortType]); + + const renderEnvGroupList = () => { + if (isLoading || (!namespace && namespace !== "")) { + return ( + + + + ); + } else if (hasError) { + return ( + + error Error connecting to cluster. + + ); + } else if (!envGroups || envGroups.length === 0) { + return ( + + category + No environment groups found with the given filters. + + ); + } + + return envGroups.map((envGroup: any, i: number) => { + return ( + + ); + }); + }; + + return {renderEnvGroupList()}; +}; + +export default withRouter(EnvGroupList); + +const LoadingWrapper = styled.div` + padding-top: 100px; +`; + +const StyledEnvGroupList = styled.div` + padding-bottom: 85px; +`; 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 8c640d625a..9bbb8bcd8c 100644 --- a/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx +++ b/dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx @@ -1,44 +1,32 @@ -import React, { - Component, - useContext, - useEffect, - useMemo, - useState, -} from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import yaml from "js-yaml"; -import { createFinalPorterYaml, PorterYamlSchema } from "../../app-dashboard/new-app-flow/schema" +import _, { remove } from "lodash"; import styled, { keyframes } from "styled-components"; -import backArrow from "assets/back_arrow.png"; -import key from "assets/key.svg"; -import loading from "assets/loading.gif"; -import leftArrow from "assets/left-arrow.svg"; - -import { type ChartType, type ClusterType, CreateUpdatePorterAppOptions } from "shared/types"; -import { Context } from "shared/Context"; -import { isAlphanumeric } from "shared/common"; -import api from "shared/api"; -import TitleSection from "components/TitleSection"; -import SaveButton from "components/SaveButton"; -import TabRegion from "components/TabRegion"; -import EnvGroupArray, { type KeyValueType } from "./EnvGroupArray"; +import DocsHelper from "components/DocsHelper"; +import DynamicLink from "components/DynamicLink"; import Heading from "components/form-components/Heading"; import Helper from "components/form-components/Helper"; import InputRow from "components/form-components/InputRow"; -import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc"; -import _, { flatMapDepth, remove, update } from "lodash"; -import { type NewPopulatedEnvGroup, type PopulatedEnvGroup } from "components/porter-form/types"; -import { isAuthorized } from "shared/auth/authorization-helpers"; -import useAuth from "shared/auth/useAuth"; -import { fillWithDeletedVariables } from "components/porter-form/utils"; -import DynamicLink from "components/DynamicLink"; -import DocsHelper from "components/DocsHelper"; +import { + type NewPopulatedEnvGroup, + type PopulatedEnvGroup, +} from "components/porter-form/types"; import Spacer from "components/porter/Spacer"; -import EnvGroups from "../stacks/ExpandedStack/components/EnvGroups"; -import { type PorterJson } from "main/home/app-dashboard/new-app-flow/schema"; -import { BuildMethod, PorterApp } from "main/home/app-dashboard/types/porterApp"; -import { Service } from "main/home/app-dashboard/new-app-flow/serviceTypes"; -import { consoleSandbox } from "@sentry/utils"; +import SaveButton from "components/SaveButton"; +import TabRegion from "components/TabRegion"; +import TitleSection from "components/TitleSection"; + +import api from "shared/api"; +import { type WithAuthProps } from "shared/auth/AuthorizationHoc"; +import useAuth from "shared/auth/useAuth"; +import { Context } from "shared/Context"; +import { type ClusterType } from "shared/types"; +import key from "assets/key.svg"; +import leftArrow from "assets/left-arrow.svg"; +import loading from "assets/loading.gif"; + +import EnvGroupArray, { type KeyValueType } from "./EnvGroupArray"; type PropsType = WithAuthProps & { namespace: string; @@ -79,32 +67,16 @@ export const ExpandedEnvGroupFC = ({ closeExpanded, allEnvGroups, }: PropsType) => { - const { - currentProject, - currentCluster, - setCurrentOverlay, - setCurrentError, - } = useContext(Context); + const { currentProject, currentCluster, setCurrentOverlay, setCurrentError } = + useContext(Context); const [isAuthorized] = useAuth(); - const [workflowCheckPassed, setWorkflowCheckPassed] = useState( - false - ); - const [isLoading, setIsLoading] = useState(true); - const [currentTab, setCurrentTab] = useState("variables-editor"); const [isDeleting, setIsDeleting] = useState(false); const [buttonStatus, setButtonStatus] = useState(""); - const [services, setServices] = useState([]); - const [envVars, setEnvVars] = useState([]); - const [subdomain, setSubdomain] = useState(""); - - - const [currentEnvGroup, setCurrentEnvGroup] = useState( - null - ); - const [hasBuiltImage, setHasBuiltImage] = useState(false); + const [currentEnvGroup, setCurrentEnvGroup] = + useState(null); const [originalEnvVars, setOriginalEnvVars] = useState< Array<{ key: string; @@ -112,48 +84,15 @@ export const ExpandedEnvGroupFC = ({ }> >(); - - const fetchPorterYamlContent = async ( - porterYaml: string, - appData: any - ) => { - try { - if (porterYaml && appData?.app?.git_repo_id) { - const res = await api.getPorterYamlContents( - "", - { - path: porterYaml, - }, - { - project_id: appData.app.project_id, - git_repo_id: appData.app.git_repo_id, - owner: appData.app.repo_name?.split("/")[0], - name: appData.app.repo_name?.split("/")[1], - kind: "github", - branch: appData.app.git_branch, - } - ); - if (res.data == null || res.data == "") { - return undefined; - } - const parsedYaml = yaml.load(atob(res.data)); - - return parsedYaml - } - } catch (err) { - // TODO: handle error - console.log("No Porter Yaml") - - } - }; - const tabOptions = useMemo(() => { if (!isAuthorized("env_group", "", ["get", "delete"])) { return [{ value: "variables-editor", label: "Environment variables" }]; } if ( !isAuthorized("env_group", "", ["get", "delete"]) && - (currentProject?.simplified_view_enabled ? currentEnvGroup?.linked_applications?.length : currentEnvGroup?.applications?.length) + (currentProject?.simplified_view_enabled + ? currentEnvGroup?.linked_applications?.length + : currentEnvGroup?.applications?.length) ) { return [ { value: "variables-editor", label: "Environment variables" }, @@ -161,7 +100,11 @@ export const ExpandedEnvGroupFC = ({ ]; } - if (currentProject?.simplified_view_enabled ? currentEnvGroup?.linked_applications?.length : currentEnvGroup?.applications?.length) { + if ( + currentProject?.simplified_view_enabled + ? currentEnvGroup?.linked_applications?.length + : currentEnvGroup?.applications?.length + ) { return [ { value: "variables-editor", label: "Environment variables" }, { value: "applications", label: "Linked applications" }, @@ -175,11 +118,7 @@ export const ExpandedEnvGroupFC = ({ ]; }, [currentEnvGroup]); const populateEnvGroup = async () => { - - // apply v2 already supplies the full env group - if (currentProject?.validate_apply_v2) { - updateEnvGroup(envGroup); - } else if (currentProject?.simplified_view_enabled) { + if (currentProject?.simplified_view_enabled) { try { const populatedEnvGroup = await api .getAllEnvGroups( @@ -191,7 +130,9 @@ export const ExpandedEnvGroupFC = ({ } ) .then((res) => res.data.environment_groups); - updateEnvGroup(populatedEnvGroup.find((i: any) => i.name === envGroup.name)); + updateEnvGroup( + populatedEnvGroup.find((i: any) => i.name === envGroup.name) + ); } catch (error) { console.log(error); } @@ -217,8 +158,6 @@ export const ExpandedEnvGroupFC = ({ }; const updateEnvGroup = (populatedEnvGroup: NewPopulatedEnvGroup) => { - - if (currentProject?.simplified_view_enabled) { const normal_variables: KeyValueType[] = Object.entries( populatedEnvGroup.variables || {} @@ -240,7 +179,6 @@ export const ExpandedEnvGroupFC = ({ })); const variables = [...normal_variables, ...secret_variables]; - setOriginalEnvVars( Object.entries({ ...(populatedEnvGroup?.variables || {}), @@ -255,7 +193,6 @@ export const ExpandedEnvGroupFC = ({ ...populatedEnvGroup, variables, }); - } else { const variables: KeyValueType[] = Object.entries( populatedEnvGroup.variables || {} @@ -268,17 +205,18 @@ export const ExpandedEnvGroupFC = ({ })); setOriginalEnvVars( - Object.entries(populatedEnvGroup?.variables || {}).map(([key, value]) => ({ - key, - value, - })) + Object.entries(populatedEnvGroup?.variables || {}).map( + ([key, value]) => ({ + key, + value, + }) + ) ); setCurrentEnvGroup({ ...populatedEnvGroup, variables, }); - } }; @@ -312,7 +250,6 @@ export const ExpandedEnvGroupFC = ({ ); } - return await api.deleteEnvGroup( "", { @@ -340,228 +277,14 @@ export const ExpandedEnvGroupFC = ({ }); }; - const getPorterApp = async ({ appName }: { appName: string }) => { - try { - if (!currentCluster || !currentProject) { - return; - } - const resPorterApp = await api.getPorterApp( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - name: appName, - } - ); - const resChartData = await api.getChart( - "", - {}, - { - id: currentProject.id, - namespace: `porter-stack-${appName}`, - cluster_id: currentCluster.id, - name: appName, - revision: 0, - } - ); - - let preDeployChartData; - // get the pre-deploy chart - try { - preDeployChartData = await api.getChart( - "", - {}, - { - id: currentProject.id, - namespace: `porter-stack-${appName}`, - cluster_id: currentCluster.id, - name: `${appName}-r`, - // this is always latest because we do not tie the pre-deploy chart to the umbrella chart - revision: 0, - } - ); - } catch (err) { - // that's ok if there's an error, just means there is no pre-deploy chart - } - - // update apps and release - const newAppData = { - app: resPorterApp?.data, - chart: resChartData?.data, - releaseChart: preDeployChartData?.data, - }; - const porterJson = await fetchPorterYamlContent( - resPorterApp?.data?.porter_yaml_path ?? "porter.yaml", - newAppData - ); - - let filteredEnvGroups: NewPopulatedEnvGroup[] = [] - filteredEnvGroups = allEnvGroups?.filter(envGroup => - envGroup.linked_applications && envGroup.linked_applications.includes(appName) - ); - - const parsedPorterApp = { ...resPorterApp?.data, buildpacks: newAppData.app.buildpacks?.split(",") ?? [] }; - const buildView = !_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks" - - const [newServices, newEnvVars] = updateServicesAndEnvVariables( - resChartData?.data, - preDeployChartData?.data, - porterJson, - ); - const finalPorterYaml = createFinalPorterYaml( - newServices, - newEnvVars, - porterJson, - // if we are using a heroku buildpack, inject a PORT env variable - newAppData.app.builder?.includes("heroku") - ); - - - // Only check GHA status if no built image is set - const hasBuiltImage = !!resChartData.data.config?.global?.image - ?.repository; - if (hasBuiltImage || !resPorterApp.data.repo_name) { - setWorkflowCheckPassed(true); - setHasBuiltImage(true); - } else { - try { - await api.getBranchContents( - "", - { - dir: `./.github/workflows/porter_stack_${resPorterApp.data.name}.yml`, - }, - { - project_id: currentProject.id, - git_repo_id: resPorterApp.data.git_repo_id, - kind: "github", - owner: resPorterApp.data.repo_name.split("/")[0], - name: resPorterApp.data.repo_name.split("/")[1], - branch: resPorterApp.data.git_branch, - } - ); - setWorkflowCheckPassed(true); - - } catch (err) { - // Handle unmerged PR - if (err.response?.status === 404) { - try { - // Check for user-copied porter.yml as fallback - const resPorterYml = await api.getBranchContents( - "", - { dir: `./.github/workflows/porter.yml` }, - { - project_id: currentProject.id, - git_repo_id: resPorterApp.data.git_repo_id, - kind: "github", - owner: resPorterApp.data.repo_name.split("/")[0], - name: resPorterApp.data.repo_name.split("/")[1], - branch: resPorterApp.data.git_branch, - } - ); - setWorkflowCheckPassed(true); - } catch (err) { - setWorkflowCheckPassed(false); - } - } - } - } - - if ( - currentCluster != null && - currentProject != null - ) { - - const yamlString = yaml.dump(finalPorterYaml); - const base64Encoded = btoa(yamlString); - - const updatedPorterApp = { - porter_yaml: base64Encoded, - override_release: true, - ...PorterApp.empty(), - build_context: newAppData?.build_context, - repo_name: newAppData?.repo_name, - git_branch: newAppData?.git_branch, - buildpacks: "", - // full_helm_values: yaml.dump(values), - environment_groups: filteredEnvGroups?.map((env) => env.name), - user_update: true, - } - - if (buildView === "docker") { - updatedPorterApp.dockerfile = newAppData?.dockerfile; - updatedPorterApp.builder = "null"; - updatedPorterApp.buildpacks = "null"; - } else { - updatedPorterApp.builder = newAppData?.builder; - updatedPorterApp.buildpacks = newAppData?.buildpacks?.join(","); - updatedPorterApp.dockerfile = "null"; - } - - await api.createPorterApp( - "", - updatedPorterApp, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - stack_name: appName, - } - ); - } else { - setButtonStatus("error"); - } - } catch (err) { - // TODO: handle error - } finally { - setIsLoading(false); - } - }; - - const updateServicesAndEnvVariables = ( - currentChart?: ChartType, - releaseChart?: ChartType, - porterJson?: PorterJson, - ): [Service[], KeyValueType[]] => { - // handle normal chart - const helmValues = currentChart?.config; - const defaultValues = (currentChart?.chart as any)?.values; - let newServices: Service[] = []; - let envVars: KeyValueType[] = []; - - if ( - (defaultValues && Object.keys(defaultValues).length > 0) || - (helmValues && Object.keys(helmValues).length > 0) - ) { - newServices = Service.deserialize(helmValues, defaultValues, porterJson); - const { global, ...helmValuesWithoutGlobal } = helmValues; - if (Object.keys(helmValuesWithoutGlobal).length > 0) { - envVars = Service.retrieveEnvFromHelmValues(helmValuesWithoutGlobal); - setEnvVars(envVars); - const subdomain = Service.retrieveSubdomainFromHelmValues( - newServices, - helmValuesWithoutGlobal - ); - setSubdomain(subdomain); - } - } - - // handle release chart - if (releaseChart?.config || porterJson?.release) { - const release = Service.deserializeRelease(releaseChart?.config, porterJson); - newServices.push(release); - } - - setServices(newServices); - - return [newServices, envVars]; - }; - const handleUpdateValues = async () => { setButtonStatus("loading"); const name = currentEnvGroup.name; const variables = currentEnvGroup?.variables; - if (currentEnvGroup.meta_version === 2 || currentProject?.simplified_view_enabled) { - + if ( + currentEnvGroup.meta_version === 2 || + currentProject?.simplified_view_enabled + ) { const secretVariables = remove(variables, (envVar) => { return !envVar.value.includes("PORTERSECRET") && envVar.hidden; }).reduce( @@ -582,7 +305,6 @@ export const ExpandedEnvGroupFC = ({ if (currentProject?.simplified_view_enabled) { try { - const normal_variables: KeyValueType[] = Object.entries( normalVariables || {} ).map(([key, value]) => ({ @@ -604,68 +326,69 @@ export const ExpandedEnvGroupFC = ({ })); const variables = [...normal_variables, ...secret_variables]; - setCurrentEnvGroup({ ...currentEnvGroup, variables, }); - const linkedApp: string[] = currentEnvGroup?.linked_applications; // doppler env groups update themselves, and we don't want to increment the version - if (currentEnvGroup?.type !== "doppler" && currentEnvGroup.type !== "infisical") { + if ( + currentEnvGroup?.type !== "doppler" && + currentEnvGroup.type !== "infisical" + ) { await api.createEnvironmentGroups( - "", - { - name, - variables: normalVariables, - secret_variables: secretVariables, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } + "", + { + name, + variables: normalVariables, + secret_variables: secretVariables, + }, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } ); } - if (!currentProject.validate_apply_v2) { - if (linkedApp) { - const promises = linkedApp.map(async appName => { - if (!currentProject.validate_apply_v2) { - await getPorterApp({ appName }); - } - }); - await Promise.all(promises); - } - } else { - try { - const res = await api.updateAppsLinkedToEnvironmentGroup( - "", - { - name: currentEnvGroup?.name, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - } catch (error) { - setCurrentError(error); - } + + try { + const res = await api.updateAppsLinkedToEnvironmentGroup( + "", + { + name: currentEnvGroup?.name, + }, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } + ); + } catch (error) { + setCurrentError(error); } - const populatedEnvGroup = await api.getAllEnvGroups("", {}, { - id: currentProject.id, - cluster_id: currentCluster.id, - }).then(res => res.data.environment_groups); + const populatedEnvGroup = await api + .getAllEnvGroups( + "", + {}, + { + id: currentProject.id, + cluster_id: currentCluster.id, + } + ) + .then((res) => res.data.environment_groups); - const newEnvGroup = populatedEnvGroup.find((i: any) => i.name === name); + const newEnvGroup = populatedEnvGroup.find( + (i: any) => i.name === name + ); updateEnvGroup(newEnvGroup); setButtonStatus("successful"); } catch (error) { setButtonStatus("Couldn't update successfully"); setCurrentError(error); - setTimeout(() => { setButtonStatus(""); }, 1000); + setTimeout(() => { + setButtonStatus(""); + }, 1000); } } else { try { @@ -689,16 +412,18 @@ export const ExpandedEnvGroupFC = ({ } updateEnvGroup(updatedEnvGroup); - setTimeout(() => { setButtonStatus(""); }, 1000); - } - catch (error) { + setTimeout(() => { + setButtonStatus(""); + }, 1000); + } catch (error) { setButtonStatus("Couldn't update successfully"); setCurrentError(error); - setTimeout(() => { setButtonStatus(""); }, 1000); + setTimeout(() => { + setButtonStatus(""); + }, 1000); } } - } - else { + } else { // SEPARATE THE TWO KINDS OF VARIABLES let secret = variables.filter( (variable) => @@ -815,11 +540,15 @@ export const ExpandedEnvGroupFC = ({ .then((res) => res.data); setButtonStatus("successful"); updateEnvGroup(updatedEnvGroup); - setTimeout(() => { setButtonStatus(""); }, 1000); + setTimeout(() => { + setButtonStatus(""); + }, 1000); } catch (error) { setButtonStatus("Couldn't update successfully"); setCurrentError(error); - setTimeout(() => { setButtonStatus(""); }, 1000); + setTimeout(() => { + setButtonStatus(""); + }, 1000); } } }; @@ -833,8 +562,9 @@ export const ExpandedEnvGroupFC = ({ case "variables-editor": return ( { setCurrentEnvGroup((prev) => ({ ...prev, variables: x })); } - } + onChange={(x) => { + setCurrentEnvGroup((prev) => ({ ...prev, variables: x })); + }} handleUpdateValues={handleUpdateValues} variables={variables} buttonStatus={buttonStatus} @@ -873,9 +603,17 @@ export const ExpandedEnvGroupFC = ({ {envGroup.name} - {!currentProject?.simplified_view_enabled && - Namespace {currentProject?.capi_provisioner_enabled && namespace.startsWith("porter-stack-") ? namespace.replace("porter-stack-", "") : namespace} - } + {!currentProject?.simplified_view_enabled && ( + + Namespace{" "} + + {currentProject?.capi_provisioner_enabled && + namespace.startsWith("porter-stack-") + ? namespace.replace("porter-stack-", "") + : namespace} + + + )} @@ -896,7 +634,9 @@ export const ExpandedEnvGroupFC = ({ ) : ( { setCurrentTab(x); }} + setCurrentTab={(x: string) => { + setCurrentTab(x); + }} options={tabOptions} color={null} > @@ -923,7 +663,7 @@ const EnvGroupVariablesEditor = ({ setButtonStatus: (status: string) => void; }) => { const [isAuthorized] = useAuth(); - const [buttonDisabled, setButtonDisabled] = useState(false) + const [buttonDisabled, setButtonDisabled] = useState(false); return ( @@ -954,7 +694,9 @@ const EnvGroupVariablesEditor = ({ {isAuthorized("env_group", "", ["get", "update"]) && ( { handleUpdateValues(); }} + onClick={() => { + handleUpdateValues(); + }} status={buttonStatus} disabled={buttonStatus == "loading" || buttonDisabled} makeFlush={true} @@ -975,19 +717,13 @@ const EnvGroupSettings = ({ handleDeleteEnvGroup: () => void; namespace?: string; }) => { - const { - setCurrentOverlay, - currentProject, - currentCluster, - setCurrentError, - } = useContext(Context); + const { setCurrentOverlay, currentProject, currentCluster, setCurrentError } = + useContext(Context); const [isAuthorized] = useAuth(); // When cloning an env group, append "-2" for the default name // (i.e. my-env-group-2) - const [name, setName] = useState( - envGroup.name + "-2" - ); + const [name, setName] = useState(envGroup.name + "-2"); const [cloneNamespace, setCloneNamespace] = useState("default"); const [cloneSuccess, setCloneSuccess] = useState(false); @@ -1053,7 +789,9 @@ const EnvGroupSettings = ({ setCurrentOverlay({ message: `Are you sure you want to delete ${envGroup.name}?`, onYes: handleDeleteEnvGroup, - onNo: () => { setCurrentOverlay(null); }, + onNo: () => { + setCurrentOverlay(null); + }, }); }} disabled={!canDelete} @@ -1070,14 +808,18 @@ const EnvGroupSettings = ({ { setName(x); }} + setValue={(x: string) => { + setName(x); + }} label="New env group name" placeholder="ex: my-cloned-env-group" /> { setCloneNamespace(x); }} + setValue={(x: string) => { + setCloneNamespace(x); + }} label="New env group namespace" placeholder="ex: default" /> @@ -1112,69 +854,69 @@ const ApplicationsList = ({ envGroup }: { envGroup: EditableEnvGroup }) => { disableMargin /> - {currentProject?.simplified_view_enabled ? ( - envGroup.linked_applications.map((appName) => { - return ( - - - - - {appName} - - - - {currentProject?.simplified_view_enabled ? ( - - open_in_new - - ) : ( - - open_in_new - - )} - - - - ); - }) - ) : ( - envGroup.applications.map((appName) => { - return ( - - - - - {appName} - - - - {currentProject?.simplified_view_enabled ? ( - - open_in_new - - ) : ( - - open_in_new - - )} - - - - ); - }) - )} + {currentProject?.simplified_view_enabled + ? envGroup.linked_applications.map((appName) => { + return ( + + + + + {appName} + + + + {currentProject?.simplified_view_enabled ? ( + + + open_in_new + + + ) : ( + + + open_in_new + + + )} + + + + ); + }) + : envGroup.applications.map((appName) => { + return ( + + + + + {appName} + + + + {currentProject?.simplified_view_enabled ? ( + + + open_in_new + + + ) : ( + + + open_in_new + + + )} + + + + ); + })} ); };
{revision.version}{readableDate(revision.info.last_deployed)} - {!imageTag ? ( - "N/A" - ) : isGithubApp && /^[0-9A-Fa-f]{7}$/g.test(imageTag) ? ( - { - e.stopPropagation(); - }} - > - {parsedImageTag} - - ) : ( - parsedImageTag - )} - v{revision.chart.metadata.version} - { - e.stopPropagation(); - this.setState({ rollbackRevision: revision.version }) - } - } - > - {isCurrent ? "Current" : "Revert"} - -
Revision no.Timestamp - {this.props.chart.git_action_config ? "Commit" : "Image Tag"} - Template versionRollback