From b5166aa78819cc476bd66fefbb6f304c9b1099d7 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Tue, 21 May 2024 16:44:02 -0400 Subject: [PATCH] Delete part 2 (#4671) --- .../app-dashboard/apps/SelectableAppList.tsx | 71 -- .../home/app-dashboard/types/buildpack.ts | 36 - .../v2/DeleteEnvModal.tsx | 47 - .../home/managed-addons/tabs/PostgresTabs.tsx | 205 ----- .../home/managed-addons/tabs/RedisTabs.tsx | 195 ----- .../main/home/managed-addons/tabs/shared.tsx | 34 - .../components/RegistryImageList.tsx | 110 --- .../dashboard/node-view/ConditionsTable.tsx | 63 -- .../dashboard/node-view/ExpandedNodeView.tsx | 200 ----- .../dashboard/node-view/NodeUsage.tsx | 134 --- .../expanded-chart/ExpandedJobChart.tsx | 819 ------------------ .../expanded-chart/GraphSection.tsx | 70 -- .../expanded-chart/ListSection.tsx | 163 ---- .../NotificationSettingsSection.tsx | 194 ----- .../expanded-chart/RevisionSection.tsx | 558 ------------ .../expanded-chart/SettingsSection.tsx | 491 ----------- .../expanded-chart/TagSelector.tsx | 349 -------- .../expanded-chart/ValuesYaml.tsx | 151 ---- .../build-settings/BuildSettingsTab.tsx | 598 ------------- .../_BuildpackConfigSection.tsx | 554 ------------ .../expanded-chart/build-settings/types.ts | 29 - .../deploy-status-section/ControllerTab.tsx | 391 --------- .../DeployStatusSection.tsx | 219 ----- .../deploy-status-section/PodDropdown.tsx | 149 ---- .../deploy-status-section/PodRow.tsx | 223 ----- .../deploy-status-section/ResourceTab.tsx | 316 ------- .../deploy-status-section/util.ts | 66 -- .../expanded-chart/events/EventList.tsx | 697 --------------- .../expanded-chart/events/EventsTab.tsx | 265 ------ .../expanded-chart/events/styles.ts | 155 ---- .../expanded-chart/graph/Edge.tsx | 100 --- .../expanded-chart/graph/GraphDisplay.tsx | 750 ---------------- .../expanded-chart/graph/InfoPanel.tsx | 245 ------ .../expanded-chart/graph/Node.tsx | 135 --- .../expanded-chart/graph/SelectRegion.tsx | 60 -- .../expanded-chart/graph/ZoomPanel.tsx | 91 -- .../incidents/DisabledNamespaces.ts | 9 - .../jobs/ConnectToJobInstructionsModal.tsx | 61 -- .../expanded-chart/jobs/ExpandedJobRun.tsx | 555 ------------ .../expanded-chart/jobs/JobList.tsx | 208 ----- .../expanded-chart/jobs/JobResource.tsx | 342 -------- .../expanded-chart/jobs/useJobs.ts | 397 --------- .../logs-section/LogsSection.tsx | 662 -------------- .../logs-section/useAgentLogs.ts | 436 ---------- .../expanded-chart/metrics/AreaChart.tsx | 497 ----------- .../metrics/JobMetricsSection.tsx | 561 ------------ .../metrics/MetricNormalizer.ts | 174 ---- .../expanded-chart/metrics/MetricsSection.tsx | 750 ---------------- .../expanded-chart/status/ControllerTab.tsx | 439 ---------- .../expanded-chart/status/Logs.tsx | 398 --------- .../expanded-chart/status/PodRow.tsx | 234 ----- .../expanded-chart/status/StatusSection.tsx | 288 ------ .../expanded-chart/status/types.ts | 19 - .../expanded-chart/status/useLogs.ts | 218 ----- .../expanded-chart/useStackEnvGroups.ts | 79 -- .../cluster-dashboard/jobs/JobDashboard.tsx | 226 ----- .../preview-environments/ConnectNewRepo.tsx | 617 ------------- .../ConnectNewRepoActionConfEditor.tsx | 116 --- .../components/ActionButton.tsx | 58 -- .../components/BranchFilterSelector.tsx | 97 --- .../components/ButtonEnablePREnvironments.tsx | 173 ---- .../components/NamespaceLabels.tsx | 164 ---- .../components/PorterYAMLErrorsModal.tsx | 98 --- .../components/PreviewEnvironmentsHeader.tsx | 104 --- .../components/styled.tsx | 84 -- .../deployments/DeploymentCard.tsx | 674 -------------- .../deployments/DeploymentDetail.tsx | 767 ---------------- .../deployments/DeploymentList.tsx | 602 ------------- .../deployments/PreviewEnvDeleted.tsx | 76 -- .../environments/CreateBranchEnvironment.tsx | 538 ------------ .../environments/CreateEnvironment.tsx | 115 --- .../environments/CreatePREnvironment.tsx | 591 ------------- .../environments/EnvironmentCard.tsx | 305 ------- .../environments/EnvironmentSettings.tsx | 493 ----------- .../environments/EnvironmentsList.tsx | 184 ---- .../preview-environments/errors.ts | 3 - .../preview-environments/mocks.ts | 177 ---- .../preview-environments/routes.tsx | 51 -- .../preview-environments/utils.ts | 50 -- .../cluster-dashboard/stacks/Dashboard.tsx | 156 ---- .../stacks/ExpandedStack/ExpandedStack.tsx | 358 -------- .../NewAppResource/_Settings.tsx | 160 ---- .../NewAppResource/_TemplateSelector.tsx | 162 ---- .../ExpandedStack/NewAppResource/index.tsx | 27 - .../stacks/ExpandedStack/NewEnvGroup.tsx | 66 -- .../stacks/ExpandedStack/Store.tsx | 99 --- .../stacks/ExpandedStack/_RevisionList.tsx | 320 ------- .../stacks/ExpandedStack/_SourceConfig.tsx | 222 ----- .../ExpandedStack/components/EnvGroups.tsx | 116 --- .../ExpandedStack/components/Select.tsx | 259 ------ .../ExpandedStack/components/Settings.tsx | 139 --- .../components/SourceEditorDocker.tsx | 328 ------- .../stacks/ExpandedStack/routes.tsx | 63 -- .../cluster-dashboard/stacks/_StackList.tsx | 264 ------ .../stacks/components/NewAppResourceForm.tsx | 316 ------- .../stacks/components/NewEnvGroupForm.tsx | 165 ---- .../stacks/components/Status.tsx | 25 - .../stacks/launch/NewApp.tsx | 67 -- .../stacks/launch/NewEnvGroup.tsx | 30 - .../stacks/launch/Overview.tsx | 315 ------- .../stacks/launch/SelectSource.tsx | 106 --- .../cluster-dashboard/stacks/launch/Store.tsx | 242 ------ .../launch/components/AddResourceButton.tsx | 108 --- .../launch/components/TemplateSelector.tsx | 71 -- .../launch/components/VersionSelector.tsx | 61 -- .../stacks/launch/components/styles.tsx | 233 ----- .../cluster-dashboard/stacks/launch/index.tsx | 54 -- .../home/cluster-dashboard/stacks/routes.tsx | 34 - .../home/cluster-dashboard/stacks/shared.ts | 47 - 109 files changed, 26236 deletions(-) delete mode 100644 dashboard/src/legacy/main/home/app-dashboard/apps/SelectableAppList.tsx delete mode 100644 dashboard/src/legacy/main/home/app-dashboard/types/buildpack.ts delete mode 100644 dashboard/src/legacy/main/home/cluster-dashboard/preview-environments/v2/DeleteEnvModal.tsx delete mode 100644 dashboard/src/legacy/main/home/managed-addons/tabs/PostgresTabs.tsx delete mode 100644 dashboard/src/legacy/main/home/managed-addons/tabs/RedisTabs.tsx delete mode 100644 dashboard/src/legacy/main/home/managed-addons/tabs/shared.tsx delete mode 100644 dashboard/src/legacy/main/home/onboarding/components/RegistryImageList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ConditionsTable.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ExpandedNodeView.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/dashboard/node-view/NodeUsage.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/NotificationSettingsSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/TagSelector.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/_BuildpackConfigSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/types.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/events/styles.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Edge.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/SelectRegion.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/ZoomPanel.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/DisabledNamespaces.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ConnectToJobInstructionsModal.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/useJobs.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/JobMetricsSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/status/PodRow.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/status/types.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/status/useLogs.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/expanded-chart/useStackEnvGroups.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/jobs/JobDashboard.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/components/ActionButton.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/components/NamespaceLabels.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/components/PorterYAMLErrorsModal.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/components/styled.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PreviewEnvDeleted.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/errors.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/mocks.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/routes.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/preview-environments/utils.ts delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_Settings.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/index.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewEnvGroup.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/Store.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/EnvGroups.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/SourceEditorDocker.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/routes.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/_StackList.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/components/NewAppResourceForm.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/components/NewEnvGroupForm.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/components/Status.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/NewApp.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/NewEnvGroup.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/Overview.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/SelectSource.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/Store.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/components/AddResourceButton.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/components/TemplateSelector.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/components/VersionSelector.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/components/styles.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/launch/index.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/routes.tsx delete mode 100644 dashboard/src/main/home/cluster-dashboard/stacks/shared.ts diff --git a/dashboard/src/legacy/main/home/app-dashboard/apps/SelectableAppList.tsx b/dashboard/src/legacy/main/home/app-dashboard/apps/SelectableAppList.tsx deleted file mode 100644 index 3d1e12231a..0000000000 --- a/dashboard/src/legacy/main/home/app-dashboard/apps/SelectableAppList.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useMemo } from "react"; -import { PorterApp } from "@porter-dev/api-contracts"; -import healthy from "legacy/assets/status-healthy.png"; -import Container from "legacy/components/porter/Container"; -import SelectableList from "legacy/components/porter/SelectableList"; -import Spacer from "legacy/components/porter/Spacer"; -import Text from "legacy/components/porter/Text"; - -import { AppIcon, AppSource } from "main/home/app-dashboard/apps/AppMeta"; - -import { type AppRevisionWithSource } from "./types"; - -type AppListProps = { - appListItems: Array<{ - app: AppRevisionWithSource; - key: string; - onSelect?: () => void; - onDeselect?: () => void; - isSelected?: boolean; - }>; -}; - -const SelectableAppList: React.FC = ({ appListItems }) => { - return ( - { - const proto = useMemo(() => { - return PorterApp.fromJsonString( - atob(ali.app.app_revision.b64_app_proto), - { - ignoreUnknownFields: true, - } - ); - }, [ali.app.app_revision.b64_app_proto]); - return { - selectable: ( - <> - - - - - {proto.name} - - - - - - - - - ), - key: ali.key, - onSelect: ali.onSelect, - onDeselect: ali.onDeselect, - isSelected: ali.isSelected, - }; - })} - /> - ); -}; - -export default SelectableAppList; diff --git a/dashboard/src/legacy/main/home/app-dashboard/types/buildpack.ts b/dashboard/src/legacy/main/home/app-dashboard/types/buildpack.ts deleted file mode 100644 index 198c436a7b..0000000000 --- a/dashboard/src/legacy/main/home/app-dashboard/types/buildpack.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { z } from "zod"; - -export const buildConfigSchema = z.object({ - builder: z.string(), - buildpacks: z.array(z.string()), - config: z.record(z.any()).optional(), -}); -export type BuildConfig = z.infer; - -export const buildpackSchema = z.object({ - name: z.string(), - buildpack: z.string(), - config: z.record(z.any()).nullish(), -}); -export type Buildpack = z.infer; - -export const detectedBuildpackSchema = z.object({ - name: z.string(), - builders: z.array(z.string()), - detected: z.array(buildpackSchema), - others: z.array(buildpackSchema), - buildConfig: buildConfigSchema.optional(), -}); -export type DetectedBuildpack = z.infer; - -export const DEFAULT_BUILDER_NAME = "heroku"; -export const DEFAULT_PAKETO_STACK = "paketobuildpacks/builder-jammy-full:latest"; -export const DEFAULT_HEROKU_STACK = "heroku/buildpacks:20"; - -export const BUILDPACK_TO_NAME: { [key: string]: string } = { - "heroku/nodejs": "NodeJS", - "heroku/python": "Python", - "heroku/java": "Java", - "heroku/ruby": "Ruby", - "heroku/go": "Go", -}; diff --git a/dashboard/src/legacy/main/home/cluster-dashboard/preview-environments/v2/DeleteEnvModal.tsx b/dashboard/src/legacy/main/home/cluster-dashboard/preview-environments/v2/DeleteEnvModal.tsx deleted file mode 100644 index 816b535254..0000000000 --- a/dashboard/src/legacy/main/home/cluster-dashboard/preview-environments/v2/DeleteEnvModal.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import Button from "legacy/components/porter/Button"; -import Modal from "legacy/components/porter/Modal"; -import Spacer from "legacy/components/porter/Spacer"; -import Text from "legacy/components/porter/Text"; - -type Props = { - closeModal: () => void; - deleteEnv: () => Promise; - loading?: boolean; -}; - -const DeleteEnvModal: React.FC = ({ - closeModal, - deleteEnv, - loading = false, -}) => { - return ( - - Confirm deletion - - - Click the button below to confirm environment deletion. This action is - irreversible. - - - - Deleting this environment will tear down all apps and any associated - resources. - - - - - ); -}; - -export default DeleteEnvModal; diff --git a/dashboard/src/legacy/main/home/managed-addons/tabs/PostgresTabs.tsx b/dashboard/src/legacy/main/home/managed-addons/tabs/PostgresTabs.tsx deleted file mode 100644 index 9ffa14277b..0000000000 --- a/dashboard/src/legacy/main/home/managed-addons/tabs/PostgresTabs.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import React, { useMemo, useState } from "react"; -import copy from "legacy/assets/copy-left.svg"; -import CopyToClipboard from "legacy/components/CopyToClipboard"; -import { ControlledInput } from "legacy/components/porter/ControlledInput"; -import Spacer from "legacy/components/porter/Spacer"; -import Text from "legacy/components/porter/Text"; -import TabSelector from "legacy/components/TabSelector"; -import { type ClientAddon } from "legacy/lib/addons"; -import { getServiceResourceAllowances } from "legacy/lib/porter-apps/services"; -import { Controller, useFormContext } from "react-hook-form"; -import { match, P } from "ts-pattern"; - -import IntelligentSlider from "main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider"; -import { type AppTemplateFormData } from "main/home/cluster-dashboard/preview-environments/v2/EnvTemplateContextProvider"; -import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider"; - -import { Code, CopyContainer, CopyIcon, IdContainer } from "./shared"; - -type Props = { - index: number; - addon: Omit & { - config: { - type: "postgres"; - }; - }; -}; - -export const PostgresTabs: React.FC = ({ index }) => { - const { register, control, watch } = useFormContext(); - const { nodes } = useClusterContext(); - const { maxRamMegabytes, maxCpuCores } = useMemo(() => { - return getServiceResourceAllowances(nodes); - }, [nodes]); - - const [currentTab, setCurrentTab] = useState<"credentials" | "resources">( - "credentials" - ); - - const name = watch(`addons.${index}.name`); - const username = watch(`addons.${index}.config.username`); - const password = watch(`addons.${index}.config.password`); - - const databaseURL = useMemo(() => { - if (!username || !password || !name.value) { - return ""; - } - - return `postgresql://${username}:${password}@${name.value}-postgres-hl:5432/postgres`; - }, [username, password, name.value]); - - return ( - <> - - - {match(currentTab) - .with("credentials", () => ( - <> - Postgres Username - - - - Postgres Password - - - - {databaseURL && ( - <> - Internal Database URL: - - - {databaseURL} - - - - - - - - - )} - - )) - .with("resources", () => ( - <> - - match(value) - .with(P.number, (v) => ( - { - onChange(e); - }} - step={0.1} - disabled={false} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - isSmartOptimizationOn={false} - decimalsToRoundTo={2} - /> - )) - .otherwise((v) => ( - { - onChange({ - ...v, - value: e, - }); - }} - step={0.1} - disabled={v.readOnly} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - isSmartOptimizationOn={false} - decimalsToRoundTo={2} - /> - )) - } - /> - - - match(value) - .with(P.number, (v) => ( - { - onChange(e); - }} - step={10} - disabled={false} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - isSmartOptimizationOn={false} - /> - )) - .otherwise((v) => ( - { - onChange({ - ...v, - value: e, - }); - }} - step={10} - disabled={v.readOnly} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - isSmartOptimizationOn={false} - /> - )) - } - /> - - )) - .exhaustive()} - - ); -}; diff --git a/dashboard/src/legacy/main/home/managed-addons/tabs/RedisTabs.tsx b/dashboard/src/legacy/main/home/managed-addons/tabs/RedisTabs.tsx deleted file mode 100644 index 8767ac2ae8..0000000000 --- a/dashboard/src/legacy/main/home/managed-addons/tabs/RedisTabs.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import React, { useMemo, useState } from "react"; -import copy from "legacy/assets/copy-left.svg"; -import CopyToClipboard from "legacy/components/CopyToClipboard"; -import { ControlledInput } from "legacy/components/porter/ControlledInput"; -import Spacer from "legacy/components/porter/Spacer"; -import Text from "legacy/components/porter/Text"; -import TabSelector from "legacy/components/TabSelector"; -import { type ClientAddon } from "legacy/lib/addons"; -import { getServiceResourceAllowances } from "legacy/lib/porter-apps/services"; -import { Controller, useFormContext } from "react-hook-form"; -import { match, P } from "ts-pattern"; - -import IntelligentSlider from "main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider"; -import { type AppTemplateFormData } from "main/home/cluster-dashboard/preview-environments/v2/EnvTemplateContextProvider"; -import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider"; - -import { Code, CopyContainer, CopyIcon, IdContainer } from "./shared"; - -type Props = { - index: number; - addon: Omit & { - config: { - type: "redis"; - }; - }; -}; - -export const RedisTabs: React.FC = ({ index }) => { - const { register, control, watch } = useFormContext(); - const { nodes } = useClusterContext(); - const { maxRamMegabytes, maxCpuCores } = useMemo(() => { - return getServiceResourceAllowances(nodes); - }, [nodes]); - - const [currentTab, setCurrentTab] = useState<"credentials" | "resources">( - "credentials" - ); - - const name = watch(`addons.${index}.name`); - const password = watch(`addons.${index}.config.password`); - - const redisURL = useMemo(() => { - if (!password || !name.value) { - return ""; - } - - return `redis://:${password}@${name.value}-redis:6379`; - }, [password, name.value]); - - return ( - <> - - - {match(currentTab) - .with("credentials", () => ( - <> - Redis Password - - - - {redisURL && ( - <> - Internal Redis URL: - - - {redisURL} - - - - - - - - - )} - - )) - .with("resources", () => ( - <> - - match(value) - .with(P.number, (v) => ( - { - onChange(e); - }} - step={0.1} - disabled={false} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - isSmartOptimizationOn={false} - decimalsToRoundTo={2} - /> - )) - .otherwise((v) => ( - { - onChange({ - ...v, - value: e, - }); - }} - step={0.1} - disabled={v.readOnly} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - isSmartOptimizationOn={false} - decimalsToRoundTo={2} - /> - )) - } - /> - - - match(value) - .with(P.number, (v) => ( - { - onChange(e); - }} - step={10} - disabled={false} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - isSmartOptimizationOn={false} - /> - )) - .otherwise((v) => ( - { - onChange({ - ...v, - value: e, - }); - }} - step={10} - disabled={v.readOnly} - disabledTooltip={ - "You may only edit this field in your porter.yaml." - } - isSmartOptimizationOn={false} - /> - )) - } - /> - - )) - .exhaustive()} - - ); -}; diff --git a/dashboard/src/legacy/main/home/managed-addons/tabs/shared.tsx b/dashboard/src/legacy/main/home/managed-addons/tabs/shared.tsx deleted file mode 100644 index 5b7a20a15e..0000000000 --- a/dashboard/src/legacy/main/home/managed-addons/tabs/shared.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import styled from "styled-components"; - -export const CopyIcon = styled.img` - cursor: pointer; - margin-left: 5px; - margin-right: 5px; - width: 15px; - height: 15px; - :hover { - opacity: 0.8; - } -`; - -export const Code = styled.span` - font-family: monospace; -`; - -export const IdContainer = styled.div` - background: #26292e; - border-radius: 5px; - padding: 10px; - display: flex; - width: 550px; - border-radius: 5px; - border: 1px solid ${({ theme }) => theme.border}; - align-items: center; - user-select: text; -`; - -export const CopyContainer = styled.div` - display: flex; - align-items: center; - margin-left: auto; -`; diff --git a/dashboard/src/legacy/main/home/onboarding/components/RegistryImageList.tsx b/dashboard/src/legacy/main/home/onboarding/components/RegistryImageList.tsx deleted file mode 100644 index 0ea092b045..0000000000 --- a/dashboard/src/legacy/main/home/onboarding/components/RegistryImageList.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useEffect, useState } from "react"; -import Helper from "legacy/components/form-components/Helper"; -import api from "legacy/shared/api"; -import { integrationList } from "legacy/shared/common"; -import styled from "styled-components"; - -const RegistryImageList: React.FC<{ - project: { - id: number; - name: string; - }; - registryType?: string; - registry_id: number; -}> = ({ project, registry_id, registryType }) => { - const [imageList, setImageList] = useState([]); - - useEffect(() => { - api - .getImageRepos( - "", - {}, - { - project_id: project.id, - registry_id, - } - ) - .then((res) => { - if (!res?.data) { - throw new Error("No data found"); - } - // console.log(res.data); - setImageList(res.data); - }) - .catch(console.error); - return () => {}; - }, []); - - const getIcon = () => { - if (registryType) { - return ( - integrationList[registryType] && integrationList[registryType].icon - ); - } else { - return integrationList["dockerhub"].icon; - } - }; - - return ( - <> - Porter was able to successfully connect to your registry: - - {imageList.length > 0 ? ( - imageList.map((data, i) => ( - - - {data.uri} - - )) - ) : ( - No container images found. - )} - -
- - ); -}; - -export default RegistryImageList; - -const Placeholder = styled.div` - width: 100%; - height: 80px; - color: #aaaabb; - display: flex; - align-items: center; - justify-content: center; - font-size: 13px; -`; - -const Br = styled.div` - width: 100%; - height: 15px; -`; - -const ImageRow = styled.div<{ isLast?: boolean }>` - width: 100%; - height: 40px; - border-bottom: ${(props) => (props.isLast ? "" : "1px solid #aaaabb")}; - display: flex; - align-items: center; - font-size: 13px; - padding: 12px; - user-select: text; - - > img { - width: 20px; - filter: grayscale(100%); - margin-right: 9px; - } -`; - -const ImageList = styled.div` - border-radius: 5px; - border: 1px solid #aaaabb; - max-height: 300px; - overflow-y: auto; - background: #ffffff11; - margin: 20px 0 20px; - user-select: text; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ConditionsTable.tsx b/dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ConditionsTable.tsx deleted file mode 100644 index 0bac08020c..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ConditionsTable.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useMemo } from "react"; -import Table from "components/OldTable"; -import { Column } from "react-table"; -import styled from "styled-components"; - -type NodeStatusModalProps = { - node: any; -}; - -export const ConditionsTable: React.FunctionComponent = ({ - node, -}) => { - const columns = useMemo[]>( - () => [ - { - Header: "Type", - accessor: "type", - }, - { - Header: "Status", - accessor: "status", - }, - { - Header: "Reason", - accessor: "reason", - }, - { - Header: "Message", - accessor: "message", - }, - { - Header: "Last Transition", - accessor: "lastTransitionTime", - Cell: ({ row }) => { - const date = new Date(row.values.lastTransitionTime); - return <>{date.toLocaleString()}; - }, - }, - ], - [] - ); - - const data = useMemo>(() => { - return node?.node_conditions || []; - }, [node]); - - return ( -
- - - - - ); -}; - -const TableWrapper = styled.div` - margin-top: 36px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ExpandedNodeView.tsx b/dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ExpandedNodeView.tsx deleted file mode 100644 index 36c57e6dbd..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ExpandedNodeView.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import { useHistory, useLocation, useParams } from "react-router"; -import styled from "styled-components"; -import leftArrow from "assets/left-arrow.svg"; -import api from "shared/api"; -import { Context } from "shared/Context"; - -import nodePng from "assets/node.png"; -import TabSelector from "components/TabSelector"; -import { pushFiltered } from "shared/routing"; -import NodeUsage from "./NodeUsage"; -import { ConditionsTable } from "./ConditionsTable"; -import StatusSection from "components/StatusSection"; -import TitleSection from "components/TitleSection"; - -type ExpandedNodeViewParams = { - nodeId: string; -}; - -type TabEnum = "conditions"; - -const tabOptions: { - label: string; - value: TabEnum; -}[] = [{ label: "Conditions", value: "conditions" }]; - -export const ExpandedNodeView = () => { - const { nodeId } = useParams(); - const history = useHistory(); - const location = useLocation(); - const { currentCluster, currentProject } = useContext(Context); - const [node, setNode] = useState(undefined); - const [currentTab, setCurrentTab] = useState("conditions"); - - useEffect(() => { - let isSubscribed = true; - api - .getClusterNode( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - nodeName: nodeId, - } - ) - .then((res) => { - if (isSubscribed) { - setNode(res.data); - } - }); - }, [nodeId, currentCluster.id, currentProject.id]); - - const closeNodeView = () => { - pushFiltered({ history, location }, "/cluster-dashboard", []); - }; - - const instanceType = useMemo(() => { - const instanceType = - node?.labels && node?.labels["node.kubernetes.io/instance-type"]; - if (instanceType) { - return ` (${instanceType})`; - } - return ""; - }, [node?.labels]); - - const currentTabPage = useMemo(() => { - switch (currentTab) { - case "conditions": - default: - return ; - } - }, [currentTab, node]); - - const nodeStatus = useMemo(() => { - if (!node || !node.node_conditions) { - return "loading"; - } - - return node.node_conditions.reduce((prevValue: boolean, current: any) => { - if (current.type !== "Ready" && current.status !== "False") { - return "failed"; - } - if (current.type === "Ready" && current.status !== "True") { - return "failed"; - } - return prevValue; - }, "healthy"); - }, [node]); - - return ( - - - - - Back - - - - - {nodeId} - {instanceType} - - - - - - - - - - setCurrentTab(value)} - /> - {currentTabPage} - - - ); -}; - -export default ExpandedNodeView; - -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 StatusWrapper = styled.div` - margin-left: 3px; - margin-bottom: 20px; -`; - -const InstanceType = styled.div` - font-weight: 400; - color: #ffffff44; - margin-left: 12px; - font-size: 16px; -`; - -const BodyWrapper = styled.div` - width: 100%; - height: 100%; - overflow: hidden; -`; - -const HeaderWrapper = styled.div` - position: relative; -`; - -const StyledExpandedNodeView = 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; - } - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/dashboard/node-view/NodeUsage.tsx b/dashboard/src/main/home/cluster-dashboard/dashboard/node-view/NodeUsage.tsx deleted file mode 100644 index e2e807e7ea..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/dashboard/node-view/NodeUsage.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React from "react"; -import styled from "styled-components"; - -type NodeUsageProps = { - node: any; -}; - -const NodeUsage: React.FunctionComponent = ({ node }) => { - const percentFormatter = (number: number) => `${Number(number).toFixed(2)}%`; - - const formatMemoryUnitToMi = (memory: string) => { - if (memory.includes("Mi")) { - return memory; - } - - if (memory.includes("Gi")) { - const [value] = memory.split("Gi"); - const numValue = Number(value); - const giToMiValue = numValue * 1024; - return `${giToMiValue.toFixed()}Mi`; - } - - if (memory.includes("Ki")) { - const [value] = memory.split("Ki"); - const numValue = Number(value); - const kiToMiValue = numValue / 1024; - return `${kiToMiValue.toFixed()}Mi`; - } - - const value = memory.replace(/[^0-9]/g, ""); - const numValue = Number(value); - const unknownToMiValue = numValue * 1024 * 1024; - return `${unknownToMiValue.toFixed()}Mi`; - }; - - return ( - - - - - CPU:{" "} - {!node?.cpu_reqs && !node?.allocatable_cpu - ? "Loading..." - : `${percentFormatter(node?.fraction_cpu_reqs)} (${ - node?.cpu_reqs - }/${node?.allocatable_cpu}m)`} - - - - RAM:{" "} - {!node?.memory_reqs && !node?.allocatable_memory - ? "Loading..." - : `${percentFormatter( - node?.fraction_memory_reqs - )} (${formatMemoryUnitToMi( - node?.memory_reqs - )}/${formatMemoryUnitToMi(node?.allocatable_memory)})`} - - - window.open( - "https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/#node-allocatable" - ) - } - className="material-icons" - > - help_outline - - - - - ); -}; - -const I = styled.i` - display: flex; - align-items: center; - cursor: pointer; - font-size: 17px; - margin-left: 12px; - color: #858faaaa; - :hover { - color: #aaaabb; - } -`; - -const Buffer = styled.div` - width: 17px; - height: 20px; -`; - -const Wrapper = styled.div` - display: flex; -`; - -const UsageWrapper = styled.div` - display: flex; - flex-direction: row; - font-size: 14px; - color: #aaaabb; - line-height: 24px; - user-select: text; - :not(last-child) { - margin-right: 20px; - } -`; - -const Bolded = styled.span` - font-weight: 500; - color: #ffffff44; - margin-right: 6px; -`; - -const Help = styled.a` - display: flex; - align-items: center; - font-size: 13px; - margin-bottom: 5px; - width: fit-content; - :hover { - color: #ffffff; - } - - > i { - margin-left: 5px; - font-size: 16px; - } -`; - -const NodeUsageWrapper = styled.div` - margin: 14px 0px 10px; -`; - -export default NodeUsage; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx deleted file mode 100644 index a71af14666..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx +++ /dev/null @@ -1,819 +0,0 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import styled from "styled-components"; -import yaml from "js-yaml"; -import { RouteComponentProps, withRouter } from "react-router"; - -import leftArrow from "assets/left-arrow.svg"; -import { cloneDeep, set } from "lodash"; -import loading from "assets/loading.gif"; - -import { ChartType, ClusterType } from "shared/types"; -import { Context } from "shared/Context"; - -import TitleSection from "components/TitleSection"; -import SettingsSection from "./SettingsSection"; -import PorterFormWrapper from "components/porter-form/PorterFormWrapper"; -import ValuesYaml from "./ValuesYaml"; -import DeploymentType from "./DeploymentType"; -import RevisionSection from "./RevisionSection"; -import Loading from "components/Loading"; -import JobList from "./jobs/JobList"; -import SaveButton from "components/SaveButton"; -import useAuth from "shared/auth/useAuth"; -import ExpandedJobRun from "./jobs/ExpandedJobRun"; -import { useJobs } from "./jobs/useJobs"; -import { useChart } from "shared/hooks/useChart"; -import ConnectToJobInstructionsModal from "./jobs/ConnectToJobInstructionsModal"; -import CommandLineIcon from "assets/command-line-icon"; -import CronParser from "cron-parser"; -import CronPrettifier from "cronstrue"; -import BuildSettingsTab from "./build-settings/BuildSettingsTab"; -import { useStackEnvGroups } from "./useStackEnvGroups"; -import api from "shared/api"; -import { getQueryParam, pushFiltered } from "shared/routing"; -import { useLocation } from "react-router"; - -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}`; -}; - -type PropsType = RouteComponentProps & { - namespace: string; - currentChart: ChartType; - currentCluster: ClusterType; - closeChart: () => void; - setSidebar: (x: boolean) => void; -} - -const ExpandedJobChart: React.FC = ({ currentChart: oldChart, closeChart, currentCluster, ...props }) => { - const { currentProject, setCurrentOverlay } = useContext(Context); - const [isAuthorized] = useAuth(); - const { - chart, - status, - saveStatus, - refreshChart, - deleteChart, - updateChart, - upgradeChart, - loadChartWithSpecificRevision, - } = useChart(oldChart, closeChart); - - const location = useLocation(); - - const { - jobs, - hasPorterImageTemplate, - status: jobsStatus, - triggerRunStatus, - runJob, - selectedJob, - setSelectedJob, - } = useJobs(chart); - - const { - isStack, - stackEnvGroups, - isLoadingStackEnvGroups, - } = useStackEnvGroups(chart); - - const [devOpsMode, setDevOpsMode] = useState( - () => localStorage.getItem("devOpsMode") === "true" - ); - const [showConnectionModal, setShowConnectionModal] = useState(false); - const [disableForm, setDisableForm] = useState(false); - - let rightTabOptions = [] as any[]; - - if (devOpsMode) { - rightTabOptions.push({ label: "Helm Values", value: "values" }); - } - - if (isAuthorized("job", "", ["get", "delete"])) { - rightTabOptions.push({ label: "Settings", value: "settings" }); - } - - if (chart?.git_action_config?.git_repo) { - rightTabOptions.push({ - label: "Build Settings", - value: "build-settings", - }); - } - - const leftTabOptions = [{ label: "Jobs", value: "jobs" }]; - - const processValuesToUpdateChart = (props?: { - values: any; - metadata: any; - }) => (currentChart: ChartType) => { - const newConfig = props.values; - let conf: string; - let values = currentChart.config; - - if (!newConfig) { - set(values, "paused", true); - - conf = yaml.dump(values, { forceQuotes: true }); - } else { - // Convert dotted keys to nested objects - for (let key in newConfig) { - set(values, key, newConfig[key]); - } - - set(values, "paused", true); - - // Weave in preexisting values and convert to yaml - conf = yaml.dump(values, { forceQuotes: true }); - } - - return { yaml: conf, metadata: props.metadata }; - }; - - const handleDeleteChart = async () => { - deleteChart(); - setCurrentOverlay(null); - }; - - const renderTabContents = (currentTab: string) => { - if (currentTab === "jobs" && hasPorterImageTemplate) { - return ( - - -
- This job is currently being deployed -
- Navigate to the - - Actions tab - {" "} - of your GitHub repo to view live build logs. -
-
- ); - } - - let interval = null; - if (chart?.config?.schedule.enabled) { - interval = CronParser.parseExpression(chart?.config?.schedule.value, { - utc: true, - }); - } - // @ts-ignore - const rtf = new Intl.DateTimeFormat("en", { - localeMatcher: "best fit", // other values: "lookup" - // @ts-ignore - dateStyle: "full", - timeStyle: "long", - }); - - let runDescription = ""; - - try { - runDescription = `Runs ${CronPrettifier.toString( - chart?.config?.schedule.value - ).toLowerCase()} UTC`; - } catch (error) { - runDescription = - "An unexpected error happened while trying to parse the cron expression."; - } - - if (currentTab === "jobs") { - return ( - - - { - runJob(); - }} - status={triggerRunStatus} - makeFlush={true} - clearPosition={true} - rounded={true} - statusPosition="right" - > - play_arrow Run Job - - { - e.preventDefault(); - setShowConnectionModal(true); - }} - > - - Shell Access - - - - {chart?.config?.schedule?.enabled ? ( - - access_time - {runDescription} - - • - {" "} - Next run on - {" " + rtf.format(interval.next().toDate())} - - ) : null} - - {jobsStatus === "loading" ? ( - - ) : ( - <> - { }} - expandJob={(job: any) => { - setSelectedJob(job); - }} - isDeployedFromGithub={!!chart?.git_action_config?.git_repo} - repositoryUrl={chart?.git_action_config?.git_repo} - currentChartVersion={Number(chart?.version)} - latestChartVersion={Number(chart?.latest_version)} - /> - - )} - - ); - } - - if (currentTab === "values") { - return ( - refreshChart()} - disabled={!isAuthorized("job", "", ["get", "update"])} - /> - ); - } - - if (currentTab === "build-settings") { - return ( - - ); - } - - if ( - currentTab === "settings" && - isAuthorized("job", "", ["get", "delete"]) - ) { - return ( - refreshChart()} - setShowDeleteOverlay={(showOverlay: boolean) => { - if (showOverlay) { - setCurrentOverlay({ - message: `Are you sure you want to delete ${chart?.name}?`, - onYes: handleDeleteChart, - onNo: () => setCurrentOverlay(null), - }); - } else { - setCurrentOverlay(null); - } - }} - saveButtonText="Save config" - /> - ); - } - - return null; - }; - - const formData = useMemo(() => cloneDeep(chart?.form || {}), [chart]); - - if (status === "loading" || isLoadingStackEnvGroups) { - return ; - } - - if (status === "deleting") { - return ( - - - - - -
- Deleting "{chart?.name}" -
- You will be automatically redirected after deletion is complete. -
-
-
- ); - } - - if (selectedJob !== null) { - return ( - { - const app = getQueryParam({ location }, "app"); - if (app) { - window.location.href = `/apps/${app}`; - } else { - setSelectedJob(null); - } - }} - /> - ); - } - - return ( - <> - setShowConnectionModal(false)} - chartName={chart?.name} - /> - - - - {(leftTabOptions?.length > 0 || - formData.tabs?.length > 0 || - rightTabOptions?.length > 0) && ( - - updateChart(processValuesToUpdateChart(formValues)) - } - includeMetadata - leftTabOptions={leftTabOptions} - rightTabOptions={rightTabOptions} - saveValuesStatus={saveStatus} - saveButtonText="Save config" - includeHiddenFields - addendum={ - - setDevOpsMode((prev) => { - localStorage.setItem("devOpsMode", prev.toString()); - return !prev; - }) - } - devOpsMode={devOpsMode} - > - offline_bolt DevOps Mode - - } - injectedProps={{ - "key-value-array": { - availableSyncEnvGroups: - isStack && !disableForm ? stackEnvGroups : undefined, - }, - "url-link": { - chart: chart, - }, - }} - /> - )} - - - - ); -}; - -export default withRouter(ExpandedJobChart); - -const ExpandedJobHeader: React.FC<{ - chart: ChartType; - jobs: any[]; - closeChart: () => void; - refreshChart: () => Promise; - upgradeChart: () => Promise; - loadChartWithSpecificRevision: (revision: number) => void; - setDisableForm: (disable: boolean) => void; - disableRevisions?: boolean; -}> = ({ - chart, - closeChart, - jobs, - refreshChart, - upgradeChart, - loadChartWithSpecificRevision, - setDisableForm, - disableRevisions, -}) => ( - <> - - - - Back - - - - - {chart?.name} - - - Namespace {chart.namespace.startsWith("porter-stack-") ? chart.namespace.replace("porter-stack-", "") : chart.namespace} - - - {chart?.config?.description ? ( - {chart?.config?.description} - ) : null} - - - {chart?.canonical_name !== "" ? ( - - Helm Release Name: - {chart?.name} - - ) : null} - - Run {jobs?.length} times Last template update at - {" " + readableDate(chart.info.last_deployed)} - - - {!disableRevisions ? ( - refreshChart()} - setRevision={(chart, isCurrent) => { - loadChartWithSpecificRevision(chart?.version); - setDisableForm(!isCurrent); - }} - forceRefreshRevisions={false} - refreshRevisionsOff={() => { }} - shouldUpdate={ - chart?.latest_version && - chart?.latest_version !== chart?.chart.metadata.version - } - latestVersion={chart?.latest_version} - upgradeVersion={(_version, cb) => { - upgradeChart().then(() => { - if (typeof cb === "function") { - cb(); - } - }); - }} - /> - ) : null} - - - ); - -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 RunsDescription = styled.div` - color: #ffffff; - font-size: 13px; - margin-top: 20px; - margin-bottom: 20px; - display: flex; - align-items: center; - padding: 14px 20px; - background: #2b2e36; - border: 1px solid #ffffff22; - color: #ffffffdd; - border-radius: 4px; - user-select: text; - - > i { - font-size: 16px; - color: #ffffffdd; - margin-right: 10px; - } -`; - -const Description = styled.div` - user-select: text; - font-size: 13px; - margin-left: 0; - display: flex; - align-items: center; - color: #ffffffdd; - line-height: 150%; -`; - -const CLIModalIconWrapper = styled.div` - height: 35px; - 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; - } -`; - -const LineBreak = styled.div` - width: calc(100% - 0px); - height: 1px; - background: #494b4f; - margin: 15px 0px 55px; -`; - -const ButtonWrapper = styled.div` - display: flex; - margin: 5px 0 0 0; - justify-content: space-between; -`; -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 TextWrap = styled.div``; - -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: 70px; - 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 BodyWrapper = styled.div` - position: relative; - overflow: hidden; -`; - -const TabWrapper = styled.div` - height: 100%; - width: 100%; - padding-bottom: 47px; - overflow: hidden; -`; - -const HeaderWrapper = styled.div` - position: relative; -`; - -const Dot = styled.div` - margin-right: 9px; - margin-left: 9px; -`; - -const InfoWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - margin: 24px 0px 17px 0px; -`; - -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: 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 TabButton = styled.div` - position: absolute; - right: 0px; - height: 30px; - background: linear-gradient(to right, #00000000, ${props => props.theme.bg} 20%); - padding-left: 30px; - display: flex; - align-items: center; - justify-content: center; - font-size: 13px; - color: ${(props: { devOpsMode: boolean }) => - props.devOpsMode ? "#aaaabb" : "#aaaabb55"}; - margin-left: 35px; - border-radius: 20px; - text-shadow: 0px 0px 8px - ${(props: { devOpsMode: boolean }) => - props.devOpsMode ? "#ffffff66" : "none"}; - cursor: pointer; - :hover { - color: ${(props: { devOpsMode: boolean }) => - props.devOpsMode ? "" : "#aaaabb99"}; - } - - > i { - font-size: 17px; - margin-right: 9px; - } -`; - -const A = styled.a` - color: #8590ff; - text-decoration: underline; - margin-left: 5px; - cursor: pointer; -`; - -const Bolded = styled.div` - font-weight: 500; - color: #ffffff44; - margin-right: 6px; -`; - -const Url = styled.div` - display: block; - font-size: 13px; - user-select: all; - user-select: text; - margin-top: -5px; - margin-bottom: 10px; - display: flex; - color: #949eff; - align-items: center; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx deleted file mode 100644 index ac3a97af88..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; - -import { Context } from "shared/Context"; -import { ChartType, ResourceType } from "shared/types"; - -import GraphDisplay from "./graph/GraphDisplay"; -import Loading from "components/Loading"; - -type PropsType = { - components: ResourceType[]; - currentChart: ChartType; - setSidebar: (x: boolean) => void; - showRevisions: boolean; -}; - -type StateType = { - isExpanded: boolean; -}; - -export default class GraphSection extends Component { - state = { - isExpanded: false, - }; - - renderContents = () => { - if (this.props.components && this.props.components.length > 0) { - return ( - - ); - } - - return ; - }; - - render() { - return {this.renderContents()}; - } -} - -GraphSection.contextType = Context; - -const StyledGraphSection = styled.div` - width: 100%; - min-height: 400px; - height: calc(100vh - 400px); - font-size: 13px; - overflow: hidden; - border-radius: 8px; - border: 1px solid #ffffff33; - 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/cluster-dashboard/expanded-chart/ListSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx deleted file mode 100644 index 2f4fe0ea70..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; -import yaml from "js-yaml"; - -import { Context } from "shared/Context"; -import { ChartType, ResourceType } from "shared/types"; - -import Loading from "components/Loading"; -import ResourceTab from "components/ResourceTab"; -import YamlEditor from "components/YamlEditor"; - -type PropsType = { - currentChart: ChartType; - components: ResourceType[]; - showRevisions: boolean; -}; - -type StateType = { - showKindLabels: boolean; - yaml: string | null; - wrapperHeight: number; - selectedResource: { kind: string; name: string } | null; -}; - -export default class ListSection extends Component { - state = { - showKindLabels: true, - yaml: "# Select a resource to view its manifest" as string | null, - wrapperHeight: 0, - selectedResource: null as { kind: string; name: string } | null, - }; - - wrapperRef: any = React.createRef(); - - componentDidMount() { - this.setState({ wrapperHeight: this.wrapperRef.offsetHeight }); - } - - componentDidUpdate(prevProps: PropsType) { - // Adjust yaml wrapper height on revision toggle - if ( - prevProps.showRevisions !== this.props.showRevisions && - this.wrapperRef - ) { - this.setState({ wrapperHeight: this.wrapperRef.offsetHeight }); - } - - if ( - prevProps.components !== this.props.components && - this.state.selectedResource - ) { - let matchingResourceFound = false; - this.props.components.forEach((resource: ResourceType) => { - if ( - resource.Kind === this.state.selectedResource.kind && - resource.Name === this.state.selectedResource.name - ) { - let rawYaml = yaml.dump(resource.RawYAML); - this.setState({ yaml: rawYaml }); - matchingResourceFound = true; - } - }); - if (!matchingResourceFound) { - this.setState({ yaml: "# Select a resource to view its manifest" }); - } - } - } - - renderResourceList = () => { - return this.props.components.map((resource: ResourceType, i: number) => { - let rawYaml = yaml.dump(resource.RawYAML); - return ( - - this.setState({ - yaml: rawYaml, - selectedResource: { kind: resource.Kind, name: resource.Name }, - }) - } - selected={this.state.yaml === rawYaml} - label={resource.Kind} - name={resource.Name} - isLast={i === this.props.components.length - 1} - /> - ); - }); - }; - - renderTabs = () => { - if (this.props.components && this.props.components.length > 0) { - return {this.renderResourceList()}; - } - - return ; - }; - - render() { - return ( - - {this.renderTabs()} - (this.wrapperRef = element)}> - - this.setState({ yaml: e })} - height={this.state.wrapperHeight - 2 + "px"} - border={true} - readOnly={true} - /> - - - - ); - } -} - -ListSection.contextType = Context; - -const YamlWrapper = styled.div` - width: 100%; - height: 100%; - overflow: visible; -`; - -const TabWrapper = styled.div` - min-width: 200px; - width: 35%; - margin-right: 10px; - overflow: hidden; - overflow-y: auto; -`; - -const FlexWrapper = styled.div` - display: flex; - flex: 1; - height: 100%; - overflow: visible; -`; - -const StyledListSection = styled.div` - display: flex; - font-size: 13px; - 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); - } - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/NotificationSettingsSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/NotificationSettingsSection.tsx deleted file mode 100644 index 47d476022a..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/NotificationSettingsSection.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import React, { useContext, useState, useEffect } from "react"; -import Heading from "components/form-components/Heading"; -import CheckboxRow from "components/form-components/CheckboxRow"; -import Helper from "components/form-components/Helper"; -import SaveButton from "components/SaveButton"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { ChartType } from "shared/types"; -import Loading from "components/Loading"; -import Banner from "components/porter/Banner"; -import styled from "styled-components"; - -const NOTIF_CATEGORIES = ["success", "fail"]; - -interface Props { - disabled?: boolean; - currentChart: ChartType; -} - -const NotificationSettingsSection: React.FC = (props) => { - const [notificationsOn, setNotificationsOn] = useState(true); - const [categories, setCategories] = useState( - NOTIF_CATEGORIES.reduce((p, c) => { - return { - ...p, - [c]: true, - }; - }, {}) - ); - const [initLoading, setInitLoading] = useState(true); - const [saveLoading, setSaveLoading] = useState(false); - const [numSaves, setNumSaves] = useState(0); - const [hasNotifications, setHasNotifications] = useState(null); - const [hasRelease, setHasRelease] = useState(true); - - const { currentProject, currentCluster } = useContext(Context); - - useEffect(() => { - api - .legacyGetNotificationConfig( - "", - {}, - { - project_id: currentProject.id, - namespace: props.currentChart.namespace, - cluster_id: currentCluster.id, - name: props.currentChart.name, - } - ) - .then(({ data }) => { - setNotificationsOn(data.enabled); - delete data.enabled; - setCategories({ - success: data.success, - failure: data.failure, - }); - setInitLoading(false); - }) - .catch(() => { - setHasRelease(false); - setInitLoading(false); - }); - api - .getSlackIntegrations( - "", - {}, - { - id: currentProject.id, - } - ) - .then(({ data }) => { - setHasNotifications(data.length > 0); - }); - }, []); - - const saveChanges = () => { - setSaveLoading(true); - let payload = { - enabled: notificationsOn, - ...categories, - }; - - api - .legacyUpdateNotificationConfig( - "", - { - payload, - }, - { - project_id: currentProject.id, - namespace: props.currentChart.namespace, - cluster_id: currentCluster.id, - name: props.currentChart.name, - } - ) - .then(() => { - setNumSaves(numSaves + 1); - setSaveLoading(false); - }) - .catch(() => { - setHasRelease(false); - setSaveLoading(false); - }); - }; - - return ( - <> - Notification Settings - Configure notification settings for this application. - {initLoading ? ( - - ) : !hasRelease ? ( - - Notifications unavailable. Porter could not find this application in - the database. - - ) : ( - <> - {hasNotifications != null && !hasNotifications ? ( - - No integration has been set up for notifications.{" "} - - Connect to Slack - - - ) : ( - <> - setNotificationsOn(!notificationsOn)} - disabled={props.disabled} - /> - {notificationsOn && ( - <> - Send notifications on: - {Object.entries(categories).map( - ([k, v]: [string, boolean]) => { - return ( - - - setCategories((prev) => { - return { - ...prev, - [k]: !v, - }; - }) - } - disabled={props.disabled} - /> - - ); - } - )} - - )} -
- saveChanges()} - text="Save Notification Settings" - clearPosition={true} - statusPosition={"right"} - disabled={props.disabled || initLoading || saveLoading} - status={ - saveLoading ? "loading" : numSaves > 0 ? "successful" : null - } - saveText={"Saving . . ."} - /> -
- - )} - - )} - - ); -}; - -export default NotificationSettingsSection; - -const A = styled.a` - text-decoration: underline; - cursor: pointer; - margin-left: 5px; -`; - -const Br = styled.div` - width: 100%; - height: 10px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx deleted file mode 100644 index cde9a23d8c..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx +++ /dev/null @@ -1,558 +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, 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"; - -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; -}; - -type StateType = { - revisions: ChartType[]; - rollbackRevision: number | null; - upgradeVersion: string; - loading: boolean; - maxVersion: number; - expandRevisions: boolean; -}; - -// TODO: handle refresh when new revision is generated from an old revision -class RevisionSection 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 = () => { - let { setCurrentError, currentCluster, currentProject } = this.context; - - let revisionNumber = this.state.rollbackRevision; - this.setState({ loading: true, rollbackRevision: null }); - - api - .rollbackChart( - "", - { - revision: revisionNumber, - }, - { - id: currentProject.id, - name: this.props.chart.name, - namespace: this.props.chart.namespace, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - this.setState({ loading: false }); - this.refreshHistory().then(() => { - this.props.setRevision(this.state.revisions[0], true); - }); - }) - .catch((err) => { - console.log(err); - setCurrentError(err.response.data); - 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.props.shouldUpdate && isCurrent && ( -
- { - e.stopPropagation(); - this.setState({ upgradeVersion: this.props.latestVersion }); - }} - > - notification_important - Template Update Available - -
- )} -
- {this.renderExpanded()} -
- ); - }; - - render() { - return ( - - {this.renderContents()} - {createPortal( - this.setState({ rollbackRevision: null })} - />, - document.body - )} - - ); - } -} - -RevisionSection.contextType = Context; - -export default withAuth(RevisionSection); - -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)"}; - } -`; - -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/cluster-dashboard/expanded-chart/SettingsSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx deleted file mode 100644 index 2c7a7693ab..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx +++ /dev/null @@ -1,491 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import styled from "styled-components"; -import api from "shared/api"; -import yaml from "js-yaml"; -import * as traverse from "traverse"; - -import { ActionConfigType, ChartType, StorageType } from "shared/types"; -import { Context } from "shared/Context"; - -import ImageSelector from "components/image-selector/ImageSelector"; -import SaveButton from "components/SaveButton"; -import Heading from "components/form-components/Heading"; -import Helper from "components/form-components/Helper"; -import _ from "lodash"; -import CopyToClipboard from "components/CopyToClipboard"; -import useAuth from "shared/auth/useAuth"; -import Loading from "components/Loading"; -import NotificationSettingsSection from "./NotificationSettingsSection"; -import { Link } from "react-router-dom"; -import { isDeployedFromGithub } from "shared/release/utils"; -import TagSelector from "./TagSelector"; -import { PORTER_IMAGE_TEMPLATES } from "shared/common"; -import DynamicLink from "components/DynamicLink"; -import CanonicalName from "./CanonicalName"; - -type PropsType = { - currentChart: ChartType; - refreshChart: () => Promise; - setShowDeleteOverlay: (x: boolean) => void; - saveButtonText?: string | null; -}; - -const SettingsSection: React.FC = ({ - currentChart, - refreshChart, - setShowDeleteOverlay, - saveButtonText, -}) => { - const [selectedImageUrl, setSelectedImageUrl] = useState(""); - const [selectedTag, setSelectedTag] = useState(""); - const [saveValuesStatus, setSaveValuesStatus] = useState(null); - const [highlightCopyButton, setHighlightCopyButton] = useState( - false - ); - const [webhookToken, setWebhookToken] = useState(""); - const [ - createWebhookButtonStatus, - setCreateWebhookButtonStatus, - ] = useState(""); - const [loadingWebhookToken, setLoadingWebhookToken] = useState(true); - - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - - const [isAuthorized] = useAuth(); - - useEffect(() => { - let isSubscribed = true; - setLoadingWebhookToken(true); - const image = currentChart?.config?.image; - setSelectedImageUrl(image?.repository); - setSelectedTag(image?.tag); - - api - .getReleaseToken( - "", - {}, - { - id: currentProject.id, - name: currentChart?.name, - namespace: currentChart?.namespace, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - if (!isSubscribed) { - return; - } - - setWebhookToken(res.data.webhook_token); - }) - .catch(console.log) - .finally(() => setLoadingWebhookToken(false)); - - return () => { - isSubscribed = false; - }; - }, [currentChart, currentCluster, currentProject]); - - const handleSubmit = async () => { - setSaveValuesStatus("loading"); - - // console.log(selectedImageUrl); - - let values = {}; - if (selectedTag) { - _.set(values, "image.repository", selectedImageUrl); - _.set(values, "image.tag", selectedTag); - } - - // if this is a job, set it to paused - if (currentChart?.chart?.metadata?.name == "job") { - _.set(values, "paused", true); - } - - // Weave in preexisting values and convert to yaml - let conf = yaml.dump( - { - ...(currentChart?.config as Object), - ...values, - }, - { forceQuotes: true } - ); - - try { - await api.upgradeChartValues( - "", - { - values: conf, - latest_revision: currentChart?.version, - }, - { - id: currentProject.id, - name: currentChart?.name, - namespace: currentChart?.namespace, - cluster_id: currentCluster.id, - } - ); - setSaveValuesStatus("successful"); - refreshChart(); - } catch (err) { - let parsedErr = err?.response?.data?.error; - - if (parsedErr) { - err = parsedErr; - } - - setSaveValuesStatus(parsedErr); - setCurrentError(parsedErr); - } - }; - - const handleCreateWebhookToken = async () => { - setCreateWebhookButtonStatus("loading"); - const { id: cluster_id } = currentCluster; - const { id: project_id } = currentProject; - const { name: chart_name, namespace } = currentChart; - try { - const res = await api.createWebhookToken( - "", - {}, - { - project_id, - chart_name, - namespace, - cluster_id, - } - ); - setCreateWebhookButtonStatus("successful"); - setTimeout(() => { - setWebhookToken(res.data.webhook_token); - }, 500); - } catch (err) { - let parsedErr = err?.response?.data?.error; - - if (parsedErr) { - err = parsedErr; - } - - setCreateWebhookButtonStatus(parsedErr); - setCurrentError(parsedErr); - } - }; - - const getCloneUrl = () => { - const params = new URLSearchParams(); - params.append("project_id", currentProject.id.toString()); - params.append("shouldClone", "true"); - params.append("release_namespace", currentChart.namespace); - params.append( - "release_template_version", - currentChart.chart.metadata.version - ); - params.append("release_type", currentChart.chart.metadata.name); - params.append("release_name", currentChart.name); - params.append("release_version", currentChart.version.toString()); - return `/launch?${params.toString()}`; - }; - - const renderWebhookSection = () => { - if (!currentChart?.form?.hasSource) { - return ; - } - - const protocol = window.location.protocol == "https:" ? "https" : "http"; - - const url = `${protocol}://${window.location.host}`; - - const curlWebhook = `curl -X POST '${url}/api/webhooks/deploy/${webhookToken}?commit=YOUR_COMMIT_HASH'`; - - const isAuthorizedToCreateWebhook = isAuthorized("application", "", [ - "get", - "create", - "update", - ]); - - let buttonStatus = createWebhookButtonStatus; - - if (!isAuthorizedToCreateWebhook) { - buttonStatus = "Unauthorized to create webhook token"; - } - - return ( - <> - {!currentChart.stack_id?.length && - !PORTER_IMAGE_TEMPLATES.includes(selectedImageUrl) ? ( - <> - Source settings - Specify an image tag to use. - setSelectedImageUrl(x)} - setSelectedTag={(x: string) => setSelectedTag(x)} - forceExpanded={true} - disableImageSelect={false} - /> - {!loadingWebhookToken && ( - <> -
-
-
- - - )} -
- - ) : null} - - <> - Canonical Name - - Set a canonical name for this application (lowercase letters, - numbers, and "-" only) - - refreshChart()} /> - - Redeploy Webhook - - Programmatically deploy by calling this secret webhook. - - - {!loadingWebhookToken && !webhookToken.length && ( - - )} - {webhookToken.length > 0 && ( - -
{curlWebhook}
- setHighlightCopyButton(true)} - wrapperProps={{ - className: "material-icons", - onMouseLeave: () => setHighlightCopyButton(false), - }} - > - content_copy - -
- )} - - Application Tags - Add tags for filtering applications. - refreshChart()} /> - - ); - }; - - const canBeCloned = () => { - if (isDeployedFromGithub(currentChart)) { - return false; - } - - // If its not web worker or job it means is an addon, and for now it's not supported - if ( - !["web", "worker", "job"].includes(currentChart?.chart?.metadata?.name) - ) { - return false; - } - - return true; - }; - - const canBeDeleted = () => { - const chart = currentChart; - - if (chart.config) { - const values = chart.config; - const t = traverse(values); - const paths = t.paths(); - - return !paths.some((path) => { - if ( - Array.isArray(path) && - path.at(-2) === "nodeSelector" && - path.at(-1) === "porter.run/system" - ) { - return t.get(path) === true; - } - - return false; - }); - } - - return true; - }; - - return ( - - {!loadingWebhookToken ? ( - - {renderWebhookSection()} - - {/* Prevent the clone button to be rendered in github deployed charts */} - {canBeCloned() && ( - <> - Clone deployment - - Click the button below to be redirected to the deploy form with - all the data prefilled - - - Clone - - - )} - - Additional Settings - - - ) : ( - - )} - - ); -}; - -export default SettingsSection; - -const DarkMatter = styled.div` - width: 100%; - height: 0; - margin-top: -40px; -`; - -const Br = styled.div` - width: 100%; - height: 10px; -`; - -const Button = styled.button` - height: 35px; - font-size: 13px; - margin-top: 20px; - 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 Webhook = styled.div` - width: 100%; - border: 1px solid #ffffff55; - background: #ffffff11; - border-radius: 3px; - display: flex; - align-items: center; - font-size: 13px; - padding-left: 10px; - color: #aaaabb; - height: 40px; - position: relative; - margin-bottom: 40px; - - > div { - user-select: all; - } - - > i { - padding: 5px; - background: ${(props: { copiedToClipboard: boolean }) => - props.copiedToClipboard ? "#616FEEcc" : "#ffffff22"}; - border-radius: 5px; - position: absolute; - right: 10px; - font-size: 14px; - cursor: pointer; - color: #ffffff; - - :hover { - background: ${(props: { copiedToClipboard: boolean }) => - props.copiedToClipboard ? "" : "#ffffff44"}; - } - } -`; - -const Highlight = styled.div` - color: #8590ff; - text-decoration: underline; - margin-left: 5px; - cursor: pointer; - padding-right: ${(props: { padRight?: boolean }) => - props.padRight ? "5px" : ""}; -`; - -const A = styled.a` - color: #8590ff; - text-decoration: underline; - margin-left: 5px; - cursor: pointer; - padding-right: ${(props: { padRight?: boolean }) => - props.padRight ? "5px" : ""}; -`; - -const Wrapper = styled.div` - width: 100%; - padding-bottom: 65px; - height: 100%; -`; - -const StyledSettingsSection = styled.div` - width: 100%; - padding: 30px; - padding-bottom: 15px; - position: relative; - border-radius: 8px; - overflow: auto; - height: calc(100% - 55px); - border-radius: 5px; - background: ${props => props.theme.fg}; - border: 1px solid #494b4f; -`; - -const Holder = styled.div` - padding: 0px 12px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/TagSelector.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/TagSelector.tsx deleted file mode 100644 index f023854089..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/TagSelector.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import styled from "styled-components"; -import { Tooltip } from "@material-ui/core"; -import Modal from "main/home/modals/Modal"; -import { TwitterPicker } from "react-color"; -import InputRow from "components/form-components/InputRow"; -import SaveButton from "components/SaveButton"; -import api from "shared/api"; -import Color from "color"; -import { Context } from "shared/Context"; -import { ChartType } from "shared/types"; -import Helper from "components/form-components/Helper"; -import { differenceBy } from "lodash"; -import SearchSelector from "components/SearchSelector"; - -type Props = { - onSave: ((values: any[]) => void) | ((values: any[]) => Promise); - release: ChartType; -}; - -const TagSelector = ({ onSave, release }: Props) => { - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - const [values, setValues] = useState([]); - const [availableTags, setAvailableTags] = useState([]); - const [openModal, setOpenModal] = useState(false); - const [buttonStatus, setButtonStatus] = useState(""); - - const onDelete = (index: number) => { - setValues((prev) => { - const newValues = [...prev]; - const removedTag = newValues.splice(index, 1); - setAvailableTags((prevAt) => [...prevAt, ...removedTag]); - return newValues; - }); - }; - - const handleSave = async () => { - setButtonStatus("loading"); - - try { - await api.updateReleaseTags( - "", - { tags: [...values.map((tag) => tag.name)] }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: release.namespace, - release_name: release.name, - } - ); - await onSave(values); - setButtonStatus("successful"); - } catch (error) { - console.log(error); - setCurrentError( - "We couldn't link the tag to the release, please try again." - ); - setButtonStatus("Couldn't link the tag to the release"); - return; - } finally { - setTimeout(() => { - setButtonStatus(""); - }, 800); - } - }; - - useEffect(() => { - api - .getTagsByProjectId( - "", - {}, - { project_id: currentProject.id } - ) - .then(({ data }) => { - const releaseTags = data.filter((tag) => - release.tags?.includes(tag.name) - ); - const tmpAvailableTags = differenceBy(data, releaseTags, "name"); - - setValues(releaseTags); - setAvailableTags(tmpAvailableTags); - }); - }, [currentProject]); - - const hasUnsavedChanges = useMemo(() => { - const hasAddedSomething = !!differenceBy( - values, - release.tags?.map((tagName: string) => ({ name: tagName })) || [], - "name" - ).length; - - const hasDeletedSomething = !!differenceBy( - release.tags?.map((tagName: string) => ({ name: tagName })) || [], - values, - "name" - ).length; - - return hasAddedSomething || hasDeletedSomething; - }, [values, release]); - - return ( - <> - {openModal ? ( - { - const newValues = [...values, newTag]; - await onSave(newValues); - setValues(newValues); - }} - onClose={() => setOpenModal(false)} - release={release} - /> - ) : null} - - {values.map((val, index) => { - return ( - - - {val.name} - - onDelete(index)}> - cancel - - - ); - })} - - ( - { - setOpenModal((prev) => !prev); - }} - > - + Create a new tag - - )} - filterBy="name" - onSelect={(value) => { - console.log(value); - setAvailableTags((prev) => - prev.filter((prevVal) => prevVal.name !== value.name) - ); - setValues((prev) => [...prev, value]); - }} - getOptionLabel={(option) => option.name} - renderOptionIcon={(option) => } - /> - - handleSave()} - status={buttonStatus} - disabled={!hasUnsavedChanges || buttonStatus === "loading"} - > - -
- - ); -}; - -const AddTagButton = styled.div` - color: #aaaabb; - font-size: 13px; - padding: 10px 0; - z-index: 999; - padding-left: 12px; - cursor: pointer; - :hover { - color: white; - } -`; - -const Br = styled.div` - width: 100%; - height: 10px; -`; - -const CreateTagModal = ({ - onSave, - onClose, - release, -}: { - onSave: ((tag: any) => void) | ((tag: any) => Promise); - onClose: () => void; - release: ChartType; -}) => { - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - - const [color, setColor] = useState("#ffffff"); - const [name, setName] = useState("some-random-tag"); - - const [buttonStatus, setButtonStatus] = useState(""); - - const createTag = async () => { - setButtonStatus("loading"); - try { - await api.createTag( - "", - { name, color }, - { - project_id: currentProject.id, - } - ); - } catch (error) { - setCurrentError(error); - setButtonStatus("Couldn't create the tag"); - return; - } - - try { - await api.updateReleaseTags( - "", - { tags: [...(release.tags || []), name] }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: release.namespace, - release_name: release.name, - } - ); - setButtonStatus("successful"); - await onSave({ name, color }); - setTimeout(() => { - onClose(); - }, 800); - } catch (error) { - console.log(error); - setCurrentError( - "We couldn't link the tag to the release, please link it manually from the settings tab." - ); - setButtonStatus("Couldn't link the tag to the release"); - return; - } - }; - - return ( - - - Create a new tag and link the release you're currently at to the brand - new tag. - - - setName(val as string)} - isRequired - width="300px" - > - - setColor(newColor.hex)} - > - - - - {name} - - - createTag()} - text={"Create Tag"} - disabled={!name.length || buttonStatus === "loading"} - > - - - ); -}; - -export default TagSelector; - -const Flex = styled.div` - display: flex; - position: relative; -`; - -const Tag = styled.div<{ color: string }>` - display: inline-flex; - color: ${(props) => Color(props.color).darken(0.4).string() || "inherit"}; - user-select: none; - border: 1px solid ${(props) => Color(props.color).darken(0.4).string()}; - border-radius: 5px; - padding: 4px 8px; - position: relative; - margin-bottom: 20px; - text-align: center; - align-items: center; - font-size: 13px; - background-color: ${(props) => props.color || "inherit"}; - - max-width: 150px; - min-width: 60px; - - :not(:last-child) { - margin-right: 10px; - } - - > .material-icons { - font-size: 16px; - :hover { - cursor: pointer; - } - } -`; - -const TagText = styled.span` - overflow-x: hidden; - white-space: nowrap; - text-overflow: ellipsis; -`; - -const Label = styled.div` - color: #ffffff; - margin-bottom: 10px; - display: flex; - align-items: center; - font-size: 13px; - font-family: "Work Sans", sans-serif; -`; - -const TagColorBox = styled.div` - width: 15px; - height: 15px; - margin-right: 10px; - border-radius: 0px; - background-color: ${(props: { color: string }) => props.color}; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx deleted file mode 100644 index 264521cb06..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; -import yaml from "js-yaml"; -import _ from "lodash"; - -import { ChartType, StorageType } from "shared/types"; -import api from "shared/api"; -import { Context } from "shared/Context"; - -import YamlEditor from "components/YamlEditor"; -import SaveButton from "components/SaveButton"; - -type PropsType = { - currentChart: ChartType; - refreshChart: () => void; - disabled?: boolean; -}; - -type StateType = { - values: string; - saveValuesStatus: string | null; -}; - -// TODO: handle zoom out -export default class ValuesYaml extends Component { - state = { - values: "", - saveValuesStatus: null as string | null, - }; - - updateValues() { - let values = "# Nothing here yet"; - if (this.props.currentChart.config) { - values = yaml.dump(this.props.currentChart.config); - } - this.setState({ values }); - } - - componentDidMount() { - this.updateValues(); - } - - componentDidUpdate(prevProps: PropsType) { - if (this.props.currentChart !== prevProps.currentChart) { - this.updateValues(); - } - } - - handleSaveValues = () => { - let { currentCluster, setCurrentError, currentProject } = this.context; - this.setState({ saveValuesStatus: "loading" }); - - let valuesString = this.state.values; - - // if this is a job, set it to paused - if (this.props.currentChart?.chart?.metadata?.name == "job") { - const valuesYAML = yaml.load(this.state.values); - _.set(valuesYAML, "paused", true); - valuesString = yaml.dump(valuesYAML); - } - - api - .upgradeChartValues( - "", - { - values: valuesString, - latest_revision: this.props.currentChart.version, - }, - { - id: currentProject.id, - name: this.props.currentChart.name, - cluster_id: currentCluster.id, - namespace: this.props.currentChart.namespace, - } - ) - .then((res) => { - this.setState({ saveValuesStatus: "successful" }); - this.props.refreshChart(); - }) - .catch((err) => { - let parsedErr = err?.response?.data?.error; - - if (parsedErr) { - err = parsedErr; - } - - this.setState({ - saveValuesStatus: parsedErr, - }); - - setCurrentError(parsedErr); - }); - }; - - render() { - return ( - - - this.setState({ values: e })} - readOnly={this.props.disabled} - height="calc(100vh - 412px)" - /> - - {!this.props.disabled && ( - - )} - - ); - } -} - -ValuesYaml.contextType = Context; - -const Wrapper = styled.div` - overflow: auto; - border-radius: 8px; - margin-bottom: 30px; - 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/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx deleted file mode 100644 index 2ef5dfaf41..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx +++ /dev/null @@ -1,598 +0,0 @@ -import Heading from "components/form-components/Heading"; -import Helper from "components/form-components/Helper"; -import KeyValueArray from "components/form-components/KeyValueArray"; -import MultiSaveButton from "components/MultiSaveButton"; -import _ from "lodash"; -import React, { useContext, useMemo, useRef, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { - BuildConfig, - ChartTypeWithExtendedConfig, - FullActionConfigType, -} from "shared/types"; -import styled from "styled-components"; -import yaml from "js-yaml"; -import { AxiosError } from "axios"; -import BranchList from "components/repo-selector/BranchList"; -import Banner from "components/porter/Banner"; -import { UpdateBuildconfigResponse } from "./types"; -import BuildpackConfigSection from "./_BuildpackConfigSection"; -import InputRow from "components/form-components/InputRow"; - -type Props = { - chart: ChartTypeWithExtendedConfig; - isPreviousVersion: boolean; - onSave: () => void; -}; - -const BuildSettingsTab: React.FC = ({ - chart, - isPreviousVersion, - onSave, -}) => { - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - - const [envVariables, setEnvVariables] = useState( - chart.config?.container?.env?.build || null - ); - const [runningWorkflowURL, setRunningWorkflowURL] = useState(""); - const [reRunError, setReRunError] = useState<{ - title: string; - description: string; - }>(null); - const [buttonStatus, setButtonStatus] = useState< - "loading" | "successful" | string - >(""); - - const [currentBranch, setCurrentBranch] = useState( - () => chart?.git_action_config?.git_branch - ); - const [gitHubSettingsExpanded, setGitHubSettingsExpanded] = useState(true); - const [envVariablesExpanded, setEnvVariablesExpanded] = useState(false); - const [branchSelectionExpanded, setBranchSelectionExpanded] = useState(false); - const [buildpackSettingsExpanded, setBuildpackSettingsExpanded] = useState( - false - ); - - const buildpackConfigRef = useRef<{ - isLoading: boolean; - getBuildConfig: () => BuildConfig; - }>(null); - - const saveNewBranch = async (newBranch: string) => { - if (!newBranch?.length) { - return; - } - - if (newBranch === chart?.git_action_config?.git_branch) { - return; - } - - const newGitActionConfig: FullActionConfigType = { - ...chart.git_action_config, - git_branch: newBranch, - }; - - try { - await api.updateGitActionConfig( - "", - { - git_action_config: newGitActionConfig, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - release_name: chart.name, - namespace: chart.namespace, - } - ); - } catch (error) { - throw error; - } - }; - - const saveBuildConfig = async (config: BuildConfig) => { - console.log({ config }); - if (config === null) { - return; - } - - try { - await api.updateBuildConfig( - "", - { ...config }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: chart.namespace, - release_name: chart.name, - } - ); - } catch (err) { - throw err; - } - }; - - const saveEnvVariables = async (envs: { [key: string]: string }) => { - let values = { ...chart.config }; - if (envs === null) { - return; - } - - values.container.env.build = { ...envs }; - const valuesYaml = yaml.dump({ ...values }); - try { - await api.upgradeChartValues( - "", - { - values: valuesYaml, - latest_revision: chart.version, - }, - { - id: currentProject.id, - namespace: chart.namespace, - name: chart.name, - cluster_id: currentCluster.id, - } - ); - } catch (error) { - throw error; - } - }; - - const triggerWorkflow = async () => { - try { - await api.reRunGHWorkflow( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - git_installation_id: chart.git_action_config?.git_repo_id, - owner: chart.git_action_config?.git_repo?.split("/")[0], - name: chart.git_action_config?.git_repo?.split("/")[1], - branch: chart.git_action_config?.git_branch, - release_name: chart.name, - } - ); - } catch (error) { - if (!error?.response) { - throw error; - } - - let tmpError: AxiosError = error; - - /** - * @smell - * Currently the expanded chart is clearing all the state when a chart update is triggered (saveEnvVariables). - * Temporary usage of setCurrentError until a context is applied to keep the state of the ReRunError during re renders. - */ - - if (tmpError.response.status === 400) { - // setReRunError({ - // title: "No previous run found", - // description: - // "There are no previous runs for this workflow, please trigger manually a run before changing the build settings.", - // }); - setCurrentError( - "There are no previous runs for this workflow. Please manually trigger a run before changing build settings." - ); - return; - } - - if (tmpError.response.status === 409) { - // setReRunError({ - // title: "The workflow is still running", - // description: - // 'If you want to make more changes, please choose the option "Save" until the workflow finishes.', - // }); - - if (typeof tmpError.response.data === "string") { - setRunningWorkflowURL(tmpError.response.data); - } - setCurrentError( - 'The workflow is still running. You can "Save" the current build settings for the next workflow run and view the current status of the workflow here: ' + - tmpError.response.data - ); - return; - } - - if (tmpError.response.status === 404) { - let description = "No action file matching this deployment was found."; - if (typeof tmpError.response.data === "string") { - const filename = tmpError.response.data; - description = description.concat( - `Please check that the file "${filename}" exists in your repository.` - ); - } - // setReRunError({ - // title: "The action doesn't seem to exist", - // description, - // }); - - setCurrentError(description); - return; - } - throw error; - } - }; - - const clearButtonStatus = (time: number = 800) => { - setTimeout(() => { - setButtonStatus(""); - }, time); - }; - - const getBuildConfig = () => { - if (buildpackConfigRef.current?.isLoading) { - return null; - } - return buildpackConfigRef.current?.getBuildConfig() || null; - }; - - const handleSave = async () => { - setButtonStatus("loading"); - - const buildConfig = getBuildConfig(); - - if (!buildConfig && !chart.git_action_config.dockerfile_path) { - setButtonStatus("Can't save until buildpack config is loaded."); - clearButtonStatus(1500); - return; - } - - try { - await saveBuildConfig(buildConfig); - await saveNewBranch(currentBranch); - await saveEnvVariables(envVariables); - setButtonStatus("successful"); - } catch (error) { - setButtonStatus("Something went wrong"); - setCurrentError(error); - } finally { - clearButtonStatus(); - onSave(); - } - }; - - const handleSaveAndReDeploy = async () => { - setButtonStatus("loading"); - - const buildConfig = getBuildConfig(); - - if (!buildConfig && !chart.git_action_config.dockerfile_path) { - setButtonStatus("Can't save until buildpack config is loaded."); - clearButtonStatus(); - return; - } - - try { - await saveBuildConfig(buildConfig); - await saveNewBranch(currentBranch); - await saveEnvVariables(envVariables); - await triggerWorkflow(); - setButtonStatus("successful"); - } catch (error) { - setButtonStatus("Something went wrong"); - setCurrentError(error); - } finally { - clearButtonStatus(); - onSave(); - } - }; - - const currentActionConfig = useMemo(() => { - const actionConf = chart.git_action_config; - if (actionConf && actionConf.gitlab_integration_id) { - return { - kind: "gitlab", - ...actionConf, - } as FullActionConfigType; - } - - return { - kind: "github", - ...actionConf, - } as FullActionConfigType; - }, [chart]); - - return ( - - {isPreviousVersion ? ( - - Build config is disabled when reviewing past versions. Please go to - the current revision to update your app build configuration. - - ) : null} - - {/* {reRunError !== null ? ( - - error - - - {reRunError.title} - - {reRunError.description} - {runningWorkflowURL.length ? ( - <> - {" "} - To go to the workflow{" "} - - click here - - - ) : null} - - { - setReRunError(null); - setRunningWorkflowURL(""); - }} - > - close - - - ) : null} */} - - setGitHubSettingsExpanded(!gitHubSettingsExpanded)} - isExpanded={!gitHubSettingsExpanded} - > - Github Settings - arrow_drop_down - - - {gitHubSettingsExpanded && ( -
- - - {chart.git_action_config.dockerfile_path && ( - - )} - {!chart.git_action_config.dockerfile_path && ( - - )} -
- )} - - setBranchSelectionExpanded(!branchSelectionExpanded)} - isExpanded={!branchSelectionExpanded} - > - Select default branch - arrow_drop_down - - - {branchSelectionExpanded && ( -
- {/* Select default branch content */} - - Change the default branch the deployments will be made from. - - - You must also update the deploy branch in your GitHub Action file. - - -
- )} - - - setEnvVariablesExpanded(!envVariablesExpanded)} - isExpanded={!envVariablesExpanded} - > - Build environment variables - arrow_drop_down - - - - {envVariablesExpanded && ( -
- { - setEnvVariables(values); - }} - > -
- )} - - {!chart.git_action_config.dockerfile_path ? ( - <> - - - setBuildpackSettingsExpanded(!buildpackSettingsExpanded) - } - isExpanded={!buildpackSettingsExpanded} - > - Buildpacks settings - arrow_drop_down - - - {buildpackSettingsExpanded && - !chart.git_action_config.dockerfile_path && ( -
- -
- )} - - ) : null} - - - -
-
- ); -}; - -export default BuildSettingsTab; - -const DisabledOverlay = styled.div` - position: absolute; - width: 100%; - height: inherit; - display: flex; - align-items: center; - justify-content: center; - background: #00000099; - z-index: 1000; - border-radius: 8px; - padding: 0 35px; - text-align: center; -`; - -const SaveButtonWrapper = styled.div` - width: 100%; - margin-top: 30px; - display: flex; - justify-content: flex-end; -`; - -const Wrapper = styled.div` - position: relative; - width: 100%; - margin-bottom: 65px; - height: 100%; -`; - -const StyledSettingsSection = styled.div<{ blurContent: boolean }>` - width: 100%; - background: #ffffff11; - padding: 0 35px; - padding-top: 35px; - padding-bottom: 15px; - position: relative; - border-radius: 8px; - height: calc(100% - 55px); - ${(props) => (props.blurContent ? "filter: blur(5px);" : "")} -`; - -const AlertCard = styled.div` - transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - border-radius: 4px; - box-shadow: none; - font-weight: 400; - font-size: 0.875rem; - line-height: 1.43; - letter-spacing: 0.01071em; - border: 1px solid rgb(229, 115, 115); - display: flex; - padding: 6px 16px; - color: rgb(244, 199, 199); - margin-top: 20px; - position: relative; -`; - -const AlertCardIcon = styled.span` - color: rgb(239, 83, 80); - margin-right: 12px; - padding: 7px 0px; - display: flex; - font-size: 22px; - opacity: 0.9; -`; - -const AlertCardTitle = styled.div` - margin: -2px 0px 0.35em; - font-size: 1rem; - line-height: 1.5; - letter-spacing: 0.00938em; - font-weight: 500; -`; - -const AlertCardContent = styled.div` - padding: 8px 0px; -`; - -const AlertCardAction = styled.button` - position: absolute; - right: 5px; - top: 5px; - border: none; - background-color: unset; - color: white; - :hover { - cursor: pointer; - } -`; - -const ExpandHeader = styled.div<{ isExpanded: boolean }>` - display: flex; - align-items: center; - cursor: pointer; - > i { - margin-left: 10px; - transform: ${(props) => (props.isExpanded ? "rotate(180deg)" : "")}; - } -`; -const DarkMatter = styled.div` - width: 100%; - margin-bottom: -28px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/_BuildpackConfigSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/_BuildpackConfigSection.tsx deleted file mode 100644 index c05f7df139..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/_BuildpackConfigSection.tsx +++ /dev/null @@ -1,554 +0,0 @@ -import { DeviconsNameList } from "assets/devicons-name-list"; -import Helper from "components/form-components/Helper"; -import SelectRow from "components/form-components/SelectRow"; -import Loading from "components/Loading"; -import Placeholder from "components/OldPlaceholder"; -import { AddCustomBuildpackForm } from "components/repo-selector/BuildpackSelection"; -import { differenceBy } from "lodash"; -import React, { - forwardRef, - useContext, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, -} from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { - BuildConfig, - ChartTypeWithExtendedConfig, - FullActionConfigType, -} from "shared/types"; -import styled, { keyframes } from "styled-components"; -import { Buildpack, DetectBuildpackResponse, DetectedBuildpack } from "./types"; - -const BuildpackConfigSection = forwardRef< - { - isLoading: boolean; - getBuildConfig: () => BuildConfig; - }, - { - actionConfig: FullActionConfigType; - currentChart: ChartTypeWithExtendedConfig; - } ->(({ actionConfig, currentChart }, ref) => { - const { currentProject } = useContext(Context); - - const [builders, setBuilders] = useState(null); - const [selectedBuilder, setSelectedBuilder] = useState(null); - - const [stacks, setStacks] = useState(null); - const [selectedStack, setSelectedStack] = useState(null); - - const [selectedBuildpacks, setSelectedBuildpacks] = useState([]); - const [availableBuildpacks, setAvailableBuildpacks] = useState( - [] - ); - - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(false); - - const state = useRef(null); - - const populateState = ( - builder: string, - stack: string, - availableBuildpacks: Buildpack[] = [], - selectedBuildpacks: Buildpack[] = [] - ) => { - state.current = { - ...state.current, - [builder]: { - stack: stack, - availableBuildpacks: availableBuildpacks, - selectedBuildpacks: selectedBuildpacks, - }, - }; - }; - - const populateBuildpacks = ( - userBuildpacks: string[], - detectedBuildpacks: Buildpack[] - ) => { - const customBuildpackFactory = (name: string): Buildpack => ({ - name: name, - buildpack: name, - config: null, - }); - - return userBuildpacks.map( - (ub) => - detectedBuildpacks.find((db) => db.buildpack === ub) || - customBuildpackFactory(ub) - ); - }; - - const detectBuildpack = () => { - if (actionConfig.kind === "gitlab") { - return api.detectGitlabBuildpack( - "", - { - repo_path: actionConfig.git_repo, - branch: actionConfig.git_branch, - dir: actionConfig.folder_path || ".", - }, - { - project_id: currentProject.id, - integration_id: actionConfig.gitlab_integration_id, - } - ); - } - - return api.detectBuildpack( - "", - { - dir: actionConfig.folder_path || ".", - }, - { - project_id: currentProject.id, - git_repo_id: actionConfig.git_repo_id, - kind: "github", - owner: actionConfig.git_repo.split("/")[0], - name: actionConfig.git_repo.split("/")[1], - branch: actionConfig.git_branch, - } - ); - }; - - useEffect(() => { - const currentBuildConfig = currentChart?.build_config; - - if (!currentBuildConfig) { - return; - } - - setIsLoading(true); - - detectBuildpack() - .then(({ data }) => { - { - console.log(data); - const builders = data; - - const defaultBuilder = builders.find((builder) => - builder.builders.find( - (stack) => stack === currentBuildConfig.builder - ) - ); - - const nonSelectedBuilder = builders.find( - (builder) => - !builder.builders.find( - (stack) => stack === currentBuildConfig.builder - ) - ); - - const fullDetectedBuildpacks = [ - ...(defaultBuilder.detected ?? []), - ...(defaultBuilder.others ?? []), - ]; - const userSelectedBuildpacks = populateBuildpacks( - currentBuildConfig.buildpacks, - fullDetectedBuildpacks - ).filter((b) => b.buildpack); - - const availableBuildpacks = differenceBy( - fullDetectedBuildpacks, - userSelectedBuildpacks, - "buildpack" - ); - - const defaultStack = defaultBuilder.builders.find((stack) => { - return stack === currentBuildConfig.builder; - }); - - populateState( - defaultBuilder.name.toLowerCase(), - defaultStack, - userSelectedBuildpacks, - availableBuildpacks - ); - - populateState( - nonSelectedBuilder.name.toLowerCase(), - nonSelectedBuilder.builders[0], - nonSelectedBuilder.others, - nonSelectedBuilder.detected - ); - - setBuilders(builders); - setSelectedBuilder(defaultBuilder.name.toLowerCase()); - - setStacks(defaultBuilder.builders); - setSelectedStack(defaultStack); - if (!Array.isArray(userSelectedBuildpacks)) { - setSelectedBuildpacks([]); - } else { - setSelectedBuildpacks(userSelectedBuildpacks); - } - if (!Array.isArray(availableBuildpacks)) { - setAvailableBuildpacks([]); - } else { - setAvailableBuildpacks(availableBuildpacks); - } - } - }) - .catch((err) => { - console.error(err); - setError(true); - }) - .finally(() => { - setIsLoading(false); - }); - }, [currentProject, actionConfig, currentChart]); - - useImperativeHandle( - ref, - () => { - return { - isLoading: isLoading, - getBuildConfig: () => { - const currentBuildConfig = currentChart?.build_config; - - if (error) { - if (typeof currentBuildConfig.config === "string") { - return { - ...currentBuildConfig, - config: JSON.parse(atob(currentBuildConfig.config)) as Record< - string, - unknown - >, - } as BuildConfig; - } else { - return currentBuildConfig; - } - } - - let buildConfig: BuildConfig = {} as BuildConfig; - - buildConfig.builder = selectedStack; - buildConfig.buildpacks = selectedBuildpacks?.map((buildpack) => { - return buildpack.buildpack; - }); - - return buildConfig; - }, - }; - }, - [selectedBuilder, selectedBuildpacks, selectedStack, isLoading, error] - ); - - useEffect(() => { - populateState( - selectedBuilder, - selectedStack, - availableBuildpacks, - selectedBuildpacks - ); - }, [selectedBuilder, selectedBuildpacks, selectedStack, availableBuildpacks]); - - const builderOptions = useMemo(() => { - if (!Array.isArray(builders)) { - return; - } - - return builders.map((builder) => ({ - label: builder.name, - value: builder.name.toLowerCase(), - })); - }, [builders]); - - const stackOptions = useMemo(() => { - if (!Array.isArray(stacks)) { - return; - } - - return stacks.map((stack) => ({ - label: stack, - value: stack.toLowerCase(), - })); - }, [stacks]); - - const handleAddCustomBuildpack = (buildpack: Buildpack) => { - setSelectedBuildpacks((selectedBuildpacks) => [ - ...selectedBuildpacks, - buildpack, - ]); - }; - - const handleSelectBuilder = (builderName: string) => { - const builder = builders.find( - (b) => b.name.toLowerCase() === builderName.toLowerCase() - ); - - setBuilders(builders); - setStacks(builder.builders); - - const currState = state.current; - if (currState[builderName]) { - const stateBuilder = currState[builderName]; - setSelectedBuilder(builderName); - setSelectedStack(stateBuilder.stack); - setAvailableBuildpacks(stateBuilder.availableBuildpacks); - setSelectedBuildpacks(stateBuilder.selectedBuildpacks); - return; - } - }; - - const renderBuildpacksList = ( - buildpacks: Buildpack[], - action: "remove" | "add" - ) => { - if (!buildpacks.length && action === "remove") { - return ( - Buildpacks will be automatically detected. - ); - } - - if (!buildpacks.length && action === "add") { - return ( - - No additional buildpacks are available. You can add a custom buildpack - below. - - ); - } - - return buildpacks?.map((buildpack, i) => { - const [languageName] = buildpack.name?.split("/").reverse(); - - const devicon = DeviconsNameList.find( - (devicon) => languageName.toLowerCase() === devicon.name - ); - - const icon = `devicon-${devicon?.name}-plain colored`; - - let disableIcon = false; - if (!devicon) { - disableIcon = true; - } - - return ( - - - - - {buildpack?.name} - - - - {action === "add" && ( - handleAddBuildpack(buildpack.buildpack)} - > - add - - )} - {action === "remove" && ( - handleRemoveBuildpack(buildpack.buildpack)} - > - delete - - )} - - - ); - }); - }; - - const handleRemoveBuildpack = (buildpackToRemove: string) => { - setSelectedBuildpacks((selBuildpacks) => { - const tmpSelectedBuildpacks = [...selBuildpacks]; - - const indexBuildpackToRemove = tmpSelectedBuildpacks.findIndex( - (buildpack) => buildpack.buildpack === buildpackToRemove - ); - const buildpack = tmpSelectedBuildpacks[indexBuildpackToRemove]; - - setAvailableBuildpacks((availableBuildpacks) => [ - ...availableBuildpacks, - buildpack, - ]); - - tmpSelectedBuildpacks.splice(indexBuildpackToRemove, 1); - - return [...tmpSelectedBuildpacks]; - }); - }; - - const handleAddBuildpack = (buildpackToAdd: string) => { - setAvailableBuildpacks((avBuildpacks) => { - const tmpAvailableBuildpacks = [...avBuildpacks]; - const indexBuildpackToAdd = tmpAvailableBuildpacks.findIndex( - (buildpack) => buildpack.buildpack === buildpackToAdd - ); - const buildpack = tmpAvailableBuildpacks[indexBuildpackToAdd]; - - setSelectedBuildpacks((selectedBuildpacks) => [ - ...selectedBuildpacks, - buildpack, - ]); - - tmpAvailableBuildpacks.splice(indexBuildpackToAdd, 1); - return [...tmpAvailableBuildpacks]; - }); - }; - - if (isLoading) { - return ( -
- -
- ); - } - - if (error) { - return ( -
- -
-

Couldn't retrieve buildpacks.

-

- Check if the branch exists and the Porter App has enough - permissions on the repository. -

-
-
-
- ); - } - - return ( - - <> - handleSelectBuilder(option)} - label="Select a builder" - /> - setSelectedStack(option)} - label="Select your stack" - /> - - The following buildpacks were automatically detected. You can also - manually add/remove buildpacks. - - <>{renderBuildpacksList(selectedBuildpacks, "remove")} - Available buildpacks: - <>{renderBuildpacksList(availableBuildpacks, "add")} - - You may also add buildpacks by directly providing their GitHub links - or links to ZIP files that contain the buildpack source code. - - - - - ); -}); - -BuildpackConfigSection.displayName = "BuildpackConfigSection"; - -export default BuildpackConfigSection; - -const fadeIn = keyframes` - from { - opacity: 0; - } - to { - opacity: 1; - } -`; - -const StyledCard = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - border: 1px solid #ffffff00; - background: #ffffff08; - margin-bottom: 5px; - border-radius: 8px; - padding: 14px; - overflow: hidden; - height: 60px; - font-size: 13px; - animation: ${fadeIn} 0.5s; -`; - -const BuildpackConfigurationContainer = styled.div` - animation: ${fadeIn} 0.75s; -`; - -const ContentContainer = styled.div` - display: flex; - height: 100%; - width: 100%; - align-items: center; -`; - -const Icon = styled.span<{ disableMarginRight: boolean }>` - font-size: 20px; - margin-left: 10px; - ${(props) => { - if (!props.disableMarginRight) { - return "margin-right: 20px"; - } - }} -`; - -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 DeleteButton = styled.button` - 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; - - :hover { - background: #ffffff11; - border: 1px solid #ffffff44; - } - - > span { - font-size: 20px; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/types.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/types.ts deleted file mode 100644 index 916fe13188..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -export type Buildpack = { - name: string; - buildpack: string; - config: { - [key: string]: string; - }; -}; - -export type DetectedBuildpack = { - name: string; - builders: string[]; - detected: Buildpack[]; - others: Buildpack[]; -}; - -export type DetectBuildpackResponse = DetectedBuildpack[]; - -export type UpdateBuildconfigResponse = { - CreatedAt: string; - DeletedAt: { Time: string; Valid: boolean }; - Time: string; - Valid: boolean; - ID: number; - UpdatedAt: string; - builder: string; - buildpacks: string; - config: string; - name: string; -}; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx deleted file mode 100644 index 20a8c42c58..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx +++ /dev/null @@ -1,391 +0,0 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import styled from "styled-components"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import ResourceTab from "./ResourceTab"; -import ConfirmOverlay from "components/ConfirmOverlay"; -import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets"; -import PodRow from "./PodRow"; -import { timeFormat } from "d3-time-format"; -import { getAvailability, getPodStatus } from "./util"; -import _ from "lodash"; - -type Props = { - controller: any; - selectedPod: any; - selectPod: (newPod: any, userSelected: boolean) => unknown; - selectors: any; - isLast?: boolean; - isFirst?: boolean; - setPodError: (x: string) => void; - onUpdate: (update: any) => void; -}; - -// Controller tab in log section that displays list of pods on click. -export type ControllerTabPodType = { - namespace: string; - name: string; - phase: string; - status: any; - replicaSetName: string; - restartCount: number | string; - podAge: string; - revisionNumber?: number; - containerStatus: any; -}; - -const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y"); - -const ControllerTabFC: React.FunctionComponent = ({ - controller, - selectPod, - isFirst, - isLast, - selectors, - setPodError, - selectedPod, - onUpdate, -}) => { - const [pods, setPods] = useState([]); - const [rawPodList, setRawPodList] = useState([]); - const [podPendingDelete, setPodPendingDelete] = useState(null); - const [available, setAvailable] = useState(null); - const [total, setTotal] = useState(null); - const [userSelectedPod, setUserSelectedPod] = useState(false); - - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - const { - newWebsocket, - openWebsocket, - closeAllWebsockets, - closeWebsocket, - } = useWebsockets(); - - const currentSelectors = useMemo(() => { - if (controller.kind.toLowerCase() == "job" && selectors) { - return [...selectors]; - } - let newSelectors = [] as string[]; - 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; - } - newSelectors.push(selector); - return [...newSelectors]; - }, [controller, selectors]); - - useEffect(() => { - updatePods(); - [controller?.kind, "pod"].forEach((kind) => { - setupWebsocket(kind, controller?.metadata?.uid); - }); - () => closeAllWebsockets(); - }, [currentSelectors, controller, currentCluster, currentProject]); - - const updatePods = async () => { - try { - const res = await api.getMatchingPods( - "", - { - namespace: controller?.metadata?.namespace, - selectors: currentSelectors, - }, - { - 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) - ); - - 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 && - pod?.metadata?.annotations["helm.sh/revision"]) || - "N/A", - }; - }); - - setPods(newPods); - setRawPodList(data); - // If the user didn't click a pod, select the first returned from list. - if (!userSelectedPod) { - let status = getPodStatus(newPods[0].status); - status === "failed" && - newPods[0].status?.message && - setPodError(newPods[0].status?.message); - handleSelectPod(newPods[0], data); - } - } catch (error) {} - }; - - /** - * handleSelectPod is a wrapper for the selectPod function received from parent. - * Internally we use the ControllerPodType but we want to pass to the parent the - * raw pod returned from the API. - * - * @param pod A ControllerPodType pod that will be used to search the raw pod to pass - * @param rawList A rawList of pods in case we don't want to use the state one. Useful to - * avoid problems with reactivity - */ - const handleSelectPod = ( - pod: ControllerTabPodType, - rawList?: any[], - userSelected?: boolean - ) => { - const rawPod = [...rawPodList, ...(rawList || [])].find( - (rawPod) => rawPod?.metadata?.name === pod?.name - ); - selectPod(rawPod, !!userSelected); - }; - - const currentSelectedPod = useMemo(() => { - const pod = selectedPod; - const replicaSetName = - Array.isArray(pod?.metadata?.ownerReferences) && - pod?.metadata?.ownerReferences[0]?.name; - return { - namespace: pod?.metadata?.namespace, - name: pod?.metadata?.name, - phase: pod?.status?.phase, - status: pod?.status, - replicaSetName, - } as ControllerTabPodType; - }, [selectedPod]); - - const currentControllerStatus = useMemo(() => { - let status = available == total ? "running" : "waiting"; - - controller?.status?.conditions?.forEach((condition: any) => { - if ( - condition.type == "Progressing" && - condition.status == "False" && - condition.reason == "ProgressDeadlineExceeded" - ) { - status = "failed"; - } - }); - - if (controller.kind.toLowerCase() === "job" && pods.length == 0) { - status = "completed"; - } - return status; - }, [controller, available, total, pods]); - - const handleDeletePod = (pod: any) => { - api - .deletePod( - "", - {}, - { - cluster_id: currentCluster.id, - name: pod?.name, - namespace: pod?.namespace, - id: currentProject.id, - } - ) - .then((res) => { - updatePods(); - setPodPendingDelete(null); - }) - .catch((err) => { - setCurrentError(JSON.stringify(err)); - setPodPendingDelete(null); - }); - }; - - const replicaSetArray = useMemo(() => { - 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.length === 1 ? [] : podsDividedByReplicaSet; - }, [pods]); - - const setupWebsocket = (kind: string, controllerUid: string) => { - let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status?`; - if (kind == "pod" && currentSelectors) { - apiEndpoint += `selectors=${currentSelectors[0]}`; - } - - const options: NewWebsocketOptions = {}; - options.onopen = () => { - console.log("connected to websocket"); - }; - - options.onmessage = (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 != "pod") { - let [available, total] = getAvailability(object.metadata.kind, object); - setAvailable(available); - setTotal(total); - return; - } - updatePods(); - }; - - options.onclose = () => { - console.log("closing websocket"); - }; - - options.onerror = (err: ErrorEvent) => { - console.log(err); - closeWebsocket(kind); - }; - - newWebsocket(kind, apiEndpoint, options); - openWebsocket(kind); - }; - - const mapPods = (podList: ControllerTabPodType[]) => { - return podList.map((pod, i, arr) => { - let status = getPodStatus(pod.status); - return ( - { - setPodError(""); - status === "failed" && - pod.status?.message && - setPodError(pod.status?.message); - handleSelectPod(pod, [], true); - setUserSelectedPod(true); - }} - onDeleteClick={(e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setPodPendingDelete(pod); - }} - /> - ); - }); - }; - - useEffect(() => { - onUpdate({ pods, available, total, replicaSetArray }); - }, [pods, replicaSetArray, available, total]); - - return ( - - {!!replicaSetArray.length && - replicaSetArray.map((subArray, index) => { - const firstItem = subArray[0]; - return ( -
- - - {firstItem?.revisionNumber && - firstItem?.revisionNumber.toString() != "N/A" && ( - Revision {firstItem.revisionNumber}: - )}{" "} - {firstItem.replicaSetName} - - - {mapPods(subArray)} -
- ); - })} - {!replicaSetArray.length && mapPods(pods)} - handleDeletePod(podPendingDelete)} - onNo={() => setPodPendingDelete(null)} - /> -
- ); -}; - -export default ControllerTabFC; - -const Bold = styled.span` - font-weight: 500; - display: inline; - color: #ffffff; -`; - -const ReplicaSetContainer = styled.div` - padding: 10px 5px; - display: flex; - overflow-wrap: anywhere; - justify-content: space-between; -`; - -const ReplicaSetName = styled.span` - padding-left: 10px; - overflow-wrap: anywhere; - max-width: calc(100% - 45px); - line-height: 1.5em; - color: #ffffff33; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx deleted file mode 100644 index 18bb96f0f5..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import React, { useState, useRef, useEffect } from "react"; -import PodDropdown from "./PodDropdown"; - -import styled from "styled-components"; -import { getPodStatus } from "./util"; -import { InitLogData } from "../logs-section/LogsSection"; - -type Props = { - chart?: any; - setLogData: (initLogData: InitLogData) => void; -}; - -type DeployStatus = "Deploying" | "Deployed" | "Failed"; - -const DeployStatusSection: React.FC = (props) => { - const [status, setStatus] = useState("Deployed"); - const [percentage, setPercentage] = useState("0%"); - const [isExpanded, setIsExpanded] = useState(false); - - const wrapperRef = useRef(null); - const parentRef = useRef(null); - - useEffect(() => { - document.addEventListener("mousedown", handleClickOutside.bind(this)); - return () => - document.removeEventListener("mousedown", handleClickOutside.bind(this)); - }, []); - - const handleClickOutside = (event: any) => { - if ( - wrapperRef && - wrapperRef.current && - !wrapperRef.current.contains(event.target) && - parentRef && - parentRef.current && - !parentRef.current.contains(event.target) - ) { - setIsExpanded(false); - } - }; - - const onUpdate = (props: any) => { - const { available, total, replicaSetArray } = props; - let pods = props.pods; - - if (total) { - const remaining = (total - available) / props.total; - setPercentage(Math.floor(remaining * 100) + "%"); - } - - if (replicaSetArray.length) { - pods = replicaSetArray[0]; - } - - const podStatuses = pods.map((pod: any) => getPodStatus(pod.status)); - - if ( - podStatuses.every((status: string) => - ["running", "Ready", "completed", "Completed"].includes(status) - ) - ) { - setStatus("Deployed"); - return; - } - - if ( - podStatuses.some((status: string) => - ["failed", "failedValidation"].includes(status) - ) - ) { - setStatus("Failed"); - return; - } - - setStatus("Deploying"); - }; - - return ( - <> - setIsExpanded(!isExpanded)} - ref={parentRef} - isExpanded={isExpanded} - > - {status === "Deploying" ? ( - <> - - {status} - - ) : ( - - - {status} - - )} - - - - { - console.log( - "SET LOG DATA", - pod?.metadata?.name, - pod?.metadata?.annotations?.["helm.sh/revision"] - ); - - props.setLogData({ - podName: pod?.metadata?.name, - revision: pod?.metadata?.annotations?.["helm.sh/revision"], - }); - }} - /> - - - - ); -}; - -export default DeployStatusSection; - -const StatusCircle = styled.div<{ percentage?: any }>` - width: 16px; - height: 16px; - border-radius: 50%; - margin-right: 10px; - background: conic-gradient( - from 0deg, - #ffffff33 ${(props) => props.percentage}, - #ffffffaa 0% ${(props) => props.percentage} - ); -`; - -const DropdownWrapper = styled.div<{ - dropdownAlignRight?: boolean; - expanded?: boolean; -}>` - display: ${(props) => (props.expanded ? "block" : "none")}; - position: absolute; - left: ${(props) => (props.dropdownAlignRight ? "" : "0")}; - right: ${(props) => (props.dropdownAlignRight ? "0" : "")}; - z-index: 1000; - top: calc(100% + 7px); - width: 35%; - min-width: 400px; - animation: floatIn 0.2s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - @keyframes floatIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0px); - } - } -`; - -const Dropdown = styled.div` - z-index: 999; - overflow-y: auto; - background: #2f3135; - padding: 0; - border-radius: 5px; - border: 1px solid #aaaabb33; -`; - -const StyledDeployStatusSection = styled.div<{ isExpanded?: boolean }>` - font-size: 13px; - height: 30px; - border-radius: 5px; - padding: 0 9px; - padding-left: 7px; - display: flex; - margin-left: -1px; - align-items: center; - ${(props) => - props.isExpanded && - ` - background: #26292e; - border: 1px solid #494b4f; - border: 1px solid #7a7b80; - margin-left: -2px; - margin-right: -1px; - `} - justify-content: center; - cursor: pointer; - :hover { - background: #26292e; - border: 1px solid #494b4f; - border: 1px solid #7a7b80; - margin-left: -2px; - margin-right: -1px; - } -`; - -const StatusWrapper = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; -`; - -const StatusColor = styled.div` - width: 8px; - min-width: 8px; - height: 8px; - background: ${(props: { status: DeployStatus }) => - props.status === "Deployed" - ? "#4797ff" - : props.status === "Failed" - ? "#ed5f85" - : "#f5cb42"}; - border-radius: 20px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx deleted file mode 100644 index ba425987e6..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx +++ /dev/null @@ -1,149 +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 ControllerTab from "./ControllerTab"; - -type Props = { - selectors?: string[]; - currentChart: ChartType; - onUpdate: (props: any) => void; - onSelectPod: (pod: any) => void; -}; - -const PodDropdown: React.FunctionComponent = ({ - currentChart, - selectors, - onUpdate, - onSelectPod, -}) => { - 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 renderTabs = () => { - return controllers.map((c, i) => { - return ( - { - setSelectedPod(pod); - - if (userSelected) { - onSelectPod(pod); - } - }} - selectors={selectors ? [selectors[i]] : null} - controller={c} - isLast={i === controllers?.length - 1} - isFirst={i === 0} - setPodError={(x: string) => setPodError(x)} - onUpdate={onUpdate} - /> - ); - }); - }; - - const renderStatusSection = () => { - if (isLoading) { - return ( - - - - ); - } - if (controllers?.length > 0) { - return {renderTabs()}; - } - - return ( - - category - No objects to display. This might happen while your app is still - deploying. - - ); - }; - - return {renderStatusSection()}; -}; - -export default PodDropdown; - -const TabWrapper = styled.div` - width: 100%; - min-height: 50px; -`; - -const StyledStatusSection = styled.div` - padding: 0px; - user-select: text; - overflow: hidden; - width: 100%; - font-size: 13px; -`; - -const NoControllers = styled.div` - position: relative; - width: 100%; - display: flex; - min-height: 50px; - 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/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx deleted file mode 100644 index dc8ef97d6c..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx +++ /dev/null @@ -1,223 +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 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; - font-size: 13px; - padding: 20px 18px 20px 42px; - text-shadow: 0px 0px 8px none; - overflow: visible; - cursor: pointer; -`; - -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: 10px; - 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-left: 12px; - mnargin-right: -1px; - width: 8px; - min-width: 7px; - height: 8px; - 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/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx deleted file mode 100644 index 966a8e6ba5..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; - -import { kindToIcon } from "shared/rosettaStone"; - -type PropsType = { - label: string; - name: string; - handleClick?: () => void; - selected?: boolean; - isLast?: boolean; - roundAllCorners?: boolean; - status?: { - label: string; - available?: number; - total?: number; - } | null; - expanded?: boolean; -}; - -type StateType = { - expanded: boolean; - showTooltip: boolean; -}; - -export default class ResourceTab extends Component { - state = { - expanded: this.props.expanded || false, - showTooltip: false, - }; - - renderDropdownIcon = () => { - if (this.props.children) { - return ( - - arrow_right - - ); - } - }; - - renderIcon = (kind: string) => { - let icon = "tonality"; - if (Object.keys(kindToIcon).includes(kind)) { - icon = kindToIcon[kind]; - } - - return ( - - {icon} - - ); - }; - - renderTooltip = (x: string): JSX.Element | undefined => { - if (this.state.showTooltip) { - return {x}; - } - }; - - getStatusText = () => { - let { status } = this.props; - if (status.available && status.total) { - return `${status.available}/${status.total}`; - } else if (status.label) { - return status.label; - } - }; - - renderStatus = () => { - let { status } = this.props; - if (status) { - return ( - - {this.getStatusText()} - - - - ); - } - }; - - renderExpanded = () => { - if (this.props.children && this.state.expanded) { - return {this.props.children}; - } - }; - - render() { - let { - label, - name, - children, - isLast, - handleClick, - selected, - status, - roundAllCorners, - } = this.props; - return ( - handleClick && handleClick()} - roundAllCorners={roundAllCorners} - > - { - if (children) { - this.setState({ expanded: !this.state.expanded }); - } - }} - > - - {this.renderDropdownIcon()} - - {this.renderIcon(label)} - {label} - { - this.setState({ showTooltip: true }); - }} - onMouseOut={() => { - this.setState({ showTooltip: false }); - }} - > - {name} - - {this.renderTooltip(name)} - - - {this.renderStatus()} - - {this.renderExpanded()} - - ); - } -} - -const StyledResourceTab = styled.div` - width: 100%; - font-size: 13px; - border-bottom-left-radius: ${(props: { - isLast: boolean; - roundAllCorners: boolean; - }) => (props.isLast ? "10px" : "")}; - animation: fadeIn 0.2s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const Tooltip = styled.div` - position: absolute; - right: 0px; - top: 25px; - white-space: nowrap; - height: 18px; - padding: 2px 5px; - background: #383842dd; - display: flex; - align-items: center; - 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 ExpandWrapper = styled.div``; - -const ResourceHeader = styled.div` - width: 100%; - height: 50px; - display: flex; - font-size: 13px; - align-items: center; - justify-content: space-between; - color: #ffffff66; - user-select: none; - padding: 8px 18px; - padding-left: ${(props: { expanded: boolean; hasChildren: boolean }) => - props.hasChildren ? "10px" : "22px"}; - cursor: pointer; - :hover { - background: #ffffff18; - - > i { - background: #ffffff22; - } - } -`; - -const Info = styled.div` - display: flex; - flex-direction: row; - align-items: center; - width: 80%; - height: 100%; -`; - -const Metadata = styled.div` - display: flex; - align-items: center; - position: relative; - max-width: ${(props: { hasStatus: boolean }) => - props.hasStatus ? "calc(100% - 20px)" : "100%"}; -`; - -const Status = styled.div` - display: flex; - width; 20%; - font-size: 12px; - text-transform: capitalize; - 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-left: 12px; - width: 8px; - min-width: 8px; - height: 8px; - background: ${(props: { status: string }) => - props.status === "running" || - props.status === "Ready" || - props.status === "Completed" - ? "#4797ff" - : props.status === "failed" || props.status === "FailedValidation" - ? "#ed5f85" - : props.status === "completed" - ? "#00d12a" - : "#f5cb42"}; - border-radius: 20px; -`; - -const ResourceName = styled.div` - color: #ffffff; - margin-right: 15px; - margin-left: ${(props: { showKindLabels: boolean }) => - props.showKindLabels ? "10px" : ""}; - text-transform: none; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - -const IconWrapper = styled.div` - width: 25px; - height: 25px; - display: flex; - align-items: center; - justify-content: center; - - > i { - font-size: 15px; - color: #ffffff; - margin-right: 14px; - } -`; - -const DropdownIcon = styled.div` - > i { - margin-top: 2px; - margin-right: 11px; - font-size: 20px; - color: #ffffff66; - cursor: pointer; - border-radius: 20px; - background: ${(props: { expanded: boolean }) => - props.expanded ? "#ffffff18" : ""}; - transform: ${(props: { expanded: boolean }) => - props.expanded ? "rotate(180deg)" : ""}; - animation: ${(props: { expanded: boolean }) => - props.expanded ? "quarterTurn 0.3s" : ""}; - animation-fill-mode: forwards; - - @keyframes quarterTurn { - from { - transform: rotate(0deg); - } - to { - transform: rotate(90deg); - } - } - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts deleted file mode 100644 index e6e62e2bf2..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts +++ /dev/null @@ -1,66 +0,0 @@ -export const getPodStatus = (status: any) => { - if (status?.phase === "Pending" && status?.containerStatuses !== undefined) { - return status.containerStatuses[0].state?.waiting?.reason || "Pending"; - } else if (status?.phase === "Pending") { - return "Pending"; - } - - if (status?.phase === "Failed") { - return "failed"; - } - - if (status?.phase === "Running") { - let collatedStatus = "running"; - - status?.containerStatuses?.forEach((s: any) => { - if (s.state?.waiting) { - collatedStatus = - s.state?.waiting?.reason === "CrashLoopBackOff" - ? "failed" - : "waiting"; - } else if ( - s.state?.terminated && - (s.state.terminated?.exitCode !== 0 || - s.state.terminated?.reason !== "Completed") - ) { - collatedStatus = "failed"; - } - }); - return collatedStatus; - } -}; - -export const getAvailability = (kind: string, c: any) => { - switch (kind?.toLowerCase()) { - case "deployment": - case "replicaset": - return [ - c.status?.availableReplicas || - c.status?.replicas - c.status?.unavailableReplicas || - 0, - c.status?.replicas || 0, - ]; - case "statefulset": - return [c.status?.readyReplicas || 0, c.status?.replicas || 0]; - case "daemonset": - return [ - c.status?.numberAvailable || 0, - c.status?.desiredNumberScheduled || 0, - ]; - case "job": - return [1, 1]; - } -}; - -export const getAvailabilityStacks = (c: any) => { - - const available = - c.status?.updatedReplicas || - c.status?.replicas - c.status?.unavailableReplicas || - 0; - const unavailable = c.status?.unavailableReplicas - const total = c.status.replicas; - const stale = (unavailable != null ? c.status?.updatedReplicas : c.status?.availableReplicas - c.status?.updatedReplicas) || 0; - return [available, total, stale, unavailable]; - -}; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx deleted file mode 100644 index fb8eef9f12..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx +++ /dev/null @@ -1,697 +0,0 @@ -import React, { useState, useEffect, useContext } from "react"; -import { CellProps } from "react-table"; - -import styled from "styled-components"; -import Table from "components/Table"; -import Loading from "components/Loading"; -import danger from "assets/danger.svg"; -import rocket from "assets/rocket.png"; -import document from "assets/document.svg"; -import info from "assets/info-outlined.svg"; -import status from "assets/info-circle.svg"; -import { readableDate, relativeDate } from "shared/string_utils"; -import TitleSection from "components/TitleSection"; -import api from "shared/api"; -import Modal from "main/home/modals/Modal"; -import time from "assets/time.svg"; -import { Context } from "shared/Context"; -import { InitLogData } from "../logs-section/LogsSection"; -import { Direction, Log, parseLogs } from "../logs-section/useAgentLogs"; -import dayjs from "dayjs"; -import Anser from "anser"; - -type Props = { - namespace: string; - filters: any; - setLogData?: (logData: InitLogData) => void; -}; - -interface ExpandedIncidentLogsProps { - logs: Log[]; - onViewMore: () => void; -} - -const ExpandedIncidentLogs = ({ - logs, - onViewMore, -}: ExpandedIncidentLogsProps) => { - if (!logs.length) { - return ( - - - - ); - } - - return ( - - - {logs?.map((log, i) => { - return ( - - {log.lineNumber}. - - {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")} - - ); - })} - - - ); - })} - - - { - e.preventDefault(); - e.stopPropagation(); - onViewMore(); - }} - > - View complete log history - open_in_new{" "} - - - - ); -}; - -const EventList: React.FC = ({ filters, namespace, setLogData }) => { - const { currentProject, currentCluster } = useContext(Context); - const [events, setEvents] = useState([]); - const [logs, setLogs] = useState([]); - const [expandedEvent, setExpandedEvent] = useState(null); - const [expandedIncidentEvents, setExpandedIncidentEvents] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [refresh, setRefresh] = useState(true); - - const redirectToLogs = (incident: any) => { - api - .getIncidentEvents( - "", - { - incident_id: incident.id, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - const podName = res.data?.events[0]?.pod_name; - const timestamp = res.data?.events[0]?.last_seen; - const revision = res.data?.events[0]?.revision; - - setLogData({ - podName, - timestamp, - revision, - }); - }); - }; - - useEffect(() => { - if (!refresh) { - return; - } - - if (filters.job_name) { - api - .listPorterJobEvents("", filters, { - project_id: currentProject.id, - cluster_id: currentCluster.id, - }) - .then((res) => { - setEvents(res.data.events); - setIsLoading(false); - setRefresh(false); - }); - } else { - api - .listPorterEvents("", filters, { - project_id: currentProject.id, - cluster_id: currentCluster.id, - }) - .then((res) => { - setEvents(res.data.events); - setIsLoading(false); - setRefresh(false); - }); - } - }, [refresh]); - - useEffect(() => { - if (!expandedEvent) { - return; - } - - api - .getIncidentEvents( - "", - { - incident_id: expandedEvent.id, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - if (!expandedEvent.should_view_logs) { - setExpandedIncidentEvents(res.data.events); - return null; - } - - const events = res.data?.events ?? []; - - api - .getLogs( - "", - { - pod_selector: events[0]?.pod_name, - namespace, - revision: events[0]?.revision, - start_range: dayjs(events[0]?.updated_at) - .subtract(14, "day") - .toISOString(), - end_range: dayjs(events[0]?.updated_at).toISOString(), - limit: 100, - direction: Direction.backward, - search_param: "", - }, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - } - ) - .then((res) => { - const logs = parseLogs( - res.data.logs - ?.filter(Boolean) - .map((logLine: any) => logLine.line) - .reverse() - ); - setLogs(logs); - }); - - setExpandedIncidentEvents(res.data.events); - }); - }, [expandedEvent]); - - const renderExpandedEventMessage = () => { - if (!expandedIncidentEvents) { - return ; - } - - return ( - <> - - - {expandedIncidentEvents[0].detail} - - {expandedEvent.should_view_logs ? ( - redirectToLogs(expandedEvent)} - /> - ) : null} - - ); - }; - - const renderIncidentSummaryCell = (incident: any) => { - return ( - - - {incident.short_summary} - {incident.severity === "normal" ? ( - <> - ) : ( - Critical - )} - - ); - }; - - const renderDeploymentFinishedCell = (release: any) => { - return ( - - - Revision {release.revision} was successfully deployed - - ); - }; - - const renderJobStartedCell = (timestamp: any) => { - return ( - - - The job started at {readableDate(timestamp)} - - ); - }; - - const renderJobFinishedCell = (timestamp: any) => { - return ( - - - The job finished at {readableDate(timestamp)} - - ); - }; - - const columns = React.useMemo( - () => [ - { - Header: "Monitors", - columns: [ - { - Header: "Description", - accessor: "type", - width: 500, - Cell: ({ row }: CellProps) => { - if (row.original.type == "incident") { - return renderIncidentSummaryCell(row.original.data); - } else if (row.original.type == "deployment_finished") { - return renderDeploymentFinishedCell(row.original.data); - } else if (row.original.type == "job_started") { - return renderJobStartedCell(row.original.timestamp); - } else if (row.original.type == "job_finished") { - return renderJobFinishedCell(row.original.timestamp); - } - - return null; - }, - }, - { - Header: "Last seen", - accessor: "timestamp", - width: 140, - Cell: ({ row }: CellProps) => { - return {relativeDate(row.original.timestamp)}; - }, - }, - { - id: "details", - accessor: "", - width: 20, - Cell: ({ row }: CellProps) => { - if (row.original.type == "incident") { - return ( - { - setExpandedEvent(row.original.data); - }} - > - - Details - - ); - } - - return null; - }, - }, - ], - }, - ], - [] - ); - - return ( - <> - {expandedEvent && ( - { - setExpandedEvent(null); - setLogs([]); - }} - height="auto" - > - - {expandedEvent.release_name} - - - - Last updated: - {readableDate(expandedEvent.updated_at)} - - - Status: - {expandedEvent.status} - - - Priority:{" "} - {expandedEvent.severity} - - - {expandedEvent?.porter_doc_link && ( - - View troubleshooting steps - open_in_new{" "} - - )} - {renderExpandedEventMessage()} - - )} - {isLoading ? ( - - - - ) : ( - -
{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} - - this.setState({ rollbackRevision: revision.version }) - } - > - {isCurrent ? "Current" : "Revert"} - -
Revision no.Timestamp - {this.props.chart.git_action_config ? "Commit" : "Image Tag"} - Template versionRollback
- - - - - - - )} - - ); -}; - -export default EventList; - -const LogsLoadWrapper = styled.div` - height: 50px; -`; - -const Message = styled.div` - padding: 20px; - background: #26292e; - border-radius: 5px; - line-height: 1.5em; - border: 1px solid #aaaabb33; - font-size: 13px; - display: flex; - align-items: center; - > img { - width: 13px; - margin-right: 20px; - } -`; - -const Capitalize = styled.div` - text-transform: capitalize; -`; - -const Bold = styled.div` - font-weight: 500; - margin-right: 5px; -`; - -const InfoTab = styled.div` - display: flex; - align-items: center; - opacity: 50%; - font-size: 13px; - margin-right: 15px; - justify-content: center; - - > img { - width: 13px; - margin-right: 7px; - } -`; - -const InfoRow = styled.div` - display: flex; - align-items: center; - justify-content: flex-start; - margin-bottom: 12px; -`; - -const Text = styled.div` - font-weight: 500; - font-size: 18px; - z-index: 999; -`; - -const Icon = styled.img` - width: 16px; - margin-right: 6px; -`; - -const TableButton = styled.div<{ width?: string }>` - border-radius: 5px; - height: 30px; - color: white; - width: ${(props) => props.width || "85px"}; - display: flex; - align-items: center; - justify-content: center; - background: #ffffff11; - border: 1px solid #aaaabb33; - margin-right: -17px; - cursor: pointer; - :hover { - border: 1px solid #7a7b80; - } -`; - -const ClusterName = styled.div` - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - background: blue; - width: 100px; -`; - -const Title = styled.div` - font-size: 18px; - margin-bottom: 10px; - color: #ffffff; -`; - -const Placeholder = styled.div` - width: 100%; - height: 300px; - color: #aaaabb55; - display: flex; - font-size: 14px; - padding-right: 50px; - align-items: center; - justify-content: center; -`; - -const ClusterIcon = styled.img` - width: 14px; - margin-right: 9px; - opacity: 70%; -`; - -const Flex = styled.div` - display: flex; - align-items: center; -`; - -const AlertIcon = styled.img` - width: 20px; - margin-right: 15px; - margin-left: 0px; -`; - -const NameWrapper = styled.div` - display: flex; - align-items: center; - color: white; -`; - -const LoadWrapper = styled.div` - width: 100%; - height: 300px; -`; - -const Status = styled.div<{ color: string }>` - padding: 5px 7px; - background: ${(props) => props.color}; - font-size: 12px; - border-radius: 3px; - word-break: keep-all; - display: flex; - color: white; - margin-right: 50px; - align-items: center; - margin-left: 15px; - justify-content: center; - height: 20px; -`; - -const TableWrapper = styled.div` - overflow-x: auto; - animation: fadeIn 0.3s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; -`; - -const StyledMonitorList = styled.div` - height: 200px; - width: 100%; - font-size: 13px; - background: #ffffff11; - border-radius: 5px; - border: 1px solid #aaaabb33; -`; - -const NoResultsFoundWrapper = styled(Flex)` - flex-direction: column; - justify-contents: center; -`; - -const Button = styled.div` - background: #26292e; - border-radius: 5px; - height: 30px; - font-size: 13px; - display: flex; - cursor: pointer; - align-items: center; - padding: 10px; - padding-left: 8px; - > i { - font-size: 16px; - margin-right: 5px; - } - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - } -`; - -const FlexRow = styled.div` - display: flex; - align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - margin-top: 20px; -`; - -const DocsLink = styled.a` - display: inline-block; - color: #8590ff; - border-bottom: 1px solid #8590ff; - cursor: pointer; - user-select: none; - padding: 3px 0; - margin-bottom: 18px; - - > i { - font-size: 12px; - margin-left: 5px; - } -`; - -const LogsSectionWrapper = styled.div` - position: relative; -`; - -const StyledLogsSection = styled.div` - margin-top: 20px; - width: 100%; - display: flex; - flex-direction: column; - position: relative; - font-size: 13px; - max-height: 400px; - border-radius: 8px; - border: 1px solid #ffffff33; - border-top: none; - background: #101420; - 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 LogSpan = 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"}; -`; - -export const ViewLogsWrapper = styled.div` - margin-bottom: -15px; - margin-top: 15px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx deleted file mode 100644 index 2ca9a78019..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import React, { useEffect, useContext, useState } from "react"; -import api from "shared/api"; -import styled from "styled-components"; -import EventList from "./EventList"; -import Loading from "components/Loading"; -import InfiniteScroll from "react-infinite-scroll-component"; -import { Context } from "shared/Context"; -import Dropdown from "components/Dropdown"; -import { InitLogData } from "../logs-section/LogsSection"; - -type Props = { - currentChart: any; - setLogData?: (logData: InitLogData) => void; - overridingJobName?: string; -}; - -const EventsTab: React.FC = ({ - currentChart, - setLogData, - overridingJobName, -}) => { - const [hasPorterAgent, setHasPorterAgent] = useState(true); - const [isPorterAgentInstalling, setIsPorterAgentInstalling] = useState(false); - const { currentProject, currentCluster } = useContext(Context); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - // determine if the agent is installed properly - if not, start by render upgrade screen - checkForAgent(); - }, []); - - useEffect(() => { - if (!isPorterAgentInstalling) { - return; - } - - const checkForAgentInterval = setInterval(checkForAgent, 3000); - - return () => clearInterval(checkForAgentInterval); - }, [isPorterAgentInstalling]); - - const checkForAgent = () => { - const project_id = currentProject?.id; - const cluster_id = currentCluster?.id; - - api - .detectPorterAgent("", {}, { project_id, cluster_id }) - .then((res) => { - if (res.data?.version != "v3") { - setHasPorterAgent(false); - } else { - // next, check whether events can be queried - if they can, we're good to go - let filters: any = getFilters(); - - let apiQuery = api.listPorterEvents; - - if (filters.job_name) { - apiQuery = api.listPorterJobEvents; - } - - apiQuery("", filters, { - project_id: currentProject.id, - cluster_id: currentCluster.id, - }) - .then((res) => { - setHasPorterAgent(true); - setIsPorterAgentInstalling(false); - }) - .catch((err) => { - // do nothing - this is expected while installing - }); - } - - setIsLoading(false); - }) - .catch((err) => { - if (err.response?.status === 404) { - setHasPorterAgent(false); - setIsLoading(false); - } - }); - }; - - const installAgent = async () => { - const project_id = currentProject?.id; - const cluster_id = currentCluster?.id; - - setIsPorterAgentInstalling(true); - - api - .installPorterAgent("", {}, { project_id, cluster_id }) - .then() - .catch((err) => { - setIsPorterAgentInstalling(false); - console.log(err); - }); - }; - - const triggerInstall = () => { - installAgent(); - }; - - const getFilters = () => { - if (overridingJobName) { - return { - release_name: currentChart.name, - release_namespace: currentChart.namespace, - job_name: overridingJobName, - }; - } - - return { - release_name: currentChart.name, - release_namespace: currentChart.namespace, - }; - }; - - if (isPorterAgentInstalling) { - return ( - -
Installing agent...
-
- ); - } - - if (isLoading) { - return ( - - - - ); - } - - if (!hasPorterAgent) { - return ( - -
-
We couldn't detect the Porter agent on your cluster
- In order to use the events tab, you need to install the Porter agent. - triggerInstall()}> - add Install Porter agent - -
-
- ); - } - - return ( - - - - ); -}; - -export default EventsTab; - -const Label = styled.div` - color: #ffffff44; - margin-right: 8px; - font-size: 13px; -`; - -const ControlRow = styled.div` - display: flex; - align-items: center; - margin-bottom: 30px; - padding-left: 0px; - font-size: 13px; -`; - -const EventsPageWrapper = styled.div` - font-size: 13px; - 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 EventsGrid = styled.div` - display: grid; - grid-row-gap: 15px; - grid-template-columns: 1; -`; - -const InstallPorterAgentButton = styled.button` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - font-size: 13px; - cursor: pointer; - font-family: "Work Sans", sans-serif; - border: none; - border-radius: 5px; - color: white; - height: 35px; - padding: 0px 8px; - padding-bottom: 1px; - margin-top: 20px; - font-weight: 500; - padding-right: 15px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - cursor: ${(props: { disabled?: boolean }) => - props.disabled ? "not-allowed" : "pointer"}; - background: ${(props: { disabled?: boolean }) => - props.disabled ? "#aaaabbee" : "#5561C0"}; - :hover { - filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")}; - } - > 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; - } -`; - -const Placeholder = styled.div` - padding: 30px; - padding-bottom: 40px; - font-size: 13px; - color: #ffffff44; - min-height: 400px; - height: 50vh; - background: #ffffff08; - border-radius: 8px; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - - > i { - font-size: 18px; - margin-right: 8px; - } -`; - -const Header = styled.div` - font-weight: 500; - color: #aaaabb; - font-size: 16px; - margin-bottom: 15px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/events/styles.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/events/styles.ts deleted file mode 100644 index 5225ef49b2..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/events/styles.ts +++ /dev/null @@ -1,155 +0,0 @@ -import styled, { css } from "styled-components"; - -const textFontStack = css` - font-family: "Work Sans", Arial, sans-serif; -`; - -export const theme = { - bg: { - default: "#FFFFFF", - reverse: "#16171A", - wash: "#FAFAFA", - divider: "#F6F7F8", - border: "#EBECED", - inactive: "#DFE7EF", - shadeone: "#26292E", - shadetwo: "#26292E", - }, - line: { - default: "1px solid #aaaabb33", - }, - brand: { - default: "#4400CC", - alt: "#7B16FF", - wash: "#E8E5FF", - border: "#DDD9FF", - dark: "#2A0080", - }, - generic: { - default: "#E6ECF7", - alt: "#F6FBFC", - }, - space: { - default: "#0062D6", - alt: "#1CD2F2", - wash: "#E5F0FF", - border: "#BDD8FF", - dark: "#0F015E", - }, - success: { - default: "#00B88B", - alt: "#00D5BD", - dark: "#00663C", - wash: "#D9FFF2", - border: "#9FF5D9", - }, - text: { - default: "#ffffffaa", - secondary: "#384047", - alt: "#67717A", - placeholder: "#7C8894", - reverse: "#FFFFFF", - }, - warn: { - default: "#E22F2F", - alt: "#E2197A", - dark: "#85000C", - wash: "#FFEDF6", - border: "#FFCCE5", - }, -}; - -export const StyledTable = styled.table` - width: 100%; - min-width: 500px; - border-radius: 5px; - overflow: hidden; - border: 1px solid #aaaabb33; - border-spacing: 0; -`; - -export const StyledTHead = styled.thead` - width: 100%; - position: sticky; - - > tr { - background: ${theme.bg.shadeone}; - line-height: 2.2em; - - > th { - border-bottom: ${theme.line.default}; - } - } - - > tr:first-child { - > th:first-child { - border-top-left-radius: 6px; - display: none; - } - - > th:last-child { - border-top-right-radius: 6px; - } - } -`; - -export const StyledTBody = styled.tbody` - > tr { - background: ${theme.bg.shadetwo}; - height: 80px; - line-height: 1.2em; - - > td { - border-bottom: ${theme.line.default}; - } - - > td:first-child { - } - - > td:last-child { - } - } - - > tr:last-child { - > td:first-child { - border-bottom-left-radius: 6px; - } - - > td:last-child { - border-bottom-right-radius: 6px; - } - - > td { - border-bottom: none; - } - } -`; - -export const StyledTd = styled.td` - ${textFontStack} - font-size: 13px; - color: ${theme.text.default}; - :first-child { - padding-left: 20px; - } - - :last-child { - } - - user-select: text; -`; - -export const StyledTh = styled.th` - ${textFontStack} - - text-align: left; - font-size: 13px; - font-weight: 400; - color: #ffffffaa; - :first-child { - padding-left: 20px; - } - :last-child { - padding-right: 10px; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Edge.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Edge.tsx deleted file mode 100644 index 8b77ce214f..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Edge.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; - -import { edgeColors } from "shared/rosettaStone"; -import { EdgeType } from "shared/types"; - -const thickness = 12; - -type PropsType = { - x1: number; - y1: number; - x2: number; - y2: number; - originX: number; - originY: number; - edge: EdgeType; - setCurrentEdge: (edge: EdgeType) => void; -}; - -type StateType = { - showArrowHead: boolean; -}; - -export default class Edge extends Component { - state = { - showArrowHead: true, - }; - - render() { - let { originX, originY, edge, setCurrentEdge } = this.props; - let x1 = Math.round(originX + this.props.x1); - let x2 = Math.round(originX + this.props.x2); - let y1 = Math.round(originY - this.props.y1); - let y2 = Math.round(originY - this.props.y2); - - var length = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); - // center - var cx = (x1 + x2) / 2 - length / 2; - var cy = (y1 + y2) / 2 - thickness / 2; - // angle - var angle = Math.atan2(y1 - y2, x1 - x2) * (180 / Math.PI); - - return ( - setCurrentEdge(edge)} - onMouseLeave={() => setCurrentEdge(null)} - type={edge.type} - > - {this.state.showArrowHead ? ( - - ) : null} - - - ); - } -} - -const ArrowHead = styled.div` - width: 0; - height: 0; - margin-left: 20px; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-right: 10px solid - ${(props: { color: string }) => (props.color ? props.color : "#ffffff66")}; -`; - -const VisibleLine = styled.section` - height: 2px; - width: 100%; - background: ${(props: { color: string }) => - props.color ? props.color : "#ffffff66"}; -`; - -const StyledEdge: any = styled.div.attrs((props: any) => ({ - style: { - top: props.cy + "px", - left: props.cx + "px", - transform: "rotate(" + props.angle + "deg)", - width: props.length + "px", - }, -}))` - position: absolute; - height: ${thickness}px; - cursor: pointer; - z-index: ${(props: { type: string; color: string }) => - props.type == "ControlRel" ? "1" : "0"}; - display: flex; - align-items: center; - justify-content: center; - :hover { - > section { - box-shadow: 0 0 10px #ffffff; - } - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx deleted file mode 100644 index 47057fd070..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx +++ /dev/null @@ -1,750 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; - -import { ChartType, EdgeType, NodeType, ResourceType } from "shared/types"; - -import Node from "./Node"; -import Edge from "./Edge"; -import InfoPanel from "./InfoPanel"; -import ZoomPanel from "./ZoomPanel"; -import SelectRegion from "./SelectRegion"; -import _ from "lodash"; - -const zoomConstant = 0.01; -const panConstant = 0.8; - -type PropsType = { - components: ResourceType[]; - isExpanded: boolean; - setSidebar: (x: boolean) => void; - currentChart: ChartType; - - // Handle revisions expansion for YAML wrapper - showRevisions: boolean; -}; - -type StateType = { - nodes: NodeType[]; - edges: EdgeType[]; - activeIds: number[]; // IDs of all currently selected nodes - originX: number | null; - originY: number | null; - cursorX: number | null; - cursorY: number | null; - deltaX: number | null; // Dragging bg x-displacement - deltaY: number | null; // Dragging y-displacement - panX: number | null; // Two-finger pan x-displacement - panY: number | null; // Two-finger pan y-displacement - anchorX: number | null; // Initial cursorX during region select - anchorY: number | null; // Initial cursorY during region select - nodeClickX: number | null; // Initial cursorX during node click (drag vs click) - nodeClickY: number | null; // Initial cursorY during node click (drag vs click) - dragBg: boolean; // Boolean to track if all nodes should move with mouse (bg drag) - preventBgDrag: boolean; // Prevent bg drag when moving selected with mouse down - relocateAllowed: boolean; // Suppress movement of selected when drawing select region - scale: number; - btnZooming: boolean; - showKindLabels: boolean; - isExpanded: boolean; - currentNode: NodeType | null; - currentEdge: EdgeType | null; - openedNode: NodeType | null; - suppressCloseNode: boolean; // Still click should close opened unless on a node - suppressDisplay: boolean; // Ignore clicks + pan/zoom on InfoPanel or ButtonSection - version?: number; // Track in localstorage for handling updates when unmounted -}; - -// TODO: region-based unselect, shift-click, multi-region -export default class GraphDisplay extends Component { - state = { - nodes: [] as NodeType[], - edges: [] as EdgeType[], - activeIds: [] as number[], - originX: null as number | null, - originY: null as number | null, - cursorX: null as number | null, - cursorY: null as number | null, - deltaX: null as number | null, - deltaY: null as number | null, - panX: null as number | null, - panY: null as number | null, - anchorX: null as number | null, - anchorY: null as number | null, - nodeClickX: null as number | null, - nodeClickY: null as number | null, - dragBg: false, - preventBgDrag: false, - relocateAllowed: false, - scale: 0.5, - btnZooming: false, - showKindLabels: true, - isExpanded: false, - currentNode: null as NodeType | null, - currentEdge: null as EdgeType | null, - openedNode: null as NodeType | null, - suppressCloseNode: false, - suppressDisplay: false, - }; - - spaceRef: any = React.createRef(); - - getRandomIntBetweenRange = (min: number, max: number) => { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min) + min); - }; - - // Handle graph from localstorage - getChartGraph = () => { - let { components, currentChart } = this.props; - - let graph = localStorage.getItem( - `charts.${currentChart.name}-${currentChart.version}` - ); - - let nodes = [] as NodeType[]; - let edges = [] as EdgeType[]; - - if (!graph) { - nodes = this.createNodes(components); - edges = this.createEdges(components); - this.setState({ nodes, edges }); - } else { - let storedState = JSON.parse( - localStorage.getItem( - `charts.${currentChart.name}-${currentChart.version}` - ) - ); - this.setState(storedState); - } - }; - - componentDidMount() { - // Initialize origin - let height = this.spaceRef.offsetHeight; - let width = this.spaceRef.offsetWidth; - this.setState({ - originX: Math.round(width / 2), - originY: Math.round(height / 2), - }); - - // Suppress trackpad gestures - this.spaceRef.addEventListener("touchmove", (e: any) => e.preventDefault()); - this.spaceRef.addEventListener("mousewheel", (e: any) => - e.preventDefault() - ); - - document.addEventListener("keydown", this.handleKeyDown); - document.addEventListener("keyup", this.handleKeyUp); - - this.getChartGraph(); - - window.onbeforeunload = () => { - this.storeChartGraph(); - }; - } - - // Live update on rollback/upgrade - componentDidUpdate(prevProps: PropsType) { - if (!_.isEqual(prevProps.currentChart, this.props.currentChart)) { - this.storeChartGraph(prevProps); - this.getChartGraph(); - } - } - - createNodes = (components: ResourceType[]) => { - return components.map((c: ResourceType) => { - switch (c.Kind) { - case "ClusterRoleBinding": - case "ClusterRole": - case "RoleBinding": - case "Role": - return { - id: c.ID, - RawYAML: c.RawYAML, - name: c.Name, - kind: c.Kind, - x: this.getRandomIntBetweenRange(-500, 0), - y: this.getRandomIntBetweenRange(0, 250), - w: 40, - h: 40, - }; - case "Deployment": - case "StatefulSet": - case "Pod": - case "ServiceAccount": - return { - id: c.ID, - RawYAML: c.RawYAML, - name: c.Name, - kind: c.Kind, - x: this.getRandomIntBetweenRange(0, 500), - y: this.getRandomIntBetweenRange(0, 250), - w: 40, - h: 40, - }; - case "Service": - case "Ingress": - case "ServiceAccount": - return { - id: c.ID, - RawYAML: c.RawYAML, - name: c.Name, - kind: c.Kind, - x: this.getRandomIntBetweenRange(0, 500), - y: this.getRandomIntBetweenRange(-250, 0), - w: 40, - h: 40, - }; - default: - return { - id: c.ID, - RawYAML: c.RawYAML, - name: c.Name, - kind: c.Kind, - x: this.getRandomIntBetweenRange(-400, 0), - y: this.getRandomIntBetweenRange(-250, 0), - w: 40, - h: 40, - }; - } - }); - }; - - createEdges = (components: ResourceType[]) => { - let edges = [] as EdgeType[]; - components.map((c: ResourceType) => { - c.Relations?.ControlRels?.map((rel: any) => { - if (rel.Source == c.ID) { - edges.push({ - type: "ControlRel", - source: rel.Source, - target: rel.Target, - }); - } - }); - c.Relations?.LabelRels?.map((rel: any) => { - if (rel.Source == c.ID) { - edges.push({ - type: "LabelRel", - source: rel.Source, - target: rel.Target, - }); - } - }); - c.Relations?.SpecRels?.map((rel: any) => { - if (rel.Source == c.ID) { - edges.push({ - type: "SpecRel", - source: rel.Source, - target: rel.Target, - }); - } - }); - }); - return edges; - }; - - storeChartGraph = (props?: PropsType) => { - let useProps = props || this.props; - - let { currentChart } = useProps; - let graph = JSON.parse(JSON.stringify(this.state)); - - // Flush non-persistent data - graph.activeIds = []; - graph.currentNode = null; - graph.currentEdge = null; - graph.isExpanded = false; - graph.openedNode = null; - graph.suppressDisplay = false; - graph.suppressCloseNode = false; - - localStorage.setItem( - `charts.${currentChart.name}-${currentChart.version}`, - JSON.stringify(graph) - ); - }; - - componentWillUnmount() { - this.storeChartGraph(); - - this.spaceRef.removeEventListener("touchmove", (e: any) => - e.preventDefault() - ); - this.spaceRef.removeEventListener("mousewheel", (e: any) => - e.preventDefault() - ); - document.removeEventListener("keydown", this.handleKeyDown); - document.removeEventListener("keyup", this.handleKeyUp); - } - - // Handle shift key for multi-select - handleKeyDown = (e: any) => { - if (e.key === "Shift") { - this.setState({ - anchorX: this.state.cursorX, - anchorY: this.state.cursorY, - relocateAllowed: false, - - // Suppress jump when panning with mouse - panX: null, - panY: null, - deltaX: null, - deltaY: null, - }); - } - }; - - handleKeyUp = (e: any) => { - if (e.key === "Shift") { - this.setState({ - anchorX: null, - anchorY: null, - - // Suppress jump when panning with mouse - panX: null, - panY: null, - deltaX: null, - deltaY: null, - }); - } - }; - - handleClickNode = (clickedId: number) => { - let { cursorX, cursorY } = this.state; - - // Store position for distinguishing click vs drag on release - this.setState({ - nodeClickX: cursorX, - nodeClickY: cursorY, - suppressCloseNode: true, - }); - - // Push to activeIds if not already present - let holding = this.state.activeIds; - if (!holding.includes(clickedId)) { - holding.push(clickedId); - } - - // Track and store offset to grab node from anywhere (must store) - this.state.nodes.forEach((node: NodeType) => { - if (this.state.activeIds.includes(node.id)) { - node.toCursorX = node.x - cursorX; - node.toCursorY = node.y - cursorY; - } - }); - - this.setState({ - activeIds: holding, - preventBgDrag: true, - relocateAllowed: true, - }); - }; - - handleReleaseNode = (node: NodeType) => { - let { cursorX, cursorY, nodeClickX, nodeClickY } = this.state; - this.setState({ activeIds: [], preventBgDrag: false }); - - // Distinguish node click vs drag (can't use onClick since drag counts) - if (cursorX === nodeClickX && cursorY === nodeClickY) { - this.setState({ openedNode: node }); - } - }; - - handleMouseDown = () => { - let { cursorX, cursorY } = this.state; - - // Store position for distinguishing click vs drag on release - this.setState({ nodeClickX: cursorX, nodeClickY: cursorY }); - - this.setState({ - dragBg: true, - - // Suppress drifting on repeated click - deltaX: null, - deltaY: null, - panX: null, - panY: null, - scale: 1, - }); - }; - - handleMouseUp = () => { - let { - cursorX, - nodeClickX, - cursorY, - nodeClickY, - suppressCloseNode, - } = this.state; - this.setState({ dragBg: false, activeIds: [] }); - - // Distinguish bg click vs drag for setting closing opened node - if ( - !suppressCloseNode && - cursorX === nodeClickX && - cursorY === nodeClickY - ) { - this.setState({ openedNode: null }); - } else if (this.state.suppressCloseNode) { - this.setState({ suppressCloseNode: false }); - } - }; - - handleMouseMove = (e: any) => { - let { - originX, - originY, - dragBg, - preventBgDrag, - scale, - panX, - panY, - anchorX, - anchorY, - nodes, - activeIds, - relocateAllowed, - } = this.state; - - // Suppress navigation gestures - if (scale !== 1 || panX !== 0 || panY !== 0) { - this.setState({ scale: 1, panX: 0, panY: 0 }); - } - - // Update origin-centered cursor coordinates - let bounds = this.spaceRef.getBoundingClientRect(); - let cursorX = e.clientX - bounds.left - originX; - let cursorY = -(e.clientY - bounds.top - originY); - this.setState({ cursorX, cursorY }); - - // Track delta for dragging background - if (dragBg && !preventBgDrag) { - this.setState({ deltaX: e.movementX, deltaY: e.movementY }); - } - - // Check if within select region - if (anchorX && anchorY) { - nodes.forEach((node: NodeType) => { - if ( - node.x > Math.min(anchorX, cursorX) && - node.x < Math.max(anchorX, cursorX) && - node.y > Math.min(anchorY, cursorY) && - node.y < Math.max(anchorY, cursorY) - ) { - activeIds.push(node.id); - this.setState({ activeIds }); - } - }); - } - }; - - // Handle pan XOR zoom (two-finger gestures count as onWheel) - handleWheel = (e: any) => { - this.setState({ btnZooming: false }); - - // Prevent nav gestures if mouse is over InfoPanel or ButtonSection - if (!this.state.suppressDisplay) { - // Pinch/zoom sets e.ctrlKey to true - if (e.ctrlKey) { - // Clip deltaY for extreme mousewheel values - let deltaY = - e.deltaY >= 0 ? Math.min(40, e.deltaY) : Math.max(-40, e.deltaY); - - let scale = 1; - scale -= deltaY * zoomConstant; - this.setState({ scale, panX: 0, panY: 0 }); - } else { - this.setState({ panX: e.deltaX, panY: e.deltaY, scale: 1 }); - } - } - }; - - btnZoomIn = () => { - this.setState({ scale: 1.24, btnZooming: true }); - }; - - btnZoomOut = () => { - this.setState({ scale: 0.76, btnZooming: true }); - }; - - toggleExpanded = () => { - this.setState({ isExpanded: !this.state.isExpanded }, () => { - this.props.setSidebar(!this.state.isExpanded); - - // Update origin on expand/collapse - let height = this.spaceRef.offsetHeight; - let width = this.spaceRef.offsetWidth; - let nudge = 0; - if (!this.state.isExpanded) { - nudge = 100; - } - this.setState({ - originX: Math.round(width / 2) - nudge, - originY: Math.round(height / 2), - }); - }); - }; - - // Pass origin to node for offset - renderNodes = () => { - let { - activeIds, - originX, - originY, - cursorX, - cursorY, - scale, - panX, - panY, - anchorX, - anchorY, - relocateAllowed, - } = this.state; - - let minX = 0; - let maxX = 0; - let minY = 0; - let maxY = 0; - this.state.nodes.map((node: NodeType, i: number) => { - if (node.x < minX) minX = node.x < minX ? node.x : minX; - maxX = node.x > maxX ? node.x : maxX; - minY = node.y < minY ? node.y : minY; - maxY = node.y > maxY ? node.y : maxY; - }); - let midX = (minX + maxX) / 2; - let midY = (minY + maxY) / 2; - - return this.state.nodes.map((node: NodeType, i: number) => { - // Update position if not highlighting and active - if ( - activeIds.includes(node.id) && - relocateAllowed && - !anchorX && - !anchorY - ) { - node.x = cursorX + node.toCursorX; - node.y = cursorY + node.toCursorY; - } - - // Apply movement from dragging background - if (this.state.dragBg && !this.state.preventBgDrag) { - node.x += this.state.deltaX; - node.y -= this.state.deltaY; - } - - // Apply cursor-centered zoom - if (this.state.scale !== 1) { - if (!this.state.btnZooming) { - node.x = cursorX + scale * (node.x - cursorX); - node.y = cursorY + scale * (node.y - cursorY); - } else { - node.x = midX + scale * (node.x - midX); - node.y = midY + scale * (node.y - midY); - } - } - - // Apply pan - if (this.state.panX !== 0 || this.state.panY !== 0) { - node.x -= panConstant * panX; - node.y += panConstant * panY; - } - - return ( - this.handleClickNode(node.id)} - nodeMouseUp={() => this.handleReleaseNode(node)} - isActive={activeIds.includes(node.id)} - showKindLabels={this.state.showKindLabels} - isOpen={node === this.state.openedNode} - // Parameterized to allow setting to null - setCurrentNode={(node: NodeType) => { - this.setState({ currentNode: node }); - }} - /> - ); - }); - }; - - renderEdges = () => { - return this.state.edges.map((edge: EdgeType, i: number) => { - return ( - - this.setState({ currentEdge: edge }) - } - /> - ); - }); - }; - - renderSelectRegion = () => { - if (this.state.anchorX && this.state.anchorY) { - return ( - - ); - } - }; - - render() { - return ( - (this.spaceRef = element)} - onMouseMove={this.handleMouseMove} - onMouseDown={this.state.suppressDisplay ? null : this.handleMouseDown} - onMouseUp={this.state.suppressDisplay ? null : this.handleMouseUp} - onWheel={this.handleWheel} - > - {this.renderNodes()} - {this.renderEdges()} - {this.renderSelectRegion()} - - this.setState({ suppressDisplay: true })} - onMouseLeave={() => this.setState({ suppressDisplay: false })} - > - - this.setState({ showKindLabels: !this.state.showKindLabels }) - } - > - - done - - Show Type - - {/* - - - {this.state.isExpanded ? "close_fullscreen" : "open_in_full"} - - - */} - - - this.setState({ suppressDisplay: x }) - } - currentNode={this.state.currentNode} - currentEdge={this.state.currentEdge} - openedNode={this.state.openedNode} - // InfoPanel won't trigger onMouseLeave for unsuppressing if close is clicked - closeNode={() => - this.setState({ openedNode: null, suppressDisplay: false }) - } - // For YAML wrapper to trigger resize - isExpanded={this.state.isExpanded} - showRevisions={this.props.showRevisions} - /> - - - ); - } -} - -const Checkbox = styled.div` - width: 16px; - height: 16px; - border: 1px solid #ffffff55; - margin: 0px 8px 0px 3px; - border-radius: 3px; - background: ${(props: { checked: boolean }) => - props.checked ? "#ffffff22" : ""}; - display: flex; - align-items: center; - justify-content: center; - color: #ffffff; - - > i { - font-size: 12px; - padding-left: 0px; - display: ${(props: { checked: boolean }) => (props.checked ? "" : "none")}; - } -`; - -const ToggleLabel = styled.div` - font: 12px "Work Sans"; - color: #ffffff; - position: relative; - height: 24px; - display: flex; - align-items: center; - justify-content: space-between; - border-radius: 3px; - padding-right: 5px; - cursor: pointer; - border: 1px solid #ffffff55; - :hover { - background: #ffffff22; - - > div { - background: #ffffff22; - } - } -`; - -const ButtonSection = styled.div` - position: absolute; - top: 15px; - right: 15px; - display: flex; - align-items: center; - z-index: 999; - cursor: pointer; -`; - -const ExpandButton = styled.div` - width: 24px; - height: 24px; - cursor: pointer; - margin-left: 10px; - display: flex; - justify-content: center; - align-items: center; - border-radius: 3px; - border: 1px solid #ffffff55; - - :hover { - background: #ffffff44; - } - - > i { - font-size: 14px; - } -`; - -const StyledGraphDisplay = styled.div` - overflow: hidden; - cursor: move; - width: ${(props: { isExpanded: boolean }) => - props.isExpanded ? "100vw" : "100%"}; - height: ${(props: { isExpanded: boolean }) => - props.isExpanded ? "100vh" : "100%"}; - background: #202227; - position: ${(props: { isExpanded: boolean }) => - props.isExpanded ? "fixed" : "relative"}; - top: ${(props: { isExpanded: boolean }) => (props.isExpanded ? "-25px" : "")}; - right: ${(props: { isExpanded: boolean }) => - props.isExpanded ? "-25px" : ""}; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx deleted file mode 100644 index b6a526c1c0..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; -import yaml from "js-yaml"; - -import { edgeColors, kindToIcon } from "shared/rosettaStone"; -import { EdgeType, NodeType } from "shared/types"; - -import YamlEditor from "components/YamlEditor"; - -type PropsType = { - currentNode: NodeType; - currentEdge: EdgeType; - openedNode: NodeType; - setSuppressDisplay: (x: boolean) => void; - closeNode: () => void; - isExpanded: boolean; - showRevisions: boolean; -}; - -type StateType = { - wrapperHeight: number; -}; - -export default class InfoPanel extends Component { - state = { - wrapperHeight: 0, - }; - - renderIcon = (kind: string) => { - let icon = "tonality"; - if (Object.keys(kindToIcon).includes(kind)) { - icon = kindToIcon[kind]; - } - - return ( - - {icon} - - ); - }; - - renderColorBlock = (type: string) => { - return ; - }; - - wrapperRef: any = React.createRef(); - - componentDidMount() { - this.setState({ wrapperHeight: this.wrapperRef.offsetHeight }); - } - - componentDidUpdate(prevProps: PropsType) { - if ( - (prevProps.openedNode !== this.props.openedNode || - prevProps.isExpanded !== this.props.isExpanded || - prevProps.showRevisions !== this.props.showRevisions) && - this.wrapperRef - ) { - this.setState({ wrapperHeight: this.wrapperRef.offsetHeight }); - } - } - - renderContents = () => { - let { currentNode, currentEdge, openedNode } = this.props; - if (openedNode) { - return ( - -
- {this.renderIcon(openedNode.kind)} - {openedNode.kind} - {openedNode.name} -
- (this.wrapperRef = element)}> - - -
- ); - } else if (currentNode) { - return ( -
- {this.renderIcon(currentNode.kind)} - {currentNode.kind} - {currentNode.name} -
- ); - } else if (currentEdge) { - return ( - - {this.renderColorBlock(currentEdge.type)} - {this.renderEdgeMessage(currentEdge)} - - ); - } - - return ( -
- - info - - Hover over a node or edge to display info. -
- ); - }; - - renderEdgeMessage = (edge: EdgeType) => { - // TODO: render more information about edges (labels, spec property field) - switch (edge.type) { - case "ControlRel": - return "Controller Relation"; - case "LabelRel": - return "Label Relation"; - case "SpecRel": - return "Spec Relation"; - } - }; - - render() { - let { openedNode, closeNode, setSuppressDisplay } = this.props; - - // Only suppress display gestures (click, pan, and zoom) if expanded - return ( - setSuppressDisplay(true) : null} - onMouseLeave={openedNode ? () => setSuppressDisplay(false) : null} - > - {this.renderContents()} - - {openedNode ? ( - - close - - ) : null} - - ); - } -} - -const Wrapped = styled.div` - height: 100%; - position: relative; -`; - -const YamlWrapper = styled.div` - width: 100%; - margin-top: 7px; - height: calc(100% - 44px); - border-radius: 5px; - border: 1px solid #ffffff22; - overflow: hidden; - background: #000000; -`; - -const ColorBlock = styled.div` - width: 15px; - height: 15px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 3px; - margin-left: -2px; - margin-right: 13px; - background: ${(props: { color: string }) => - props.color ? props.color : "#ffffff66"}; -`; - -const Div = styled.div` - display: flex; - padding-left: 7px; - align-items: center; - padding-right: 23px; -`; - -const EdgeInfo = styled.div` - display: flex; - align-items: center; - padding-left: 7px; - padding-right: 23px; - margin-top: 5px; -`; - -const ResourceName = styled.div` - color: #ffffff; - margin-left: 10px; - text-transform: none; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -`; - -const IconWrapper = styled.div` - width: 25px; - height: 25px; - display: flex; - align-items: center; - justify-content: center; - - > i { - font-size: 16px; - color: #ffffff; - margin-right: 14px; - } -`; - -const StyledInfoPanel = styled.div` - position: absolute; - right: 15px; - bottom: 15px; - color: #ffffff66; - height: ${(props: { expanded: boolean }) => - props.expanded ? "calc(100% - 68px)" : "40px"}; - width: ${(props: { expanded: boolean }) => - props.expanded ? "calc(50% - 68px)" : "400px"}; - max-width: 600px; - min-width: 400px; - background: #34373cdf; - border-radius: 3px; - padding-left: 11px; - display: inline-block; - z-index: 999; - padding-top: 7px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - padding-right: 11px; - cursor: default; - - > i { - position: absolute; - padding: 5px; - top: 6px; - right: 6px; - border-radius: 50px; - font-size: 17px; - cursor: pointer; - color: white; - :hover { - background: #ffffff22; - } - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx deleted file mode 100644 index 3ac93bafa6..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; - -import { kindToIcon } from "shared/rosettaStone"; -import { NodeType } from "shared/types"; - -type PropsType = { - node: NodeType; - originX: number; - originY: number; - nodeMouseDown: () => void; - nodeMouseUp: () => void; - isActive: boolean; - showKindLabels: boolean; - setCurrentNode: (node: NodeType) => void; - isOpen: boolean; -}; - -type StateType = {}; - -export default class Node extends Component { - state = {}; - - render() { - let { x, y, w, h, name, kind } = this.props.node; - let { originX, originY, nodeMouseDown, nodeMouseUp, isActive } = this.props; - - let icon = "tonality"; - if (Object.keys(kindToIcon).includes(kind)) { - icon = kindToIcon[kind]; - } - - return ( - - - {this.props.showKindLabels ? kind : null} - - this.props.setCurrentNode(this.props.node)} - onMouseLeave={() => this.props.setCurrentNode(null)} - isActive={isActive} - isOpen={this.props.isOpen} - > - {icon} - - - {name} - - - ); - } -} - -const Kind = styled.div` - color: #ffffff33; - position: relative; - margin-top: -25px; - padding-bottom: 10px; - max-width: 140px; - text-align: center; - min-width: 1px; - height: 25px; - font-size: 13px; - font-family: "Work Sans", sans-serif; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - z-index: 101; -`; - -const NodeLabel = styled.div` - position: relative; - margin-bottom: -25px; - padding-top: 10px; - color: #aaaabb; - max-width: 140px; - font-size: 13px; - font-family: "Work Sans", sans-serif; - text-align: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - z-index: 101; -`; - -const NodeBlock = styled.div` - background: #444446; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - border-radius: 100px; - border: ${(props: { isActive: boolean; isOpen: boolean }) => - props.isOpen ? "3px solid #ffffff" : ""}; - box-shadow: ${(props: { isActive: boolean; isOpen: boolean }) => - props.isActive ? "0 0 10px #ffffff66" : "0px 0px 10px 2px #00000022"}; - z-index: 100; - cursor: pointer; - :hover { - background: #555556; - } - > i { - color: white; - font-size: 18px; - } -`; - -const StyledNode: any = styled.div.attrs((props: NodeType) => ({ - style: { - top: props.y + "px", - left: props.x + "px", - }, -}))` - position: absolute; - width: ${(props: NodeType) => props.w + "px"}; - height: ${(props: NodeType) => props.h + "px"}; - color: #ffffff22; - border-radius: 100px; - display: flex; - flex-direction: column; - align-items: center; -`; - -const StyledMark = styled.mark` - background-color: #202227aa; - color: #aaaabb; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/SelectRegion.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/SelectRegion.tsx deleted file mode 100644 index 99200eaffc..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/SelectRegion.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; - -type PropsType = { - anchorX: number; - anchorY: number; - originX: number; - originY: number; - cursorX: number; - cursorY: number; -}; - -type StateType = {}; - -export default class SelectRegion extends Component { - state = {}; - - render() { - let { cursorX, cursorY, anchorX, anchorY, originX, originY } = this.props; - - var x, y, w, h; - if (cursorY < anchorY) { - y = anchorY; - } else { - y = cursorY; - } - if (cursorX < anchorX) { - x = cursorX; - } else { - x = anchorX; - } - - w = Math.abs(cursorX - anchorX); - h = Math.abs(cursorY - anchorY); - - return ( - - ); - } -} - -const StyledSelectRegion: any = styled.div.attrs( - (props: { x: number; y: number; w: number; h: number }) => ({ - style: { - top: props.y + "px", - left: props.x + "px", - width: props.w + "px", - height: props.h + "px", - }, - }) -)` - position: absolute; - background: #ffffff22; - z-index: 1; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/ZoomPanel.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/ZoomPanel.tsx deleted file mode 100644 index 0d0d1a666c..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/ZoomPanel.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { Component } from "react"; -import styled from "styled-components"; - -type PropsType = { - btnZoomIn: () => void; - btnZoomOut: () => void; -}; - -type StateType = { - wrapperHeight: number; -}; - -export default class ZoomPanel extends Component { - state = { - wrapperHeight: 0, - }; - - wrapperRef: any = React.createRef(); - - componentDidMount() { - this.setState({ wrapperHeight: this.wrapperRef.offsetHeight }); - } - - renderContents = () => { - return ( -
- - add - - - - remove - -
- ); - }; - - render() { - return {this.renderContents()}; - } -} - -const Div = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - height: calc(100% - 7px); -`; - -const IconWrapper = styled.div` - width: 25px; - height: 25px; - display: flex; - align-items: center; - justify-content: center; - margin-top: -4px; - margin-bottom: -4px; - cursor: pointer; - - > i { - font-size: 16px; - color: #ffffff; - } -`; - -const StyledZoomer = styled.div` - position: absolute; - left: 15px; - bottom: 15px; - color: #ffffff; - height: 64px; - width: 36px; - background: #34373cdf; - border-radius: 3px; - padding-left: 11px; - display: inline-block; - z-index: 999; - padding-top: 7px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - padding-right: 11px; - cursor: default; -`; - -const ZoomBreaker = styled.div` - background: #ffffff20; - height: 1px; - width: 22px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/DisabledNamespaces.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/DisabledNamespaces.ts deleted file mode 100644 index 055b94cc7c..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/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/cluster-dashboard/expanded-chart/jobs/ConnectToJobInstructionsModal.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ConnectToJobInstructionsModal.tsx deleted file mode 100644 index 2b3686ce3e..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ConnectToJobInstructionsModal.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import Modal from "main/home/modals/Modal"; -import React, { useContext } from "react"; -import { Context } from "shared/Context"; -import { ChartType } from "shared/types"; -import styled from "styled-components"; - -const ConnectToJobInstructionsModal: React.FC<{ - show: boolean; - onClose: () => void; - chartName: string; -}> = ({ show, chartName, onClose }) => { - const { currentCluster, currentProject } = useContext(Context); - if (!show) { - return null; - } - - return ( - onClose()} - width="700px" - height="300px" - title="Shell Access Instructions" - > - To get shell access to this job run, make sure you have the Porter CLI - installed (installation instructions  - - here - - ). -
-
- Run the following commands to set your current project and cluster - - porter config set-project {currentProject.id} -
- porter config set-cluster {currentCluster.id} -
-
- Run the following line of code, and make sure to change the command to - something your container can run: - porter run {chartName || "[APP-NAME]"} -- [COMMAND] - Note that this will create a copy of the most recent job run for this - template. -
- ); -}; - -export default ConnectToJobInstructionsModal; - -const Code = styled.div` - background: #181b21; - padding: 10px 15px; - border: 1px solid #ffffff44; - border-radius: 5px; - margin: 10px 0px 15px; - color: #ffffff; - font-size: 13px; - user-select: text; - line-height: 1em; - font-family: monospace; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx deleted file mode 100644 index bd502e2a72..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx +++ /dev/null @@ -1,555 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import { get, isEmpty } from "lodash"; -import styled from "styled-components"; - -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 "../DeploymentType"; -import JobMetricsSection from "../metrics/JobMetricsSection"; -import Logs from "../status/Logs"; -import { useRouting } from "shared/routing"; -import LogsSection, { InitLogData } from "../logs-section/LogsSection"; -import EventsTab from "../events/EventsTab"; -import { getPodStatus } from "../deploy-status-section/util"; -import { capitalize } from "shared/string_utils"; -import { usePods } from "shared/hooks/usePods"; - -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" | "metrics" | "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={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: "Metrics", - value: "metrics", - }, - { - label: "Config", - value: "config", - } - ); - - return ( - - - - - Back - - - - - {chart.name} at {readableDate(run.status.startTime)} - - - - {renderStatus( - false, - run, - pods[0], - run.status.completionTime - ? readableDate(run.status.completionTime) - : "" - )} - - Namespace {currentProject?.capi_provisioner_enabled && chart.namespace.startsWith("porter-stack-") ? chart.namespace.replace("porter-stack-", "") : chart.namespace} - - - - - - - { - setCurrentTab(newTab); - }} - options={options} - > - {currentTab === "events" && renderEventsSection()} - {currentTab === "logs" && renderLogsSection()} - {currentTab === "config" && <>{renderConfigSection(run)}} - {currentTab === "metrics" && ( - - )} - - - - ); -}; - -export default ExpandedJobRun; - -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; - margin: 24px 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: 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/cluster-dashboard/expanded-chart/jobs/JobList.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx deleted file mode 100644 index a5f9db3b6f..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import React, { useContext, useState } from "react"; -import styled from "styled-components"; - -import api from "shared/api"; -import { Context } from "shared/Context"; -import JobResource from "./JobResource"; -import useAuth from "shared/auth/useAuth"; -import usePagination from "shared/hooks/usePagination"; -import Placeholder from "components/Placeholder"; - -type PropsType = { - jobs: any[]; - setJobs: (job: any) => void; - expandJob: any; - currentChartVersion: number; - latestChartVersion: number; - isDeployedFromGithub: boolean; - repositoryUrl?: string; -}; - -const JobListFC = (props: PropsType): JSX.Element => { - const [isAuthorized] = useAuth(); - const { - currentCluster, - currentProject, - setCurrentOverlay, - setCurrentError, - } = useContext(Context); - const [deletionJob, setDeletionJob] = useState(null); - - const { - firstContentIndex, - lastContentIndex, - nextPage, - page, - prevPage, - totalPages, - canNextPage, - canPreviousPage, - } = usePagination({ - count: props.jobs?.length, - initialPageSize: 30, - }); - - const deleteJob = (job: any) => { - setCurrentOverlay(null); - api - .deleteJob( - "", - {}, - { - id: currentProject.id, - name: job.metadata?.name, - cluster_id: currentCluster.id, - namespace: job.metadata?.namespace, - } - ) - .then((res) => { - setDeletionJob(job); - }) - .catch((err) => { - let parsedErr = err?.response?.data?.error; - if (parsedErr) { - err = parsedErr; - } - setCurrentError(err); - }) - .finally(() => { - setCurrentOverlay(null); - }); - }; - - if (!props.jobs?.length) { - return ( - - - category - There are no jobs currently running. - - - ); - } - - return ( - <> - - {props.jobs - .slice(firstContentIndex, lastContentIndex) - .map((job: any, i: number) => { - return ( - { - setCurrentOverlay({ - message: "Are you sure you want to delete this job run?", - onYes: () => deleteJob(job), - onNo: () => setCurrentOverlay(null), - }); - }} - deleting={deletionJob?.metadata?.name == job.metadata?.name} - readOnly={!isAuthorized("job", "", ["get", "update", "delete"])} - isDeployedFromGithub={props.isDeployedFromGithub} - repositoryUrl={props.repositoryUrl} - currentChartVersion={props.currentChartVersion} - latestChartVersion={props.latestChartVersion} - /> - ); - })} - - - {/* Disable the page count selector until find a fix for their styles */} - {/* - Page size: - setPageSize(Number(val))} - width="70px" - > - */} - - - {"<"} - - - Page {page} of {totalPages} - - - {">"} - - - - - ); -}; - -export default JobListFC; - -const FlexEnd = styled.div` - display: flex; - justify-content: flex-end; - align-items: center; - width: 100%; -`; - -const PaginationActionsWrapper = styled.div``; - -const PageCountWrapper = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - min-width: 160px; - margin-right: 10px; -`; - -const PaginationAction = styled.button` - border: none; - background: unset; - color: white; - padding: 10px; - cursor: pointer; - border-radius: 5px; - :hover { - background: #ffffff40; - } - - :disabled { - color: #ffffff88; - cursor: unset; - :hover { - background: unset; - } - } -`; - -const PageCounter = styled.span` - margin: 0 5px; -`; - -const JobListWrapper = styled.div` - width: 100%; - height: calc(100% - 65px); - position: relative; - font-size: 13px; - padding: 0px; - user-select: text; - border-radius: 5px; - overflow-y: auto; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx deleted file mode 100644 index a3d972ed3d..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx +++ /dev/null @@ -1,342 +0,0 @@ -import React, { MouseEvent, useContext, useState } from "react"; -import styled from "styled-components"; -import { Context } from "shared/Context"; -import _ from "lodash"; - -import api from "shared/api"; -import DynamicLink from "components/DynamicLink"; -import { readableDate } from "shared/string_utils"; -import { isRunning, renderStatus } from "./ExpandedJobRun"; -import { usePods } from "shared/hooks/usePods"; - -type Props = { - job: any; - handleDelete: () => void; - deleting: boolean; - readOnly?: boolean; - expandJob: any; - currentChartVersion: number; - latestChartVersion: number; - isDeployedFromGithub: boolean; - repositoryUrl?: string; -}; - -const JobResource: React.FC = (props) => { - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - - const [showConnectionModal, setShowConnectionModal] = useState(false); - - const [pods, isLoading] = usePods({ - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: props.job.metadata?.namespace, - selectors: [`job-name=${props.job.metadata?.name}`], - controller_kind: "job", - controller_name: props.job.metadata?.name, - subscribed: props.job?.status.active, - }); - - const stopJob = (event: MouseEvent) => { - if (event) { - event.stopPropagation(); - } - - api - .stopJob( - "", - {}, - { - id: currentProject.id, - name: props.job.metadata?.name, - namespace: props.job.metadata?.namespace, - cluster_id: currentCluster.id, - } - ) - .then(() => {}) - .catch((err) => { - let parsedErr = err?.response?.data?.error; - if (parsedErr) { - err = parsedErr; - } - setCurrentError(err); - }); - }; - - const getCompletedReason = () => { - let completeCondition: any; - - // get the completed reason from the status - props.job.status?.conditions?.forEach((condition: any) => { - if (condition.type == "Complete") { - completeCondition = condition; - } - }); - - if (!completeCondition) { - // otherwise look for a failed reason - props.job.status?.conditions?.forEach((condition: any) => { - if (condition.type == "Failed") { - completeCondition = condition; - } - }); - } - - // if still no complete condition, return unknown - if (!completeCondition) { - return "Succeeded"; - } - - return ( - completeCondition?.reason || - `Completed at ${readableDate(completeCondition?.lastTransitionTime)}` - ); - }; - - const getFailedReason = () => { - let failedCondition: any; - - // get the completed reason from the status - props.job.status?.conditions?.forEach((condition: any) => { - if (condition.type == "Failed") { - failedCondition = condition; - } - }); - - return failedCondition - ? `Failed at ${readableDate(failedCondition.lastTransitionTime)}` - : "Failed"; - }; - - const getSubtitle = () => { - if (props.job.status?.succeeded >= 1) { - return getCompletedReason(); - } - - if (props.job.status?.failed >= 1) { - return getFailedReason(); - } - - return "Running"; - }; - - const renderStopButton = () => { - if (props.readOnly) { - return null; - } - - if (isRunning(props.deleting, props.job, pods[0])) { - return ( - - stop - - ); - } - - return null; - }; - - const getImageTag = () => { - const container = props.job?.spec?.template?.spec?.containers[0]; - const tag = container?.image?.split(":")[1]; - - if (!tag) { - return "unknown"; - } - - if (props.isDeployedFromGithub && tag !== "latest") { - return ( - e.preventDefault()} - target="_blank" - > - {tag} - - ); - } - - return tag; - }; - - const getRevisionNumber = () => { - const revision = props.job?.metadata?.labels["helm.sh/revision"]; - let status: RevisionContainerProps["status"] = "current"; - if (props.currentChartVersion > revision) { - status = "outdated"; - } - return ( - - Revision No - {revision || "unknown"} - - ); - }; - - const icon = - "https://user-images.githubusercontent.com/65516095/111258413-4e2c3800-85f3-11eb-8a6a-88e03460f8fe.png"; - const commandString = props.job?.spec?.template?.spec?.containers[0]?.command?.join( - " " - ); - - return ( - <> - - props.expandJob(props.job)}> - - - - - {getSubtitle()} - - - - - {getRevisionNumber()} - {commandString} - - - {renderStatus(props.deleting, props.job, pods[0])} - - {renderStopButton()} - {!props.readOnly && ( - { - e.stopPropagation(); - props.handleDelete(); - }} - > - delete - - )} - - - - - - ); -}; - -export default JobResource; - -type RevisionContainerProps = { - status: "outdated" | "current"; -}; - -const RevisionContainer = styled.span` - margin-right: 15px; - ${({ status }) => { - if (status === "outdated") { - return "color: rgb(245, 203, 66);"; - } - return ""; - }} -`; - -const Dot = styled.div` - margin-right: 9px; - margin-left: 9px; - color: #ffffff88; -`; - -const CommandString = styled.div` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 200px; - color: #ffffff55; - margin-right: 27px; - font-family: monospace; -`; - -const EndWrapper = styled.div` - display: flex; - align-items: center; -`; - -const Icon = styled.img` - width: 30px; - margin-right: 18px; -`; - -const Flex = styled.div` - display: flex; - align-items: center; - justify-content: center; -`; - -const StyledJob = styled.div` - display: flex; - flex-direction: column; - margin-bottom: 20px; - overflow: hidden; - border-radius: 5px; - background: ${props => props.theme.clickable.bg}; - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - } -`; - -const MainRow = styled.div` - height: 70px; - width: 100%; - display: flex; - cursor: pointer; - align-items: center; - justify-content: space-between; - padding: 25px; - padding-right: 18px; - border-radius: 5px; -`; - -const MaterialIconTray = styled.div` - user-select: none; - display: flex; - align-items: center; - justify-content: space-between; - > i { - border-radius: 20px; - font-size: 18px; - padding: 5px; - margin: 0 5px; - color: #ffffff44; - :hover { - background: ${(props: { disabled: boolean }) => - props.disabled ? "" : "#ffffff11"}; - } - } -`; - -const Description = styled.div` - display: flex; - flex-direction: column; - margin: 0; - padding: 0; -`; - -const Label = styled.div` - color: #ffffff; - font-size: 13px; - font-weight: 500; - display: flex; - > span { - color: #ffffff88; - } -`; - -const Subtitle = styled.div` - color: #aaaabb; - font-size: 13px; - display: flex; - align-items: center; - padding-top: 5px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/useJobs.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/useJobs.ts deleted file mode 100644 index 82f303517f..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/useJobs.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { set } from "lodash"; -import { useContext, useEffect, useRef, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets"; -import { ChartType, ChartTypeWithExtendedConfig } from "shared/types"; -import yaml from "js-yaml"; -import { usePrevious } from "shared/hooks/usePrevious"; -import { useRouting } from "shared/routing"; -import { PORTER_IMAGE_TEMPLATES } from "shared/common"; - -export const useJobs = (chart: ChartType) => { - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - const [jobs, setJobs] = useState([]); - const jobsRef = useRef([]); - const lastStreamStatus = useRef(""); - const [hasError, setHasError] = useState(false); - const [hasPorterImageTemplate, setHasPorterImageTemplate] = useState(true); - const [selectedJob, setSelectedJob] = useState(null); - const [status, setStatus] = useState<"loading" | "ready">("loading"); - const [triggerRunStatus, setTriggerRunStatus] = useState< - "loading" | "successful" | string - >(""); - - const previousChart = usePrevious(chart, null); - - const { pushQueryParams, getQueryParam } = useRouting(); - - const { - newWebsocket, - openWebsocket, - closeAllWebsockets, - closeWebsocket, - } = useWebsockets(); - - const isBeingDeployed = (latestJob: any) => { - const currentChart: ChartTypeWithExtendedConfig = chart; - const chartImage = currentChart.config.image.repository; - - let latestImageDetected = - latestJob?.spec?.template?.spec?.containers[0]?.image; - - if (!PORTER_IMAGE_TEMPLATES.includes(chartImage)) { - return false; - } - - if ( - latestImageDetected && - !PORTER_IMAGE_TEMPLATES.includes(latestImageDetected) - ) { - return false; - } - - return true; - }; - - const sortJobsAndSave = (newJobs: any[]) => { - // Set job run from URL if needed - const urlParams = new URLSearchParams(location.search); - - const getTime = (job: any) => { - return new Date(job?.status?.startTime).getTime(); - }; - - newJobs.sort((job1, job2) => getTime(job2) - getTime(job1)); - - if (!isBeingDeployed(newJobs[0])) { - setHasPorterImageTemplate(false); - } - jobsRef.current = newJobs; - setJobs(newJobs); - }; - - const addJob = (newJob: any) => { - let newJobs = [...jobsRef.current]; - const existingJobIndex = newJobs.findIndex((currentJob) => { - return ( - currentJob.metadata?.name === newJob.metadata?.name && - currentJob.metadata?.namespace === newJob.metadata?.namespace - ); - }); - - if (existingJobIndex > -1) { - return; - } - - newJobs.push(newJob); - sortJobsAndSave(newJobs); - }; - - const mergeNewJob = (newJob: any) => { - let newJobs = [...jobsRef.current]; - const existingJobIndex = newJobs.findIndex((currentJob) => { - return ( - currentJob.metadata?.name === newJob.metadata?.name && - currentJob.metadata?.namespace === newJob.metadata?.namespace - ); - }); - - if (existingJobIndex > -1) { - newJobs.splice(existingJobIndex, 1, newJob); - } else { - newJobs.push(newJob); - } - sortJobsAndSave(newJobs); - }; - - const removeJob = (deletedJob: any) => { - let newJobs = jobsRef.current.filter((job: any) => { - return deletedJob.metadata?.name !== job.metadata?.name; - }); - - sortJobsAndSave([...newJobs]); - }; - - const setupCronJobWebsocket = () => { - const releaseName = chart.name; - const releaseNamespace = chart.namespace; - if (!releaseName || !releaseNamespace) { - return; - } - - const websocketId = `cronjob-websocket-${releaseName}`; - - const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/cronjob/status`; - - const config: NewWebsocketOptions = { - onopen: console.log, - onmessage: (evt: MessageEvent) => { - const event = JSON.parse(evt.data); - const object = event.Object; - object.metadata.kind = event.Kind; - - setHasPorterImageTemplate((prevValue) => { - // if imageIsPlaceholder is true update the newestImage and imageIsPlaceholder fields - - if (event.event_type !== "ADD" && event.event_type !== "UPDATE") { - return prevValue; - } - - if (!hasPorterImageTemplate) { - return prevValue; - } - - if (!event.Object?.metadata?.annotations) { - return prevValue; - } - - // filter job belonging to chart - const relNameAnnotation = - event.Object?.metadata?.annotations["meta.helm.sh/release-name"]; - const relNamespaceAnnotation = - event.Object?.metadata?.annotations[ - "meta.helm.sh/release-namespace" - ]; - - if ( - releaseName !== relNameAnnotation || - releaseNamespace !== relNamespaceAnnotation - ) { - return prevValue; - } - - const newestImage = - event.Object?.spec?.jobTemplate?.spec?.template?.spec?.containers[0] - ?.image; - - if (!PORTER_IMAGE_TEMPLATES.includes(newestImage)) { - return false; - } - - return true; - }); - }, - onclose: console.log, - onerror: (err: ErrorEvent) => { - console.log(err); - closeWebsocket(websocketId); - }, - }; - - newWebsocket(websocketId, endpoint, config); - openWebsocket(websocketId); - }; - - const setupJobWebsocket = () => { - const chartVersion = `${chart?.chart?.metadata?.name}-${chart?.chart?.metadata?.version}`; - - const websocketId = `job-websocket-${chart.name}`; - - const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/job/status`; - - const config: NewWebsocketOptions = { - onopen: console.log, - onmessage: (evt: MessageEvent) => { - const event = JSON.parse(evt.data); - - const chartLabel = event.Object?.metadata?.labels["helm.sh/chart"]; - const releaseLabel = - event.Object?.metadata?.labels["meta.helm.sh/release-name"]; - const namespace = event.Object?.metadata?.namespace; - - if ( - chartLabel !== chartVersion || - releaseLabel !== chart.name || - namespace !== chart.namespace - ) { - return; - } - - if (event.event_type === "ADD") { - addJob(event.Object); - return; - } - - // if event type is add or update, merge with existing jobs - if (event.event_type === "UPDATE") { - mergeNewJob(event.Object); - return; - } - - if (event.event_type === "DELETE") { - // filter job belonging to chart - removeJob(event.Object); - } - }, - onclose: console.log, - onerror: (err: ErrorEvent) => { - console.log(err); - closeWebsocket(websocketId); - }, - }; - newWebsocket(websocketId, endpoint, config); - openWebsocket(websocketId); - }; - - const loadJobFromurl = () => { - const jobName = getQueryParam("job"); - - const job: any = jobs.find((tmpJob) => tmpJob.metadata.name === jobName); - - if (!job) { - return; - } - - setSelectedJob(job); - }; - - useEffect(() => { - if (!chart || !chart.namespace || !chart.name) { - return () => {}; - } - - if ( - previousChart?.name === chart?.name && - previousChart?.namespace === chart?.namespace - ) { - return () => {}; - } - - setStatus("loading"); - const newestImage = chart?.config?.image?.repository; - - setHasPorterImageTemplate(PORTER_IMAGE_TEMPLATES.includes(newestImage)); - - const namespace = chart.namespace; - const release_name = chart.name; - - closeAllWebsockets(); - jobsRef.current = []; - lastStreamStatus.current = ""; - setJobs([]); - - const websocketId = `job-runs-websocket-${release_name}-${namespace}`; - - const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/jobs/stream?name=${release_name}`; - - const config: NewWebsocketOptions = { - onopen: console.log, - onmessage: (message) => { - const data = JSON.parse(message.data); - - if (data.streamStatus === "finished") { - setHasError(false); - setStatus("ready"); - sortJobsAndSave(jobsRef.current); - lastStreamStatus.current = data.streamStatus; - setupJobWebsocket(); - setupCronJobWebsocket(); - return; - } - - if (data.streamStatus === "errored") { - setHasError(true); - jobsRef.current = []; - setJobs([]); - setStatus("ready"); - return; - } - - jobsRef.current = [...jobsRef.current, data]; - }, - onclose: (event) => { - // console.log(event); - closeWebsocket(websocketId); - }, - onerror: (error) => { - setHasError(true); - setStatus("ready"); - console.log(error); - closeWebsocket(websocketId); - }, - }; - newWebsocket(websocketId, endpoint, config); - openWebsocket(websocketId); - }, [chart]); - - useEffect(() => { - if (!jobs.length) { - return; - } - - loadJobFromurl(); - }, [jobs]); - - useEffect(() => { - return () => { - closeAllWebsockets(); - }; - }, []); - - const runJob = () => { - setTriggerRunStatus("loading"); - const config = chart.config; - const values = {}; - - for (let key in config) { - set(values, key, config[key]); - } - - set(values, "paused", false); - - const yamlValues = yaml.dump( - { - ...values, - }, - { forceQuotes: true } - ); - - api - .upgradeChartValues( - "", - { - values: yamlValues, - latest_revision: chart.version, - }, - { - id: currentProject.id, - name: chart.name, - namespace: chart.namespace, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - setTriggerRunStatus("successful"); - setTimeout(() => setTriggerRunStatus(""), 500); - }) - .catch((err) => { - let parsedErr = err?.response?.data?.error; - - if (parsedErr) { - err = parsedErr; - } - - setTriggerRunStatus("Couldn't trigger a new run for this job."); - setTimeout(() => setTriggerRunStatus(""), 500); - setCurrentError(parsedErr); - }); - }; - - const handleSetSelectedJob = (job: any) => { - setSelectedJob(job); - pushQueryParams({ job: job?.metadata?.name }); - }; - - return { - jobs, - hasPorterImageTemplate, - status, - triggerRunStatus, - runJob, - selectedJob, - setSelectedJob: handleSetSelectedJob, - }; -}; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx deleted file mode 100644 index c08f68b35d..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx +++ /dev/null @@ -1,662 +0,0 @@ -import React, { - useCallback, - useContext, - useEffect, - useRef, - useState, -} from "react"; - -import styled from "styled-components"; -import RadioFilter from "components/RadioFilter"; - -import filterOutline from "assets/filter-outline.svg"; -import time from "assets/time.svg"; -import { Context } from "shared/Context"; -import api from "shared/api"; -import { Direction, useLogs } from "./useAgentLogs"; -import type Anser from "anser"; -import DateTimePicker from "components/date-time-picker/DateTimePicker"; -import dayjs from "dayjs"; -import Loading from "components/Loading"; -import _ from "lodash"; -import { type ChartType } from "shared/types"; -import Banner from "components/porter/Banner"; - -export type InitLogData = Partial<{ - podName: string; - timestamp: string; - revision: string; -}>; - -type Props = { - currentChart?: ChartType; - isFullscreen: boolean; - setIsFullscreen: (x: boolean) => void; - initData?: InitLogData; - setInitData?: (initData: InitLogData) => void; - overridingPodSelector?: string; -}; - -const escapeExp = (str: string) => { - // regex special character need to be escaped twice - const regEscaped = str.replace(/[.*+?^${}()|[\]\\]/g, "\\\\$&"); - // double quotes need to be escaped once - const quoteEscaped = regEscaped.replace(/["]/g, "\\$&"); - return quoteEscaped; -}; - -type QueryModeSelectionToggleProps = { - selectedDate?: Date; - setSelectedDate: React.Dispatch>; - resetSearch: () => void; -} - -const QueryModeSelectionToggle = (props: QueryModeSelectionToggleProps) => { - return ( -
- - { - props.setSelectedDate(undefined); - props.resetSearch(); - }} - selected={!props.selectedDate} - > - - Live - - { props.setSelectedDate(dayjs().toDate()); }} - selected={!!props.selectedDate} - > - - {props.selectedDate && ( - - )} - - -
- ); -}; - -const Dot = styled.div<{ selected?: boolean }>` - display: inline-black; - width: 8px; - height: 8px; - margin-right: 9px; - border-radius: 20px; - background: ${(props) => (props.selected ? "#ed5f85" : "#ffffff22")}; - border: 0px; - outline: none; - box-shadow: ${(props) => (props.selected ? "0px 0px 5px 1px #ed5f85" : "")}; -`; - -const LogsSection: React.FC = ({ - currentChart, - isFullscreen, - setIsFullscreen, - initData = {}, - setInitData, - overridingPodSelector, -}) => { - const scrollToBottomRef = useRef(undefined); - const { currentProject, currentCluster } = useContext(Context); - const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true); - const [searchText, setSearchText] = useState(""); - const [enteredSearchText, setEnteredSearchText] = useState(""); - const [selectedDate, setSelectedDate] = useState( - initData.timestamp ? dayjs(initData.timestamp).toDate() : undefined - ); - const [notification, setNotification] = useState(); - const [loading, setLoading] = useState(true); - - const notify = (message: string) => { - setNotification(message); - - setTimeout(() => { - setNotification(undefined); - }, 5000); - }; - - const { logs, refresh, moveCursor, paginationInfo } = useLogs( - overridingPodSelector ?? "", - "", - enteredSearchText, - notify, - currentChart, - setLoading, - selectedDate - ); - - useEffect(() => { - if (!loading && scrollToBottomRef.current && scrollToBottomEnabled) { - scrollToBottomRef.current.scrollIntoView({ - behavior: "smooth", - block: "end", - }); - } - }, [loading, logs, scrollToBottomRef, scrollToBottomEnabled]); - - useEffect(() => { - if (initData.timestamp) { - setSelectedDate(dayjs(initData.timestamp).toDate()); - } - }, [initData]); - - const renderLogs = () => { - return 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")} - - ); - })} - - - ); - }); - }; - - const onLoadPrevious = useCallback(() => { - if (!selectedDate) { - setSelectedDate(dayjs(logs[0].timestamp).toDate()); - return; - } - - moveCursor(Direction.backward); - }, [logs, selectedDate]); - - const resetSearch = () => { - setSearchText(""); - setEnteredSearchText(""); - }; - - const renderContents = () => { - return ( - <> - - - - - search - { - setSearchText(e.target.value); - }} - onKeyPress={(event) => { - if (event.key === "Enter") { - setEnteredSearchText(escapeExp(searchText)); - if (selectedDate == null) { - setSelectedDate(dayjs().toDate()); - } - } - }} - placeholder="Search logs..." - /> - - - - - - - - - {!isFullscreen && ( - <> - - { setIsFullscreen(true); }}> - open_in_full - - - )} - - - - - {loading || (logs.length == 0 && selectedDate == null) ? ( - - ) : logs.length == 0 ? ( - <> - - No logs found. - - autorenew - Refresh - - - - ) : ( - <> - - Load Previous - - {renderLogs()} - { await moveCursor(Direction.forward); }} - > - Load more - - - )} -
- - - {notification} - - - - ); - }; - - return ( - <> - {isFullscreen ? ( - - - { setIsFullscreen(false); }}> - navigate_before - - Logs ({currentChart.name}) - - {renderContents()} - - ) : ( - <>{renderContents()} - )} - - ); -}; - -export default LogsSection; - -const BackButton = styled.div` - display: flex; - width: 30px; - z-index: 2; - 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 Fullscreen = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - padding-top: 60px; -`; - -const Icon = styled.div` - background: #26292e; - border-radius: 5px; - height: 30px; - width: 30px; - display: flex; - cursor: pointer; - align-items: center; - justify-content: center; - > i { - font-size: 14px; - } - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - } -`; - -const Checkbox = styled.div<{ checked: boolean }>` - width: 16px; - height: 16px; - border: 1px solid #ffffff55; - margin: 1px 10px 0px 1px; - border-radius: 3px; - background: ${(props) => (props.checked ? "#ffffff22" : "#ffffff11")}; - display: flex; - align-items: center; - justify-content: center; - - > i { - font-size: 12px; - padding-left: 0px; - display: ${(props) => (props.checked ? "" : "none")}; - } -`; - -const Spacer = styled.div<{ width?: string }>` - height: 100%; - width: ${(props) => props.width || "10px"}; -`; - -const Button = styled.div` - background: #26292e; - border-radius: 5px; - height: 30px; - font-size: 13px; - display: flex; - cursor: pointer; - align-items: center; - padding: 10px; - padding-left: 8px; - > i { - font-size: 16px; - margin-right: 5px; - } - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - } -`; - -const Flex = styled.div` - display: flex; - align-items: center; - border-bottom: 25px solid transparent; -`; - -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 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 FlexRow = styled.div<{ isFullscreen?: boolean }>` - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - margin-top: ${(props) => (props.isFullscreen ? "10px" : "")}; - padding: ${(props) => (props.isFullscreen ? "0 20px" : "")}; -`; - -const SearchBarWrapper = styled.div` - display: flex; - flex: 1; - - > i { - color: #aaaabb; - padding-top: 1px; - margin-left: 8px; - font-size: 16px; - margin-right: 8px; - } -`; - -const SearchInput = styled.input` - outline: none; - border: none; - font-size: 13px; - background: none; - width: 100%; - color: white; - height: 100%; -`; - -const SearchRow = styled.div` - display: flex; - align-items: center; - height: 30px; - margin-right: 10px; - background: #26292e; - border-radius: 5px; - border: 1px solid #aaaabb33; -`; - -const SearchRowWrapper = styled(SearchRow)` - border-radius: 5px; - width: 250px; -`; - -const StyledLogsSection = styled.div<{ isFullscreen: boolean }>` - width: 100%; - min-height: 400px; - height: ${(props) => - props.isFullscreen ? "calc(100vh - 125px)" : "calc(100vh - 460px)"}; - display: flex; - flex-direction: column; - position: relative; - font-size: 13px; - border-radius: ${(props) => (props.isFullscreen ? "" : "8px")}; - border: ${(props) => (props.isFullscreen ? "" : "1px solid #ffffff33")}; - border-top: ${(props) => (props.isFullscreen ? "1px solid #ffffff33" : "")}; - background: #101420; - 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 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"}; -`; - -const LoadMoreButton = styled.div<{ active: boolean }>` - width: 100%; - display: ${(props) => (props.active ? "flex" : "none")}; - justify-content: center; - align-items: center; - padding-block: 10px; - background: #1f2023; - cursor: pointer; - font-family: monospace; -`; - -const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>` - padding: 0 10px; - color: ${(props) => (props.selected ? "" : "#494b4f")}; - border: 1px solid #494b4f; - height: 100%; - display: flex; - margin-left: ${(props) => (props.nudgeLeft ? "-1px" : "")}; - align-items: center; - border-radius: ${(props) => - props.nudgeLeft ? "0 5px 5px 0" : "5px 0 0 5px"}; - :hover { - border: 1px solid #7a7b80; - z-index: 2; - } -`; - -const ToggleButton = styled.div` - background: #26292e; - border-radius: 5px; - font-size: 13px; - height: 30px; - display: flex; - align-items: center; - cursor: pointer; -`; - -const TimeIcon = styled.img<{ selected?: boolean }>` - width: 16px; - height: 16px; - z-index: 2; - opacity: ${(props) => (props.selected ? "" : "50%")}; -`; - -const NotificationWrapper = styled.div<{ active?: boolean }>` - position: absolute; - bottom: 10px; - display: ${(props) => (props.active ? "flex" : "none")}; - justify-content: center; - align-items: center; - left: 50%; - transform: translateX(-50%); - width: fit-content; - background: #101420; - z-index: 9999; - - @keyframes bounceIn { - 0% { - transform: translateZ(-1400px); - opacity: 0; - } - 100% { - transform: translateZ(0); - opacity: 1; - } - } -`; - -const LogsSectionWrapper = styled.div` - position: relative; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts deleted file mode 100644 index 306aa14572..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts +++ /dev/null @@ -1,436 +0,0 @@ -import Anser, { AnserJsonEntry } from "anser"; -import dayjs from "dayjs"; -import _ from "lodash"; -import { z } from "zod"; -import { useContext, useEffect, useRef, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets"; -import { ChartType } from "shared/types"; -import { isJSON } from "shared/util"; - -const MAX_LOGS = 5000; -const MAX_BUFFER_LOGS = 1000; -const QUERY_LIMIT = 1000; - -export enum Direction { - forward = "forward", - backward = "backward", -} - -export interface Log { - line: AnserJsonEntry[]; - lineNumber: number; - timestamp?: string; -} - -const LogSchema = z.object({ - log: z.string(), - stream: z.string(), - time: z.string(), -}); - -type LogLine = z.infer; - -export const parseLogs = (logs: string[] = []): Log[] => { - return logs.filter(Boolean).map((logLine: string, idx) => { - try { - if (!isJSON(logLine)) { - return { - line: Anser.ansiToJson(logLine), - lineNumber: idx + 1, - timestamp: undefined, - }; - } - - const parsedLine: LogLine = JSON.parse(logLine); - - LogSchema.parse(parsedLine); - - // TODO Move log parsing to the render method - const ansiLog = Anser.ansiToJson(parsedLine.log); - return { - line: ansiLog, - lineNumber: idx + 1, - timestamp: parsedLine.time, - }; - } catch (err) { - console.error(err, logLine); - return { - line: Anser.ansiToJson(logLine), - lineNumber: idx + 1, - timestamp: undefined, - }; - } - }); -}; - -interface PaginationInfo { - previousCursor: string | null; - nextCursor: string | null; -} - -export const useLogs = ( - currentPod: string, - namespace: string, - searchParam: string, - notify: (message: string) => void, - currentChart: ChartType, - setLoading: (loading: boolean) => void, - // if setDate is set, results are not live - setDate?: Date -) => { - const isLive = !setDate; - const logsBufferRef = useRef([]); - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - const [logs, setLogs] = useState([]); - const [paginationInfo, setPaginationInfo] = useState({ - previousCursor: null, - nextCursor: null, - }); - - if (currentPod === "") { - currentPod = currentChart.name; - } - namespace = currentChart.namespace; - - // if we are live: - // - start date is initially set to 2 weeks ago - // - the query has an end date set to current date - // - moving the cursor forward does nothing - - // if we are not live: - // - end date is set to the setDate - // - start date is initially set to 2 weeks ago, but then gets set to the - // result of the initial query - // - moving the cursor both forward and backward changes the start and end dates - - const { - newWebsocket, - openWebsocket, - closeWebsocket, - closeAllWebsockets, - } = useWebsockets(); - - const updateLogs = ( - newLogs: Log[], - direction: Direction = Direction.forward - ) => { - // Nothing to update here - if (!newLogs.length) { - return; - } - - setLogs((logs) => { - let updatedLogs = _.cloneDeep(logs); - - /** - * If direction = Direction.forward, we want to append the new logs - * at the end of the current logs, else we want to append before the current logs - * - */ - if (direction === Direction.forward) { - const lastLineNumber = updatedLogs.at(-1)?.lineNumber ?? 0; - - updatedLogs.push( - ...newLogs.map((log, idx) => ({ - ...log, - lineNumber: lastLineNumber + idx + 1, - })) - ); - - // For direction = Direction.forward, remove logs from the front - if (updatedLogs.length > MAX_LOGS) { - const logsToBeRemoved = - newLogs.length < MAX_BUFFER_LOGS ? newLogs.length : MAX_BUFFER_LOGS; - updatedLogs = updatedLogs.slice(logsToBeRemoved); - } - } else { - updatedLogs = newLogs.concat( - updatedLogs.map((log) => ({ - ...log, - lineNumber: log.lineNumber + newLogs.length, - })) - ); - - // For direction = Direction.backward, remove logs from the back - if (updatedLogs.length > MAX_LOGS) { - const logsToBeRemoved = - newLogs.length < MAX_BUFFER_LOGS ? newLogs.length : MAX_BUFFER_LOGS; - - updatedLogs = updatedLogs.slice(0, logsToBeRemoved); - } - } - - return updatedLogs; - }); - }; - - /** - * Flushes the logs buffer. If `discard` is true, - * it will update `current logs` before executing - * the flush operation - */ - const flushLogsBuffer = (discard: boolean = false) => { - if (!discard) { - updateLogs(logsBufferRef.current ?? []); - } - - logsBufferRef.current = []; - }; - - const pushLogs = (newLogs: Log[]) => { - logsBufferRef.current.push(...newLogs); - - if (logsBufferRef.current.length >= MAX_BUFFER_LOGS) { - flushLogsBuffer(); - } - }; - - const setupWebsocket = (websocketKey: string) => { - if (namespace == "") { - return; - } - - const websocketBaseURL = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/logs/loki`; - - const q = new URLSearchParams({ - pod_selector: currentPod + "-.*", - namespace: namespace, - search_param: searchParam, - revision: currentChart.version.toString(), - }).toString(); - - const endpoint = `${websocketBaseURL}?${q}`; - - const config: NewWebsocketOptions = { - onopen: () => { - console.log("Opened websocket:", websocketKey); - }, - onmessage: (evt: MessageEvent) => { - // Nothing to do here - if (!evt?.data || typeof evt.data !== "string") { - return; - } - - const newLogs = parseLogs( - evt?.data?.split("}\n").map((line: string) => line + "}") - ); - - pushLogs(newLogs); - }, - onclose: () => { - console.log("Closed websocket:", websocketKey); - }, - }; - - newWebsocket(websocketKey, endpoint, config); - openWebsocket(websocketKey); - }; - - const queryLogs = ( - startDate: string, - endDate: string, - direction: Direction, - limit: number = QUERY_LIMIT - ): Promise<{ - logs: Log[]; - previousCursor: string | null; - nextCursor: string | null; - }> => { - return api - .getLogs( - "", - { - pod_selector: currentPod + "-.*", - namespace: namespace, - revision: currentChart.version.toString(), - search_param: searchParam, - start_range: startDate, - end_range: endDate, - limit, - direction, - }, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - } - ) - .then((res) => { - const newLogs = parseLogs( - res.data.logs?.filter(Boolean).map((logLine: any) => logLine.line) - ); - - if (direction === Direction.backward) { - newLogs.reverse(); - } - - return { - logs: newLogs, - previousCursor: - // There are no more historical logs so don't set the previous cursor - newLogs.length < QUERY_LIMIT && direction == Direction.backward - ? null - : res.data.backward_continue_time, - nextCursor: res.data.forward_continue_time, - }; - }) - .catch((err) => { - setCurrentError(err); - - return { - logs: [], - previousCursor: null, - nextCursor: null, - }; - }); - }; - - const refresh = async () => { - if (!currentPod) { - return; - } - - setLoading(true); - setLogs([]); - flushLogsBuffer(true); - const websocketKey = `${currentPod}-${namespace}-websocket`; - const endDate = dayjs(setDate); - const sixHoursAgo = endDate.subtract(6, "hour"); - - const { logs: initialLogs, previousCursor, nextCursor } = await queryLogs( - sixHoursAgo.toISOString(), - endDate.toISOString(), - Direction.backward - ); - - setPaginationInfo({ - previousCursor, - nextCursor, - }); - - updateLogs(initialLogs); - - if (!isLive && !initialLogs.length) { - notify( - "You have no logs for this time period. Try with a different time range." - ); - } - - closeWebsocket(websocketKey); - - setLoading(false); - - if (isLive) { - setupWebsocket(websocketKey); - } - - return () => isLive && closeWebsocket(websocketKey); - }; - - const moveCursor = async (direction: Direction) => { - if (direction === Direction.backward) { - // we query by setting the endDate equal to the previous startDate, and setting the direction - // to "backward" - const refDate = paginationInfo.previousCursor ?? dayjs().toISOString(); - const sixHoursAgo = dayjs(refDate).subtract(6, "hour"); - - const { logs: newLogs, previousCursor } = await queryLogs( - sixHoursAgo.toISOString(), - refDate, - Direction.backward - ); - - const logsToUpdate = paginationInfo.previousCursor - ? newLogs.slice(0, -1) - : newLogs; - - updateLogs(logsToUpdate, direction); - - if (!logsToUpdate.length) { - notify("You have reached the beginning of the logs"); - } - - setPaginationInfo((paginationInfo) => ({ - ...paginationInfo, - previousCursor, - })); - } else { - if (isLive) { - return; - } - - // we query by setting the startDate equal to the previous endDate, setting the endDate equal to the - // current time, and setting the direction to "forward" - const refDate = paginationInfo.nextCursor ?? dayjs(setDate).toISOString(); - const currDate = dayjs(); - - const { logs: newLogs, nextCursor } = await queryLogs( - refDate, - currDate.toISOString(), - Direction.forward - ); - - const logsToUpdate = paginationInfo.nextCursor - ? newLogs.slice(1) - : newLogs; - - // If previously we had next cursor set, it is likely that the log might have a duplicate entry so we ignore the first line - updateLogs(logsToUpdate); - - if (!logsToUpdate.length) { - notify("You are already at the latest logs"); - } - - setPaginationInfo((paginationInfo) => ({ - ...paginationInfo, - nextCursor, - })); - } - }; - - useEffect(() => { - setLogs([]); - flushLogsBuffer(true); - }, []); - - /** - * In some situations, we might never hit the limit for the max buffer size. - * An example is if the total logs for the pod < MAX_BUFFER_LOGS. - * - * For handling situations like this, we would want to force a flush operation - * on the buffer so that we dont have any stale logs - */ - useEffect(() => { - /** - * We don't want users to wait for too long for the initial - * logs to appear. So we use a setTimeout for 1s to force-flush - * logs after 1s of load - */ - setTimeout(flushLogsBuffer, 500); - - const flushLogsBufferInterval = setInterval(flushLogsBuffer, 3000); - - return () => clearInterval(flushLogsBufferInterval); - }, []); - - useEffect(() => { - refresh(); - }, [currentPod, namespace, searchParam, setDate]); - - useEffect(() => { - // if the streaming is no longer live, close all websockets - if (!isLive) { - closeAllWebsockets(); - } - }, [isLive]); - - return { - logs, - refresh, - moveCursor, - paginationInfo, - }; -}; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx deleted file mode 100644 index 7beb343d08..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx +++ /dev/null @@ -1,497 +0,0 @@ -import _ from "lodash"; -import React, { useCallback, useMemo, useRef } from "react"; -import chroma from "chroma-js"; -import * as stats from "simple-statistics"; -import styled from "styled-components"; -import { AreaClosed, Bar, Line, LinePath } from "@visx/shape"; -import { curveMonotoneX } from "@visx/curve"; -import { scaleLinear, scaleTime } from "@visx/scale"; -import { AxisBottom, AxisLeft } from "@visx/axis"; - -import { defaultStyles, TooltipWithBounds, useTooltip } from "@visx/tooltip"; - -import { GridColumns, GridRows } from "@visx/grid"; - -import { localPoint } from "@visx/event"; -import { LinearGradient } from "@visx/gradient"; -import { bisector, extent, max } from "d3-array"; -import { timeFormat } from "d3-time-format"; -import { NormalizedMetricsData } from "./types"; -import { AggregatedDataColors } from "./utils"; - -var globalData: NormalizedMetricsData[]; - -export const background = "#3b697800"; -export const background2 = "#20405100"; -export const accentColor = "#949eff"; -export const accentColorDark = "#949eff"; - -// util -const formatDate = timeFormat("%H:%M:%S %b %d, '%y"); - -const hourFormat = timeFormat("%H:%M"); -const dayFormat = timeFormat("%b %d"); - -// map resolutions to formats -const formats: { [range: string]: (date: Date) => string } = { - "1H": hourFormat, - "6H": hourFormat, - "1D": hourFormat, - "1M": dayFormat, -}; - -// accessors -const getDate = (d: NormalizedMetricsData) => new Date(d.date * 1000); -const getValue = (d: NormalizedMetricsData) => - d?.value && Number(d.value?.toFixed(4)); - -const bisectDate = bisector( - (d) => new Date(d.date * 1000) -).left; - -export type AreaProps = { - data: NormalizedMetricsData[]; - aggregatedData?: Record; - dataKey: string; - hpaEnabled?: boolean; - hpaData?: NormalizedMetricsData[]; - resolution: string; - width: number; - height: number; - margin?: { top: number; right: number; bottom: number; left: number }; -}; - -const AreaChart: React.FunctionComponent = ({ - data, - aggregatedData = {}, - dataKey, - hpaEnabled = false, - hpaData = [], - resolution, - width, - height, - margin = { top: 0, right: 0, bottom: 0, left: 0 }, -}) => { - globalData = data; - - const { - showTooltip, - hideTooltip, - tooltipData, - tooltipTop, - tooltipLeft, - } = useTooltip<{ - data: NormalizedMetricsData; - tooltipHpaData: NormalizedMetricsData; - aggregatedData?: Record; - }>(); - - const svgContainer = useRef(); - // bounds - const innerWidth = width - margin.left - margin.right - 40; - const innerHeight = height - margin.top - margin.bottom - 20; - const isHpaEnabled = hpaEnabled && !!hpaData.length; - - // scales - const dateScale = useMemo( - () => - scaleTime({ - range: [margin.left, innerWidth + margin.left], - domain: extent( - [...globalData, ...(isHpaEnabled ? hpaData : [])], - getDate - ) as [Date, Date], - }), - [margin.left, width, height, data, hpaData, isHpaEnabled] - ); - const valueScale = useMemo( - () => - scaleLinear({ - range: [innerHeight + margin.top, margin.top], - domain: [ - 0, - 1.25 * - max( - [ - ...globalData, - ...Object.values(aggregatedData).flat(), - ...(isHpaEnabled ? hpaData : []), - ], - getValue - ), - ], - nice: true, - }), - [margin.top, width, height, data, hpaData, isHpaEnabled] - ); - - const getAggregatedDataTooltip = (x0: Date) => { - let aggregatedTooltipData: Record = {}; - for (let [key, values] of Object.entries(aggregatedData)) { - const index = bisectDate(values, x0, 1); - const d0 = values[index - 1]; - const d1 = values[index]; - let d = d0; - - if (d1 && getDate(d1)) { - d = - x0.valueOf() - getDate(d0).valueOf() > - getDate(d1).valueOf() - x0.valueOf() - ? d1 - : d0; - } - - aggregatedTooltipData[key] = d; - } - - return aggregatedTooltipData; - }; - - // tooltip handler - const handleTooltip = useCallback( - ( - event: React.TouchEvent | React.MouseEvent - ) => { - const isHpaEnabled = hpaEnabled && !!hpaData.length; - - const { x } = localPoint(event) || { x: 0 }; - const x0 = dateScale.invert(x); - - const index = bisectDate(globalData, x0, 1); - const d0 = globalData[index - 1]; - const d1 = globalData[index]; - let d = d0; - - if (d1 && getDate(d1)) { - d = - x0.valueOf() - getDate(d0).valueOf() > - getDate(d1).valueOf() - x0.valueOf() - ? d1 - : d0; - } - - const hpaIndex = bisectDate(hpaData, x0, 1); - // Get new index without min value to be sure that data exists for HPA - const hpaIndex2 = bisectDate(hpaData, x0); - - if (!isHpaEnabled || hpaIndex !== hpaIndex2) { - showTooltip({ - tooltipData: { - data: d, - tooltipHpaData: undefined, - aggregatedData: getAggregatedDataTooltip(x0), - }, - tooltipLeft: x || 0, - tooltipTop: valueScale(getValue(d)) || 0, - }); - return; - } - - const tooltipHpaData0 = hpaData[hpaIndex - 1]; - const tooltipHpaData1 = hpaData[hpaIndex]; - let tooltipHpaData = tooltipHpaData0; - - if (tooltipHpaData1 && getDate(tooltipHpaData1)) { - tooltipHpaData = - x0.valueOf() - getDate(tooltipHpaData0).valueOf() > - getDate(tooltipHpaData1).valueOf() - x0.valueOf() - ? tooltipHpaData1 - : tooltipHpaData0; - } - - const container: SVGSVGElement = svgContainer.current; - - let point = container.createSVGPoint(); - // @ts-ignore - point.x = (event as any)?.clientX || 0; - // @ts-ignore - point.y = (event as any)?.clientY || 0; - point = point?.matrixTransform(container.getScreenCTM().inverse()); - - showTooltip({ - tooltipData: { - data: d, - tooltipHpaData, - aggregatedData: getAggregatedDataTooltip(x0), - }, - tooltipLeft: x || 0, - tooltipTop: point.y || 0, - }); - }, - [ - showTooltip, - valueScale, - dateScale, - width, - height, - data, - hpaData, - svgContainer, - hpaEnabled, - ] - ); - - if (width == 0 || height == 0 || width < 10) { - return null; - } - const hpaGraphTooltipGlyphPosition = - (hpaEnabled && - tooltipData?.tooltipHpaData && - valueScale(getValue(tooltipData?.tooltipHpaData))) || - null; - - const dataGraphTooltipGlyphPosition = - (tooltipData?.data && valueScale(getValue(tooltipData.data))) || 0; - - return ( -
- - - - - - {Object.entries(AggregatedDataColors).map(([dataKey, color]) => ( - - ))} - - - - data={data} - x={(d) => dateScale(getDate(d)) ?? 0} - y={(d) => valueScale(getValue(d)) ?? 0} - height={innerHeight} - yScale={valueScale} - strokeWidth={1} - stroke="url(#area-gradient)" - fill="url(#area-gradient)" - curve={curveMonotoneX} - /> - {Object.entries(aggregatedData).map(([key, data]) => ( - - key={key} - data={data} - x={(d) => dateScale(getDate(d)) ?? 0} - y={(d) => valueScale(getValue(d)) ?? 0} - height={innerHeight} - strokeWidth={1} - stroke={AggregatedDataColors[key]} - // fill={`url(#area-gradient-${key})`} - curve={curveMonotoneX} - /> - ))} - {isHpaEnabled && ( - - stroke="#ffffff" - strokeWidth={2} - data={hpaData} - x={(d) => dateScale(getDate(d)) ?? 0} - y={(d) => valueScale(getValue(d)) ?? 0} - strokeDasharray="6,4" - strokeOpacity={1} - pointerEvents="none" - /> - )} - - ({ - fill: "white", - fontSize: 11, - textAnchor: "start", - fillOpacity: 0.4, - dy: 0, - })} - /> - ({ - fill: "white", - fontSize: 11, - textAnchor: "middle", - fillOpacity: 0.4, - })} - /> - hideTooltip()} - /> - {tooltipData && ( - - - - {Object.entries(tooltipData.aggregatedData).map(([key, d]) => ( - - ))} - - {Object.entries(tooltipData.aggregatedData).map(([key, d]) => ( - - ))} - {isHpaEnabled && hpaGraphTooltipGlyphPosition !== null && ( - <> - - - - )} - - )} - - {tooltipData && ( -
- - {formatDate(getDate(tooltipData.data))} - - {dataKey}: {getValue(tooltipData.data)} - - {Object.entries(tooltipData.aggregatedData).map(([key, value]) => ( - - {`${key.toUpperCase()}. ${dataKey}`}: {getValue(value)} - - ))} - {isHpaEnabled && hpaGraphTooltipGlyphPosition !== null && ( -
- Autoscaling Threshold: {getValue(tooltipData.tooltipHpaData)} -
- )} -
-
- )} -
- ); -}; - -export default AreaChart; - -const TooltipDate = styled.div` - text-align: center; - margin-bottom: 8px; -`; - -const TooltipDataRow = styled.div<{ color?: string }>` - color: ${(props) => props.color ?? accentColor}; - margin-bottom: 4px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/JobMetricsSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/JobMetricsSection.tsx deleted file mode 100644 index 8fab45137e..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/JobMetricsSection.tsx +++ /dev/null @@ -1,561 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import styled from "styled-components"; -import ParentSize from "@visx/responsive/lib/components/ParentSize"; - -import settings from "assets/settings.svg"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { ChartTypeWithExtendedConfig, StorageType } from "shared/types"; - -import TabSelector from "components/TabSelector"; -import Loading from "components/Loading"; -import SelectRow from "components/form-components/SelectRow"; -import AreaChart from "./AreaChart"; -import { MetricNormalizer } from "./MetricNormalizer"; -import { AvailableMetrics, NormalizedMetricsData } from "./types"; -import CheckboxRow from "components/form-components/CheckboxRow"; - -type PropsType = { - jobChart: ChartTypeWithExtendedConfig; - jobRun: any; -}; - -export const resolutions: { [range: string]: string } = { - "1H": "1s", - "6H": "15s", - "1D": "15s", - "1M": "5h", -}; - -export const secondsBeforeNow: { [range: string]: number } = { - "1H": 60 * 60, - "6H": 60 * 60 * 6, - "1D": 60 * 60 * 24, - "1M": 60 * 60 * 24 * 30, -}; - -const JobMetricsSection: React.FunctionComponent = ({ - jobChart: currentChart, - jobRun, -}) => { - const [controllerOptions, setControllerOptions] = useState([]); - const [selectedController, setSelectedController] = useState(null); - const [ingressOptions, setIngressOptions] = useState([]); - const [selectedIngress, setSelectedIngress] = useState(null); - const [selectedRange, setSelectedRange] = useState("1H"); - const [selectedMetric, setSelectedMetric] = useState("cpu"); - const [selectedMetricLabel, setSelectedMetricLabel] = useState( - "CPU Utilization (vCPUs)" - ); - const [dropdownExpanded, setDropdownExpanded] = useState(false); - const [data, setData] = useState([]); - const [showMetricsSettings, setShowMetricsSettings] = useState(false); - const [metricsOptions, setMetricsOptions] = useState([ - { value: "cpu", label: "CPU Utilization (vCPUs)" }, - { value: "memory", label: "RAM Utilization (Mi)" }, - ]); - const [isLoading, setIsLoading] = useState(0); - const [hpaData, setHpaData] = useState([]); - const [hpaEnabled, setHpaEnabled] = useState( - currentChart?.config?.autoscaling?.enabled - ); - - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - - useEffect(() => { - setIsLoading((prev) => prev + 1); - - api - .getChartControllers( - "", - {}, - { - id: currentProject.id, - name: currentChart.name, - namespace: currentChart.namespace, - cluster_id: currentCluster.id, - revision: currentChart.version, - } - ) - .then((res) => { - const controllerOptions = res.data.map((controller: any) => { - let name = controller?.metadata?.name; - return { value: controller, label: name }; - }); - - setControllerOptions(controllerOptions); - setSelectedController(controllerOptions[0]?.value); - }) - .catch((err) => { - setCurrentError(JSON.stringify(err)); - setControllerOptions([]); - }) - .finally(() => { - setIsLoading((prev) => prev - 1); - }); - }, [currentChart, currentCluster, currentProject]); - - // prometheus has a limit of 11,000 data points to return per metric. we thus ensure that - // the resolution will not exceed 11,000 data points. - // - // This breaks down if the job runs for over 6 years. - const getJobResolution = (start: number, end: number) => { - let duration = end - start; - if (duration <= 3600) { - return "1s"; - } else if (duration <= 54000) { - return "15s"; - } else if (duration <= 216000) { - return "60s"; - } - - return "5h"; - }; - - const getAutoscalingThreshold = async ( - metricType: "cpu_hpa_threshold" | "memory_hpa_threshold", - shouldsum: boolean, - namespace: string, - start: number, - end: number - ) => { - setIsLoading((prev) => prev + 1); - setHpaData([]); - try { - const res = await api.getMetrics( - "", - { - metric: metricType, - shouldsum: shouldsum, - kind: selectedController?.kind, - name: selectedController?.metadata.name, - namespace: namespace, - startrange: start, - endrange: end, - resolution: getJobResolution(start, end), - pods: [], - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - - if (!Array.isArray(res.data) || !res.data[0]?.results) { - return; - } - const autoscalingMetrics = new MetricNormalizer(res.data, metricType); - setHpaData(autoscalingMetrics.getParsedData()); - return; - } catch (error) { - console.error(error); - } finally { - setIsLoading((prev) => prev - 1); - } - }; - - const getMetrics = async () => { - try { - let namespace = currentChart.namespace; - - const start = Math.round( - new Date(jobRun?.status?.startTime).getTime() / 1000 - ); - - let end = Math.round( - new Date(jobRun?.status?.completionTime).getTime() / 1000 - ); - - if (!jobRun?.status?.completionTime) { - end = Math.round(new Date().getTime() / 1000); - } - - setIsLoading((prev) => prev + 1); - setData([]); - - const res = await api.getMetrics( - "", - { - metric: selectedMetric, - shouldsum: true, - kind: "job", - name: jobRun?.metadata?.name, - namespace: namespace, - startrange: start, - endrange: end, - resolution: getJobResolution(start, end), - // pods: podNames, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - - if (res.data.length > 0) { - const metrics = new MetricNormalizer( - res.data, - selectedMetric as AvailableMetrics - ); - - // transform the metrics to expected form - setData(metrics.getParsedData()); - } - } catch (error) { - setCurrentError(JSON.stringify(error)); - } finally { - setIsLoading((prev) => prev - 1); - } - }; - - useEffect(() => { - if (selectedMetric && selectedRange && selectedController) { - getMetrics(); - } - }, [selectedMetric, selectedRange, selectedController, selectedIngress]); - - const renderMetricsSettings = () => { - if (showMetricsSettings && true) { - if (selectedMetric == "nginx:errors") { - return ( - <> - setShowMetricsSettings(false)} /> - - - setSelectedIngress(x)} - options={ingressOptions} - width="100%" - /> - - - ); - } - - return ( - <> - setShowMetricsSettings(false)} /> - - - setSelectedController(x)} - options={controllerOptions} - width="100%" - /> - - - ); - } - }; - - const renderDropdown = () => { - if (dropdownExpanded) { - return ( - <> - setDropdownExpanded(false)} /> - setDropdownExpanded(false)} - > - {renderOptionList()} - - - ); - } - }; - - const renderOptionList = () => { - return metricsOptions.map( - (option: { value: string; label: string }, i: number) => { - return ( - - ); - } - ); - }; - - const hasJobRunnedForMoreThan5m = () => { - const firstDate = new Date(jobRun.status.startTime); - const secondDate = jobRun?.status?.completionTime - ? new Date(jobRun?.status?.completionTime) - : new Date(); - const _5M_IN_MILISECONDS = 60000; - return secondDate.getTime() - firstDate.getTime() > _5M_IN_MILISECONDS; - }; - - return ( - - - - setDropdownExpanded(!dropdownExpanded)} - > - {selectedMetricLabel} - arrow_drop_down - {renderDropdown()} - - - - autorenew - - - - {isLoading > 0 && } - {data.length === 0 && isLoading === 0 && ( - <> - {selectedMetric === "cpu" && hasJobRunnedForMoreThan5m() ? ( - - No data available yet. - - autorenew - Refresh - - - ) : ( - - - CPU data is not available for jobs that ran for less than 5 - minutes. - - - )} - - )} - {data.length > 0 && isLoading === 0 && ( - <> - {currentChart?.config?.autoscaling?.enabled && - ["cpu", "memory"].includes(selectedMetric) && ( - setHpaEnabled((prev: any) => !prev)} - checked={hpaEnabled} - label="Show Autoscaling Threshold" - /> - )} - - {({ width, height }) => ( - - )} - - - )} - - ); -}; - -export default JobMetricsSection; - -const Highlight = styled.div` - display: flex; - align-items: center; - justify-content: center; - margin-left: 8px; - color: ${(props: { color: string; disableHover?: boolean }) => props.color}; - cursor: ${(props) => (props.disableHover ? "unset" : "pointer")}; - - > i { - font-size: 20px; - margin-right: 3px; - } -`; - -const Label = styled.div` - font-weight: bold; -`; - -const Relative = styled.div` - position: relative; -`; - -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 IconWrapper = styled.div` - display: flex; - position: relative; - align-items: center; - justify-content: center; - margin-top: 2px; - border-radius: 30px; - height: 25px; - width: 25px; - margin-left: 8px; - cursor: pointer; - :hover { - background: #ffffff22; - } -`; - -const SettingsIcon = styled.img` - opacity: 0.4; - width: 20px; - height: 20px; - margin-left: -1px; - margin-bottom: -2px; -`; - -const Flex = styled.div` - display: flex; - align-items: center; -`; - -const MetricsHeader = styled.div` - width: 100%; - display: flex; - align-items: center; - overflow: visible; - justify-content: space-between; -`; - -const DropdownOverlay = styled.div` - position: fixed; - width: 100%; - height: 100%; - z-index: 10; - left: 0px; - top: 0px; - cursor: default; -`; - -const Option = styled.div` - width: 100%; - border-top: 1px solid #00000000; - border-bottom: 1px solid - ${(props: { selected: boolean; lastItem: boolean }) => - props.lastItem ? "#ffffff00" : "#ffffff15"}; - height: 37px; - font-size: 13px; - padding-top: 9px; - align-items: center; - padding-left: 15px; - cursor: pointer; - padding-right: 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - background: ${(props: { selected: boolean; lastItem: boolean }) => - props.selected ? "#ffffff11" : ""}; - - :hover { - background: #ffffff22; - } -`; - -const Dropdown = styled.div` - position: absolute; - left: 0; - top: calc(100% + 10px); - background: #26282f; - width: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) => - props.dropdownWidth}; - max-height: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) => - props.dropdownMaxHeight || "300px"}; - border-radius: 3px; - z-index: 999; - overflow-y: auto; - margin-bottom: 20px; - box-shadow: 0px 4px 10px 0px #00000088; -`; - -const DropdownAlt = styled(Dropdown)` - padding: 20px 20px 7px; - overflow: visible; -`; - -const RangeWrapper = styled.div` - float: right; - font-weight: bold; - width: 156px; - margin-top: -8px; -`; - -const MetricSelector = styled.div` - font-size: 13px; - font-weight: 500; - position: relative; - color: #ffffff; - display: flex; - align-items: center; - cursor: pointer; - border-radius: 5px; - :hover { - > i { - background: #ffffff22; - } - } - - > i { - border-radius: 20px; - font-size: 20px; - margin-left: 10px; - } -`; - -const MetricsLabel = styled.div` - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - max-width: 200px; -`; - -const StyledMetricsSection = styled.div` - width: 100%; - min-height: 400px; - height: 50vh; - display: flex; - flex-direction: column; - position: relative; - font-size: 13px; - border-radius: 8px; - border: 1px solid #ffffff33; - padding: 18px 22px; - 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/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts deleted file mode 100644 index d49e94844b..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts +++ /dev/null @@ -1,174 +0,0 @@ -import _ from "lodash"; -import * as stats from "simple-statistics"; - -import { - type AvailableMetrics, - type GenericMetricResponse, - type MetricsCPUDataResponse, - type MetricsMemoryDataResponse, - type MetricsNetworkDataResponse, - type MetricsNGINXErrorsDataResponse, - type MetricsNGINXLatencyDataResponse, - type MetricsNGINXStatusDataResponse, - type MetricsReplicasDataResponse, - type NormalizedMetricsData, - type NormalizedNginxStatusMetricsData, -} from "./types"; - -/** - * Normalize values from the API to be readable by the AreaChart component. - * This class was created to reduce the amount of parsing inside the MetricsSection component - * and improve readability - */ -export class MetricNormalizer { - metric_results: GenericMetricResponse["results"]; - kind: AvailableMetrics; - - constructor(data: GenericMetricResponse[], kind: AvailableMetrics) { - if (!Array.isArray(data) || !data[0]?.results) { - throw new Error("Failed parsing response" + JSON.stringify(data)); - } - this.metric_results = data[0].results; - this.kind = kind; - } - - getParsedData(): NormalizedMetricsData[] { - if (this.kind.includes("cpu")) { - return this.parseCPUMetrics(this.metric_results); - } - if (this.kind.includes("memory")) { - return this.parseMemoryMetrics(this.metric_results); - } - if (this.kind.includes("network")) { - return this.parseNetworkMetrics(this.metric_results); - } - if (this.kind.includes("nginx:errors")) { - return this.parseNGINXErrorsMetrics(this.metric_results); - } - if ( - this.kind.includes("nginx:latency") || - this.kind.includes("nginx:latency-histogram") - ) { - return this.parseNGINXLatencyMetrics(this.metric_results); - } - if (this.kind.includes("hpa_replicas")) { - return this.parseHpaReplicaMetrics(this.metric_results); - } - return []; - } - - getNginxStatusData(): NormalizedNginxStatusMetricsData[] { - if (this.kind.includes("nginx:status")) { - return this.parseNGINXStatusMetrics(this.metric_results); - } - - return []; - } - - getAggregatedData(): Record { - const groupedByDate = _.groupBy(this.getParsedData(), "date"); - - const avg = Object.keys(groupedByDate).map((date) => { - const values = groupedByDate[date].map((d) => d.value); - return { - date: Number(date), - value: stats.mean(values), - }; - }); - - const min = Object.keys(groupedByDate).map((date) => { - const values = groupedByDate[date].map((d) => d.value); - return { - date: Number(date), - value: stats.min(values), - }; - }); - - const max = Object.keys(groupedByDate).map((date) => { - const values = groupedByDate[date].map((d) => d.value); - return { - date: Number(date), - value: stats.max(values), - }; - }); - - return { - min, - avg, - max, - }; - } - - private parseCPUMetrics(arr: MetricsCPUDataResponse["results"]) { - return arr.map((d) => { - return { - date: d.date, - value: parseFloat(d.cpu), - }; - }); - } - - private parseMemoryMetrics(arr: MetricsMemoryDataResponse["results"]) { - return arr.map((d) => { - return { - date: d.date, - value: parseFloat(d.memory) / (1024 * 1024), // put units in Mi - }; - }); - } - - private parseNetworkMetrics(arr: MetricsNetworkDataResponse["results"]) { - return arr.map((d) => { - return { - date: d.date, - value: parseFloat(d.bytes) / 1024, // put units in Ki - }; - }); - } - - private parseNGINXErrorsMetrics( - arr: MetricsNGINXErrorsDataResponse["results"] - ) { - return arr.map((d) => { - return { - date: d.date, - value: parseFloat(d.error_pct), - }; - }); - } - - private parseNGINXStatusMetrics( - arr: MetricsNGINXStatusDataResponse["results"] - ) { - return arr.map((d) => { - return { - date: d.date, - "1xx": parseInt(d["1xx"]), - "2xx": parseInt(d["2xx"]), - "3xx": parseInt(d["3xx"]), - "4xx": parseInt(d["4xx"]), - "5xx": parseInt(d["5xx"]), - }; - }); - } - - private parseNGINXLatencyMetrics( - arr: MetricsNGINXLatencyDataResponse["results"] - ) { - return arr.map((d) => { - return { - date: d.date, - value: d.latency != "NaN" ? parseFloat(d.latency) : 0, - }; - }); - } - - private parseHpaReplicaMetrics(arr: MetricsReplicasDataResponse["results"]) { - return arr.map((d) => { - return { - date: d.date, - value: parseInt(d.replicas), - }; - }); - } -} diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx deleted file mode 100644 index 0372c24910..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx +++ /dev/null @@ -1,750 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import styled from "styled-components"; -import ParentSize from "@visx/responsive/lib/components/ParentSize"; - -import settings from "assets/settings.svg"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { ChartTypeWithExtendedConfig, StorageType } from "shared/types"; - -import TabSelector from "components/TabSelector"; -import Loading from "components/Loading"; -import SelectRow from "components/form-components/SelectRow"; -import AreaChart from "./AreaChart"; -import { MetricNormalizer } from "./MetricNormalizer"; -import { - AvailableMetrics, - GenericMetricResponse, - NormalizedMetricsData, -} from "./types"; -import CheckboxRow from "components/form-components/CheckboxRow"; -import AggregatedDataLegend from "./AggregatedDataLegend"; - -type PropsType = { - currentChart: ChartTypeWithExtendedConfig; -}; - -export const resolutions: { [range: string]: string } = { - "1H": "1s", - "6H": "15s", - "1D": "15s", - "1M": "5h", -}; - -export const secondsBeforeNow: { [range: string]: number } = { - "1H": 60 * 60, - "6H": 60 * 60 * 6, - "1D": 60 * 60 * 24, - "1M": 60 * 60 * 24 * 30, -}; - -const MetricsSection: React.FunctionComponent = ({ - currentChart, -}) => { - const [pods, setPods] = useState([]); - const [selectedPod, setSelectedPod] = useState(""); - const [controllerOptions, setControllerOptions] = useState([]); - const [selectedController, setSelectedController] = useState(null); - const [ingressOptions, setIngressOptions] = useState([]); - const [selectedIngress, setSelectedIngress] = useState(null); - const [selectedRange, setSelectedRange] = useState("1H"); - const [selectedMetric, setSelectedMetric] = useState("cpu"); - const [selectedMetricLabel, setSelectedMetricLabel] = useState( - "CPU Utilization (vCPUs)" - ); - const [dropdownExpanded, setDropdownExpanded] = useState(false); - const [data, setData] = useState([]); - const [aggregatedData, setAggregatedData] = useState< - Record - >({}); - const [showMetricsSettings, setShowMetricsSettings] = useState(false); - const [metricsOptions, setMetricsOptions] = useState([ - { value: "cpu", label: "CPU Utilization (vCPUs)" }, - { value: "memory", label: "RAM Utilization (Mi)" }, - { value: "network", label: "Network Received Bytes (Ki)" }, - ]); - const [isLoading, setIsLoading] = useState(0); - const [hpaData, setHpaData] = useState([]); - const [hpaEnabled, setHpaEnabled] = useState( - currentChart?.config?.autoscaling?.enabled - ); - - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - - // Add or remove hpa replicas chart option when current chart is updated - useEffect(() => { - if (currentChart?.config?.autoscaling?.enabled) { - setMetricsOptions((prev) => { - if (prev.find((option) => option.value === "hpa_replicas")) { - return [...prev]; - } - return [ - ...prev, - { value: "hpa_replicas", label: "Number of replicas" }, - ]; - }); - } else { - setMetricsOptions((prev) => { - const hpaReplicasOptionIndex = prev.findIndex( - (option) => option.value === "hpa_replicas" - ); - const options = [...prev]; - if (hpaReplicasOptionIndex > -1) { - options.splice(hpaReplicasOptionIndex, 1); - } - return [...options]; - }); - } - }, [currentChart]); - - useEffect(() => { - if (currentChart?.chart?.metadata?.name == "ingress-nginx") { - setIsLoading((prev) => prev + 1); - - api - .getNGINXIngresses( - "", - {}, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - setMetricsOptions((prev) => { - return [ - ...prev, - { - value: "nginx:errors", - label: "5XX Error Percentage", - }, - ]; - }); - - const ingressOptions = res.data.map((ingress: any) => ({ - value: ingress, - label: ingress.name, - })); - setIngressOptions(ingressOptions); - setSelectedIngress(ingressOptions[0]?.value); - // iterate through the controllers to get the list of pods - }) - .catch((err) => { - setCurrentError(JSON.stringify(err)); - }) - .finally(() => { - setIsLoading((prev) => prev - 1); - }); - } - - setIsLoading((prev) => prev + 1); - - api - .getChartControllers( - "", - {}, - { - id: currentProject.id, - name: currentChart.name, - namespace: currentChart.namespace, - cluster_id: currentCluster.id, - revision: currentChart.version, - } - ) - .then((res) => { - const controllerOptions = res.data.map((controller: any) => { - let name = controller?.metadata?.name; - return { value: controller, label: name }; - }); - - setControllerOptions(controllerOptions); - setSelectedController(controllerOptions[0]?.value); - }) - .catch((err) => { - setCurrentError(JSON.stringify(err)); - setControllerOptions([]); - }) - .finally(() => { - setIsLoading((prev) => prev - 1); - }); - }, [currentChart, currentCluster, currentProject]); - - useEffect(() => { - getPods(); - }, [selectedController]); - - const getPods = () => { - let selectors = [] as string[]; - let ml = - selectedController?.spec?.selector?.matchLabels || - selectedController?.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; - } - - selectors.push(selector); - - if (selectors[0] === "") { - return; - } - - setIsLoading((prev) => prev + 1); - - api - .getMatchingPods( - "", - { - namespace: selectedController?.metadata?.namespace, - selectors, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then((res) => { - let pods = [{ value: "All", label: "All (Summed)" }] as any[]; - res?.data?.forEach((pod: any) => { - let name = pod?.metadata?.name; - pods.push({ value: name, label: name }); - }); - setPods(pods); - setSelectedPod("All"); - - getMetrics(); - }) - .catch((err) => { - setCurrentError(JSON.stringify(err)); - return; - }) - .finally(() => { - setIsLoading((prev) => prev - 1); - }); - }; - - const getAutoscalingThreshold = async ( - metricType: "cpu_hpa_threshold" | "memory_hpa_threshold", - shouldsum: boolean, - namespace: string, - start: number, - end: number - ) => { - setIsLoading((prev) => prev + 1); - setHpaData([]); - try { - const res = await api.getMetrics( - "", - { - metric: metricType, - shouldsum: shouldsum, - kind: selectedController?.kind, - name: selectedController?.metadata.name, - namespace: namespace, - startrange: start, - endrange: end, - resolution: resolutions[selectedRange], - pods: [], - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - - if (!Array.isArray(res.data) || !res.data[0]?.results) { - return; - } - const autoscalingMetrics = new MetricNormalizer(res.data, metricType); - setHpaData(autoscalingMetrics.getParsedData()); - return; - } catch (error) { - console.error(error); - } finally { - setIsLoading((prev) => prev - 1); - } - }; - - const getMetrics = async () => { - if (pods?.length == 0) { - return; - } - try { - let shouldsum = selectedPod === "All"; - let namespace = currentChart.namespace; - - // calculate start and end range - const d = new Date(); - const end = Math.round(d.getTime() / 1000); - const start = end - secondsBeforeNow[selectedRange]; - - let podNames = [] as string[]; - - if (!shouldsum) { - podNames = [selectedPod]; - } - - if (selectedMetric == "nginx:errors") { - podNames = [selectedIngress?.name]; - namespace = selectedIngress?.namespace || "default"; - shouldsum = false; - } - - setIsLoading((prev) => prev + 1); - setData([]); - setAggregatedData({}); - - // Get aggregated metrics - const allPodsRes = await api.getMetrics( - "", - { - metric: selectedMetric, - shouldsum: false, - kind: selectedController?.kind, - name: selectedController?.metadata.name, - namespace: namespace, - startrange: start, - endrange: end, - resolution: resolutions[selectedRange], - pods: [], - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - - const allPodsData: GenericMetricResponse[] = allPodsRes.data ?? []; - const allPodsMetrics = allPodsData.flatMap((d) => d.results); - const allPodsMetricsNormalized = new MetricNormalizer( - [{ results: allPodsMetrics }], - selectedMetric as AvailableMetrics - ); - setAggregatedData(allPodsMetricsNormalized.getAggregatedData()); - // - - const res = await api.getMetrics( - "", - { - metric: selectedMetric, - shouldsum: shouldsum, - kind: selectedController?.kind, - name: selectedController?.metadata.name, - namespace: namespace, - startrange: start, - endrange: end, - resolution: resolutions[selectedRange], - pods: podNames, - }, - { - id: currentProject.id, - cluster_id: currentCluster.id, - } - ); - - setHpaData([]); - const isHpaEnabled = currentChart?.config?.autoscaling?.enabled; - if (shouldsum && isHpaEnabled) { - if (selectedMetric === "cpu") { - await getAutoscalingThreshold( - "cpu_hpa_threshold", - shouldsum, - namespace, - start, - end - ); - } else if (selectedMetric === "memory") { - await getAutoscalingThreshold( - "memory_hpa_threshold", - shouldsum, - namespace, - start, - end - ); - } - } - - const metrics = new MetricNormalizer( - res.data, - selectedMetric as AvailableMetrics - ); - - // transform the metrics to expected form - setData(metrics.getParsedData()); - } catch (error) { - setCurrentError(JSON.stringify(error)); - } finally { - setIsLoading((prev) => prev - 1); - } - }; - - useEffect(() => { - if (selectedMetric && selectedRange && selectedPod && selectedController) { - getMetrics(); - } - }, [ - selectedMetric, - selectedRange, - selectedPod, - selectedController, - selectedIngress, - ]); - - const renderMetricsSettings = () => { - if (showMetricsSettings && true) { - if (selectedMetric == "nginx:errors") { - return ( - <> - setShowMetricsSettings(false)} /> - - - setSelectedIngress(x)} - options={ingressOptions} - width="100%" - /> - - - ); - } - - return ( - <> - setShowMetricsSettings(false)} /> - - - setSelectedController(x)} - options={controllerOptions} - width="100%" - /> - setSelectedPod(x)} - options={pods} - width="100%" - /> - - - ); - } - }; - - const renderDropdown = () => { - if (dropdownExpanded) { - return ( - <> - setDropdownExpanded(false)} /> - setDropdownExpanded(false)} - > - {renderOptionList()} - - - ); - } - }; - - const renderOptionList = () => { - return metricsOptions.map( - (option: { value: string; label: string }, i: number) => { - return ( - - ); - } - ); - }; - - return ( - - - - setDropdownExpanded(!dropdownExpanded)} - > - {selectedMetricLabel} - arrow_drop_down - {renderDropdown()} - - - setShowMetricsSettings(true)}> - - - {renderMetricsSettings()} - - - - autorenew - - - - setSelectedRange(x)} - /> - - - {isLoading > 0 && } - {data.length === 0 && isLoading === 0 && ( - - No data available yet. - - autorenew - Refresh - - - )} - {data.length > 0 && isLoading === 0 && ( - <> - - {currentChart?.config?.autoscaling?.enabled && - ["cpu", "memory"].includes(selectedMetric) && ( - setHpaEnabled((prev: any) => !prev)} - checked={hpaEnabled} - label="Show Autoscaling Threshold" - /> - )} - - {({ width, height }) => ( - - )} - - - )} - - ); -}; - -export default MetricsSection; - -const Highlight = styled.div` - display: flex; - align-items: center; - justify-content: center; - margin-left: 8px; - color: ${(props: { color: string }) => props.color}; - cursor: pointer; - - > i { - font-size: 20px; - margin-right: 3px; - } -`; - -const Label = styled.div` - font-weight: bold; -`; - -const Relative = styled.div` - position: relative; -`; - -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 IconWrapper = styled.div` - display: flex; - position: relative; - align-items: center; - justify-content: center; - margin-top: 2px; - border-radius: 30px; - height: 25px; - width: 25px; - margin-left: 8px; - cursor: pointer; - :hover { - background: #ffffff22; - } -`; - -const SettingsIcon = styled.img` - opacity: 0.4; - width: 20px; - height: 20px; - margin-left: -1px; - margin-bottom: -2px; -`; - -const Flex = styled.div` - display: flex; - align-items: center; -`; - -const MetricsHeader = styled.div` - width: 100%; - display: flex; - align-items: center; - overflow: visible; - justify-content: space-between; -`; - -const DropdownOverlay = styled.div` - position: fixed; - width: 100%; - height: 100%; - z-index: 10; - left: 0px; - top: 0px; - cursor: default; -`; - -const Option = styled.div` - width: 100%; - border-top: 1px solid #00000000; - border-bottom: 1px solid - ${(props: { selected: boolean; lastItem: boolean }) => - props.lastItem ? "#ffffff00" : "#ffffff15"}; - height: 37px; - font-size: 13px; - padding-top: 9px; - align-items: center; - padding-left: 15px; - cursor: pointer; - padding-right: 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - background: ${(props: { selected: boolean; lastItem: boolean }) => - props.selected ? "#ffffff11" : ""}; - - :hover { - background: #ffffff22; - } -`; - -const Dropdown = styled.div` - position: absolute; - left: 0; - top: calc(100% + 10px); - background: #26282f; - width: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) => - props.dropdownWidth}; - max-height: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) => - props.dropdownMaxHeight || "300px"}; - border-radius: 3px; - z-index: 999; - overflow-y: auto; - margin-bottom: 20px; - box-shadow: 0px 4px 10px 0px #00000088; -`; - -const DropdownAlt = styled(Dropdown)` - padding: 20px 20px 7px; - overflow: visible; -`; - -const RangeWrapper = styled.div` - float: right; - font-weight: bold; - width: 158px; - margin-top: -8px; -`; - -const MetricSelector = styled.div` - font-size: 13px; - font-weight: 500; - position: relative; - color: #ffffff; - display: flex; - align-items: center; - cursor: pointer; - border-radius: 5px; - :hover { - > i { - background: #ffffff22; - } - } - - > i { - border-radius: 20px; - font-size: 20px; - margin-left: 10px; - } -`; - -const MetricsLabel = styled.div` - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - max-width: 200px; -`; - -const StyledMetricsSection = styled.div` - width: 100%; - min-height: 400px; - height: calc(100vh - 400px); - display: flex; - flex-direction: column; - position: relative; - font-size: 13px; - border-radius: 8px; - border: 1px solid #ffffff33; - padding: 18px 22px; - 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/cluster-dashboard/expanded-chart/status/ControllerTab.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx deleted file mode 100644 index 98cbb88dfc..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx +++ /dev/null @@ -1,439 +0,0 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import styled from "styled-components"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import ResourceTab from "components/ResourceTab"; -import ConfirmOverlay from "components/ConfirmOverlay"; -import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets"; -import PodRow from "./PodRow"; -import { timeFormat } from "d3-time-format"; - -type Props = { - controller: any; - selectedPod: any; - selectPod: (newPod: any) => unknown; - selectors: any; - isLast?: boolean; - isFirst?: boolean; - setPodError: (x: string) => void; -}; - -// Controller tab in log section that displays list of pods on click. -export type ControllerTabPodType = { - namespace: string; - name: string; - phase: string; - status: any; - replicaSetName: string; - restartCount: number | string; - podAge: string; - revisionNumber?: number; - containerStatus: any; -}; - -const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y"); - -const ControllerTabFC: React.FunctionComponent = ({ - controller, - selectPod, - isFirst, - isLast, - selectors, - setPodError, - selectedPod, -}) => { - const [pods, setPods] = useState([]); - const [rawPodList, setRawPodList] = useState([]); - const [podPendingDelete, setPodPendingDelete] = useState(null); - const [available, setAvailable] = useState(null); - const [total, setTotal] = useState(null); - const [userSelectedPod, setUserSelectedPod] = useState(false); - - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - const { - newWebsocket, - openWebsocket, - closeAllWebsockets, - closeWebsocket, - } = useWebsockets(); - - const currentSelectors = useMemo(() => { - if (controller.kind.toLowerCase() == "job" && selectors) { - return [...selectors]; - } - let newSelectors = [] as string[]; - 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; - } - newSelectors.push(selector); - return [...newSelectors]; - }, [controller, selectors]); - - useEffect(() => { - updatePods(); - [controller?.kind, "pod"].forEach((kind) => { - setupWebsocket(kind, controller?.metadata?.uid); - }); - () => closeAllWebsockets(); - }, [currentSelectors, controller, currentCluster, currentProject]); - - const updatePods = async () => { - try { - const res = await api.getMatchingPods( - "", - { - namespace: controller?.metadata?.namespace, - selectors: currentSelectors, - }, - { - 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) - ); - - 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 && - pod?.metadata?.annotations["helm.sh/revision"]) || - "N/A", - }; - }); - - setPods(newPods); - setRawPodList(data); - // If the user didn't click a pod, select the first returned from list. - if (!userSelectedPod) { - let status = getPodStatus(newPods[0].status); - status === "failed" && - newPods[0].status?.message && - setPodError(newPods[0].status?.message); - handleSelectPod(newPods[0], data); - } - } catch (error) {} - }; - - /** - * handleSelectPod is a wrapper for the selectPod function received from parent. - * Internally we use the ControllerPodType but we want to pass to the parent the - * raw pod returned from the API. - * - * @param pod A ControllerPodType pod that will be used to search the raw pod to pass - * @param rawList A rawList of pods in case we don't want to use the state one. Useful to - * avoid problems with reactivity - */ - const handleSelectPod = (pod: ControllerTabPodType, rawList?: any[]) => { - const rawPod = [...rawPodList, ...(rawList || [])].find( - (rawPod) => rawPod?.metadata?.name === pod?.name - ); - selectPod(rawPod); - }; - - const currentSelectedPod = useMemo(() => { - const pod = selectedPod; - const replicaSetName = - Array.isArray(pod?.metadata?.ownerReferences) && - pod?.metadata?.ownerReferences[0]?.name; - return { - namespace: pod?.metadata?.namespace, - name: pod?.metadata?.name, - phase: pod?.status?.phase, - status: pod?.status, - replicaSetName, - } as ControllerTabPodType; - }, [selectedPod]); - - const currentControllerStatus = useMemo(() => { - let status = available == total ? "running" : "waiting"; - - controller?.status?.conditions?.forEach((condition: any) => { - if ( - condition.type == "Progressing" && - condition.status == "False" && - condition.reason == "ProgressDeadlineExceeded" - ) { - status = "failed"; - } - }); - - if (controller.kind.toLowerCase() === "job" && pods.length == 0) { - status = "completed"; - } - return status; - }, [controller, available, total, pods]); - - const getPodStatus = (status: any) => { - if ( - status?.phase === "Pending" && - status?.containerStatuses !== undefined - ) { - return status.containerStatuses[0].state?.waiting?.reason || "Pending"; - } else if (status?.phase === "Pending") { - return "Pending"; - } - - if (status?.phase === "Failed") { - return "failed"; - } - - if (status?.phase === "Running") { - let collatedStatus = "running"; - - status?.containerStatuses?.forEach((s: any) => { - if (s.state?.waiting) { - collatedStatus = - s.state?.waiting?.reason === "CrashLoopBackOff" - ? "failed" - : "waiting"; - } else if (s.state?.terminated) { - collatedStatus = "failed"; - } - }); - return collatedStatus; - } - }; - - const handleDeletePod = (pod: any) => { - api - .deletePod( - "", - {}, - { - cluster_id: currentCluster.id, - name: pod?.name, - namespace: pod?.namespace, - id: currentProject.id, - } - ) - .then((res) => { - updatePods(); - setPodPendingDelete(null); - }) - .catch((err) => { - setCurrentError(JSON.stringify(err)); - setPodPendingDelete(null); - }); - }; - - const replicaSetArray = useMemo(() => { - const podsDividedByReplicaSet = pods.reduce< - Array> - >(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; - }, []); - - if (podsDividedByReplicaSet.length === 1) { - return []; - } else { - return podsDividedByReplicaSet; - } - }, [pods]); - - const getAvailability = (kind: string, c: any) => { - switch (kind?.toLowerCase()) { - case "deployment": - case "replicaset": - return [ - c.status?.availableReplicas || - c.status?.replicas - c.status?.unavailableReplicas || - 0, - c.status?.replicas || 0, - ]; - case "statefulset": - return [c.status?.readyReplicas || 0, c.status?.replicas || 0]; - case "daemonset": - return [ - c.status?.numberAvailable || 0, - c.status?.desiredNumberScheduled || 0, - ]; - case "job": - return [1, 1]; - } - }; - - const setupWebsocket = (kind: string, controllerUid: string) => { - let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status?`; - if (kind == "pod" && currentSelectors) { - apiEndpoint += `selectors=${currentSelectors[0]}`; - } - - const options: NewWebsocketOptions = {}; - options.onopen = () => { - console.log("connected to websocket"); - }; - - options.onmessage = (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 != "pod") { - let [available, total] = getAvailability(object.metadata.kind, object); - setAvailable(available); - setTotal(total); - return; - } - updatePods(); - }; - - options.onclose = () => { - console.log("closing websocket"); - }; - - options.onerror = (err: ErrorEvent) => { - console.log(err); - closeWebsocket(kind); - }; - - newWebsocket(kind, apiEndpoint, options); - openWebsocket(kind); - }; - - const mapPods = (podList: ControllerTabPodType[]) => { - return podList.map((pod, i, arr) => { - let status = getPodStatus(pod.status); - return ( - { - setPodError(""); - status === "failed" && - pod.status?.message && - setPodError(pod.status?.message); - handleSelectPod(pod); - setUserSelectedPod(true); - }} - onDeleteClick={() => setPodPendingDelete(pod)} - /> - ); - }); - }; - - return ( - - {!!replicaSetArray.length && - replicaSetArray.map((subArray, index) => { - const firstItem = subArray[0]; - return ( -
- - - {firstItem?.revisionNumber && - firstItem?.revisionNumber.toString() != "N/A" && ( - Revision {firstItem.revisionNumber}: - )}{" "} - {firstItem.replicaSetName} - - - {mapPods(subArray)} -
- ); - })} - {!replicaSetArray.length && mapPods(pods)} - handleDeletePod(podPendingDelete)} - onNo={() => setPodPendingDelete(null)} - /> -
- ); -}; - -export default ControllerTabFC; - -const Bold = styled.span` - font-weight: 500; - display: inline; - color: #ffffff; -`; - -const RevisionLabel = styled.div` - font-size: 12px; - color: #ffffff33; - width: 78px; - text-align: right; - padding-top: 7px; - margin-right: 10px; - margin-left: 10px; - overflow-wrap: anywhere; -`; - -const ReplicaSetContainer = styled.div` - padding: 10px 5px; - display: flex; - overflow-wrap: anywhere; - justify-content: space-between; - border-top: 2px solid #ffffff11; -`; - -const ReplicaSetName = styled.span` - padding-left: 10px; - overflow-wrap: anywhere; - max-width: calc(100% - 45px); - line-height: 1.5em; - color: #ffffff33; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx deleted file mode 100644 index c0952d27b2..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx +++ /dev/null @@ -1,398 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import styled from "styled-components"; -import Anser from "anser"; -import CommandLineIcon from "assets/command-line-icon"; -import { SelectedPodType } from "./types"; -import { useLogs } from "./useLogs"; - -const LogsFC: React.FC<{ - selectedPod: SelectedPodType; - podError: string; - rawText?: boolean; -}> = ({ selectedPod, podError, rawText }) => { - const [isScrollToBottomEnabled, setIsScrollToBottomEnabled] = useState(true); - - const [showConnectionModal, setShowConnectionModal] = useState(false); - - const shouldScroll = useRef(true); - const wrapperRef = useRef(); - - const scrollToBottom = (smooth: boolean) => { - if (!wrapperRef.current || !shouldScroll.current) { - return; - } - - if (smooth) { - wrapperRef.current.lastElementChild.scrollIntoView({ - behavior: "smooth", - block: "nearest", - inline: "start", - }); - } else { - wrapperRef.current.lastElementChild.scrollIntoView({ - behavior: "auto", - block: "nearest", - inline: "start", - }); - } - }; - - const { - logs, - previousLogs, - containers, - currentContainer, - setCurrentContainer, - refresh, - } = useLogs(selectedPod, scrollToBottom); - - const [showPreviousLogs, setShowPreviousLogs] = useState(false); - - useEffect(() => { - shouldScroll.current = isScrollToBottomEnabled; - }, [isScrollToBottomEnabled]); - - const renderLogs = () => { - if (podError && podError != "") { - return {podError}; - } - - if (!selectedPod?.metadata?.name) { - return Please select a pod to view its logs.; - } - - if (selectedPod?.status.phase === "Succeeded" && !rawText) { - return ( - - ⌛ This job has been completed. You can now delete this job. - - ); - } - - if ( - showPreviousLogs && - Array.isArray(previousLogs) && - previousLogs.length - ) { - return previousLogs?.map((log, i) => { - return ( - - {log.map((ansi, j) => { - if (ansi.clearLine) { - return null; - } - - return ( - - {ansi.content.replace(/ /g, "\u00a0")} - - ); - })} - - ); - }); - } - - if (!Array.isArray(logs) || logs?.length === 0) { - return ( - - No logs to display from this pod. - - autorenew - Refresh - - - ); - } - - return logs?.map((log, i) => { - return ( - - {log.map((ansi, j) => { - if (ansi.clearLine) { - return null; - } - - return ( - - {ansi.content.replace(/ /g, "\u00a0")} - - ); - })} - - ); - }); - }; - - const renderContent = () => ( - <> - {/* setShowConnectionModal(false)} - chartName={selectedPod?.metadata?.labels["app.kubernetes.io/instance"]} - namespace={selectedPod?.metadata?.namespace} - /> - { - e.preventDefault(); - setShowConnectionModal(true); - }} - > - - CLI Logs Instructions - */} - {renderLogs()} - - {containers.map((containerName, _i, arr) => { - return ( - { - setCurrentContainer(containerName); - }} - clicked={currentContainer === containerName} - > - {arr.length > 1 ? containerName : "Application"} - - ); - })} - { - setCurrentContainer("system"); - }} - clicked={currentContainer == "system"} - > - System - - - - { - setIsScrollToBottomEnabled(!isScrollToBottomEnabled); - if (isScrollToBottomEnabled) { - scrollToBottom(true); - } - }} - > - {}} - /> - 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/cluster-dashboard/expanded-chart/status/PodRow.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/PodRow.tsx deleted file mode 100644 index d2a20abe51..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/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/cluster-dashboard/expanded-chart/status/StatusSection.tsx b/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx deleted file mode 100644 index 8bee3b9ee1..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx +++ /dev/null @@ -1,288 +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"; - -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 ( - <> - {fullscreen ? ( - - - - navigate_before - - Status ({currentChart.name}) - - - close_fullscreen - - {renderStatusSection()} - - ) : ( - - - open_in_full - - {renderStatusSection()} - - )} - - ); -}; - -export default StatusSectionFC; - -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/cluster-dashboard/expanded-chart/status/types.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/types.ts deleted file mode 100644 index ffcfd4597d..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/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/cluster-dashboard/expanded-chart/status/useLogs.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/status/useLogs.ts deleted file mode 100644 index dfb594abc0..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/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/cluster-dashboard/expanded-chart/useStackEnvGroups.ts b/dashboard/src/main/home/cluster-dashboard/expanded-chart/useStackEnvGroups.ts deleted file mode 100644 index 3c55938403..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/expanded-chart/useStackEnvGroups.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { PopulatedEnvGroup } from "components/porter-form/types"; -import { useContext, useEffect, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { ChartType } from "shared/types"; -import { Stack } from "../stacks/types"; - -export const useStackEnvGroups = (chart: ChartType) => { - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - const [stackEnvGroups, setStackEnvGroups] = useState([]); - const [loading, setLoading] = useState(true); - - const getEnvGroups = async (stack: Stack) => { - const envGroups = stack.latest_revision.env_groups; - - const envGroupsWithValues = envGroups.map((envGroup) => { - return api - .getEnvGroup( - "", - {}, - { - id: currentProject.id, - namespace: chart.namespace, - cluster_id: currentCluster.id, - name: envGroup.name, - version: envGroup.env_group_version, - } - ) - .then((res) => res.data); - }); - - return Promise.all(envGroupsWithValues); - }; - - const getStack = (stack_id: string) => - api - .getStack( - "token", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - stack_id, - namespace: chart.namespace, - } - ) - .then((res) => res.data); - - useEffect(() => { - const stack_id = chart?.stack_id; - if (!stack_id) { - // if the chart has been loaded and the chart doesn't have a stack id, set loading to false - if (loading && chart) { - setLoading(false); - } - - return; - } - setLoading(true); - getStack(stack_id) - .then((stack) => getEnvGroups(stack)) - .then((populatedEnvGroups) => { - setStackEnvGroups(populatedEnvGroups); - - setLoading(false); - }) - .catch((error) => { - setCurrentError(error); - }); - }, [chart?.stack_id]); - - return { - isStack: chart?.stack_id?.length ? true : false, - stackEnvGroups, - isLoadingStackEnvGroups: loading, - }; -}; diff --git a/dashboard/src/main/home/cluster-dashboard/jobs/JobDashboard.tsx b/dashboard/src/main/home/cluster-dashboard/jobs/JobDashboard.tsx deleted file mode 100644 index cb4c5d94e5..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/jobs/JobDashboard.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import React, { useContext, useState } from "react"; -import styled from "styled-components"; -import { RouteComponentProps, withRouter } from "react-router"; - -import job from "assets/job.png"; - -import { Context } from "shared/Context"; -import { JobStatusType } from "shared/types"; -import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc"; -import { - pushQueryParams, - pushFiltered, - PorterUrl, -} from "shared/routing"; - -import { NamespaceSelector } from "../NamespaceSelector"; -import TagFilter from "../TagFilter"; -import DashboardHeader from "../DashboardHeader"; -import LastRunStatusSelector from "../LastRunStatusSelector"; -import JobRunTable from "../chart/JobRunTable"; -import ChartList from "../chart/ChartList"; -import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder"; - -type Props = RouteComponentProps & WithAuthProps & { - currentView: PorterUrl; - namespace?: string; - setNamespace?: (namespace: string) => void; - sortType: any; -}; - -// TODO: Pull namespace (and sort) down out of DashboardRouter -const JobDashboard: React.FC = ({ - currentView, - namespace, - setNamespace, - sortType, - ...props -}) => { - const { currentProject, currentCluster } = useContext(Context); - const [lastRunStatus, setLastRunStatus] = useState("all" as JobStatusType); - const [showRuns, setShowRuns] = useState(false); - const [selectedTag, setSelectedTag] = useState("none"); - - return ( - - - {currentCluster.status === "UPDATING_UNAVAILABLE" ? ( - - ) : ( - <> - - - - { - setNamespace(x); - pushQueryParams(props, { - namespace: x || "ALL", - }); - }} - namespace={namespace} - /> - - - - - setShowRuns(false)} - selected={!showRuns} - > - Jobs - - setShowRuns(true)} - selected={showRuns} - > - Runs - - - {props.isAuthorized( - "namespace", - [], - ["get", "create"] - ) && ( - - )} - - - - - - - - - - )} - - ); -}; - -export default withRouter(withAuth(JobDashboard)); - -const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>` - padding: 0 10px; - color: ${(props) => (props.selected ? "" : "#494b4f")}; - border: 1px solid #494b4f; - height: 100%; - display: flex; - margin-left: ${(props) => (props.nudgeLeft ? "-1px" : "")}; - align-items: center; - border-radius: ${(props) => - props.nudgeLeft ? "0 5px 5px 0" : "5px 0 0 5px"}; - :hover { - border: 1px solid #7a7b80; - z-index: 999; - } -`; - -const ToggleButton = styled.div` - background: #26292e; - border-radius: 5px; - font-size: 13px; - height: 30px; - display: flex; - align-items: center; - cursor: pointer; -`; - -const Flex = styled.div` - display: flex; - align-items: center; - border-bottom: 30px solid transparent; -`; - -const StyledJobDashboard = styled.div` -`; - -const ControlRow = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; -`; - -const FilterWrapper = styled.div` - display: flex; - justify-content: space-between; - border-bottom: 30px solid transparent; - > div:not(:first-child) { - } -`; - -const Button = styled.div` - display: flex; - flex-direction: row; - align-items: center; - margin-left: 10px; - justify-content: space-between; - font-size: 13px; - cursor: pointer; - font-family: "Work Sans", sans-serif; - border-radius: 5px; - font-weight: 500; - 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; - } -`; - -const HidableElement = styled.div<{ show: boolean }>` - display: ${(props) => (props.show ? "unset" : "none")}; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx deleted file mode 100644 index bb6adfcb4f..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx +++ /dev/null @@ -1,617 +0,0 @@ -import DynamicLink from "components/DynamicLink"; -import Heading from "components/form-components/Heading"; -import RepoList from "components/repo-selector/RepoList"; -import SaveButton from "components/SaveButton"; -import DocsHelper from "components/DocsHelper"; -import { ActionConfigType, GithubActionConfigType } from "shared/types"; -import React, { useContext, useEffect, useState } from "react"; -import styled from "styled-components"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { useRouting } from "shared/routing"; -import { Environment } from "./types"; -import DashboardHeader from "../DashboardHeader"; -import PullRequestIcon from "assets/pull_request_icon.svg"; -import CheckboxRow from "components/form-components/CheckboxRow"; -import BranchFilterSelector from "./components/BranchFilterSelector"; -import Helper from "components/form-components/Helper"; -import NamespaceLabels, { KeyValueType } from "./components/NamespaceLabels"; -import AnimateHeight from "react-animate-height"; -import Text from "components/porter/Text"; -import Spacer from "components/porter/Spacer"; -import ConnectNewRepoActionConfEditor from "./ConnectNewRepoActionConfEditor"; -import VerticalSteps from "components/porter/VerticalSteps"; -import Back from "components/porter/Back"; - -const ConnectNewRepo: React.FC = () => { - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - const [repo, setRepo] = useState(null); - const [enableAutomaticDeployments, setEnableAutomaticDeployments] = useState( - false - ); - const [filteredRepos, setFilteredRepos] = useState([]); - - const [status, setStatus] = useState(null); - const { pushFiltered } = useRouting(); - const [showSettings, setShowSettings] = useState(false); - const [currentStep, setCurrentStep] = useState(0); - - // NOTE: git_repo_id is a misnomer as this actually refers to the github app's installation id. - const [actionConfig, setActionConfig] = useState({ - git_repo: null, - image_repo_uri: null, - git_branch: null, - git_repo_id: 0, - kind: "github", - }); - - // Branch selector data - const [baseBranches, setBaseBranches] = useState([]); - const [deployBranches, setDeployBranches] = useState([]); - const [availableBranches, setAvailableBranches] = useState([]); - const [isLoadingBranches, setIsLoadingBranches] = useState(false); - - // Disable new comments data - const [isNewCommentsDisabled, setIsNewCommentsDisabled] = useState(false); - - // Namespace labels - const [namespaceLabels, setNamespaceLabels] = useState([]); - - useEffect(() => { - api - .listEnvironments( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - } - ) - .then(({ data }) => { - // console.log("github account", data); - - if (!Array.isArray(data)) { - throw Error("Data is not an array"); - } - const newFilteredRepos = data.map((env) => { - return `${env.git_repo_owner}/${env.git_repo_name}`; - }); - setFilteredRepos(newFilteredRepos || []); - }) - .catch(() => {}); - }, []); - - useEffect(() => { - if (!actionConfig.git_repo || !actionConfig.git_repo_id) { - return; - } - - let isSubscribed = true; - const repoName = actionConfig.git_repo.split("/")[1]; - const repoOwner = actionConfig.git_repo.split("/")[0]; - setIsLoadingBranches(true); - api - .getBranches( - "", - {}, - { - project_id: currentProject.id, - kind: "github", - name: repoName, - owner: repoOwner, - git_repo_id: actionConfig.git_repo_id, - } - ) - .then(({ data }) => { - if (isSubscribed) { - setIsLoadingBranches(false); - setAvailableBranches(data); - } - }) - .catch(() => { - if (isSubscribed) { - setIsLoadingBranches(false); - setCurrentError( - "Couldn't load branches for this repository, using all branches by default." - ); - } - }); - }, [actionConfig]); - - const addRepo = () => { - let [owner, repoName] = actionConfig.git_repo.split("/"); - const labels: Record = {}; - - setStatus("loading"); - - namespaceLabels - .filter((elem: KeyValueType, index: number, self: KeyValueType[]) => { - // remove any collisions that are duplicates - let numCollisions = self.reduce((n, _elem: KeyValueType) => { - return n + (_elem.key === elem.key ? 1 : 0); - }, 0); - - if (numCollisions == 1) { - return true; - } else { - return ( - index === - self.findIndex((_elem: KeyValueType) => _elem.key === elem.key) - ); - } - }) - .forEach((elem: KeyValueType) => { - if (elem.key !== "" && elem.value !== "") { - labels[elem.key] = elem.value; - } - }); - - api - .createEnvironment( - "", - { - name: `preview`, - mode: enableAutomaticDeployments ? "auto" : "manual", - disable_new_comments: isNewCommentsDisabled, - git_repo_branches: baseBranches, - namespace_labels: labels, - git_deploy_branches: deployBranches, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - git_installation_id: actionConfig.git_repo_id, - git_repo_name: repoName, - git_repo_owner: owner, - } - ) - .then(() => { - setStatus("successful"); - pushFiltered(`/preview-environments`, []); - }) - .catch((err) => { - err = JSON.stringify(err); - setStatus("error"); - setCurrentError(err); - }); - }; - - if (currentProject?.simplified_view_enabled) { - return ( - -
- - - - Choose a repository - { - setActionConfig( - (currentActionConfig: ActionConfigType) => ({ - ...currentActionConfig, - ...actionConfig, - }) - ); - - if (!!actionConfig.git_repo) { - setCurrentStep((prev) => { - if (prev > 0) { - return prev; - } - - return prev + 1; - }); - } - }} - /> - - Note: you will need to add a{" "} - porter.yaml file to create a preview - environment. - - - , - - <> - Automatic pull request deployments - - If you enable this option, the new pull requests will be - automatically deployed. - - - { - setEnableAutomaticDeployments( - !enableAutomaticDeployments - ); - }} - wrapperStyles={{ - disableMargin: true, - }} - /> - - , - ]} - /> - - - -
-
- ); - } - - return ( - <> - - - - - - <div - style={{ - display: "flex", - alignItems: "center", - }} - > - Enable Preview Environments on a Repository - <DocsHelper - tooltipText="Learn more about preview environments" - link="https://docs.porter.run/preview-environments/overview/" - placement="top-end" - /> - </div> - - - - Select a Repository -
- {/* { - setActionConfig(a); - setRepo(a.git_repo); - }} - readOnly={false} - filteredRepos={filteredRepos} - /> */} - { - setActionConfig((currentActionConfig: ActionConfigType) => ({ - ...currentActionConfig, - ...actionConfig, - })); - }} - /> - - Note: you will need to add a porter.yaml file to - create a preview environment. - - - - {/* { - setShowSettings(!showSettings); - }} - > */} - { - setShowSettings(!showSettings); - }} - > - - arrow_drop_down - Configure Additonal settings - - - - - Deploy from branches - - {" "} - Choose the list of branches that you want to deploy changes from. - - - - Select allowed branches - - {" "} - If the pull request has a base branch included in this list, it will - be allowed to be deployed. -
- (Leave empty to allow all branches) -
- - Automatic pull request deployments - - If you enable this option, the new pull requests will be - automatically deployed. - - - - setEnableAutomaticDeployments(!enableAutomaticDeployments) - } - wrapperStyles={{ - disableMargin: true, - }} - /> - - - Disable new comments for new deployments - - When enabled new comments will not be created for new deployments. - Instead the last comment will be updated. - - - setIsNewCommentsDisabled(!isNewCommentsDisabled)} - wrapperStyles={{ - disableMargin: true, - }} - /> - - - - Namespace labels - - Custom labels to be injected into the Kubernetes namespace created - for each deployment. - - { - let labels: KeyValueType[] = []; - x.forEach((entry) => { - labels.push({ key: entry.key, value: entry.value }); - }); - setNamespaceLabels(labels); - }} - /> -
-
- - - - - - ); -}; - -export default ConnectNewRepo; - -const CenterWrapper = styled.div` - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -`; - -const Div = styled.div` - width: 100%; - max-width: 900px; -`; - -const FlexWrap = styled.div` - display: flex; - align-items: center; -`; - -const Button = styled(DynamicLink)` - 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 ActionContainer = styled.div` - display: flex; - justify-content: flex-end; - margin-top: 20px; - padding-bottom: 100px; -`; - -const CodeBlock = styled.span` - display: inline-block; - background-color: #1b1d26; - color: white; - border-radius: 8px; - font-family: monospace; - padding: 2px 3px; - user-select: text; - margin: 0 6px; -`; - -const HelperContainer = styled.div` - margin-top: 24px; - width: 555px; - display: flex; - justify-content: start; - align-items: center; - color: #aaaabb; - line-height: 1.6em; - font-size: 13px; -`; - -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 CheckboxWrapper = styled.div` - display: flex; - align-items: center; - margin-top: 20px; -`; -const StyledSourceBox = styled.div` - width: 100%; - color: #ffffff; - padding: 25px 35px 25px; - position: relative; - font-size: 13px; - border-radius: 5px; - background: ${(props) => props.theme.fg}; - border: 1px solid #494b4f; - border-top: 0px; - border-top-left-radius: 0px; - border-top-right-radius: 0px; -`; -const StyledAdvancedBuildSettings = styled.div` - color: ${({ showSettings }) => (showSettings ? "white" : "#aaaabb")}; - background: ${({ theme }) => theme.fg}; - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - color: white; - } - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 15px; - border-radius: 5px; - height: 40px; - font-size: 13px; - width: 100%; - padding-left: 10px; - cursor: pointer; - border-bottom-left-radius: ${({ showSettings }) => showSettings && "0px"}; - border-bottom-right-radius: ${({ showSettings }) => showSettings && "0px"}; - - .dropdown { - margin-right: 8px; - font-size: 20px; - cursor: pointer; - border-radius: 20px; - transform: ${(props: { showSettings: boolean; isCurrent: boolean }) => - props.showSettings ? "" : "rotate(-90deg)"}; - } -`; -const AdvancedBuildTitle = styled.div` - display: flex; - align-items: center; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx deleted file mode 100644 index 1d61c0eb10..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from "react"; -import styled from "styled-components"; - -import { ActionConfigType } from "shared/types"; - -import Input from "components/porter/Input"; -import RepoList from "components/repo-selector/RepoList"; - -type Props = { - actionConfig: ActionConfigType | null; - setActionConfig: (x: ActionConfigType) => void; - setBranch?: (x: string) => void; - setDockerfilePath?: (x: string) => void; - setFolderPath?: (x: string) => void; - setBuildView?: (x: string) => void; - setPorterYamlPath?: (x: string) => void; -}; - -const defaultActionConfig: ActionConfigType = { - git_repo: null, - image_repo_uri: null, - git_branch: null, - git_repo_id: 0, - kind: "github", -}; - -const ConnectNewRepoActionConfEditor: React.FC = ({ - actionConfig, - setBranch, - setActionConfig, - setFolderPath, - setDockerfilePath, - setBuildView, - setPorterYamlPath, -}) => { - if (!actionConfig.git_repo) { - return ( - - setActionConfig(x)} - readOnly={false} - /> - - ); - } else { - return ( - <> - {}} - placeholder="" - /> - { - setActionConfig({ ...defaultActionConfig }); - setBranch ? setBranch("") : null; - setFolderPath ? setFolderPath("") : null; - setDockerfilePath ? setDockerfilePath("") : null; - setBuildView ? setBuildView("buildpacks") : null; - setPorterYamlPath && setPorterYamlPath(""); - }} - > - keyboard_backspace - Select repo - - - ); - } -}; - -export default ConnectNewRepoActionConfEditor; - -const ExpandedWrapper = styled.div` - margin-top: 10px; - width: 100%; - border-radius: 3px; - border: 1px solid #ffffff44; - max-height: 275px; -`; - -const ExpandedWrapperAlt = styled(ExpandedWrapper)` - border: 0; -`; - -const BackButton = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 22px; - cursor: pointer; - font-size: 13px; - height: 35px; - padding: 5px 13px; - margin-bottom: -7px; - padding-right: 15px; - border: 1px solid #ffffff55; - border-radius: 100px; - width: ${(props: { width: string }) => props.width}; - color: white; - background: #ffffff11; - - :hover { - background: #ffffff22; - } - - > i { - color: white; - font-size: 16px; - margin-right: 6px; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/ActionButton.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/components/ActionButton.tsx deleted file mode 100644 index f6d3446ab0..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/ActionButton.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import styled, { css, keyframes } from "styled-components"; - -const Shake = keyframes` -10%, 90% { - transform: translate3d(-0.5px, 0, 0); -} - -20%, 80% { - transform: translate3d(1px, 0, 0); -} - -30%, 50%, 70% { - transform: translate3d(-2px, 0, 0); -} - -40%, 60% { - transform: translate3d(2px, 0, 0); -} -`; - -const ShakeAnimation = css` - animation: ${Shake} 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; - transform: translate3d(0, 0, 0); - backface-visibility: hidden; - perspective: 1000px; -`; - -export const ActionButton = styled.button` - font-size: 12px; - padding: 8px 10px; - margin-left: 10px; - border-radius: 5px; - color: #ffffff; - border: 1px solid - ${(props: { disabled: boolean; hasError: boolean }) => - props.hasError ? "#dd4b4b" : "#aaaabb"}; - display: flex; - align-items: center; - background: ${(props: { disabled: boolean; hasError: boolean }) => - props.disabled ? "#ffffff22" : "#ffffff08"}; - cursor: pointer; - min-height: 32px; - min-width: 220px; - :hover { - background: #ffffff22; - } - - ${(props: { disabled: boolean; hasError: boolean }) => { - if (props.hasError) { - return ShakeAnimation; - } - }} - - > i { - font-size: 14px; - margin-right: 8px; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx deleted file mode 100644 index 69320d6056..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import SearchSelector from "components/SearchSelector"; -import React, { useMemo } from "react"; -import styled from "styled-components"; - -const BranchFilterSelector = ({ - value, - options, - onChange, - showLoading, - multiSelect = true, -}: { - value: string[]; - options: string[]; - onChange: (value: string[]) => void; - showLoading?: boolean; - multiSelect?: boolean; -}) => { - const filteredBranches = useMemo(() => { - if (!options.length) { - return []; - } - - if (value.find((branch) => branch === "")) { - return options; - } - - return options.filter((branch) => !value.includes(branch)); - }, [options, value]); - - const handleAddBranch = (branch: string) => { - if (!multiSelect) { - onChange([branch]); - return; - } - - const newSelectedBranches = [...value, branch]; - - onChange(newSelectedBranches); - }; - - const handleDeleteBranch = (branch: string) => { - const newSelectedBranches = value.filter( - (selectedBranch) => selectedBranch !== branch - ); - - onChange(newSelectedBranches); - }; - - const placeholder = options?.length - ? "Find or add a branch..." - : "No branches found for current repository."; - - return ( - <> - handleAddBranch(newBranch)} - getOptionLabel={(option) => option} - placeholder={placeholder} - showLoading={showLoading} - /> - {/* List selected branches */} - - - {value.map((branch) => ( - -
{branch}
- handleDeleteBranch(branch)}> - x - -
- ))} -
- - ); -}; - -export default BranchFilterSelector; - -const BranchRowList = styled.div` - margin-block: 15px; - max-height: 200px; - overflow-y: auto; -`; - -const BranchRow = styled.div` - padding-inline: 8px; - gap: 10px; - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-between; -`; - -const RemoveBranchButton = styled.div` - cursor: pointer; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx deleted file mode 100644 index 9e532402e1..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import styled from "styled-components"; -import pr_icon from "assets/pull_request_icon.svg"; -import { Link } from "react-router-dom"; -import DynamicLink from "components/DynamicLink"; -import Loading from "components/Loading"; - -type Props = { - setIsReady: (status: boolean) => void; -}; - -// TODO: Billing is still not capable to show if a user can use or not PR environments, add that instead of "hasBillingEnabled" -const ButtonEnablePREnvironments = ({ setIsReady }: Props) => { - // const { hasBillingEnabled } = useContext(Context); - const [isLoading, setIsLoading] = useState(true); - const [hasGHAccountConnected, setHasGHAccountConnected] = useState(false); - let hasBillingEnabled = true; - - const getAccounts = async () => { - setIsLoading(true); - try { - const res = await api.getGithubAccounts("", {}, {}); - if (res.status !== 200) { - throw new Error("Not authorized"); - } - - return res.data; - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - let isSubscribed = true; - getAccounts().then((accountsData) => { - if (isSubscribed) { - if (!accountsData) { - setHasGHAccountConnected(false); - } else { - setHasGHAccountConnected(true); - } - } - }); - return () => { - isSubscribed = false; - }; - }, []); - - useEffect(() => { - setIsReady(!isLoading); - }, [isLoading]); - - const getButtonProps = () => { - const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`; - - const encoded_redirect_uri = encodeURIComponent(url); - - const backendUrl = `${window.location.protocol}//${window.location.host}`; - - if (!hasGHAccountConnected) { - return { - to: `${backendUrl}/api/integrations/github-app/install?redirect_uri=${encoded_redirect_uri}`, - target: "_self", - }; - } - - if (!hasBillingEnabled) { - return { - to: { - pathname: "/project-settings", - search: "?selected_tab=billing", - }, - }; - } - return { - to: "/preview-environments/connect-repo", - }; - }; - - if (isLoading) { - return ( - - - - ); - } - - if (!hasGHAccountConnected) { - return ( - <> - - - - - ); - } - - return ( - <> - - - - - ); -}; - -export default ButtonEnablePREnvironments; - -const Button = styled(DynamicLink)` - 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: 5px; - color: white; - height: 30px; - padding: 0px 8px; - padding-bottom: 1px; - margin-right: 10px; - font-weight: 500; - padding-right: 15px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - box-shadow: 0 5px 8px 0px #00000010; - 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"}; - } - - img { - margin-left: 2px; - margin-right: 5px; - width: 18px; - height: 18px; - } - - > 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; - } -`; - -const Container = styled.div` -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/NamespaceLabels.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/components/NamespaceLabels.tsx deleted file mode 100644 index f406bc9943..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/NamespaceLabels.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, { useEffect } from "react"; -import styled from "styled-components"; - -export type KeyValueType = { - key: string; - value: string; -}; - -type PropsType = { - values: KeyValueType[]; - setValues: (x: KeyValueType[]) => void; -}; - -const NamespaceLabels = ({ values, setValues }: PropsType) => { - useEffect(() => { - if (!values) { - setValues([]); - } - }, [values]); - - if (!values) { - return null; - } - - return ( - <> - - {!!values?.length && - values.map((entry: KeyValueType, i: number) => { - return ( - - { - let _values = values; - _values[i].key = e.target.value; - setValues(_values); - }} - /> - - { - let _values = values; - _values[i].value = e.target.value; - setValues(_values); - }} - /> - { - let _values = values; - _values = _values.filter((val) => val.key !== entry.key); - setValues(_values); - }} - > - cancel - - - ); - })} - - { - let _values = values; - _values.push({ - key: "", - value: "", - }); - setValues(_values); - }} - > - add Add Row - - - - - - ); -}; - -export default NamespaceLabels; - -const Spacer = styled.div` - width: 10px; - height: 20px; -`; - -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 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 InputWrapper = styled.div` - display: flex; - align-items: center; - margin-top: 5px; -`; - -const Input = styled.input` - outline: none; - border: none; - margin-bottom: 5px; - font-size: 13px; - background: #ffffff11; - border: 1px solid #ffffff55; - border-radius: 3px; - width: ${(props: { disabled?: boolean; width: string }) => - props.width ? props.width : "270px"}; - color: ${(props: { disabled?: boolean; width: string }) => - props.disabled ? "#ffffff44" : "white"}; - padding: 5px 10px; - height: 35px; -`; - -const StyledInputArray = styled.div` - margin-bottom: 15px; - margin-top: 22px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/PorterYAMLErrorsModal.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/components/PorterYAMLErrorsModal.tsx deleted file mode 100644 index 08e5908119..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/PorterYAMLErrorsModal.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import TitleSection from "components/TitleSection"; -import danger from "assets/danger.svg"; -import info from "assets/info.svg"; -import Modal from "main/home/modals/Modal"; - -interface PorterYAMLErrorsModalProps { - errors: string[]; - onClose: (...args: any[]) => void; - repo: string; - branch?: string; -} - -const PorterYAMLErrorsModal = ({ - errors, - onClose, - repo, - branch, -}: PorterYAMLErrorsModalProps) => { - if (!errors.length) { - return null; - } - - return ( - onClose()} height="auto"> - - porter.yaml - - - - Repo: - {repo} - - {branch ? ( - - Branch: - {branch} - - ) : null} - - - {errors.map((el) => { - return ( -
- {"- "} - {el} -
- ); - })} -
-
- ); -}; - -const Text = styled.div` - font-weight: 500; - font-size: 18px; - z-index: 999; -`; - -const InfoTab = styled.div` - display: flex; - align-items: center; - opacity: 50%; - font-size: 13px; - margin-right: 15px; - justify-content: center; - - > img { - width: 13px; - margin-right: 7px; - } -`; - -const InfoRow = styled.div` - display: flex; - align-items: center; - justify-content: flex-start; - margin-bottom: 12px; -`; - -const Bold = styled.div` - font-weight: 500; - margin-right: 5px; -`; - -const Message = styled.div` - padding: 20px; - background: #26292e; - border-radius: 5px; - line-height: 1.5em; - border: 1px solid #aaaabb33; - font-size: 13px; - margin-top: 40px; -`; - -export default PorterYAMLErrorsModal; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx deleted file mode 100644 index f9ae777a77..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useEffect, useState } from "react"; -import styled from "styled-components"; -import DashboardHeader from "../../DashboardHeader"; -import PullRequestIcon from "assets/pull_request_icon.svg"; -import api from "shared/api"; -import Banner from "components/porter/Banner"; -import Spacer from "components/porter/Spacer"; - -export const PreviewEnvironmentsHeader = () => { - const [githubStatus, setGithubStatus] = useState( - "no active incidents" - ); - - useEffect(() => { - console.log("clearing cache") - navigator.serviceWorker.getRegistrations().then((registrations) => { - registrations.forEach((registration) => { - registration.unregister(); - }); - }); - caches.keys().then((keyList) => { - return Promise.all( - keyList.map((key) => { - return caches.delete(key); - }) - ); - }); - - api - .getGithubStatus("", {}, {}) - .then(({ data }) => { - setGithubStatus(data); - }) - .catch((err) => { - console.error(err); - }); - }, []); - - return ( - <> - - {githubStatus != "no active incidents" ? ( - <> - - GitHub has an ongoing incident. - - View details - - - - - ) : null} - - ); -}; - -const StyledLink = styled.a` - text-decoration: underline; - margin-left: 7px; -`; - -const AlertCard = styled.div` - transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - border-radius: 4px; - box-shadow: none; - font-weight: 400; - font-size: 0.875rem; - line-height: 1.43; - letter-spacing: 0.01071em; - border: 1px solid rgb(229, 115, 115); - display: flex; - padding: 6px 16px; - color: rgb(244, 199, 199); - margin-top: 20px; - position: relative; - margin-bottom: 20px; -`; - -const AlertCardIcon = styled.span` - color: rgb(239, 83, 80); - margin-right: 12px; - padding: 7px 0px; - display: flex; - font-size: 22px; - opacity: 0.9; -`; - -const AlertCardTitle = styled.div` - margin: -2px 0px 0.35em; - font-size: 1rem; - line-height: 1.5; - letter-spacing: 0.00938em; - font-weight: 500; -`; - -const AlertCardContent = styled.div` - padding: 8px 0px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/styled.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/components/styled.tsx deleted file mode 100644 index 7a66b667a8..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/components/styled.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import styled from "styled-components"; -import DynamicLink from "components/DynamicLink"; -import React, { useState } from "react"; - -export const EllipsisTextWrapper: React.FC< - { tooltipText?: string } & React.HTMLAttributes -> = ({ children, tooltipText, className, ...divProps }) => { - const [showTooltip, setShowTooltip] = useState(false); - return ( - setShowTooltip(true)} - onMouseOut={() => setShowTooltip(false)} - > - {children} - {tooltipText && showTooltip ? {tooltipText} : null} - - ); -}; - -export const Tooltip = styled.div` - position: absolute; - left: -20px; - top: 10px; - min-height: 18px; - max-width: calc(700px); - padding: 5px 7px; - background: #272731; - z-index: 999; - color: white; - 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 StyledTooltipWrapper = styled.div` - position: relative; - overflow: visible; -`; - -export const StyledEllipsisTextWrapper = styled.span` - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - position: relative; - max-width: 350px; -`; - -export const RepoLink = styled(DynamicLink)` - height: 22px; - border-radius: 50px; - margin-left: 6px; - display: flex; - font-size: 12px; - cursor: pointer; - color: #a7a6bb; - align-items: center; - justify-content: center; - :hover { - color: #ffffff; - > i { - color: #ffffff; - } - } - - > i { - margin-right: 5px; - color: #a7a6bb; - font-size: 16px; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx deleted file mode 100644 index 30a5ff5820..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx +++ /dev/null @@ -1,674 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import styled, { keyframes } from "styled-components"; -import { DeploymentStatus, PRDeployment } from "../types"; -import pr_icon from "assets/pull_request_icon.svg"; -import DynamicLink from "components/DynamicLink"; -import { capitalize, readableDate } from "shared/string_utils"; -import api from "shared/api"; -import { useContext } from "react"; -import { Context } from "shared/Context"; -import Loading from "components/Loading"; -import { ActionButton } from "../components/ActionButton"; -import { EllipsisTextWrapper, RepoLink } from "../components/styled"; -import MaterialTooltip from "@material-ui/core/Tooltip"; -import _ from "lodash"; - -interface DeploymentCardAction { - active: boolean; - label: string; - action: (...args: any) => void; -} - -interface DeploymentCardActionsDropdownProps { - options: DeploymentCardAction[]; -} - -const DeploymentCardActionsDropdown = ({ - options, -}: DeploymentCardActionsDropdownProps) => { - const wrapperRef = useRef(); - const [expanded, setExpanded] = useState(false); - - const handleOutsideClick = (event: any) => { - if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { - setExpanded(false); - } - }; - - useEffect(() => { - document.addEventListener("mousedown", handleOutsideClick.bind(this)); - - return () => { - document.removeEventListener("mousedown", handleOutsideClick.bind(this)); - }; - }, []); - - return ( -
- { - e.preventDefault(); - e.stopPropagation(); - setExpanded((expanded) => !expanded); - }} - > - more_vert - - - - {options.length ? ( - - {options - .filter((option) => option.active) - .map(({ label, action }, idx) => { - return ( - - {label} - - ); - })} - - ) : null} - - -
- ); -}; - -const DeploymentCard: React.FC<{ - deployment: PRDeployment; - onDelete: () => void; - onReEnable: () => void; - onReRun: () => void; -}> = ({ deployment, onDelete, onReEnable, onReRun }) => { - const { - setCurrentOverlay, - currentProject, - currentCluster, - setCurrentError, - } = useContext(Context); - const [isDeleting, setIsDeleting] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [hasErrorOnReEnabling, setHasErrorOnReEnabling] = useState(false); - const [showMergeInfoTooltip, setShowMergeInfoTooltip] = useState(false); - const [isReRunningWorkflow, setIsReRunningWorkflow] = useState(false); - const [hasErrorOnReRun, setHasErrorOnReRun] = useState(false); - - const deleteDeployment = () => { - setIsDeleting(true); - - api - .deletePRDeployment( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - deployment_id: deployment.id, - } - ) - .then(() => { - setIsDeleting(false); - onDelete(); - setCurrentOverlay(null); - }); - }; - - const reEnablePreviewEnvironment = async () => { - setIsLoading(true); - try { - await api.reenablePreviewEnvironmentDeployment( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - deployment_id: deployment.id, - } - ); - - setIsLoading(false); - onReEnable(); - } catch (err) { - setHasErrorOnReEnabling(true); - setIsLoading(false); - setCurrentError(err?.response?.data?.error || err); - setTimeout(() => { - setHasErrorOnReEnabling(false); - }, 500); - } - }; - - const reRunWorkflow = async () => { - setIsReRunningWorkflow(true); - try { - await api.triggerPreviewEnvWorkflow( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - deployment_id: deployment.id, - } - ); - setIsReRunningWorkflow(false); - onReEnable(); - } catch (error) { - setHasErrorOnReRun(true); - setIsReRunningWorkflow(false); - setCurrentError(error); - setTimeout(() => { - setHasErrorOnReRun(false); - }, 500); - } - }; - - const DeploymentCardActions = [ - { - active: !!deployment.last_workflow_run_url, - label: "View last workflow", - action: (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - window.open(deployment.last_workflow_run_url, "_blank"); - }, - }, - { - active: true, - label: "Delete", - action: (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - deleteDeployment(); - }, - }, - ]; - - console.error( - deployment, - deployment.gh_pr_branch_from, - deployment.gh_pr_branch_into, - deployment.gh_pr_branch_from === deployment.gh_pr_branch_into - ); - - return ( - - - - - - { - e.preventDefault(); - e.stopPropagation(); - window.open( - `https://github.com/${deployment.gh_repo_owner}/${deployment.gh_repo_name}/pull/${deployment.pull_request_id}`, - "_blank" - ); - }} - to={`https://github.com/${deployment.gh_repo_owner}/${deployment.gh_repo_name}/pull/${deployment.pull_request_id}`} - target="_blank" - > - {deployment.gh_pr_name} - - - {deployment.gh_pr_branch_from && - deployment.gh_pr_branch_into && - deployment.gh_pr_branch_from !== deployment.gh_pr_branch_into ? ( - - setShowMergeInfoTooltip(true)} - onMouseOut={() => setShowMergeInfoTooltip(false)} - > - {deployment.gh_pr_branch_from} - arrow_forward - {deployment.gh_pr_branch_into} - - {showMergeInfoTooltip && ( - - {deployment.gh_pr_branch_from} {"->"}{" "} - {deployment.gh_pr_branch_into} - - )} - - ) : null} - - - - - - - {capitalize(deployment.status)} - - - - - - - Last updated {readableDate(deployment.updated_at)} - - - - - - - {!isDeleting ? ( - <> - {deployment.status === DeploymentStatus.Failed || - deployment.status === DeploymentStatus.TimedOut ? ( - <> - - reRunWorkflow()} - disabled={isReRunningWorkflow} - hasError={hasErrorOnReRun} - > - loop - - - - ) : null} - - {deployment.subdomain && - deployment.status === DeploymentStatus.Created ? ( - { - e.preventDefault(); - e.stopPropagation(); - - window.open(deployment.subdomain, "_blank"); - }} - key={deployment.subdomain} - > - open_in_new - View Live - - ) : null} - - {/* */} - - ) : ( - - Deleting - - - - - )} - - - ); -}; - -export default DeploymentCard; - -const ReRunButton = styled(ActionButton)` - min-width: unset; - - > i { - margin-right: unset; - } -`; - -const SepDot = styled.div` - color: #aaaabb66; -`; - -const DeleteMessage = styled.div` - display: flex; - align-items: flex-end; - justify-content: center; -`; - -export const DissapearAnimation = keyframes` - 0% { - background-color: #ffffff; - } - - 25% { - background-color: #ffffff50; - } - - 50% { - background-color: none; - } - - 75% { - background-color: #ffffff50; - } - - 100% { - background-color: #ffffff; - } -`; - -const Dot = styled.div` - background-color: black; - border-radius: 50%; - width: 5px; - height: 5px; - margin: 0 0.25rem; - margin-bottom: 2px; - //Animation - animation: ${DissapearAnimation} 0.5s linear infinite; - animation-delay: ${(props: { delay: string }) => props.delay}; -`; - -const Flex = styled.div` - display: flex; - align-items: center; -`; - -const PRName = styled.div` - font-family: "Work Sans", sans-serif; - font-weight: 500; - color: #ffffff; - display: flex; - font-size: 14px; - align-items: center; - margin-bottom: 10px; -`; - -const DeploymentCardWrapper = styled(DynamicLink)` - display: flex; - justify-content: space-between; - font-size: 13px; - height: 75px; - padding: 12px; - padding-left: 14px; - border-radius: 5px; - background: #26292e; - border: 1px solid #494b4f; - - animation: fadeIn 0.5s; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const DataContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: space-between; -`; - -const StatusContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: flex-start; - height: 100%; -`; - -const PRIcon = styled.img` - font-size: 20px; - height: 17px; - margin-right: 10px; - color: #aaaabb; - opacity: 50%; -`; - -const RowButton = styled.button` - white-space: nowrap; - font-size: 12px; - padding: 8px 10px; - margin-left: 10px; - border-radius: 5px; - color: #ffffff; - border: 1px solid #aaaabb; - display: flex; - align-items: center; - background: #ffffff08; - cursor: pointer; - :hover { - background: #ffffff22; - } - - > i { - font-size: 14px; - margin-right: 8px; - } -`; - -const Button = styled.div` - font-size: 12px; - padding: 8px 10px; - margin-left: 10px; - border-radius: 5px; - color: #ffffff; - border: 1px solid #aaaabb; - display: flex; - align-items: center; - background: #ffffff08; - cursor: pointer; - :hover { - background: #ffffff22; - } - - > i { - font-size: 14px; - margin-right: 8px; - } -`; - -const Status = styled.span` - font-size: 13px; - display: flex; - align-items: center; - min-height: 17px; - color: #a7a6bb; -`; - -const StatusDot = styled.div` - width: 8px; - height: 8px; - margin-right: 10px; - background: ${(props: { status: string }) => - props.status === "created" - ? "#4797ff" - : props.status === "failed" - ? "#ed5f85" - : props.status === "completed" - ? "#00d12a" - : "#f5cb42"}; - border-radius: 20px; - margin-left: 3px; -`; - -const DeploymentImageContainer = styled.div` - height: 20px; - font-size: 13px; - position: relative; - display: flex; - align-items: center; - font-weight: 400; - justify-content: center; - color: #ffffff66; - padding-left: 10px; -`; - -const Icon = styled.img` - width: 100%; -`; - -const DeploymentTypeIcon = styled(Icon)` - width: 20px; - margin-right: 10px; -`; - -const RepositoryName = styled.div` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 390px; - position: relative; - margin-right: 3px; -`; - -const Tooltip = styled.div` - position: absolute; - left: -20px; - top: 10px; - min-height: 18px; - max-width: calc(700px); - padding: 5px 7px; - background: #272731; - z-index: 999; - color: white; - 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 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-top: -1px; - margin-left: 10px; - display: flex; - align-items: center; - color: #aaaabb66; -`; - -const MergeInfoWrapper = styled.div` - display: flex; - align-items: center; - margin-right: 8px; - position: relative; -`; - -const MergeInfo = styled.div` - font-size: 13px; - margin-left: 14px; - align-items: center; - color: #aaaabb66; - white-space: nowrap; - display: flex; - align-items: center; - text-overflow: ellipsis; - overflow: hidden; - max-width: 300px; - - > i { - font-size: 16px; - margin: 0 2px; - } -`; - -const I = styled.i` - user-select: none; - margin-left: 15px; - color: #aaaabb; - cursor: pointer; - border-radius: 40px; - font-size: 18px; - width: 30px; - height: 30px; - display: flex; - justify-content: center; - align-items: center; - &:hover { - background: #26292e; - border: 1px solid #494b4f; - } -`; - -const ActionsDropdown = styled.div` - width: 150px; - border-radius: 3px; - z-index: 999; - overflow-y: auto; - background: #2f3135; - padding: 0; - border-radius: 5px; - border: 1px solid #aaaabb33; -`; - -const ActionsDropdownWrapper = styled.div<{ expanded: boolean }>` - display: ${(props) => (props.expanded ? "block" : "none")}; - position: absolute; - right: calc(-100%); - z-index: 1; - top: calc(100% + 5px); -`; - -const ActionsScrollableWrapper = styled.div` - overflow-y: auto; - max-height: 350px; -`; - -const ActionsRow = styled.div<{ isLast: boolean; selected?: boolean }>` - width: 100%; - height: 35px; - padding-left: 10px; - display: flex; - cursor: pointer; - align-items: center; - font-size: 13px; - background: ${(props) => (props.selected ? "#ffffff11" : "")}; - - :hover { - background: #ffffff18; - } -`; - -const ActionsRowText = styled.div` - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - word-break: anywhere; - margin-right: 10px; - color: white; -`; - -const StyledLink = styled(DynamicLink)` - color: white; - :hover { - text-decoration: underline; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx deleted file mode 100644 index 03378e0647..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx +++ /dev/null @@ -1,767 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import styled from "styled-components"; -import TitleSection from "components/TitleSection"; -import pr_icon from "assets/pull_request_icon.svg"; -import { useRouteMatch, useLocation } from "react-router"; -import DynamicLink from "components/DynamicLink"; -import { DeploymentStatus, PRDeployment } from "../types"; -import PullRequestIcon from "assets/pull_request_icon.svg"; -import Loading from "components/Loading"; -import { Context } from "shared/Context"; -import api from "shared/api"; -import ChartList from "../../chart/ChartList"; -import github from "assets/github-white.png"; -import { integrationList } from "shared/common"; -import { capitalize } from "shared/string_utils"; -import Banner from "components/porter/Banner"; -import Modal from "main/home/modals/Modal"; -import { validatePorterYAML } from "../utils"; -import Placeholder from "components/Placeholder"; -import GithubIcon from "assets/GithubIcon"; -import Dropdown from "components/Dropdown"; -import { useHistory } from "react-router-dom"; -import PreviewEnvDeleted from "./PreviewEnvDeleted"; -import Button from "components/porter/Button"; - -const DeploymentDetail = () => { - const [showDropdown, setShowDropdown] = useState(false); - const { params } = useRouteMatch<{ id: string }>(); - const context = useContext(Context); - const [prDeployment, setPRDeployment] = useState(null); - const [environmentId, setEnvironmentId] = useState(""); - const [showRepoTooltip, setShowRepoTooltip] = useState(false); - const [porterYAMLErrors, setPorterYAMLErrors] = useState([]); - const [expandedPorterYAMLErrors, setExpandedPorterYAMLErrors] = useState< - string[] - >([]); - const [ - expandedLastDeploymentErrors, - setExpandedLastDeploymentErrors, - ] = useState([]); - - const { currentProject, currentCluster } = useContext(Context); - - const { search } = useLocation(); - let searchParams = new URLSearchParams(search); - const history = useHistory(); - - useEffect(() => { - let isSubscribed = true; - let environment_id = parseInt(searchParams.get("environment_id")); - setEnvironmentId(searchParams.get("environment_id")); - api - .getPRDeploymentByID( - "", - { - id: parseInt(params.id), - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - environment_id: environment_id, - } - ) - .then(({ data }) => { - if (!isSubscribed) { - return; - } - setPRDeployment(data); - }) - .catch((err) => { - console.error(err); - if (isSubscribed) { - setPRDeployment(null); - } - }); - }, [params]); - - useEffect(() => { - if (!prDeployment) { - return; - } - - const isSubscribed = true; - const environment_id = parseInt(searchParams.get("environment_id")); - - validatePorterYAML({ - projectID: currentProject.id, - clusterID: currentCluster.id, - environmentID: environment_id, - branch: prDeployment.gh_pr_branch_from, - }) - .then(({ data }) => { - if (!isSubscribed) { - return; - } - - setPorterYAMLErrors(data.errors ?? []); - }) - .catch((err) => { - console.error(err); - if (isSubscribed) { - setPorterYAMLErrors([]); - } - }); - }, [prDeployment]); - - if (!prDeployment) { - return ; - } - const repository = `${prDeployment.gh_repo_owner}/${prDeployment.gh_repo_name}`; - - const deleteDeployment = () => { - //setIsDeleting(true); - - api - .deletePRDeployment( - "", - {}, - { - cluster_id: currentCluster.id, - project_id: currentProject.id, - deployment_id: prDeployment.id, - } - ) - .then(() => { - //setIsDeleting(false); - history.push( - `/preview-environments/deployments/${currentProject.id}/${repository}` - ); // Navigate to deployments page - }); - }; - - if ( - !prDeployment.namespace && - ["creating", "updating"].includes(prDeployment.status) - ) { - return ( - <> - - - - Preview environments - - / - - - {repository} - - - - - - - window.open( - `https://github.com/${repository}/pull/${prDeployment.pull_request_id}`, - "_blank" - ) - } - > - {prDeployment.gh_pr_name} - - setShowDropdown(!showDropdown)} - style={{ cursor: "pointer" }} - > - settings - {showDropdown && ( - - - - )} - - - - {prDeployment.subdomain && ( - - link - {prDeployment.subdomain} - - )} - - - - - - {capitalize(prDeployment.status)} - - - - - { - setShowRepoTooltip(true); - }} - onMouseOut={() => { - setShowRepoTooltip(false); - }} - > - {repository} - - {showRepoTooltip && {repository}} - - - - - View PR - open_in_new - - - - - - - This preview deployment has not been created yet.{" "} - - View last workflow - - - - - - ); - } - - return ( - <> - {expandedPorterYAMLErrors.length > 0 && ( - setExpandedPorterYAMLErrors([])} - height="auto" - > - - {expandedPorterYAMLErrors.map((el) => { - return ( -
- {"- "} - {el} -
- ); - })} -
-
- )} - {expandedLastDeploymentErrors.length > 0 && ( - setExpandedLastDeploymentErrors([])} - height="auto" - > - - {expandedLastDeploymentErrors.map((el) => { - return ( -
- {"- "} - {el} -
- ); - })} -
-
- )} - - - - Preview environments - - / - - - {repository} - - - - - - window.open( - `https://github.com/${repository}/pull/${prDeployment.pull_request_id}`, - "_blank" - ) - } - > - {prDeployment.gh_pr_name} - - - {prDeployment.subdomain && ( - - link - {prDeployment.subdomain} - - )} - - Namespace {prDeployment.namespace} - - - - - - {capitalize(prDeployment.status)} - - - - - { - setShowRepoTooltip(true); - }} - onMouseOut={() => { - setShowRepoTooltip(false); - }} - > - {repository} - - {showRepoTooltip && {repository}} - - - {prDeployment.last_workflow_run_url ? ( - - View last workflow run - open_in_new - - ) : null} - - - - {porterYAMLErrors.length > 0 ? ( - - - Your porter.yaml file has errors. Please fix them before - deploying. - { - setExpandedPorterYAMLErrors(porterYAMLErrors); - }} - > - View details - - - - ) : null} - {prDeployment.last_errors.length > 0 ? ( - - - The previous deployment had errors. - { - setExpandedLastDeploymentErrors( - prDeployment.last_errors.split(",") - ); - }} - > - View details - - - - ) : null} - - - - - - - ); -}; - -export default DeploymentDetail; - -const ErrorBannerWrapper = styled.div` - margin-block: 20px; -`; - -const Slash = styled.div` - margin: 0 4px; - color: #aaaabb88; -`; - -const ArrowIcon = styled.img` - width: 15px; - margin-right: 8px; - opacity: 50%; -`; - -const LinkButton = styled.a` - text-decoration: underline; - margin-left: 7px; - cursor: pointer; -`; - -const Message = styled.div` - padding: 20px; - background: #26292e; - border-radius: 5px; - line-height: 1.5em; - border: 1px solid #aaaabb33; - font-size: 13px; - margin-top: 40px; -`; - -const BreadcrumbRow = styled.div` - width: 100%; - display: flex; - margin-top: -5px; - justify-content: flex-start; - align-items: center; - margin-bottom: 15px; -`; - -const Breadcrumb = styled(DynamicLink)` - color: #aaaabb88; - font-size: 13px; - display: flex; - align-items: center; - z-index: 999; - padding: 5px; - padding-right: 7px; - border-radius: 5px; - cursor: pointer; - :hover { - background: #ffffff11; - } -`; - -const Wrap = styled.div` - z-index: 999; -`; - -const Flex = styled.div` - display: flex; - align-items: center; - margin-top: 20px; -`; - -const GHALink = styled(DynamicLink)` - font-size: 13px; - font-weight: 400; - margin-left: 7px; - color: #aaaabb; - display: flex; - align-items: center; - - :hover { - color: white; - } - - > img { - height: 16px; - margin-right: 9px; - margin-left: 5px; - - :hover { - color: white; - } - } - - > span { - font-size: 17px; - margin-right: 9px; - margin-left: 5px; - text-decoration: none; - } - - > i { - margin-left: 7px; - font-size: 17px; - } -`; - -const ViewLastWorkflowLink = styled(DynamicLink)` - display: flex; - align-items: center; - text-decoration: underline; - margin-left: 7px; - color: currentcolor; - - :hover { - color: white; - } -`; - -const LineBreak = styled.div` - width: calc(100% - 0px); - height: 1px; - background: #494b4f; - margin-bottom: 20px; -`; - -const BackButton = styled(DynamicLink)` - 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 HeaderWrapper = styled.div` - position: relative; - padding-right: 40px; -`; - -const Dot = styled.div` - margin-left: 9px; - font-size: 14px; - color: #ffffff33; -`; - -const InfoWrapper = styled.div` - display: flex; - align-items: center; - margin-top: 20px; -`; - -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 Icon = styled.img` - width: 100%; -`; - -const GitIcon = styled.img` - width: 15px; - margin-right: 8px; -`; - -const StyledExpandedChart = styled.div` - width: 100%; - z-index: 0; - animation: fadeIn 0.3s; - animation-timing-function: ease-out; - animation-fill-mode: forwards; - display: flex; - flex-direction: column; - - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const Title = styled(TitleSection)` - font-size: 16px; - margin-top: 4px; -`; - -const Status = styled.span` - font-size: 13px; - display: flex; - align-items: center; - margin-left: 1px; - min-height: 17px; - color: #a7a6bb; -`; - -const StatusDot = styled.div` - width: 8px; - height: 8px; - background: ${(props: { status: string }) => - props.status === "created" - ? "#4797ff" - : props.status === "failed" - ? "#ed5f85" - : props.status === "completed" - ? "#00d12a" - : "#f5cb42"}; - border-radius: 20px; - margin-left: 3px; - margin-right: 15px; -`; - -const PRLink = styled(DynamicLink)` - margin-left: 0px; - display: flex; - margin-top: 1px; - align-items: center; - font-size: 13px; - > i { - font-size: 15px; - margin-right: 10px; - } -`; - -const ChartListWrapper = styled.div` - width: 100%; - margin: auto; - padding-bottom: 125px; -`; - -const LinkToActionsWrapper = styled.div` - width: 100%; - margin-top: 15px; - margin-bottom: 25px; - display: flex; - align-items: center; - justify-content: center; -`; - -const DeploymentImageContainer = styled.div` - height: 20px; - font-size: 13px; - position: relative; - display: flex; - margin-left: 5px; - margin-bottom: -3px; - align-items: center; - font-weight: 400; - justify-content: center; - color: #ffffff66; - padding-left: 5px; -`; - -const DeploymentTypeIcon = styled(Icon)` - width: 20px; - margin-right: 10px; -`; - -const RepositoryName = styled.div` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 390px; - position: relative; - margin-right: 3px; -`; - -const Tooltip = styled.div` - position: absolute; - left: -40px; - top: 28px; - min-height: 18px; - max-width: calc(700px); - padding: 5px 7px; - background: #272731; - z-index: 999; - color: white; - 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 I = styled.i` - font-size: 18px; - user-select: none; - margin-left: 15px; - color: #aaaabb; - margin-bottom: -3px; - cursor: pointer; - width: 30px; - border-radius: 40px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - :hover { - background: #26292e; - border: 1px solid #494b4f; - } -`; -const DeleteDropdown = styled.div` - position: absolute; - border-radius: 3px; - padding: 10px; - min-width: 150px; - z-index: 999; -`; - -const DeleteButton = styled.button` - background-color: #f44336; - color: white; - padding: 6px 12px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - - &:hover { - background-color: #d32f2f; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx deleted file mode 100644 index 799cffec2a..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx +++ /dev/null @@ -1,602 +0,0 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; -import _ from "lodash"; -import { useHistory, useLocation, useParams } from "react-router"; -import styled from "styled-components"; - -import DynamicLink from "components/DynamicLink"; -import Loading from "components/Loading"; -import Placeholder from "components/Placeholder"; -import Banner from "components/porter/Banner"; -import RadioFilter from "components/RadioFilter"; - -import api from "shared/api"; -import { Context } from "shared/Context"; -import { useRouting } from "shared/routing"; -import { search } from "shared/search"; -import filterOutline from "assets/filter-outline.svg"; -import pullRequestIcon from "assets/pull_request_icon.svg"; -import sort from "assets/sort.svg"; - -import DashboardHeader from "../../DashboardHeader"; -import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal"; -import { PorterYAMLErrors } from "../errors"; -import { deployments, pull_requests } from "../mocks"; -import { Environment, type PRDeployment, type PullRequest } from "../types"; -import { getPRDeploymentList, validatePorterYAML } from "../utils"; -import DeploymentCard from "./DeploymentCard"; - -const AvailableStatusFilters = [ - "all", - "creating", - "created", - "failed", - "timed_out", - "updating", -]; - -type AvailableStatusFiltersType = (typeof AvailableStatusFilters)[number]; - -const DeploymentList = () => { - const [sortOrder, setSortOrder] = useState("Newest"); - const [isLoading, setIsLoading] = useState(true); - const [hasError, setHasError] = useState(false); - const [deploymentList, setDeploymentList] = useState([]); - const [pullRequests, setPullRequests] = useState([]); - const [searchValue, setSearchValue] = useState(""); - const [newCommentsDisabled, setNewCommentsDisabled] = useState(false); - const [porterYAMLErrors, setPorterYAMLErrors] = useState([]); - const [expandedPorterYAMLErrors, setExpandedPorterYAMLErrors] = useState< - string[] - >([]); - - const [statusSelectorVal, setStatusSelectorVal] = - useState("all"); - - const { currentProject, currentCluster, setCurrentError } = - useContext(Context); - const { getQueryParam, pushQueryParams } = useRouting(); - const location = useLocation(); - const history = useHistory(); - const { environment_id, repo_name, repo_owner } = useParams<{ - environment_id: string; - repo_name: string; - repo_owner: string; - }>(); - - const selectedRepo = `${repo_owner}/${repo_name}`; - - const getEnvironment = async () => { - return await api.getEnvironment( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - environment_id: Number(environment_id), - } - ); - }; - - useEffect(() => { - const status_filter = getQueryParam("status_filter"); - - if (!AvailableStatusFilters.includes(status_filter)) { - pushQueryParams({}, ["status_filter"]); - return; - } - - if (status_filter !== statusSelectorVal) { - setStatusSelectorVal(status_filter); - } - }, [location.search, history]); - - useEffect(() => { - pushQueryParams({}, ["status_filter"]); - }, []); - - useEffect(() => { - let isSubscribed = true; - setIsLoading(true); - - Promise.allSettled([ - validatePorterYAML({ - projectID: currentProject.id, - clusterID: currentCluster.id, - environmentID: Number(environment_id), - }), - getPRDeploymentList({ - projectID: currentProject.id, - clusterID: currentCluster.id, - environmentID: Number(environment_id), - }), - getEnvironment(), - ]) - .then( - ([ - validatePorterYAMLResponse, - getDeploymentsResponse, - getEnvironmentResponse, - ]) => { - const deploymentList = - getDeploymentsResponse.status === "fulfilled" - ? getDeploymentsResponse.value.data - : {}; - const environmentList = - getEnvironmentResponse.status === "fulfilled" - ? getEnvironmentResponse.value.data - : {}; - const porterYAMLErrors = - validatePorterYAMLResponse.status === "fulfilled" - ? validatePorterYAMLResponse.value.data.errors - : []; - - if (!isSubscribed) { - return; - } - - setPorterYAMLErrors(porterYAMLErrors); - setDeploymentList(deploymentList.deployments ?? []); - setPullRequests(deploymentList.pull_requests || []); - - setNewCommentsDisabled( - environmentList.new_comments_disabled || false - ); - - setIsLoading(false); - } - ) - .catch((err) => { - setDeploymentList([]); - setCurrentError(err); - }); - - return () => { - isSubscribed = false; - }; - }, [currentCluster, currentProject, environment_id]); - - const handleRefresh = async () => { - setIsLoading(true); - try { - const { data } = await getPRDeploymentList({ - projectID: currentProject.id, - clusterID: currentCluster.id, - environmentID: Number(environment_id), - }); - setDeploymentList(data.deployments ?? []); - setPullRequests(data.pull_requests || []); - } catch (error) { - setHasError(true); - console.error(error); - } - setIsLoading(false); - }; - - const searchFilter = (value: string | number) => { - const val = String(value); - - return val.toLowerCase().includes(searchValue.toLowerCase()); - }; - - const filteredDeployments = useMemo(() => { - const filteredByStatus = deploymentList.filter((d) => { - if (["deleted", "inactive"].includes(d.status)) { - return false; - } - - if (statusSelectorVal === "all") { - return true; - } - - if (d.status === statusSelectorVal) { - return true; - } - - return false; - }); - - const filteredBySearch = search( - filteredByStatus, - searchValue, - { - threshold: 0.2, - distance: 50, - isCaseSensitive: false, - keys: ["gh_pr_name", "gh_repo_name", "gh_repo_owner"], - } - ); - - switch (sortOrder) { - case "Newest": - return _.sortBy(filteredBySearch, "updated_at").reverse(); - case "Oldest": - return _.sortBy(filteredBySearch, "updated_at"); - case "Alphabetical": - default: - return _.sortBy(filteredBySearch, "gh_pr_name"); - } - }, [statusSelectorVal, deploymentList, searchValue, sortOrder]); - - const filteredPullRequests = useMemo(() => { - if (statusSelectorVal !== "inactive") { - return []; - } - - return pullRequests.filter((pr) => { - return Object.values(pr).find(searchFilter) !== undefined; - }); - }, [pullRequests, statusSelectorVal, searchValue]); - - const renderDeploymentList = () => { - if (isLoading) { - return ( - - - - ); - } - - if (!deploymentList.length) { - return ( - - No preview developments have been found. Open a PR to create a new - preview environment. - - ); - } - - if (!filteredDeployments.length) { - return ( - - No preview developments have been found with the given filter. - - ); - } - - return ( - <> - {filteredDeployments.map((d: any) => { - return ( - - ); - })} - - ); - }; - - useEffect(() => { - pushQueryParams({ status_filter: statusSelectorVal }); - }, [statusSelectorVal]); - - return ( - <> - { - setExpandedPorterYAMLErrors([]); - }} - repo={selectedRepo} - /> - - - - - Preview environments - - - - - {selectedRepo} - - - settings - - - } - description={`Preview environments for the ${selectedRepo} repository.`} - disableLineBreak - capitalize={false} - /> - {/* {porterYAMLErrors.length > 0 ? ( - - - No porter.yaml file in the default branch. - { - setExpandedPorterYAMLErrors(porterYAMLErrors); - }} - > - Learn more - - - - ) : null} */} - - - - - search - { - setSearchValue(e.target.value); - }} - placeholder="Search" - /> - - - ({ - value: filter, - label: _.startCase(filter), - }))} - name="Status" - /> - - - - refresh - - - - add New preview deployment - - - - - {renderDeploymentList()} - - - ); -}; - -export default DeploymentList; - -const mockRequest = async () => - await new Promise((res) => { - setTimeout(() => { - res({ - data: { deployments, pull_requests }, - }); - }, 1000); - }); - -const LoadingWrapper = styled.div` - padding-top: 100px; -`; - -const I = styled.i` - font-size: 18px; - user-select: none; - margin-left: 15px; - color: #aaaabb; - margin-bottom: -3px; - cursor: pointer; - width: 30px; - border-radius: 40px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - :hover { - background: #26292e; - border: 1px solid #494b4f; - } -`; - -const StyledLink = styled(DynamicLink)` - color: white; - :hover { - text-decoration: underline; - } -`; - -const LinkButton = styled.a` - text-decoration: underline; - margin-left: 7px; - cursor: pointer; -`; - -const Message = styled.div` - padding: 20px; - background: #26292e; - border-radius: 5px; - line-height: 1.5em; - border: 1px solid #aaaabb33; - font-size: 13px; - margin-top: 40px; -`; - -const BreadcrumbRow = styled.div` - width: 100%; - margin-top: 5px; - display: flex; - justify-content: flex-start; -`; - -const ArrowIcon = styled.img` - width: 15px; - margin-right: 8px; - opacity: 50%; -`; - -const Wrap = styled.div` - z-index: 999; -`; - -const Breadcrumb = styled(DynamicLink)` - 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 Flex = styled.div` - display: flex; - align-items: center; -`; - -const RefreshButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - color: ${(props: { color: string }) => props.color}; - cursor: pointer; - border: none; - width: 30px; - height: 30px; - margin-right: 15px; - background: none; - border-radius: 50%; - margin-left: 10px; - > i { - font-size: 20px; - } - :hover { - background-color: rgb(97 98 102 / 44%); - color: white; - } -`; - -const Container = styled.div` - margin-top: 33px; - padding-bottom: 120px; -`; - -const EventsGrid = styled.div` - display: grid; - grid-row-gap: 20px; - grid-template-columns: 1; -`; - -const SearchInput = styled.input` - outline: none; - border: none; - font-size: 13px; - background: none; - width: 100%; - color: white; - height: 100%; -`; - -const SearchRow = styled.div` - display: flex; - align-items: center; - height: 30px; - margin-right: 10px; - background: #26292e; - border-radius: 5px; - border: 1px solid #aaaabb33; -`; - -const SearchRowWrapper = styled(SearchRow)` - border-radius: 5px; - width: 250px; -`; - -const SearchBarWrapper = styled.div` - display: flex; - flex: 1; - - > i { - color: #aaaabb; - padding-top: 1px; - margin-left: 8px; - font-size: 16px; - margin-right: 8px; - } -`; - -const FlexRow = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 10px; -`; - -const CreatePreviewEnvironmentButton = styled(DynamicLink)` - display: flex; - flex-direction: row; - align-items: center; - margin-left: 10px; - justify-content: space-between; - font-size: 13px; - cursor: pointer; - font-family: "Work Sans", sans-serif; - border-radius: 5px; - font-weight: 500; - 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; - } -`; - -const PorterYAMLBannerWrapper = styled.div` - margin-block: 15px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PreviewEnvDeleted.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PreviewEnvDeleted.tsx deleted file mode 100644 index c07d963a18..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PreviewEnvDeleted.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { ProjectType } from "shared/types"; -import { useHistory } from "react-router-dom"; -import Button from "components/porter/Button"; -import Text from "components/porter/Text"; -import Spacer from "components/porter/Spacer"; - -interface PreviewEnvDeletedProps { - repository?: string; - currentProject?: ProjectType; -} - -const PreviewEnvDeleted: React.FC = ({}) => { - const history = useHistory(); - - const handleBackButtonClick = () => { - history.push("/preview-environments/deployments/"); - }; - - return ( - - This preview environment has been deleted. - - - - ); -}; - -const DeletedContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - width: 100%; -`; - -const ClusterPlaceholder = styled.div` - padding: 25px; - border-radius: 5px; - background: ${(props) => props.theme.fg}; - border: 1px solid #494b4f; - padding-bottom: 35px; -`; -const BackButton = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 22px; - cursor: pointer; - font-size: 13px; - height: 35px; - padding: 5px 13px; - margin-bottom: -7px; - padding-right: 15px; - border: 1px solid #ffffff55; - border-radius: 100px; - width: ${(props: { width: string }) => props.width}; - color: white; - background: #ffffff11; - - :hover { - background: #ffffff22; - } - - > i { - color: white; - font-size: 16px; - margin-right: 6px; - } -`; -export default PreviewEnvDeleted; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx deleted file mode 100644 index 93e3b97447..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx +++ /dev/null @@ -1,538 +0,0 @@ -import React, { useContext, useMemo, useState } from "react"; -import styled from "styled-components"; -import { Context } from "shared/Context"; -import { Environment } from "../types"; -import Helper from "components/form-components/Helper"; -import api from "shared/api"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { validatePorterYAML } from "../utils"; -import Banner from "components/porter/Banner"; -import { useRouting } from "shared/routing"; -import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal"; -import Placeholder from "components/Placeholder"; -import _ from "lodash"; -import Loading from "components/Loading"; -import { EllipsisTextWrapper } from "../components/styled"; -import pr_icon from "assets/pull_request_icon.svg"; -import { search } from "shared/search"; -import RadioFilter from "components/RadioFilter"; -import sort from "assets/sort.svg"; -import Modal from "components/porter/Modal"; -import Text from "components/porter/Text"; -import Button from "components/porter/Button"; -import Spacer from "components/porter/Spacer"; - -interface Props { - environmentID: string; -} - -const CreateBranchEnvironment = ({ environmentID }: Props) => { - const queryClient = useQueryClient(); - const router = useRouting(); - const [searchValue, setSearchValue] = useState(""); - const [sortOrder, setSortOrder] = useState("Newest"); - const [loading, setLoading] = useState(false); - const [showErrorsModal, setShowErrorsModal] = useState(false); - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - - const { - data: environment, - isLoading: environmentLoading, - } = useQuery( - ["environment", currentProject.id, currentCluster.id, environmentID], - async () => { - const { data: environment } = await api.getEnvironment( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - environment_id: parseInt(environmentID), - } - ); - - return environment; - } - ); - - // Get all branches for the current environment - const { isLoading: branchesLoading, data: branches } = useQuery( - ["branches", currentProject.id, currentCluster.id, environment], - async () => { - try { - const res = await api.getBranches( - "", - {}, - { - project_id: currentProject.id, - kind: "github", - name: environment.git_repo_name, - owner: environment.git_repo_owner, - git_repo_id: environment.git_installation_id, - } - ); - return res.data ?? []; - } catch (err) { - setCurrentError( - "Couldn't load branches for this repository, please try again later." - ); - } - }, - { - enabled: !!environment, - } - ); - const [showCreatePreviewModal, setShowCreatePreviewModal] = useState( - false - ); - const environmentGitDeployBranches = environment?.git_deploy_branches ?? []; - const [selectedBranch, setSelectedBranch] = useState(null); - const [porterYAMLErrors, setPorterYAMLErrors] = useState([]); - - const handleRowItemClick = async (branch: string) => { - setSelectedBranch(branch); - setLoading(true); - setShowCreatePreviewModal(true); - - const res = await validatePorterYAML({ - projectID: currentProject.id, - clusterID: currentCluster.id, - environmentID: Number(environmentID), - branch, - }); - - setPorterYAMLErrors(res.data.errors ?? []); - - setLoading(false); - }; - - const handleRefresh = () => { - queryClient.invalidateQueries({ - queryKey: ["branches"], - }); - }; - - const filteredBranches = useMemo(() => { - const filteredBySearch = search(branches ?? [], searchValue, { - isCaseSensitive: false, - threshold: 0.2, // Adjust this value to fine-tune the matching - distance: 50, - }); - - switch (sortOrder) { - case "Alphabetical": - default: - return _.sortBy(filteredBySearch); - } - }, [branches, searchValue, sortOrder]); - const handleModalSubmit = () => { - updateDeployBranchesMutation.mutate(); - setShowCreatePreviewModal(false); - }; - const updateDeployBranchesMutation = useMutation({ - mutationFn: () => { - return api.updateEnvironment( - "token", - { - disable_new_comments: environment.new_comments_disabled, - ...environment, - git_deploy_branches: _.uniq([ - ...environmentGitDeployBranches, - selectedBranch, - ]), - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - environment_id: environment.id, - } - ); - }, - onError: (err) => { - setCurrentError(err as string); - }, - onSuccess: () => - router.push( - `/preview-environments/deployments/${environmentID}/${environment.git_repo_name}/${environment.git_repo_owner}?status_filter=all` - ), - }); - - if (branchesLoading || environmentLoading) { - return ( - <> -
- - - - - ); - } - - if (!branches?.length) { - return ( - <> -
- You do not have any branches. - - ); - } - - return ( - <> - - Select a branch to preview. Branches must contain a{" "} - porter.yaml file. - - - - - - search - { - setSelectedBranch(undefined); - setPorterYAMLErrors([]); - setSearchValue(e.target.value); - }} - placeholder="Search" - /> - - - - - - refresh - - - - -
- - {(filteredBranches ?? []).map((branch, i) => ( - handleRowItemClick(branch)} - isLast={i === filteredBranches.length - 1} - isSelected={branch === selectedBranch} - > - - - - {branch} - - - - ))} - - {showErrorsModal && selectedBranch ? ( - setShowErrorsModal(false)} - repo={environment.git_repo_name + "/" + environment.git_repo_owner} - branch={selectedBranch} - /> - ) : null} - {selectedBranch && porterYAMLErrors.length ? ( - - - We found some errors in the porter.yaml file in the  - {selectedBranch} branch.   - setShowErrorsModal(true)}> - Learn more - - - - ) : null} - - {showCreatePreviewModal && - selectedBranch && - porterYAMLErrors.length == 0 && ( - setShowCreatePreviewModal(false)} - > - <> - - Create Preview Deployment for branch: {selectedBranch}? - - - updateDeployBranchesMutation.mutate()} - disabled={ - updateDeployBranchesMutation.isLoading || - loading || - porterYAMLErrors.length > 0 || - !selectedBranch - } - > - {updateDeployBranchesMutation.isLoading - ? "Creating..." - : "Create Preview Deployment"} - - - - )} - {selectedBranch && porterYAMLErrors.length ? ( - - Please fix your porter.yaml file to continue.{" "} - { - e.preventDefault(); - e.stopPropagation(); - - if (!selectedBranch) { - return; - } - - handleRowItemClick(selectedBranch); - }} - > - Refresh - - - ) : null} - - - ); -}; - -export default CreateBranchEnvironment; - -const BranchList = styled.div` - border: 1px solid #494b4f; - border-radius: 5px; - overflow: hidden; - margin-top: 33px; -`; - -const BranchRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>` - width: 100%; - padding: 15px; - cursor: pointer; - background: ${(props) => (props.isSelected ? "#ffffff11" : "#26292e")}; - border-bottom: ${(props) => (props.isLast ? "" : "1px solid #494b4f")}; - :hover { - background: #ffffff11; - } -`; - -const SearchRowWrapper = styled.div` - display: flex; - align-items: center; - height: 30px; - margin-right: 10px; - background: #26292e; - border-radius: 5px; - border: 1px solid #aaaabb33; - border-radius: 5px; - width: 250px; -`; - -const SearchBarWrapper = styled.div` - display: flex; - flex: 1; - - > i { - color: #aaaabb; - padding-top: 1px; - margin-left: 8px; - font-size: 16px; - margin-right: 8px; - } -`; - -const BranchName = styled.div` - font-family: "Work Sans", sans-serif; - font-weight: 500; - color: #ffffff; - display: flex; - font-size: 14px; - align-items: center; - margin-bottom: 10px; -`; - -const Code = styled.span` - font-family: monospace; ; -`; - -const Flex = styled.div` - display: flex; - align-items: center; -`; - -const FlexRow = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 10px; -`; - -const DeploymentImageContainer = styled.div` - height: 20px; - font-size: 13px; - position: relative; - display: flex; - align-items: center; - font-weight: 400; - justify-content: center; - color: #ffffff66; - padding-left: 10px; -`; - -const LastDeployed = styled.div` - font-size: 13px; - margin-top: -1px; - margin-left: 10px; - display: flex; - align-items: center; - color: #aaaabb66; -`; - -const MergeInfoWrapper = styled.div` - display: flex; - align-items: center; - margin-right: 8px; - position: relative; - margin-left: 10px; -`; - -const MergeInfo = styled.div` - font-size: 13px; - align-items: center; - color: #aaaabb66; - white-space: nowrap; - display: flex; - align-items: center; - text-overflow: ellipsis; - overflow: hidden; - max-width: 300px; - - > i { - font-size: 16px; - margin: 0 2px; - } -`; - -const BranchIcon = styled.img` - font-size: 20px; - height: 16px; - margin-right: 10px; - color: #aaaabb; - opacity: 50%; -`; - -const SubmitButton = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - font-size: 13px; - cursor: pointer; - font-family: "Work Sans", sans-serif; - border-radius: 5px; - font-weight: 500; - color: white; - height: 30px; - padding: 0 8px; - width: 200px; - 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; - } -`; - -const SearchInput = styled.input` - outline: none; - border: none; - font-size: 13px; - background: none; - width: 100%; - color: white; - height: 100%; -`; - -const Br = styled.div<{ height: string }>` - width: 100%; - height: ${(props) => props.height || "2px"}; -`; - -const ValidationErrorBannerWrapper = styled.div` - margin-block: 20px; -`; - -const LearnMoreButton = styled.div` - text-decoration: underline; - font-weight: bold; - cursor: pointer; -`; - -const CreatePreviewDeploymentWrapper = styled.div` - margin-top: 30px; - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 10px; -`; - -const RevalidatePorterYAMLSpanWrapper = styled.div` - font-size: 13px; - color: #aaaabb; -`; - -const RevalidateSpan = styled.span` - color: #aaaabb; - text-decoration: underline; - cursor: pointer; -`; - -const RefreshButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - color: ${(props: { color: string }) => props.color}; - cursor: pointer; - border: none; - width: 30px; - height: 30px; - margin-right: 15px; - background: none; - border-radius: 50%; - margin-left: 10px; - > i { - font-size: 20px; - } - :hover { - background-color: rgb(97 98 102 / 44%); - color: white; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx deleted file mode 100644 index 3ceea02eaf..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import DynamicLink from "components/DynamicLink"; -import React, { useState } from "react"; -import styled from "styled-components"; -import { useParams } from "react-router"; -import DashboardHeader from "../../DashboardHeader"; -import PullRequestIcon from "assets/pull_request_icon.svg"; -import CreatePREnvironment from "./CreatePREnvironment"; -import TabSelector from "components/TabSelector"; -import CreateBranchEnvironment from "./CreateBranchEnvironment"; - -const TAB_OPTIONS = [ - { label: "Pull Requests", value: "pull_requests" }, - { label: "Branches", value: "branches" }, -]; - -const CreateEnvironment: React.FC = () => { - const [currentTab, setCurrentTab] = useState( - TAB_OPTIONS[0] - ); - const { environment_id, repo_name, repo_owner } = useParams<{ - environment_id: string; - repo_name: string; - repo_owner: string; - }>(); - - const selectedRepo = `${repo_owner}/${repo_name}`; - - return ( - <> - - - - Preview environments - - / - - - {selectedRepo} - - - - - - setCurrentTab(TAB_OPTIONS.find((tab) => tab.value === value)) - } - /> - - {currentTab.value === "pull_requests" ? ( - - ) : ( - - )} - - ); -}; - -export default CreateEnvironment; - -const DarkMatter = styled.div` - width: 100%; - margin-top: -15px; -`; - -const Slash = styled.div` - margin: 0 4px; - color: #aaaabb88; -`; - -const Wrap = styled.div` - z-index: 999; -`; - -const ArrowIcon = styled.img` - width: 15px; - margin-right: 8px; - opacity: 50%; -`; - -const Icon = styled.img` - width: 15px; - margin-right: 8px; -`; - -const BreadcrumbRow = styled.div` - width: 100%; - display: flex; - justify-content: flex-start; - margin-bottom: 15px; - margin-top: -10px; - align-items: center; -`; - -const Breadcrumb = styled(DynamicLink)` - color: #aaaabb88; - font-size: 13px; - display: flex; - align-items: center; - z-index: 999; - padding: 5px; - padding-right: 7px; - border-radius: 5px; - cursor: pointer; - :hover { - background: #ffffff11; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx deleted file mode 100644 index d5f89ef2aa..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx +++ /dev/null @@ -1,591 +0,0 @@ -import React, { useContext, useMemo, useState } from "react"; -import styled from "styled-components"; -import { Context } from "shared/Context"; -import { PullRequest } from "../types"; -import Helper from "components/form-components/Helper"; -import pr_icon from "assets/pull_request_icon.svg"; -import api from "shared/api"; -import { EllipsisTextWrapper } from "../components/styled"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { getPRDeploymentList, validatePorterYAML } from "../utils"; -import Banner from "components/porter/Banner"; -import { useRouting } from "shared/routing"; -import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal"; -import Placeholder from "components/Placeholder"; -import RadioFilter from "components/RadioFilter"; - -import sort from "assets/sort.svg"; -import { search } from "shared/search"; -import _, { create } from "lodash"; -import { readableDate } from "shared/string_utils"; -import dayjs from "dayjs"; -import Loading from "components/Loading"; -import Modal from "components/porter/Modal"; -import Text from "components/porter/Text"; -import Spacer from "components/porter/Spacer"; -import Button from "components/porter/Button"; - -interface Props { - environmentID: string; -} - -const CreatePREnvironment = ({ environmentID }: Props) => { - const queryClient = useQueryClient(); - const router = useRouting(); - const [searchValue, setSearchValue] = useState(""); - const [sortOrder, setSortOrder] = useState("Newest"); - const [showErrorsModal, setShowErrorsModal] = useState(false); - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - - // Get all PRs for the current environment - const { isLoading: pullRequestsLoading, data: pullRequests } = useQuery< - PullRequest[] - >( - ["pullRequests", currentProject.id, currentCluster.id, environmentID], - async () => { - try { - const res = await getPRDeploymentList({ - projectID: currentProject.id, - clusterID: currentCluster.id, - environmentID: Number(environmentID), - }); - - return res.data.pull_requests || []; - } catch (err) { - setCurrentError(err); - } - } - ); - - const [selectedPR, setSelectedPR] = useState(); - const [loading, setLoading] = useState(false); - const [porterYAMLErrors, setPorterYAMLErrors] = useState([]); - const [showCreatePreviewModal, setShowCreatePreviewModal] = useState( - false - ); - - const handleCreatePreviewDeployment = async () => { - try { - await api.createPreviewEnvironmentDeployment("", selectedPR, { - cluster_id: currentCluster?.id, - project_id: currentProject?.id, - }); - - router.push( - `/preview-environments/deployments/${environmentID}/${selectedPR.repo_owner}/${selectedPR.repo_name}?status_filter=all` - ); - } catch (err) { - throw err; - } - }; - - const createPreviewDeploymentMutation = useMutation({ - mutationFn: handleCreatePreviewDeployment, - onError: (err) => { - setCurrentError(err as any); - }, - }); - - const handleRefresh = () => { - queryClient.invalidateQueries({ - queryKey: ["pullRequests"], - }); - }; - - const handlePRRowItemClick = async (pullRequest: PullRequest) => { - setSelectedPR(pullRequest); - setLoading(true); - - const res = await validatePorterYAML({ - projectID: currentProject.id, - clusterID: currentCluster.id, - environmentID: Number(environmentID), - branch: pullRequest.branch_from, - }); - setShowCreatePreviewModal(true); - - setPorterYAMLErrors(res.data.errors ?? []); - - setLoading(false); - }; - const handleModalSubmit = () => { - createPreviewDeploymentMutation.mutate(); - setShowCreatePreviewModal(false); - }; - - const filteredPullRequests = useMemo(() => { - const filteredBySearch = search( - pullRequests ?? [], - searchValue, - { - isCaseSensitive: false, - threshold: 0.2, // Adjust this value to fine-tune the matching - distance: 50, - keys: ["pr_title"], - } - ); - - switch (sortOrder) { - case "Recently Updated": - return _.sortBy(filteredBySearch, "updated_at").reverse(); - case "Newest": - return _.sortBy(filteredBySearch, "created_at").reverse(); - case "Oldest": - return _.sortBy(filteredBySearch, "created_at"); - case "Alphabetical": - default: - return _.sortBy(filteredBySearch, "gh_pr_name"); - } - }, [pullRequests, searchValue, sortOrder]); - - if (pullRequestsLoading) { - return ( - <> -
- - - - - ); - } - - if (!pullRequests.length) { - return ( - <> -
- {`You do not have any pull requests.`} - - ); - } - - return ( - <> - - Select an open pull request to preview. Pull requests must contain a{" "} - porter.yaml file. - - - - - - search - { - setSelectedPR(undefined); - setPorterYAMLErrors([]); - setSearchValue(e.target.value); - }} - placeholder="Search" - /> - - - - - - refresh - - - - -
- {filteredPullRequests?.length ? ( - - {(filteredPullRequests ?? []).map( - (pullRequest: PullRequest, i: number) => { - return ( - { - handlePRRowItemClick(pullRequest); - }} - isLast={i === filteredPullRequests.length - 1} - isSelected={pullRequest === selectedPR} - > - - - - {pullRequest.pr_title} - - - - - - {/* - - #{pullRequest.pr_number} last updated xyz - - - */} - - - {pullRequest.branch_from} - arrow_forward - {pullRequest.branch_into} - - - - Last updated{" "} - {dayjs(pullRequest.updated_at).format( - "hh:mma on MM/DD/YYYY" - )} - - - - - - ); - } - )} - - ) : ( - <> -
- {`No pull requests match your search query.`} - - )} - {showErrorsModal && selectedPR ? ( - setShowErrorsModal(false)} - repo={selectedPR.repo_owner + "/" + selectedPR.repo_name} - branch={selectedPR.branch_from} - /> - ) : null} - {selectedPR && porterYAMLErrors.length ? ( - - - We found some errors in the porter.yaml file in the  - {selectedPR.branch_from} branch.   - setShowErrorsModal(true)}> - Learn more - - - - ) : null} - - {showCreatePreviewModal && selectedPR && porterYAMLErrors.length == 0 && ( - setShowCreatePreviewModal(false)} - > - <> - - Create Preview Deployment for {selectedPR.pr_title}? - - - {/* */} - createPreviewDeploymentMutation.mutate()} - disabled={ - loading || - !selectedPR || - porterYAMLErrors.length > 0 || - createPreviewDeploymentMutation.isLoading - } - > - {createPreviewDeploymentMutation.isLoading - ? "Creating..." - : "Create preview deployment"} - - - - )} - {selectedPR && porterYAMLErrors.length ? ( - - Please fix your porter.yaml file to continue.{" "} - { - e.preventDefault(); - e.stopPropagation(); - - if (!selectedPR) { - return; - } - - handlePRRowItemClick(selectedPR); - }} - > - Refresh - - - ) : null} - - - ); -}; - -export default CreatePREnvironment; - -const PullRequestList = styled.div` - border: 1px solid #494b4f; - border-radius: 5px; - overflow: hidden; - margin-top: 33px; -`; - -const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>` - width: 100%; - padding: 15px; - cursor: pointer; - background: ${(props) => (props.isSelected ? "#ffffff11" : "#26292e")}; - border-bottom: ${(props) => (props.isLast ? "" : "1px solid #494b4f")}; - :hover { - background: #ffffff11; - } -`; - -const InfoWrapper = styled.div` - display: flex; - align-items: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-right: 8px; -`; - -const Code = styled.span` - font-family: monospace; ; -`; - -const Flex = styled.div` - display: flex; - align-items: center; -`; - -const DeploymentImageContainer = styled.div` - height: 20px; - font-size: 13px; - position: relative; - display: flex; - align-items: center; - font-weight: 400; - justify-content: center; - color: #ffffff66; - padding-left: 10px; -`; - -const LastDeployed = styled.div` - font-size: 13px; - margin-top: -1px; - display: flex; - align-items: center; - color: #aaaabb66; -`; - -const MergeInfoWrapper = styled.div` - display: flex; - align-items: center; - margin-right: 8px; - position: relative; - margin-left: 10px; - gap: 8px; -`; - -const MergeInfo = styled.div` - font-size: 13px; - align-items: center; - color: #aaaabb66; - white-space: nowrap; - display: flex; - align-items: center; - text-overflow: ellipsis; - overflow: hidden; - max-width: 300px; - - > i { - font-size: 16px; - margin: 0 2px; - } -`; - -const SepDot = styled.div` - color: #aaaabb66; -`; - -const PRIcon = styled.img` - font-size: 20px; - height: 16px; - margin-right: 10px; - color: #aaaabb; - opacity: 50%; -`; - -const PRName = styled.div` - font-family: "Work Sans", sans-serif; - font-weight: 500; - color: #ffffff; - display: flex; - font-size: 14px; - align-items: center; - margin-bottom: 10px; -`; - -const SubmitButton = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - font-size: 13px; - cursor: pointer; - font-family: "Work Sans", sans-serif; - border-radius: 5px; - font-weight: 500; - color: white; - height: 30px; - padding: 0 8px; - width: 200px; - 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; - } -`; - -const Br = styled.div<{ height: string }>` - width: 100%; - height: ${(props) => props.height || "2px"}; -`; - -const ValidationErrorBannerWrapper = styled.div` - margin-block: 20px; -`; - -const LearnMoreButton = styled.div` - text-decoration: underline; - font-weight: bold; - cursor: pointer; -`; - -const CreatePreviewDeploymentWrapper = styled.div` - margin-top: 30px; - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 10px; -`; - -const RevalidatePorterYAMLSpanWrapper = styled.div` - font-size: 13px; - color: #aaaabb; -`; - -const RevalidateSpan = styled.span` - color: #aaaabb; - text-decoration: underline; - cursor: pointer; -`; - -const SearchInput = styled.input` - outline: none; - border: none; - font-size: 13px; - background: none; - width: 100%; - color: white; - height: 100%; -`; - -const SearchRow = styled.div` - display: flex; - align-items: center; - height: 30px; - margin-right: 10px; - background: #26292e; - border-radius: 5px; - border: 1px solid #aaaabb33; -`; - -const SearchRowWrapper = styled(SearchRow)` - border-radius: 5px; - width: 250px; -`; - -const SearchBarWrapper = styled.div` - display: flex; - flex: 1; - - > i { - color: #aaaabb; - padding-top: 1px; - margin-left: 8px; - font-size: 16px; - margin-right: 8px; - } -`; - -const FlexRow = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 10px; -`; - -const RefreshButton = styled.button` - display: flex; - align-items: center; - justify-content: center; - color: ${(props: { color: string }) => props.color}; - cursor: pointer; - border: none; - width: 30px; - height: 30px; - margin-right: 15px; - background: none; - border-radius: 50%; - margin-left: 10px; - > i { - font-size: 20px; - } - :hover { - background-color: rgb(97 98 102 / 44%); - color: white; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx deleted file mode 100644 index f3de766c26..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import React, { useContext, useState } from "react"; -import { capitalize } from "shared/string_utils"; -import styled from "styled-components"; -import { Environment } from "../types"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import DynamicLink from "components/DynamicLink"; -import { RepoLink } from "../components/styled"; - -type Props = { - environment: Environment; - onDelete: (env: Environment) => void; -}; - -const EnvironmentCard = ({ environment, onDelete }: Props) => { - const { currentCluster, currentProject, setCurrentError } = useContext( - Context - ); - - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [deleteConfirmationRepoName, setDeleteConfirmationRepoName] = useState( - "" - ); - - const { - id, - name, - deployment_count, - git_repo_owner, - git_repo_name, - git_installation_id, - last_deployment_status, - } = environment; - - const handleDelete = () => { - if (!canDelete()) { - return; - } - api - .deleteEnvironment( - "", - { - name: name, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - git_installation_id: git_installation_id, - git_repo_owner: git_repo_owner, - git_repo_name: git_repo_name, - } - ) - .then(() => { - onDelete(environment); - closeForm(); - }) - .catch((err) => { - setCurrentError(JSON.stringify(err)); - }); - }; - - const closeForm = () => { - setShowDeleteModal(false); - setDeleteConfirmationRepoName(""); - }; - - const canDelete = () => { - const repoName = deleteConfirmationRepoName; - return repoName === `${git_repo_owner}/${git_repo_name}`; - }; - - return ( - <> - {/* {showDeleteModal ? ( - - - ⚠️ All Preview Environment deployments associated with this repo - will be deleted. - - setDeleteConfirmationRepoName(x)} - width={"500px"} - /> - - handleDelete()} - disabled={!canDelete()} - > - Delete - - - - ) : null} */} - - - - - {git_repo_owner}/{git_repo_name} - { - e.preventDefault(); - e.stopPropagation(); - - window.open( - `https://github.com/${git_repo_owner}/${git_repo_name}`, - "_blank" - ); - }} - > - open_in_new - View Repo - - - - {deployment_count > 0 ? ( - <> - - Last PR status was "{capitalize(last_deployment_status || "")}" - - - ) : null} - {deployment_count > 0 ? ( - - {deployment_count || 0} pull{" "} - {deployment_count > 1 ? "requests" : "request"} deployed - - ) : ( - - There is no pull request deployed for this environment - - )} - - - - - ); -}; - -export default EnvironmentCard; - -const Span = styled.span` - color: #aaaabb66; -`; - -const OptionWrapper = styled.div` - display: flex; - align-items: center; - justify-content: center; -`; - -const EnvironmentCardWrapper = styled(DynamicLink)` - display: flex; - color: #ffffff; - justify-content: space-between; - cursor: pointer; - height: 75px; - padding: 12px; - padding-left: 14px; - border-radius: 5px; - background: #26292e; - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - } - animation: fadeIn 0.5s; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -`; - -const DataContainer = styled.div` - display: flex; - flex-direction: column; -`; - -const RepoName = styled.div` - display: flex; - font-size: 14px; - font-weight: 500; - align-items: center; -`; - -const Status = styled.span` - font-size: 13px; - display: flex; - align-items: center; - min-height: 17px; - color: #a7a6bb; - margin-top: 10px; -`; - -const StatusDot = styled.div` - width: 8px; - height: 8px; - margin-right: 15px; - background: ${(props: { status: string }) => - props.status === "created" - ? "#4797ff" - : props.status === "failed" - ? "#ed5f85" - : props.status === "completed" - ? "#00d12a" - : "#f5cb42"}; - border-radius: 20px; - margin-left: 4px; -`; - -const Icon = styled.img` - width: 18px; - height: 18px; - margin-right: 12px; -`; - -const Button = styled.button` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - font-size: 13px; - margin-top: 13px; - cursor: pointer; - font-family: "Work Sans", sans-serif; - border-radius: 5px; - color: white; - height: 35px; - padding: 10px 16px; - font-weight: 500; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - cursor: pointer; - border: none; - :not(:last-child) { - margin-right: 10px; - } -`; - -const DeleteButton = styled(Button)` - ${({ disabled }: { disabled: boolean }) => { - if (disabled) { - return ` - background: #aaaabbee; - :hover { - background: #aaaabbee; - } - `; - } - - return ` - background: #dd4b4b; - :hover { - background: #b13d3d; - }`; - }} -`; - -const ActionWrapper = styled.div` - display: flex; - align-items: center; -`; - -const Warning = styled.div` - font-size: 13px; - display: flex; - border-radius: 3px; - width: calc(100%); - margin-top: 18px; - margin-left: 2px; - line-height: 1.4em; - align-items: center; - color: white; - > i { - margin-right: 10px; - font-size: 18px; - } - color: ${(props: { highlight: boolean; makeFlush?: boolean }) => - props.highlight ? "#f5cb42" : ""}; -`; - -const Dot = styled.div` - margin-right: 9px; - color: #aaaabb66; - margin-left: 9px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx deleted file mode 100644 index ae9a9d3294..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx +++ /dev/null @@ -1,493 +0,0 @@ -import DynamicLink from "components/DynamicLink"; -import Loading from "components/Loading"; -import React, { useContext, useEffect, useMemo, useState } from "react"; -import api from "shared/api"; -import styled from "styled-components"; -import { useParams } from "react-router"; -import DashboardHeader from "../../DashboardHeader"; -import PullRequestIcon from "assets/pull_request_icon.svg"; -import Heading from "components/form-components/Heading"; -import Helper from "components/form-components/Helper"; -import CheckboxRow from "components/form-components/CheckboxRow"; -import { Environment, EnvironmentDeploymentMode } from "../types"; -import SaveButton from "components/SaveButton"; -import _ from "lodash"; -import { Context } from "shared/Context"; -import PageNotFound from "components/PageNotFound"; -import Banner from "components/porter/Banner"; -import InputRow from "components/form-components/InputRow"; -import Modal from "main/home/modals/Modal"; -import { useRouting } from "shared/routing"; -import NamespaceLabels, { KeyValueType } from "../components/NamespaceLabels"; -import BranchFilterSelector from "../components/BranchFilterSelector"; - -const EnvironmentSettings = () => { - const router = useRouting(); - const [isLoadingBranches, setIsLoadingBranches] = useState(false); - const [availableBranches, setAvailableBranches] = useState([]); - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [deleteConfirmationPrompt, setDeleteConfirmationPrompt] = useState(""); - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - const [baseBranches, setBaseBranches] = useState([]); - const [deployBranches, setDeployBranches] = useState([]); - const [environment, setEnvironment] = useState(); - const [saveStatus, setSaveStatus] = useState(""); - const [newCommentsDisabled, setNewCommentsDisabled] = useState(false); - const [ - deploymentMode, - setDeploymentMode, - ] = useState("manual"); - const [namespaceLabels, setNamespaceLabels] = useState([]); - const { - environment_id: environmentId, - repo_name: repoName, - repo_owner: repoOwner, - } = useParams<{ - environment_id: string; - repo_name: string; - repo_owner: string; - }>(); - - const selectedRepo = `${repoOwner}/${repoName}`; - - useEffect(() => { - const getPreviewEnvironmentSettings = async () => { - const { data: environment } = await api.getEnvironment( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - environment_id: parseInt(environmentId), - } - ); - - setEnvironment(environment); - setBaseBranches(environment.git_repo_branches); - setNewCommentsDisabled(environment.new_comments_disabled); - setDeploymentMode(environment.mode); - setDeployBranches(environment.git_deploy_branches); - - if (environment.namespace_labels) { - const labels: KeyValueType[] = Object.entries( - environment.namespace_labels - ).map(([key, value]) => ({ - key, - value, - })); - - setNamespaceLabels(labels); - } - }; - - try { - getPreviewEnvironmentSettings(); - } catch (err) { - setCurrentError(err); - } - }, []); - - useEffect(() => { - if (!environment) { - return; - } - - const repoName = environment.git_repo_name; - const repoOwner = environment.git_repo_owner; - setIsLoadingBranches(true); - api - .getBranches( - "", - {}, - { - project_id: currentProject.id, - kind: "github", - name: repoName, - owner: repoOwner, - git_repo_id: environment.git_installation_id, - } - ) - .then(({ data }) => { - setIsLoadingBranches(false); - setAvailableBranches(data); - }) - .catch(() => { - setIsLoadingBranches(false); - setCurrentError( - "Couldn't load branches for this repository, using all branches by default." - ); - }); - }, [environment]); - - const handleSave = async () => { - let labels: Record = {}; - - setSaveStatus("loading"); - - namespaceLabels - .filter((elem: KeyValueType, index: number, self: KeyValueType[]) => { - // remove any collisions that are duplicates - let numCollisions = self.reduce((n, _elem: KeyValueType) => { - return n + (_elem.key === elem.key ? 1 : 0); - }, 0); - - if (numCollisions == 1) { - return true; - } else { - return ( - index === - self.findIndex((_elem: KeyValueType) => _elem.key === elem.key) - ); - } - }) - .forEach((elem: KeyValueType) => { - if (elem.key !== "" && elem.value !== "") { - labels[elem.key] = elem.value; - } - }); - - try { - await api.updateEnvironment( - "", - { - mode: deploymentMode, - disable_new_comments: newCommentsDisabled, - git_repo_branches: baseBranches, - namespace_labels: labels, - git_deploy_branches: deployBranches, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - environment_id: Number(environmentId), - } - ); - } catch (err) { - setCurrentError(err); - } - - setSaveStatus(""); - }; - - const closeDeleteConfirmationModal = () => { - setShowDeleteModal(false); - setDeleteConfirmationPrompt(""); - }; - - const canDelete = useMemo(() => { - return deleteConfirmationPrompt === `${repoOwner}/${repoName}`; - }, [deleteConfirmationPrompt]); - - const handleDelete = async () => { - if (!canDelete) { - return; - } - - try { - await api.deleteEnvironment( - "", - { - name: environment?.name, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - git_installation_id: environment?.git_installation_id, - git_repo_owner: repoOwner, - git_repo_name: repoName, - } - ); - - closeDeleteConfirmationModal(); - router.push(`/preview-environments`); - } catch (err) { - setCurrentError(JSON.stringify(err)); - closeDeleteConfirmationModal(); - } - }; - - return ( - <> - {showDeleteModal ? ( - - ) : null} - - - - Preview environments - - / - - - {selectedRepo} - - - - - - Changes made here will not affect existing deployments in this preview - environment. - - - - Pull request comment settings - - Update the most recent PR comment on every deploy. If disabled, a new - PR comment is made per deploy. - - setNewCommentsDisabled(!newCommentsDisabled)} - /> -
- Automatic preview deployments - - When enabled, preview deployments are automatically created for all - new pull requests. - - - setDeploymentMode((deploymentMode) => - deploymentMode === "auto" ? "manual" : "auto" - ) - } - /> -
- Deploy from branches - - Choose the list of branches that you want to deploy changes from. - - -
- Select allowed branches - - If the pull request has a base branch included in this list, it will - be allowed to be deployed. -
- (Leave empty to allow all branches) -
- -
- Namespace labels - - Custom labels to be injected into the Kubernetes namespace created for - each deployment. - - { - const labels: KeyValueType[] = x.map((entry) => ({ - key: entry.key, - value: entry.value, - })); - - setNamespaceLabels(labels); - }} - /> - -
- Delete preview environment - - Delete the Porter preview environment integration for this repo. All - preview deployments will also be destroyed. - - { - setShowDeleteModal(true); - }} - > - Delete preview environment - -
- - ); -}; - -interface DeletePreviewEnvironmentModalProps { - repoName: string; - repoOwner: string; - prompt: string; - setPrompt: (prompt: string) => void; - onDelete: () => void; - onClose: () => void; - disabled: boolean; -} - -const DeletePreviewEnvironmentModal = ( - props: DeletePreviewEnvironmentModalProps -) => { - return ( - - - - All Preview Environment deployments associated with this repo will be - deleted. - - props.setPrompt(x)} - width={"500px"} - /> - - props.onDelete()} - disabled={props.disabled} - > - Delete - - - - - ); -}; - -export default EnvironmentSettings; - -const DeletePreviewEnvironmentModalContentsWrapper = styled.div` - margin-block-start: 25px; -`; - -const SavePreviewEnvironmentSettings = styled(SaveButton)` - margin-top: 30px; -`; - -const Flex = styled.div<{ - justifyContent?: string; - alignItems?: string; -}>` - display: flex; - align-items: ${({ alignItems }) => alignItems || "flex-start"}; - justify-content: ${({ justifyContent }) => justifyContent || "flex-start"}; -`; - -const DeleteButton = styled.button<{ disabled?: boolean }>` - font-size: 13px; - font-weight: 500; - font-family: "Work Sans", sans-serif; - color: white; - display: flex; - align-items: center; - padding: 10px 15px; - margin-top: 20px; - text-align: left; - border-radius: 5px; - user-select: none; - background: #b91133; - border: none; - cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; - filter: ${({ disabled }) => (disabled ? "brightness(0.8)" : "none")}; - - &:focus { - outline: 0; - } - &:hover { - filter: ${({ disabled }) => (disabled ? "brightness(0.8)" : "none")}; - } -`; - -const Br = styled.div` - width: 100%; - height: 2px; -`; - -const StyledPlaceholder = styled.div` - width: 100%; - padding: 30px; - font-size: 13px; - border-radius: 5px; - background: #26292e; - border: 1px solid #494b4f; -`; - -const Slash = styled.div` - margin: 0 4px; - color: #aaaabb88; -`; - -const Wrap = styled.div` - z-index: 999; -`; - -const ArrowIcon = styled.img` - width: 15px; - margin-right: 8px; - opacity: 50%; -`; - -const Icon = styled.img` - width: 15px; - margin-right: 8px; -`; - -const BreadcrumbRow = styled.div` - width: 100%; - display: flex; - justify-content: flex-start; - margin-bottom: 15px; - margin-top: -5px; - align-items: center; -`; - -const Breadcrumb = styled(DynamicLink)` - color: #aaaabb88; - font-size: 13px; - display: flex; - align-items: center; - z-index: 999; - padding: 5px; - padding-right: 7px; - border-radius: 5px; - cursor: pointer; - :hover { - background: #ffffff11; - } -`; - -const WarningBannerWrapper = styled.div` - margin-block: 20px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx deleted file mode 100644 index ac8298a4a7..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import DynamicLink from "components/DynamicLink"; -import Loading from "components/Loading"; -import React, { useContext, useEffect, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import styled from "styled-components"; -import ButtonEnablePREnvironments from "../components/ButtonEnablePREnvironments"; -import { PreviewEnvironmentsHeader } from "../components/PreviewEnvironmentsHeader"; -import { Environment } from "../types"; -import EnvironmentCard from "./EnvironmentCard"; -import Placeholder from "components/Placeholder"; - -const EnvironmentsList = () => { - const { currentCluster, currentProject } = useContext(Context); - const [isLoading, setIsLoading] = useState(true); - const [buttonIsReady, setButtonIsReady] = useState(false); - - const [environments, setEnvironments] = useState([]); - - const removeEnvironmentFromList = (deletedEnv: Environment) => { - setEnvironments((prev) => { - return prev.filter((env) => env.id !== deletedEnv.id); - }); - }; - - const getEnvironments = async () => { - try { - const { data } = await api.listEnvironments( - "", - {}, - { - project_id: currentProject?.id, - cluster_id: currentCluster?.id, - } - ); - - return data; - } catch (error) { - throw error; - } - }; - - const checkPreviewEnvironmentsEnabling = async (subscribeStauts: { - subscribed: boolean; - }) => { - try { - const envs = await getEnvironments(); - // const envs = await mockRequest(); - - if (!subscribeStauts.subscribed) { - return; - } - - if (!Array.isArray(envs)) { - return; - } - - setEnvironments(envs); - } catch (error) { - setEnvironments([]); - } - }; - - useEffect(() => { - let subscribedStatus = { subscribed: true }; - - setIsLoading(true); - - checkPreviewEnvironmentsEnabling(subscribedStatus).finally(() => { - if (subscribedStatus.subscribed) { - setIsLoading(false); - } - }); - - return () => { - subscribedStatus.subscribed = false; - }; - }, [currentCluster, currentProject]); - - return ( - <> - - - - - - {isLoading ? ( - - - - ) : ( - <> - {environments.length === 0 ? ( - - No repositories were found with Preview Environments enabled. - - ) : ( - - {environments.map((env) => ( - - ))} - - )} - - )} - - - ); -}; - -export default EnvironmentsList; - -const LoadingWrapper = styled.div` - padding-top: 100px; -`; - -const Relative = styled.div` - position: relative; -`; - -const EnvironmentsGrid = styled.div` - padding-bottom: 150px; - display: grid; - grid-row-gap: 15px; -`; - -const ControlRow = styled.div` - display: flex; - margin-left: auto; - justify-content: space-between; - align-items: center; - margin-bottom: 30px; - padding-left: 0px; -`; - -const Button = styled(DynamicLink)` - 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; - padding: 0px 8px; - padding-bottom: 1px; - margin-right: 10px; - font-weight: 500; - padding-right: 15px; - 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/preview-environments/errors.ts b/dashboard/src/main/home/cluster-dashboard/preview-environments/errors.ts deleted file mode 100644 index 88cdd072c7..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/errors.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum PorterYAMLErrors { - FileNotFound = "porter.yaml does not exist in the root of this repository", -} diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/mocks.ts b/dashboard/src/main/home/cluster-dashboard/preview-environments/mocks.ts deleted file mode 100644 index d93a38ca27..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/mocks.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { PRDeployment } from "./types"; - -export const environments = [ - { - id: 29, - project_id: 3, - cluster_id: 34, - git_installation_id: 22158312, - git_repo_owner: "porter-dev", - git_repo_name: "porter-docs", - name: "Preview", - }, - { - id: 36, - project_id: 3, - cluster_id: 34, - git_installation_id: 21704327, - git_repo_owner: "jnfrati", - git_repo_name: "angular-todo-app", - name: "Preview", - }, - { - id: 37, - project_id: 3, - cluster_id: 34, - git_installation_id: 21704327, - git_repo_owner: "jnfrati", - git_repo_name: "porter-docs", - name: "Preview", - deployment_count: 3, - last_deployment_status: "failed", - }, - { - id: 38, - project_id: 3, - cluster_id: 34, - git_installation_id: 21704327, - git_repo_owner: "jnfrati", - git_repo_name: "porter-docs", - name: "Preview", - }, - { - id: 39, - project_id: 3, - cluster_id: 34, - git_installation_id: 21704327, - git_repo_owner: "jnfrati", - git_repo_name: "multi-tenant-blog", - name: "Preview", - }, - { - id: 40, - project_id: 3, - cluster_id: 34, - git_installation_id: 18424822, - git_repo_owner: "sunguroku", - git_repo_name: "node", - name: "Preview", - }, - { - id: 41, - project_id: 3, - cluster_id: 34, - git_installation_id: 18424822, - git_repo_owner: "sunguroku", - git_repo_name: "code-server", - name: "Preview", - }, - { - id: 42, - project_id: 3, - cluster_id: 34, - git_installation_id: 22158312, - git_repo_owner: "porter-dev", - git_repo_name: "preview-env", - name: "Preview", - }, - { - id: 43, - project_id: 3, - cluster_id: 34, - git_installation_id: 22158312, - git_repo_owner: "porter-dev", - git_repo_name: "preview", - name: "Preview", - }, - { - id: 44, - project_id: 3, - cluster_id: 34, - git_installation_id: 22158312, - git_repo_owner: "porter-dev", - git_repo_name: "preview-env-test", - name: "Preview", - }, - { - id: 45, - project_id: 3, - cluster_id: 34, - git_installation_id: 22158312, - git_repo_owner: "porter-dev", - git_repo_name: "ptrtr", - name: "Preview", - }, -]; - -export const deployments: PRDeployment[] = [ - { - gh_deployment_id: 534980099, - gh_pr_name: "Update porter.yaml to enable preview environments on porter", - gh_pr_branch_from: "some-branch-name", - gh_pr_branch_into: "master", - gh_repo_name: "preview", - gh_repo_owner: "porter-dev", - gh_commit_sha: "74a1191", - id: 43, - created_at: "2022-03-28T19:28:11.012729Z", - updated_at: "2022-03-28T19:31:53.871666Z", - gh_installation_id: 0, - environment_id: 43, - namespace: "pr-3-preview", - status: "failed", - subdomain: "", - pull_request_id: 3, - last_workflow_run_url: "https://something.com", - }, - { - gh_deployment_id: 532608734, - gh_pr_name: "Testing pr preview", - gh_pr_branch_from: "some-branch-name", - gh_pr_branch_into: "master", - gh_repo_name: "porter-docs", - gh_repo_owner: "jnfrati", - gh_commit_sha: "6a4b67e", - id: 41, - created_at: "2022-03-24T20:24:17.103471Z", - updated_at: "2022-03-24T20:45:06.684096Z", - gh_installation_id: 0, - environment_id: 37, - namespace: "pr-1-porter-docs", - status: "inactive", - subdomain: "", - pull_request_id: 1, - last_workflow_run_url: "", - }, - { - gh_deployment_id: 514002155, - gh_pr_name: - "Testing PR with job run and a really long name to explain what's going on over this pull request", - gh_pr_branch_from: "some-branch-name", - gh_pr_branch_into: "master", - gh_repo_name: "porter-docs", - gh_repo_owner: "porter-dev", - gh_commit_sha: "443d930", - id: 32, - created_at: "2022-01-30T11:04:14.496147Z", - updated_at: "2022-02-24T22:02:27.17928Z", - gh_installation_id: 0, - environment_id: 29, - namespace: "pr-20-porter-docs", - status: "created", - subdomain: "https://docs-web-78a048205ac7869b.staging-onporter.run", - pull_request_id: 20, - last_workflow_run_url: "https://something.com", - }, -]; - -export const pull_requests = [ - { - pr_title: "Testing PR with job run", - pr_number: 1, - repo_owner: "porter-docs", - repo_name: "porter-dev", - branch_from: "some_branch", - branch_into: "main", - }, -]; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/routes.tsx b/dashboard/src/main/home/cluster-dashboard/preview-environments/routes.tsx deleted file mode 100644 index d3a74e45f9..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/routes.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useContext } from "react"; -import { Redirect, Route, Switch, useRouteMatch } from "react-router"; -import { Context } from "shared/Context"; -import ConnectNewRepo from "./ConnectNewRepo"; -import DeploymentDetail from "./deployments/DeploymentDetail"; -import DeploymentList from "./deployments/DeploymentList"; -import EnvironmentsList from "./environments/EnvironmentsList"; -import EnvironmentSettings from "./environments/EnvironmentSettings"; -import DeployEnvironment from "./environments/CreateEnvironment"; - -export const Routes = () => { - const { path } = useRouteMatch(); - const { currentProject } = useContext(Context); - - if (!currentProject?.preview_envs_enabled) { - return ; - } - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default Routes; diff --git a/dashboard/src/main/home/cluster-dashboard/preview-environments/utils.ts b/dashboard/src/main/home/cluster-dashboard/preview-environments/utils.ts deleted file mode 100644 index 1861a6cb5c..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/preview-environments/utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -import api from "shared/api"; - -interface ValidatePorterYAMLProps { - projectID: number; - clusterID: number; - environmentID: number; - branch?: string; -} - -export const validatePorterYAML = ({ - projectID, - clusterID, - environmentID, - branch, -}: ValidatePorterYAMLProps) => { - return api.validatePorterYAML( - "", - { - ...(branch ? { branch } : {}), - }, - { - project_id: projectID, - cluster_id: clusterID, - environment_id: environmentID, - } - ); -}; - -interface GetPRDeploymentListProps { - projectID: number; - clusterID: number; - environmentID: number; -} - -export const getPRDeploymentList = ({ - clusterID, - projectID, - environmentID, -}: GetPRDeploymentListProps) => { - return api.getPRDeploymentList( - "", - { - environment_id: environmentID, - }, - { - project_id: projectID, - cluster_id: clusterID, - } - ); -}; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx deleted file mode 100644 index 46120d9edf..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import DynamicLink from "components/DynamicLink"; -import RadioFilter from "components/RadioFilter"; -import React, { useEffect, useState } from "react"; -import { useHistory, useLocation } from "react-router"; -import { useRouting } from "shared/routing"; -import styled from "styled-components"; -import DashboardHeader from "../DashboardHeader"; -import { NamespaceSelector } from "../NamespaceSelector"; -import sort from "assets/sort.svg"; -import StackList from "./_StackList"; -const Dashboard = () => { - const [currentNamespace, setCurrentNamespace] = useState("default"); - const [currentSort, setCurrentSort] = useState< - "created_at" | "updated_at" | "alphabetical" - >("created_at"); - - const location = useLocation(); - const history = useHistory(); - const { getQueryParam, pushQueryParams } = useRouting(); - - const handleNamespaceChange = (namespace: string) => { - setCurrentNamespace(namespace); - pushQueryParams({ namespace }); - }; - - useEffect(() => { - const newNamespace = getQueryParam("namespace"); - if (newNamespace !== currentNamespace) { - setCurrentNamespace(newNamespace); - } - }, [location.search, history]); - - return ( - <> - - - - - - - setCurrentSort(sortType as any)} - options={[ - { - value: "created_at", - label: "Created at", - }, - { - value: "updated_at", - label: "Last updated", - }, - { - value: "alphabetical", - label: "Alphabetical", - }, - ]} - name="Sort" - icon={sort} - /> - - - - - - ); -}; - -export default Dashboard; - -const Flex = styled.div` - display: flex; - align-items: center; - border-bottom: 30px solid transparent; -`; - -const Button = styled(DynamicLink)` - 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: 5px; - color: white; - margin-left: 10px; - height: 30px; - padding: 0 8px; - 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; - } -`; - -const FilterWrapper = 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: space-between; - align-items: center; - flex-wrap: wrap; -`; - -const Label = styled.div` - display: flex; - align-items: center; - margin-right: 12px; - - > i { - margin-right: 8px; - font-size: 18px; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx deleted file mode 100644 index 69023e8ca4..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import Loading from "components/Loading"; -import Placeholder from "components/OldPlaceholder"; -import TabSelector from "components/TabSelector"; -import TitleSection from "components/TitleSection"; -import React, { useContext, useState } from "react"; -import leftArrow from "assets/left-arrow.svg"; -import { useParams, useRouteMatch } from "react-router"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { useRouting } from "shared/routing"; -import { readableDate } from "shared/string_utils"; -import styled from "styled-components"; -import ChartList from "../../chart/ChartList"; -import Status from "../components/Status"; -import { - Action, - Br, - InfoWrapper, - LastDeployed, - NamespaceTag, - SepDot, - Text, -} from "../components/styles"; -import { getStackStatus, getStackStatusMessage } from "../shared"; -import { FullStackRevision, Stack, StackRevision } from "../types"; -import EnvGroups from "./components/EnvGroups"; -import RevisionList from "./_RevisionList"; -import SourceConfig from "./_SourceConfig"; -import { NavLink } from "react-router-dom"; -import Settings from "./components/Settings"; -import { ExpandedStackStore } from "./Store"; -import DynamicLink from "components/DynamicLink"; - -const ExpandedStack = () => { - const { namespace } = useParams<{ - namespace: string; - stack_id: string; - }>(); - - const { stack, refreshStack } = useContext(ExpandedStackStore); - - const { pushFiltered } = useRouting(); - - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - - const { url } = useRouteMatch(); - - const [isDeleting, setIsDeleting] = useState(false); - const [currentTab, setCurrentTab] = useState("apps"); - - const [currentRevision, setCurrentRevision] = useState( - () => stack.latest_revision - ); - - const handleDelete = () => { - setIsDeleting(true); - api - .deleteStack( - "", - {}, - { - namespace, - project_id: currentProject.id, - cluster_id: currentCluster.id, - stack_id: stack.id, - } - ) - .then(() => { - pushFiltered("/stacks", []); - }) - .catch((err) => { - setCurrentError(err); - setIsDeleting(false); - }); - }; - - if (stack === null) { - return null; - } - - if (isDeleting) { - return ( - -
-

Deleting Stack

-

This may take some time...

- -
-
- ); - } - - return ( -
- - - - Back - - - - - {stack.name} - - - Namespace - {stack.namespace} - - - - {/* Stack error message */} - {currentRevision && - currentRevision?.reason && - currentRevision?.message?.length > 0 ? ( - - history - - {currentRevision?.status === "failed" ? "Error: " : ""} - {currentRevision?.message} - - - ) : null} - - - - - - - Last updated {readableDate(stack.updated_at)} - - - - setCurrentRevision(revision)} - onRollback={() => refreshStack()} - > -
- - - - - add - Create app resource - - - {currentRevision.id !== stack.latest_revision.id ? ( - - - Not available when previewing revisions - - - ) : ( - - res.name - ) || [] - } - closeChartRedirectUrl={`${window.location.pathname}${window.location.search}`} - /> - - )} - - ), - }, - { - label: "Source config", - value: "source_config", - component: ( - <> - refreshStack()} - > - - ), - }, - { - label: "Env groups", - value: "env_groups", - component: ( - <> - - - - add - Create env group - - - - - ), - }, - { - label: "Settings", - value: "settings", - component: ( - <> - - - - ), - }, - ]} - setCurrentTab={(tab) => { - setCurrentTab(tab); - }} - > - -
- ); -}; - -export default ExpandedStack; - -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(DynamicLink)` - 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 PaddingBottom = styled.div` - width: 100%; - height: 150px; -`; - -const Break = styled.div` - width: 100%; - height: 20px; -`; - -const BackButton = styled(NavLink)` - 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 ChartListWrapper = styled.div` - width: 100%; - margin: auto; - padding-bottom: 125px; -`; - -const Gap = styled.div` - width: 100%; - background: none; - height: 30px; -`; - -const StackErrorMessageStyles = { - Text: styled(Text)` - font-size: 13px; - `, - Wrapper: styled.div` - display: flex; - align-items: center; - - margin-top: 5px; - > i { - color: #ffffff44; - margin-right: 8px; - font-size: 20px; - } - `, - Title: styled(Text)` - font-size: 16px; - font-weight: bold; - `, -}; - -const StackTitleWrapper = styled.div` - width: 100%; - display: flex; - position: relative; - align-items: center; - - // Hotfix to make sure the title section and the namespace tag are aligned - ${NamespaceTag.Wrapper} { - margin-left: 17px; - margin-bottom: 13px; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_Settings.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_Settings.tsx deleted file mode 100644 index 7ee4bddcf9..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_Settings.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { AxiosError } from "axios"; -import { PopulatedEnvGroup } from "components/porter-form/types"; -import React, { useContext, useEffect, useState } from "react"; -import { useParams } from "react-router"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { useRouting } from "shared/routing"; -import NewAppResourceForm from "../../components/NewAppResourceForm"; -import { CreateStackBody } from "../../types"; -import { ExpandedStackStore } from "../Store"; - -const parsePopulatedEnvGroup = (envGroup: PopulatedEnvGroup) => { - const variables = Object.entries(envGroup.variables) - .filter(([_, value]) => !value.includes("PORTERSECRET")) - .reduce( - (acc, [key, value]) => ({ ...acc, [key]: value }), - {} as Record - ); - const secret_variables = Object.entries(envGroup.variables) - .filter(([_, value]) => value.includes("PORTERSECRET")) - .reduce( - (acc, [key, value]) => ({ ...acc, [key]: value }), - {} as Record - ); - - return { - name: envGroup.name, - variables, - secret_variables, - linked_applications: envGroup.applications as string[], - }; -}; - -const Settings = () => { - const params = useParams<{ - template_name: string; - template_version: string; - }>(); - const { stack, refreshStack } = useContext(ExpandedStackStore); - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - const [availableEnvGroups, setAvailableEnvGroups] = useState< - { - name: string; - variables: Record; - secret_variables: Record; - linked_applications: string[]; - }[] - >([]); - - const { pushFiltered } = useRouting(); - - const populateEnvGroups = async () => { - const stackEnvGroups = stack.latest_revision.env_groups; - const envGroupsPromises = stackEnvGroups.map((envGroup) => - api - .getEnvGroup( - "", - {}, - { - id: currentProject.id, - cluster_id: currentCluster.id, - name: envGroup.name, - namespace: stack.namespace, - version: envGroup.env_group_version, - } - ) - .then((res) => res.data) - ); - - try { - const response = await Promise.allSettled(envGroupsPromises); - - const envGroups = response - .map((res) => { - if (res.status === "fulfilled") { - return res.value; - } - return undefined; - }) - .filter(Boolean); - - return envGroups; - } catch (error) { - setCurrentError(error); - throw error; - } - }; - - useEffect(() => { - let isSubscribed = true; - - populateEnvGroups().then((populatedEnvGroups) => { - if (!isSubscribed) { - return; - } - - if (Array.isArray(populatedEnvGroups)) { - const availableEnvGroups = populatedEnvGroups.map( - parsePopulatedEnvGroup - ); - - setAvailableEnvGroups(availableEnvGroups); - } - }); - - return () => { - isSubscribed = false; - }; - }, [stack, params, currentProject, currentCluster]); - - const handleSubmit = async ( - appResource: CreateStackBody["app_resources"][0] - ) => { - try { - await api.addStackAppResource( - "", - { - ...appResource, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: stack.namespace, - stack_id: stack.id, - } - ); - - await refreshStack(); - - pushFiltered(`/stacks/${stack.namespace}/${stack.id}`, []); - } catch (error) { - const axiosError: AxiosError = error; - if (axiosError.code === "409") { - throw "Application resource name already exists."; - } - - throw "Unexpected error, please try again."; - } - }; - - return ( - { - pushFiltered(`../template-selector`, []); - }} - onSubmit={handleSubmit} - /> - ); -}; - -export default Settings; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx deleted file mode 100644 index 7b337f2937..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import api from "shared/api"; -import { PorterTemplate } from "shared/types"; -import semver from "semver"; -import Loading from "components/Loading"; -import Placeholder from "components/OldPlaceholder"; -import { BackButton, Card } from "../../launch/components/styles"; -import DynamicLink from "components/DynamicLink"; -import { VersionSelector } from "../../launch/components/VersionSelector"; -import TitleSection from "components/TitleSection"; -import { Context } from "shared/Context"; - -const TemplateSelector = () => { - const { capabilities, currentProject } = useContext(Context); - - const [templates, setTemplates] = useState([]); - const [selectedVersion, setSelectedVersion] = useState<{ - [template_name: string]: string; - }>({}); - - const [isLoading, setIsLoading] = useState(true); - const [hasError, setHasError] = useState(false); - - const getTemplates = async () => { - try { - const res = await api.getTemplates( - "", - { - repo_url: capabilities?.default_app_helm_repo_url, - }, - { - project_id: currentProject.id, - } - ); - let sortedVersionData = res.data - .map((template: PorterTemplate) => { - let versions = template.versions.reverse(); - - versions = template.versions.sort(semver.rcompare); - - return { - ...template, - versions, - currentVersion: versions[0], - }; - }) - .sort((a, b) => { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; - }); - - return sortedVersionData; - } catch (err) { - throw err; - } - }; - - useEffect(() => { - let isSubscribed = true; - setIsLoading(true); - getTemplates() - .then((porterTemplates) => { - const latestVersions = porterTemplates.reduce((acc, template) => { - return { - ...acc, - [template.name]: template.versions[0], - }; - }, {} as Record); - - if (isSubscribed) { - setTemplates(porterTemplates); - setSelectedVersion(latestVersions); - } - }) - .catch(() => { - if (isSubscribed) { - setHasError(true); - } - }) - .finally(() => { - if (isSubscribed) { - setIsLoading(false); - } - }); - - return () => { - isSubscribed = false; - }; - }, []); - - if (isLoading) { - return ; - } - - if (hasError) { - return ( - -
-

Unexpected error

-

- We had an error retrieving the available templates, please try - again. -

-
-
- ); - } - - return ( - <> - - - - keyboard_backspace - - - Select a template - - - {templates.map((template) => { - return ( - - - New {template.name} with version: -
{ - e.preventDefault(); - }} - > - { - setSelectedVersion((prev) => ({ - ...prev, - [template.name]: newVersion, - })); - }} - /> -
-
- - arrow_forward - -
- ); - })} -
- - ); -}; - -export default TemplateSelector; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/index.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/index.tsx deleted file mode 100644 index 05d94945ce..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import { Redirect, Route, Switch, useRouteMatch } from "react-router"; -import Settings from "./_Settings"; -import TemplateSelector from "./_TemplateSelector"; - -const NewAppResourceRoutes = () => { - const { url } = useRouteMatch(); - - return ( - - - - - - - - - - - - - - - ); -}; - -export default NewAppResourceRoutes; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewEnvGroup.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewEnvGroup.tsx deleted file mode 100644 index 756eed06a9..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewEnvGroup.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { AxiosError } from "axios"; -import React, { useContext } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { useRouting } from "shared/routing"; -import NewEnvGroupForm from "../components/NewEnvGroupForm"; -import { CreateStackBody } from "../types"; -import { ExpandedStackStore } from "./Store"; - -const NewEnvGroup = () => { - const { stack, refreshStack } = useContext(ExpandedStackStore); - const { currentProject, currentCluster } = useContext(Context); - - const { pushFiltered } = useRouting(); - - const createEnvGroup = async ( - newEnvGroup: CreateStackBody["env_groups"][0] - ) => { - try { - await api.addStackEnvGroup( - "", - { - ...newEnvGroup, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: stack.namespace, - stack_id: stack.id, - } - ); - - await refreshStack(); - pushFiltered("../" + stack.id, []); - } catch (error) { - const axiosError: AxiosError = error; - - if (axiosError.code === "404" || axiosError.code === "405") { - throw "New env group not implemented"; - } - - if (axiosError.code === "409") { - throw "Name is already in use"; - } - - if (error?.message) { - throw error.message; - } - - throw error; - } - }; - - return ( - <> - { - pushFiltered("../" + stack.id, []); - }} - /> - - ); -}; - -export default NewEnvGroup; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/Store.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/Store.tsx deleted file mode 100644 index fb49326fba..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/Store.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import Loading from "components/Loading"; -import Placeholder from "components/OldPlaceholder"; -import React, { createContext, useContext, useEffect, useState } from "react"; -import { useParams } from "react-router"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { useRouting } from "shared/routing"; -import type { Stack } from "../types"; - -interface StoreType { - stack: Stack; - refreshStack: () => Promise; -} - -const defaultValues: StoreType = { - stack: {} as Stack, - refreshStack: async () => {}, -}; - -export const ExpandedStackStore = createContext(defaultValues); - -const ExpandedStackStoreProvider: React.FC = ({ children }) => { - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - - const [stack, setStack] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - const { namespace, stack_id } = useParams<{ - namespace: string; - stack_id: string; - }>(); - const { pushFiltered } = useRouting(); - - const getStack = async (props: { subscribed: boolean }) => { - setIsLoading(true); - api - .getStack( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace, - stack_id, - } - ) - .then((res) => { - if (props.subscribed) { - setStack(res.data); - } - }) - .catch(() => { - if (props.subscribed) { - setCurrentError("Couldn't find any stack with the given ID"); - pushFiltered("/stacks", []); - } - }) - .finally(() => { - if (props.subscribed) { - setIsLoading(false); - } - }); - }; - - useEffect(() => { - let isSubscribed = { subscribed: true }; - - getStack(isSubscribed); - - return () => { - isSubscribed.subscribed = false; - }; - }, [currentCluster, currentProject, namespace, stack_id]); - - if (isLoading) { - return ( - - - - ); - } - - return ( - { - await getStack({ subscribed: true }); - }, - }} - > - {children} - - ); -}; - -export default ExpandedStackStoreProvider; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx deleted file mode 100644 index 87f2da5975..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import Loading from "components/Loading"; -import React, { useContext, useRef, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { readableDate } from "shared/string_utils"; -import styled from "styled-components"; -import { FullStackRevision, Stack, StackRevision } from "../types"; - -type RevisionListProps = { - revisions: StackRevision[]; - currentRevision: StackRevision; - latestRevision: StackRevision; - stackNamespace: string; - stackId: string; - onRevisionClick: (revision: FullStackRevision) => void; - onRollback: () => void; -}; - -const _RevisionList = ({ - revisions, - currentRevision, - latestRevision, - stackNamespace, - stackId, - onRevisionClick, - onRollback, -}: RevisionListProps) => { - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - const [isLoading, setIsLoading] = useState(false); - const [isExpanded, setIsExpanded] = useState(false); - - const revisionCache = useRef<{ [id: number]: FullStackRevision }>({}); - - const handleRevisionPreview = (revision: StackRevision) => { - setIsLoading(true); - - if (revisionCache.current[revision.id]) { - onRevisionClick(revisionCache.current[revision.id]); - setIsLoading(false); - return; - } - - api - .getStackRevision( - "", - {}, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: stackNamespace, - revision_id: revision.id, - stack_id: stackId, - } - ) - .then((res) => { - const newRevision = res.data; - revisionCache.current = { - ...revisionCache.current, - [newRevision.id]: newRevision, - }; - onRevisionClick(newRevision); - }) - .catch((err) => { - setCurrentError(err); - }) - .finally(() => { - setIsLoading(false); - }); - }; - - const handleRevisionRollback = (revision: StackRevision) => { - setIsLoading(true); - - api - .rollbackStack( - "", - { - target_revision: revision.id, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: stackNamespace, - stack_id: stackId, - } - ) - .then(() => { - onRollback(); - }) - .catch((err) => { - setCurrentError(err); - }) - .finally(() => { - setIsLoading(false); - }); - }; - - const revisionList = () => { - if (revisions.length === 0) { - return
No revisions
; - } - - return revisions.map((revision, i) => { - let isCurrent = latestRevision.id === revision.id; - return ( -
handleRevisionPreview(revision)} - selected={currentRevision.id === revision.id} - > - - - - - - ); - }); - }; - - return ( - <> - - {isLoading ? ( - - - - ) : null} - setIsExpanded((prev) => !prev)} - > - - {currentRevision.id === latestRevision.id - ? `Current version` - : `Previewing revision (not deployed)`}{" "} - - No. {currentRevision.id} - arrow_drop_down - - - - - - - - - - - - {revisionList()} - - - - - - ); -}; - -export default _RevisionList; - -const Revision = styled.div` - color: #ffffff; - margin-left: 5px; -`; - -const StyledRevisionSection = styled.div` - display: flex; - flex-direction: column; - position: relative; - width: 100%; - max-height: ${(props: { showRevisions: boolean }) => - props.showRevisions ? "255px" : "40px"}; - background: #ffffff11; - margin: 25px 0px 18px; - overflow: hidden; - border-radius: 8px; - 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 RevisionHeader = styled.div` - color: ${(props: { showRevisions: boolean; isCurrent: boolean }) => - props.isCurrent ? "#ffffff66" : "#f5cb42"}; - display: flex; - justify-content: space-between; - align-items: center; - min-height: 40px; - font-size: 13px; - width: 100%; - padding-left: 15px; - cursor: pointer; - background: ${(props: { showRevisions: boolean; isCurrent: boolean }) => - props.showRevisions ? "#ffffff11" : ""}; - :hover { - background: #ffffff18; - > div > i { - background: #ffffff22; - } - } - - > div > i { - margin-left: 12px; - font-size: 20px; - cursor: pointer; - border-radius: 20px; - background: ${(props: { showRevisions: boolean; isCurrent: boolean }) => - props.showRevisions ? "#ffffff18" : ""}; - transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) => - props.showRevisions ? "rotate(180deg)" : ""}; - } -`; - -const RevisionPreview = styled.div` - display: flex; - align-items: center; -`; - -const TableWrapper = styled.div` - padding-bottom: 20px; - overflow-y: auto; -`; - -const RevisionsTable = styled.table` - width: 100%; - margin-top: 5px; - padding-left: 32px; - padding-bottom: 20px; - min-width: 500px; - border-collapse: collapse; -`; -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 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 LoadingOverlay = styled.div` - background: #43454b90; - width: 100%; - height: 100%; - position: absolute; -`; - -const RevisionStatusWrapper = styled.span<{ status: StackRevision["status"] }>` - text-transform: capitalize; - color: ${(props) => { - if (props.status === "deployed") { - return "#00b300"; - } - if (props.status === "failed") { - return "#ff0000"; - } - return "#ffffff"; - }}; - font-weight: 500; - font-size: 13px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx deleted file mode 100644 index 43e0eec33a..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import SaveButton from "components/SaveButton"; -import React, { useContext, useReducer, useRef, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import styled from "styled-components"; -import { FullStackRevision, SourceConfig } from "../types"; -import SourceEditorDocker from "./components/SourceEditorDocker"; - -const _SourceConfig = ({ - namespace, - revision, - readOnly, - onSourceConfigUpdate, -}: { - namespace: string; - revision: FullStackRevision; - readOnly: boolean; - onSourceConfigUpdate: () => void; -}) => { - const { currentProject, currentCluster, setCurrentError } = useContext( - Context - ); - const [sourceConfigArrayCopy, setSourceConfigArrayCopy] = useState< - SourceConfig[] - >(() => revision.source_configs); - const [buttonStatus, setButtonStatus] = useState(""); - - const handleChange = (sourceConfig: SourceConfig) => { - const newSourceConfigArray = [...sourceConfigArrayCopy]; - const index = newSourceConfigArray.findIndex( - (sc) => sc.id === sourceConfig.id - ); - - newSourceConfigArray[index] = { - ...sourceConfig, - display_name: sourceConfig.display_name || sourceConfig.name, - }; - - setSourceConfigArrayCopy(newSourceConfigArray); - }; - - const handleSave = () => { - setButtonStatus("loading"); - api - .updateStackSourceConfig( - "", - { - source_configs: sourceConfigArrayCopy, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - namespace: namespace, - stack_id: revision.stack_id, - } - ) - .then(() => { - setButtonStatus("successful"); - onSourceConfigUpdate(); - }) - .catch((err) => { - setButtonStatus("Something went wrong"); - setCurrentError(err); - }); - }; - - return ( - - {revision.source_configs.map((sourceConfig) => { - return ( - - ); - })} - {readOnly ? null : ( - - - - )} - - ); -}; - -export default _SourceConfig; - -const SourceConfigStyles = { - Wrapper: styled.div` - margin-top: 30px; - position: relative; - `, - ItemContainer: styled.div` - background: #ffffff11; - border-radius: 8px; - padding: 30px 35px 35px; - `, - ItemTitle: styled.div` - font-size: 16px; - font-weight: 500; - - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - > span { - overflow-x: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - `, - TooltipItem: styled.div` - font-size: 14px; - `, - SaveButtonRow: styled.div` - margin-top: 15px; - display: flex; - justify-content: flex-end; - `, - SaveButton: styled(SaveButton)` - z-index: unset; - `, -}; - -const SourceConfigItem = ({ - sourceConfig, - handleChange, - disabled, -}: { - sourceConfig: SourceConfig; - handleChange: (sourceConfig: SourceConfig) => void; - disabled: boolean; -}) => { - const [editNameMode, toggleEditNameMode] = useReducer((prev) => !prev, false); - const prevName = useRef(sourceConfig.display_name || sourceConfig.name); - const [name, setName] = useState( - sourceConfig.display_name || sourceConfig.name - ); - - const handleNameChange = (newName: string) => { - setName(newName); - handleChange({ ...sourceConfig, display_name: newName }); - }; - - const handleNameChangeCancel = () => { - setName(prevName.current); - handleChange({ ...sourceConfig, display_name: prevName.current }); - toggleEditNameMode(); - }; - - return ( - - {editNameMode && !disabled ? ( - <> - - handleNameChange(e.target.value)} - type="text" - disabled={disabled} - /> - - close - - - - ) : ( - - {name} - - - edit - - - )} - - - - ); -}; - -const EditButton = styled.button` - outline: none; - cursor: pointer; - color: white; - border: 1px solid rgba(255, 255, 255, 0.333); - background: rgba(255, 255, 255, 0.067); - height: 35px; - width: 35px; - border-radius: 24px; - display: flex; - align-items: center; - justify-content: center; - > i { - font-size: 20px; - } -`; - -const PlainTextInput = styled.input` - outline: none; - border: 1px solid #ffffff55; - border-radius: 3px; - font-size: 13px; - background: #ffffff11; - width: 100%; - color: white; - padding: 5px 10px; - height: 35px; -`; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/EnvGroups.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/EnvGroups.tsx deleted file mode 100644 index 451cd2303f..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/EnvGroups.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { Card } from "../../launch/components/styles"; -import { Stack } from "../../types"; -import sliders from "assets/sliders.svg"; -import DynamicLink from "components/DynamicLink"; -import Placeholder from "components/OldPlaceholder"; -import Loading from "components/Loading"; -import { useRouteMatch } from "react-router"; - -type PopulatedEnvGroup = { - applications: string[]; - created_at: string; - meta_version: number; - name: string; - namespace: string; - variables: Record; - version: number; -}; - -const EnvGroups = ({ stack }: { stack: Stack }) => { - const { currentProject, currentCluster } = useContext(Context); - const [isLoading, setIsLoading] = useState(true); - const [envGroups, setEnvGroups] = useState([]); - const { url } = useRouteMatch(); - - const getEnvGroups = async () => { - const stackEnvGroups = stack.latest_revision.env_groups; - return Promise.all( - stackEnvGroups.map((envGroup) => - api - .getEnvGroup( - "", - {}, - { - cluster_id: currentCluster.id, - id: currentProject.id, - name: envGroup.name, - namespace: stack.namespace, - version: envGroup.env_group_version, - } - ) - .then((res) => res.data) - ) - ); - }; - - useEffect(() => { - let isSubscribed = true; - getEnvGroups().then((envGroups) => { - if (!isSubscribed) { - return; - } - setEnvGroups(envGroups); - setIsLoading(false); - }); - - return () => { - isSubscribed = false; - }; - }, [stack]); - - if (isLoading) { - return ( - - - - ); - } - - if (envGroups.length === 0) { - return ( - -
-

No environment groups found for this stack

-
-
- ); - } - - return ( - <> - - {envGroups.map((envGroup) => { - return ( - - - - {envGroup.name} - - - - - launch - - - - ); - })} - - - ); -}; - -export default EnvGroups; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx deleted file mode 100644 index 85c4cbb4d0..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import Loading from "components/Loading"; -import React, { useRef, useState } from "react"; -import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter"; -import styled from "styled-components"; - -export type SelectProps = { - value: T; - options: T[]; - accessor: (option: T) => string | React.ReactNode; - onChange: (value: T) => void; - isOptionEqualToValue?: (option: T, value: T) => boolean; - label: string; - isLoading?: boolean; - dropdown?: { - maxH?: string; - width?: string; - label?: string; - option?: { - height?: string; - }; - }; - placeholder: string; - className?: string; - readOnly?: boolean; -}; - -const Select = ({ - value, - options, - accessor, - onChange, - isOptionEqualToValue, - label, - isLoading, - placeholder, - dropdown, - className, - readOnly, -}: SelectProps) => { - const wrapperRef = useRef(); - const [expanded, setExpanded] = useState(false); - - useOutsideAlerter(wrapperRef, () => { - setExpanded(false); - }); - - const handleOptionClick = (value: T) => { - setExpanded(false); - onChange(value); - }; - - const getLabel = () => { - if (label) { - return {label} ; - } - return null; - }; - - if (isLoading) { - return ( -
- {getLabel()} - - - - - - - -
- ); - } - - const isSelected = (option: T, value: T) => { - if (!value) { - return false; - } - - if (isOptionEqualToValue) { - return isOptionEqualToValue(option, value); - } - }; - - return ( -
- {getLabel()} - - setExpanded(!expanded)} - expanded={!readOnly && expanded} - readOnly={readOnly} - > - - {value ? accessor(value) : placeholder} - - {readOnly ? null : arrow_drop_down} - - {expanded && !readOnly ? ( - - {dropdown?.label && ( - - {dropdown?.label} - - )} - {options.length > 0 ? ( - <> - {options.map((option, i) => ( - !readOnly && handleOptionClick(option)} - lastItem={i === options.length - 1} - selected={isSelected(option, value)} - height={dropdown?.option?.height} - > - {accessor(option)} - - ))} - - ) : ( - - No options available - - )} - - ) : null} - -
- ); -}; - -export default Select; - -export const SelectStyles = { - Wrapper: styled.div` - position: relative; - `, - Label: styled.div` - color: #ffffff; - margin-bottom: 10px; - margin-top: 20px; - font-size: 13px; - `, - - Selector: styled.div<{ expanded: boolean; readOnly: boolean }>` - height: 35px; - border: 1px solid #ffffff55; - font-size: 13px; - color: ${(props) => (props.readOnly ? "#ffffff44" : "")}; - padding: 5px 10px; - padding-left: 15px; - border-radius: 3px; - display: flex; - justify-content: space-between; - align-items: center; - cursor: ${(props) => (props.readOnly ? "not-allowed" : "pointer")}; - background: ${(props) => { - if (props.expanded) { - return "#ffffff33"; - } - return "#ffffff11"; - }}; - - :hover { - background: ${(props) => { - if (props.readOnly) { - return "#ffffff11"; - } else if (props.expanded) { - return "#ffffff33"; - } - return "#ffffff22"; - }}; - } - - > i { - font-size: 20px; - transform: ${(props) => (props.expanded ? "rotate(180deg)" : "")}; - } - `, - - Loading: styled.div` - width: 100%; - `, - - CurrentValue: styled.div` - display: flex; - align-items: center; - width: 85%; - - > span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - z-index: 0; - } - `, - - Dropdown: { - Wrapper: styled.div<{ width: string; maxH?: string }>` - background: #26282f; - width: ${(props) => props.width || "100%"}; - max-height: ${(props) => props.maxH || "300px"}; - border-radius: 3px; - z-index: 999; - overflow-y: auto; - margin-bottom: 20px; - box-shadow: 0 8px 20px 0px #00000088; - position: absolute; - `, - Option: styled.div<{ - selected: boolean; - lastItem: boolean; - height?: string; - }>` - width: 100%; - border-top: 1px solid #00000000; - border-bottom: 1px solid - ${(props) => (props.lastItem ? "#ffffff00" : "#ffffff15")}; - height: ${(props) => props.height || "37px"}; - font-size: 13px; - align-items: center; - display: flex; - align-items: center; - padding-left: 15px; - cursor: pointer; - padding-right: 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - background: ${(props) => (props.selected ? "#ffffff11" : "")}; - - :hover { - background: #ffffff22; - } - `, - Label: styled.div` - font-size: 13px; - color: #ffffff44; - font-weight: 500; - margin: 10px 13px; - `, - NoOptions: styled.div` - font-size: 13px; - color: #ffffff44; - font-weight: 500; - margin: 10px 13px; - :not(:first-child) { - border-top: 1px solid #ffffff15; - } - `, - }, -}; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx deleted file mode 100644 index 5fbd01c6f1..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import Heading from "components/form-components/Heading"; -import Helper from "components/form-components/Helper"; -import InputRow from "components/form-components/InputRow"; -import React, { useContext, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import styled from "styled-components"; -import { SubmitButton } from "../../launch/components/styles"; -import { Stack } from "../../types"; - -const Settings = ({ - stack, - onDelete, - onUpdate, -}: { - stack: Stack; - onDelete: () => void; - onUpdate: () => Promise; -}) => { - const { - currentCluster, - currentProject, - setCurrentOverlay, - setCurrentError, - } = useContext(Context); - const [stackName, setStackName] = useState(stack.name); - const [buttonStatus, setButtonStatus] = useState(""); - - const handleDelete = () => { - setCurrentOverlay({ - message: `Are you sure you want to delete ${stackName}?`, - onYes: () => { - onDelete(); - setCurrentOverlay(null); - }, - onNo: () => setCurrentOverlay(null), - }); - }; - - const handleStackNameChange = async () => { - setButtonStatus("loading"); - try { - await api.updateStack( - "", - { - name: stackName, - }, - { - project_id: currentProject.id, - cluster_id: currentCluster.id, - stack_id: stack.id, - namespace: stack.namespace, - } - ); - await onUpdate(); - setButtonStatus("successful"); - } catch (err) { - setCurrentError(err); - setButtonStatus("Couldn't update the stack name. Try again later."); - } - }; - - return ( - - - Update Stack name - - - - - Additional Settings - - - - - ); -}; - -export default Settings; - -const SaveButton = styled(SubmitButton)` - justify-content: flex-start; -`; - -const Wrapper = styled.div` - width: 100%; - padding-bottom: 65px; - height: 100%; -`; - -const StyledSettingsSection = styled.div` - width: 100%; - background: #ffffff11; - padding: 0 35px; - padding-bottom: 15px; - position: relative; - border-radius: 8px; - overflow: auto; - height: calc(100% - 55px); -`; - -const Button = styled.button` - height: 35px; - font-size: 13px; - margin-top: 20px; - 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%)" : "")}; - } -`; diff --git a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/SourceEditorDocker.tsx b/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/SourceEditorDocker.tsx deleted file mode 100644 index 3ed4ff7e24..0000000000 --- a/dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/SourceEditorDocker.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import SelectRow from "components/form-components/SelectRow"; -import SearchSelector from "components/SearchSelector"; -import Selector from "components/Selector"; -import React, { useContext, useEffect, useMemo, useState } from "react"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter"; -import styled from "styled-components"; -import { proxy, useSnapshot } from "valtio"; -import { SourceConfig } from "../../types"; -import Select from "./Select"; - -const SourceEditorDocker = ({ - sourceConfig, - onChange, - readOnly = false, -}: { - readOnly: boolean; - sourceConfig: SourceConfig; - onChange: (sourceConfig: SourceConfig) => void; -}) => { - const [registry, setRegistry] = useState(null); - const [image, setImage] = useState( - () => sourceConfig.image_repo_uri - ); - const [tag, setTag] = useState(() => sourceConfig.image_tag); - - const imageName = useMemo(() => { - if (!registry) { - return ""; - } - - if (!image) { - return ""; - } - - return image.replace(registry.url + "/", ""); - }, [image, registry]); - - useEffect(() => { - if (sourceConfig.image_repo_uri) { - setImage(sourceConfig.image_repo_uri); - setTag(sourceConfig.image_tag); - } - }, [sourceConfig]); - - useEffect(() => { - const newSourceConfig: SourceConfig = { - ...sourceConfig, - image_repo_uri: image, - image_tag: tag, - }; - - onChange(newSourceConfig); - }, [image, tag]); - - return ( - <> - - <_DockerRepositorySelector - currentImageUrl={sourceConfig.image_repo_uri} - value={registry} - onChange={setRegistry} - readOnly={readOnly} - /> - - {registry && ( - - <_ImageSelector - registry={registry} - value={image} - onChange={setImage} - readOnly={readOnly} - /> - - {registry && imageName && ( - <_TagSelector - registry={registry} - imageName={imageName} - value={tag} - onChange={setTag} - readOnly={readOnly} - /> - )} - - )} - - ); -}; - -type DockerRegistry = { - id: number; - project_id: number; - name: string; - url: string; - service: string; - infra_id: number; - aws_integration_id: number; -}; - -const _DockerRepositorySelector = ({ - currentImageUrl, - value, - onChange, - readOnly, -}: { - currentImageUrl: string; - value: DockerRegistry; - onChange: (newRegistry: DockerRegistry) => void; - readOnly: boolean; -}) => { - const { currentProject } = useContext(Context); - - const [registries, setRegistries] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - api - .getProjectRegistries( - "", - {}, - { - id: currentProject.id, - } - ) - .then(({ data }) => { - setRegistries(data); - if (!value) { - const currentRegistry = data.find((r) => - currentImageUrl.includes(r.url) - ); - onChange(currentRegistry); - } - setIsLoading(false); - }); - }, [currentImageUrl]); - - const handleChange = (newRegistry: DockerRegistry) => { - onChange(newRegistry); - }; - - return ( - <> - image.uri)} - accessor={displayName} - label="Image" - placeholder="Select an image" - onChange={handleChange} - isOptionEqualToValue={(a, b) => a === b} - readOnly={readOnly} - isLoading={isLoading} - dropdown={{ - maxH: "200px", - }} - /> - ); -}; - -type DockerImageTag = { - digest: string; - tag: string; - manifest: string; - repository_name: string; - pushed_at: string; -}; - -const _TagSelector = ({ - registry, - imageName, - value, - onChange, - readOnly, -}: { - registry: DockerRegistry; - imageName: string; - value: string; - onChange: (newTag: string) => void; - readOnly: boolean; -}) => { - const { currentProject } = useContext(Context); - const [imageTags, setImageTags] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - setIsLoading(true); - api - .getImageTags( - "", - {}, - { - project_id: currentProject.id, - registry_id: registry?.id, - repo_name: imageName, - } - ) - .then(({ data }) => { - if (!data?.length) { - setImageTags([]); - onChange(""); - setIsLoading(false); - return; - } - - const sortedTags = data.sort((a, b) => { - const aDate = new Date(a.pushed_at); - const bDate = new Date(b.pushed_at); - return bDate.getTime() - aDate.getTime(); - }); - setImageTags(sortedTags.map((tag) => tag.tag)); - - if (sortedTags.map((tag) => tag.tag).includes(value)) { - onChange(value); - } else { - onChange(sortedTags[0].tag); - } - - setIsLoading(false); - }); - }, [registry, imageName]); - - const handleChange = (tag: string) => { - onChange(tag); - }; - - return ( -
{revision.id}{readableDate(revision.created_at)} - - {revision.status} - - - { - e.stopPropagation(); - handleRevisionRollback(revision); - }} - > - {isCurrent ? "Current" : "Revert"} - -
Revision No.TimestampStatusRollback